CoolFace
Apppublic

OpenMOSS-Team/MOSS-TTS-v1.5

sourceHugging Faceapache-2.0updated 4mo agoView on Hugging Face
73likes
app.py727 linesDownload Raw Back to root
1import argparse2import functools3import importlib.util4import inspect5import os6from pathlib import Path7import re8import time9import orjson10 11try:12    import spaces13except ImportError:14    class _SpacesFallback:15        @staticmethod16        def GPU(*_args, **_kwargs):17            def _decorator(func):18                return func19 20            return _decorator21 22    spaces = _SpacesFallback()23 24import gradio as gr25import numpy as np26import torch27from transformers import AutoModel, AutoProcessor28 29# Disable the broken cuDNN SDPA backend30torch.backends.cuda.enable_cudnn_sdp(False)31# Keep these enabled as fallbacks32torch.backends.cuda.enable_flash_sdp(True)33torch.backends.cuda.enable_mem_efficient_sdp(True)34torch.backends.cuda.enable_math_sdp(True)35 36MODEL_PATH = "OpenMOSS-Team/MOSS-TTS-v1.5"37DEFAULT_ATTN_IMPLEMENTATION = "auto"38DEFAULT_MAX_NEW_TOKENS = 409639PRELOAD_ENV_VAR = "MOSS_TTS_PRELOAD_AT_STARTUP"40CONTINUATION_NOTICE = (41    "Continuation mode is active. Make sure the reference audio transcript is prepended to the input text."42)43 44MODE_CLONE = "Clone"45MODE_CONTINUE = "Continuation"46MODE_CONTINUE_CLONE = "Continuation + Clone"47ZH_TOKENS_PER_CHAR = 3.09841195131303348EN_TOKENS_PER_CHAR = 0.867337626275521949REFERENCE_AUDIO_DIR = Path(__file__).resolve().parent / "assets" / "audio"50EXAMPLE_TEXTS_JSONL_PATH = Path(__file__).resolve().parent / "assets" / "text" / "moss_tts_example_texts.jsonl"51LANGUAGE_TAG_AUTO = "Auto (omit)"52LANGUAGE_TAG_CHOICES = [53    LANGUAGE_TAG_AUTO,54    "Chinese",55    "Cantonese",56    "English",57    "Arabic",58    "Czech",59    "Danish",60    "Dutch",61    "Finnish",62    "French",63    "German",64    "Greek",65    "Hebrew",66    "Hindi",67    "Hungarian",68    "Italian",69    "Japanese",70    "Korean",71    "Macedonian",72    "Malay",73    "Persian (Farsi)",74    "Polish",75    "Portuguese",76    "Romanian",77    "Russian",78    "Spanish",79    "Swahili",80    "Swedish",81    "Tagalog",82    "Thai",83    "Turkish",84    "Vietnamese",85]86 87 88def _parse_example_id(example_id: str) -> tuple[str, int] | None:89    matched = re.fullmatch(r"(zh|en)/(\d+)", (example_id or "").strip())90    if matched is None:91        return None92    return matched.group(1), int(matched.group(2))93 94 95def _resolve_reference_audio_path(language: str, index: int) -> Path | None:96    stem_candidates = [f"reference_{language}_{index}"]97    for stem in stem_candidates:98        for ext in (".wav", ".mp3"):99            audio_path = REFERENCE_AUDIO_DIR / f"{stem}{ext}"100            if audio_path.exists():101                return audio_path102    return None103 104 105def build_example_rows() -> list[tuple[str, str, str]]:106    rows: list[tuple[str, str, str]] = []107 108    with open(EXAMPLE_TEXTS_JSONL_PATH, "rb") as f:109        for line in f:110            if not line.strip():111                continue112            sample = orjson.loads(line)113            parsed = _parse_example_id(sample.get("id", ""))114            if parsed is None:115                continue116 117            language, index = parsed118            text = str(sample.get("text", "")).strip()119            audio_path = _resolve_reference_audio_path(language, index)120            if audio_path is None:121                continue122 123            rows.append((sample['role'], str(audio_path), text))124 125    return rows126 127 128EXAMPLE_ROWS = build_example_rows()129 130 131@functools.lru_cache(maxsize=1)132def load_backend(model_path: str, device_str: str, attn_implementation: str):133    device = torch.device(device_str if torch.cuda.is_available() else "cpu")134    dtype = torch.bfloat16 if device.type == "cuda" else torch.float32135    resolved_attn_implementation = resolve_attn_implementation(136        requested=attn_implementation,137        device=device,138        dtype=dtype,139    )140 141    processor = AutoProcessor.from_pretrained(142        model_path,143        trust_remote_code=True,144    )145    if hasattr(processor, "audio_tokenizer"):146        processor.audio_tokenizer = processor.audio_tokenizer.to(device)147        processor.audio_tokenizer.eval()148 149    model_kwargs = {150        "trust_remote_code": True,151        "torch_dtype": dtype,152    }153    if resolved_attn_implementation:154        model_kwargs["attn_implementation"] = resolved_attn_implementation155 156    model = AutoModel.from_pretrained(model_path, **model_kwargs).to(device)157    model.eval()158 159    sample_rate = int(getattr(processor.model_config, "sampling_rate", 24000))160    return model, processor, device, sample_rate161 162 163def resolve_attn_implementation(requested: str, device: torch.device, dtype: torch.dtype) -> str | None:164    requested_norm = (requested or "").strip().lower()165 166    if requested_norm in {"none"}:167        return None168 169    if requested_norm not in {"", "auto"}:170        return requested171 172    # Prefer FlashAttention 2 when package + device conditions are met.173    if (174        device.type == "cuda"175        and importlib.util.find_spec("flash_attn") is not None176        and dtype in {torch.float16, torch.bfloat16}177    ):178        major, _ = torch.cuda.get_device_capability(device)179        if major >= 8:180            return "flash_attention_2"181 182    # CUDA fallback: use PyTorch SDPA kernels.183    if device.type == "cuda":184        return "sdpa"185 186    # CPU fallback.187    return "eager"188 189 190def detect_text_language(text: str) -> str:191    zh_chars = len(re.findall(r"[\u4e00-\u9fff]", text))192    en_chars = len(re.findall(r"[A-Za-z]", text))193    if zh_chars == 0 and en_chars == 0:194        return "en"195    return "zh" if zh_chars >= en_chars else "en"196 197 198def supports_duration_control(mode_with_reference: str) -> bool:199    return mode_with_reference not in {MODE_CONTINUE, MODE_CONTINUE_CLONE}200 201 202def estimate_duration_tokens(text: str) -> tuple[str, int, int, int]:203    normalized = text or ""204    effective_len = max(len(normalized), 1)205    language = detect_text_language(normalized)206    factor = ZH_TOKENS_PER_CHAR if language == "zh" else EN_TOKENS_PER_CHAR207    default_tokens = max(1, int(effective_len * factor))208    min_tokens = max(1, int(default_tokens * 0.5))209    max_tokens = max(min_tokens, int(default_tokens * 1.5))210    return language, default_tokens, min_tokens, max_tokens211 212 213def update_duration_controls(214    enabled: bool,215    text: str,216    current_tokens: float | int | None,217    mode_with_reference: str,218):219    if not supports_duration_control(mode_with_reference):220        return (221            gr.update(visible=False),222            "Duration control is disabled for Continuation modes.",223            gr.update(value=False, interactive=False),224        )225 226    checkbox_update = gr.update(interactive=True)227    if not enabled:228        return gr.update(visible=False), "Duration control is disabled.", checkbox_update229 230    language, default_tokens, min_tokens, max_tokens = estimate_duration_tokens(text)231    # Slider is initialized with value=1 as a placeholder; treat it as "unset"232    # so first-time estimation uses the computed default instead of clamping to min.233    if current_tokens is None or int(current_tokens) == 1:234        slider_value = default_tokens235    else:236        slider_value = int(current_tokens)237        slider_value = max(min_tokens, min(max_tokens, slider_value))238 239    language_label = "Chinese" if language == "zh" else "English"240    hint = (241        f"Duration control enabled | detected language: {language_label} | "242        f"default={default_tokens}, range=[{min_tokens}, {max_tokens}]"243    )244    return (245        gr.update(246            visible=True,247            minimum=min_tokens,248            maximum=max_tokens,249            value=slider_value,250            step=1,251        ),252        hint,253        checkbox_update,254    )255 256 257def normalize_language_tag(language_tag: str | None) -> str | None:258    language_tag = (language_tag or "").strip()259    if not language_tag or language_tag == LANGUAGE_TAG_AUTO:260        return None261    return language_tag262 263 264def build_conversation(265    text: str,266    reference_audio: str | None,267    mode_with_reference: str,268    expected_tokens: int | None,269    language_tag: str | None,270    processor,271):272    text = (text or "").strip()273    if not text:274        raise ValueError("Please enter text to synthesize.")275 276    user_kwargs = {"text": text}277    normalized_language = normalize_language_tag(language_tag)278    if normalized_language is not None:279        user_kwargs["language"] = normalized_language280    if expected_tokens is not None:281        user_kwargs["tokens"] = int(expected_tokens)282 283    if not reference_audio:284        conversations = [[processor.build_user_message(**user_kwargs)]]285        return conversations, "generation", "Direct Generation"286 287    if mode_with_reference == MODE_CLONE:288        clone_kwargs = dict(user_kwargs)289        clone_kwargs["reference"] = [reference_audio]290        conversations = [[processor.build_user_message(**clone_kwargs)]]291        return conversations, "generation", MODE_CLONE292 293    if mode_with_reference == MODE_CONTINUE:294        conversations = [295            [296                processor.build_user_message(**user_kwargs),297                processor.build_assistant_message(audio_codes_list=[reference_audio]),298            ]299        ]300        return conversations, "continuation", MODE_CONTINUE301 302    continue_clone_kwargs = dict(user_kwargs)303    continue_clone_kwargs["reference"] = [reference_audio]304    conversations = [305        [306            processor.build_user_message(**continue_clone_kwargs),307            processor.build_assistant_message(audio_codes_list=[reference_audio]),308        ]309    ]310    return conversations, "continuation", MODE_CONTINUE_CLONE311 312 313def render_mode_hint(reference_audio: str | None, mode_with_reference: str):314    if not reference_audio:315        return "Current mode: **Direct Generation** (no reference audio uploaded)"316    if mode_with_reference == MODE_CLONE:317        return "Current mode: **Clone** (speaker timbre will be cloned from the reference audio)"318    return f"Current mode: **{mode_with_reference}**  \n> {CONTINUATION_NOTICE}"319 320 321def apply_example_selection(322    mode_with_reference: str,323    duration_control_enabled: bool,324    duration_tokens: int,325    evt: gr.SelectData,326):327    if evt is None or evt.index is None:328        return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()329 330    if isinstance(evt.index, (tuple, list)):331        row_idx = int(evt.index[0])332    else:333        row_idx = int(evt.index)334 335    if row_idx < 0 or row_idx >= len(EXAMPLE_ROWS):336        return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()337 338    _, audio_path, example_text = EXAMPLE_ROWS[row_idx]339    duration_slider_update, duration_hint, duration_checkbox_update = update_duration_controls(340        duration_control_enabled,341        example_text,342        duration_tokens,343        mode_with_reference,344    )345    return (346        audio_path,347        example_text,348        render_mode_hint(audio_path, mode_with_reference),349        duration_slider_update,350        duration_hint,351        duration_checkbox_update,352    )353 354 355@spaces.GPU(duration=180)356def run_inference(357    text: str,358    reference_audio: str | None,359    mode_with_reference: str,360    duration_control_enabled: bool,361    duration_tokens: int,362    language_tag: str | None,363    temperature: float,364    top_p: float,365    top_k: int,366    repetition_penalty: float,367    model_path: str,368    device: str,369    attn_implementation: str,370    max_new_tokens: int,371):372    started_at = time.monotonic()373    model, processor, torch_device, sample_rate = load_backend(374        model_path=model_path,375        device_str=device,376        attn_implementation=attn_implementation,377    )378    duration_enabled = bool(duration_control_enabled and supports_duration_control(mode_with_reference))379    expected_tokens = int(duration_tokens) if duration_enabled else None380    conversations, mode, mode_name = build_conversation(381        text=text,382        reference_audio=reference_audio,383        mode_with_reference=mode_with_reference,384        expected_tokens=expected_tokens,385        language_tag=language_tag,386        processor=processor,387    )388 389    batch = processor(conversations, mode=mode)390    input_ids = batch["input_ids"].to(torch_device)391    attention_mask = batch["attention_mask"].to(torch_device)392 393    with torch.no_grad():394        outputs = model.generate(395            input_ids=input_ids,396            attention_mask=attention_mask,397            max_new_tokens=int(max_new_tokens),398            audio_temperature=float(temperature),399            audio_top_p=float(top_p),400            audio_top_k=int(top_k),401            audio_repetition_penalty=float(repetition_penalty),402        )403 404    messages = processor.decode(outputs)405    if not messages or messages[0] is None:406        raise RuntimeError("The model did not return a decodable audio result.")407 408    audio = messages[0].audio_codes_list[0]409    if isinstance(audio, torch.Tensor):410        audio_np = audio.detach().float().cpu().numpy()411    else:412        audio_np = np.asarray(audio, dtype=np.float32)413 414    if audio_np.ndim > 1:415        audio_np = audio_np.reshape(-1)416    audio_np = audio_np.astype(np.float32, copy=False)417 418    elapsed = time.monotonic() - started_at419    normalized_language = normalize_language_tag(language_tag)420    status = (421        f"Done | mode: {mode_name} | language={normalized_language or 'auto'} | "422        f"elapsed: {elapsed:.2f}s | "423        f"max_new_tokens={int(max_new_tokens)}, "424        f"expected_tokens={expected_tokens if expected_tokens is not None else 'off'}, "425        f"audio_temperature={float(temperature):.2f}, audio_top_p={float(top_p):.2f}, "426        f"audio_top_k={int(top_k)}, audio_repetition_penalty={float(repetition_penalty):.2f}"427    )428    return (sample_rate, audio_np), status429 430 431def build_demo(args: argparse.Namespace):432    custom_css = """433    :root {434      --bg: #f6f7f8;435      --panel: #ffffff;436      --ink: #111418;437      --muted: #4d5562;438      --line: #e5e7eb;439      --accent: #0f766e;440    }441    .gradio-container {442      background: linear-gradient(180deg, #f7f8fa 0%, #f3f5f7 100%);443      color: var(--ink);444    }445    .app-card {446      border: 1px solid var(--line);447      border-radius: 16px;448      background: var(--panel);449      padding: 14px;450    }451    .app-title {452      font-size: 22px;453      font-weight: 700;454      margin-bottom: 6px;455      letter-spacing: 0.2px;456    }457    .app-subtitle {458      color: var(--muted);459      font-size: 14px;460      margin-bottom: 8px;461    }462    #output_audio {463      padding-bottom: 12px;464      margin-bottom: 8px;465      overflow: hidden !important;466    }467    #output_audio > .wrap {468      overflow: hidden !important;469    }470    #output_audio audio {471      margin-bottom: 6px;472    }473    #run-btn {474      background: var(--accent);475      border: none;476    }477    """478 479    with gr.Blocks(title="MOSS-TTS Demo") as demo:480        gr.Markdown(481            """482            <div class="app-card">483              <div class="app-title">MOSS-TTS v1.5</div>484              <div class="app-subtitle">Direct Generation, Clone, Continuation, Continuation + Clone, language tags, and inline pause markers</div>485            </div>486            """487        )488 489        with gr.Row(equal_height=False):490            with gr.Column(scale=3):491                text = gr.Textbox(492                    label="Text",493                    lines=9,494                    placeholder="Enter text to synthesize. In continuation modes, prepend the reference audio transcript.",495                )496                reference_audio = gr.Audio(497                    label="Reference Audio (Optional)",498                    type="filepath",499                )500                mode_with_reference = gr.Radio(501                    choices=[MODE_CLONE, MODE_CONTINUE, MODE_CONTINUE_CLONE],502                    value=MODE_CLONE,503                    label="Mode with Reference Audio",504                    info="If no reference audio is uploaded, Direct Generation will be used automatically.",505                )506                mode_hint = gr.Markdown(render_mode_hint(None, MODE_CLONE))507                language_tag = gr.Dropdown(508                    choices=LANGUAGE_TAG_CHOICES,509                    value=LANGUAGE_TAG_AUTO,510                    label="Language Tag",511                    info="Optional for v1.5. Set this when the input language is known, especially outside Chinese and English.",512                )513                duration_control_enabled = gr.Checkbox(514                    value=False,515                    label="Enable Duration Control (Expected Audio Tokens)",516                )517                duration_tokens = gr.Slider(518                    minimum=1,519                    maximum=2,520                    step=1,521                    value=1,522                    label="expected_tokens",523                    visible=False,524                )525                duration_hint = gr.Markdown("Duration control is disabled.")526 527                with gr.Accordion("Sampling Parameters (Audio)", open=True):528                    temperature = gr.Slider(529                        minimum=0.1,530                        maximum=3.0,531                        step=0.05,532                        value=1.7,533                        label="temperature",534                    )535                    top_p = gr.Slider(536                        minimum=0.1,537                        maximum=1.0,538                        step=0.01,539                        value=0.8,540                        label="top_p",541                    )542                    top_k = gr.Slider(543                        minimum=1,544                        maximum=200,545                        step=1,546                        value=25,547                        label="top_k",548                    )549                    repetition_penalty = gr.Slider(550                        minimum=0.8,551                        maximum=2.0,552                        step=0.05,553                        value=1.0,554                        label="repetition_penalty",555                    )556                    max_new_tokens = gr.Slider(557                        minimum=256,558                        maximum=8192,559                        step=128,560                        value=DEFAULT_MAX_NEW_TOKENS,561                        label="max_new_tokens",562                    )563 564                run_btn = gr.Button("Generate Speech", variant="primary", elem_id="run-btn")565 566            with gr.Column(scale=2):567                output_audio = gr.Audio(label="Output Audio", type="numpy", elem_id="output_audio")568                status = gr.Textbox(label="Status", lines=4, interactive=False)569                examples_table = gr.Dataframe(570                    headers=["Reference Speech", "Example Text"],571                    value=[[name, text] for name, _, text in EXAMPLE_ROWS],572                    datatype=["str", "str"],573                    row_count=(len(EXAMPLE_ROWS), "fixed"),574                    col_count=(2, "fixed"),575                    interactive=False,576                    wrap=True,577                    label="Examples (click a row to fill inputs)",578                )579 580        reference_audio.change(581            fn=render_mode_hint,582            inputs=[reference_audio, mode_with_reference],583            outputs=[mode_hint],584        )585        mode_with_reference.change(586            fn=render_mode_hint,587            inputs=[reference_audio, mode_with_reference],588            outputs=[mode_hint],589        )590        duration_control_enabled.change(591            fn=update_duration_controls,592            inputs=[duration_control_enabled, text, duration_tokens, mode_with_reference],593            outputs=[duration_tokens, duration_hint, duration_control_enabled],594        )595        text.change(596            fn=update_duration_controls,597            inputs=[duration_control_enabled, text, duration_tokens, mode_with_reference],598            outputs=[duration_tokens, duration_hint, duration_control_enabled],599        )600        mode_with_reference.change(601            fn=update_duration_controls,602            inputs=[duration_control_enabled, text, duration_tokens, mode_with_reference],603            outputs=[duration_tokens, duration_hint, duration_control_enabled],604        )605        examples_table.select(606            fn=apply_example_selection,607            inputs=[mode_with_reference, duration_control_enabled, duration_tokens],608            outputs=[609                reference_audio,610                text,611                mode_hint,612                duration_tokens,613                duration_hint,614                duration_control_enabled,615            ],616        )617 618        run_btn.click(619            fn=run_inference,620            inputs=[621                text,622                reference_audio,623                mode_with_reference,624                duration_control_enabled,625                duration_tokens,626                language_tag,627                temperature,628                top_p,629                top_k,630                repetition_penalty,631                gr.State(args.model_path),632                gr.State(args.device),633                gr.State(args.attn_implementation),634                max_new_tokens,635            ],636            outputs=[output_audio, status],637        )638    demo._moss_custom_css = custom_css639    return demo640 641 642def resolve_runtime_attn(args: argparse.Namespace) -> argparse.Namespace:643    runtime_device = torch.device(args.device if torch.cuda.is_available() else "cpu")644    runtime_dtype = torch.bfloat16 if runtime_device.type == "cuda" else torch.float32645    args.attn_implementation = resolve_attn_implementation(646        requested=args.attn_implementation,647        device=runtime_device,648        dtype=runtime_dtype,649    ) or "none"650    return args651 652 653def parse_bool_env(name: str, default: bool) -> bool:654    value = os.getenv(name)655    if value is None:656        return default657    return value.strip().lower() in {"1", "true", "yes", "y", "on"}658 659 660def parse_port(value: str | None, default: int) -> int:661    if not value:662        return default663    try:664        return int(value)665    except ValueError:666        return default667 668 669def main():670    parser = argparse.ArgumentParser(description="MossTTS Gradio Demo")671    parser.add_argument("--model_path", type=str, default=MODEL_PATH)672    parser.add_argument("--device", type=str, default="cuda:0")673    parser.add_argument("--attn_implementation", type=str, default=DEFAULT_ATTN_IMPLEMENTATION)674    parser.add_argument("--host", type=str, default="0.0.0.0")675    parser.add_argument(676        "--port",677        type=int,678        default=int(os.getenv("GRADIO_SERVER_PORT", os.getenv("PORT", "7860"))),679    )680    parser.add_argument("--share", action="store_true")681    args = parser.parse_args()682 683    args.host = os.getenv("GRADIO_SERVER_NAME", args.host)684    args.port = parse_port(os.getenv("GRADIO_SERVER_PORT", os.getenv("PORT")), args.port)685    args = resolve_runtime_attn(args)686    print(f"[INFO] Using attn_implementation={args.attn_implementation}", flush=True)687 688    preload_enabled = parse_bool_env(PRELOAD_ENV_VAR, default=not bool(os.getenv("SPACE_ID")))689    if preload_enabled:690        preload_started_at = time.monotonic()691        print(692            f"[Startup] Preloading backend: model={args.model_path}, device={args.device}, attn={args.attn_implementation}",693            flush=True,694        )695        load_backend(696            model_path=args.model_path,697            device_str=args.device,698            attn_implementation=args.attn_implementation,699        )700        print(701            f"[Startup] Backend preload finished in {time.monotonic() - preload_started_at:.2f}s",702            flush=True,703        )704    else:705        print(706            f"[Startup] Skipping preload (set {PRELOAD_ENV_VAR}=1 to enable).",707            flush=True,708        )709 710    demo = build_demo(args)711    launch_kwargs = {712        "server_name": args.host,713        "server_port": args.port,714        "share": args.share,715    }716    launch_parameters = inspect.signature(demo.launch).parameters717    if "css" in launch_parameters:718        launch_kwargs["css"] = getattr(demo, "_moss_custom_css", None)719    if "ssr_mode" in launch_parameters:720        launch_kwargs["ssr_mode"] = False721 722    demo.queue(max_size=16, default_concurrency_limit=1).launch(**launch_kwargs)723 724 725if __name__ == "__main__":726    main()727