Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- README.md +32 -8
- app.py +484 -0
- decoder4_features_torch.py +39 -0
- portable_tts_runtime.py +800 -0
- requirements.txt +4 -0
- speaker_hours_tiers.csv +44 -0
- speaker_id_map.csv +44 -0
- torch_hf_optimizations.py +0 -0
README.md
CHANGED
|
@@ -1,13 +1,37 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: MOSS-TTS-PNY
|
| 3 |
+
emoji: 🎙️
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.15.1
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: Speaker-conditioned TTS with emotion & energy control
|
| 10 |
+
python_version: "3.12"
|
| 11 |
+
startup_duration_timeout: 1h
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# MOSS-TTS-PNY
|
| 15 |
+
|
| 16 |
+
Speaker-conditioned text-to-speech demo using the [ZDisket/MOSS-TTS-PNY](https://huggingface.co/ZDisket/MOSS-TTS-PNY) model — a fine-tuned MOSS-TTS checkpoint with character voice clones from My Little Pony: Friendship is Magic and Team Fortress 2.
|
| 17 |
+
|
| 18 |
+
## Features
|
| 19 |
+
|
| 20 |
+
- **43 speakers** — MLP:FiM characters (Twilight, Pinkie, Rainbow, etc.) and TF2 characters (Heavy, Scout, Soldier, etc.)
|
| 21 |
+
- **12 emotions** — Neutral, Happy, Sad, Angry, Annoyed, Fearful, Surprised, Calm, Disgusted, Whispering, Nonverbal, Shouting
|
| 22 |
+
- **Continuous energy control** — adjust the energy/intensity of the generated speech (0.0 – 1.0)
|
| 23 |
+
- **48 kHz output** via iSTFTNet2 TorchScript CUDA vocoder
|
| 24 |
+
|
| 25 |
+
## How it works
|
| 26 |
+
|
| 27 |
+
1. Enter the text you want to synthesize.
|
| 28 |
+
2. Pick a speaker from the dropdown.
|
| 29 |
+
3. Choose an emotion and set the energy level.
|
| 30 |
+
4. Click **Generate** to produce audio.
|
| 31 |
+
|
| 32 |
+
## Notes
|
| 33 |
+
|
| 34 |
+
- Uses `trust_remote_code=True` to load the custom MOSS-TTS model code.
|
| 35 |
+
- `transformers==4.55.0` is pinned to prevent gibberish outputs.
|
| 36 |
+
- The model downloads ~12 GB of weights at startup.
|
| 37 |
+
- Max 30 seconds of audio per generation.
|
app.py
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Gradio demo for ZDisket/MOSS-TTS-PNY — a speaker-conditioned TTS model.
|
| 2 |
+
|
| 3 |
+
This Space loads the MOSS-TTS-PNY checkpoint (fine-tuned on MLP:FiM and TF2
|
| 4 |
+
voices) and provides a Gradio interface for text-to-speech synthesis with
|
| 5 |
+
speaker selection, emotion control, and energy adjustment.
|
| 6 |
+
|
| 7 |
+
Model: https://huggingface.co/ZDisket/MOSS-TTS-PNY
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import csv
|
| 13 |
+
import json
|
| 14 |
+
import math
|
| 15 |
+
import os
|
| 16 |
+
import random
|
| 17 |
+
import tempfile
|
| 18 |
+
import time
|
| 19 |
+
import unicodedata
|
| 20 |
+
import uuid
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Any
|
| 23 |
+
|
| 24 |
+
# ── ZeroGPU: import spaces before any CUDA-touching import ──────────────
|
| 25 |
+
import spaces # noqa: E402
|
| 26 |
+
|
| 27 |
+
import torch # noqa: E402
|
| 28 |
+
import gradio as gr # noqa: E402
|
| 29 |
+
from huggingface_hub import snapshot_download # noqa: E402
|
| 30 |
+
|
| 31 |
+
# ── Path setup ───────────────────────────────────────────────────────────
|
| 32 |
+
BUILD_ROOT = Path(__file__).resolve().parent
|
| 33 |
+
MODEL_REPO = "ZDisket/MOSS-TTS-PNY"
|
| 34 |
+
|
| 35 |
+
# Skip the os.execv re-exec inside portable_tts_runtime
|
| 36 |
+
os.environ.setdefault("MOSS_TTS_NVIDIA_LD_LIBRARY_PATH_READY", "1")
|
| 37 |
+
os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
|
| 38 |
+
|
| 39 |
+
# ── Download model weights at startup ─────────────────────────────────────
|
| 40 |
+
print("Downloading model weights from ZDisket/MOSS-TTS-PNY …", flush=True)
|
| 41 |
+
_download_start = time.perf_counter()
|
| 42 |
+
_model_root = snapshot_download(
|
| 43 |
+
repo_id=MODEL_REPO,
|
| 44 |
+
repo_type="model",
|
| 45 |
+
allow_patterns=[
|
| 46 |
+
"moss_tts_local_clipper_checkpoint/**",
|
| 47 |
+
"moss_audio_tokenizer/**",
|
| 48 |
+
"istftnet2_decoder4_50hz/**",
|
| 49 |
+
],
|
| 50 |
+
)
|
| 51 |
+
_download_elapsed = time.perf_counter() - _download_start
|
| 52 |
+
print(f"Model weights downloaded in {_download_elapsed:.1f}s -> {_model_root}", flush=True)
|
| 53 |
+
|
| 54 |
+
CHECKPOINT_PATH = str(Path(_model_root) / "moss_tts_local_clipper_checkpoint")
|
| 55 |
+
CODEC_PATH = str(Path(_model_root) / "moss_audio_tokenizer")
|
| 56 |
+
DECODER_DIR = str(Path(_model_root) / "istftnet2_decoder4_50hz")
|
| 57 |
+
|
| 58 |
+
# ── Constants ─────────────────────────────────────────────────────────────
|
| 59 |
+
CODEC_FRAMES_PER_SECOND = 12.5
|
| 60 |
+
MAX_OUTPUT_SECONDS = 30
|
| 61 |
+
MAX_OUTPUT_FRAMES = int(CODEC_FRAMES_PER_SECOND * MAX_OUTPUT_SECONDS)
|
| 62 |
+
MAX_TEXT_LINES = 12
|
| 63 |
+
MAX_LANGUAGE_CHARS = 16
|
| 64 |
+
MAX_TEXT_CHARS = 500
|
| 65 |
+
|
| 66 |
+
EMOTION_CHOICES = [
|
| 67 |
+
("Neutral", 0),
|
| 68 |
+
("Happy", 1),
|
| 69 |
+
("Sad", 2),
|
| 70 |
+
("Angry", 3),
|
| 71 |
+
("Annoyed", 4),
|
| 72 |
+
("Fearful", 5),
|
| 73 |
+
("Surprised", 6),
|
| 74 |
+
("Calm", 7),
|
| 75 |
+
("Disgusted", 8),
|
| 76 |
+
("Whispering", 9),
|
| 77 |
+
("Nonverbal", 10),
|
| 78 |
+
("Shouting", 11),
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
RANDOM_TEXTS = [
|
| 82 |
+
"I know this sounds sudden, but I really missed hearing your voice today.",
|
| 83 |
+
"Wait, you actually finished the whole thing already?",
|
| 84 |
+
"I'm trying to stay calm, but that was way closer than I expected.",
|
| 85 |
+
"Honestly, I'm proud of us for getting through that without giving up.",
|
| 86 |
+
"No, no, it's fine. I'm only a little dramatically offended.",
|
| 87 |
+
"I cannot believe you kept that surprise hidden for so long.",
|
| 88 |
+
"Please tell me you saw that too, because I am not ready to explain it.",
|
| 89 |
+
"That was kind of beautiful, in a messy and completely unexpected way.",
|
| 90 |
+
"I'm sorry, I should have listened before jumping to conclusions.",
|
| 91 |
+
"Okay, deep breath. We can fix this if we take it one step at a time.",
|
| 92 |
+
"You have no idea how happy that made me.",
|
| 93 |
+
"I was nervous at first, but now I'm actually excited to try again.",
|
| 94 |
+
]
|
| 95 |
+
|
| 96 |
+
# ── Load speaker map & tiers ──────────────────────────────────────────────
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def load_speaker_tiers(csv_path: str) -> dict[int, int]:
|
| 100 |
+
path = Path(csv_path)
|
| 101 |
+
if not path.exists():
|
| 102 |
+
return {}
|
| 103 |
+
tiers: dict[int, int] = {}
|
| 104 |
+
with path.open("r", encoding="utf-8", newline="") as handle:
|
| 105 |
+
for row in csv.DictReader(handle):
|
| 106 |
+
tiers[int(row["speaker_id"])] = int(row["tier"])
|
| 107 |
+
return tiers
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def load_speaker_choices(csv_path: str, tiers: dict[int, int]) -> tuple[list[str], dict[str, int]]:
|
| 111 |
+
path = Path(csv_path)
|
| 112 |
+
if not path.exists():
|
| 113 |
+
choices = [f"Speaker {i} ({i})" for i in range(43)]
|
| 114 |
+
return choices, {c: i for i, c in enumerate(choices)}
|
| 115 |
+
choice_rows: list[tuple[int, str, str, int]] = []
|
| 116 |
+
with path.open("r", encoding="utf-8", newline="") as handle:
|
| 117 |
+
for row in csv.DictReader(handle):
|
| 118 |
+
speaker_id = int(row["speaker_id"])
|
| 119 |
+
label = row.get("label") or f"{row.get('display_name') or row.get('name')} ({speaker_id})"
|
| 120 |
+
tier = tiers.get(speaker_id, 9)
|
| 121 |
+
if tier != 9:
|
| 122 |
+
label = f"T{tier} - {label}"
|
| 123 |
+
choice_rows.append((tier, label.lower(), label, speaker_id))
|
| 124 |
+
choices: list[str] = []
|
| 125 |
+
speaker_ids: dict[str, int] = {}
|
| 126 |
+
for _tier, _sort_label, label, speaker_id in sorted(choice_rows):
|
| 127 |
+
choices.append(label)
|
| 128 |
+
speaker_ids[label] = speaker_id
|
| 129 |
+
return choices, speaker_ids
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
SPEAKER_TIERS = load_speaker_tiers(str(BUILD_ROOT / "speaker_hours_tiers.csv"))
|
| 133 |
+
SPEAKER_CHOICES, SPEAKER_IDS = load_speaker_choices(
|
| 134 |
+
str(BUILD_ROOT / "speaker_id_map.csv"), SPEAKER_TIERS
|
| 135 |
+
)
|
| 136 |
+
DEFAULT_SPEAKER = next(
|
| 137 |
+
(c for c in SPEAKER_CHOICES if "Twilight (31)" in c),
|
| 138 |
+
SPEAKER_CHOICES[0] if SPEAKER_CHOICES else "Speaker 31 (31)",
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
# ── Load model at module scope (ZeroGPU rule 2) ───────────────────────────
|
| 142 |
+
from portable_tts_runtime import ( # noqa: E402
|
| 143 |
+
OptimizedTTSConfig,
|
| 144 |
+
OptimizedTTSRunner,
|
| 145 |
+
save_wav_pcm16,
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
# Use fixed-full torch opt mode (avoids the ~4-minute Triton compile of the
|
| 149 |
+
# packed-local path; still gets torch.compile on the global transformer).
|
| 150 |
+
RUNNER = OptimizedTTSRunner(
|
| 151 |
+
OptimizedTTSConfig(
|
| 152 |
+
checkpoint=CHECKPOINT_PATH,
|
| 153 |
+
codec_path=CODEC_PATH,
|
| 154 |
+
decoder_dir=DECODER_DIR,
|
| 155 |
+
decoder_runtime="torchscript_cuda",
|
| 156 |
+
decoder4_features_runtime="torch_fp16",
|
| 157 |
+
dtype="fp16",
|
| 158 |
+
attn_implementation="sdpa",
|
| 159 |
+
torch_opt_mode="fixed-full",
|
| 160 |
+
cache_implementation="static",
|
| 161 |
+
compile_global_transformer=True,
|
| 162 |
+
global_compile_mode="default",
|
| 163 |
+
fast_prepare_inputs=True,
|
| 164 |
+
triton_top_p=True,
|
| 165 |
+
packed_local_qkv=False,
|
| 166 |
+
packed_local_mlp=False,
|
| 167 |
+
static_packed_weights=False,
|
| 168 |
+
tensorize_rmsnorm_eps=True,
|
| 169 |
+
feedback_lookup=True,
|
| 170 |
+
vocoder_bucket_frames=64,
|
| 171 |
+
vocoder_prewarm_buckets="64,128,192,256,320,384,448,512,576,640",
|
| 172 |
+
)
|
| 173 |
+
)
|
| 174 |
+
SAMPLE_RATE = int(RUNNER.sample_rate)
|
| 175 |
+
print(f"Model loaded. sample_rate={SAMPLE_RATE}", flush=True)
|
| 176 |
+
|
| 177 |
+
# Prewarm the model with a short dummy generation so the first user request
|
| 178 |
+
# does not pay the torch.compile cost.
|
| 179 |
+
print("Prewarming model …", flush=True)
|
| 180 |
+
_prewarm_start = time.perf_counter()
|
| 181 |
+
try:
|
| 182 |
+
_pw = RUNNER.synthesize(
|
| 183 |
+
text="The optimized demo is ready.",
|
| 184 |
+
language="en",
|
| 185 |
+
speaker_id=31,
|
| 186 |
+
max_new_tokens=64,
|
| 187 |
+
text_temperature=0.0,
|
| 188 |
+
text_top_p=1.0,
|
| 189 |
+
text_top_k=None,
|
| 190 |
+
audio_temperature=0.8,
|
| 191 |
+
audio_top_p=0.92,
|
| 192 |
+
audio_top_k=None,
|
| 193 |
+
audio_repetition_penalty=1.05,
|
| 194 |
+
n_vq_for_inference=32,
|
| 195 |
+
style_text="The optimized demo is ready.",
|
| 196 |
+
style_emotion_id=0,
|
| 197 |
+
style_energy=0.5,
|
| 198 |
+
)
|
| 199 |
+
print(f"Prewarm complete in {time.perf_counter() - _prewarm_start:.1f}s", flush=True)
|
| 200 |
+
except Exception as exc:
|
| 201 |
+
print(f"Prewarm failed ({exc!r}) — continuing anyway", flush=True)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
# ── Helpers ────────────────────────────────────────────────────────────────
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def energy_label(energy: float) -> str:
|
| 208 |
+
if energy < 0.33:
|
| 209 |
+
return "low energy"
|
| 210 |
+
if energy < 0.66:
|
| 211 |
+
return "medium energy"
|
| 212 |
+
return "high energy"
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def normalize_client_text(value: str, *, max_chars: int) -> str:
|
| 216 |
+
if not isinstance(value, str):
|
| 217 |
+
raise gr.Error("Text must be a string.")
|
| 218 |
+
text = unicodedata.normalize("NFKC", value).replace("\r\n", "\n").replace("\r", "\n").strip()
|
| 219 |
+
if not text:
|
| 220 |
+
raise gr.Error("Enter some text first.")
|
| 221 |
+
if max_chars > 0 and len(text) > max_chars:
|
| 222 |
+
raise gr.Error(f"Text is too long. Keep it under {max_chars} characters.")
|
| 223 |
+
if text.count("\n") + 1 > MAX_TEXT_LINES:
|
| 224 |
+
raise gr.Error(f"Text has too many lines. Keep it under {MAX_TEXT_LINES} lines.")
|
| 225 |
+
for char in text:
|
| 226 |
+
cat = unicodedata.category(char)
|
| 227 |
+
if cat.startswith("C") and char not in {"\n", "\t"}:
|
| 228 |
+
raise gr.Error("Text contains unsupported control characters.")
|
| 229 |
+
if not any(c.isalnum() for c in text):
|
| 230 |
+
raise gr.Error("Text must contain at least one letter or number.")
|
| 231 |
+
if "\ufffd" in text:
|
| 232 |
+
raise gr.Error("Text contains malformed replacement characters.")
|
| 233 |
+
return text
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def normalize_language(value: str) -> str:
|
| 237 |
+
language = unicodedata.normalize("NFKC", value or "en").strip().lower()
|
| 238 |
+
if not language:
|
| 239 |
+
return "en"
|
| 240 |
+
if len(language) > MAX_LANGUAGE_CHARS:
|
| 241 |
+
raise gr.Error("Language code is too long.")
|
| 242 |
+
allowed = set("abcdefghijklmnopqrstuvwxyz-")
|
| 243 |
+
if any(c not in allowed for c in language):
|
| 244 |
+
raise gr.Error("Language must be a simple code like 'en'.")
|
| 245 |
+
return language
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def validate_float(name: str, value: float, minimum: float, maximum: float) -> float:
|
| 249 |
+
value = float(value)
|
| 250 |
+
if not math.isfinite(value) or value < minimum or value > maximum:
|
| 251 |
+
raise gr.Error(f"{name} must be between {minimum} and {maximum}.")
|
| 252 |
+
return value
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def validate_int(name: str, value: int, minimum: int, maximum: int) -> int:
|
| 256 |
+
value = int(value)
|
| 257 |
+
if value < minimum or value > maximum:
|
| 258 |
+
raise gr.Error(f"{name} must be between {minimum} and {maximum}.")
|
| 259 |
+
return value
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def _random_text() -> str:
|
| 263 |
+
return random.choice(RANDOM_TEXTS)
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
# ── Inference (decorated for ZeroGPU) ─────────────────────────────────────
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
@spaces.GPU(duration=120)
|
| 270 |
+
def synthesize(
|
| 271 |
+
text: str,
|
| 272 |
+
style_text: str,
|
| 273 |
+
language: str,
|
| 274 |
+
speaker_label: str,
|
| 275 |
+
emotion_label: str,
|
| 276 |
+
style_energy: float,
|
| 277 |
+
audio_temperature: float,
|
| 278 |
+
audio_top_p: float,
|
| 279 |
+
audio_top_k: int,
|
| 280 |
+
audio_repetition_penalty: float,
|
| 281 |
+
) -> tuple[str, str]:
|
| 282 |
+
"""Generate speech audio from text using MOSS-TTS-PNY.
|
| 283 |
+
|
| 284 |
+
Args:
|
| 285 |
+
text: The text to synthesize.
|
| 286 |
+
style_text: Optional style reference text (falls back to ``text``).
|
| 287 |
+
language: Language code (e.g. ``"en"``).
|
| 288 |
+
speaker_label: Speaker name from the dropdown.
|
| 289 |
+
emotion_label: Emotion name from the dropdown.
|
| 290 |
+
style_energy: Energy level (0.0 – 1.0).
|
| 291 |
+
audio_temperature: Sampling temperature for audio tokens.
|
| 292 |
+
audio_top_p: Top-p value for audio token sampling.
|
| 293 |
+
audio_top_k: Top-k value (0 = disabled).
|
| 294 |
+
audio_repetition_penalty: Repetition penalty for audio tokens.
|
| 295 |
+
|
| 296 |
+
Returns:
|
| 297 |
+
A tuple of (wav_filepath, stats_json).
|
| 298 |
+
"""
|
| 299 |
+
clean_text = normalize_client_text(text, max_chars=MAX_TEXT_CHARS)
|
| 300 |
+
clean_style_text = (
|
| 301 |
+
normalize_client_text(style_text, max_chars=MAX_TEXT_CHARS)
|
| 302 |
+
if style_text and style_text.strip()
|
| 303 |
+
else ""
|
| 304 |
+
)
|
| 305 |
+
clean_language = normalize_language(language)
|
| 306 |
+
if speaker_label not in SPEAKER_IDS:
|
| 307 |
+
raise gr.Error("Choose a valid speaker.")
|
| 308 |
+
emotion_lookup = {label: eid for label, eid in EMOTION_CHOICES}
|
| 309 |
+
if emotion_label not in emotion_lookup:
|
| 310 |
+
raise gr.Error("Choose a valid emotion.")
|
| 311 |
+
clean_energy = validate_float("Energy", style_energy, 0.0, 1.0)
|
| 312 |
+
clean_temperature = validate_float("Audio temperature", audio_temperature, 0.0, 1.5)
|
| 313 |
+
clean_top_p = validate_float("Audio top-p", audio_top_p, 0.05, 1.0)
|
| 314 |
+
clean_top_k = validate_int("Audio top-k", audio_top_k, 0, 200)
|
| 315 |
+
clean_rep_pen = validate_float(
|
| 316 |
+
"Audio repetition penalty", audio_repetition_penalty, 0.0, 2.0
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
start = time.perf_counter()
|
| 320 |
+
result = RUNNER.synthesize(
|
| 321 |
+
text=clean_text,
|
| 322 |
+
language=clean_language,
|
| 323 |
+
speaker_id=SPEAKER_IDS[speaker_label],
|
| 324 |
+
max_new_tokens=MAX_OUTPUT_FRAMES,
|
| 325 |
+
text_temperature=0.0,
|
| 326 |
+
text_top_p=1.0,
|
| 327 |
+
text_top_k=None,
|
| 328 |
+
audio_temperature=clean_temperature,
|
| 329 |
+
audio_top_p=clean_top_p,
|
| 330 |
+
audio_top_k=clean_top_k if clean_top_k > 0 else None,
|
| 331 |
+
audio_repetition_penalty=clean_rep_pen,
|
| 332 |
+
n_vq_for_inference=32,
|
| 333 |
+
style_text=clean_style_text or clean_text,
|
| 334 |
+
style_emotion_id=emotion_lookup[emotion_label],
|
| 335 |
+
style_energy=clean_energy,
|
| 336 |
+
)
|
| 337 |
+
elapsed = time.perf_counter() - start
|
| 338 |
+
|
| 339 |
+
# Write to a temp file (concurrency-safe — no fixed output paths).
|
| 340 |
+
tmp_wav = tempfile.NamedTemporaryFile(
|
| 341 |
+
suffix=".wav", delete=False, dir=str(BUILD_ROOT / "outputs")
|
| 342 |
+
)
|
| 343 |
+
tmp_wav.close()
|
| 344 |
+
output_path = Path(tmp_wav.name)
|
| 345 |
+
save_wav_pcm16(output_path, result["audio"], SAMPLE_RATE)
|
| 346 |
+
|
| 347 |
+
stats = {
|
| 348 |
+
"speaker": speaker_label,
|
| 349 |
+
"speaker_id": SPEAKER_IDS[speaker_label],
|
| 350 |
+
"emotion": emotion_label,
|
| 351 |
+
"style_energy": clean_energy,
|
| 352 |
+
"energy_label": energy_label(clean_energy),
|
| 353 |
+
"language": clean_language,
|
| 354 |
+
"text_chars": len(clean_text),
|
| 355 |
+
"audio_sec": round(result["audio_sec"], 3),
|
| 356 |
+
"prompt_sec": round(result["prompt_sec"], 3),
|
| 357 |
+
"generate_sec": round(result["generate_sec"], 3),
|
| 358 |
+
"decode_sec": round(result["decode_sec"], 3),
|
| 359 |
+
"total_sec": round(elapsed, 3),
|
| 360 |
+
"generate_x_realtime": round(result["generate_x_realtime"], 2),
|
| 361 |
+
"generated_tokens": result.get("generated_tokens", 0),
|
| 362 |
+
"prompt_tokens": result.get("prompt_tokens", 0),
|
| 363 |
+
"sample_rate": SAMPLE_RATE,
|
| 364 |
+
}
|
| 365 |
+
return str(output_path), json.dumps(stats, indent=2)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
# ── UI ────────────────────────────────────────────────────────────────────
|
| 369 |
+
|
| 370 |
+
CSS = """
|
| 371 |
+
#col-container { max-width: 1100px; margin: 0 auto; }
|
| 372 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 373 |
+
"""
|
| 374 |
+
|
| 375 |
+
with gr.Blocks(
|
| 376 |
+
title="MOSS-TTS-PNY",
|
| 377 |
+
theme=gr.themes.Citrus(),
|
| 378 |
+
css=CSS,
|
| 379 |
+
) as demo:
|
| 380 |
+
gr.Markdown(
|
| 381 |
+
"# 🎙️ MOSS-TTS-PNY\n"
|
| 382 |
+
"Speaker-conditioned text-to-speech with emotion and energy control. "
|
| 383 |
+
"Fine-tuned MOSS-TTS checkpoint featuring character voices.\n\n"
|
| 384 |
+
"Model: [ZDisket/MOSS-TTS-PNY](https://huggingface.co/ZDisket/MOSS-TTS-PNY)"
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
with gr.Row():
|
| 388 |
+
with gr.Column(scale=3):
|
| 389 |
+
text = gr.Textbox(
|
| 390 |
+
label="Text",
|
| 391 |
+
value="I wasn't expecting that to work, but now I'm kind of excited.",
|
| 392 |
+
lines=4,
|
| 393 |
+
max_length=MAX_TEXT_CHARS,
|
| 394 |
+
)
|
| 395 |
+
style_text = gr.Textbox(
|
| 396 |
+
label="Style text (optional)",
|
| 397 |
+
placeholder="Leave empty to use the same text as input.",
|
| 398 |
+
lines=2,
|
| 399 |
+
max_length=MAX_TEXT_CHARS,
|
| 400 |
+
)
|
| 401 |
+
with gr.Row():
|
| 402 |
+
randomize = gr.Button("Random Text")
|
| 403 |
+
generate = gr.Button("Generate", variant="primary")
|
| 404 |
+
|
| 405 |
+
with gr.Column(scale=2):
|
| 406 |
+
language = gr.Textbox(label="Language", value="en")
|
| 407 |
+
speaker = gr.Dropdown(
|
| 408 |
+
label="Speaker",
|
| 409 |
+
choices=SPEAKER_CHOICES,
|
| 410 |
+
value=DEFAULT_SPEAKER,
|
| 411 |
+
)
|
| 412 |
+
emotion = gr.Dropdown(
|
| 413 |
+
label="Emotion",
|
| 414 |
+
choices=[label for label, _ in EMOTION_CHOICES],
|
| 415 |
+
value="Neutral",
|
| 416 |
+
)
|
| 417 |
+
style_energy = gr.Slider(
|
| 418 |
+
label="Energy",
|
| 419 |
+
minimum=0.0,
|
| 420 |
+
maximum=1.0,
|
| 421 |
+
value=0.5,
|
| 422 |
+
step=0.05,
|
| 423 |
+
)
|
| 424 |
+
gr.Markdown(f"Max {MAX_OUTPUT_SECONDS}s of audio output")
|
| 425 |
+
|
| 426 |
+
with gr.Accordion("Advanced settings", open=False):
|
| 427 |
+
gr.Markdown("If you don't know what you're doing, leave these untouched.")
|
| 428 |
+
with gr.Row():
|
| 429 |
+
audio_temperature = gr.Slider(
|
| 430 |
+
label="Audio temperature", minimum=0.0, maximum=1.5, value=0.8, step=0.05
|
| 431 |
+
)
|
| 432 |
+
audio_top_p = gr.Slider(
|
| 433 |
+
label="Audio top-p", minimum=0.05, maximum=1.0, value=0.92, step=0.05
|
| 434 |
+
)
|
| 435 |
+
audio_top_k = gr.Slider(
|
| 436 |
+
label="Audio top-k", minimum=0, maximum=200, value=0, step=1
|
| 437 |
+
)
|
| 438 |
+
audio_repetition_penalty = gr.Slider(
|
| 439 |
+
label="Audio repetition penalty",
|
| 440 |
+
minimum=0.0,
|
| 441 |
+
maximum=2.0,
|
| 442 |
+
value=1.05,
|
| 443 |
+
step=0.05,
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
+
with gr.Row():
|
| 447 |
+
audio_output = gr.Audio(label="Generated audio", type="filepath", autoplay=True)
|
| 448 |
+
stats = gr.Code(label="Stats", language="json")
|
| 449 |
+
|
| 450 |
+
gr.Examples(
|
| 451 |
+
examples=[
|
| 452 |
+
["I wasn't expecting that to work, but now I'm kind of excited.", "", "en",
|
| 453 |
+
DEFAULT_SPEAKER, "Neutral", 0.5, 0.8, 0.92, 0, 1.05],
|
| 454 |
+
["You have no idea how happy that made me.", "", "en",
|
| 455 |
+
DEFAULT_SPEAKER, "Happy", 0.8, 0.8, 0.92, 0, 1.05],
|
| 456 |
+
["I'm trying to stay calm, but that was way closer than I expected.", "", "en",
|
| 457 |
+
DEFAULT_SPEAKER, "Fearful", 0.3, 0.8, 0.92, 0, 1.05],
|
| 458 |
+
["Okay, deep breath. We can fix this if we take it one step at a time.", "", "en",
|
| 459 |
+
DEFAULT_SPEAKER, "Calm", 0.4, 0.8, 0.92, 0, 1.05],
|
| 460 |
+
],
|
| 461 |
+
inputs=[
|
| 462 |
+
text, style_text, language, speaker, emotion, style_energy,
|
| 463 |
+
audio_temperature, audio_top_p, audio_top_k, audio_repetition_penalty,
|
| 464 |
+
],
|
| 465 |
+
outputs=[audio_output, stats],
|
| 466 |
+
fn=synthesize,
|
| 467 |
+
cache_examples=True,
|
| 468 |
+
cache_mode="lazy",
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
randomize.click(_random_text, inputs=[], outputs=[text], queue=False)
|
| 472 |
+
generate.click(
|
| 473 |
+
synthesize,
|
| 474 |
+
inputs=[
|
| 475 |
+
text, style_text, language, speaker, emotion, style_energy,
|
| 476 |
+
audio_temperature, audio_top_p, audio_top_k, audio_repetition_penalty,
|
| 477 |
+
],
|
| 478 |
+
outputs=[audio_output, stats],
|
| 479 |
+
api_name="generate",
|
| 480 |
+
)
|
| 481 |
+
|
| 482 |
+
if __name__ == "__main__":
|
| 483 |
+
demo.queue(default_concurrency_limit=1, max_size=20)
|
| 484 |
+
demo.launch(mcp_server=True, show_error=False)
|
decoder4_features_torch.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Decoder4FeatureExtractor(nn.Module):
|
| 8 |
+
"""MOSS audio-tokenizer codebook decode plus decoder blocks 0..4."""
|
| 9 |
+
|
| 10 |
+
def __init__(
|
| 11 |
+
self,
|
| 12 |
+
audio_tokenizer: nn.Module,
|
| 13 |
+
num_quantizers: int = 32,
|
| 14 |
+
output_dtype: torch.dtype = torch.float16,
|
| 15 |
+
) -> None:
|
| 16 |
+
super().__init__()
|
| 17 |
+
quantizer = getattr(audio_tokenizer, "quantizer")
|
| 18 |
+
self.quantizers = quantizer.quantizers
|
| 19 |
+
self.output_proj = quantizer.output_proj
|
| 20 |
+
self.decoder_prefix = nn.ModuleList(list(audio_tokenizer.decoder[:5]))
|
| 21 |
+
self.rvq_dim = int(quantizer.rvq_dim)
|
| 22 |
+
self.num_quantizers = int(num_quantizers)
|
| 23 |
+
self.output_dtype = output_dtype
|
| 24 |
+
|
| 25 |
+
def forward(self, codes: torch.Tensor, lengths: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 26 |
+
_, batch, frames = codes.shape
|
| 27 |
+
emb = torch.zeros(batch, self.rvq_dim, frames, device=codes.device, dtype=self.output_dtype)
|
| 28 |
+
for index, quantizer in enumerate(self.quantizers[: self.num_quantizers]):
|
| 29 |
+
if self.output_dtype == torch.float16:
|
| 30 |
+
z_q = quantizer.embed_code(codes[index]).transpose(1, 2).to(self.output_dtype)
|
| 31 |
+
z_q = quantizer.out_proj(z_q)
|
| 32 |
+
else:
|
| 33 |
+
z_q = quantizer.decode_code(codes[index])
|
| 34 |
+
emb = emb + z_q.to(self.output_dtype)
|
| 35 |
+
features = self.output_proj(emb)
|
| 36 |
+
feature_lengths = lengths
|
| 37 |
+
for module in self.decoder_prefix:
|
| 38 |
+
features, feature_lengths = module(features, feature_lengths)
|
| 39 |
+
return features.to(self.output_dtype), feature_lengths
|
portable_tts_runtime.py
ADDED
|
@@ -0,0 +1,800 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import gc
|
| 5 |
+
import importlib.util
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import site
|
| 9 |
+
import sys
|
| 10 |
+
import time
|
| 11 |
+
import wave
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from types import SimpleNamespace
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
import torch
|
| 19 |
+
from transformers import AutoConfig, AutoModel, AutoTokenizer, TorchAoConfig
|
| 20 |
+
|
| 21 |
+
from decoder4_features_torch import Decoder4FeatureExtractor
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
BUNDLE_ROOT = Path(__file__).resolve().parent
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _candidate_roots() -> list[Path]:
|
| 28 |
+
roots = [
|
| 29 |
+
BUNDLE_ROOT,
|
| 30 |
+
BUNDLE_ROOT.parent,
|
| 31 |
+
Path.cwd(),
|
| 32 |
+
Path.cwd() / "moss_tts_clipper_istftnet2_release",
|
| 33 |
+
BUNDLE_ROOT.parent / "moss_tts_clipper_istftnet2_release",
|
| 34 |
+
]
|
| 35 |
+
seen: set[Path] = set()
|
| 36 |
+
unique: list[Path] = []
|
| 37 |
+
for root in roots:
|
| 38 |
+
resolved = root.resolve()
|
| 39 |
+
if resolved not in seen:
|
| 40 |
+
seen.add(resolved)
|
| 41 |
+
unique.append(resolved)
|
| 42 |
+
return unique
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def resolve_asset(value: str | Path) -> Path:
|
| 46 |
+
path = Path(value)
|
| 47 |
+
if path.exists():
|
| 48 |
+
return path.resolve()
|
| 49 |
+
for root in _candidate_roots():
|
| 50 |
+
candidate = root / path
|
| 51 |
+
if candidate.exists():
|
| 52 |
+
return candidate.resolve()
|
| 53 |
+
return path
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def add_release_root_to_syspath(checkpoint: str | Path) -> Path:
|
| 57 |
+
checkpoint_path = resolve_asset(checkpoint)
|
| 58 |
+
release_root = checkpoint_path.parent
|
| 59 |
+
release_root_str = str(release_root)
|
| 60 |
+
if release_root_str not in sys.path:
|
| 61 |
+
sys.path.insert(0, release_root_str)
|
| 62 |
+
return checkpoint_path
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def ensure_nvidia_library_path() -> None:
|
| 66 |
+
if os.environ.get("MOSS_TTS_NVIDIA_LD_LIBRARY_PATH_READY") == "1":
|
| 67 |
+
return
|
| 68 |
+
lib_dirs: list[str] = []
|
| 69 |
+
site_roots = list(dict.fromkeys(site.getsitepackages() + [site.getusersitepackages()]))
|
| 70 |
+
for site_root in site_roots:
|
| 71 |
+
nvidia_root = Path(site_root) / "nvidia"
|
| 72 |
+
if not nvidia_root.exists():
|
| 73 |
+
continue
|
| 74 |
+
for lib_dir in nvidia_root.glob("*/lib"):
|
| 75 |
+
if lib_dir.is_dir():
|
| 76 |
+
lib_dirs.append(str(lib_dir))
|
| 77 |
+
if not lib_dirs:
|
| 78 |
+
return
|
| 79 |
+
current = os.environ.get("LD_LIBRARY_PATH", "")
|
| 80 |
+
current_parts = [part for part in current.split(":") if part]
|
| 81 |
+
wanted = [part for part in lib_dirs if part not in current_parts]
|
| 82 |
+
if not wanted:
|
| 83 |
+
os.environ["MOSS_TTS_NVIDIA_LD_LIBRARY_PATH_READY"] = "1"
|
| 84 |
+
return
|
| 85 |
+
os.environ["LD_LIBRARY_PATH"] = ":".join(wanted + current_parts)
|
| 86 |
+
os.environ["MOSS_TTS_NVIDIA_LD_LIBRARY_PATH_READY"] = "1"
|
| 87 |
+
os.execv(sys.executable, [sys.executable, *sys.argv])
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def choose_providers(requested: str, available: list[str]) -> list[str]:
|
| 91 |
+
return [requested] if requested in available else ["CPUExecutionProvider"]
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def select_dtype(device: torch.device, dtype_arg: str) -> torch.dtype:
|
| 95 |
+
if device.type != "cuda":
|
| 96 |
+
return torch.float32
|
| 97 |
+
if dtype_arg == "fp16":
|
| 98 |
+
return torch.float16
|
| 99 |
+
if dtype_arg == "bf16":
|
| 100 |
+
return torch.bfloat16
|
| 101 |
+
if dtype_arg == "fp32":
|
| 102 |
+
return torch.float32
|
| 103 |
+
major, _minor = torch.cuda.get_device_capability(device)
|
| 104 |
+
return torch.bfloat16 if major >= 8 else torch.float16
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def save_wav_pcm16(path: Path, audio: torch.Tensor, sample_rate: int) -> None:
|
| 108 |
+
pcm = (audio.clamp(-1.0, 1.0) * 32767.0).to(torch.int16).cpu().numpy()
|
| 109 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 110 |
+
with wave.open(str(path), "wb") as handle:
|
| 111 |
+
handle.setnchannels(1)
|
| 112 |
+
handle.setsampwidth(2)
|
| 113 |
+
handle.setframerate(sample_rate)
|
| 114 |
+
handle.writeframes(pcm.tobytes())
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def summarize(values: list[float]) -> dict[str, float]:
|
| 118 |
+
if not values:
|
| 119 |
+
return {"mean": 0.0, "min": 0.0, "max": 0.0}
|
| 120 |
+
return {
|
| 121 |
+
"mean": sum(values) / len(values),
|
| 122 |
+
"min": min(values),
|
| 123 |
+
"max": max(values),
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def build_processor_without_audio_model(checkpoint: str | Path):
|
| 128 |
+
bundled_release_root = BUNDLE_ROOT.parent
|
| 129 |
+
if (bundled_release_root / "moss_tts_local_clipper_checkpoint").exists():
|
| 130 |
+
bundled_release_root_str = str(bundled_release_root)
|
| 131 |
+
if bundled_release_root_str not in sys.path:
|
| 132 |
+
sys.path.insert(0, bundled_release_root_str)
|
| 133 |
+
try:
|
| 134 |
+
from moss_tts_local_clipper_checkpoint.processing_moss_tts import MossTTSDelayProcessor
|
| 135 |
+
except ImportError:
|
| 136 |
+
processing_file = Path(checkpoint) / "processing_moss_tts.py"
|
| 137 |
+
if not processing_file.exists():
|
| 138 |
+
raise
|
| 139 |
+
spec = importlib.util.spec_from_file_location("moss_tts_checkpoint_processing", processing_file)
|
| 140 |
+
if spec is None or spec.loader is None:
|
| 141 |
+
raise ImportError(f"Could not load processor module from {processing_file}.")
|
| 142 |
+
module = importlib.util.module_from_spec(spec)
|
| 143 |
+
sys.modules[spec.name] = module
|
| 144 |
+
spec.loader.exec_module(module)
|
| 145 |
+
MossTTSDelayProcessor = module.MossTTSDelayProcessor
|
| 146 |
+
|
| 147 |
+
config = AutoConfig.from_pretrained(checkpoint, trust_remote_code=True)
|
| 148 |
+
tokenizer = AutoTokenizer.from_pretrained(checkpoint, trust_remote_code=True)
|
| 149 |
+
return MossTTSDelayProcessor(tokenizer=tokenizer, model_config=config)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def load_style_extractor_class(checkpoint: str | Path):
|
| 153 |
+
try:
|
| 154 |
+
from moss_tts_local.style_features import BertStyleFeatureExtractor
|
| 155 |
+
|
| 156 |
+
return BertStyleFeatureExtractor
|
| 157 |
+
except ImportError:
|
| 158 |
+
pass
|
| 159 |
+
|
| 160 |
+
style_file = Path(checkpoint) / "style_features.py"
|
| 161 |
+
if style_file.exists():
|
| 162 |
+
spec = importlib.util.spec_from_file_location("moss_tts_checkpoint_style_features", style_file)
|
| 163 |
+
if spec is None or spec.loader is None:
|
| 164 |
+
raise ImportError(f"Could not load style feature module from {style_file}.")
|
| 165 |
+
module = importlib.util.module_from_spec(spec)
|
| 166 |
+
sys.modules[spec.name] = module
|
| 167 |
+
spec.loader.exec_module(module)
|
| 168 |
+
return module.BertStyleFeatureExtractor
|
| 169 |
+
|
| 170 |
+
from moss_tts_local_clipper_checkpoint.style_features import BertStyleFeatureExtractor
|
| 171 |
+
|
| 172 |
+
return BertStyleFeatureExtractor
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
class TorchDecoder4FeatureRuntime:
|
| 176 |
+
def __init__(self, codec_path: str | Path, device: torch.device, dtype: torch.dtype) -> None:
|
| 177 |
+
if device.type != "cuda" or dtype != torch.float16:
|
| 178 |
+
raise RuntimeError("torch_fp16 decoder4 runtime requires CUDA fp16.")
|
| 179 |
+
audio_tokenizer = AutoModel.from_pretrained(codec_path, trust_remote_code=True).to(
|
| 180 |
+
device=device,
|
| 181 |
+
dtype=dtype,
|
| 182 |
+
)
|
| 183 |
+
audio_tokenizer.eval()
|
| 184 |
+
num_quantizers = int(audio_tokenizer.config.quantizer_kwargs.get("num_quantizers", 32))
|
| 185 |
+
self.extractor = Decoder4FeatureExtractor(
|
| 186 |
+
audio_tokenizer,
|
| 187 |
+
num_quantizers=num_quantizers,
|
| 188 |
+
output_dtype=dtype,
|
| 189 |
+
).to(device=device, dtype=dtype)
|
| 190 |
+
self.extractor.eval()
|
| 191 |
+
self.device = device
|
| 192 |
+
self.inputs = [SimpleNamespace(name="codes"), SimpleNamespace(name="lengths")]
|
| 193 |
+
del audio_tokenizer
|
| 194 |
+
gc.collect()
|
| 195 |
+
torch.cuda.empty_cache()
|
| 196 |
+
|
| 197 |
+
def get_inputs(self) -> list[Any]:
|
| 198 |
+
return self.inputs
|
| 199 |
+
|
| 200 |
+
def get_providers(self) -> list[str]:
|
| 201 |
+
return ["torch_fp16_cuda"]
|
| 202 |
+
|
| 203 |
+
def run(self, output_names: Any, feeds: dict[str, np.ndarray]) -> list[np.ndarray]:
|
| 204 |
+
del output_names
|
| 205 |
+
codes = torch.from_numpy(feeds["codes"]).to(device=self.device, dtype=torch.long)
|
| 206 |
+
lengths = torch.from_numpy(feeds["lengths"]).to(device=self.device, dtype=torch.long)
|
| 207 |
+
with torch.inference_mode():
|
| 208 |
+
features, feature_lengths = self.extractor(codes, lengths)
|
| 209 |
+
if self.device.type == "cuda":
|
| 210 |
+
torch.cuda.synchronize()
|
| 211 |
+
return [
|
| 212 |
+
features.detach().float().cpu().numpy(),
|
| 213 |
+
feature_lengths.detach().cpu().numpy(),
|
| 214 |
+
]
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def run_vocoder_onnx(session: Any, features: np.ndarray, feature_lengths: np.ndarray) -> torch.Tensor:
|
| 218 |
+
input_name = session.get_inputs()[0].name
|
| 219 |
+
audio_np = session.run(None, {input_name: features.astype(np.float32, copy=False)})[0]
|
| 220 |
+
samples = int(feature_lengths[0]) * 960
|
| 221 |
+
audio = torch.from_numpy(audio_np[0, 0, :samples]).float().cpu()
|
| 222 |
+
return torch.nan_to_num(audio).reshape(-1).clamp(-1.0, 1.0)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
class TorchScriptVocoderRuntime:
|
| 226 |
+
def __init__(
|
| 227 |
+
self,
|
| 228 |
+
decoder_dir: str | Path,
|
| 229 |
+
device: torch.device,
|
| 230 |
+
*,
|
| 231 |
+
use_cudagraph: bool = False,
|
| 232 |
+
bucket_frames: int = 0,
|
| 233 |
+
) -> None:
|
| 234 |
+
artifact = Path(decoder_dir) / (
|
| 235 |
+
"istftnet2_decoder_cuda.ts" if device.type == "cuda" else "istftnet2_decoder_cpu.ts"
|
| 236 |
+
)
|
| 237 |
+
if not artifact.exists():
|
| 238 |
+
raise FileNotFoundError(f"Missing TorchScript vocoder artifact: {artifact}")
|
| 239 |
+
self.device = device
|
| 240 |
+
self.module = torch.jit.load(str(artifact), map_location=device).eval()
|
| 241 |
+
self.use_cudagraph = bool(use_cudagraph and device.type == "cuda")
|
| 242 |
+
self.bucket_frames = max(0, int(bucket_frames))
|
| 243 |
+
self.graphs: dict[tuple[Any, ...], tuple[torch.cuda.CUDAGraph, torch.Tensor, torch.Tensor]] = {}
|
| 244 |
+
|
| 245 |
+
def get_inputs(self) -> list[Any]:
|
| 246 |
+
return [SimpleNamespace(name="features")]
|
| 247 |
+
|
| 248 |
+
def get_providers(self) -> list[str]:
|
| 249 |
+
suffix = "_cudagraph" if self.use_cudagraph else ""
|
| 250 |
+
return [f"torchscript_{self.device.type}{suffix}"]
|
| 251 |
+
|
| 252 |
+
def prewarm_buckets(self, frame_lengths: list[int]) -> dict[str, Any]:
|
| 253 |
+
requested = [int(length) for length in frame_lengths if int(length) > 0]
|
| 254 |
+
if not requested:
|
| 255 |
+
return {"requested": [], "elapsed_sec": 0.0}
|
| 256 |
+
if self.device.type == "cuda":
|
| 257 |
+
torch.cuda.synchronize(self.device)
|
| 258 |
+
start = time.perf_counter()
|
| 259 |
+
warmed: list[int] = []
|
| 260 |
+
with torch.inference_mode():
|
| 261 |
+
for frames in requested:
|
| 262 |
+
features = torch.randn(1, 768, frames, device=self.device, dtype=torch.float32)
|
| 263 |
+
self.run_tensor(features)
|
| 264 |
+
warmed.append(frames)
|
| 265 |
+
if self.device.type == "cuda":
|
| 266 |
+
torch.cuda.synchronize(self.device)
|
| 267 |
+
return {"requested": requested, "warmed": warmed, "elapsed_sec": time.perf_counter() - start}
|
| 268 |
+
|
| 269 |
+
def run_tensor(self, features_tensor: torch.Tensor) -> torch.Tensor:
|
| 270 |
+
features_tensor = features_tensor.to(device=self.device, dtype=torch.float32).contiguous()
|
| 271 |
+
if self.bucket_frames > 0 and features_tensor.ndim == 3:
|
| 272 |
+
frames = int(features_tensor.shape[-1])
|
| 273 |
+
bucketed = ((frames + self.bucket_frames - 1) // self.bucket_frames) * self.bucket_frames
|
| 274 |
+
if bucketed > frames:
|
| 275 |
+
padded = torch.zeros(
|
| 276 |
+
*features_tensor.shape[:-1],
|
| 277 |
+
bucketed,
|
| 278 |
+
device=features_tensor.device,
|
| 279 |
+
dtype=features_tensor.dtype,
|
| 280 |
+
)
|
| 281 |
+
padded[..., :frames] = features_tensor
|
| 282 |
+
features_tensor = padded
|
| 283 |
+
if not self.use_cudagraph:
|
| 284 |
+
with torch.inference_mode():
|
| 285 |
+
return self.module(features_tensor)
|
| 286 |
+
key = (
|
| 287 |
+
features_tensor.device.index,
|
| 288 |
+
features_tensor.dtype,
|
| 289 |
+
tuple(features_tensor.shape),
|
| 290 |
+
)
|
| 291 |
+
entry = self.graphs.get(key)
|
| 292 |
+
if entry is None:
|
| 293 |
+
try:
|
| 294 |
+
static_features = torch.empty_like(features_tensor)
|
| 295 |
+
static_features.copy_(features_tensor)
|
| 296 |
+
warmup_stream = torch.cuda.Stream(device=features_tensor.device)
|
| 297 |
+
warmup_stream.wait_stream(torch.cuda.current_stream(features_tensor.device))
|
| 298 |
+
with torch.cuda.stream(warmup_stream), torch.inference_mode():
|
| 299 |
+
for _ in range(3):
|
| 300 |
+
static_audio = self.module(static_features)
|
| 301 |
+
torch.cuda.current_stream(features_tensor.device).wait_stream(warmup_stream)
|
| 302 |
+
|
| 303 |
+
graph = torch.cuda.CUDAGraph()
|
| 304 |
+
with torch.cuda.graph(graph), torch.inference_mode():
|
| 305 |
+
static_audio = self.module(static_features)
|
| 306 |
+
entry = (graph, static_features, static_audio)
|
| 307 |
+
self.graphs[key] = entry
|
| 308 |
+
except Exception:
|
| 309 |
+
self.use_cudagraph = False
|
| 310 |
+
self.graphs.clear()
|
| 311 |
+
try:
|
| 312 |
+
torch.cuda.synchronize(features_tensor.device)
|
| 313 |
+
except Exception:
|
| 314 |
+
pass
|
| 315 |
+
with torch.inference_mode():
|
| 316 |
+
return self.module(features_tensor)
|
| 317 |
+
|
| 318 |
+
graph, static_features, static_audio = entry
|
| 319 |
+
static_features.copy_(features_tensor)
|
| 320 |
+
graph.replay()
|
| 321 |
+
return static_audio
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def run_vocoder(
|
| 325 |
+
session: Any,
|
| 326 |
+
features: np.ndarray | torch.Tensor,
|
| 327 |
+
feature_lengths: np.ndarray | torch.Tensor,
|
| 328 |
+
) -> torch.Tensor:
|
| 329 |
+
if isinstance(session, TorchScriptVocoderRuntime):
|
| 330 |
+
if isinstance(features, np.ndarray):
|
| 331 |
+
features_tensor = torch.from_numpy(features).to(device=session.device, dtype=torch.float32)
|
| 332 |
+
else:
|
| 333 |
+
features_tensor = features.to(device=session.device, dtype=torch.float32)
|
| 334 |
+
with torch.inference_mode():
|
| 335 |
+
audio_tensor = session.run_tensor(features_tensor)
|
| 336 |
+
if session.device.type == "cuda":
|
| 337 |
+
torch.cuda.synchronize()
|
| 338 |
+
if isinstance(feature_lengths, torch.Tensor):
|
| 339 |
+
samples = int(feature_lengths[0].item()) * 960
|
| 340 |
+
else:
|
| 341 |
+
samples = int(feature_lengths[0]) * 960
|
| 342 |
+
audio = audio_tensor[0, 0, :samples].detach().float().cpu()
|
| 343 |
+
return torch.nan_to_num(audio).reshape(-1).clamp(-1.0, 1.0)
|
| 344 |
+
|
| 345 |
+
if isinstance(features, torch.Tensor):
|
| 346 |
+
features = features.detach().cpu().numpy()
|
| 347 |
+
if isinstance(feature_lengths, torch.Tensor):
|
| 348 |
+
feature_lengths = feature_lengths.detach().cpu().numpy()
|
| 349 |
+
return run_vocoder_onnx(session, features, feature_lengths)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def parse_int_csv(value: str) -> list[int]:
|
| 353 |
+
values: list[int] = []
|
| 354 |
+
for part in value.split(","):
|
| 355 |
+
part = part.strip()
|
| 356 |
+
if not part:
|
| 357 |
+
continue
|
| 358 |
+
values.append(int(part))
|
| 359 |
+
return values
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def enable_static_cache_for_global_model(model: Any) -> None:
|
| 363 |
+
type(model)._can_compile_fullgraph = True
|
| 364 |
+
language_config = model.config.language_config
|
| 365 |
+
for field in (
|
| 366 |
+
"max_position_embeddings",
|
| 367 |
+
"hidden_size",
|
| 368 |
+
"num_attention_heads",
|
| 369 |
+
"head_dim",
|
| 370 |
+
"num_key_value_heads",
|
| 371 |
+
"sliding_window",
|
| 372 |
+
"layer_types",
|
| 373 |
+
"num_hidden_layers",
|
| 374 |
+
):
|
| 375 |
+
if hasattr(language_config, field):
|
| 376 |
+
setattr(model.config, field, getattr(language_config, field))
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
def configure_torch_compile() -> None:
|
| 380 |
+
import importlib
|
| 381 |
+
import torch._inductor.config as inductor_config
|
| 382 |
+
|
| 383 |
+
dynamo_config = importlib.import_module("torch._dynamo.config")
|
| 384 |
+
dynamo_config.cache_size_limit = 256
|
| 385 |
+
dynamo_config.capture_scalar_outputs = True
|
| 386 |
+
inductor_config.triton.cudagraphs = False
|
| 387 |
+
inductor_config.triton.cudagraph_trees = False
|
| 388 |
+
inductor_config.triton.cudagraph_skip_dynamic_graphs = True
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
@dataclass
|
| 392 |
+
class OptimizedTTSConfig:
|
| 393 |
+
checkpoint: str | Path = "moss_tts_local_clipper_checkpoint"
|
| 394 |
+
codec_path: str | Path = "moss_audio_tokenizer"
|
| 395 |
+
decoder_dir: str | Path = "istftnet2_decoder4_50hz"
|
| 396 |
+
decoder4_features_onnx: str | Path = "ort_sessions/decoder4_features_fp32/model.onnx"
|
| 397 |
+
decoder_runtime: str = "torchscript_cuda"
|
| 398 |
+
vocoder_cudagraph: bool = False
|
| 399 |
+
vocoder_bucket_frames: int = 0
|
| 400 |
+
vocoder_prewarm_buckets: str = ""
|
| 401 |
+
decoder4_features_runtime: str = "torch_fp16"
|
| 402 |
+
decoder4_provider: str = "CUDAExecutionProvider"
|
| 403 |
+
dtype: str = "fp16"
|
| 404 |
+
tts_quantization: str = "none"
|
| 405 |
+
torch_opt_mode: str = "static-local-cache-triton-compile-cudagraph"
|
| 406 |
+
compile_mode: str = "max-autotune-no-cudagraphs"
|
| 407 |
+
cache_implementation: str = "static"
|
| 408 |
+
compile_global_transformer: bool = True
|
| 409 |
+
global_compile_mode: str = "default"
|
| 410 |
+
attn_implementation: str = "sdpa"
|
| 411 |
+
fast_prepare_inputs: bool = True
|
| 412 |
+
tensorrt_local: bool = False
|
| 413 |
+
triton_top_p: bool = False
|
| 414 |
+
triton_fused_lm_head: bool = False
|
| 415 |
+
triton_qkv_cache: bool = False
|
| 416 |
+
packed_local_qkv: bool = False
|
| 417 |
+
packed_local_mlp: bool = False
|
| 418 |
+
packed_adapter_mlp: bool = False
|
| 419 |
+
packed_adapter_mlp_scope: str = "heads"
|
| 420 |
+
static_packed_weights: bool = False
|
| 421 |
+
triton_rmsnorm: bool = False
|
| 422 |
+
tensorize_rmsnorm_eps: bool = True
|
| 423 |
+
fast_control_head: bool = False
|
| 424 |
+
feedback_lookup: bool = True
|
| 425 |
+
local_compile_fullgraph: bool = False
|
| 426 |
+
style_bert_model: str = "cirimus/modernbert-base-go-emotions"
|
| 427 |
+
style_bert_layer: int = 19
|
| 428 |
+
style_bert_max_length: int = 512
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
class OptimizedTTSRunner:
|
| 432 |
+
def __init__(self, config: OptimizedTTSConfig) -> None:
|
| 433 |
+
ensure_nvidia_library_path()
|
| 434 |
+
torch.backends.cudnn.benchmark = False
|
| 435 |
+
try:
|
| 436 |
+
torch.backends.cuda.enable_cudnn_sdp(False)
|
| 437 |
+
torch.backends.cuda.enable_flash_sdp(True)
|
| 438 |
+
torch.backends.cuda.enable_mem_efficient_sdp(True)
|
| 439 |
+
torch.backends.cuda.enable_math_sdp(True)
|
| 440 |
+
except Exception:
|
| 441 |
+
pass
|
| 442 |
+
|
| 443 |
+
checkpoint = add_release_root_to_syspath(config.checkpoint)
|
| 444 |
+
codec_path = resolve_asset(config.codec_path)
|
| 445 |
+
decoder_dir = resolve_asset(config.decoder_dir)
|
| 446 |
+
decoder4_features_onnx = resolve_asset(config.decoder4_features_onnx)
|
| 447 |
+
|
| 448 |
+
from torch_hf_optimizations import install_torch_frame_sampler, tensorize_rmsnorm_eps
|
| 449 |
+
|
| 450 |
+
import onnxruntime as ort
|
| 451 |
+
|
| 452 |
+
self.config = config
|
| 453 |
+
self.checkpoint = checkpoint
|
| 454 |
+
self.codec_path = codec_path
|
| 455 |
+
self.decoder_dir = decoder_dir
|
| 456 |
+
self.decoder4_features_onnx = decoder4_features_onnx
|
| 457 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 458 |
+
self.dtype = select_dtype(self.device, config.dtype)
|
| 459 |
+
|
| 460 |
+
wants_cuda = (
|
| 461 |
+
config.decoder4_provider == "CUDAExecutionProvider"
|
| 462 |
+
or config.decoder_runtime == "onnx_cuda"
|
| 463 |
+
or config.decoder4_features_runtime == "torch_fp16"
|
| 464 |
+
)
|
| 465 |
+
if wants_cuda and hasattr(ort, "preload_dlls"):
|
| 466 |
+
ort.preload_dlls(cuda=True, cudnn=True, directory="")
|
| 467 |
+
available_providers = ort.get_available_providers()
|
| 468 |
+
session_options = ort.SessionOptions()
|
| 469 |
+
session_options.log_severity_level = 2
|
| 470 |
+
self.available_ort_providers = available_providers
|
| 471 |
+
|
| 472 |
+
self.processor = build_processor_without_audio_model(checkpoint)
|
| 473 |
+
self.processor.model_config.sampling_rate = 48000
|
| 474 |
+
if config.decoder4_features_runtime == "onnx":
|
| 475 |
+
self.decoder4_session = ort.InferenceSession(
|
| 476 |
+
str(decoder4_features_onnx),
|
| 477 |
+
sess_options=session_options,
|
| 478 |
+
providers=choose_providers(config.decoder4_provider, available_providers),
|
| 479 |
+
)
|
| 480 |
+
elif config.decoder4_features_runtime == "torch_fp16":
|
| 481 |
+
self.decoder4_session = TorchDecoder4FeatureRuntime(codec_path, device=self.device, dtype=torch.float16)
|
| 482 |
+
else:
|
| 483 |
+
raise ValueError(f"Unsupported decoder4_features_runtime: {config.decoder4_features_runtime}")
|
| 484 |
+
|
| 485 |
+
if config.decoder_runtime.startswith("torchscript_"):
|
| 486 |
+
vocoder_device = torch.device(
|
| 487 |
+
"cuda" if config.decoder_runtime == "torchscript_cuda" and torch.cuda.is_available() else "cpu"
|
| 488 |
+
)
|
| 489 |
+
self.vocoder_session = TorchScriptVocoderRuntime(
|
| 490 |
+
decoder_dir,
|
| 491 |
+
vocoder_device,
|
| 492 |
+
use_cudagraph=config.vocoder_cudagraph,
|
| 493 |
+
bucket_frames=config.vocoder_bucket_frames,
|
| 494 |
+
)
|
| 495 |
+
else:
|
| 496 |
+
vocoder_provider = "CUDAExecutionProvider" if config.decoder_runtime == "onnx_cuda" else "CPUExecutionProvider"
|
| 497 |
+
self.vocoder_session = ort.InferenceSession(
|
| 498 |
+
str(decoder_dir / "istftnet2_decoder.onnx"),
|
| 499 |
+
sess_options=session_options,
|
| 500 |
+
providers=choose_providers(vocoder_provider, available_providers),
|
| 501 |
+
)
|
| 502 |
+
self.vocoder_prewarm_result: dict[str, Any] | None = None
|
| 503 |
+
if isinstance(self.vocoder_session, TorchScriptVocoderRuntime):
|
| 504 |
+
prewarm_buckets = parse_int_csv(config.vocoder_prewarm_buckets)
|
| 505 |
+
if prewarm_buckets:
|
| 506 |
+
self.vocoder_prewarm_result = self.vocoder_session.prewarm_buckets(prewarm_buckets)
|
| 507 |
+
tts_load_kwargs: dict[str, Any] = {
|
| 508 |
+
"trust_remote_code": True,
|
| 509 |
+
"torch_dtype": self.dtype,
|
| 510 |
+
"attn_implementation": config.attn_implementation,
|
| 511 |
+
}
|
| 512 |
+
if config.tts_quantization != "none":
|
| 513 |
+
if self.device.type != "cuda":
|
| 514 |
+
raise RuntimeError("TorchAO TTS quantization requires CUDA in this runner.")
|
| 515 |
+
modules_to_not_convert = None
|
| 516 |
+
if config.tts_quantization in {
|
| 517 |
+
"torchao_fp8_weight_only_global",
|
| 518 |
+
"torchao_fp8_dynamic_activation_fp8_weight_global",
|
| 519 |
+
}:
|
| 520 |
+
modules_to_not_convert = [
|
| 521 |
+
"local_transformer",
|
| 522 |
+
"speech_embedding_to_local_mlp",
|
| 523 |
+
"local_to_speech_embedding_mlps",
|
| 524 |
+
"layer_norm_before_lm_heads",
|
| 525 |
+
"lm_heads",
|
| 526 |
+
]
|
| 527 |
+
if config.tts_quantization == "torchao_fp8_weight_only_global":
|
| 528 |
+
quant_kind = "torchao_fp8_weight_only"
|
| 529 |
+
else:
|
| 530 |
+
quant_kind = "torchao_fp8_dynamic_activation_fp8_weight"
|
| 531 |
+
else:
|
| 532 |
+
quant_kind = config.tts_quantization
|
| 533 |
+
if quant_kind == "torchao_fp8_weight_only":
|
| 534 |
+
from torchao.quantization import Float8WeightOnlyConfig
|
| 535 |
+
|
| 536 |
+
tts_load_kwargs["quantization_config"] = TorchAoConfig(
|
| 537 |
+
Float8WeightOnlyConfig(),
|
| 538 |
+
modules_to_not_convert=modules_to_not_convert,
|
| 539 |
+
)
|
| 540 |
+
elif quant_kind == "torchao_fp8_dynamic_activation_fp8_weight":
|
| 541 |
+
from torchao.quantization import Float8DynamicActivationFloat8WeightConfig
|
| 542 |
+
|
| 543 |
+
tts_load_kwargs["quantization_config"] = TorchAoConfig(
|
| 544 |
+
Float8DynamicActivationFloat8WeightConfig(),
|
| 545 |
+
modules_to_not_convert=modules_to_not_convert,
|
| 546 |
+
)
|
| 547 |
+
else:
|
| 548 |
+
raise ValueError(f"Unsupported tts_quantization: {config.tts_quantization}")
|
| 549 |
+
tts_load_kwargs["device_map"] = {"": self.device.index or 0}
|
| 550 |
+
self.model = AutoModel.from_pretrained(checkpoint, **tts_load_kwargs)
|
| 551 |
+
if config.tts_quantization == "none":
|
| 552 |
+
self.model = self.model.to(self.device)
|
| 553 |
+
self.model.eval()
|
| 554 |
+
self.style_feature_dim = int(getattr(self.model.config, "style_feature_dim", 0) or 0)
|
| 555 |
+
self.num_emotions = int(getattr(self.model.config, "num_emotions", 0) or 0)
|
| 556 |
+
self.style_extractor = None
|
| 557 |
+
if self.style_feature_dim > 0 and self.style_feature_dim != 2:
|
| 558 |
+
BertStyleFeatureExtractor = load_style_extractor_class(checkpoint)
|
| 559 |
+
self.style_extractor = BertStyleFeatureExtractor(
|
| 560 |
+
repo_id=config.style_bert_model,
|
| 561 |
+
layer_index=config.style_bert_layer,
|
| 562 |
+
max_length=config.style_bert_max_length,
|
| 563 |
+
target_dim=self.style_feature_dim,
|
| 564 |
+
device=self.device,
|
| 565 |
+
dtype=self.dtype,
|
| 566 |
+
attn_implementation="sdpa" if self.device.type == "cuda" else "eager",
|
| 567 |
+
)
|
| 568 |
+
self.tensorized_rmsnorm_eps = 0
|
| 569 |
+
if config.tensorize_rmsnorm_eps:
|
| 570 |
+
self.tensorized_rmsnorm_eps = tensorize_rmsnorm_eps(self.model, device=self.device)
|
| 571 |
+
if config.compile_global_transformer:
|
| 572 |
+
configure_torch_compile()
|
| 573 |
+
if config.compile_global_transformer or config.cache_implementation == "static":
|
| 574 |
+
enable_static_cache_for_global_model(self.model)
|
| 575 |
+
if config.compile_global_transformer:
|
| 576 |
+
if config.global_compile_mode == "default":
|
| 577 |
+
global_compile_mode = None
|
| 578 |
+
global_compile_options = None
|
| 579 |
+
self.global_compile_mode_effective = "default"
|
| 580 |
+
else:
|
| 581 |
+
global_compile_mode = None
|
| 582 |
+
global_compile_options = {
|
| 583 |
+
"triton.cudagraphs": False,
|
| 584 |
+
"triton.cudagraph_trees": False,
|
| 585 |
+
"triton.cudagraph_skip_dynamic_graphs": True,
|
| 586 |
+
}
|
| 587 |
+
self.global_compile_mode_effective = "default-no-inductor-cudagraphs"
|
| 588 |
+
self.model.model.language_model = torch.compile(
|
| 589 |
+
self.model.model.language_model,
|
| 590 |
+
mode=global_compile_mode,
|
| 591 |
+
options=global_compile_options,
|
| 592 |
+
fullgraph=False,
|
| 593 |
+
dynamic=True,
|
| 594 |
+
)
|
| 595 |
+
else:
|
| 596 |
+
self.global_compile_mode_effective = "disabled"
|
| 597 |
+
install_torch_frame_sampler(
|
| 598 |
+
self.model,
|
| 599 |
+
mode=config.torch_opt_mode,
|
| 600 |
+
compile_mode=config.compile_mode,
|
| 601 |
+
packed_local_qkv=config.packed_local_qkv,
|
| 602 |
+
packed_local_mlp=config.packed_local_mlp,
|
| 603 |
+
packed_adapter_mlp=config.packed_adapter_mlp,
|
| 604 |
+
packed_adapter_mlp_scope=config.packed_adapter_mlp_scope,
|
| 605 |
+
static_packed_weights=config.static_packed_weights,
|
| 606 |
+
triton_rmsnorm=config.triton_rmsnorm,
|
| 607 |
+
tensorrt_local=config.tensorrt_local,
|
| 608 |
+
triton_top_p=config.triton_top_p,
|
| 609 |
+
triton_fused_lm_head=config.triton_fused_lm_head,
|
| 610 |
+
triton_qkv_cache=config.triton_qkv_cache,
|
| 611 |
+
fast_prepare_inputs=config.fast_prepare_inputs,
|
| 612 |
+
fast_control_head=config.fast_control_head,
|
| 613 |
+
feedback_lookup=config.feedback_lookup,
|
| 614 |
+
local_compile_fullgraph=config.local_compile_fullgraph,
|
| 615 |
+
)
|
| 616 |
+
self.sample_rate = int(self.processor.model_config.sampling_rate)
|
| 617 |
+
|
| 618 |
+
def decode_outputs(self, outputs: list[tuple[int, torch.Tensor]]) -> tuple[torch.Tensor, float]:
|
| 619 |
+
if torch.cuda.is_available():
|
| 620 |
+
torch.cuda.synchronize()
|
| 621 |
+
start = time.perf_counter()
|
| 622 |
+
decoded_segments: list[torch.Tensor] = []
|
| 623 |
+
codes_input_name = self.decoder4_session.get_inputs()[0].name
|
| 624 |
+
lengths_input_name = self.decoder4_session.get_inputs()[1].name
|
| 625 |
+
for start_length, generation_ids in outputs:
|
| 626 |
+
frame_tokens = generation_ids.detach().cpu()[:, 0]
|
| 627 |
+
audio_codes = generation_ids.detach().cpu()[:, 1:]
|
| 628 |
+
is_pad = (audio_codes == int(self.processor.model_config.audio_pad_code)).all(dim=1)
|
| 629 |
+
is_eos = frame_tokens == int(self.processor.model_config.audio_end_token_id)
|
| 630 |
+
non_pad = ~is_pad & ~is_eos
|
| 631 |
+
if not non_pad.any():
|
| 632 |
+
continue
|
| 633 |
+
idx = torch.nonzero(non_pad).squeeze(1)
|
| 634 |
+
breaks = torch.where(idx[1:] != idx[:-1] + 1)[0] + 1
|
| 635 |
+
segments_idx = [idx] if breaks.numel() == 0 else list(torch.split(idx, breaks.tolist()))
|
| 636 |
+
for segment_index, segment_idx in enumerate(segments_idx):
|
| 637 |
+
segment_codes = audio_codes[segment_idx].contiguous()
|
| 638 |
+
if int(segment_codes.shape[0]) <= 0:
|
| 639 |
+
continue
|
| 640 |
+
codes_np = segment_codes.T.unsqueeze(1).numpy().astype(np.int64, copy=False)
|
| 641 |
+
lengths_np = np.asarray([int(segment_codes.shape[0])], dtype=np.int64)
|
| 642 |
+
feeds = {codes_input_name: codes_np, lengths_input_name: lengths_np}
|
| 643 |
+
if hasattr(self.decoder4_session, "run_tensors"):
|
| 644 |
+
features, feature_lengths = self.decoder4_session.run_tensors(feeds)
|
| 645 |
+
else:
|
| 646 |
+
features, feature_lengths = self.decoder4_session.run(None, feeds)
|
| 647 |
+
segment_audio = run_vocoder(self.vocoder_session, features, feature_lengths)
|
| 648 |
+
if segment_index == 0 and int(start_length) > 0:
|
| 649 |
+
trim_ratio = max(0.0, min(float(start_length) / float(segment_codes.shape[0]), 1.0))
|
| 650 |
+
if trim_ratio >= 1.0:
|
| 651 |
+
continue
|
| 652 |
+
if trim_ratio > 0.0:
|
| 653 |
+
segment_audio = segment_audio[..., int(segment_audio.shape[-1] * trim_ratio) :]
|
| 654 |
+
decoded_segments.append(segment_audio)
|
| 655 |
+
if torch.cuda.is_available():
|
| 656 |
+
torch.cuda.synchronize()
|
| 657 |
+
elapsed = time.perf_counter() - start
|
| 658 |
+
if not decoded_segments:
|
| 659 |
+
raise RuntimeError("Generation did not produce decodable audio.")
|
| 660 |
+
return torch.cat(decoded_segments, dim=-1).float().cpu(), elapsed
|
| 661 |
+
|
| 662 |
+
def synthesize(
|
| 663 |
+
self,
|
| 664 |
+
*,
|
| 665 |
+
text: str,
|
| 666 |
+
language: str = "en",
|
| 667 |
+
speaker_id: int = 31,
|
| 668 |
+
max_new_tokens: int = 160,
|
| 669 |
+
text_temperature: float = 0.0,
|
| 670 |
+
text_top_p: float = 1.0,
|
| 671 |
+
text_top_k: int | None = None,
|
| 672 |
+
audio_temperature: float = 0.8,
|
| 673 |
+
audio_top_p: float = 0.92,
|
| 674 |
+
audio_top_k: int | None = None,
|
| 675 |
+
audio_repetition_penalty: float = 1.0,
|
| 676 |
+
n_vq_for_inference: int = 32,
|
| 677 |
+
style_text: str | None = None,
|
| 678 |
+
style_features: torch.Tensor | None = None,
|
| 679 |
+
style_emotion_id: int | None = None,
|
| 680 |
+
style_energy: float = 0.7,
|
| 681 |
+
) -> dict[str, Any]:
|
| 682 |
+
conversations = [[self.processor.build_user_message(text=text, language=language)]]
|
| 683 |
+
prompt_start = time.perf_counter()
|
| 684 |
+
batch = self.processor(conversations, mode="generation")
|
| 685 |
+
input_ids = batch["input_ids"].to(self.device)
|
| 686 |
+
attention_mask = batch["attention_mask"].to(self.device)
|
| 687 |
+
prompt_sec = time.perf_counter() - prompt_start
|
| 688 |
+
|
| 689 |
+
if self.device.type == "cuda":
|
| 690 |
+
torch.cuda.synchronize()
|
| 691 |
+
generate_start = time.perf_counter()
|
| 692 |
+
generation_kwargs = {}
|
| 693 |
+
if self.config.cache_implementation != "none":
|
| 694 |
+
generation_kwargs["cache_implementation"] = self.config.cache_implementation
|
| 695 |
+
speaker_tensor = torch.tensor([speaker_id], device=self.device, dtype=torch.long)
|
| 696 |
+
generation_kwargs["speaker_ids"] = speaker_tensor
|
| 697 |
+
if self.style_feature_dim > 0:
|
| 698 |
+
if style_features is None:
|
| 699 |
+
if self.style_feature_dim == 2 and style_emotion_id is not None:
|
| 700 |
+
max_emotion = max(0, (self.num_emotions or 1) - 1)
|
| 701 |
+
style_features = torch.tensor(
|
| 702 |
+
[
|
| 703 |
+
[
|
| 704 |
+
float(max(0, min(max_emotion, int(style_emotion_id)))),
|
| 705 |
+
float(max(0.0, min(1.0, style_energy))),
|
| 706 |
+
]
|
| 707 |
+
],
|
| 708 |
+
dtype=torch.float32,
|
| 709 |
+
)
|
| 710 |
+
elif self.style_extractor is None:
|
| 711 |
+
raise RuntimeError("Checkpoint expects style features, but no style extractor is available.")
|
| 712 |
+
else:
|
| 713 |
+
style_features = self.style_extractor.encode((style_text or text).strip())
|
| 714 |
+
generation_kwargs["style_features"] = style_features.to(device=self.device)
|
| 715 |
+
with torch.inference_mode():
|
| 716 |
+
outputs = self.model.generate(
|
| 717 |
+
input_ids=input_ids,
|
| 718 |
+
attention_mask=attention_mask,
|
| 719 |
+
max_new_tokens=max_new_tokens,
|
| 720 |
+
n_vq_for_inference=n_vq_for_inference,
|
| 721 |
+
text_temperature=text_temperature,
|
| 722 |
+
text_top_p=text_top_p,
|
| 723 |
+
text_top_k=text_top_k,
|
| 724 |
+
audio_temperature=audio_temperature,
|
| 725 |
+
audio_top_p=audio_top_p,
|
| 726 |
+
audio_top_k=None,
|
| 727 |
+
audio_repetition_penalty=audio_repetition_penalty,
|
| 728 |
+
**generation_kwargs,
|
| 729 |
+
)
|
| 730 |
+
if self.device.type == "cuda":
|
| 731 |
+
torch.cuda.synchronize()
|
| 732 |
+
generate_sec = time.perf_counter() - generate_start
|
| 733 |
+
|
| 734 |
+
audio, decode_sec = self.decode_outputs(outputs)
|
| 735 |
+
audio_sec = float(audio.numel() / self.sample_rate)
|
| 736 |
+
return {
|
| 737 |
+
"audio": audio,
|
| 738 |
+
"audio_sec": audio_sec,
|
| 739 |
+
"prompt_sec": prompt_sec,
|
| 740 |
+
"generate_sec": generate_sec,
|
| 741 |
+
"decode_sec": decode_sec,
|
| 742 |
+
"generate_x_realtime": audio_sec / generate_sec if generate_sec else 0.0,
|
| 743 |
+
"total_x_realtime": audio_sec / (generate_sec + decode_sec) if generate_sec + decode_sec else 0.0,
|
| 744 |
+
"generated_tokens": int(outputs[0][1].shape[0]) if outputs else 0,
|
| 745 |
+
"prompt_tokens": int(input_ids.shape[1]),
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
def runtime_summary(self) -> dict[str, Any]:
|
| 749 |
+
return {
|
| 750 |
+
"device": str(self.device),
|
| 751 |
+
"dtype": str(self.dtype),
|
| 752 |
+
"tts_quantization": self.config.tts_quantization,
|
| 753 |
+
"processor_loads_audio_tokenizer": False,
|
| 754 |
+
"audio_decode_runtime": f"{self.config.decoder4_features_runtime}_decoder4_features_plus_{self.config.decoder_runtime}_vocoder",
|
| 755 |
+
"checkpoint": str(self.checkpoint),
|
| 756 |
+
"codec_path": str(self.codec_path),
|
| 757 |
+
"decoder_dir": str(self.decoder_dir),
|
| 758 |
+
"decoder4_features_onnx": str(self.decoder4_features_onnx),
|
| 759 |
+
"vocoder_onnx": str(self.decoder_dir / "istftnet2_decoder.onnx"),
|
| 760 |
+
"decoder4_providers": self.decoder4_session.get_providers(),
|
| 761 |
+
"vocoder_providers": self.vocoder_session.get_providers(),
|
| 762 |
+
"available_ort_providers": self.available_ort_providers,
|
| 763 |
+
"torch_opt_mode": self.config.torch_opt_mode,
|
| 764 |
+
"compile_mode": self.config.compile_mode,
|
| 765 |
+
"cache_implementation": self.config.cache_implementation,
|
| 766 |
+
"compile_global_transformer": bool(self.config.compile_global_transformer),
|
| 767 |
+
"global_compile_mode": self.global_compile_mode_effective,
|
| 768 |
+
"fast_prepare_inputs": bool(self.config.fast_prepare_inputs),
|
| 769 |
+
"tensorrt_local": bool(self.config.tensorrt_local),
|
| 770 |
+
"triton_top_p": bool(self.config.triton_top_p),
|
| 771 |
+
"triton_fused_lm_head": bool(self.config.triton_fused_lm_head),
|
| 772 |
+
"triton_qkv_cache": bool(self.config.triton_qkv_cache),
|
| 773 |
+
"packed_local_qkv": bool(self.config.packed_local_qkv),
|
| 774 |
+
"packed_local_mlp": bool(self.config.packed_local_mlp),
|
| 775 |
+
"packed_adapter_mlp": bool(self.config.packed_adapter_mlp),
|
| 776 |
+
"packed_adapter_mlp_scope": self.config.packed_adapter_mlp_scope,
|
| 777 |
+
"static_packed_weights": bool(self.config.static_packed_weights),
|
| 778 |
+
"triton_rmsnorm": bool(self.config.triton_rmsnorm),
|
| 779 |
+
"tensorize_rmsnorm_eps": bool(self.config.tensorize_rmsnorm_eps),
|
| 780 |
+
"tensorized_rmsnorm_eps": int(self.tensorized_rmsnorm_eps),
|
| 781 |
+
"fast_control_head": bool(self.config.fast_control_head),
|
| 782 |
+
"feedback_lookup": bool(self.config.feedback_lookup),
|
| 783 |
+
"local_compile_fullgraph": bool(self.config.local_compile_fullgraph),
|
| 784 |
+
"style_feature_dim": int(self.style_feature_dim),
|
| 785 |
+
"num_emotions": int(self.num_emotions),
|
| 786 |
+
"vocoder_cudagraph": bool(self.config.vocoder_cudagraph),
|
| 787 |
+
"vocoder_bucket_frames": int(self.config.vocoder_bucket_frames),
|
| 788 |
+
"vocoder_prewarm_buckets": parse_int_csv(self.config.vocoder_prewarm_buckets),
|
| 789 |
+
"vocoder_prewarm_result": self.vocoder_prewarm_result,
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
|
| 793 |
+
def write_summary(path: Path, summary: dict[str, Any]) -> None:
|
| 794 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 795 |
+
serializable = {
|
| 796 |
+
key: value
|
| 797 |
+
for key, value in summary.items()
|
| 798 |
+
if not isinstance(value, torch.Tensor)
|
| 799 |
+
}
|
| 800 |
+
path.write_text(json.dumps(serializable, indent=2))
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transformers==4.55.0
|
| 2 |
+
onnxruntime-gpu==1.26.0
|
| 3 |
+
safetensors
|
| 4 |
+
numpy
|
speaker_hours_tiers.csv
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
tier,speaker_id,speaker,utterances,codec_hours,flac_hours,avg_sec,min_sec,max_sec,missing
|
| 2 |
+
1,0,Apple Bloom,1296,0.8802888888888889,0.8941135879629628,2.445246913580247,0.32,8.64,0
|
| 3 |
+
1,1,Applejack,3143,2.1064666666666665,2.1398366145833307,2.412752147629653,0.24,8.4,0
|
| 4 |
+
3,2,Autumn Blaze,161,0.11495555555555555,0.11679866898148142,2.5704347826086953,0.16,7.2,0
|
| 5 |
+
3,3,Big Mac,222,0.11515555555555555,0.11741705439814816,1.8673873873873874,0.32,7.12,0
|
| 6 |
+
3,4,Cadance,165,0.12504444444444446,0.12680443865740743,2.7282424242424246,0.64,9.6,0
|
| 7 |
+
2,5,Celestia,583,0.4552222222222222,0.46121357638888894,2.8109777015437394,0.48,10.56,0
|
| 8 |
+
3,6,Cheerilee,111,0.07991111111111111,0.08106452546296296,2.591711711711712,0.72,6.56,0
|
| 9 |
+
2,7,Chrysalis,178,0.1591111111111111,0.16089883101851857,3.2179775280898872,0.48,8.24,0
|
| 10 |
+
3,8,Cozy Glow,157,0.11946666666666667,0.1210344386574074,2.739363057324841,0.72,7.2,0
|
| 11 |
+
3,9,Diamond Tiara,102,0.08711111111111111,0.08823683449074078,3.074509803921569,0.8,8.4,0
|
| 12 |
+
2,10,Discord,476,0.38424444444444444,0.38949334490740734,2.906050420168067,0.56,9.12,0
|
| 13 |
+
1,11,Fluttershy,1792,1.2326666666666668,1.252261811342593,2.476339285714286,0.24,9.04,0
|
| 14 |
+
2,12,Goldie Delicious,177,0.15811111111111112,0.16000179398148143,3.215819209039548,0.96,7.44,0
|
| 15 |
+
2,13,Granny Smith,286,0.23762222222222223,0.24077664351851852,2.9910489510489513,0.64,9.36,0
|
| 16 |
+
3,14,Luna,191,0.1398888888888889,0.14183635416666662,2.636649214659686,0.56,7.28,0
|
| 17 |
+
3,15,Maud,177,0.09362222222222223,0.0954588541666667,1.904180790960452,0.48,8.32,0
|
| 18 |
+
3,16,Mrs. Cake,135,0.11806666666666667,0.11953124421296295,3.1484444444444444,0.72,7.04,0
|
| 19 |
+
1,17,Pinkie,2080,1.2596,1.2820753935185158,2.1800769230769235,0.24,8.8,0
|
| 20 |
+
1,18,Rainbow,2333,1.513088888888889,1.5377426851851859,2.3348135447921132,0.32,9.2,0
|
| 21 |
+
1,19,Rarity,2383,1.6880444444444445,1.71413467013889,2.550130088124213,0.16,12.16,0
|
| 22 |
+
1,20,Scootaloo,774,0.5134888888888889,0.5219100347222223,2.3883204134366927,0.4,8.32,0
|
| 23 |
+
3,21,Shining Armor,118,0.09484444444444444,0.09610426504629628,2.8935593220338984,0.4,11.28,0
|
| 24 |
+
1,22,Spike,1791,1.0443111111111112,1.0638599907486153,2.099117811278615,0.32,9.6,0
|
| 25 |
+
3,23,Spitfire,134,0.08724444444444444,0.08878343749999998,2.3438805970149255,0.4,6.16,0
|
| 26 |
+
1,24,Starlight,1125,0.8553777777777778,0.8673828676933746,2.737208888888889,0.24,8.56,0
|
| 27 |
+
3,25,Sunburst,198,0.131,0.1332123370575082,2.381818181818182,0.4,7.6,0
|
| 28 |
+
2,26,Sunset Shimmer,549,0.36691111111111113,0.3728748379629632,2.4059744990892535,0.48,7.6,0
|
| 29 |
+
1,27,Sweetie Belle,771,0.5104222222222222,0.5186199594907405,2.383294422827497,0.16,8.08,0
|
| 30 |
+
3,28,Thorax,124,0.08544444444444445,0.0869096238425926,2.480645161290323,0.64,7.04,0
|
| 31 |
+
2,29,Tirek,150,0.15304444444444446,0.1547323090277777,3.6730666666666667,0.88,8.16,0
|
| 32 |
+
1,30,Trixie,609,0.5680888888888889,0.5745289583333333,3.3581609195402295,0.4,9.12,0
|
| 33 |
+
1,31,Twilight,4628,3.041355555555555,3.0922718935027667,2.365790838375108,0.16,10.96,0
|
| 34 |
+
3,32,Zecora,111,0.10197777777777778,0.1031401215277778,3.3073873873873874,0.96,6.8,0
|
| 35 |
+
2,33,Demo,571,0.4166666666666667,0.42297333333333315,2.626970227670753,0.32,10.56,0
|
| 36 |
+
2,34,Engineer,665,0.35619999999999996,0.3636970312500003,1.928300751879699,0.24,9.04,0
|
| 37 |
+
1,35,Heavy,807,0.5254888888888889,0.5347120081018519,2.344188351920694,0.32,19.52,0
|
| 38 |
+
2,36,Medic,489,0.25353333333333333,0.2588444097222222,1.8665030674846625,0.24,6.4,0
|
| 39 |
+
1,37,Merasmus,739,1.079911111111111,1.0880798206018514,5.260730717185385,0.72,43.68,0
|
| 40 |
+
2,38,Pauling,317,0.20524444444444445,0.20856595486111115,2.3308517350157727,0.4,11.36,0
|
| 41 |
+
2,39,Scout,992,0.4975333333333333,0.5081581655092591,1.8055645161290321,0.16,15.28,0
|
| 42 |
+
2,40,Sniper,852,0.43008888888888885,0.43924168402777747,1.8172769953051642,0.24,7.52,0
|
| 43 |
+
1,41,Soldier,898,0.5638444444444444,0.5731967187500004,2.260400890868597,0.24,13.04,0
|
| 44 |
+
2,42,Spy,691,0.37537777777777775,0.38310017939814806,1.9556584659913168,0.32,9.92,0
|
speaker_id_map.csv
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
speaker_id,name,display_name,label,utterance_count
|
| 2 |
+
0,Apple Bloom,Apple Bloom,Apple Bloom (0),1296
|
| 3 |
+
1,Applejack,Applejack,Applejack (1),3143
|
| 4 |
+
2,Autumn Blaze,Autumn Blaze,Autumn Blaze (2),161
|
| 5 |
+
3,Big Mac,Big Mac,Big Mac (3),222
|
| 6 |
+
4,Cadance,Cadance,Cadance (4),165
|
| 7 |
+
5,Celestia,Celestia,Celestia (5),583
|
| 8 |
+
6,Cheerilee,Cheerilee,Cheerilee (6),111
|
| 9 |
+
7,Chrysalis,Chrysalis,Chrysalis (7),178
|
| 10 |
+
8,Cozy Glow,Cozy Glow,Cozy Glow (8),157
|
| 11 |
+
9,Diamond Tiara,Diamond Tiara,Diamond Tiara (9),102
|
| 12 |
+
10,Discord,Discord,Discord (10),476
|
| 13 |
+
11,Fluttershy,Fluttershy,Fluttershy (11),1792
|
| 14 |
+
12,Goldie Delicious,Goldie Delicious,Goldie Delicious (12),177
|
| 15 |
+
13,Granny Smith,Granny Smith,Granny Smith (13),286
|
| 16 |
+
14,Luna,Luna,Luna (14),191
|
| 17 |
+
15,Maud,Maud,Maud (15),177
|
| 18 |
+
16,Mrs. Cake,Mrs. Cake,Mrs. Cake (16),135
|
| 19 |
+
17,Pinkie,Pinkie,Pinkie (17),2080
|
| 20 |
+
18,Rainbow,Rainbow,Rainbow (18),2333
|
| 21 |
+
19,Rarity,Rarity,Rarity (19),2383
|
| 22 |
+
20,Scootaloo,Scootaloo,Scootaloo (20),774
|
| 23 |
+
21,Shining Armor,Shining Armor,Shining Armor (21),118
|
| 24 |
+
22,Spike,Spike,Spike (22),1791
|
| 25 |
+
23,Spitfire,Spitfire,Spitfire (23),134
|
| 26 |
+
24,Starlight,Starlight,Starlight (24),1125
|
| 27 |
+
25,Sunburst,Sunburst,Sunburst (25),198
|
| 28 |
+
26,Sunset Shimmer,Sunset Shimmer,Sunset Shimmer (26),549
|
| 29 |
+
27,Sweetie Belle,Sweetie Belle,Sweetie Belle (27),771
|
| 30 |
+
28,Thorax,Thorax,Thorax (28),124
|
| 31 |
+
29,Tirek,Tirek,Tirek (29),150
|
| 32 |
+
30,Trixie,Trixie,Trixie (30),609
|
| 33 |
+
31,Twilight,Twilight,Twilight (31),4628
|
| 34 |
+
32,Zecora,Zecora,Zecora (32),111
|
| 35 |
+
33,demo,Demo,Demo (33),571
|
| 36 |
+
34,engineer,Engineer,Engineer (34),665
|
| 37 |
+
35,heavy,Heavy,Heavy (35),807
|
| 38 |
+
36,medic,Medic,Medic (36),489
|
| 39 |
+
37,merasmus,Merasmus,Merasmus (37),739
|
| 40 |
+
38,pauling,Pauling,Pauling (38),317
|
| 41 |
+
39,scout,Scout,Scout (39),992
|
| 42 |
+
40,sniper,Sniper,Sniper (40),852
|
| 43 |
+
41,soldier,Soldier,Soldier (41),898
|
| 44 |
+
42,spy,Spy,Spy (42),691
|
torch_hf_optimizations.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|