NewGame's picture
Upload folder using huggingface_hub
a76e292 verified
Raw History Blame Contribute Delete
9.29 kB
"""Recipe-local Gradio launcher for ESPnet3 F5-TTS demos.
The generic launcher (``egs3/TEMPLATE/esp2_asr/src/app.py``) binds the demo
session's inference function straight to the Gradio components; the session
already turns the model's ``Audio`` into the ``(sample_rate, samples)`` pair
``gr.Audio`` plays. This launcher adds what a text-to-speech UI needs on top:
an untouched text box arrives as ``""``, which is passed on as "not given" so
the contract refuses it by name instead of synthesizing from an empty
transcript, and that refusal is shown in the UI rather than as a bare error.
"""
from __future__ import annotations
# ZeroGPU Spaces kill an app that declares no `@spaces.GPU` function and need
# `spaces` imported before any package that touches CUDA, so this import comes
# first. The package exists only on Hugging Face Spaces.
try:
import spaces # isort: skip
except ImportError: # local runs and plain CPU Spaces
spaces = None
import argparse # noqa: E402
import logging # noqa: E402
from pathlib import Path # noqa: E402
import gradio as gr # noqa: E402
from omegaconf import OmegaConf # noqa: E402
from espnet3.publication.demo.session import load_demo_session # noqa: E402
from espnet3.utils.logging_utils import configure_logging # noqa: E402
logger = logging.getLogger(__name__)
# Example values with one of these suffixes are files inside the packed demo.
_AUDIO_SUFFIXES = {".wav", ".flac", ".mp3", ".ogg"}
def _read_example_rows(demo_cfg, demo_dir: Path) -> list[list]:
"""Return ``ui.examples`` as plain rows, audio paths made absolute.
Args:
demo_cfg: The packed demo config.
demo_dir: The packed demo directory the paths are relative to.
Returns:
One list per example, in the order of ``ui.inputs``; empty when the
config has no ``ui.examples``.
Raises:
FileNotFoundError: If an example names a file that is not in the
packed demo, so a broken example is caught at launch, not on
click.
Example:
.. code-block:: python
_read_example_rows(cfg, Path("demo"))
# -> [["Hello.", "/abs/demo/examples/prompt.wav", "the prompt"]]
"""
ui_cfg = getattr(demo_cfg, "ui", None)
raw = ui_cfg.get("examples") if ui_cfg is not None else None
if not raw:
return []
rows = []
for row in OmegaConf.to_container(raw, resolve=True):
values = []
for value in row:
if isinstance(value, str) and Path(value).suffix.lower() in _AUDIO_SUFFIXES:
path = demo_dir / value
if not path.is_file():
raise FileNotFoundError(
f"ui.examples names {value!r}, which is not in the packed "
f"demo {demo_dir}; list it under pack.include"
)
value = str(path)
values.append(value)
rows.append(values)
return rows
def build_demo(
demo_dir: Path,
demo_config_path: Path | None = None,
):
"""Build the Gradio Blocks app for one packed F5-TTS demo.
Input and output components are read from the packed ``demo.yaml``. With
the template's ``conf/demo.yaml`` that is: target text, reference speech
and reference transcript in, synthesized speech out.
Args:
demo_dir: Packed demo directory, containing ``demo.yaml`` and the
model reference it points at.
demo_config_path: Optional demo config override. Defaults to
``demo_dir / "demo.yaml"``.
Returns:
gradio.Blocks: App with one component per input/output spec, bound
positionally to the session's inference function.
Raises:
FileNotFoundError: If ``ui.examples`` names an audio file that is not
in the packed demo, so a broken example fails at launch rather
than on click.
Example:
.. code-block:: python
app = build_demo(Path("exp/training/demo"))
app.launch()
Note:
Reference speech is passed to the model as Gradio delivers it, a
``(rate, samples)`` pair; ``Inference`` resamples it to the model's
rate. A packed config that names the bare ``F5TTSInference`` engine
instead does no such conversion and is not supported by this app.
"""
# Resolve first: a relative config path is joined onto the demo directory
# by `load_demo_session`, which would double a relative `demo_dir`.
demo_dir = Path(demo_dir).resolve()
if demo_config_path is None:
demo_config_path = demo_dir / "demo.yaml"
logger.info(
"Building recipe demo UI | demo_dir=%s demo_config_path=%s",
demo_dir,
demo_config_path,
)
session = load_demo_session(demo_dir, demo_config_path)
logger.info(
"Resolved demo specs | inputs=%s outputs=%s",
session.input_specs,
session.output_specs,
)
inference_fn = session.create_inference_fn(
session.input_specs,
session.output_specs,
)
def synthesize(*values):
"""Run the packed model on the UI values; the session shapes the outputs."""
# An untouched Gradio text box holds "", which the model would take
# as an empty transcript; None means "not given", which the contract
# refuses for a required field with a message naming it.
values = [None if value == "" else value for value in values]
try:
return inference_fn(*values)
except TypeError as error:
# A missing or malformed input, e.g. no reference speech or no
# transcript: show the reason in the UI instead of a bare "Error".
raise gr.Error(str(error)) from error
if spaces is not None:
# On ZeroGPU the model runs on a GPU leased per call; the decorated
# function is what the Space's scheduler sees.
logger.info("ZeroGPU detected; registering the handler with spaces.GPU")
synthesize = spaces.GPU(synthesize)
with gr.Blocks(title=session.title) as app:
if session.title:
gr.Markdown(f"# {session.title}")
# What the inputs are and what to expect, before the inputs themselves.
if session.description:
gr.Markdown(session.description)
input_components = []
with gr.Column():
# Gradio click handlers bind positional values, so this list keeps
# the order of session.input_specs.
for spec in session.input_specs:
logger.info("Building input component | spec=%s", spec)
input_components.append(session.build_input_component(spec))
submit_button = gr.Button("Synthesize")
output_components = []
with gr.Column():
# Outputs stay positional too: one value per output spec, in order.
for spec in session.output_specs:
logger.info("Building output component | spec=%s", spec)
output_components.append(session.build_output_component(spec))
logger.info("Binding Synthesize button click handler")
submit_button.click(
fn=synthesize,
inputs=input_components,
outputs=output_components,
)
# `ui.examples`: rows of input values in the order of `ui.inputs`; an
# audio value is a path relative to the demo directory, which is where
# the app runs from. A click fills the inputs; Synthesize runs them.
examples = _read_example_rows(session.demo_cfg, demo_dir)
if examples:
logger.info("Adding %d example row(s)", len(examples))
gr.Examples(examples=examples, inputs=input_components, label="Examples")
# The first example is filled in when the page opens, so a visitor
# can press Synthesize right away; the rows below refill the inputs.
first = tuple(examples[0]) if len(input_components) > 1 else examples[0][0]
app.load(fn=lambda: first, inputs=None, outputs=input_components)
logger.info("Recipe demo UI ready")
return app
def main() -> None:
"""Parse CLI arguments and launch the packed demo.
Returns:
None. Blocks while the Gradio server is running.
Example:
.. code-block:: bash
python app.py --demo-dir exp/training/demo
"""
parser = argparse.ArgumentParser(description="Launch an ESPnet3 F5-TTS demo.")
parser.add_argument(
"--demo-dir",
type=Path,
default=Path(__file__).resolve().parent,
help="Path to the demo directory. Defaults to this script's directory.",
)
parser.add_argument(
"--demo-config",
type=Path,
default=None,
help="Optional packed demo config path. Relative paths use --demo-dir.",
)
args = parser.parse_args()
configure_logging(log_dir=args.demo_dir, filename="demo.log")
logger.info("Starting recipe demo CLI | args=%s", args)
demo_config_path = args.demo_config or (args.demo_dir / "demo.yaml")
app = build_demo(
args.demo_dir,
demo_config_path=demo_config_path,
)
logger.info("Launching Gradio app")
app.launch()
if __name__ == "__main__":
main()