Pluggable backends for SFX / music / ambience asset generation.
The pipeline generates every non-silence sound effect through the ElevenLabs
Sound Effects API. This module keeps a thin :class:SfxBackend adapter so the
shared generation path in :mod:xil_pipeline.sfx_common does not talk to the
ElevenLabs client directly:
- :class:
ElevenLabsSfxBackend — wraps client.text_to_sound_effects.convert
with stream-to-temp, atomic rename, and 429 / 5xx / network retry handling.
Two local diffusion backends (AudioLDM 2, Stable Audio Open) were removed in #62
after both trials produced unusable audio. The adapter and factory survive them
deliberately — they are the seam a future backend plugs into, and collapsing them
into a direct ElevenLabs call would have to be undone to add one.
The contract is::
backend.generate_to(out_path, prompt, duration_seconds, prompt_influence)
backend.close()
Use :func:make_sfx_backend to construct the right backend from a CLI flag.
logger
module-attribute
logger = get_logger(__name__)
SfxBackend
Bases: Protocol
Minimal contract for a sound-effect generation backend.
Source code in src/xil_pipeline/sfx_backends.py
| @runtime_checkable
class SfxBackend(Protocol):
"""Minimal contract for a sound-effect generation backend."""
name: str
def generate_to(
self,
out_path: str,
prompt: str,
duration_seconds: float,
prompt_influence: float,
) -> None:
"""Generate audio for *prompt* and write it to *out_path*."""
...
def close(self) -> None:
"""Release any resources (subprocess, sockets). No-op for stateless backends."""
...
|
generate_to
generate_to(out_path: str, prompt: str, duration_seconds: float, prompt_influence: float) -> None
Generate audio for prompt and write it to out_path.
Source code in src/xil_pipeline/sfx_backends.py
| def generate_to(
self,
out_path: str,
prompt: str,
duration_seconds: float,
prompt_influence: float,
) -> None:
"""Generate audio for *prompt* and write it to *out_path*."""
...
|
close
Release any resources (subprocess, sockets). No-op for stateless backends.
Source code in src/xil_pipeline/sfx_backends.py
| def close(self) -> None:
"""Release any resources (subprocess, sockets). No-op for stateless backends."""
...
|
ElevenLabsSfxBackend
SFX backend backed by the ElevenLabs Sound Effects API.
Wraps client.text_to_sound_effects.convert with the streaming
download, atomic temp-file rename, and retry behaviour that previously
lived inline in :func:xil_pipeline.sfx_common.ensure_shared_sfx.
Retries 429 (rate limit), 5xx (server error), and network transport
errors up to five times with linear backoff (10s, 20s, …).
Source code in src/xil_pipeline/sfx_backends.py
| class ElevenLabsSfxBackend:
"""SFX backend backed by the ElevenLabs Sound Effects API.
Wraps ``client.text_to_sound_effects.convert`` with the streaming
download, atomic temp-file rename, and retry behaviour that previously
lived inline in :func:`xil_pipeline.sfx_common.ensure_shared_sfx`.
Retries 429 (rate limit), 5xx (server error), and network transport
errors up to five times with linear backoff (10s, 20s, …).
"""
name = "elevenlabs"
def __init__(self, client) -> None:
self._client = client
def generate_to(
self,
out_path: str,
prompt: str,
duration_seconds: float,
prompt_influence: float,
) -> None:
if self._client is None:
raise ValueError(
"ElevenLabs client is required to generate SFX "
"(set ELEVENLABS_API_KEY)."
)
logger.info(" [api] text-to-sound-effects → %r (%.1fs)", prompt, duration_seconds)
tmp_path = None
try:
max_retries, delay = 5, 10
for attempt in range(1, max_retries + 1):
try:
audio_stream = self._client.text_to_sound_effects.convert(
text=prompt,
duration_seconds=duration_seconds,
prompt_influence=prompt_influence,
)
tmp_fd, tmp_path = tempfile.mkstemp(
dir=os.path.dirname(out_path) or ".", suffix=".tmp"
)
with os.fdopen(tmp_fd, "wb") as f:
for chunk in audio_stream:
if chunk:
f.write(chunk)
os.replace(tmp_path, out_path)
tmp_path = None
logger.info(" [api] saved %s", os.path.basename(out_path))
return
except (ApiError, httpx.TransportError) as exc:
if tmp_path is not None:
with contextlib.suppress(FileNotFoundError):
os.unlink(tmp_path)
tmp_path = None
is_rate_limit = isinstance(exc, ApiError) and exc.status_code == 429
is_server_error = (
isinstance(exc, ApiError)
and exc.status_code is not None
and exc.status_code >= 500
)
is_network_error = isinstance(exc, httpx.TransportError)
is_retryable = is_rate_limit or is_server_error or is_network_error
if is_retryable and attempt < max_retries:
wait = delay * attempt
if is_rate_limit:
reason = "429 rate limited"
elif is_server_error:
reason = f"{exc.status_code} server error"
else:
reason = f"network error ({type(exc).__name__})"
logger.warning(
"[%s] — retrying in %ds (attempt %d/%d)",
reason, wait, attempt, max_retries,
)
time.sleep(wait)
else:
raise
finally:
if tmp_path is not None:
with contextlib.suppress(FileNotFoundError):
os.unlink(tmp_path)
def close(self) -> None:
# The ElevenLabs client is owned by the caller; nothing to release here.
return
|
name
class-attribute
instance-attribute
__init__
Source code in src/xil_pipeline/sfx_backends.py
| def __init__(self, client) -> None:
self._client = client
|
generate_to
generate_to(out_path: str, prompt: str, duration_seconds: float, prompt_influence: float) -> None
Source code in src/xil_pipeline/sfx_backends.py
| def generate_to(
self,
out_path: str,
prompt: str,
duration_seconds: float,
prompt_influence: float,
) -> None:
if self._client is None:
raise ValueError(
"ElevenLabs client is required to generate SFX "
"(set ELEVENLABS_API_KEY)."
)
logger.info(" [api] text-to-sound-effects → %r (%.1fs)", prompt, duration_seconds)
tmp_path = None
try:
max_retries, delay = 5, 10
for attempt in range(1, max_retries + 1):
try:
audio_stream = self._client.text_to_sound_effects.convert(
text=prompt,
duration_seconds=duration_seconds,
prompt_influence=prompt_influence,
)
tmp_fd, tmp_path = tempfile.mkstemp(
dir=os.path.dirname(out_path) or ".", suffix=".tmp"
)
with os.fdopen(tmp_fd, "wb") as f:
for chunk in audio_stream:
if chunk:
f.write(chunk)
os.replace(tmp_path, out_path)
tmp_path = None
logger.info(" [api] saved %s", os.path.basename(out_path))
return
except (ApiError, httpx.TransportError) as exc:
if tmp_path is not None:
with contextlib.suppress(FileNotFoundError):
os.unlink(tmp_path)
tmp_path = None
is_rate_limit = isinstance(exc, ApiError) and exc.status_code == 429
is_server_error = (
isinstance(exc, ApiError)
and exc.status_code is not None
and exc.status_code >= 500
)
is_network_error = isinstance(exc, httpx.TransportError)
is_retryable = is_rate_limit or is_server_error or is_network_error
if is_retryable and attempt < max_retries:
wait = delay * attempt
if is_rate_limit:
reason = "429 rate limited"
elif is_server_error:
reason = f"{exc.status_code} server error"
else:
reason = f"network error ({type(exc).__name__})"
logger.warning(
"[%s] — retrying in %ds (attempt %d/%d)",
reason, wait, attempt, max_retries,
)
time.sleep(wait)
else:
raise
finally:
if tmp_path is not None:
with contextlib.suppress(FileNotFoundError):
os.unlink(tmp_path)
|
close
Source code in src/xil_pipeline/sfx_backends.py
| def close(self) -> None:
# The ElevenLabs client is owned by the caller; nothing to release here.
return
|
MMAudioSfxBackend
Local SFX backend backed by MMAudio (text-to-audio mode).
The model weights are CC BY-NC 4.0 — non-commercial use only. The code
is MIT, the checkpoints are not. Audio produced here must not end up in a
monetised episode. Construction therefore requires an explicit
accept_noncommercial=True, every session logs the constraint, and
generated assets carry it in their ID3 comment (:attr:asset_comment) on
top of the .mmaudio filename infix that
:func:xil_pipeline.sfx_common.shared_sfx_path already applies. Between
the two, an asset stays identifiable even if it is renamed.
Duration handling is the interesting part. MMAudio is trained at 8 seconds
and the project warns that a large deviation degrades quality, but SFX cues
here are typically shorter. So generation always runs at the native
duration and the result is trimmed afterwards to the caller's
duration_seconds. The trim cannot be left to the mixer: for a
prompt-generated cue duration_seconds is the requested generation
length and there is no mix-time clip (that only applies to source=
cues), so an untrimmed asset would simply play long.
Source code in src/xil_pipeline/sfx_backends.py
| class MMAudioSfxBackend:
"""Local SFX backend backed by MMAudio (text-to-audio mode).
**The model weights are CC BY-NC 4.0 — non-commercial use only.** The code
is MIT, the checkpoints are not. Audio produced here must not end up in a
monetised episode. Construction therefore requires an explicit
``accept_noncommercial=True``, every session logs the constraint, and
generated assets carry it in their ID3 comment (:attr:`asset_comment`) on
top of the ``.mmaudio`` filename infix that
:func:`xil_pipeline.sfx_common.shared_sfx_path` already applies. Between
the two, an asset stays identifiable even if it is renamed.
Duration handling is the interesting part. MMAudio is trained at 8 seconds
and the project warns that a large deviation degrades quality, but SFX cues
here are typically shorter. So generation always runs at the native
duration and the result is **trimmed afterwards** to the caller's
``duration_seconds``. The trim cannot be left to the mixer: for a
prompt-generated cue ``duration_seconds`` is the *requested generation
length* and there is no mix-time clip (that only applies to ``source=``
cues), so an untrimmed asset would simply play long.
"""
name = "mmaudio"
#: Written into every generated asset's ID3 comment.
asset_comment = (
"Generated by MMAudio (hkchengrex/MMAudio). Model weights are "
"CC BY-NC 4.0 — NON-COMMERCIAL USE ONLY."
)
#: MMAudio's training duration. Generating here and trimming beats asking
#: the model for a short clip directly.
NATIVE_DURATION_S = 8.0
def __init__(self, client: _MMAudioClient, *,
accept_noncommercial: bool = False,
native_duration: float = NATIVE_DURATION_S) -> None:
if not accept_noncommercial:
raise ValueError(
"MMAudio weights are CC BY-NC 4.0 (non-commercial only). Pass "
"--mmaudio-accept-noncommercial to acknowledge that generated "
"audio must not be used in a monetised production."
)
self._client = client
self._native_duration = native_duration
logger.warning(
"MMAudio weights are CC BY-NC 4.0 — NON-COMMERCIAL USE ONLY. "
"Generated assets are tagged .mmaudio and carry the notice in ID3."
)
def generate_to(
self,
out_path: str,
prompt: str,
duration_seconds: float,
prompt_influence: float,
) -> None:
"""Generate at MMAudio's native duration, then trim to *duration_seconds*.
``prompt_influence`` has no direct MMAudio analogue; it is not silently
dropped — the client's ``cfg_strength`` plays the equivalent role and is
set from ``--mmaudio-cfg``.
"""
target = max(0.0, float(duration_seconds or 0.0))
gen_seconds = max(self._native_duration, target)
logger.info(
" [mmaudio] %r — generating %.1fs (native), trimming to %.1fs",
prompt, gen_seconds, target or gen_seconds,
)
self._client.generate(prompt, out_path, gen_seconds)
if target and target < gen_seconds:
_trim_audio_file(out_path, target)
def close(self) -> None:
self._client.close()
|
name
class-attribute
instance-attribute
asset_comment = 'Generated by MMAudio (hkchengrex/MMAudio). Model weights are CC BY-NC 4.0 — NON-COMMERCIAL USE ONLY.'
NATIVE_DURATION_S
class-attribute
instance-attribute
__init__
__init__(client: _MMAudioClient, *, accept_noncommercial: bool = False, native_duration: float = NATIVE_DURATION_S) -> None
Source code in src/xil_pipeline/sfx_backends.py
| def __init__(self, client: _MMAudioClient, *,
accept_noncommercial: bool = False,
native_duration: float = NATIVE_DURATION_S) -> None:
if not accept_noncommercial:
raise ValueError(
"MMAudio weights are CC BY-NC 4.0 (non-commercial only). Pass "
"--mmaudio-accept-noncommercial to acknowledge that generated "
"audio must not be used in a monetised production."
)
self._client = client
self._native_duration = native_duration
logger.warning(
"MMAudio weights are CC BY-NC 4.0 — NON-COMMERCIAL USE ONLY. "
"Generated assets are tagged .mmaudio and carry the notice in ID3."
)
|
generate_to
generate_to(out_path: str, prompt: str, duration_seconds: float, prompt_influence: float) -> None
Generate at MMAudio's native duration, then trim to duration_seconds.
prompt_influence has no direct MMAudio analogue; it is not silently
dropped — the client's cfg_strength plays the equivalent role and is
set from --mmaudio-cfg.
Source code in src/xil_pipeline/sfx_backends.py
| def generate_to(
self,
out_path: str,
prompt: str,
duration_seconds: float,
prompt_influence: float,
) -> None:
"""Generate at MMAudio's native duration, then trim to *duration_seconds*.
``prompt_influence`` has no direct MMAudio analogue; it is not silently
dropped — the client's ``cfg_strength`` plays the equivalent role and is
set from ``--mmaudio-cfg``.
"""
target = max(0.0, float(duration_seconds or 0.0))
gen_seconds = max(self._native_duration, target)
logger.info(
" [mmaudio] %r — generating %.1fs (native), trimming to %.1fs",
prompt, gen_seconds, target or gen_seconds,
)
self._client.generate(prompt, out_path, gen_seconds)
if target and target < gen_seconds:
_trim_audio_file(out_path, target)
|
close
Source code in src/xil_pipeline/sfx_backends.py
| def close(self) -> None:
self._client.close()
|
make_sfx_backend
make_sfx_backend(name: str, client: ElevenLabs | None = None, *, mmaudio_python: str | None = None, device: str = 'cuda', mmaudio_cfg: float = 4.5, mmaudio_steps: int = 25, mmaudio_negative_prompt: str = '', mmaudio_seed: int | None = None, mmaudio_duration: float = MMAudioSfxBackend.NATIVE_DURATION_S, accept_noncommercial: bool = False) -> SfxBackend
Construct an :class:SfxBackend for the given backend name.
"elevenlabs" calls the Sound Effects API; "mmaudio" runs MMAudio
locally in venv-mmaudio (CC BY-NC 4.0 weights — non-commercial only,
hence accept_noncommercial).
The factory is kept rather than inlined because it is the seam a backend
plugs into — the audioldm2/stableaudio trials were removed through it in #62
and MMAudio was added through it in #64 without touching call sites.
Parameters:
-
name
(str)
–
"elevenlabs" or "mmaudio".
-
client
(ElevenLabs | None, default:
None
)
–
ElevenLabs client (used only for "elevenlabs").
-
mmaudio_python
(str | None, default:
None
)
–
Explicit venv-mmaudio interpreter; auto-detected when None.
-
device
(str, default:
'cuda'
)
–
"cuda" (default) or "cpu" for the local backend.
-
mmaudio_cfg
(float, default:
4.5
)
–
Classifier-free guidance strength.
-
mmaudio_steps
(int, default:
25
)
–
Flow-matching sampling steps.
-
mmaudio_negative_prompt
(str, default:
''
)
–
Optional negative prompt.
-
mmaudio_seed
(int | None, default:
None
)
–
Reproducibility seed (None = nondeterministic).
-
mmaudio_duration
(float, default:
NATIVE_DURATION_S
)
–
Generation length before trimming (default: the 8 s
training duration).
-
accept_noncommercial
(bool, default:
False
)
–
Required acknowledgement of the CC BY-NC weights.
Returns:
Raises:
-
ValueError
–
For an unknown name, or for "mmaudio" without
accept_noncommercial — a stale script must fail loudly rather
than quietly producing audio that cannot be used commercially.
Source code in src/xil_pipeline/sfx_backends.py
| def make_sfx_backend(
name: str,
client: ElevenLabs | None = None,
*,
mmaudio_python: str | None = None,
device: str = "cuda",
mmaudio_cfg: float = 4.5,
mmaudio_steps: int = 25,
mmaudio_negative_prompt: str = "",
mmaudio_seed: int | None = None,
mmaudio_duration: float = MMAudioSfxBackend.NATIVE_DURATION_S,
accept_noncommercial: bool = False,
) -> SfxBackend:
"""Construct an :class:`SfxBackend` for the given backend *name*.
``"elevenlabs"`` calls the Sound Effects API; ``"mmaudio"`` runs MMAudio
locally in ``venv-mmaudio`` (**CC BY-NC 4.0 weights — non-commercial only**,
hence ``accept_noncommercial``).
The factory is kept rather than inlined because it is the seam a backend
plugs into — the audioldm2/stableaudio trials were removed through it in #62
and MMAudio was added through it in #64 without touching call sites.
Args:
name: ``"elevenlabs"`` or ``"mmaudio"``.
client: ElevenLabs client (used only for ``"elevenlabs"``).
mmaudio_python: Explicit venv-mmaudio interpreter; auto-detected when ``None``.
device: ``"cuda"`` (default) or ``"cpu"`` for the local backend.
mmaudio_cfg: Classifier-free guidance strength.
mmaudio_steps: Flow-matching sampling steps.
mmaudio_negative_prompt: Optional negative prompt.
mmaudio_seed: Reproducibility seed (``None`` = nondeterministic).
mmaudio_duration: Generation length before trimming (default: the 8 s
training duration).
accept_noncommercial: Required acknowledgement of the CC BY-NC weights.
Returns:
A ready-to-use backend instance.
Raises:
ValueError: For an unknown name, or for ``"mmaudio"`` without
``accept_noncommercial`` — a stale script must fail loudly rather
than quietly producing audio that cannot be used commercially.
"""
if name == "elevenlabs":
return ElevenLabsSfxBackend(client)
if name == "mmaudio":
# Check the licence acknowledgement BEFORE resolving the venv: someone
# who has not accepted the CC BY-NC terms should be told that, not sent
# off to install a venv they may then be unable to use.
if not accept_noncommercial:
raise ValueError(
"MMAudio weights are CC BY-NC 4.0 (non-commercial only). Pass "
"--mmaudio-accept-noncommercial to acknowledge that generated "
"audio must not be used in a monetised production."
)
worker = _MMAudioClient(
python_path=_find_mmaudio_python(mmaudio_python),
device=device,
guidance=mmaudio_cfg,
steps=mmaudio_steps,
negative_prompt=mmaudio_negative_prompt,
seed=mmaudio_seed,
)
return MMAudioSfxBackend(
worker,
accept_noncommercial=accept_noncommercial,
native_duration=mmaudio_duration,
)
raise ValueError(f"Unknown sfx backend: {name!r}")
|