Upload folder using huggingface_hub
Browse files- README.md +11 -6
- app.py +436 -0
- irodori_tts/__init__.py +16 -0
- irodori_tts/codec.py +282 -0
- irodori_tts/config.py +257 -0
- irodori_tts/duration.py +182 -0
- irodori_tts/inference_runtime.py +1063 -0
- irodori_tts/model.py +1491 -0
- irodori_tts/rf.py +542 -0
- irodori_tts/text_normalization.py +74 -0
- irodori_tts/tokenizer.py +144 -0
- irodori_tts/watermark.py +82 -0
- requirements.txt +12 -0
README.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
| 1 |
---
|
| 2 |
-
title: Irodori
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
pinned:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Irodori-TTS-500M-v3 Demo
|
| 3 |
+
emoji: 🎤
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.6.0
|
| 8 |
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
+
pinned: true
|
| 11 |
+
license: mit
|
| 12 |
+
short_description: TTS demo for Irodori-TTS-500M-v3
|
| 13 |
+
models:
|
| 14 |
+
- Aratako/Irodori-TTS-500M-v3
|
| 15 |
+
- Aratako/Semantic-DACVAE-Japanese-32dim
|
| 16 |
---
|
| 17 |
|
| 18 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
import gradio as gr
|
| 5 |
+
import numpy as np
|
| 6 |
+
import spaces
|
| 7 |
+
import torch
|
| 8 |
+
from huggingface_hub import hf_hub_download
|
| 9 |
+
|
| 10 |
+
from irodori_tts.inference_runtime import (
|
| 11 |
+
InferenceRuntime,
|
| 12 |
+
RuntimeKey,
|
| 13 |
+
SamplingRequest,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# ---------------------------------------------------------------------------
|
| 17 |
+
# Configuration
|
| 18 |
+
# ---------------------------------------------------------------------------
|
| 19 |
+
|
| 20 |
+
MODEL_REPO = os.environ.get("MODEL_REPO", "Aratako/Irodori-TTS-500M-v3")
|
| 21 |
+
CODEC_REPO = "Aratako/Semantic-DACVAE-Japanese-32dim"
|
| 22 |
+
MAX_GRADIO_CANDIDATES = int(os.environ.get("MAX_GRADIO_CANDIDATES", "32"))
|
| 23 |
+
GRADIO_AUDIO_COLS_PER_ROW = 8
|
| 24 |
+
|
| 25 |
+
# Global state
|
| 26 |
+
_runtime: InferenceRuntime | None = None
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
+
# Helpers
|
| 31 |
+
# ---------------------------------------------------------------------------
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _parse_optional_float(raw: str | None, label: str) -> float | None:
|
| 35 |
+
if raw is None:
|
| 36 |
+
return None
|
| 37 |
+
text = str(raw).strip()
|
| 38 |
+
if text == "" or text.lower() == "none":
|
| 39 |
+
return None
|
| 40 |
+
try:
|
| 41 |
+
return float(text)
|
| 42 |
+
except ValueError as exc:
|
| 43 |
+
raise ValueError(f"{label} must be a float or blank.") from exc
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _parse_optional_int(raw: str | None, label: str) -> int | None:
|
| 47 |
+
if raw is None:
|
| 48 |
+
return None
|
| 49 |
+
text = str(raw).strip()
|
| 50 |
+
if text == "" or text.lower() == "none":
|
| 51 |
+
return None
|
| 52 |
+
try:
|
| 53 |
+
return int(text)
|
| 54 |
+
except ValueError as exc:
|
| 55 |
+
raise ValueError(f"{label} must be an int or blank.") from exc
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _format_timings(stage_timings: list[tuple[str, float]], total_to_decode: float) -> str:
|
| 59 |
+
lines = [
|
| 60 |
+
"[timing] ---- request ----",
|
| 61 |
+
*[f"[timing] {name}: {sec * 1000.0:.1f} ms" for name, sec in stage_timings],
|
| 62 |
+
f"[timing] total_to_decode: {total_to_decode:.3f} s",
|
| 63 |
+
]
|
| 64 |
+
return "\n".join(lines)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# ---------------------------------------------------------------------------
|
| 68 |
+
# Model Loading
|
| 69 |
+
# ---------------------------------------------------------------------------
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def load_models():
|
| 73 |
+
global _runtime
|
| 74 |
+
|
| 75 |
+
if _runtime is not None:
|
| 76 |
+
return
|
| 77 |
+
|
| 78 |
+
print(f"[Info] Downloading checkpoint from {MODEL_REPO}...")
|
| 79 |
+
checkpoint_path = hf_hub_download(repo_id=MODEL_REPO, filename="model.safetensors")
|
| 80 |
+
|
| 81 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 82 |
+
precision = "bf16" if device == "cuda" else "fp32"
|
| 83 |
+
|
| 84 |
+
key = RuntimeKey(
|
| 85 |
+
checkpoint=checkpoint_path,
|
| 86 |
+
model_device=device,
|
| 87 |
+
codec_repo=CODEC_REPO,
|
| 88 |
+
model_precision=precision,
|
| 89 |
+
codec_device=device,
|
| 90 |
+
codec_precision=precision,
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
print("[Info] Building runtime...")
|
| 94 |
+
_runtime = InferenceRuntime.from_key(key)
|
| 95 |
+
print("[Info] All models loaded successfully.")
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# Load models at startup
|
| 99 |
+
load_models()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
# ---------------------------------------------------------------------------
|
| 103 |
+
# GPU-decorated Inference
|
| 104 |
+
# ---------------------------------------------------------------------------
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
@spaces.GPU(duration=120)
|
| 108 |
+
def run_inference_gpu(
|
| 109 |
+
text: str,
|
| 110 |
+
uploaded_audio: str | None,
|
| 111 |
+
num_steps: int,
|
| 112 |
+
num_candidates: int,
|
| 113 |
+
seed_raw: str,
|
| 114 |
+
seconds_raw: str,
|
| 115 |
+
duration_scale: float,
|
| 116 |
+
cfg_guidance_mode: str,
|
| 117 |
+
cfg_scale_text: float,
|
| 118 |
+
cfg_scale_speaker: float,
|
| 119 |
+
cfg_scale_raw: str,
|
| 120 |
+
cfg_min_t: float,
|
| 121 |
+
cfg_max_t: float,
|
| 122 |
+
context_kv_cache: bool,
|
| 123 |
+
truncation_factor_raw: str,
|
| 124 |
+
rescale_k_raw: str,
|
| 125 |
+
rescale_sigma_raw: str,
|
| 126 |
+
speaker_kv_scale_raw: str,
|
| 127 |
+
speaker_kv_min_t_raw: str,
|
| 128 |
+
speaker_kv_max_layers_raw: str,
|
| 129 |
+
) -> tuple[list[tuple[int, np.ndarray]], str]:
|
| 130 |
+
load_models()
|
| 131 |
+
|
| 132 |
+
log_buffer = io.StringIO()
|
| 133 |
+
|
| 134 |
+
def stdout_log(msg: str) -> None:
|
| 135 |
+
print(msg, flush=True)
|
| 136 |
+
log_buffer.write(msg + "\n")
|
| 137 |
+
|
| 138 |
+
if not str(text).strip():
|
| 139 |
+
raise gr.Error("Please enter text to synthesize.")
|
| 140 |
+
|
| 141 |
+
cfg_scale = _parse_optional_float(cfg_scale_raw, "cfg_scale")
|
| 142 |
+
truncation_factor = _parse_optional_float(truncation_factor_raw, "truncation_factor")
|
| 143 |
+
rescale_k = _parse_optional_float(rescale_k_raw, "rescale_k")
|
| 144 |
+
rescale_sigma = _parse_optional_float(rescale_sigma_raw, "rescale_sigma")
|
| 145 |
+
speaker_kv_scale = _parse_optional_float(speaker_kv_scale_raw, "speaker_kv_scale")
|
| 146 |
+
speaker_kv_min_t = _parse_optional_float(speaker_kv_min_t_raw, "speaker_kv_min_t")
|
| 147 |
+
speaker_kv_max_layers = _parse_optional_int(speaker_kv_max_layers_raw, "speaker_kv_max_layers")
|
| 148 |
+
seed = _parse_optional_int(seed_raw, "seed")
|
| 149 |
+
manual_seconds = _parse_optional_float(seconds_raw, "seconds")
|
| 150 |
+
requested_candidates = int(num_candidates)
|
| 151 |
+
if requested_candidates <= 0:
|
| 152 |
+
raise gr.Error("num_candidates must be >= 1.")
|
| 153 |
+
if requested_candidates > MAX_GRADIO_CANDIDATES:
|
| 154 |
+
raise gr.Error(f"num_candidates must be <= {MAX_GRADIO_CANDIDATES}.")
|
| 155 |
+
|
| 156 |
+
ref_wav: str | None = None
|
| 157 |
+
no_ref = True
|
| 158 |
+
if uploaded_audio is not None and str(uploaded_audio).strip() != "":
|
| 159 |
+
ref_wav = str(uploaded_audio)
|
| 160 |
+
no_ref = False
|
| 161 |
+
|
| 162 |
+
stdout_log(
|
| 163 |
+
(
|
| 164 |
+
"[Info] request: mode={} seconds={} duration_scale={} "
|
| 165 |
+
"steps={} seed={} no_ref={} candidates={}"
|
| 166 |
+
).format(
|
| 167 |
+
cfg_guidance_mode,
|
| 168 |
+
"auto" if manual_seconds is None else manual_seconds,
|
| 169 |
+
float(duration_scale),
|
| 170 |
+
int(num_steps),
|
| 171 |
+
"random" if seed is None else seed,
|
| 172 |
+
no_ref,
|
| 173 |
+
requested_candidates,
|
| 174 |
+
)
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
result = _runtime.synthesize(
|
| 178 |
+
SamplingRequest(
|
| 179 |
+
text=str(text),
|
| 180 |
+
ref_wav=ref_wav,
|
| 181 |
+
ref_latent=None,
|
| 182 |
+
no_ref=bool(no_ref),
|
| 183 |
+
ref_normalize_db=-16.0,
|
| 184 |
+
ref_ensure_max=True,
|
| 185 |
+
num_candidates=requested_candidates,
|
| 186 |
+
decode_mode="sequential",
|
| 187 |
+
seconds=manual_seconds,
|
| 188 |
+
duration_scale=float(duration_scale),
|
| 189 |
+
max_ref_seconds=30.0,
|
| 190 |
+
max_text_len=None,
|
| 191 |
+
num_steps=int(num_steps),
|
| 192 |
+
seed=None if seed is None else int(seed),
|
| 193 |
+
cfg_guidance_mode=str(cfg_guidance_mode),
|
| 194 |
+
cfg_scale_text=float(cfg_scale_text),
|
| 195 |
+
cfg_scale_speaker=float(cfg_scale_speaker),
|
| 196 |
+
cfg_scale=cfg_scale,
|
| 197 |
+
cfg_min_t=float(cfg_min_t),
|
| 198 |
+
cfg_max_t=float(cfg_max_t),
|
| 199 |
+
truncation_factor=truncation_factor,
|
| 200 |
+
rescale_k=rescale_k,
|
| 201 |
+
rescale_sigma=rescale_sigma,
|
| 202 |
+
context_kv_cache=bool(context_kv_cache),
|
| 203 |
+
speaker_kv_scale=speaker_kv_scale,
|
| 204 |
+
speaker_kv_min_t=speaker_kv_min_t,
|
| 205 |
+
speaker_kv_max_layers=speaker_kv_max_layers,
|
| 206 |
+
trim_tail=True,
|
| 207 |
+
),
|
| 208 |
+
log_fn=stdout_log,
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
sample_rate = result.sample_rate
|
| 212 |
+
audio_results: list[tuple[int, np.ndarray]] = []
|
| 213 |
+
for audio in result.audios:
|
| 214 |
+
waveform = audio.squeeze(0).float().numpy()
|
| 215 |
+
audio_results.append((sample_rate, waveform))
|
| 216 |
+
stdout_log(f"[Info] seed_used: {result.used_seed}")
|
| 217 |
+
stdout_log(f"[Info] candidates: {len(result.audios)}")
|
| 218 |
+
for message in result.messages:
|
| 219 |
+
stdout_log(message)
|
| 220 |
+
stdout_log(_format_timings(result.stage_timings, result.total_to_decode))
|
| 221 |
+
return audio_results, log_buffer.getvalue()
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
# ---------------------------------------------------------------------------
|
| 225 |
+
# Gradio UI
|
| 226 |
+
# ---------------------------------------------------------------------------
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def build_demo():
|
| 230 |
+
MODEL_LINK = f"https://huggingface.co/{MODEL_REPO}"
|
| 231 |
+
GITHUB_REPO = "https://github.com/Aratako/Irodori-TTS"
|
| 232 |
+
|
| 233 |
+
title = "# Irodori-TTS-500M-v3 Demo"
|
| 234 |
+
description = f"""\
|
| 235 |
+
[Model]({MODEL_LINK}) | [GitHub]({GITHUB_REPO})
|
| 236 |
+
|
| 237 |
+
Flow-matching based Japanese TTS model (500M parameters). \
|
| 238 |
+
Generates speech from text using rectified flow over DACVAE latents.
|
| 239 |
+
|
| 240 |
+
- **Reference audio**: Optional. Upload to condition the speaker voice. \
|
| 241 |
+
Leave blank for unconditional generation.
|
| 242 |
+
- **Duration**: By default, v3 predicts the output duration automatically. \
|
| 243 |
+
Use Duration Scale for small adjustments or Seconds for exact manual control.
|
| 244 |
+
"""
|
| 245 |
+
|
| 246 |
+
with gr.Blocks() as demo:
|
| 247 |
+
gr.Markdown(title)
|
| 248 |
+
gr.Markdown(description)
|
| 249 |
+
|
| 250 |
+
text = gr.Textbox(label="Text", lines=4)
|
| 251 |
+
uploaded_audio = gr.Audio(
|
| 252 |
+
label="Reference Audio Upload (optional, blank = no-reference mode)",
|
| 253 |
+
type="filepath",
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
with gr.Accordion("Sampling", open=True):
|
| 257 |
+
with gr.Row():
|
| 258 |
+
num_steps = gr.Slider(
|
| 259 |
+
label="Num Steps",
|
| 260 |
+
minimum=1,
|
| 261 |
+
maximum=120,
|
| 262 |
+
value=40,
|
| 263 |
+
step=1,
|
| 264 |
+
)
|
| 265 |
+
num_candidates = gr.Slider(
|
| 266 |
+
label="Num Candidates",
|
| 267 |
+
minimum=1,
|
| 268 |
+
maximum=MAX_GRADIO_CANDIDATES,
|
| 269 |
+
value=1,
|
| 270 |
+
step=1,
|
| 271 |
+
)
|
| 272 |
+
seed_raw = gr.Textbox(
|
| 273 |
+
label="Seed (blank=random)",
|
| 274 |
+
value="",
|
| 275 |
+
)
|
| 276 |
+
seconds_raw = gr.Textbox(
|
| 277 |
+
label="Seconds (blank=auto)",
|
| 278 |
+
value="",
|
| 279 |
+
)
|
| 280 |
+
duration_scale = gr.Slider(
|
| 281 |
+
label="Duration Scale",
|
| 282 |
+
minimum=0.5,
|
| 283 |
+
maximum=1.5,
|
| 284 |
+
value=1.0,
|
| 285 |
+
step=0.01,
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
with gr.Row():
|
| 289 |
+
cfg_guidance_mode = gr.Dropdown(
|
| 290 |
+
label="CFG Guidance Mode",
|
| 291 |
+
choices=["independent", "joint", "alternating"],
|
| 292 |
+
value="independent",
|
| 293 |
+
)
|
| 294 |
+
cfg_scale_text = gr.Slider(
|
| 295 |
+
label="CFG Scale Text",
|
| 296 |
+
minimum=0.0,
|
| 297 |
+
maximum=10.0,
|
| 298 |
+
value=3.0,
|
| 299 |
+
step=0.1,
|
| 300 |
+
)
|
| 301 |
+
cfg_scale_speaker = gr.Slider(
|
| 302 |
+
label="CFG Scale Speaker",
|
| 303 |
+
minimum=0.0,
|
| 304 |
+
maximum=10.0,
|
| 305 |
+
value=5.0,
|
| 306 |
+
step=0.1,
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
with gr.Accordion("Advanced (Optional)", open=False):
|
| 310 |
+
cfg_scale_raw = gr.Textbox(label="CFG Scale Override (optional)", value="")
|
| 311 |
+
with gr.Row():
|
| 312 |
+
cfg_min_t = gr.Number(label="CFG Min t", value=0.5)
|
| 313 |
+
cfg_max_t = gr.Number(label="CFG Max t", value=1.0)
|
| 314 |
+
context_kv_cache = gr.Checkbox(label="Context KV Cache", value=True)
|
| 315 |
+
with gr.Row():
|
| 316 |
+
truncation_factor_raw = gr.Textbox(label="Truncation Factor (optional)", value="")
|
| 317 |
+
rescale_k_raw = gr.Textbox(label="Rescale k (optional)", value="")
|
| 318 |
+
rescale_sigma_raw = gr.Textbox(label="Rescale sigma (optional)", value="")
|
| 319 |
+
with gr.Row():
|
| 320 |
+
speaker_kv_scale_raw = gr.Textbox(label="Speaker KV Scale (optional)", value="")
|
| 321 |
+
speaker_kv_min_t_raw = gr.Textbox(label="Speaker KV Min t (optional)", value="0.9")
|
| 322 |
+
speaker_kv_max_layers_raw = gr.Textbox(
|
| 323 |
+
label="Speaker KV Max Layers (optional)", value=""
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
generate_btn = gr.Button("Generate", variant="primary")
|
| 327 |
+
|
| 328 |
+
out_audios: list[gr.Audio] = []
|
| 329 |
+
num_rows = (
|
| 330 |
+
MAX_GRADIO_CANDIDATES + GRADIO_AUDIO_COLS_PER_ROW - 1
|
| 331 |
+
) // GRADIO_AUDIO_COLS_PER_ROW
|
| 332 |
+
with gr.Column():
|
| 333 |
+
for row_idx in range(num_rows):
|
| 334 |
+
with gr.Row():
|
| 335 |
+
for col_idx in range(GRADIO_AUDIO_COLS_PER_ROW):
|
| 336 |
+
i = row_idx * GRADIO_AUDIO_COLS_PER_ROW + col_idx
|
| 337 |
+
if i >= MAX_GRADIO_CANDIDATES:
|
| 338 |
+
break
|
| 339 |
+
out_audios.append(
|
| 340 |
+
gr.Audio(
|
| 341 |
+
label=f"Generated Audio {i + 1}",
|
| 342 |
+
type="numpy",
|
| 343 |
+
visible=(i == 0),
|
| 344 |
+
)
|
| 345 |
+
)
|
| 346 |
+
out_log = gr.Textbox(label="Run Log", lines=6)
|
| 347 |
+
|
| 348 |
+
def gradio_inference(
|
| 349 |
+
text,
|
| 350 |
+
uploaded_audio,
|
| 351 |
+
num_steps,
|
| 352 |
+
num_candidates,
|
| 353 |
+
seed_raw,
|
| 354 |
+
seconds_raw,
|
| 355 |
+
duration_scale,
|
| 356 |
+
cfg_guidance_mode,
|
| 357 |
+
cfg_scale_text,
|
| 358 |
+
cfg_scale_speaker,
|
| 359 |
+
cfg_scale_raw,
|
| 360 |
+
cfg_min_t,
|
| 361 |
+
cfg_max_t,
|
| 362 |
+
context_kv_cache,
|
| 363 |
+
truncation_factor_raw,
|
| 364 |
+
rescale_k_raw,
|
| 365 |
+
rescale_sigma_raw,
|
| 366 |
+
speaker_kv_scale_raw,
|
| 367 |
+
speaker_kv_min_t_raw,
|
| 368 |
+
speaker_kv_max_layers_raw,
|
| 369 |
+
):
|
| 370 |
+
try:
|
| 371 |
+
audio_results, log_text = run_inference_gpu(
|
| 372 |
+
text=text,
|
| 373 |
+
uploaded_audio=uploaded_audio,
|
| 374 |
+
num_steps=num_steps,
|
| 375 |
+
num_candidates=num_candidates,
|
| 376 |
+
seed_raw=seed_raw,
|
| 377 |
+
seconds_raw=seconds_raw,
|
| 378 |
+
duration_scale=duration_scale,
|
| 379 |
+
cfg_guidance_mode=cfg_guidance_mode,
|
| 380 |
+
cfg_scale_text=cfg_scale_text,
|
| 381 |
+
cfg_scale_speaker=cfg_scale_speaker,
|
| 382 |
+
cfg_scale_raw=cfg_scale_raw,
|
| 383 |
+
cfg_min_t=cfg_min_t,
|
| 384 |
+
cfg_max_t=cfg_max_t,
|
| 385 |
+
context_kv_cache=context_kv_cache,
|
| 386 |
+
truncation_factor_raw=truncation_factor_raw,
|
| 387 |
+
rescale_k_raw=rescale_k_raw,
|
| 388 |
+
rescale_sigma_raw=rescale_sigma_raw,
|
| 389 |
+
speaker_kv_scale_raw=speaker_kv_scale_raw,
|
| 390 |
+
speaker_kv_min_t_raw=speaker_kv_min_t_raw,
|
| 391 |
+
speaker_kv_max_layers_raw=speaker_kv_max_layers_raw,
|
| 392 |
+
)
|
| 393 |
+
audio_updates: list[object] = []
|
| 394 |
+
for i in range(MAX_GRADIO_CANDIDATES):
|
| 395 |
+
if i < len(audio_results):
|
| 396 |
+
audio_updates.append(gr.update(value=audio_results[i], visible=True))
|
| 397 |
+
else:
|
| 398 |
+
audio_updates.append(gr.update(value=None, visible=False))
|
| 399 |
+
return (*audio_updates, log_text)
|
| 400 |
+
except Exception as e:
|
| 401 |
+
raise gr.Error(str(e)) from e
|
| 402 |
+
|
| 403 |
+
generate_btn.click(
|
| 404 |
+
fn=gradio_inference,
|
| 405 |
+
inputs=[
|
| 406 |
+
text,
|
| 407 |
+
uploaded_audio,
|
| 408 |
+
num_steps,
|
| 409 |
+
num_candidates,
|
| 410 |
+
seed_raw,
|
| 411 |
+
seconds_raw,
|
| 412 |
+
duration_scale,
|
| 413 |
+
cfg_guidance_mode,
|
| 414 |
+
cfg_scale_text,
|
| 415 |
+
cfg_scale_speaker,
|
| 416 |
+
cfg_scale_raw,
|
| 417 |
+
cfg_min_t,
|
| 418 |
+
cfg_max_t,
|
| 419 |
+
context_kv_cache,
|
| 420 |
+
truncation_factor_raw,
|
| 421 |
+
rescale_k_raw,
|
| 422 |
+
rescale_sigma_raw,
|
| 423 |
+
speaker_kv_scale_raw,
|
| 424 |
+
speaker_kv_min_t_raw,
|
| 425 |
+
speaker_kv_max_layers_raw,
|
| 426 |
+
],
|
| 427 |
+
outputs=[*out_audios, out_log],
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
return demo
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
if __name__ == "__main__":
|
| 434 |
+
demo = build_demo()
|
| 435 |
+
demo.queue(default_concurrency_limit=1)
|
| 436 |
+
demo.launch()
|
irodori_tts/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Irodori-TTS package: text-conditioned RF diffusion over DACVAE latents."""
|
| 2 |
+
|
| 3 |
+
from .config import ModelConfig, SamplingConfig, TrainConfig
|
| 4 |
+
from .lora import LORA_TARGET_PRESETS
|
| 5 |
+
from .model import TextToLatentRFDiT
|
| 6 |
+
from .tokenizer import ByteTokenizer, PretrainedTextTokenizer
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
"ByteTokenizer",
|
| 10 |
+
"LORA_TARGET_PRESETS",
|
| 11 |
+
"ModelConfig",
|
| 12 |
+
"PretrainedTextTokenizer",
|
| 13 |
+
"SamplingConfig",
|
| 14 |
+
"TextToLatentRFDiT",
|
| 15 |
+
"TrainConfig",
|
| 16 |
+
]
|
irodori_tts/codec.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import torchaudio
|
| 9 |
+
from huggingface_hub import hf_hub_download
|
| 10 |
+
|
| 11 |
+
_CODEC_DEFAULT = object()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def patchify_latent(latent: torch.Tensor, patch_size: int) -> torch.Tensor:
|
| 15 |
+
"""
|
| 16 |
+
Convert latent from (B, T, D) -> (B, T//patch, D*patch).
|
| 17 |
+
Extra tail tokens are dropped.
|
| 18 |
+
"""
|
| 19 |
+
if patch_size <= 1:
|
| 20 |
+
return latent
|
| 21 |
+
bsz, seq_len, dim = latent.shape
|
| 22 |
+
usable = (seq_len // patch_size) * patch_size
|
| 23 |
+
latent = latent[:, :usable]
|
| 24 |
+
latent = latent.reshape(bsz, usable // patch_size, dim * patch_size)
|
| 25 |
+
return latent
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def unpatchify_latent(patched: torch.Tensor, patch_size: int, latent_dim: int) -> torch.Tensor:
|
| 29 |
+
"""
|
| 30 |
+
Convert latent from (B, T_p, D*patch) -> (B, T_p*patch, D).
|
| 31 |
+
"""
|
| 32 |
+
if patch_size <= 1:
|
| 33 |
+
return patched
|
| 34 |
+
return patched.reshape(patched.shape[0], patched.shape[1] * patch_size, latent_dim)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclass
|
| 38 |
+
class DACVAECodec:
|
| 39 |
+
model: torch.nn.Module
|
| 40 |
+
sample_rate: int
|
| 41 |
+
latent_dim: int
|
| 42 |
+
device: torch.device
|
| 43 |
+
dtype: torch.dtype
|
| 44 |
+
deterministic_encode: bool
|
| 45 |
+
deterministic_decode: bool
|
| 46 |
+
normalize_db: float | None
|
| 47 |
+
|
| 48 |
+
@classmethod
|
| 49 |
+
def load(
|
| 50 |
+
cls,
|
| 51 |
+
repo_id: str = "Aratako/Semantic-DACVAE-Japanese-32dim",
|
| 52 |
+
device: str = "cuda",
|
| 53 |
+
dtype: torch.dtype | None = None,
|
| 54 |
+
deterministic_encode: bool = True,
|
| 55 |
+
deterministic_decode: bool = True,
|
| 56 |
+
normalize_db: float | None = -16.0,
|
| 57 |
+
) -> DACVAECodec:
|
| 58 |
+
# Prefer installed package; fallback to local clone at ../dacvae.
|
| 59 |
+
try:
|
| 60 |
+
from dacvae import DACVAE
|
| 61 |
+
except ImportError:
|
| 62 |
+
local_repo = Path(__file__).resolve().parents[2] / "dacvae"
|
| 63 |
+
if local_repo.exists():
|
| 64 |
+
sys.path.insert(0, str(local_repo))
|
| 65 |
+
from dacvae import DACVAE
|
| 66 |
+
|
| 67 |
+
location = str(repo_id).strip()
|
| 68 |
+
if location.startswith("hf://"):
|
| 69 |
+
location = location[len("hf://") :]
|
| 70 |
+
if not Path(location).exists() and "/" in location and not location.endswith(".pth"):
|
| 71 |
+
try:
|
| 72 |
+
location = hf_hub_download(repo_id=location, filename="weights.pth")
|
| 73 |
+
print(f"[codec] dacvae: hf://{repo_id} -> {location}", flush=True)
|
| 74 |
+
except Exception:
|
| 75 |
+
# Let DACVAE.load surface a clearer error if this is not a valid path/repo.
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
model = DACVAE.load(location).eval().to(device)
|
| 79 |
+
if dtype is not None:
|
| 80 |
+
model = model.to(dtype=dtype)
|
| 81 |
+
|
| 82 |
+
decoder = getattr(model, "decoder", None)
|
| 83 |
+
if decoder is not None and hasattr(decoder, "alpha"):
|
| 84 |
+
decoder.alpha = 0.0
|
| 85 |
+
if hasattr(decoder, "wm_model"):
|
| 86 |
+
# Irodori checkpoints were trained without the DACVAE watermark branch.
|
| 87 |
+
# Keep decode output mono while skipping that encode/decode path.
|
| 88 |
+
def _watermark_passthrough(
|
| 89 |
+
x: torch.Tensor,
|
| 90 |
+
message: torch.Tensor | None = None,
|
| 91 |
+
_decoder=decoder,
|
| 92 |
+
) -> torch.Tensor:
|
| 93 |
+
del message
|
| 94 |
+
return _decoder.wm_model.encoder_block.forward_no_conv(x)
|
| 95 |
+
|
| 96 |
+
decoder.watermark = _watermark_passthrough
|
| 97 |
+
|
| 98 |
+
if deterministic_decode:
|
| 99 |
+
cls._configure_deterministic_decode(model=model, device=device)
|
| 100 |
+
|
| 101 |
+
model_dtype = next(model.parameters()).dtype
|
| 102 |
+
# Infer latent dimension by encoding a tiny random signal.
|
| 103 |
+
dummy = torch.zeros(1, 1, 2048, device=device, dtype=model_dtype)
|
| 104 |
+
with torch.inference_mode():
|
| 105 |
+
z = model.encode(dummy) # (B, D, T)
|
| 106 |
+
return cls(
|
| 107 |
+
model=model,
|
| 108 |
+
sample_rate=int(model.sample_rate),
|
| 109 |
+
latent_dim=int(z.shape[1]),
|
| 110 |
+
device=torch.device(device),
|
| 111 |
+
dtype=model_dtype,
|
| 112 |
+
deterministic_encode=bool(deterministic_encode),
|
| 113 |
+
deterministic_decode=bool(deterministic_decode),
|
| 114 |
+
normalize_db=None if normalize_db is None else float(normalize_db),
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
@staticmethod
|
| 118 |
+
def _configure_deterministic_decode(model: torch.nn.Module, device: str | torch.device) -> None:
|
| 119 |
+
decoder = getattr(model, "decoder", None)
|
| 120 |
+
wm_model = getattr(decoder, "wm_model", None)
|
| 121 |
+
msg_processor = getattr(wm_model, "msg_processor", None)
|
| 122 |
+
if msg_processor is None:
|
| 123 |
+
return
|
| 124 |
+
nbits = int(msg_processor.nbits)
|
| 125 |
+
message_device = torch.device(device)
|
| 126 |
+
|
| 127 |
+
def _fixed_message(batch_size: int) -> torch.Tensor:
|
| 128 |
+
return torch.zeros((batch_size, nbits), dtype=torch.float32, device=message_device)
|
| 129 |
+
|
| 130 |
+
wm_model.random_message = _fixed_message
|
| 131 |
+
|
| 132 |
+
@staticmethod
|
| 133 |
+
def _normalize_loudness(
|
| 134 |
+
wav: torch.Tensor, sample_rate: int, target_db: float | None
|
| 135 |
+
) -> torch.Tensor:
|
| 136 |
+
if target_db is None:
|
| 137 |
+
return wav
|
| 138 |
+
wav_device = wav.device
|
| 139 |
+
wav = wav.to(dtype=torch.float32)
|
| 140 |
+
if wav.ndim == 2:
|
| 141 |
+
if wav.shape[0] == 1:
|
| 142 |
+
wav = wav[0]
|
| 143 |
+
elif wav.shape[1] == 1:
|
| 144 |
+
wav = wav[:, 0]
|
| 145 |
+
else:
|
| 146 |
+
wav = wav.mean(dim=0)
|
| 147 |
+
if wav.ndim != 1:
|
| 148 |
+
raise ValueError(
|
| 149 |
+
"normalize_loudness expects a mono waveform with shape (T,) "
|
| 150 |
+
f"or singleton-channel (1, T)/(T, 1), got {tuple(wav.shape)}"
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
try:
|
| 154 |
+
from audiotools import AudioSignal
|
| 155 |
+
except Exception as exc:
|
| 156 |
+
raise RuntimeError(
|
| 157 |
+
"audiotools is required when normalize_db is set. "
|
| 158 |
+
"Install audiotools or disable normalize_db."
|
| 159 |
+
) from exc
|
| 160 |
+
|
| 161 |
+
signal = AudioSignal(wav.unsqueeze(0).unsqueeze(0), int(sample_rate))
|
| 162 |
+
signal.normalize(float(target_db))
|
| 163 |
+
signal.ensure_max_of_audio()
|
| 164 |
+
normalized = signal.audio_data
|
| 165 |
+
if not isinstance(normalized, torch.Tensor):
|
| 166 |
+
normalized = torch.as_tensor(normalized)
|
| 167 |
+
normalized = normalized.to(dtype=torch.float32, device=wav_device)
|
| 168 |
+
normalized = normalized.squeeze()
|
| 169 |
+
if normalized.ndim != 1:
|
| 170 |
+
raise RuntimeError(
|
| 171 |
+
"audiotools normalization returned an unexpected waveform shape "
|
| 172 |
+
f"{tuple(normalized.shape)}"
|
| 173 |
+
)
|
| 174 |
+
return normalized
|
| 175 |
+
|
| 176 |
+
@torch.inference_mode()
|
| 177 |
+
def encode_waveform(
|
| 178 |
+
self,
|
| 179 |
+
waveform: torch.Tensor,
|
| 180 |
+
sample_rate: int,
|
| 181 |
+
*,
|
| 182 |
+
normalize_db: float | None | object = _CODEC_DEFAULT,
|
| 183 |
+
ensure_max: bool | None = None,
|
| 184 |
+
) -> torch.Tensor:
|
| 185 |
+
"""
|
| 186 |
+
Input:
|
| 187 |
+
waveform: (B, C, T) or (C, T)
|
| 188 |
+
normalize_db: Optional target loudness (LUFS-like dB) applied before encode
|
| 189 |
+
ensure_max: If True and normalize_db is None, scale down only when abs peak exceeds 1.0
|
| 190 |
+
Output:
|
| 191 |
+
latent: (B, T_latent, D_latent)
|
| 192 |
+
"""
|
| 193 |
+
if waveform.ndim == 2:
|
| 194 |
+
waveform = waveform.unsqueeze(0)
|
| 195 |
+
if waveform.ndim != 3:
|
| 196 |
+
raise ValueError(f"Expected waveform ndim=3, got shape={tuple(waveform.shape)}")
|
| 197 |
+
|
| 198 |
+
if waveform.shape[1] != 1:
|
| 199 |
+
waveform = waveform.mean(dim=1, keepdim=True)
|
| 200 |
+
if sample_rate != self.sample_rate:
|
| 201 |
+
waveform = torchaudio.functional.resample(waveform, sample_rate, self.sample_rate)
|
| 202 |
+
|
| 203 |
+
if normalize_db is _CODEC_DEFAULT:
|
| 204 |
+
effective_normalize_db = self.normalize_db
|
| 205 |
+
elif normalize_db is None:
|
| 206 |
+
effective_normalize_db = None
|
| 207 |
+
else:
|
| 208 |
+
effective_normalize_db = float(normalize_db)
|
| 209 |
+
# audiotools normalization already applies ensure_max_of_audio(), so codec-side
|
| 210 |
+
# peak scaling is only needed when normalization is disabled.
|
| 211 |
+
effective_ensure_max = (
|
| 212 |
+
effective_normalize_db is None and bool(ensure_max) if ensure_max is not None else False
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
waveform = waveform.to(dtype=torch.float32)
|
| 216 |
+
if effective_normalize_db is not None or effective_ensure_max:
|
| 217 |
+
# Keep behavior deterministic per utterance by normalizing each waveform independently.
|
| 218 |
+
processed: list[torch.Tensor] = []
|
| 219 |
+
for wav in waveform.squeeze(1):
|
| 220 |
+
if effective_normalize_db is not None:
|
| 221 |
+
wav = self._normalize_loudness(
|
| 222 |
+
wav, sample_rate=self.sample_rate, target_db=effective_normalize_db
|
| 223 |
+
)
|
| 224 |
+
wav = wav.squeeze()
|
| 225 |
+
if wav.ndim != 1:
|
| 226 |
+
raise RuntimeError(
|
| 227 |
+
"Expected mono per-item waveform after preprocessing, "
|
| 228 |
+
f"got shape={tuple(wav.shape)}"
|
| 229 |
+
)
|
| 230 |
+
if effective_ensure_max:
|
| 231 |
+
peak = wav.abs().max()
|
| 232 |
+
if torch.isfinite(peak) and peak > 1.0:
|
| 233 |
+
wav = wav * (1.0 / float(peak))
|
| 234 |
+
processed.append(wav)
|
| 235 |
+
waveform = torch.stack(processed, dim=0).unsqueeze(1)
|
| 236 |
+
|
| 237 |
+
waveform = waveform.to(self.device, dtype=self.dtype)
|
| 238 |
+
if self.deterministic_encode:
|
| 239 |
+
required_paths_present = (
|
| 240 |
+
hasattr(self.model, "encoder")
|
| 241 |
+
and hasattr(self.model, "_pad")
|
| 242 |
+
and hasattr(self.model, "quantizer")
|
| 243 |
+
and hasattr(self.model.quantizer, "in_proj")
|
| 244 |
+
)
|
| 245 |
+
if not required_paths_present:
|
| 246 |
+
raise RuntimeError(
|
| 247 |
+
"deterministic_encode=True requires encoder/_pad/quantizer.in_proj on DACVAE model."
|
| 248 |
+
)
|
| 249 |
+
z = self.model.encoder(self.model._pad(waveform))
|
| 250 |
+
mean, _scale = self.model.quantizer.in_proj(z).chunk(2, dim=1)
|
| 251 |
+
encoded = mean
|
| 252 |
+
else:
|
| 253 |
+
encoded = self.model.encode(waveform) # (B, D, T)
|
| 254 |
+
return encoded.transpose(1, 2).contiguous() # (B, T, D)
|
| 255 |
+
|
| 256 |
+
@torch.inference_mode()
|
| 257 |
+
def decode_latent(self, latent: torch.Tensor) -> torch.Tensor:
|
| 258 |
+
"""
|
| 259 |
+
Input:
|
| 260 |
+
latent: (B, T, D)
|
| 261 |
+
Output:
|
| 262 |
+
audio: (B, 1, samples)
|
| 263 |
+
"""
|
| 264 |
+
if latent.ndim != 3:
|
| 265 |
+
raise ValueError(f"Expected latent ndim=3, got shape={tuple(latent.shape)}")
|
| 266 |
+
z = latent.transpose(1, 2).contiguous().to(self.device, dtype=self.dtype) # (B, D, T)
|
| 267 |
+
return self.model.decode(z)
|
| 268 |
+
|
| 269 |
+
def encode_file(self, path: str | Path) -> torch.Tensor:
|
| 270 |
+
try:
|
| 271 |
+
wav, sr = torchaudio.load(str(path))
|
| 272 |
+
except RuntimeError:
|
| 273 |
+
import soundfile as sf
|
| 274 |
+
|
| 275 |
+
data, sr = sf.read(str(path), dtype="float32")
|
| 276 |
+
wav = torch.from_numpy(data)
|
| 277 |
+
if wav.ndim == 1:
|
| 278 |
+
wav = wav.unsqueeze(0)
|
| 279 |
+
else:
|
| 280 |
+
wav = wav.T
|
| 281 |
+
wav = wav.unsqueeze(0) # (1, C, T)
|
| 282 |
+
return self.encode_waveform(wav, sr).cpu()
|
irodori_tts/config.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from dataclasses import asdict, dataclass, fields
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Any, TypeVar
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@dataclass
|
| 8 |
+
class ModelConfig:
|
| 9 |
+
latent_dim: int = 128
|
| 10 |
+
latent_patch_size: int = 1
|
| 11 |
+
model_dim: int = 2048
|
| 12 |
+
num_layers: int = 24
|
| 13 |
+
num_heads: int = 16
|
| 14 |
+
mlp_ratio: float = 2.875
|
| 15 |
+
text_mlp_ratio: float | None = 2.6
|
| 16 |
+
speaker_mlp_ratio: float | None = 2.6
|
| 17 |
+
dropout: float = 0.0
|
| 18 |
+
text_vocab_size: int = 102400
|
| 19 |
+
text_tokenizer_repo: str = "sbintuitions/sarashina2.2-0.5b"
|
| 20 |
+
text_add_bos: bool = True
|
| 21 |
+
text_dim: int = 1280
|
| 22 |
+
text_layers: int = 14
|
| 23 |
+
text_heads: int = 10
|
| 24 |
+
use_caption_condition: bool = False
|
| 25 |
+
caption_vocab_size: int | None = None
|
| 26 |
+
caption_tokenizer_repo: str | None = None
|
| 27 |
+
caption_add_bos: bool | None = None
|
| 28 |
+
caption_dim: int | None = None
|
| 29 |
+
caption_layers: int | None = None
|
| 30 |
+
caption_heads: int | None = None
|
| 31 |
+
caption_mlp_ratio: float | None = None
|
| 32 |
+
speaker_dim: int = 1280
|
| 33 |
+
speaker_layers: int = 14
|
| 34 |
+
speaker_heads: int = 10
|
| 35 |
+
speaker_patch_size: int = 1
|
| 36 |
+
timestep_embed_dim: int = 512
|
| 37 |
+
adaln_rank: int = 256
|
| 38 |
+
norm_eps: float = 1e-5
|
| 39 |
+
use_duration_predictor: bool = False
|
| 40 |
+
duration_aux_dim: int = 14
|
| 41 |
+
duration_hidden_dim: int = 1024
|
| 42 |
+
duration_layers: int = 3
|
| 43 |
+
duration_dropout: float = 0.1
|
| 44 |
+
duration_attention_heads: int = 8
|
| 45 |
+
duration_architecture: str = "token_sum_adarn_zero_no_aux"
|
| 46 |
+
duration_token_init_frames: float = 9.0
|
| 47 |
+
duration_speaker_fusion: str = "adarn_zero"
|
| 48 |
+
|
| 49 |
+
@property
|
| 50 |
+
def patched_latent_dim(self) -> int:
|
| 51 |
+
return self.latent_dim * self.latent_patch_size
|
| 52 |
+
|
| 53 |
+
@property
|
| 54 |
+
def speaker_patched_latent_dim(self) -> int:
|
| 55 |
+
return self.patched_latent_dim * self.speaker_patch_size
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
def use_speaker_condition(self) -> bool:
|
| 59 |
+
# Voice-design checkpoints are caption-driven and intentionally omit
|
| 60 |
+
# reference-speaker conditioning to avoid the easier shortcut.
|
| 61 |
+
return not bool(self.use_caption_condition)
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def text_mlp_ratio_resolved(self) -> float:
|
| 65 |
+
if self.text_mlp_ratio is None:
|
| 66 |
+
return self.mlp_ratio
|
| 67 |
+
return float(self.text_mlp_ratio)
|
| 68 |
+
|
| 69 |
+
@property
|
| 70 |
+
def caption_vocab_size_resolved(self) -> int:
|
| 71 |
+
if self.caption_vocab_size is None:
|
| 72 |
+
return int(self.text_vocab_size)
|
| 73 |
+
return int(self.caption_vocab_size)
|
| 74 |
+
|
| 75 |
+
@property
|
| 76 |
+
def caption_tokenizer_repo_resolved(self) -> str:
|
| 77 |
+
if self.caption_tokenizer_repo is None:
|
| 78 |
+
return self.text_tokenizer_repo
|
| 79 |
+
return str(self.caption_tokenizer_repo)
|
| 80 |
+
|
| 81 |
+
@property
|
| 82 |
+
def caption_add_bos_resolved(self) -> bool:
|
| 83 |
+
if self.caption_add_bos is None:
|
| 84 |
+
return bool(self.text_add_bos)
|
| 85 |
+
return bool(self.caption_add_bos)
|
| 86 |
+
|
| 87 |
+
@property
|
| 88 |
+
def caption_dim_resolved(self) -> int:
|
| 89 |
+
if self.caption_dim is None:
|
| 90 |
+
return int(self.text_dim)
|
| 91 |
+
return int(self.caption_dim)
|
| 92 |
+
|
| 93 |
+
@property
|
| 94 |
+
def caption_layers_resolved(self) -> int:
|
| 95 |
+
if self.caption_layers is None:
|
| 96 |
+
return int(self.text_layers)
|
| 97 |
+
return int(self.caption_layers)
|
| 98 |
+
|
| 99 |
+
@property
|
| 100 |
+
def caption_heads_resolved(self) -> int:
|
| 101 |
+
if self.caption_heads is None:
|
| 102 |
+
return int(self.text_heads)
|
| 103 |
+
return int(self.caption_heads)
|
| 104 |
+
|
| 105 |
+
@property
|
| 106 |
+
def caption_mlp_ratio_resolved(self) -> float:
|
| 107 |
+
if self.caption_mlp_ratio is None:
|
| 108 |
+
return self.text_mlp_ratio_resolved
|
| 109 |
+
return float(self.caption_mlp_ratio)
|
| 110 |
+
|
| 111 |
+
@property
|
| 112 |
+
def speaker_mlp_ratio_resolved(self) -> float:
|
| 113 |
+
if self.speaker_mlp_ratio is None:
|
| 114 |
+
return self.mlp_ratio
|
| 115 |
+
return float(self.speaker_mlp_ratio)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
@dataclass
|
| 119 |
+
class TrainConfig:
|
| 120 |
+
manifest_path: str = ""
|
| 121 |
+
output_dir: str = "outputs"
|
| 122 |
+
batch_size: int = 8
|
| 123 |
+
num_workers: int = 2
|
| 124 |
+
dataloader_persistent_workers: bool = False
|
| 125 |
+
dataloader_prefetch_factor: int = 2
|
| 126 |
+
allow_tf32: bool = False
|
| 127 |
+
compile_model: bool = False
|
| 128 |
+
train_mode: str = "rf"
|
| 129 |
+
learning_rate: float = 1e-4
|
| 130 |
+
weight_decay: float = 0.01
|
| 131 |
+
optimizer: str = "muon"
|
| 132 |
+
adam_beta1: float = 0.9
|
| 133 |
+
adam_beta2: float = 0.999
|
| 134 |
+
adam_eps: float = 1e-8
|
| 135 |
+
muon_momentum: float = 0.95
|
| 136 |
+
muon_adjust_lr_fn: str = "match_rms_adamw"
|
| 137 |
+
lr_scheduler: str = "none"
|
| 138 |
+
warmup_steps: int = 0
|
| 139 |
+
caption_warmup: bool = False
|
| 140 |
+
caption_warmup_steps: int = 0
|
| 141 |
+
stable_steps: int = 0
|
| 142 |
+
min_lr_scale: float = 0.1
|
| 143 |
+
max_steps: int = 200000
|
| 144 |
+
log_every: int = 100
|
| 145 |
+
save_every: int = 1000
|
| 146 |
+
checkpoint_best_n: int = 0
|
| 147 |
+
valid_ratio: float = 0.0
|
| 148 |
+
valid_every: int = 0
|
| 149 |
+
progress: bool = True
|
| 150 |
+
progress_all_ranks: bool = False
|
| 151 |
+
precision: str = "bf16"
|
| 152 |
+
grad_clip_norm: float = 1.0
|
| 153 |
+
gradient_accumulation_steps: int = 1
|
| 154 |
+
max_text_len: int = 256
|
| 155 |
+
max_caption_len: int | None = None
|
| 156 |
+
text_condition_dropout: float = 0.1
|
| 157 |
+
caption_condition_dropout: float = 0.1
|
| 158 |
+
speaker_condition_dropout: float = 0.1
|
| 159 |
+
max_latent_steps: int = 750
|
| 160 |
+
fixed_target_latent_steps: int | None = 750
|
| 161 |
+
fixed_target_full_mask: bool = True
|
| 162 |
+
rf_loss_mode: str = "echo"
|
| 163 |
+
duration_loss_weight: float = 0.1
|
| 164 |
+
duration_speaker_dropout: float = 0.1
|
| 165 |
+
duration_huber_delta: float = 0.1
|
| 166 |
+
timestep_logit_mean: float = 0.0
|
| 167 |
+
timestep_logit_std: float = 1.0
|
| 168 |
+
timestep_stratified: bool = True
|
| 169 |
+
timestep_min: float = 0.001
|
| 170 |
+
timestep_max: float = 0.999
|
| 171 |
+
wandb_enabled: bool = False
|
| 172 |
+
wandb_project: str = "Irodori-TTS"
|
| 173 |
+
wandb_entity: str | None = None
|
| 174 |
+
wandb_run_name: str | None = None
|
| 175 |
+
wandb_mode: str = "online"
|
| 176 |
+
ddp_find_unused_parameters: bool = False
|
| 177 |
+
lora_enabled: bool = False
|
| 178 |
+
lora_r: int = 16
|
| 179 |
+
lora_alpha: int = 32
|
| 180 |
+
lora_dropout: float = 0.0
|
| 181 |
+
lora_bias: str = "none"
|
| 182 |
+
lora_target_modules: str = "diffusion_attn"
|
| 183 |
+
lora_modules_to_save: str | None = "auto"
|
| 184 |
+
seed: int = 0
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
@dataclass
|
| 188 |
+
class SamplingConfig:
|
| 189 |
+
num_steps: int = 40
|
| 190 |
+
cfg_scale_text: float = 3.0
|
| 191 |
+
cfg_scale_caption: float = 3.0
|
| 192 |
+
cfg_scale_speaker: float = 5.0
|
| 193 |
+
cfg_guidance_mode: str = "independent"
|
| 194 |
+
cfg_scale: float | None = None
|
| 195 |
+
cfg_min_t: float = 0.5
|
| 196 |
+
cfg_max_t: float = 1.0
|
| 197 |
+
truncation_factor: float | None = None
|
| 198 |
+
rescale_k: float | None = None
|
| 199 |
+
rescale_sigma: float | None = None
|
| 200 |
+
context_kv_cache: bool = True
|
| 201 |
+
speaker_kv_scale: float | None = None
|
| 202 |
+
speaker_kv_min_t: float | None = 0.9
|
| 203 |
+
speaker_kv_max_layers: int | None = None
|
| 204 |
+
# Deprecated: inference length is derived from --seconds and codec hop_length.
|
| 205 |
+
sequence_length: int | None = None
|
| 206 |
+
seed: int = 0
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def save_json(path: str | Path, payload: dict) -> None:
|
| 210 |
+
path = Path(path)
|
| 211 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 212 |
+
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def dump_configs(path: str | Path, model_cfg: ModelConfig, train_cfg: TrainConfig) -> None:
|
| 216 |
+
save_json(path, {"model": asdict(model_cfg), "train": asdict(train_cfg)})
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
T = TypeVar("T")
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def load_experiment_yaml(path: str | Path) -> dict[str, Any]:
|
| 223 |
+
"""
|
| 224 |
+
Load experiment config YAML. Returns {} for an empty document.
|
| 225 |
+
"""
|
| 226 |
+
try:
|
| 227 |
+
import yaml
|
| 228 |
+
except ImportError as exc:
|
| 229 |
+
raise RuntimeError(
|
| 230 |
+
"PyYAML is required for --config support. Install with `pip install pyyaml`."
|
| 231 |
+
) from exc
|
| 232 |
+
|
| 233 |
+
payload = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
|
| 234 |
+
if payload is None:
|
| 235 |
+
return {}
|
| 236 |
+
if not isinstance(payload, dict):
|
| 237 |
+
raise ValueError(f"Config root must be a mapping: {path}")
|
| 238 |
+
return payload
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def merge_dataclass_overrides(base: T, overrides: dict[str, Any] | None, section: str) -> T:
|
| 242 |
+
"""
|
| 243 |
+
Merge mapping overrides into a dataclass instance with key validation.
|
| 244 |
+
"""
|
| 245 |
+
if overrides is None:
|
| 246 |
+
return base
|
| 247 |
+
if not isinstance(overrides, dict):
|
| 248 |
+
raise ValueError(f"Config section '{section}' must be a mapping.")
|
| 249 |
+
|
| 250 |
+
allowed = {f.name for f in fields(base)}
|
| 251 |
+
unknown = sorted(set(overrides) - allowed)
|
| 252 |
+
if unknown:
|
| 253 |
+
raise ValueError(f"Unknown keys in '{section}' config: {unknown}")
|
| 254 |
+
|
| 255 |
+
merged = asdict(base)
|
| 256 |
+
merged.update(overrides)
|
| 257 |
+
return type(base)(**merged)
|
irodori_tts/duration.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
import re
|
| 5 |
+
from collections.abc import Iterable, Sequence
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
ALLOWED_ANNOTATION_EMOJIS: tuple[str, ...] = (
|
| 10 |
+
"⏩",
|
| 11 |
+
"⏱️",
|
| 12 |
+
"⏸️",
|
| 13 |
+
"🌬️",
|
| 14 |
+
"🍭",
|
| 15 |
+
"🎛️",
|
| 16 |
+
"🎭",
|
| 17 |
+
"🎵",
|
| 18 |
+
"🐢",
|
| 19 |
+
"🐱",
|
| 20 |
+
"👂",
|
| 21 |
+
"👃",
|
| 22 |
+
"👅",
|
| 23 |
+
"👌",
|
| 24 |
+
"👏",
|
| 25 |
+
"💋",
|
| 26 |
+
"💥",
|
| 27 |
+
"💦",
|
| 28 |
+
"💪",
|
| 29 |
+
"📄",
|
| 30 |
+
"📞",
|
| 31 |
+
"📢",
|
| 32 |
+
"📣",
|
| 33 |
+
"😆",
|
| 34 |
+
"😊",
|
| 35 |
+
"😌",
|
| 36 |
+
"😎",
|
| 37 |
+
"😏",
|
| 38 |
+
"😒",
|
| 39 |
+
"😖",
|
| 40 |
+
"😟",
|
| 41 |
+
"😠",
|
| 42 |
+
"😪",
|
| 43 |
+
"😭",
|
| 44 |
+
"😮",
|
| 45 |
+
"😮💨",
|
| 46 |
+
"😰",
|
| 47 |
+
"😱",
|
| 48 |
+
"😲",
|
| 49 |
+
"😴",
|
| 50 |
+
"🙄",
|
| 51 |
+
"🙏",
|
| 52 |
+
"🤐",
|
| 53 |
+
"🤔",
|
| 54 |
+
"🤢",
|
| 55 |
+
"🤧",
|
| 56 |
+
"🤭",
|
| 57 |
+
"🥤",
|
| 58 |
+
"🥱",
|
| 59 |
+
"🥴",
|
| 60 |
+
"🥵",
|
| 61 |
+
"🥹",
|
| 62 |
+
"🥺",
|
| 63 |
+
"🫣",
|
| 64 |
+
"🫶",
|
| 65 |
+
"📖",
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
_ALLOWED_ANNOTATION_EMOJI_PATTERN = re.compile(
|
| 69 |
+
"|".join(sorted((re.escape(x) for x in ALLOWED_ANNOTATION_EMOJIS), key=len, reverse=True))
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _log1p_cap(count: int, cap: int) -> float:
|
| 74 |
+
return math.log1p(float(min(max(int(count), 0), int(cap)))) / math.log1p(float(cap))
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _log1p_cap_float(value: float, cap: float) -> float:
|
| 78 |
+
value = min(max(float(value), 0.0), float(cap))
|
| 79 |
+
return math.log1p(value) / math.log1p(float(cap))
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _is_kana(ch: str) -> bool:
|
| 83 |
+
code = ord(ch)
|
| 84 |
+
return (0x3040 <= code <= 0x309F) or (0x30A0 <= code <= 0x30FF)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _is_kanji(ch: str) -> bool:
|
| 88 |
+
code = ord(ch)
|
| 89 |
+
return (
|
| 90 |
+
(0x3400 <= code <= 0x4DBF)
|
| 91 |
+
or (0x4E00 <= code <= 0x9FFF)
|
| 92 |
+
or (0xF900 <= code <= 0xFAFF)
|
| 93 |
+
or (0x20000 <= code <= 0x2FA1F)
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _is_alnum(ch: str) -> bool:
|
| 98 |
+
return ch.isascii() and ch.isalnum()
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def count_annotation_emojis(text: str) -> int:
|
| 102 |
+
return len(_ALLOWED_ANNOTATION_EMOJI_PATTERN.findall(text))
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def build_duration_features(
|
| 106 |
+
texts: Sequence[str] | Iterable[str],
|
| 107 |
+
*,
|
| 108 |
+
token_counts: Sequence[int] | torch.Tensor,
|
| 109 |
+
max_text_len: int,
|
| 110 |
+
has_speaker: Sequence[bool] | torch.Tensor,
|
| 111 |
+
) -> torch.Tensor:
|
| 112 |
+
text_list = [str(x) for x in texts]
|
| 113 |
+
if isinstance(token_counts, torch.Tensor):
|
| 114 |
+
token_count_list = [int(x) for x in token_counts.detach().cpu().tolist()]
|
| 115 |
+
else:
|
| 116 |
+
token_count_list = [int(x) for x in token_counts]
|
| 117 |
+
if isinstance(has_speaker, torch.Tensor):
|
| 118 |
+
has_speaker_list = [bool(x) for x in has_speaker.detach().cpu().tolist()]
|
| 119 |
+
else:
|
| 120 |
+
has_speaker_list = [bool(x) for x in has_speaker]
|
| 121 |
+
|
| 122 |
+
if len(text_list) != len(token_count_list) or len(text_list) != len(has_speaker_list):
|
| 123 |
+
raise ValueError(
|
| 124 |
+
"Duration feature inputs must have matching lengths: "
|
| 125 |
+
f"texts={len(text_list)} token_counts={len(token_count_list)} "
|
| 126 |
+
f"has_speaker={len(has_speaker_list)}"
|
| 127 |
+
)
|
| 128 |
+
if max_text_len <= 0:
|
| 129 |
+
raise ValueError(f"max_text_len must be > 0, got {max_text_len}")
|
| 130 |
+
|
| 131 |
+
rows: list[list[float]] = []
|
| 132 |
+
for text, token_count, speaker_available in zip(
|
| 133 |
+
text_list, token_count_list, has_speaker_list, strict=True
|
| 134 |
+
):
|
| 135 |
+
char_count = max(len(text), 1)
|
| 136 |
+
kana_count = sum(1 for ch in text if _is_kana(ch))
|
| 137 |
+
kanji_count = sum(1 for ch in text if _is_kanji(ch))
|
| 138 |
+
alnum_count = sum(1 for ch in text if _is_alnum(ch))
|
| 139 |
+
emoji_count = count_annotation_emojis(text)
|
| 140 |
+
|
| 141 |
+
period_count = text.count("。") + text.count(".")
|
| 142 |
+
comma_count = text.count("、") + text.count(",")
|
| 143 |
+
long_vowel_count = text.count("ー")
|
| 144 |
+
ellipsis_count = text.count("…")
|
| 145 |
+
exclamation_count = text.count("!") + text.count("!")
|
| 146 |
+
question_count = text.count("?") + text.count("?")
|
| 147 |
+
|
| 148 |
+
rows.append(
|
| 149 |
+
[
|
| 150 |
+
min(max(float(token_count), 0.0), float(max_text_len)) / float(max_text_len),
|
| 151 |
+
_log1p_cap_float(float(char_count), 512.0),
|
| 152 |
+
float(token_count) / float(char_count),
|
| 153 |
+
_log1p_cap(period_count, 8),
|
| 154 |
+
_log1p_cap(comma_count, 16),
|
| 155 |
+
_log1p_cap(long_vowel_count, 8),
|
| 156 |
+
_log1p_cap(ellipsis_count, 8),
|
| 157 |
+
_log1p_cap(exclamation_count, 8),
|
| 158 |
+
_log1p_cap(question_count, 8),
|
| 159 |
+
_log1p_cap(emoji_count, 8),
|
| 160 |
+
float(kana_count) / float(char_count),
|
| 161 |
+
float(kanji_count) / float(char_count),
|
| 162 |
+
float(alnum_count) / float(char_count),
|
| 163 |
+
1.0 if speaker_available else 0.0,
|
| 164 |
+
]
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
return torch.tensor(rows, dtype=torch.float32)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def set_duration_has_speaker_feature(
|
| 171 |
+
features: torch.Tensor,
|
| 172 |
+
has_speaker: torch.Tensor,
|
| 173 |
+
) -> torch.Tensor:
|
| 174 |
+
if features.ndim != 2:
|
| 175 |
+
raise ValueError(f"Expected duration features shape (B, D), got {tuple(features.shape)}")
|
| 176 |
+
if features.shape[1] <= 0:
|
| 177 |
+
raise ValueError(
|
| 178 |
+
f"duration features must have at least one column, got {features.shape[1]}"
|
| 179 |
+
)
|
| 180 |
+
out = features.clone()
|
| 181 |
+
out[:, -1] = has_speaker.to(device=features.device, dtype=features.dtype)
|
| 182 |
+
return out
|
irodori_tts/inference_runtime.py
ADDED
|
@@ -0,0 +1,1063 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import gc
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import secrets
|
| 7 |
+
import threading
|
| 8 |
+
import time
|
| 9 |
+
from collections.abc import Callable
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import torchaudio
|
| 15 |
+
from safetensors import safe_open
|
| 16 |
+
from safetensors.torch import load_file as load_safetensors_file
|
| 17 |
+
|
| 18 |
+
from .codec import DACVAECodec, patchify_latent, unpatchify_latent
|
| 19 |
+
from .config import ModelConfig
|
| 20 |
+
from .duration import build_duration_features
|
| 21 |
+
from .lora import checkpoint_state_uses_lora
|
| 22 |
+
from .model import TextToLatentRFDiT
|
| 23 |
+
from .rf import sample_euler_rf_cfg
|
| 24 |
+
from .text_normalization import normalize_text
|
| 25 |
+
from .tokenizer import PretrainedTextTokenizer
|
| 26 |
+
from .watermark import SilentCipherWatermarker
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _is_mps_available() -> bool:
|
| 30 |
+
backends = getattr(torch, "backends", None)
|
| 31 |
+
if backends is None or not hasattr(backends, "mps"):
|
| 32 |
+
return False
|
| 33 |
+
return bool(torch.backends.mps.is_available())
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def resolve_runtime_device(device: str | torch.device) -> torch.device:
|
| 37 |
+
resolved = torch.device(device)
|
| 38 |
+
if resolved.type == "cpu":
|
| 39 |
+
return resolved
|
| 40 |
+
if resolved.type == "cuda":
|
| 41 |
+
if not torch.cuda.is_available():
|
| 42 |
+
raise ValueError("CUDA device requested but torch.cuda.is_available() is False.")
|
| 43 |
+
return resolved
|
| 44 |
+
if resolved.type == "mps":
|
| 45 |
+
if resolved.index is not None:
|
| 46 |
+
raise ValueError("MPS device index is not supported. Use 'mps'.")
|
| 47 |
+
if not _is_mps_available():
|
| 48 |
+
raise ValueError("MPS device requested but torch.backends.mps.is_available() is False.")
|
| 49 |
+
return torch.device("mps")
|
| 50 |
+
raise ValueError(f"Unsupported inference device={resolved!s}. Expected one of: cpu, cuda, mps.")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def list_available_runtime_devices() -> list[str]:
|
| 54 |
+
devices: list[str] = []
|
| 55 |
+
if torch.cuda.is_available():
|
| 56 |
+
devices.append("cuda")
|
| 57 |
+
if _is_mps_available():
|
| 58 |
+
devices.append("mps")
|
| 59 |
+
devices.append("cpu")
|
| 60 |
+
return devices
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def default_runtime_device() -> str:
|
| 64 |
+
return list_available_runtime_devices()[0]
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def list_available_runtime_precisions(device: str | torch.device) -> list[str]:
|
| 68 |
+
resolved = resolve_runtime_device(device)
|
| 69 |
+
if resolved.type == "cuda":
|
| 70 |
+
return ["fp32", "bf16"]
|
| 71 |
+
return ["fp32"]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _sync_device(device: torch.device) -> None:
|
| 75 |
+
if device.type == "cuda":
|
| 76 |
+
torch.cuda.synchronize(device)
|
| 77 |
+
elif device.type == "mps":
|
| 78 |
+
mps = getattr(torch, "mps", None)
|
| 79 |
+
if mps is not None and hasattr(mps, "synchronize"):
|
| 80 |
+
mps.synchronize()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _sync_devices(*devices: torch.device) -> None:
|
| 84 |
+
seen: set[tuple[str, int | None]] = set()
|
| 85 |
+
for device in devices:
|
| 86 |
+
key = (device.type, device.index)
|
| 87 |
+
if key in seen:
|
| 88 |
+
continue
|
| 89 |
+
_sync_device(device)
|
| 90 |
+
seen.add(key)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _measure_start(device: torch.device, *extra_devices: torch.device) -> float:
|
| 94 |
+
_sync_devices(device, *extra_devices)
|
| 95 |
+
return time.perf_counter()
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _measure_end(device: torch.device, t0: float, *extra_devices: torch.device) -> float:
|
| 99 |
+
_sync_devices(device, *extra_devices)
|
| 100 |
+
return time.perf_counter() - t0
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _coerce_latent_shape(latent: torch.Tensor, latent_dim: int) -> torch.Tensor:
|
| 104 |
+
if latent.ndim == 3 and latent.shape[0] == 1:
|
| 105 |
+
latent = latent[0]
|
| 106 |
+
if latent.ndim != 2:
|
| 107 |
+
raise ValueError(f"Unsupported latent shape: {tuple(latent.shape)}")
|
| 108 |
+
if latent.shape[1] == latent_dim:
|
| 109 |
+
return latent
|
| 110 |
+
if latent.shape[0] == latent_dim:
|
| 111 |
+
return latent.transpose(0, 1).contiguous()
|
| 112 |
+
raise ValueError(
|
| 113 |
+
f"Could not infer latent layout for shape={tuple(latent.shape)} and latent_dim={latent_dim}"
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def find_flattening_point(
|
| 118 |
+
latent: torch.Tensor,
|
| 119 |
+
target_value: float = 0.0,
|
| 120 |
+
window_size: int = 20,
|
| 121 |
+
std_threshold: float = 0.05,
|
| 122 |
+
mean_threshold: float = 0.1,
|
| 123 |
+
) -> int:
|
| 124 |
+
"""
|
| 125 |
+
Echo-style heuristic: find first index where a trailing window becomes near-flat and near-zero.
|
| 126 |
+
|
| 127 |
+
Args:
|
| 128 |
+
latent: (T, D) latent sequence.
|
| 129 |
+
Returns:
|
| 130 |
+
Flattening index in [0, T].
|
| 131 |
+
"""
|
| 132 |
+
if latent.ndim != 2:
|
| 133 |
+
raise ValueError(f"Expected latent shape (T, D), got {tuple(latent.shape)}")
|
| 134 |
+
total_steps = int(latent.shape[0])
|
| 135 |
+
if total_steps <= 0 or window_size <= 0:
|
| 136 |
+
return total_steps
|
| 137 |
+
|
| 138 |
+
pad = torch.zeros(
|
| 139 |
+
(window_size, latent.shape[1]),
|
| 140 |
+
device=latent.device,
|
| 141 |
+
dtype=latent.dtype,
|
| 142 |
+
)
|
| 143 |
+
padded = torch.cat([latent, pad], dim=0)
|
| 144 |
+
for i in range(padded.shape[0] - window_size):
|
| 145 |
+
window = padded[i : i + window_size]
|
| 146 |
+
window_std = window.std(unbiased=False)
|
| 147 |
+
window_mean = window.mean()
|
| 148 |
+
if window_std < std_threshold and torch.abs(window_mean - target_value) < mean_threshold:
|
| 149 |
+
return int(i)
|
| 150 |
+
return total_steps
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@dataclass(frozen=True)
|
| 154 |
+
class RuntimeKey:
|
| 155 |
+
checkpoint: str
|
| 156 |
+
model_device: str
|
| 157 |
+
codec_repo: str = "Aratako/Semantic-DACVAE-Japanese-32dim"
|
| 158 |
+
model_precision: str = "fp32"
|
| 159 |
+
codec_device: str = "cpu"
|
| 160 |
+
codec_precision: str = "fp32"
|
| 161 |
+
codec_deterministic_encode: bool = True
|
| 162 |
+
codec_deterministic_decode: bool = True
|
| 163 |
+
compile_model: bool = False
|
| 164 |
+
compile_dynamic: bool = False
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
@dataclass
|
| 168 |
+
class SamplingRequest:
|
| 169 |
+
text: str
|
| 170 |
+
caption: str | None = None
|
| 171 |
+
ref_wav: str | None = None
|
| 172 |
+
ref_latent: str | None = None
|
| 173 |
+
no_ref: bool = False
|
| 174 |
+
ref_normalize_db: float | None = -16.0
|
| 175 |
+
ref_ensure_max: bool = True
|
| 176 |
+
num_candidates: int = 1
|
| 177 |
+
decode_mode: str = "sequential"
|
| 178 |
+
seconds: float | None = None
|
| 179 |
+
duration_scale: float = 1.0
|
| 180 |
+
min_seconds: float = 0.5
|
| 181 |
+
max_seconds: float = 30.0
|
| 182 |
+
max_ref_seconds: float | None = 30.0
|
| 183 |
+
max_text_len: int | None = None
|
| 184 |
+
max_caption_len: int | None = None
|
| 185 |
+
num_steps: int = 40
|
| 186 |
+
cfg_scale_text: float = 3.0
|
| 187 |
+
cfg_scale_caption: float = 3.0
|
| 188 |
+
cfg_scale_speaker: float = 5.0
|
| 189 |
+
cfg_guidance_mode: str = "independent"
|
| 190 |
+
cfg_scale: float | None = None
|
| 191 |
+
cfg_min_t: float = 0.5
|
| 192 |
+
cfg_max_t: float = 1.0
|
| 193 |
+
truncation_factor: float | None = None
|
| 194 |
+
rescale_k: float | None = None
|
| 195 |
+
rescale_sigma: float | None = None
|
| 196 |
+
context_kv_cache: bool = True
|
| 197 |
+
speaker_kv_scale: float | None = None
|
| 198 |
+
speaker_kv_min_t: float | None = None
|
| 199 |
+
speaker_kv_max_layers: int | None = None
|
| 200 |
+
seed: int | None = None
|
| 201 |
+
trim_tail: bool = True
|
| 202 |
+
tail_window_size: int = 20
|
| 203 |
+
tail_std_threshold: float = 0.05
|
| 204 |
+
tail_mean_threshold: float = 0.1
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
@dataclass
|
| 208 |
+
class SamplingResult:
|
| 209 |
+
audio: torch.Tensor
|
| 210 |
+
audios: list[torch.Tensor]
|
| 211 |
+
sample_rate: int
|
| 212 |
+
stage_timings: list[tuple[str, float]]
|
| 213 |
+
total_to_decode: float
|
| 214 |
+
used_seed: int
|
| 215 |
+
messages: list[str]
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def _maybe_compile_inference_model(
|
| 219 |
+
model: TextToLatentRFDiT,
|
| 220 |
+
*,
|
| 221 |
+
enabled: bool,
|
| 222 |
+
dynamic: bool,
|
| 223 |
+
) -> TextToLatentRFDiT:
|
| 224 |
+
if not enabled:
|
| 225 |
+
return model
|
| 226 |
+
if not hasattr(torch, "compile"):
|
| 227 |
+
raise RuntimeError("compile_model=True requires torch.compile (PyTorch 2+).")
|
| 228 |
+
compile_kwargs = {"dynamic": bool(dynamic)}
|
| 229 |
+
model.encode_conditions = torch.compile(model.encode_conditions, **compile_kwargs)
|
| 230 |
+
model.build_context_kv_cache = torch.compile(model.build_context_kv_cache, **compile_kwargs)
|
| 231 |
+
model.forward_with_encoded_conditions = torch.compile(
|
| 232 |
+
model.forward_with_encoded_conditions,
|
| 233 |
+
**compile_kwargs,
|
| 234 |
+
)
|
| 235 |
+
return model
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def resolve_runtime_dtype(*, precision: str, device: torch.device) -> torch.dtype:
|
| 239 |
+
mode = str(precision).strip().lower()
|
| 240 |
+
if mode == "fp32":
|
| 241 |
+
return torch.float32
|
| 242 |
+
if mode == "bf16":
|
| 243 |
+
if device.type != "cuda":
|
| 244 |
+
raise ValueError("precision='bf16' currently requires CUDA device.")
|
| 245 |
+
return torch.bfloat16
|
| 246 |
+
raise ValueError(f"Unsupported precision={precision!r}. Expected one of: fp32, bf16.")
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def resolve_cfg_scales(
|
| 250 |
+
*,
|
| 251 |
+
cfg_guidance_mode: str,
|
| 252 |
+
cfg_scale_text: float,
|
| 253 |
+
cfg_scale_caption: float,
|
| 254 |
+
cfg_scale_speaker: float,
|
| 255 |
+
cfg_scale: float | None,
|
| 256 |
+
use_caption_condition: bool = True,
|
| 257 |
+
use_speaker_condition: bool = True,
|
| 258 |
+
) -> tuple[float, float, float, list[str]]:
|
| 259 |
+
"""Normalize/validate CFG scales for guidance mode."""
|
| 260 |
+
messages: list[str] = []
|
| 261 |
+
text_val = float(cfg_scale_text)
|
| 262 |
+
caption_val = float(cfg_scale_caption)
|
| 263 |
+
speaker_val = float(cfg_scale_speaker)
|
| 264 |
+
|
| 265 |
+
if cfg_scale is not None:
|
| 266 |
+
text_val = float(cfg_scale)
|
| 267 |
+
caption_val = float(cfg_scale)
|
| 268 |
+
speaker_val = float(cfg_scale)
|
| 269 |
+
if not use_speaker_condition:
|
| 270 |
+
if speaker_val > 0.0:
|
| 271 |
+
messages.append(
|
| 272 |
+
"info: speaker conditioning is disabled for this checkpoint; ignoring cfg_scale_speaker."
|
| 273 |
+
)
|
| 274 |
+
speaker_val = 0.0
|
| 275 |
+
|
| 276 |
+
mode = str(cfg_guidance_mode).strip().lower()
|
| 277 |
+
enabled_vals = [value for value in (text_val, speaker_val) if value > 0.0]
|
| 278 |
+
if use_caption_condition and caption_val > 0.0:
|
| 279 |
+
enabled_vals.append(caption_val)
|
| 280 |
+
if mode == "joint" and enabled_vals and (max(enabled_vals) - min(enabled_vals) > 1e-6):
|
| 281 |
+
raise ValueError(
|
| 282 |
+
"cfg_guidance_mode='joint' requires equal enabled cfg_scale_text/cfg_scale_caption/cfg_scale_speaker, "
|
| 283 |
+
"or set cfg_scale."
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
return text_val, caption_val, speaker_val, messages
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def _load_torch_checkpoint_payload(path: Path) -> dict:
|
| 290 |
+
payload = torch.load(path, map_location="cpu", weights_only=True)
|
| 291 |
+
if not isinstance(payload, dict):
|
| 292 |
+
raise ValueError(f"Unsupported checkpoint payload type: {type(payload)!r}")
|
| 293 |
+
return payload
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
_CONFIG_META_KEY = "config_json"
|
| 297 |
+
_INFERENCE_CONFIG_KEYS = {"max_text_len", "max_caption_len", "fixed_target_latent_steps"}
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def _load_checkpoint_from_pt(path: Path) -> tuple[dict[str, torch.Tensor], dict, dict | None]:
|
| 301 |
+
ckpt = _load_torch_checkpoint_payload(path)
|
| 302 |
+
model_state = ckpt.get("model")
|
| 303 |
+
model_cfg = ckpt.get("model_config")
|
| 304 |
+
train_cfg = ckpt.get("train_config")
|
| 305 |
+
|
| 306 |
+
if not isinstance(model_state, dict):
|
| 307 |
+
raise ValueError(f"Checkpoint missing model weights dictionary: {path}")
|
| 308 |
+
if not isinstance(model_cfg, dict):
|
| 309 |
+
raise ValueError(f"Checkpoint missing model_config dictionary: {path}")
|
| 310 |
+
if train_cfg is not None and not isinstance(train_cfg, dict):
|
| 311 |
+
raise ValueError(f"Checkpoint train_config must be a dictionary when present: {path}")
|
| 312 |
+
|
| 313 |
+
if checkpoint_state_uses_lora(model_state):
|
| 314 |
+
raise ValueError(
|
| 315 |
+
f"LoRA checkpoints must be loaded from adapter directories or merged safetensors: {path}"
|
| 316 |
+
)
|
| 317 |
+
return model_state, model_cfg, _extract_inference_train_config(train_cfg)
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def _parse_json_mapping(
|
| 321 |
+
raw: str | None,
|
| 322 |
+
*,
|
| 323 |
+
field: str,
|
| 324 |
+
path: Path,
|
| 325 |
+
required: bool = False,
|
| 326 |
+
) -> dict | None:
|
| 327 |
+
if raw is None:
|
| 328 |
+
if required:
|
| 329 |
+
raise ValueError(f"Missing required metadata field '{field}' in checkpoint: {path}")
|
| 330 |
+
return None
|
| 331 |
+
try:
|
| 332 |
+
payload = json.loads(raw)
|
| 333 |
+
except json.JSONDecodeError as exc:
|
| 334 |
+
raise ValueError(f"Invalid JSON in '{field}' metadata for checkpoint: {path}") from exc
|
| 335 |
+
if not isinstance(payload, dict):
|
| 336 |
+
raise ValueError(f"Metadata field '{field}' must decode to an object: {path}")
|
| 337 |
+
return payload
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def _extract_inference_train_config(raw: dict | None) -> dict | None:
|
| 341 |
+
if raw is None:
|
| 342 |
+
return None
|
| 343 |
+
|
| 344 |
+
inference_cfg: dict[str, int] = {}
|
| 345 |
+
for key in _INFERENCE_CONFIG_KEYS:
|
| 346 |
+
value = raw.get(key)
|
| 347 |
+
if value is None:
|
| 348 |
+
continue
|
| 349 |
+
if not isinstance(value, int):
|
| 350 |
+
raise ValueError(f"Inference config key '{key}' must be int, got {type(value)!r}.")
|
| 351 |
+
inference_cfg[key] = int(value)
|
| 352 |
+
|
| 353 |
+
return inference_cfg or None
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def _split_flat_checkpoint_config(path: Path, flat_config: dict) -> tuple[dict, dict | None]:
|
| 357 |
+
model_cfg: dict[str, object] = {}
|
| 358 |
+
inference_cfg: dict[str, int] = {}
|
| 359 |
+
for key, value in flat_config.items():
|
| 360 |
+
if key in _INFERENCE_CONFIG_KEYS:
|
| 361 |
+
if not isinstance(value, int):
|
| 362 |
+
raise ValueError(
|
| 363 |
+
f"Inference config key '{key}' must be int in checkpoint metadata: {path}"
|
| 364 |
+
)
|
| 365 |
+
inference_cfg[key] = int(value)
|
| 366 |
+
continue
|
| 367 |
+
model_cfg[key] = value
|
| 368 |
+
return model_cfg, (inference_cfg or None)
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def _load_checkpoint_from_safetensors(
|
| 372 |
+
path: Path,
|
| 373 |
+
) -> tuple[dict[str, torch.Tensor], dict, dict | None]:
|
| 374 |
+
model_state = load_safetensors_file(str(path), device="cpu")
|
| 375 |
+
if not isinstance(model_state, dict) or not model_state:
|
| 376 |
+
raise ValueError(f"Safetensors checkpoint has no model weights: {path}")
|
| 377 |
+
|
| 378 |
+
with safe_open(str(path), framework="pt", device="cpu") as handle:
|
| 379 |
+
metadata = handle.metadata() or {}
|
| 380 |
+
|
| 381 |
+
flat_config = _parse_json_mapping(
|
| 382 |
+
metadata.get(_CONFIG_META_KEY),
|
| 383 |
+
field=_CONFIG_META_KEY,
|
| 384 |
+
path=path,
|
| 385 |
+
required=True,
|
| 386 |
+
)
|
| 387 |
+
model_cfg, inference_cfg = _split_flat_checkpoint_config(path=path, flat_config=flat_config)
|
| 388 |
+
return model_state, model_cfg, inference_cfg
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
def _load_checkpoint_for_inference(path: Path) -> tuple[dict[str, torch.Tensor], dict, dict | None]:
|
| 392 |
+
if path.suffix.lower() == ".safetensors":
|
| 393 |
+
return _load_checkpoint_from_safetensors(path)
|
| 394 |
+
return _load_checkpoint_from_pt(path)
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
class InferenceRuntime:
|
| 398 |
+
def __init__(
|
| 399 |
+
self,
|
| 400 |
+
*,
|
| 401 |
+
key: RuntimeKey,
|
| 402 |
+
model_cfg: ModelConfig,
|
| 403 |
+
train_cfg: dict | None,
|
| 404 |
+
model: TextToLatentRFDiT,
|
| 405 |
+
tokenizer: PretrainedTextTokenizer,
|
| 406 |
+
caption_tokenizer: PretrainedTextTokenizer | None,
|
| 407 |
+
codec: DACVAECodec,
|
| 408 |
+
default_text_max_len: int,
|
| 409 |
+
default_caption_max_len: int,
|
| 410 |
+
) -> None:
|
| 411 |
+
self.key = key
|
| 412 |
+
self.model_device = resolve_runtime_device(key.model_device)
|
| 413 |
+
self.codec_device = resolve_runtime_device(key.codec_device)
|
| 414 |
+
self.model_cfg = model_cfg
|
| 415 |
+
self.train_cfg = train_cfg
|
| 416 |
+
self.model = model
|
| 417 |
+
self.tokenizer = tokenizer
|
| 418 |
+
self.caption_tokenizer = caption_tokenizer
|
| 419 |
+
self.codec = codec
|
| 420 |
+
self.default_text_max_len = default_text_max_len
|
| 421 |
+
self.default_caption_max_len = default_caption_max_len
|
| 422 |
+
self.watermarker = SilentCipherWatermarker(device=str(self.codec_device))
|
| 423 |
+
self._infer_lock = threading.Lock()
|
| 424 |
+
|
| 425 |
+
@classmethod
|
| 426 |
+
def from_key(cls, key: RuntimeKey) -> InferenceRuntime:
|
| 427 |
+
model_device = resolve_runtime_device(key.model_device)
|
| 428 |
+
codec_device = resolve_runtime_device(key.codec_device)
|
| 429 |
+
model_dtype = resolve_runtime_dtype(
|
| 430 |
+
precision=key.model_precision,
|
| 431 |
+
device=model_device,
|
| 432 |
+
)
|
| 433 |
+
codec_dtype = resolve_runtime_dtype(
|
| 434 |
+
precision=key.codec_precision,
|
| 435 |
+
device=codec_device,
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
model_state, model_cfg_dict, train_cfg = _load_checkpoint_for_inference(
|
| 439 |
+
Path(key.checkpoint)
|
| 440 |
+
)
|
| 441 |
+
model_cfg = ModelConfig(**model_cfg_dict)
|
| 442 |
+
|
| 443 |
+
model = TextToLatentRFDiT(model_cfg).to(model_device)
|
| 444 |
+
model.load_state_dict(model_state)
|
| 445 |
+
model = model.to(dtype=model_dtype)
|
| 446 |
+
model.eval()
|
| 447 |
+
model = _maybe_compile_inference_model(
|
| 448 |
+
model,
|
| 449 |
+
enabled=bool(key.compile_model),
|
| 450 |
+
dynamic=bool(key.compile_dynamic),
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
tokenizer = PretrainedTextTokenizer.from_pretrained(
|
| 454 |
+
repo_id=model_cfg.text_tokenizer_repo,
|
| 455 |
+
add_bos=bool(model_cfg.text_add_bos),
|
| 456 |
+
local_files_only=False,
|
| 457 |
+
)
|
| 458 |
+
if tokenizer.vocab_size != model_cfg.text_vocab_size:
|
| 459 |
+
raise ValueError(
|
| 460 |
+
f"text_vocab_size mismatch: checkpoint text_vocab_size={model_cfg.text_vocab_size} but tokenizer "
|
| 461 |
+
f"({model_cfg.text_tokenizer_repo}) vocab_size={tokenizer.vocab_size}."
|
| 462 |
+
)
|
| 463 |
+
caption_tokenizer = None
|
| 464 |
+
if model_cfg.use_caption_condition:
|
| 465 |
+
caption_tokenizer = PretrainedTextTokenizer.from_pretrained(
|
| 466 |
+
repo_id=model_cfg.caption_tokenizer_repo_resolved,
|
| 467 |
+
add_bos=model_cfg.caption_add_bos_resolved,
|
| 468 |
+
local_files_only=False,
|
| 469 |
+
)
|
| 470 |
+
if caption_tokenizer.vocab_size != model_cfg.caption_vocab_size_resolved:
|
| 471 |
+
raise ValueError(
|
| 472 |
+
f"caption_vocab_size mismatch: checkpoint caption_vocab_size={model_cfg.caption_vocab_size_resolved} but tokenizer ({model_cfg.caption_tokenizer_repo_resolved}) "
|
| 473 |
+
f"vocab_size={caption_tokenizer.vocab_size}."
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
default_text_max_len = 256
|
| 477 |
+
default_caption_max_len = default_text_max_len
|
| 478 |
+
if isinstance(train_cfg, dict):
|
| 479 |
+
ckpt_text_max_len = train_cfg.get("max_text_len")
|
| 480 |
+
if isinstance(ckpt_text_max_len, int) and ckpt_text_max_len > 0:
|
| 481 |
+
default_text_max_len = int(ckpt_text_max_len)
|
| 482 |
+
ckpt_caption_max_len = train_cfg.get("max_caption_len")
|
| 483 |
+
if isinstance(ckpt_caption_max_len, int) and ckpt_caption_max_len > 0:
|
| 484 |
+
default_caption_max_len = int(ckpt_caption_max_len)
|
| 485 |
+
else:
|
| 486 |
+
default_caption_max_len = default_text_max_len
|
| 487 |
+
|
| 488 |
+
codec = DACVAECodec.load(
|
| 489 |
+
repo_id=key.codec_repo,
|
| 490 |
+
device=str(codec_device),
|
| 491 |
+
dtype=codec_dtype,
|
| 492 |
+
deterministic_encode=bool(key.codec_deterministic_encode),
|
| 493 |
+
deterministic_decode=bool(key.codec_deterministic_decode),
|
| 494 |
+
)
|
| 495 |
+
if model_cfg.latent_dim != codec.latent_dim:
|
| 496 |
+
raise ValueError(
|
| 497 |
+
f"Latent dimension mismatch: checkpoint latent_dim={model_cfg.latent_dim} but codec latent_dim={codec.latent_dim}. "
|
| 498 |
+
"Use a compatible codec/checkpoint pair."
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
return cls(
|
| 502 |
+
key=key,
|
| 503 |
+
model_cfg=model_cfg,
|
| 504 |
+
train_cfg=train_cfg if isinstance(train_cfg, dict) else None,
|
| 505 |
+
model=model,
|
| 506 |
+
tokenizer=tokenizer,
|
| 507 |
+
caption_tokenizer=caption_tokenizer,
|
| 508 |
+
codec=codec,
|
| 509 |
+
default_text_max_len=default_text_max_len,
|
| 510 |
+
default_caption_max_len=default_caption_max_len,
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
def _load_reference_latent(
|
| 514 |
+
self,
|
| 515 |
+
*,
|
| 516 |
+
req: SamplingRequest,
|
| 517 |
+
batch_size: int,
|
| 518 |
+
messages: list[str],
|
| 519 |
+
) -> tuple[torch.Tensor | None, torch.Tensor | None]:
|
| 520 |
+
runtime_dtype = next(self.model.parameters()).dtype
|
| 521 |
+
if not self.model_cfg.use_speaker_condition:
|
| 522 |
+
if req.ref_wav is not None or req.ref_latent is not None:
|
| 523 |
+
messages.append(
|
| 524 |
+
"info: speaker conditioning is disabled for this checkpoint; ignoring reference input."
|
| 525 |
+
)
|
| 526 |
+
return None, None
|
| 527 |
+
if req.no_ref:
|
| 528 |
+
ref_len = max(1, int(self.model_cfg.speaker_patch_size))
|
| 529 |
+
ref_latent_patched = torch.zeros(
|
| 530 |
+
(
|
| 531 |
+
batch_size,
|
| 532 |
+
ref_len,
|
| 533 |
+
self.model_cfg.latent_dim * self.model_cfg.latent_patch_size,
|
| 534 |
+
),
|
| 535 |
+
device=self.model_device,
|
| 536 |
+
dtype=runtime_dtype,
|
| 537 |
+
)
|
| 538 |
+
ref_mask = torch.zeros(
|
| 539 |
+
(batch_size, ref_len), dtype=torch.bool, device=self.model_device
|
| 540 |
+
)
|
| 541 |
+
return ref_latent_patched, ref_mask
|
| 542 |
+
|
| 543 |
+
if req.ref_wav is None and req.ref_latent is None:
|
| 544 |
+
raise ValueError("Specify either ref_wav/ref_latent, or set no_ref=True.")
|
| 545 |
+
|
| 546 |
+
max_ref_latent_steps = None
|
| 547 |
+
if req.max_ref_seconds is not None and req.max_ref_seconds > 0:
|
| 548 |
+
max_ref_latent_steps = max(
|
| 549 |
+
1,
|
| 550 |
+
math.ceil(
|
| 551 |
+
float(req.max_ref_seconds)
|
| 552 |
+
* float(self.codec.sample_rate)
|
| 553 |
+
/ float(int(self.codec.model.hop_length))
|
| 554 |
+
),
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
if req.ref_latent is not None:
|
| 558 |
+
latent_raw = torch.load(req.ref_latent, map_location="cpu", weights_only=True)
|
| 559 |
+
ref_latent = _coerce_latent_shape(
|
| 560 |
+
latent_raw, latent_dim=self.model_cfg.latent_dim
|
| 561 |
+
).unsqueeze(0)
|
| 562 |
+
ref_latent = ref_latent.to(dtype=runtime_dtype)
|
| 563 |
+
else:
|
| 564 |
+
wav, sr = _load_audio(req.ref_wav)
|
| 565 |
+
if req.max_ref_seconds is not None and req.max_ref_seconds > 0:
|
| 566 |
+
max_ref_samples = max(1, int(float(req.max_ref_seconds) * float(sr)))
|
| 567 |
+
if wav.shape[1] > max_ref_samples:
|
| 568 |
+
messages.append(
|
| 569 |
+
f"warning: reference audio exceeds max_ref_seconds ({req.max_ref_seconds}s). "
|
| 570 |
+
f"Trimming from {float(wav.shape[1]) / float(sr):.2f}s to {float(max_ref_samples) / float(sr):.2f}s."
|
| 571 |
+
)
|
| 572 |
+
wav = wav[:, :max_ref_samples]
|
| 573 |
+
if req.ref_normalize_db is not None:
|
| 574 |
+
messages.append(
|
| 575 |
+
f"info: reference loudness normalize enabled (target_db={float(req.ref_normalize_db):.2f}, includes peak safety scaling)."
|
| 576 |
+
)
|
| 577 |
+
elif req.ref_ensure_max:
|
| 578 |
+
messages.append("info: reference peak safety scaling enabled (ensure_max=True).")
|
| 579 |
+
ref_latent = self.codec.encode_waveform(
|
| 580 |
+
wav.unsqueeze(0),
|
| 581 |
+
sample_rate=int(sr),
|
| 582 |
+
normalize_db=req.ref_normalize_db,
|
| 583 |
+
ensure_max=bool(req.ref_ensure_max),
|
| 584 |
+
).cpu()
|
| 585 |
+
|
| 586 |
+
if max_ref_latent_steps is not None and ref_latent.shape[1] > max_ref_latent_steps:
|
| 587 |
+
messages.append(
|
| 588 |
+
f"warning: reference latent steps ({ref_latent.shape[1]}) exceed max_ref_seconds bound ({max_ref_latent_steps} steps). "
|
| 589 |
+
"Trimming reference latent."
|
| 590 |
+
)
|
| 591 |
+
ref_latent = ref_latent[:, :max_ref_latent_steps]
|
| 592 |
+
|
| 593 |
+
ref_latent_patched = patchify_latent(ref_latent, self.model_cfg.latent_patch_size).to(
|
| 594 |
+
self.model_device
|
| 595 |
+
)
|
| 596 |
+
if ref_latent_patched.shape[1] == 0:
|
| 597 |
+
raise ValueError(
|
| 598 |
+
"Reference latent length became zero after patchify. Use longer reference audio."
|
| 599 |
+
)
|
| 600 |
+
if batch_size > 1:
|
| 601 |
+
ref_latent_patched = ref_latent_patched.repeat(batch_size, 1, 1)
|
| 602 |
+
ref_mask = torch.ones(
|
| 603 |
+
(batch_size, ref_latent_patched.shape[1]), dtype=torch.bool, device=self.model_device
|
| 604 |
+
)
|
| 605 |
+
return ref_latent_patched, ref_mask
|
| 606 |
+
|
| 607 |
+
def synthesize(
|
| 608 |
+
self,
|
| 609 |
+
req: SamplingRequest,
|
| 610 |
+
*,
|
| 611 |
+
log_fn: Callable[[str], None] | None = None,
|
| 612 |
+
) -> SamplingResult:
|
| 613 |
+
def _log(msg: str) -> None:
|
| 614 |
+
if log_fn is not None:
|
| 615 |
+
log_fn(msg)
|
| 616 |
+
|
| 617 |
+
messages: list[str] = []
|
| 618 |
+
_log(
|
| 619 |
+
(
|
| 620 |
+
"[runtime] start synthesize "
|
| 621 |
+
"model_device={} model_precision={} codec_device={} codec_precision={} "
|
| 622 |
+
"silentcipher_watermark={} mode={} seconds={} steps={} seed={} candidates={} decode_mode={}"
|
| 623 |
+
).format(
|
| 624 |
+
self.key.model_device,
|
| 625 |
+
self.key.model_precision,
|
| 626 |
+
self.key.codec_device,
|
| 627 |
+
self.key.codec_precision,
|
| 628 |
+
self.watermarker.ready,
|
| 629 |
+
req.cfg_guidance_mode,
|
| 630 |
+
req.seconds,
|
| 631 |
+
req.num_steps,
|
| 632 |
+
"random" if req.seed is None else int(req.seed),
|
| 633 |
+
req.num_candidates,
|
| 634 |
+
req.decode_mode,
|
| 635 |
+
)
|
| 636 |
+
)
|
| 637 |
+
|
| 638 |
+
manual_seconds = None if req.seconds is None else float(req.seconds)
|
| 639 |
+
if manual_seconds is not None and manual_seconds <= 0:
|
| 640 |
+
raise ValueError(f"seconds must be > 0 when provided, got {req.seconds}")
|
| 641 |
+
duration_scale = float(req.duration_scale)
|
| 642 |
+
if duration_scale <= 0:
|
| 643 |
+
raise ValueError(f"duration_scale must be > 0, got {duration_scale}")
|
| 644 |
+
min_seconds = float(req.min_seconds)
|
| 645 |
+
max_seconds = float(req.max_seconds)
|
| 646 |
+
if min_seconds <= 0:
|
| 647 |
+
raise ValueError(f"min_seconds must be > 0, got {min_seconds}")
|
| 648 |
+
if max_seconds < min_seconds:
|
| 649 |
+
raise ValueError(
|
| 650 |
+
f"max_seconds must be >= min_seconds, got min={min_seconds} max={max_seconds}"
|
| 651 |
+
)
|
| 652 |
+
num_candidates = int(req.num_candidates)
|
| 653 |
+
if num_candidates <= 0:
|
| 654 |
+
raise ValueError(f"num_candidates must be > 0, got {num_candidates}")
|
| 655 |
+
decode_mode = str(req.decode_mode).strip().lower()
|
| 656 |
+
if decode_mode not in {"sequential", "batch"}:
|
| 657 |
+
raise ValueError(
|
| 658 |
+
f"Unsupported decode_mode={req.decode_mode!r}. Expected one of: sequential, batch."
|
| 659 |
+
)
|
| 660 |
+
|
| 661 |
+
raw_text = str(req.text)
|
| 662 |
+
normalized_text = normalize_text(raw_text).strip()
|
| 663 |
+
if normalized_text == "":
|
| 664 |
+
raise ValueError("text became empty after normalization.")
|
| 665 |
+
|
| 666 |
+
text_max_len = (
|
| 667 |
+
self.default_text_max_len if req.max_text_len is None else int(req.max_text_len)
|
| 668 |
+
)
|
| 669 |
+
if text_max_len <= 0:
|
| 670 |
+
raise ValueError(f"max_text_len must be > 0, got {text_max_len}")
|
| 671 |
+
caption_max_len = (
|
| 672 |
+
self.default_caption_max_len
|
| 673 |
+
if req.max_caption_len is None
|
| 674 |
+
else int(req.max_caption_len)
|
| 675 |
+
)
|
| 676 |
+
if self.model_cfg.use_caption_condition and caption_max_len <= 0:
|
| 677 |
+
raise ValueError(f"max_caption_len must be > 0, got {caption_max_len}")
|
| 678 |
+
has_caption_text = bool(
|
| 679 |
+
self.model_cfg.use_caption_condition
|
| 680 |
+
and req.caption is not None
|
| 681 |
+
and str(req.caption).strip() != ""
|
| 682 |
+
)
|
| 683 |
+
|
| 684 |
+
truncation_factor = None if req.truncation_factor is None else float(req.truncation_factor)
|
| 685 |
+
rescale_k = None if req.rescale_k is None else float(req.rescale_k)
|
| 686 |
+
rescale_sigma = None if req.rescale_sigma is None else float(req.rescale_sigma)
|
| 687 |
+
if truncation_factor is not None and truncation_factor <= 0:
|
| 688 |
+
raise ValueError(f"truncation_factor must be > 0, got {truncation_factor}")
|
| 689 |
+
if (rescale_k is None) != (rescale_sigma is None):
|
| 690 |
+
raise ValueError("rescale_k and rescale_sigma must be set together.")
|
| 691 |
+
if rescale_k is not None and rescale_k <= 0:
|
| 692 |
+
raise ValueError(f"rescale_k must be > 0, got {rescale_k}")
|
| 693 |
+
if rescale_sigma is not None and rescale_sigma <= 0:
|
| 694 |
+
raise ValueError(f"rescale_sigma must be > 0, got {rescale_sigma}")
|
| 695 |
+
|
| 696 |
+
speaker_kv_scale = None if req.speaker_kv_scale is None else float(req.speaker_kv_scale)
|
| 697 |
+
speaker_kv_min_t = None
|
| 698 |
+
speaker_kv_max_layers = (
|
| 699 |
+
None if req.speaker_kv_max_layers is None else int(req.speaker_kv_max_layers)
|
| 700 |
+
)
|
| 701 |
+
if speaker_kv_scale is not None:
|
| 702 |
+
if not self.model_cfg.use_speaker_condition:
|
| 703 |
+
messages.append(
|
| 704 |
+
"info: speaker conditioning is disabled for this checkpoint; ignoring speaker_kv_scale."
|
| 705 |
+
)
|
| 706 |
+
speaker_kv_scale = None
|
| 707 |
+
else:
|
| 708 |
+
if speaker_kv_scale <= 0:
|
| 709 |
+
raise ValueError(f"speaker_kv_scale must be > 0, got {speaker_kv_scale}")
|
| 710 |
+
speaker_kv_min_t = (
|
| 711 |
+
0.9 if req.speaker_kv_min_t is None else float(req.speaker_kv_min_t)
|
| 712 |
+
)
|
| 713 |
+
if not (0.0 <= speaker_kv_min_t <= 1.0):
|
| 714 |
+
raise ValueError(f"speaker_kv_min_t must be in [0, 1], got {speaker_kv_min_t}")
|
| 715 |
+
if speaker_kv_max_layers is not None and speaker_kv_max_layers < 0:
|
| 716 |
+
raise ValueError(
|
| 717 |
+
f"speaker_kv_max_layers must be >= 0 when specified, got {speaker_kv_max_layers}"
|
| 718 |
+
)
|
| 719 |
+
|
| 720 |
+
cfg_mode = str(req.cfg_guidance_mode).strip().lower()
|
| 721 |
+
if cfg_mode not in {"independent", "joint", "alternating"}:
|
| 722 |
+
raise ValueError(
|
| 723 |
+
f"Unsupported cfg_guidance_mode={req.cfg_guidance_mode!r}. "
|
| 724 |
+
"Expected one of: independent, joint, alternating."
|
| 725 |
+
)
|
| 726 |
+
|
| 727 |
+
cfg_scale_text, cfg_scale_caption, cfg_scale_speaker, scale_messages = resolve_cfg_scales(
|
| 728 |
+
cfg_guidance_mode=cfg_mode,
|
| 729 |
+
cfg_scale_text=req.cfg_scale_text,
|
| 730 |
+
cfg_scale_caption=req.cfg_scale_caption,
|
| 731 |
+
cfg_scale_speaker=req.cfg_scale_speaker,
|
| 732 |
+
cfg_scale=req.cfg_scale,
|
| 733 |
+
use_caption_condition=has_caption_text,
|
| 734 |
+
use_speaker_condition=self.model_cfg.use_speaker_condition,
|
| 735 |
+
)
|
| 736 |
+
messages.extend(scale_messages)
|
| 737 |
+
for msg in scale_messages:
|
| 738 |
+
_log(msg)
|
| 739 |
+
|
| 740 |
+
stage_timings: list[tuple[str, float]] = []
|
| 741 |
+
if req.seed is None:
|
| 742 |
+
used_seed = int(secrets.randbits(63))
|
| 743 |
+
msg = f"info: seed not specified; using random seed {used_seed}."
|
| 744 |
+
messages.append(msg)
|
| 745 |
+
_log(msg)
|
| 746 |
+
else:
|
| 747 |
+
used_seed = int(req.seed)
|
| 748 |
+
_log(f"[runtime] using seed: {used_seed}")
|
| 749 |
+
post_load_t0 = _measure_start(self.model_device, self.codec_device)
|
| 750 |
+
|
| 751 |
+
with self._infer_lock, torch.inference_mode():
|
| 752 |
+
t0 = _measure_start(self.model_device)
|
| 753 |
+
text_ids, text_mask = self.tokenizer.batch_encode(
|
| 754 |
+
[normalized_text] * num_candidates,
|
| 755 |
+
max_length=text_max_len,
|
| 756 |
+
)
|
| 757 |
+
stage_sec = _measure_end(self.model_device, t0)
|
| 758 |
+
stage_timings.append(("tokenize_text", stage_sec))
|
| 759 |
+
_log(f"[runtime] tokenize_text: {stage_sec * 1000.0:.1f} ms")
|
| 760 |
+
text_ids = text_ids.to(self.model_device)
|
| 761 |
+
text_mask = text_mask.to(self.model_device)
|
| 762 |
+
caption_ids = None
|
| 763 |
+
caption_mask = None
|
| 764 |
+
if self.model_cfg.use_caption_condition:
|
| 765 |
+
if self.caption_tokenizer is None:
|
| 766 |
+
raise RuntimeError(
|
| 767 |
+
"Caption conditioning is enabled but caption tokenizer is not loaded."
|
| 768 |
+
)
|
| 769 |
+
caption_text = "" if req.caption is None else str(req.caption).strip()
|
| 770 |
+
caption_ids, caption_mask = self.caption_tokenizer.batch_encode(
|
| 771 |
+
[caption_text] * num_candidates,
|
| 772 |
+
max_length=caption_max_len,
|
| 773 |
+
)
|
| 774 |
+
if caption_text == "":
|
| 775 |
+
caption_mask.zero_()
|
| 776 |
+
caption_ids = caption_ids.to(self.model_device)
|
| 777 |
+
caption_mask = caption_mask.to(self.model_device)
|
| 778 |
+
|
| 779 |
+
t0 = _measure_start(self.model_device, self.codec_device)
|
| 780 |
+
msg_count_before_ref = len(messages)
|
| 781 |
+
ref_latent, ref_mask = self._load_reference_latent(
|
| 782 |
+
req=req,
|
| 783 |
+
batch_size=num_candidates,
|
| 784 |
+
messages=messages,
|
| 785 |
+
)
|
| 786 |
+
stage_sec = _measure_end(self.model_device, t0, self.codec_device)
|
| 787 |
+
stage_timings.append(("prepare_reference", stage_sec))
|
| 788 |
+
for msg in messages[msg_count_before_ref:]:
|
| 789 |
+
_log(msg)
|
| 790 |
+
_log(f"[runtime] prepare_reference: {stage_sec * 1000.0:.1f} ms")
|
| 791 |
+
|
| 792 |
+
hop_length = int(self.codec.model.hop_length)
|
| 793 |
+
if manual_seconds is not None:
|
| 794 |
+
clamped_seconds = min(max_seconds, max(min_seconds, manual_seconds))
|
| 795 |
+
if clamped_seconds != manual_seconds:
|
| 796 |
+
duration_msg = (
|
| 797 |
+
f"warning: manual duration {manual_seconds:.3f}s was clamped to "
|
| 798 |
+
f"{clamped_seconds:.3f}s."
|
| 799 |
+
)
|
| 800 |
+
messages.append(duration_msg)
|
| 801 |
+
_log(duration_msg)
|
| 802 |
+
target_samples = max(1, int(clamped_seconds * self.codec.sample_rate))
|
| 803 |
+
latent_steps = math.ceil(target_samples / hop_length)
|
| 804 |
+
duration_msg = f"info: using manual duration {clamped_seconds:.3f}s."
|
| 805 |
+
messages.append(duration_msg)
|
| 806 |
+
_log(duration_msg)
|
| 807 |
+
elif self.model_cfg.use_duration_predictor:
|
| 808 |
+
t0 = _measure_start(self.model_device)
|
| 809 |
+
has_speaker_duration = torch.zeros(
|
| 810 |
+
(num_candidates,), dtype=torch.bool, device=self.model_device
|
| 811 |
+
)
|
| 812 |
+
if self.model_cfg.use_speaker_condition and ref_mask is not None:
|
| 813 |
+
has_speaker_duration = ref_mask.any(dim=1)
|
| 814 |
+
duration_features = build_duration_features(
|
| 815 |
+
[normalized_text] * num_candidates,
|
| 816 |
+
token_counts=text_mask.sum(dim=1),
|
| 817 |
+
max_text_len=text_max_len,
|
| 818 |
+
has_speaker=has_speaker_duration,
|
| 819 |
+
).to(self.model_device)
|
| 820 |
+
(
|
| 821 |
+
duration_text_state,
|
| 822 |
+
duration_text_mask,
|
| 823 |
+
duration_speaker_state,
|
| 824 |
+
_duration_speaker_mask,
|
| 825 |
+
_duration_caption_state,
|
| 826 |
+
_duration_caption_mask,
|
| 827 |
+
) = self.model.encode_conditions(
|
| 828 |
+
text_input_ids=text_ids,
|
| 829 |
+
text_mask=text_mask,
|
| 830 |
+
ref_latent=ref_latent,
|
| 831 |
+
ref_mask=ref_mask,
|
| 832 |
+
caption_input_ids=caption_ids,
|
| 833 |
+
caption_mask=caption_mask,
|
| 834 |
+
)
|
| 835 |
+
pred_log_frames = self.model.predict_duration_log_frames(
|
| 836 |
+
text_state=duration_text_state,
|
| 837 |
+
text_mask=duration_text_mask,
|
| 838 |
+
speaker_state=duration_speaker_state,
|
| 839 |
+
speaker_mask=_duration_speaker_mask,
|
| 840 |
+
duration_features=duration_features,
|
| 841 |
+
has_speaker=has_speaker_duration,
|
| 842 |
+
)
|
| 843 |
+
pred_frames = torch.expm1(pred_log_frames).float().mean().item()
|
| 844 |
+
scaled_frames = pred_frames * duration_scale
|
| 845 |
+
min_frames = max(1, math.ceil(min_seconds * self.codec.sample_rate / hop_length))
|
| 846 |
+
max_frames = max(1, math.floor(max_seconds * self.codec.sample_rate / hop_length))
|
| 847 |
+
latent_steps = int(round(scaled_frames))
|
| 848 |
+
latent_steps = max(min_frames, min(max_frames, latent_steps))
|
| 849 |
+
target_samples = int(latent_steps * hop_length)
|
| 850 |
+
stage_sec = _measure_end(self.model_device, t0)
|
| 851 |
+
stage_timings.append(("predict_duration", stage_sec))
|
| 852 |
+
msg = (
|
| 853 |
+
f"info: predicted duration frames={pred_frames:.1f}, "
|
| 854 |
+
f"scale={duration_scale:.3f}, using_frames={latent_steps} "
|
| 855 |
+
f"({target_samples / float(self.codec.sample_rate):.3f}s)."
|
| 856 |
+
)
|
| 857 |
+
messages.append(msg)
|
| 858 |
+
_log(msg)
|
| 859 |
+
_log(f"[runtime] predict_duration: {stage_sec * 1000.0:.1f} ms")
|
| 860 |
+
else:
|
| 861 |
+
fallback_seconds = 30.0
|
| 862 |
+
target_samples = int(fallback_seconds * self.codec.sample_rate)
|
| 863 |
+
latent_steps = math.ceil(target_samples / hop_length)
|
| 864 |
+
msg = "info: checkpoint has no duration predictor; falling back to 30.000s."
|
| 865 |
+
messages.append(msg)
|
| 866 |
+
_log(msg)
|
| 867 |
+
patched_steps = math.ceil(latent_steps / self.model_cfg.latent_patch_size)
|
| 868 |
+
|
| 869 |
+
if isinstance(self.train_cfg, dict):
|
| 870 |
+
fixed_steps = self.train_cfg.get("fixed_target_latent_steps")
|
| 871 |
+
if isinstance(fixed_steps, int) and fixed_steps > 0 and latent_steps > fixed_steps:
|
| 872 |
+
msg = (
|
| 873 |
+
f"warning: requested latent length ({latent_steps}) exceeds fixed_target_latent_steps ({fixed_steps}) "
|
| 874 |
+
"used in training. Long-tail stability may degrade."
|
| 875 |
+
)
|
| 876 |
+
messages.append(msg)
|
| 877 |
+
_log(msg)
|
| 878 |
+
|
| 879 |
+
t0 = _measure_start(self.model_device)
|
| 880 |
+
z_patched = sample_euler_rf_cfg(
|
| 881 |
+
model=self.model,
|
| 882 |
+
text_input_ids=text_ids,
|
| 883 |
+
text_mask=text_mask,
|
| 884 |
+
ref_latent=ref_latent,
|
| 885 |
+
ref_mask=ref_mask,
|
| 886 |
+
sequence_length=patched_steps,
|
| 887 |
+
caption_input_ids=caption_ids,
|
| 888 |
+
caption_mask=caption_mask,
|
| 889 |
+
num_steps=int(req.num_steps),
|
| 890 |
+
cfg_scale_text=cfg_scale_text,
|
| 891 |
+
cfg_scale_caption=cfg_scale_caption,
|
| 892 |
+
cfg_scale_speaker=cfg_scale_speaker,
|
| 893 |
+
cfg_guidance_mode=cfg_mode,
|
| 894 |
+
cfg_min_t=float(req.cfg_min_t),
|
| 895 |
+
cfg_max_t=float(req.cfg_max_t),
|
| 896 |
+
seed=used_seed,
|
| 897 |
+
truncation_factor=truncation_factor,
|
| 898 |
+
rescale_k=rescale_k,
|
| 899 |
+
rescale_sigma=rescale_sigma,
|
| 900 |
+
use_context_kv_cache=bool(req.context_kv_cache),
|
| 901 |
+
speaker_kv_scale=speaker_kv_scale,
|
| 902 |
+
speaker_kv_max_layers=speaker_kv_max_layers,
|
| 903 |
+
speaker_kv_min_t=speaker_kv_min_t,
|
| 904 |
+
)
|
| 905 |
+
stage_sec = _measure_end(self.model_device, t0)
|
| 906 |
+
stage_timings.append(("sample_rf", stage_sec))
|
| 907 |
+
_log(f"[runtime] sample_rf: {stage_sec * 1000.0:.1f} ms")
|
| 908 |
+
|
| 909 |
+
t0 = _measure_start(self.model_device)
|
| 910 |
+
z = unpatchify_latent(
|
| 911 |
+
z_patched,
|
| 912 |
+
patch_size=self.model_cfg.latent_patch_size,
|
| 913 |
+
latent_dim=self.model_cfg.latent_dim,
|
| 914 |
+
)
|
| 915 |
+
stage_sec = _measure_end(self.model_device, t0)
|
| 916 |
+
stage_timings.append(("unpatchify_latent", stage_sec))
|
| 917 |
+
_log(f"[runtime] unpatchify_latent: {stage_sec * 1000.0:.1f} ms")
|
| 918 |
+
z = z[:, :latent_steps]
|
| 919 |
+
|
| 920 |
+
t0 = _measure_start(self.model_device, self.codec_device)
|
| 921 |
+
trimmed_audios: list[torch.Tensor] = []
|
| 922 |
+
if decode_mode == "batch":
|
| 923 |
+
audio_batch = self.codec.decode_latent(z).cpu()
|
| 924 |
+
for i in range(num_candidates):
|
| 925 |
+
audio_i = audio_batch[i]
|
| 926 |
+
max_samples = target_samples
|
| 927 |
+
if bool(req.trim_tail):
|
| 928 |
+
flattening_point = find_flattening_point(
|
| 929 |
+
z[i],
|
| 930 |
+
window_size=max(1, int(req.tail_window_size)),
|
| 931 |
+
std_threshold=float(req.tail_std_threshold),
|
| 932 |
+
mean_threshold=float(req.tail_mean_threshold),
|
| 933 |
+
)
|
| 934 |
+
flattening_samples = int(
|
| 935 |
+
flattening_point * int(self.codec.model.hop_length)
|
| 936 |
+
)
|
| 937 |
+
if flattening_samples > 0:
|
| 938 |
+
max_samples = min(max_samples, flattening_samples)
|
| 939 |
+
trimmed_audios.append(audio_i[:, :max_samples])
|
| 940 |
+
else:
|
| 941 |
+
for i in range(num_candidates):
|
| 942 |
+
audio_i = self.codec.decode_latent(z[i : i + 1]).cpu()[0]
|
| 943 |
+
max_samples = target_samples
|
| 944 |
+
if bool(req.trim_tail):
|
| 945 |
+
flattening_point = find_flattening_point(
|
| 946 |
+
z[i],
|
| 947 |
+
window_size=max(1, int(req.tail_window_size)),
|
| 948 |
+
std_threshold=float(req.tail_std_threshold),
|
| 949 |
+
mean_threshold=float(req.tail_mean_threshold),
|
| 950 |
+
)
|
| 951 |
+
flattening_samples = int(
|
| 952 |
+
flattening_point * int(self.codec.model.hop_length)
|
| 953 |
+
)
|
| 954 |
+
if flattening_samples > 0:
|
| 955 |
+
max_samples = min(max_samples, flattening_samples)
|
| 956 |
+
trimmed_audios.append(audio_i[:, :max_samples])
|
| 957 |
+
stage_sec = _measure_end(self.model_device, t0, self.codec_device)
|
| 958 |
+
stage_timings.append(("decode_latent", stage_sec))
|
| 959 |
+
_log(f"[runtime] decode_latent ({decode_mode}): {stage_sec * 1000.0:.1f} ms")
|
| 960 |
+
|
| 961 |
+
if self.watermarker.ready:
|
| 962 |
+
t0 = _measure_start(self.codec_device)
|
| 963 |
+
trimmed_audios = self.watermarker.encode_batch(
|
| 964 |
+
trimmed_audios,
|
| 965 |
+
sample_rate=int(self.codec.sample_rate),
|
| 966 |
+
)
|
| 967 |
+
stage_sec = _measure_end(self.codec_device, t0)
|
| 968 |
+
stage_timings.append(("silentcipher_watermark", stage_sec))
|
| 969 |
+
_log(f"[runtime] silentcipher_watermark: {stage_sec * 1000.0:.1f} ms")
|
| 970 |
+
else:
|
| 971 |
+
msg = (
|
| 972 |
+
"warning: SilentCipher watermark is unavailable; generated audio was not "
|
| 973 |
+
"watermarked."
|
| 974 |
+
)
|
| 975 |
+
messages.append(msg)
|
| 976 |
+
_log(msg)
|
| 977 |
+
|
| 978 |
+
total_to_decode = _measure_end(self.model_device, post_load_t0, self.codec_device)
|
| 979 |
+
_log(f"[runtime] total_to_decode: {total_to_decode:.3f} s")
|
| 980 |
+
|
| 981 |
+
_log("[runtime] done synthesize")
|
| 982 |
+
return SamplingResult(
|
| 983 |
+
audio=trimmed_audios[0],
|
| 984 |
+
audios=trimmed_audios,
|
| 985 |
+
sample_rate=int(self.codec.sample_rate),
|
| 986 |
+
stage_timings=stage_timings,
|
| 987 |
+
total_to_decode=total_to_decode,
|
| 988 |
+
used_seed=used_seed,
|
| 989 |
+
messages=messages,
|
| 990 |
+
)
|
| 991 |
+
|
| 992 |
+
def unload(self) -> None:
|
| 993 |
+
del self.model
|
| 994 |
+
del self.tokenizer
|
| 995 |
+
del self.codec
|
| 996 |
+
gc.collect()
|
| 997 |
+
for device in (self.model_device, self.codec_device):
|
| 998 |
+
if device.type == "cuda":
|
| 999 |
+
torch.cuda.empty_cache()
|
| 1000 |
+
elif device.type == "mps":
|
| 1001 |
+
mps = getattr(torch, "mps", None)
|
| 1002 |
+
if mps is not None and hasattr(mps, "empty_cache"):
|
| 1003 |
+
mps.empty_cache()
|
| 1004 |
+
|
| 1005 |
+
|
| 1006 |
+
_RUNTIME_CACHE_LOCK = threading.Lock()
|
| 1007 |
+
_RUNTIME_CACHE_KEY: RuntimeKey | None = None
|
| 1008 |
+
_RUNTIME_CACHE_VALUE: InferenceRuntime | None = None
|
| 1009 |
+
|
| 1010 |
+
|
| 1011 |
+
def get_cached_runtime(key: RuntimeKey) -> tuple[InferenceRuntime, bool]:
|
| 1012 |
+
global _RUNTIME_CACHE_KEY, _RUNTIME_CACHE_VALUE
|
| 1013 |
+
with _RUNTIME_CACHE_LOCK:
|
| 1014 |
+
if _RUNTIME_CACHE_VALUE is not None and _RUNTIME_CACHE_KEY == key:
|
| 1015 |
+
return _RUNTIME_CACHE_VALUE, False
|
| 1016 |
+
|
| 1017 |
+
old_runtime = _RUNTIME_CACHE_VALUE
|
| 1018 |
+
runtime = InferenceRuntime.from_key(key)
|
| 1019 |
+
_RUNTIME_CACHE_KEY = key
|
| 1020 |
+
_RUNTIME_CACHE_VALUE = runtime
|
| 1021 |
+
|
| 1022 |
+
if old_runtime is not None:
|
| 1023 |
+
old_runtime.unload()
|
| 1024 |
+
|
| 1025 |
+
return runtime, True
|
| 1026 |
+
|
| 1027 |
+
|
| 1028 |
+
def clear_cached_runtime() -> None:
|
| 1029 |
+
global _RUNTIME_CACHE_KEY, _RUNTIME_CACHE_VALUE
|
| 1030 |
+
with _RUNTIME_CACHE_LOCK:
|
| 1031 |
+
runtime = _RUNTIME_CACHE_VALUE
|
| 1032 |
+
_RUNTIME_CACHE_KEY = None
|
| 1033 |
+
_RUNTIME_CACHE_VALUE = None
|
| 1034 |
+
|
| 1035 |
+
if runtime is not None:
|
| 1036 |
+
runtime.unload()
|
| 1037 |
+
|
| 1038 |
+
|
| 1039 |
+
def _load_audio(path: str | Path) -> tuple[torch.Tensor, int]:
|
| 1040 |
+
try:
|
| 1041 |
+
return torchaudio.load(str(path))
|
| 1042 |
+
except RuntimeError:
|
| 1043 |
+
import soundfile as sf
|
| 1044 |
+
|
| 1045 |
+
data, sr = sf.read(str(path), dtype="float32")
|
| 1046 |
+
wav = torch.from_numpy(data)
|
| 1047 |
+
if wav.ndim == 1:
|
| 1048 |
+
wav = wav.unsqueeze(0)
|
| 1049 |
+
else:
|
| 1050 |
+
wav = wav.T
|
| 1051 |
+
return wav, sr
|
| 1052 |
+
|
| 1053 |
+
|
| 1054 |
+
def save_wav(path: str | Path, audio: torch.Tensor, sample_rate: int) -> Path:
|
| 1055 |
+
out_path = Path(path)
|
| 1056 |
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 1057 |
+
try:
|
| 1058 |
+
torchaudio.save(str(out_path), audio, sample_rate)
|
| 1059 |
+
except RuntimeError:
|
| 1060 |
+
import soundfile as sf
|
| 1061 |
+
|
| 1062 |
+
sf.write(str(out_path), audio.squeeze(0).numpy(), sample_rate)
|
| 1063 |
+
return out_path
|
irodori_tts/model.py
ADDED
|
@@ -0,0 +1,1491 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
from dataclasses import asdict
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
|
| 10 |
+
from .config import ModelConfig
|
| 11 |
+
|
| 12 |
+
DURATION_SPEAKER_FUSIONS = {
|
| 13 |
+
"concat",
|
| 14 |
+
"adarn",
|
| 15 |
+
"adarn_zero",
|
| 16 |
+
"speaker_cross_attn",
|
| 17 |
+
"text_cross_attn",
|
| 18 |
+
}
|
| 19 |
+
DURATION_ARCHITECTURES = {"pooled", "token_sum_adarn_zero_no_aux"}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0) -> torch.Tensor:
|
| 23 |
+
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
|
| 24 |
+
t = torch.arange(end, dtype=torch.float32)
|
| 25 |
+
freqs = torch.outer(t, freqs)
|
| 26 |
+
return torch.complex(torch.cos(freqs), torch.sin(freqs))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def apply_rotary_emb(x: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
|
| 30 |
+
# x: (B, S, H, Dh), Dh must be even.
|
| 31 |
+
x_ = torch.view_as_complex(x.float().reshape(*x.shape[:3], -1, 2))
|
| 32 |
+
x_ = x_ * freqs_cis[None, :, None, :]
|
| 33 |
+
x_ = torch.view_as_real(x_).reshape_as(x)
|
| 34 |
+
return x_.type_as(x)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def get_timestep_embedding(timestep: torch.Tensor, dim: int) -> torch.Tensor:
|
| 38 |
+
assert dim % 2 == 0
|
| 39 |
+
half = dim // 2
|
| 40 |
+
freqs = 1000.0 * torch.exp(
|
| 41 |
+
-torch.log(torch.tensor(10000.0, device=timestep.device, dtype=torch.float32))
|
| 42 |
+
* torch.arange(half, device=timestep.device, dtype=torch.float32)
|
| 43 |
+
/ half
|
| 44 |
+
)
|
| 45 |
+
args = timestep[:, None].float() * freqs[None, :]
|
| 46 |
+
return torch.cat([torch.cos(args), torch.sin(args)], dim=-1).to(timestep.dtype)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class RMSNorm(nn.Module):
|
| 50 |
+
def __init__(self, dim: int | tuple[int, ...], eps: float = 1e-6):
|
| 51 |
+
super().__init__()
|
| 52 |
+
if isinstance(dim, int):
|
| 53 |
+
dim = (dim,)
|
| 54 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 55 |
+
self.eps = eps
|
| 56 |
+
|
| 57 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 58 |
+
x_dtype = x.dtype
|
| 59 |
+
x = x.float()
|
| 60 |
+
x = x * torch.rsqrt((x * x).mean(dim=-1, keepdim=True) + self.eps)
|
| 61 |
+
return (x * self.weight).to(x_dtype)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class LowRankAdaLN(nn.Module):
|
| 65 |
+
"""
|
| 66 |
+
Echo-style low-rank AdaLN that returns both modulated activations and a residual gate.
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
def __init__(self, model_dim: int, rank: int, eps: float):
|
| 70 |
+
super().__init__()
|
| 71 |
+
rank = max(1, min(int(rank), int(model_dim)))
|
| 72 |
+
self.eps = eps
|
| 73 |
+
self.shift_down = nn.Linear(model_dim, rank, bias=False)
|
| 74 |
+
self.scale_down = nn.Linear(model_dim, rank, bias=False)
|
| 75 |
+
self.gate_down = nn.Linear(model_dim, rank, bias=False)
|
| 76 |
+
self.shift_up = nn.Linear(rank, model_dim, bias=True)
|
| 77 |
+
self.scale_up = nn.Linear(rank, model_dim, bias=True)
|
| 78 |
+
self.gate_up = nn.Linear(rank, model_dim, bias=True)
|
| 79 |
+
# Match Echo/JAX AdaLN behavior: zero-init output projections.
|
| 80 |
+
nn.init.zeros_(self.shift_up.weight)
|
| 81 |
+
nn.init.zeros_(self.scale_up.weight)
|
| 82 |
+
nn.init.zeros_(self.gate_up.weight)
|
| 83 |
+
if self.shift_up.bias is not None:
|
| 84 |
+
nn.init.zeros_(self.shift_up.bias)
|
| 85 |
+
if self.scale_up.bias is not None:
|
| 86 |
+
nn.init.zeros_(self.scale_up.bias)
|
| 87 |
+
if self.gate_up.bias is not None:
|
| 88 |
+
nn.init.zeros_(self.gate_up.bias)
|
| 89 |
+
|
| 90 |
+
def forward(
|
| 91 |
+
self, x: torch.Tensor, cond_embed: torch.Tensor
|
| 92 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 93 |
+
shift, scale, gate = cond_embed.chunk(3, dim=-1)
|
| 94 |
+
shift = self.shift_up(self.shift_down(F.silu(shift))) + shift
|
| 95 |
+
scale = self.scale_up(self.scale_down(F.silu(scale))) + scale
|
| 96 |
+
gate = self.gate_up(self.gate_down(F.silu(gate))) + gate
|
| 97 |
+
|
| 98 |
+
x_dtype = x.dtype
|
| 99 |
+
x = x.float()
|
| 100 |
+
x = x * torch.rsqrt((x * x).mean(dim=-1, keepdim=True) + self.eps)
|
| 101 |
+
x = x * (1.0 + scale) + shift
|
| 102 |
+
gate = torch.tanh(gate)
|
| 103 |
+
return x.to(x_dtype), gate
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def patch_sequence_with_mask(
|
| 107 |
+
seq: torch.Tensor,
|
| 108 |
+
mask: torch.Tensor,
|
| 109 |
+
patch_size: int,
|
| 110 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 111 |
+
"""
|
| 112 |
+
Patch along sequence axis:
|
| 113 |
+
seq: (B, S, D) -> (B, S//patch, D*patch)
|
| 114 |
+
mask: (B, S) -> (B, S//patch) with all() over patch window.
|
| 115 |
+
|
| 116 |
+
Note:
|
| 117 |
+
For speaker conditioning in this project, `seq` is already in
|
| 118 |
+
latent-patched space (D = latent_dim * latent_patch_size).
|
| 119 |
+
This helper applies an additional sequence patching for
|
| 120 |
+
`speaker_patch_size`.
|
| 121 |
+
"""
|
| 122 |
+
if patch_size <= 1:
|
| 123 |
+
return seq, mask
|
| 124 |
+
if seq.ndim != 3 or mask.ndim != 2:
|
| 125 |
+
raise ValueError(
|
| 126 |
+
f"Expected seq=(B,S,D), mask=(B,S), got seq={tuple(seq.shape)} mask={tuple(mask.shape)}"
|
| 127 |
+
)
|
| 128 |
+
if seq.shape[0] != mask.shape[0] or seq.shape[1] != mask.shape[1]:
|
| 129 |
+
raise ValueError(
|
| 130 |
+
f"Sequence/mask shape mismatch: seq={tuple(seq.shape)}, mask={tuple(mask.shape)}. "
|
| 131 |
+
"Expected matching (B,S)."
|
| 132 |
+
)
|
| 133 |
+
bsz, seq_len, dim = seq.shape
|
| 134 |
+
usable = (seq_len // patch_size) * patch_size
|
| 135 |
+
if usable <= 0:
|
| 136 |
+
raise ValueError(
|
| 137 |
+
f"Reference sequence too short for speaker_patch_size={patch_size}: seq_len={seq_len}"
|
| 138 |
+
)
|
| 139 |
+
seq = seq[:, :usable].reshape(bsz, usable // patch_size, dim * patch_size)
|
| 140 |
+
mask = mask[:, :usable].reshape(bsz, usable // patch_size, patch_size).all(dim=-1)
|
| 141 |
+
return seq, mask
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class SelfAttention(nn.Module):
|
| 145 |
+
def __init__(self, dim: int, heads: int, norm_eps: float):
|
| 146 |
+
super().__init__()
|
| 147 |
+
if dim % heads != 0:
|
| 148 |
+
raise ValueError(f"dim={dim} must be divisible by heads={heads}")
|
| 149 |
+
if (dim // heads) % 2 != 0:
|
| 150 |
+
raise ValueError("head_dim must be even for RoPE")
|
| 151 |
+
self.dim = dim
|
| 152 |
+
self.heads = heads
|
| 153 |
+
self.head_dim = dim // heads
|
| 154 |
+
|
| 155 |
+
self.wq = nn.Linear(dim, dim, bias=False)
|
| 156 |
+
self.wk = nn.Linear(dim, dim, bias=False)
|
| 157 |
+
self.wv = nn.Linear(dim, dim, bias=False)
|
| 158 |
+
self.wo = nn.Linear(dim, dim, bias=False)
|
| 159 |
+
self.gate = nn.Linear(dim, dim, bias=False)
|
| 160 |
+
|
| 161 |
+
self.q_norm = RMSNorm((self.heads, self.head_dim), eps=norm_eps)
|
| 162 |
+
self.k_norm = RMSNorm((self.heads, self.head_dim), eps=norm_eps)
|
| 163 |
+
|
| 164 |
+
def forward(
|
| 165 |
+
self,
|
| 166 |
+
x: torch.Tensor,
|
| 167 |
+
key_mask: torch.Tensor | None,
|
| 168 |
+
freqs_cis: torch.Tensor,
|
| 169 |
+
) -> torch.Tensor:
|
| 170 |
+
bsz, seq_len, _ = x.shape
|
| 171 |
+
q = self.wq(x).reshape(bsz, seq_len, self.heads, self.head_dim)
|
| 172 |
+
k = self.wk(x).reshape(bsz, seq_len, self.heads, self.head_dim)
|
| 173 |
+
v = self.wv(x).reshape(bsz, seq_len, self.heads, self.head_dim)
|
| 174 |
+
gate = self.gate(x)
|
| 175 |
+
|
| 176 |
+
q = self.q_norm(q)
|
| 177 |
+
k = self.k_norm(k)
|
| 178 |
+
q = apply_rotary_emb(q, freqs_cis[:seq_len])
|
| 179 |
+
k = apply_rotary_emb(k, freqs_cis[:seq_len])
|
| 180 |
+
|
| 181 |
+
attn_mask = None
|
| 182 |
+
if key_mask is not None:
|
| 183 |
+
attn_mask = key_mask[:, None, None, :]
|
| 184 |
+
|
| 185 |
+
y = F.scaled_dot_product_attention(
|
| 186 |
+
q.transpose(1, 2),
|
| 187 |
+
k.transpose(1, 2),
|
| 188 |
+
v.transpose(1, 2),
|
| 189 |
+
attn_mask=attn_mask,
|
| 190 |
+
is_causal=False,
|
| 191 |
+
).transpose(1, 2)
|
| 192 |
+
y = y.reshape(bsz, seq_len, self.dim)
|
| 193 |
+
y = y * torch.sigmoid(gate)
|
| 194 |
+
return self.wo(y)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class JointAttention(nn.Module):
|
| 198 |
+
"""
|
| 199 |
+
Echo-style joint attention over latent self tokens + conditioning contexts.
|
| 200 |
+
"""
|
| 201 |
+
|
| 202 |
+
def __init__(
|
| 203 |
+
self,
|
| 204 |
+
dim: int,
|
| 205 |
+
heads: int,
|
| 206 |
+
text_ctx_dim: int,
|
| 207 |
+
speaker_ctx_dim: int | None,
|
| 208 |
+
caption_ctx_dim: int | None,
|
| 209 |
+
norm_eps: float,
|
| 210 |
+
):
|
| 211 |
+
super().__init__()
|
| 212 |
+
if dim % heads != 0:
|
| 213 |
+
raise ValueError(f"dim={dim} must be divisible by heads={heads}")
|
| 214 |
+
if (dim // heads) % 2 != 0:
|
| 215 |
+
raise ValueError("head_dim must be even for RoPE")
|
| 216 |
+
self.dim = dim
|
| 217 |
+
self.heads = heads
|
| 218 |
+
self.head_dim = dim // heads
|
| 219 |
+
|
| 220 |
+
self.wq = nn.Linear(dim, dim, bias=False)
|
| 221 |
+
self.wk = nn.Linear(dim, dim, bias=False)
|
| 222 |
+
self.wv = nn.Linear(dim, dim, bias=False)
|
| 223 |
+
self.wk_text = nn.Linear(text_ctx_dim, dim, bias=False)
|
| 224 |
+
self.wv_text = nn.Linear(text_ctx_dim, dim, bias=False)
|
| 225 |
+
self.has_speaker_condition = speaker_ctx_dim is not None
|
| 226 |
+
if self.has_speaker_condition:
|
| 227 |
+
self.wk_speaker = nn.Linear(int(speaker_ctx_dim), dim, bias=False)
|
| 228 |
+
self.wv_speaker = nn.Linear(int(speaker_ctx_dim), dim, bias=False)
|
| 229 |
+
self.has_caption_condition = caption_ctx_dim is not None
|
| 230 |
+
if self.has_caption_condition:
|
| 231 |
+
self.wk_caption = nn.Linear(int(caption_ctx_dim), dim, bias=False)
|
| 232 |
+
self.wv_caption = nn.Linear(int(caption_ctx_dim), dim, bias=False)
|
| 233 |
+
self.gate = nn.Linear(dim, dim, bias=False)
|
| 234 |
+
self.wo = nn.Linear(dim, dim, bias=False)
|
| 235 |
+
|
| 236 |
+
self.q_norm = RMSNorm((self.heads, self.head_dim), eps=norm_eps)
|
| 237 |
+
self.k_norm = RMSNorm((self.heads, self.head_dim), eps=norm_eps)
|
| 238 |
+
|
| 239 |
+
def _apply_rotary_half(self, x: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
|
| 240 |
+
x_rot, x_passthrough = x.chunk(2, dim=-2)
|
| 241 |
+
x_rot = apply_rotary_emb(x_rot, freqs_cis)
|
| 242 |
+
return torch.cat([x_rot, x_passthrough], dim=-2)
|
| 243 |
+
|
| 244 |
+
def project_context_kv(
|
| 245 |
+
self,
|
| 246 |
+
text_context: torch.Tensor,
|
| 247 |
+
speaker_context: torch.Tensor | None,
|
| 248 |
+
caption_context: torch.Tensor | None = None,
|
| 249 |
+
) -> tuple[torch.Tensor, ...]:
|
| 250 |
+
"""
|
| 251 |
+
Precompute conditioning KV projections for static conditioning.
|
| 252 |
+
"""
|
| 253 |
+
bsz = text_context.shape[0]
|
| 254 |
+
k_text = self.wk_text(text_context).reshape(
|
| 255 |
+
bsz, text_context.shape[1], self.heads, self.head_dim
|
| 256 |
+
)
|
| 257 |
+
v_text = self.wv_text(text_context).reshape(
|
| 258 |
+
bsz, text_context.shape[1], self.heads, self.head_dim
|
| 259 |
+
)
|
| 260 |
+
k_text = self.k_norm(k_text)
|
| 261 |
+
projected: list[torch.Tensor] = [k_text, v_text]
|
| 262 |
+
if self.has_speaker_condition:
|
| 263 |
+
if speaker_context is None:
|
| 264 |
+
raise ValueError(
|
| 265 |
+
"speaker_context is required when speaker conditioning is enabled."
|
| 266 |
+
)
|
| 267 |
+
if speaker_context.shape[0] != bsz:
|
| 268 |
+
raise ValueError(
|
| 269 |
+
"Batch mismatch for context projection: "
|
| 270 |
+
f"text={tuple(text_context.shape)} speaker={tuple(speaker_context.shape)}"
|
| 271 |
+
)
|
| 272 |
+
k_speaker = self.wk_speaker(speaker_context).reshape(
|
| 273 |
+
bsz, speaker_context.shape[1], self.heads, self.head_dim
|
| 274 |
+
)
|
| 275 |
+
v_speaker = self.wv_speaker(speaker_context).reshape(
|
| 276 |
+
bsz, speaker_context.shape[1], self.heads, self.head_dim
|
| 277 |
+
)
|
| 278 |
+
k_speaker = self.k_norm(k_speaker)
|
| 279 |
+
projected.extend([k_speaker, v_speaker])
|
| 280 |
+
elif speaker_context is not None and speaker_context.shape[0] != bsz:
|
| 281 |
+
raise ValueError(
|
| 282 |
+
"Batch mismatch for ignored speaker context: "
|
| 283 |
+
f"text={tuple(text_context.shape)} speaker={tuple(speaker_context.shape)}"
|
| 284 |
+
)
|
| 285 |
+
if not self.has_caption_condition:
|
| 286 |
+
return tuple(projected)
|
| 287 |
+
if caption_context is None:
|
| 288 |
+
raise ValueError("caption_context is required when caption conditioning is enabled.")
|
| 289 |
+
if caption_context.shape[0] != bsz:
|
| 290 |
+
raise ValueError(
|
| 291 |
+
"Batch mismatch for caption context projection: "
|
| 292 |
+
f"text={tuple(text_context.shape)} caption={tuple(caption_context.shape)}"
|
| 293 |
+
)
|
| 294 |
+
k_caption = self.wk_caption(caption_context).reshape(
|
| 295 |
+
bsz, caption_context.shape[1], self.heads, self.head_dim
|
| 296 |
+
)
|
| 297 |
+
v_caption = self.wv_caption(caption_context).reshape(
|
| 298 |
+
bsz, caption_context.shape[1], self.heads, self.head_dim
|
| 299 |
+
)
|
| 300 |
+
k_caption = self.k_norm(k_caption)
|
| 301 |
+
projected.extend([k_caption, v_caption])
|
| 302 |
+
return tuple(projected)
|
| 303 |
+
|
| 304 |
+
def forward(
|
| 305 |
+
self,
|
| 306 |
+
x: torch.Tensor,
|
| 307 |
+
text_context: torch.Tensor,
|
| 308 |
+
text_mask: torch.Tensor | None,
|
| 309 |
+
speaker_context: torch.Tensor | None,
|
| 310 |
+
speaker_mask: torch.Tensor | None,
|
| 311 |
+
caption_context: torch.Tensor | None,
|
| 312 |
+
caption_mask: torch.Tensor | None,
|
| 313 |
+
freqs_cis: torch.Tensor,
|
| 314 |
+
self_mask: torch.Tensor | None = None,
|
| 315 |
+
context_kv: tuple[torch.Tensor, ...] | None = None,
|
| 316 |
+
) -> torch.Tensor:
|
| 317 |
+
bsz, seq_len, _ = x.shape
|
| 318 |
+
q = self.wq(x).reshape(bsz, seq_len, self.heads, self.head_dim)
|
| 319 |
+
k_self = self.wk(x).reshape(bsz, seq_len, self.heads, self.head_dim)
|
| 320 |
+
v_self = self.wv(x).reshape(bsz, seq_len, self.heads, self.head_dim)
|
| 321 |
+
if context_kv is None:
|
| 322 |
+
projected = self.project_context_kv(
|
| 323 |
+
text_context=text_context,
|
| 324 |
+
speaker_context=speaker_context,
|
| 325 |
+
caption_context=caption_context,
|
| 326 |
+
)
|
| 327 |
+
else:
|
| 328 |
+
projected = context_kv
|
| 329 |
+
if projected is None:
|
| 330 |
+
raise RuntimeError("JointAttention projected context unexpectedly missing.")
|
| 331 |
+
offset = 0
|
| 332 |
+
k_text, v_text = projected[offset], projected[offset + 1]
|
| 333 |
+
offset += 2
|
| 334 |
+
k_speaker = None
|
| 335 |
+
v_speaker = None
|
| 336 |
+
if self.has_speaker_condition:
|
| 337 |
+
k_speaker, v_speaker = projected[offset], projected[offset + 1]
|
| 338 |
+
offset += 2
|
| 339 |
+
k_caption = None
|
| 340 |
+
v_caption = None
|
| 341 |
+
if self.has_caption_condition:
|
| 342 |
+
k_caption, v_caption = projected[offset], projected[offset + 1]
|
| 343 |
+
|
| 344 |
+
q = self.q_norm(q)
|
| 345 |
+
k_self = self.k_norm(k_self)
|
| 346 |
+
q = self._apply_rotary_half(q, freqs_cis[:seq_len])
|
| 347 |
+
k_self = self._apply_rotary_half(k_self, freqs_cis[:seq_len])
|
| 348 |
+
|
| 349 |
+
if self_mask is None:
|
| 350 |
+
self_mask = torch.ones((bsz, seq_len), dtype=torch.bool, device=x.device)
|
| 351 |
+
if text_mask is None:
|
| 352 |
+
text_mask = torch.ones(
|
| 353 |
+
(bsz, text_context.shape[1]),
|
| 354 |
+
dtype=torch.bool,
|
| 355 |
+
device=x.device,
|
| 356 |
+
)
|
| 357 |
+
context_k = [k_self, k_text]
|
| 358 |
+
context_v = [v_self, v_text]
|
| 359 |
+
context_masks = [self_mask, text_mask]
|
| 360 |
+
if self.has_speaker_condition:
|
| 361 |
+
if speaker_context is None or k_speaker is None or v_speaker is None:
|
| 362 |
+
raise ValueError(
|
| 363 |
+
"speaker_context is required when speaker conditioning is enabled."
|
| 364 |
+
)
|
| 365 |
+
if speaker_mask is None:
|
| 366 |
+
speaker_mask = torch.ones(
|
| 367 |
+
(bsz, speaker_context.shape[1]),
|
| 368 |
+
dtype=torch.bool,
|
| 369 |
+
device=x.device,
|
| 370 |
+
)
|
| 371 |
+
context_k.append(k_speaker)
|
| 372 |
+
context_v.append(v_speaker)
|
| 373 |
+
context_masks.append(speaker_mask)
|
| 374 |
+
if self.has_caption_condition:
|
| 375 |
+
if caption_context is None:
|
| 376 |
+
raise ValueError(
|
| 377 |
+
"caption_context is required when caption conditioning is enabled."
|
| 378 |
+
)
|
| 379 |
+
if caption_mask is None:
|
| 380 |
+
caption_mask = torch.ones(
|
| 381 |
+
(bsz, caption_context.shape[1]),
|
| 382 |
+
dtype=torch.bool,
|
| 383 |
+
device=x.device,
|
| 384 |
+
)
|
| 385 |
+
if k_caption is None or v_caption is None:
|
| 386 |
+
raise RuntimeError(
|
| 387 |
+
"Caption projections are missing despite enabled caption conditioning."
|
| 388 |
+
)
|
| 389 |
+
context_k.append(k_caption)
|
| 390 |
+
context_v.append(v_caption)
|
| 391 |
+
context_masks.append(caption_mask)
|
| 392 |
+
|
| 393 |
+
k = torch.cat(context_k, dim=1)
|
| 394 |
+
v = torch.cat(context_v, dim=1)
|
| 395 |
+
attn_mask = torch.cat(context_masks, dim=1)
|
| 396 |
+
attn_mask = attn_mask[:, None, None, :]
|
| 397 |
+
|
| 398 |
+
y = F.scaled_dot_product_attention(
|
| 399 |
+
q.transpose(1, 2),
|
| 400 |
+
k.transpose(1, 2),
|
| 401 |
+
v.transpose(1, 2),
|
| 402 |
+
attn_mask=attn_mask,
|
| 403 |
+
is_causal=False,
|
| 404 |
+
).transpose(1, 2)
|
| 405 |
+
y = y.reshape(bsz, seq_len, self.dim)
|
| 406 |
+
y = y * torch.sigmoid(self.gate(x))
|
| 407 |
+
return self.wo(y)
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
class SwiGLU(nn.Module):
|
| 411 |
+
def __init__(self, dim: int, hidden_dim: int):
|
| 412 |
+
super().__init__()
|
| 413 |
+
self.w1 = nn.Linear(dim, hidden_dim, bias=False)
|
| 414 |
+
self.w2 = nn.Linear(hidden_dim, dim, bias=False)
|
| 415 |
+
self.w3 = nn.Linear(dim, hidden_dim, bias=False)
|
| 416 |
+
|
| 417 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 418 |
+
return self.w2(F.silu(self.w1(x)) * self.w3(x))
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def _safe_attention_mask(
|
| 422 |
+
x: torch.Tensor,
|
| 423 |
+
mask: torch.Tensor,
|
| 424 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 425 |
+
if mask.ndim != 2 or mask.shape[0] != x.shape[0] or mask.shape[1] != x.shape[1]:
|
| 426 |
+
raise ValueError(
|
| 427 |
+
f"mask must have shape (B, S) matching x, got x={tuple(x.shape)} "
|
| 428 |
+
f"mask={tuple(mask.shape)}"
|
| 429 |
+
)
|
| 430 |
+
mask = mask.to(device=x.device, dtype=torch.bool)
|
| 431 |
+
has_any = mask.any(dim=1)
|
| 432 |
+
if bool(has_any.all()):
|
| 433 |
+
return x, mask
|
| 434 |
+
if x.shape[1] <= 0:
|
| 435 |
+
raise ValueError("Cannot attention-pool an empty sequence.")
|
| 436 |
+
x = x.clone()
|
| 437 |
+
mask = mask.clone()
|
| 438 |
+
x[~has_any] = 0
|
| 439 |
+
mask[~has_any, 0] = True
|
| 440 |
+
return x, mask
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
class AttentionPooling(nn.Module):
|
| 444 |
+
def __init__(self, dim: int, heads: int, norm_eps: float):
|
| 445 |
+
super().__init__()
|
| 446 |
+
if dim % heads != 0:
|
| 447 |
+
raise ValueError(f"dim={dim} must be divisible by heads={heads}")
|
| 448 |
+
self.dim = int(dim)
|
| 449 |
+
self.heads = int(heads)
|
| 450 |
+
self.head_dim = int(dim) // int(heads)
|
| 451 |
+
self.query = nn.Parameter(torch.empty(1, 1, int(dim)))
|
| 452 |
+
nn.init.normal_(self.query, mean=0.0, std=0.02)
|
| 453 |
+
self.q_norm = RMSNorm(dim, eps=norm_eps)
|
| 454 |
+
self.k_norm = RMSNorm(dim, eps=norm_eps)
|
| 455 |
+
self.wq = nn.Linear(dim, dim, bias=False)
|
| 456 |
+
self.wk = nn.Linear(dim, dim, bias=False)
|
| 457 |
+
self.wv = nn.Linear(dim, dim, bias=False)
|
| 458 |
+
self.wo = nn.Linear(dim, dim, bias=False)
|
| 459 |
+
|
| 460 |
+
def forward(self, x: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 461 |
+
if x.ndim != 3 or x.shape[-1] != self.dim:
|
| 462 |
+
raise ValueError(f"x must have shape (B, S, {self.dim}), got {tuple(x.shape)}")
|
| 463 |
+
x, mask = _safe_attention_mask(x, mask)
|
| 464 |
+
bsz, seq_len, _ = x.shape
|
| 465 |
+
q = self.query.to(dtype=x.dtype).expand(bsz, -1, -1)
|
| 466 |
+
q = self.wq(self.q_norm(q)).reshape(bsz, 1, self.heads, self.head_dim)
|
| 467 |
+
k = self.wk(self.k_norm(x)).reshape(bsz, seq_len, self.heads, self.head_dim)
|
| 468 |
+
v = self.wv(x).reshape(bsz, seq_len, self.heads, self.head_dim)
|
| 469 |
+
y = F.scaled_dot_product_attention(
|
| 470 |
+
q.transpose(1, 2),
|
| 471 |
+
k.transpose(1, 2),
|
| 472 |
+
v.transpose(1, 2),
|
| 473 |
+
attn_mask=mask[:, None, None, :],
|
| 474 |
+
is_causal=False,
|
| 475 |
+
)
|
| 476 |
+
y = y.transpose(1, 2).reshape(bsz, 1, self.dim)
|
| 477 |
+
return self.wo(y).squeeze(1)
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
class CrossAttentionPooling(nn.Module):
|
| 481 |
+
def __init__(
|
| 482 |
+
self,
|
| 483 |
+
*,
|
| 484 |
+
query_dim: int,
|
| 485 |
+
context_dim: int,
|
| 486 |
+
output_dim: int,
|
| 487 |
+
heads: int,
|
| 488 |
+
norm_eps: float,
|
| 489 |
+
):
|
| 490 |
+
super().__init__()
|
| 491 |
+
if output_dim % heads != 0:
|
| 492 |
+
raise ValueError(f"output_dim={output_dim} must be divisible by heads={heads}")
|
| 493 |
+
self.query_dim = int(query_dim)
|
| 494 |
+
self.context_dim = int(context_dim)
|
| 495 |
+
self.output_dim = int(output_dim)
|
| 496 |
+
self.heads = int(heads)
|
| 497 |
+
self.head_dim = int(output_dim) // int(heads)
|
| 498 |
+
self.q_norm = RMSNorm(query_dim, eps=norm_eps)
|
| 499 |
+
self.k_norm = RMSNorm(context_dim, eps=norm_eps)
|
| 500 |
+
self.wq = nn.Linear(query_dim, output_dim, bias=False)
|
| 501 |
+
self.wk = nn.Linear(context_dim, output_dim, bias=False)
|
| 502 |
+
self.wv = nn.Linear(context_dim, output_dim, bias=False)
|
| 503 |
+
self.wo = nn.Linear(output_dim, output_dim, bias=False)
|
| 504 |
+
|
| 505 |
+
def forward(
|
| 506 |
+
self,
|
| 507 |
+
query: torch.Tensor,
|
| 508 |
+
context: torch.Tensor,
|
| 509 |
+
context_mask: torch.Tensor,
|
| 510 |
+
) -> torch.Tensor:
|
| 511 |
+
if query.ndim != 2 or query.shape[-1] != self.query_dim:
|
| 512 |
+
raise ValueError(
|
| 513 |
+
f"query must have shape (B, {self.query_dim}), got {tuple(query.shape)}"
|
| 514 |
+
)
|
| 515 |
+
if context.ndim != 3 or context.shape[-1] != self.context_dim:
|
| 516 |
+
raise ValueError(
|
| 517 |
+
f"context must have shape (B, S, {self.context_dim}), got {tuple(context.shape)}"
|
| 518 |
+
)
|
| 519 |
+
context, context_mask = _safe_attention_mask(context, context_mask)
|
| 520 |
+
bsz, seq_len, _ = context.shape
|
| 521 |
+
q = query[:, None, :]
|
| 522 |
+
q = self.wq(self.q_norm(q)).reshape(bsz, 1, self.heads, self.head_dim)
|
| 523 |
+
k = self.wk(self.k_norm(context)).reshape(bsz, seq_len, self.heads, self.head_dim)
|
| 524 |
+
v = self.wv(context).reshape(bsz, seq_len, self.heads, self.head_dim)
|
| 525 |
+
y = F.scaled_dot_product_attention(
|
| 526 |
+
q.transpose(1, 2),
|
| 527 |
+
k.transpose(1, 2),
|
| 528 |
+
v.transpose(1, 2),
|
| 529 |
+
attn_mask=context_mask[:, None, None, :],
|
| 530 |
+
is_causal=False,
|
| 531 |
+
)
|
| 532 |
+
y = y.transpose(1, 2).reshape(bsz, 1, self.output_dim)
|
| 533 |
+
return self.wo(y).squeeze(1)
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
class DurationSwiGLUBlock(nn.Module):
|
| 537 |
+
def __init__(
|
| 538 |
+
self,
|
| 539 |
+
*,
|
| 540 |
+
dim: int,
|
| 541 |
+
hidden_dim: int,
|
| 542 |
+
dropout: float,
|
| 543 |
+
norm_eps: float,
|
| 544 |
+
cond_dim: int | None = None,
|
| 545 |
+
):
|
| 546 |
+
super().__init__()
|
| 547 |
+
self.norm = RMSNorm(dim, eps=norm_eps)
|
| 548 |
+
self.mlp = SwiGLU(dim, hidden_dim)
|
| 549 |
+
self.dropout = nn.Dropout(dropout)
|
| 550 |
+
self.cond_dim = cond_dim
|
| 551 |
+
self.modulation = None
|
| 552 |
+
if cond_dim is not None:
|
| 553 |
+
self.modulation = nn.Linear(cond_dim, dim * 3, bias=True)
|
| 554 |
+
nn.init.zeros_(self.modulation.weight)
|
| 555 |
+
nn.init.zeros_(self.modulation.bias)
|
| 556 |
+
|
| 557 |
+
def forward(self, x: torch.Tensor, cond: torch.Tensor | None = None) -> torch.Tensor:
|
| 558 |
+
h = self.norm(x)
|
| 559 |
+
if self.modulation is not None:
|
| 560 |
+
if cond is None:
|
| 561 |
+
raise ValueError("cond is required for AdaRN-Zero duration blocks.")
|
| 562 |
+
shift, scale, gate = self.modulation(F.silu(cond)).chunk(3, dim=-1)
|
| 563 |
+
if h.ndim == 3 and shift.ndim == 2:
|
| 564 |
+
shift = shift.unsqueeze(1)
|
| 565 |
+
scale = scale.unsqueeze(1)
|
| 566 |
+
gate = gate.unsqueeze(1)
|
| 567 |
+
h = h * (1.0 + scale) + shift
|
| 568 |
+
return x + self.dropout(torch.tanh(gate) * self.mlp(h))
|
| 569 |
+
return x + self.dropout(self.mlp(h))
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
class TextBlock(nn.Module):
|
| 573 |
+
def __init__(self, dim: int, heads: int, mlp_ratio: float, norm_eps: float, dropout: float):
|
| 574 |
+
super().__init__()
|
| 575 |
+
self.attention_norm = RMSNorm(dim, eps=norm_eps)
|
| 576 |
+
self.attention = SelfAttention(dim, heads, norm_eps=norm_eps)
|
| 577 |
+
self.mlp_norm = RMSNorm(dim, eps=norm_eps)
|
| 578 |
+
self.mlp = SwiGLU(dim, int(dim * mlp_ratio))
|
| 579 |
+
self.dropout = nn.Dropout(dropout)
|
| 580 |
+
|
| 581 |
+
def forward(self, x: torch.Tensor, mask: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
|
| 582 |
+
x = x + self.dropout(
|
| 583 |
+
self.attention(self.attention_norm(x), key_mask=mask, freqs_cis=freqs_cis)
|
| 584 |
+
)
|
| 585 |
+
x = x + self.dropout(self.mlp(self.mlp_norm(x)))
|
| 586 |
+
return x
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
class TextEncoder(nn.Module):
|
| 590 |
+
def __init__(
|
| 591 |
+
self,
|
| 592 |
+
*,
|
| 593 |
+
vocab_size: int,
|
| 594 |
+
dim: int,
|
| 595 |
+
layers: int,
|
| 596 |
+
heads: int,
|
| 597 |
+
mlp_ratio: float,
|
| 598 |
+
norm_eps: float,
|
| 599 |
+
dropout: float,
|
| 600 |
+
):
|
| 601 |
+
super().__init__()
|
| 602 |
+
self.text_embedding = nn.Embedding(vocab_size, dim)
|
| 603 |
+
self.blocks = nn.ModuleList(
|
| 604 |
+
TextBlock(
|
| 605 |
+
dim=dim,
|
| 606 |
+
heads=heads,
|
| 607 |
+
mlp_ratio=mlp_ratio,
|
| 608 |
+
norm_eps=norm_eps,
|
| 609 |
+
dropout=dropout,
|
| 610 |
+
)
|
| 611 |
+
for _ in range(layers)
|
| 612 |
+
)
|
| 613 |
+
self.head_dim = dim // heads
|
| 614 |
+
self.register_buffer(
|
| 615 |
+
"_freqs_cis_cache", torch.empty(0, 0, dtype=torch.complex64), persistent=False
|
| 616 |
+
)
|
| 617 |
+
|
| 618 |
+
def _rope_freqs(self, seq_len: int, device: torch.device) -> torch.Tensor:
|
| 619 |
+
cache = self._freqs_cis_cache
|
| 620 |
+
if cache.device != device or cache.shape[0] < seq_len:
|
| 621 |
+
cache = precompute_freqs_cis(self.head_dim, seq_len).to(device)
|
| 622 |
+
self._freqs_cis_cache = cache
|
| 623 |
+
return cache[:seq_len]
|
| 624 |
+
|
| 625 |
+
def forward(self, input_ids: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 626 |
+
x = self.text_embedding(input_ids)
|
| 627 |
+
# Hard-mask invalid tokens so fully-masked conditioning becomes truly unconditional.
|
| 628 |
+
mask_f = mask.unsqueeze(-1).to(dtype=x.dtype)
|
| 629 |
+
x = x * mask_f
|
| 630 |
+
freqs = self._rope_freqs(input_ids.shape[1], x.device)
|
| 631 |
+
for block in self.blocks:
|
| 632 |
+
x = block(x, mask=mask, freqs_cis=freqs)
|
| 633 |
+
x = x * mask_f
|
| 634 |
+
return x * mask_f
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
class ReferenceLatentEncoder(nn.Module):
|
| 638 |
+
"""
|
| 639 |
+
Encoder for reference latents used as speaker/style conditioning.
|
| 640 |
+
"""
|
| 641 |
+
|
| 642 |
+
def __init__(self, cfg: ModelConfig):
|
| 643 |
+
super().__init__()
|
| 644 |
+
self.in_proj = nn.Linear(cfg.speaker_patched_latent_dim, cfg.speaker_dim, bias=True)
|
| 645 |
+
speaker_mlp_ratio = cfg.speaker_mlp_ratio_resolved
|
| 646 |
+
self.blocks = nn.ModuleList(
|
| 647 |
+
TextBlock(
|
| 648 |
+
dim=cfg.speaker_dim,
|
| 649 |
+
heads=cfg.speaker_heads,
|
| 650 |
+
mlp_ratio=speaker_mlp_ratio,
|
| 651 |
+
norm_eps=cfg.norm_eps,
|
| 652 |
+
dropout=cfg.dropout,
|
| 653 |
+
)
|
| 654 |
+
for _ in range(cfg.speaker_layers)
|
| 655 |
+
)
|
| 656 |
+
self.head_dim = cfg.speaker_dim // cfg.speaker_heads
|
| 657 |
+
self.register_buffer(
|
| 658 |
+
"_freqs_cis_cache", torch.empty(0, 0, dtype=torch.complex64), persistent=False
|
| 659 |
+
)
|
| 660 |
+
|
| 661 |
+
def _rope_freqs(self, seq_len: int, device: torch.device) -> torch.Tensor:
|
| 662 |
+
cache = self._freqs_cis_cache
|
| 663 |
+
if cache.device != device or cache.shape[0] < seq_len:
|
| 664 |
+
cache = precompute_freqs_cis(self.head_dim, seq_len).to(device)
|
| 665 |
+
self._freqs_cis_cache = cache
|
| 666 |
+
return cache[:seq_len]
|
| 667 |
+
|
| 668 |
+
def forward(self, latent: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 669 |
+
x = self.in_proj(latent)
|
| 670 |
+
x = x / 6.0
|
| 671 |
+
# Keep masked reference positions strictly zero across residual/MLP paths.
|
| 672 |
+
mask_f = mask.unsqueeze(-1).to(dtype=x.dtype)
|
| 673 |
+
x = x * mask_f
|
| 674 |
+
freqs = self._rope_freqs(x.shape[1], x.device)
|
| 675 |
+
for block in self.blocks:
|
| 676 |
+
x = block(x, mask=mask, freqs_cis=freqs)
|
| 677 |
+
x = x * mask_f
|
| 678 |
+
return x * mask_f
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
class DiffusionBlock(nn.Module):
|
| 682 |
+
def __init__(self, cfg: ModelConfig):
|
| 683 |
+
super().__init__()
|
| 684 |
+
self.attention = JointAttention(
|
| 685 |
+
cfg.model_dim,
|
| 686 |
+
cfg.num_heads,
|
| 687 |
+
cfg.text_dim,
|
| 688 |
+
cfg.speaker_dim if cfg.use_speaker_condition else None,
|
| 689 |
+
cfg.caption_dim_resolved if cfg.use_caption_condition else None,
|
| 690 |
+
norm_eps=cfg.norm_eps,
|
| 691 |
+
)
|
| 692 |
+
self.mlp = SwiGLU(cfg.model_dim, int(cfg.model_dim * cfg.mlp_ratio))
|
| 693 |
+
adaln_rank = max(1, min(int(cfg.adaln_rank), int(cfg.model_dim)))
|
| 694 |
+
self.attention_adaln = LowRankAdaLN(
|
| 695 |
+
model_dim=cfg.model_dim,
|
| 696 |
+
rank=adaln_rank,
|
| 697 |
+
eps=cfg.norm_eps,
|
| 698 |
+
)
|
| 699 |
+
self.mlp_adaln = LowRankAdaLN(
|
| 700 |
+
model_dim=cfg.model_dim,
|
| 701 |
+
rank=adaln_rank,
|
| 702 |
+
eps=cfg.norm_eps,
|
| 703 |
+
)
|
| 704 |
+
self.dropout = nn.Dropout(cfg.dropout)
|
| 705 |
+
|
| 706 |
+
def forward(
|
| 707 |
+
self,
|
| 708 |
+
x: torch.Tensor,
|
| 709 |
+
cond_embed: torch.Tensor,
|
| 710 |
+
text_state: torch.Tensor,
|
| 711 |
+
text_mask: torch.Tensor,
|
| 712 |
+
speaker_state: torch.Tensor | None,
|
| 713 |
+
speaker_mask: torch.Tensor | None,
|
| 714 |
+
caption_state: torch.Tensor | None,
|
| 715 |
+
caption_mask: torch.Tensor | None,
|
| 716 |
+
freqs_cis: torch.Tensor,
|
| 717 |
+
self_mask: torch.Tensor | None = None,
|
| 718 |
+
context_kv: tuple[torch.Tensor, ...] | None = None,
|
| 719 |
+
) -> torch.Tensor:
|
| 720 |
+
h, attention_gate = self.attention_adaln(x, cond_embed)
|
| 721 |
+
x = x + self.dropout(
|
| 722 |
+
attention_gate
|
| 723 |
+
* self.attention(
|
| 724 |
+
x=h,
|
| 725 |
+
text_context=text_state,
|
| 726 |
+
text_mask=text_mask,
|
| 727 |
+
speaker_context=speaker_state,
|
| 728 |
+
speaker_mask=speaker_mask,
|
| 729 |
+
caption_context=caption_state,
|
| 730 |
+
caption_mask=caption_mask,
|
| 731 |
+
freqs_cis=freqs_cis,
|
| 732 |
+
self_mask=self_mask,
|
| 733 |
+
context_kv=context_kv,
|
| 734 |
+
)
|
| 735 |
+
)
|
| 736 |
+
|
| 737 |
+
h, mlp_gate = self.mlp_adaln(x, cond_embed)
|
| 738 |
+
x = x + self.dropout(mlp_gate * self.mlp(h))
|
| 739 |
+
return x
|
| 740 |
+
|
| 741 |
+
|
| 742 |
+
class DurationPredictor(nn.Module):
|
| 743 |
+
def __init__(
|
| 744 |
+
self,
|
| 745 |
+
*,
|
| 746 |
+
text_dim: int,
|
| 747 |
+
aux_dim: int,
|
| 748 |
+
hidden_dim: int,
|
| 749 |
+
layers: int,
|
| 750 |
+
dropout: float,
|
| 751 |
+
speaker_dim: int | None = None,
|
| 752 |
+
speaker_fusion: str = "concat",
|
| 753 |
+
attention_heads: int = 8,
|
| 754 |
+
norm_eps: float = 1e-5,
|
| 755 |
+
architecture: str = "pooled",
|
| 756 |
+
token_init_frames: float = 6.3,
|
| 757 |
+
):
|
| 758 |
+
super().__init__()
|
| 759 |
+
if text_dim <= 0:
|
| 760 |
+
raise ValueError(f"duration predictor text_dim must be > 0, got {text_dim}")
|
| 761 |
+
if aux_dim <= 0:
|
| 762 |
+
raise ValueError(f"duration predictor aux_dim must be > 0, got {aux_dim}")
|
| 763 |
+
if hidden_dim <= 0:
|
| 764 |
+
raise ValueError(f"duration predictor hidden_dim must be > 0, got {hidden_dim}")
|
| 765 |
+
if layers <= 0:
|
| 766 |
+
raise ValueError(f"duration predictor layers must be > 0, got {layers}")
|
| 767 |
+
if speaker_dim is not None and speaker_dim <= 0:
|
| 768 |
+
raise ValueError(f"duration predictor speaker_dim must be > 0, got {speaker_dim}")
|
| 769 |
+
speaker_fusion = str(speaker_fusion).strip().lower()
|
| 770 |
+
if speaker_fusion not in DURATION_SPEAKER_FUSIONS:
|
| 771 |
+
raise ValueError(
|
| 772 |
+
f"duration speaker fusion must be one of {sorted(DURATION_SPEAKER_FUSIONS)}, "
|
| 773 |
+
f"got {speaker_fusion!r}"
|
| 774 |
+
)
|
| 775 |
+
architecture = str(architecture).strip().lower()
|
| 776 |
+
if architecture not in DURATION_ARCHITECTURES:
|
| 777 |
+
raise ValueError(
|
| 778 |
+
"duration architecture must be one of "
|
| 779 |
+
f"{sorted(DURATION_ARCHITECTURES)}, got {architecture!r}"
|
| 780 |
+
)
|
| 781 |
+
if attention_heads <= 0:
|
| 782 |
+
raise ValueError(
|
| 783 |
+
f"duration predictor attention_heads must be > 0, got {attention_heads}"
|
| 784 |
+
)
|
| 785 |
+
if token_init_frames <= 0:
|
| 786 |
+
raise ValueError(
|
| 787 |
+
f"duration token_init_frames must be > 0, got {token_init_frames}"
|
| 788 |
+
)
|
| 789 |
+
if speaker_dim is None and speaker_fusion != "concat":
|
| 790 |
+
raise ValueError(
|
| 791 |
+
f"duration speaker fusion {speaker_fusion!r} requires speaker_dim."
|
| 792 |
+
)
|
| 793 |
+
if architecture == "token_sum_adarn_zero_no_aux" and speaker_dim is None:
|
| 794 |
+
raise ValueError("token_sum_adarn_zero_no_aux requires speaker_dim.")
|
| 795 |
+
if architecture == "token_sum_adarn_zero_no_aux" and speaker_fusion != "adarn_zero":
|
| 796 |
+
raise ValueError(
|
| 797 |
+
"token_sum_adarn_zero_no_aux uses block-level speaker AdaRN-Zero and "
|
| 798 |
+
"requires speaker_fusion='adarn_zero'."
|
| 799 |
+
)
|
| 800 |
+
|
| 801 |
+
self.text_dim = int(text_dim)
|
| 802 |
+
self.aux_dim = int(aux_dim)
|
| 803 |
+
self.hidden_dim = int(hidden_dim)
|
| 804 |
+
self.speaker_dim = None if speaker_dim is None else int(speaker_dim)
|
| 805 |
+
self.speaker_fusion = speaker_fusion
|
| 806 |
+
self.duration_architecture = architecture
|
| 807 |
+
self.text_pool = None
|
| 808 |
+
self.null_speaker = (
|
| 809 |
+
nn.Parameter(torch.zeros(int(speaker_dim))) if speaker_dim is not None else None
|
| 810 |
+
)
|
| 811 |
+
self.text_adarn_norm = None
|
| 812 |
+
self.text_adarn = None
|
| 813 |
+
self.speaker_cross_attn = None
|
| 814 |
+
self.text_cross_attn = None
|
| 815 |
+
self.token_input_proj = None
|
| 816 |
+
self.token_blocks = None
|
| 817 |
+
self.token_out_norm = None
|
| 818 |
+
self.token_out_proj = None
|
| 819 |
+
|
| 820 |
+
if architecture == "token_sum_adarn_zero_no_aux":
|
| 821 |
+
self.token_input_proj = nn.Linear(int(text_dim), int(hidden_dim))
|
| 822 |
+
self.token_blocks = nn.ModuleList(
|
| 823 |
+
DurationSwiGLUBlock(
|
| 824 |
+
dim=int(hidden_dim),
|
| 825 |
+
hidden_dim=int(hidden_dim),
|
| 826 |
+
dropout=float(dropout),
|
| 827 |
+
norm_eps=float(norm_eps),
|
| 828 |
+
cond_dim=int(speaker_dim),
|
| 829 |
+
)
|
| 830 |
+
for _ in range(int(layers))
|
| 831 |
+
)
|
| 832 |
+
self.token_out_norm = RMSNorm(int(hidden_dim), eps=float(norm_eps))
|
| 833 |
+
self.token_out_proj = nn.Linear(int(hidden_dim), 1)
|
| 834 |
+
nn.init.zeros_(self.token_out_proj.weight)
|
| 835 |
+
nn.init.constant_(
|
| 836 |
+
self.token_out_proj.bias,
|
| 837 |
+
float(math.log(math.expm1(float(token_init_frames)))),
|
| 838 |
+
)
|
| 839 |
+
return
|
| 840 |
+
|
| 841 |
+
self.text_pool = AttentionPooling(
|
| 842 |
+
dim=int(text_dim),
|
| 843 |
+
heads=int(attention_heads),
|
| 844 |
+
norm_eps=float(norm_eps),
|
| 845 |
+
)
|
| 846 |
+
|
| 847 |
+
if speaker_dim is not None:
|
| 848 |
+
if speaker_fusion == "concat":
|
| 849 |
+
input_dim = int(text_dim) + int(speaker_dim) + int(aux_dim)
|
| 850 |
+
elif speaker_fusion == "adarn":
|
| 851 |
+
input_dim = int(text_dim) + int(aux_dim)
|
| 852 |
+
self.text_adarn_norm = RMSNorm(int(text_dim), eps=float(norm_eps))
|
| 853 |
+
self.text_adarn = nn.Linear(int(speaker_dim), int(text_dim) * 2)
|
| 854 |
+
nn.init.zeros_(self.text_adarn.weight)
|
| 855 |
+
nn.init.zeros_(self.text_adarn.bias)
|
| 856 |
+
elif speaker_fusion == "adarn_zero":
|
| 857 |
+
input_dim = int(text_dim) + int(aux_dim)
|
| 858 |
+
elif speaker_fusion == "speaker_cross_attn":
|
| 859 |
+
input_dim = int(text_dim) * 2 + int(aux_dim)
|
| 860 |
+
self.speaker_cross_attn = CrossAttentionPooling(
|
| 861 |
+
query_dim=int(text_dim),
|
| 862 |
+
context_dim=int(speaker_dim),
|
| 863 |
+
output_dim=int(text_dim),
|
| 864 |
+
heads=int(attention_heads),
|
| 865 |
+
norm_eps=float(norm_eps),
|
| 866 |
+
)
|
| 867 |
+
elif speaker_fusion == "text_cross_attn":
|
| 868 |
+
input_dim = int(text_dim) + int(speaker_dim) + int(aux_dim)
|
| 869 |
+
self.text_cross_attn = CrossAttentionPooling(
|
| 870 |
+
query_dim=int(speaker_dim),
|
| 871 |
+
context_dim=int(text_dim),
|
| 872 |
+
output_dim=int(text_dim),
|
| 873 |
+
heads=int(attention_heads),
|
| 874 |
+
norm_eps=float(norm_eps),
|
| 875 |
+
)
|
| 876 |
+
else:
|
| 877 |
+
raise RuntimeError(f"Unsupported duration speaker fusion: {speaker_fusion!r}")
|
| 878 |
+
else:
|
| 879 |
+
input_dim = int(text_dim) + int(aux_dim)
|
| 880 |
+
|
| 881 |
+
self.input_proj = nn.Linear(int(input_dim), int(hidden_dim))
|
| 882 |
+
block_cond_dim = int(speaker_dim) if speaker_fusion == "adarn_zero" else None
|
| 883 |
+
self.blocks = nn.ModuleList(
|
| 884 |
+
DurationSwiGLUBlock(
|
| 885 |
+
dim=int(hidden_dim),
|
| 886 |
+
hidden_dim=int(hidden_dim),
|
| 887 |
+
dropout=float(dropout),
|
| 888 |
+
norm_eps=float(norm_eps),
|
| 889 |
+
cond_dim=block_cond_dim,
|
| 890 |
+
)
|
| 891 |
+
for _ in range(int(layers))
|
| 892 |
+
)
|
| 893 |
+
self.out_norm = RMSNorm(int(hidden_dim), eps=float(norm_eps))
|
| 894 |
+
self.out_proj = nn.Linear(int(hidden_dim), 1)
|
| 895 |
+
|
| 896 |
+
def _speaker_vec(
|
| 897 |
+
self,
|
| 898 |
+
*,
|
| 899 |
+
batch_size: int,
|
| 900 |
+
device: torch.device,
|
| 901 |
+
dtype: torch.dtype,
|
| 902 |
+
speaker_state: torch.Tensor | None,
|
| 903 |
+
has_speaker: torch.Tensor,
|
| 904 |
+
) -> torch.Tensor:
|
| 905 |
+
if self.null_speaker is None or self.speaker_dim is None:
|
| 906 |
+
raise RuntimeError("Duration speaker modules are missing.")
|
| 907 |
+
null_vec = self.null_speaker.to(device=device, dtype=dtype)[None, :].expand(
|
| 908 |
+
batch_size, -1
|
| 909 |
+
)
|
| 910 |
+
if speaker_state is None:
|
| 911 |
+
return null_vec
|
| 912 |
+
if speaker_state.ndim != 3 or speaker_state.shape[0] != batch_size:
|
| 913 |
+
raise ValueError(
|
| 914 |
+
f"speaker_state must have shape (B, S, D), got {tuple(speaker_state.shape)}"
|
| 915 |
+
)
|
| 916 |
+
if speaker_state.shape[-1] != self.speaker_dim:
|
| 917 |
+
raise ValueError(
|
| 918 |
+
f"speaker_state last dim must be {self.speaker_dim}, got {speaker_state.shape[-1]}"
|
| 919 |
+
)
|
| 920 |
+
speaker_vec = speaker_state[:, 0].to(device=device, dtype=dtype)
|
| 921 |
+
return torch.where(has_speaker[:, None], speaker_vec, null_vec)
|
| 922 |
+
|
| 923 |
+
def _speaker_sequence(
|
| 924 |
+
self,
|
| 925 |
+
*,
|
| 926 |
+
batch_size: int,
|
| 927 |
+
device: torch.device,
|
| 928 |
+
dtype: torch.dtype,
|
| 929 |
+
speaker_state: torch.Tensor | None,
|
| 930 |
+
speaker_mask: torch.Tensor | None,
|
| 931 |
+
has_speaker: torch.Tensor,
|
| 932 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 933 |
+
if self.null_speaker is None or self.speaker_dim is None:
|
| 934 |
+
raise RuntimeError("Duration speaker modules are missing.")
|
| 935 |
+
null_token = self.null_speaker.to(device=device, dtype=dtype)[None, None, :].expand(
|
| 936 |
+
batch_size, 1, -1
|
| 937 |
+
)
|
| 938 |
+
if speaker_state is None:
|
| 939 |
+
return null_token, torch.ones((batch_size, 1), dtype=torch.bool, device=device)
|
| 940 |
+
if speaker_state.ndim != 3 or speaker_state.shape[0] != batch_size:
|
| 941 |
+
raise ValueError(
|
| 942 |
+
f"speaker_state must have shape (B, S, D), got {tuple(speaker_state.shape)}"
|
| 943 |
+
)
|
| 944 |
+
if speaker_state.shape[-1] != self.speaker_dim:
|
| 945 |
+
raise ValueError(
|
| 946 |
+
f"speaker_state last dim must be {self.speaker_dim}, got {speaker_state.shape[-1]}"
|
| 947 |
+
)
|
| 948 |
+
speaker_state = speaker_state.to(device=device, dtype=dtype)
|
| 949 |
+
if speaker_mask is None:
|
| 950 |
+
speaker_mask = torch.ones(
|
| 951 |
+
(batch_size, speaker_state.shape[1]), dtype=torch.bool, device=device
|
| 952 |
+
)
|
| 953 |
+
elif speaker_mask.ndim != 2 or speaker_mask.shape[:2] != speaker_state.shape[:2]:
|
| 954 |
+
raise ValueError(
|
| 955 |
+
"speaker_mask must have shape matching speaker_state (B, S), "
|
| 956 |
+
f"got speaker_state={tuple(speaker_state.shape)} mask={tuple(speaker_mask.shape)}"
|
| 957 |
+
)
|
| 958 |
+
speaker_mask = speaker_mask.to(device=device, dtype=torch.bool)
|
| 959 |
+
real_mask = speaker_mask & has_speaker[:, None]
|
| 960 |
+
fallback_mask = ~real_mask.any(dim=1, keepdim=True)
|
| 961 |
+
context = torch.cat([speaker_state, null_token], dim=1)
|
| 962 |
+
context_mask = torch.cat([real_mask, fallback_mask], dim=1)
|
| 963 |
+
return context, context_mask
|
| 964 |
+
|
| 965 |
+
def forward(
|
| 966 |
+
self,
|
| 967 |
+
*,
|
| 968 |
+
text_state: torch.Tensor,
|
| 969 |
+
text_mask: torch.Tensor,
|
| 970 |
+
aux_features: torch.Tensor,
|
| 971 |
+
speaker_state: torch.Tensor | None = None,
|
| 972 |
+
speaker_mask: torch.Tensor | None = None,
|
| 973 |
+
has_speaker: torch.Tensor | None = None,
|
| 974 |
+
) -> torch.Tensor:
|
| 975 |
+
if text_state.ndim != 3 or text_state.shape[-1] != self.text_dim:
|
| 976 |
+
raise ValueError(
|
| 977 |
+
f"text_state must have shape (B, S, {self.text_dim}), "
|
| 978 |
+
f"got {tuple(text_state.shape)}"
|
| 979 |
+
)
|
| 980 |
+
if aux_features.ndim != 2 or aux_features.shape[1] != self.aux_dim:
|
| 981 |
+
raise ValueError(
|
| 982 |
+
f"aux_features must have shape (B, {self.aux_dim}), "
|
| 983 |
+
f"got {tuple(aux_features.shape)}"
|
| 984 |
+
)
|
| 985 |
+
if aux_features.shape[0] != text_state.shape[0]:
|
| 986 |
+
raise ValueError(
|
| 987 |
+
"Batch mismatch for duration predictor: "
|
| 988 |
+
f"text_state={tuple(text_state.shape)} aux_features={tuple(aux_features.shape)}"
|
| 989 |
+
)
|
| 990 |
+
text_state, text_mask = _safe_attention_mask(text_state, text_mask)
|
| 991 |
+
aux_features = aux_features.to(device=text_state.device, dtype=text_state.dtype)
|
| 992 |
+
|
| 993 |
+
if self.duration_architecture == "token_sum_adarn_zero_no_aux":
|
| 994 |
+
if self.speaker_dim is None:
|
| 995 |
+
raise RuntimeError("Token-sum duration architecture requires speaker modules.")
|
| 996 |
+
if has_speaker is None:
|
| 997 |
+
raise ValueError(
|
| 998 |
+
"has_speaker is required for speaker-conditioned duration prediction."
|
| 999 |
+
)
|
| 1000 |
+
has_speaker = has_speaker.to(device=text_state.device, dtype=torch.bool)
|
| 1001 |
+
if has_speaker.ndim != 1 or has_speaker.shape[0] != text_state.shape[0]:
|
| 1002 |
+
raise ValueError(
|
| 1003 |
+
f"has_speaker must have shape (B,), got {tuple(has_speaker.shape)}"
|
| 1004 |
+
)
|
| 1005 |
+
speaker_vec = self._speaker_vec(
|
| 1006 |
+
batch_size=text_state.shape[0],
|
| 1007 |
+
device=text_state.device,
|
| 1008 |
+
dtype=text_state.dtype,
|
| 1009 |
+
speaker_state=speaker_state,
|
| 1010 |
+
has_speaker=has_speaker,
|
| 1011 |
+
)
|
| 1012 |
+
if (
|
| 1013 |
+
self.token_input_proj is None
|
| 1014 |
+
or self.token_blocks is None
|
| 1015 |
+
or self.token_out_norm is None
|
| 1016 |
+
or self.token_out_proj is None
|
| 1017 |
+
):
|
| 1018 |
+
raise RuntimeError("Token-sum duration modules are missing.")
|
| 1019 |
+
h = self.token_input_proj(text_state)
|
| 1020 |
+
for block in self.token_blocks:
|
| 1021 |
+
h = block(h, cond=speaker_vec)
|
| 1022 |
+
token_logits = self.token_out_proj(self.token_out_norm(h)).squeeze(-1)
|
| 1023 |
+
token_frames = F.softplus(token_logits.float())
|
| 1024 |
+
total_frames = (token_frames * text_mask.to(dtype=token_frames.dtype)).sum(dim=1)
|
| 1025 |
+
return torch.log1p(total_frames.clamp_min(0.0))
|
| 1026 |
+
|
| 1027 |
+
if self.text_pool is None:
|
| 1028 |
+
raise RuntimeError("Pooled duration modules are missing.")
|
| 1029 |
+
text_vec = self.text_pool(text_state, text_mask)
|
| 1030 |
+
if self.speaker_dim is None:
|
| 1031 |
+
x = torch.cat([text_vec, aux_features], dim=-1)
|
| 1032 |
+
h = self.input_proj(x)
|
| 1033 |
+
for block in self.blocks:
|
| 1034 |
+
h = block(h)
|
| 1035 |
+
return self.out_proj(self.out_norm(h)).squeeze(-1)
|
| 1036 |
+
|
| 1037 |
+
if has_speaker is None:
|
| 1038 |
+
raise ValueError("has_speaker is required for speaker-conditioned duration prediction.")
|
| 1039 |
+
has_speaker = has_speaker.to(device=text_vec.device, dtype=torch.bool)
|
| 1040 |
+
if has_speaker.ndim != 1 or has_speaker.shape[0] != text_vec.shape[0]:
|
| 1041 |
+
raise ValueError(
|
| 1042 |
+
f"has_speaker must have shape (B,), got {tuple(has_speaker.shape)}"
|
| 1043 |
+
)
|
| 1044 |
+
speaker_vec = self._speaker_vec(
|
| 1045 |
+
batch_size=text_vec.shape[0],
|
| 1046 |
+
device=text_vec.device,
|
| 1047 |
+
dtype=text_vec.dtype,
|
| 1048 |
+
speaker_state=speaker_state,
|
| 1049 |
+
has_speaker=has_speaker,
|
| 1050 |
+
)
|
| 1051 |
+
|
| 1052 |
+
if self.speaker_fusion == "concat":
|
| 1053 |
+
x = torch.cat([text_vec, speaker_vec, aux_features], dim=-1)
|
| 1054 |
+
cond = None
|
| 1055 |
+
elif self.speaker_fusion == "adarn":
|
| 1056 |
+
if self.text_adarn_norm is None or self.text_adarn is None:
|
| 1057 |
+
raise RuntimeError("AdaRN duration speaker modules are missing.")
|
| 1058 |
+
scale, shift = self.text_adarn(speaker_vec).chunk(2, dim=-1)
|
| 1059 |
+
text_vec = (self.text_adarn_norm(text_vec) * (1.0 + scale)) + shift
|
| 1060 |
+
x = torch.cat([text_vec, aux_features], dim=-1)
|
| 1061 |
+
cond = None
|
| 1062 |
+
elif self.speaker_fusion == "adarn_zero":
|
| 1063 |
+
x = torch.cat([text_vec, aux_features], dim=-1)
|
| 1064 |
+
cond = speaker_vec
|
| 1065 |
+
elif self.speaker_fusion == "speaker_cross_attn":
|
| 1066 |
+
if self.speaker_cross_attn is None:
|
| 1067 |
+
raise RuntimeError("speaker_cross_attn duration module is missing.")
|
| 1068 |
+
speaker_context, speaker_context_mask = self._speaker_sequence(
|
| 1069 |
+
batch_size=text_vec.shape[0],
|
| 1070 |
+
device=text_vec.device,
|
| 1071 |
+
dtype=text_vec.dtype,
|
| 1072 |
+
speaker_state=speaker_state,
|
| 1073 |
+
speaker_mask=speaker_mask,
|
| 1074 |
+
has_speaker=has_speaker,
|
| 1075 |
+
)
|
| 1076 |
+
context_vec = self.speaker_cross_attn(
|
| 1077 |
+
query=text_vec,
|
| 1078 |
+
context=speaker_context,
|
| 1079 |
+
context_mask=speaker_context_mask,
|
| 1080 |
+
)
|
| 1081 |
+
x = torch.cat([text_vec, context_vec, aux_features], dim=-1)
|
| 1082 |
+
cond = None
|
| 1083 |
+
elif self.speaker_fusion == "text_cross_attn":
|
| 1084 |
+
if self.text_cross_attn is None:
|
| 1085 |
+
raise RuntimeError("text_cross_attn duration module is missing.")
|
| 1086 |
+
context_vec = self.text_cross_attn(
|
| 1087 |
+
query=speaker_vec,
|
| 1088 |
+
context=text_state,
|
| 1089 |
+
context_mask=text_mask,
|
| 1090 |
+
)
|
| 1091 |
+
x = torch.cat([context_vec, speaker_vec, aux_features], dim=-1)
|
| 1092 |
+
cond = None
|
| 1093 |
+
else:
|
| 1094 |
+
raise RuntimeError(f"Unsupported duration speaker fusion: {self.speaker_fusion!r}")
|
| 1095 |
+
|
| 1096 |
+
h = self.input_proj(x)
|
| 1097 |
+
for block in self.blocks:
|
| 1098 |
+
h = block(h, cond=cond)
|
| 1099 |
+
return self.out_proj(self.out_norm(h)).squeeze(-1)
|
| 1100 |
+
|
| 1101 |
+
|
| 1102 |
+
class TextToLatentRFDiT(nn.Module):
|
| 1103 |
+
"""
|
| 1104 |
+
Text + reference-latent conditioned RF diffusion model over patched DACVAE latent sequences.
|
| 1105 |
+
|
| 1106 |
+
Input x_t shape: (B, S, latent_dim * latent_patch_size)
|
| 1107 |
+
Output v_pred shape: same as input.
|
| 1108 |
+
"""
|
| 1109 |
+
|
| 1110 |
+
def __init__(self, cfg: ModelConfig):
|
| 1111 |
+
super().__init__()
|
| 1112 |
+
self.cfg = cfg
|
| 1113 |
+
self.text_encoder = TextEncoder(
|
| 1114 |
+
vocab_size=cfg.text_vocab_size,
|
| 1115 |
+
dim=cfg.text_dim,
|
| 1116 |
+
layers=cfg.text_layers,
|
| 1117 |
+
heads=cfg.text_heads,
|
| 1118 |
+
mlp_ratio=cfg.text_mlp_ratio_resolved,
|
| 1119 |
+
norm_eps=cfg.norm_eps,
|
| 1120 |
+
dropout=cfg.dropout,
|
| 1121 |
+
)
|
| 1122 |
+
self.caption_encoder = None
|
| 1123 |
+
self.caption_norm = None
|
| 1124 |
+
if cfg.use_caption_condition:
|
| 1125 |
+
self.caption_encoder = TextEncoder(
|
| 1126 |
+
vocab_size=cfg.caption_vocab_size_resolved,
|
| 1127 |
+
dim=cfg.caption_dim_resolved,
|
| 1128 |
+
layers=cfg.caption_layers_resolved,
|
| 1129 |
+
heads=cfg.caption_heads_resolved,
|
| 1130 |
+
mlp_ratio=cfg.caption_mlp_ratio_resolved,
|
| 1131 |
+
norm_eps=cfg.norm_eps,
|
| 1132 |
+
dropout=cfg.dropout,
|
| 1133 |
+
)
|
| 1134 |
+
self.caption_norm = RMSNorm(cfg.caption_dim_resolved, eps=cfg.norm_eps)
|
| 1135 |
+
self.speaker_encoder = None
|
| 1136 |
+
if cfg.use_speaker_condition:
|
| 1137 |
+
self.speaker_encoder = ReferenceLatentEncoder(cfg)
|
| 1138 |
+
self.text_norm = RMSNorm(cfg.text_dim, eps=cfg.norm_eps)
|
| 1139 |
+
self.speaker_norm = None
|
| 1140 |
+
if cfg.use_speaker_condition:
|
| 1141 |
+
self.speaker_norm = RMSNorm(cfg.speaker_dim, eps=cfg.norm_eps)
|
| 1142 |
+
self.duration_predictor = None
|
| 1143 |
+
if cfg.use_duration_predictor:
|
| 1144 |
+
duration_speaker_dim = None
|
| 1145 |
+
if cfg.use_speaker_condition:
|
| 1146 |
+
duration_speaker_dim = int(cfg.speaker_dim)
|
| 1147 |
+
self.duration_predictor = DurationPredictor(
|
| 1148 |
+
text_dim=cfg.text_dim,
|
| 1149 |
+
aux_dim=cfg.duration_aux_dim,
|
| 1150 |
+
hidden_dim=cfg.duration_hidden_dim,
|
| 1151 |
+
layers=cfg.duration_layers,
|
| 1152 |
+
dropout=cfg.duration_dropout,
|
| 1153 |
+
speaker_dim=duration_speaker_dim,
|
| 1154 |
+
speaker_fusion=cfg.duration_speaker_fusion,
|
| 1155 |
+
attention_heads=cfg.duration_attention_heads,
|
| 1156 |
+
norm_eps=cfg.norm_eps,
|
| 1157 |
+
architecture=cfg.duration_architecture,
|
| 1158 |
+
token_init_frames=cfg.duration_token_init_frames,
|
| 1159 |
+
)
|
| 1160 |
+
|
| 1161 |
+
self.cond_module = nn.Sequential(
|
| 1162 |
+
nn.Linear(cfg.timestep_embed_dim, cfg.model_dim, bias=False),
|
| 1163 |
+
nn.SiLU(),
|
| 1164 |
+
nn.Linear(cfg.model_dim, cfg.model_dim, bias=False),
|
| 1165 |
+
nn.SiLU(),
|
| 1166 |
+
nn.Linear(cfg.model_dim, cfg.model_dim * 3, bias=False),
|
| 1167 |
+
)
|
| 1168 |
+
|
| 1169 |
+
self.in_proj = nn.Linear(cfg.patched_latent_dim, cfg.model_dim)
|
| 1170 |
+
self.blocks = nn.ModuleList(DiffusionBlock(cfg) for _ in range(cfg.num_layers))
|
| 1171 |
+
self.out_norm = RMSNorm(cfg.model_dim, eps=cfg.norm_eps)
|
| 1172 |
+
self.out_proj = nn.Linear(cfg.model_dim, cfg.patched_latent_dim)
|
| 1173 |
+
# Echo/JAX training initializes decoder out projection to zero for stable early training.
|
| 1174 |
+
nn.init.zeros_(self.out_proj.weight)
|
| 1175 |
+
if self.out_proj.bias is not None:
|
| 1176 |
+
nn.init.zeros_(self.out_proj.bias)
|
| 1177 |
+
|
| 1178 |
+
self.head_dim = cfg.model_dim // cfg.num_heads
|
| 1179 |
+
if self.head_dim % 2 != 0:
|
| 1180 |
+
raise ValueError("model head_dim must be even for RoPE")
|
| 1181 |
+
self.register_buffer(
|
| 1182 |
+
"_freqs_cis_cache", torch.empty(0, 0, dtype=torch.complex64), persistent=False
|
| 1183 |
+
)
|
| 1184 |
+
|
| 1185 |
+
def _rope_freqs(self, seq_len: int, device: torch.device) -> torch.Tensor:
|
| 1186 |
+
cache = self._freqs_cis_cache
|
| 1187 |
+
if cache.device != device or cache.shape[0] < seq_len:
|
| 1188 |
+
cache = precompute_freqs_cis(self.head_dim, seq_len).to(device)
|
| 1189 |
+
self._freqs_cis_cache = cache
|
| 1190 |
+
return cache[:seq_len]
|
| 1191 |
+
|
| 1192 |
+
@staticmethod
|
| 1193 |
+
def _prepend_masked_mean_token(
|
| 1194 |
+
state: torch.Tensor,
|
| 1195 |
+
mask: torch.Tensor,
|
| 1196 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 1197 |
+
"""
|
| 1198 |
+
Prepend one global summary token computed as masked mean over time.
|
| 1199 |
+
"""
|
| 1200 |
+
mask_f = mask.unsqueeze(-1).to(dtype=state.dtype)
|
| 1201 |
+
denom = mask_f.sum(dim=1, keepdim=True).clamp_min(1.0)
|
| 1202 |
+
mean_token = (state * mask_f).sum(dim=1, keepdim=True) / denom
|
| 1203 |
+
has_any = mask.any(dim=1, keepdim=True)
|
| 1204 |
+
state = torch.cat([mean_token, state], dim=1)
|
| 1205 |
+
mask = torch.cat([has_any, mask], dim=1)
|
| 1206 |
+
return state, mask
|
| 1207 |
+
|
| 1208 |
+
def encode_conditions(
|
| 1209 |
+
self,
|
| 1210 |
+
text_input_ids: torch.Tensor,
|
| 1211 |
+
text_mask: torch.Tensor,
|
| 1212 |
+
ref_latent: torch.Tensor | None,
|
| 1213 |
+
ref_mask: torch.Tensor | None,
|
| 1214 |
+
caption_input_ids: torch.Tensor | None = None,
|
| 1215 |
+
caption_mask: torch.Tensor | None = None,
|
| 1216 |
+
text_condition_dropout: torch.Tensor | None = None,
|
| 1217 |
+
speaker_condition_dropout: torch.Tensor | None = None,
|
| 1218 |
+
caption_condition_dropout: torch.Tensor | None = None,
|
| 1219 |
+
) -> tuple[
|
| 1220 |
+
torch.Tensor,
|
| 1221 |
+
torch.Tensor,
|
| 1222 |
+
torch.Tensor | None,
|
| 1223 |
+
torch.Tensor | None,
|
| 1224 |
+
torch.Tensor | None,
|
| 1225 |
+
torch.Tensor | None,
|
| 1226 |
+
]:
|
| 1227 |
+
if text_condition_dropout is not None:
|
| 1228 |
+
text_mask = text_mask.clone()
|
| 1229 |
+
text_mask[text_condition_dropout] = False
|
| 1230 |
+
if self.cfg.use_speaker_condition:
|
| 1231 |
+
if self.speaker_encoder is None or self.speaker_norm is None:
|
| 1232 |
+
raise RuntimeError(
|
| 1233 |
+
"Speaker conditioning is enabled but speaker modules are missing."
|
| 1234 |
+
)
|
| 1235 |
+
if ref_latent is None or ref_mask is None:
|
| 1236 |
+
raise ValueError(
|
| 1237 |
+
"ref_latent and ref_mask are required when speaker conditioning is enabled."
|
| 1238 |
+
)
|
| 1239 |
+
if speaker_condition_dropout is not None:
|
| 1240 |
+
ref_mask = ref_mask.clone()
|
| 1241 |
+
ref_mask[speaker_condition_dropout] = False
|
| 1242 |
+
if self.cfg.use_caption_condition:
|
| 1243 |
+
if self.caption_encoder is None or self.caption_norm is None:
|
| 1244 |
+
raise RuntimeError(
|
| 1245 |
+
"Caption conditioning is enabled but caption modules are missing."
|
| 1246 |
+
)
|
| 1247 |
+
if caption_input_ids is None or caption_mask is None:
|
| 1248 |
+
raise ValueError(
|
| 1249 |
+
"caption_input_ids and caption_mask are required when caption conditioning is enabled."
|
| 1250 |
+
)
|
| 1251 |
+
if caption_condition_dropout is not None:
|
| 1252 |
+
caption_mask = caption_mask.clone()
|
| 1253 |
+
caption_mask[caption_condition_dropout] = False
|
| 1254 |
+
|
| 1255 |
+
text_state = self.text_encoder(text_input_ids, text_mask)
|
| 1256 |
+
text_state = self.text_norm(text_state)
|
| 1257 |
+
ref_state = None
|
| 1258 |
+
if self.cfg.use_speaker_condition:
|
| 1259 |
+
ref_latent, ref_mask = patch_sequence_with_mask(
|
| 1260 |
+
seq=ref_latent,
|
| 1261 |
+
mask=ref_mask,
|
| 1262 |
+
patch_size=self.cfg.speaker_patch_size,
|
| 1263 |
+
)
|
| 1264 |
+
ref_state = self.speaker_encoder(ref_latent, ref_mask)
|
| 1265 |
+
ref_state = self.speaker_norm(ref_state)
|
| 1266 |
+
ref_state, ref_mask = self._prepend_masked_mean_token(ref_state, ref_mask)
|
| 1267 |
+
caption_state = None
|
| 1268 |
+
if self.cfg.use_caption_condition:
|
| 1269 |
+
caption_state = self.caption_encoder(caption_input_ids, caption_mask)
|
| 1270 |
+
caption_state = self.caption_norm(caption_state)
|
| 1271 |
+
return text_state, text_mask, ref_state, ref_mask, caption_state, caption_mask
|
| 1272 |
+
|
| 1273 |
+
def forward_with_encoded_conditions(
|
| 1274 |
+
self,
|
| 1275 |
+
x_t: torch.Tensor,
|
| 1276 |
+
t: torch.Tensor,
|
| 1277 |
+
text_state: torch.Tensor,
|
| 1278 |
+
text_mask: torch.Tensor,
|
| 1279 |
+
speaker_state: torch.Tensor | None,
|
| 1280 |
+
speaker_mask: torch.Tensor | None,
|
| 1281 |
+
caption_state: torch.Tensor | None = None,
|
| 1282 |
+
caption_mask: torch.Tensor | None = None,
|
| 1283 |
+
latent_mask: torch.Tensor | None = None,
|
| 1284 |
+
context_kv_cache: list[tuple[torch.Tensor, ...]] | None = None,
|
| 1285 |
+
) -> torch.Tensor:
|
| 1286 |
+
t_embed = get_timestep_embedding(t, self.cfg.timestep_embed_dim).to(dtype=x_t.dtype)
|
| 1287 |
+
cond_embed = self.cond_module(t_embed)
|
| 1288 |
+
cond_embed = cond_embed[:, None, :]
|
| 1289 |
+
|
| 1290 |
+
x = self.in_proj(x_t)
|
| 1291 |
+
freqs = self._rope_freqs(x.shape[1], x.device)
|
| 1292 |
+
for i, block in enumerate(self.blocks):
|
| 1293 |
+
x = block(
|
| 1294 |
+
x=x,
|
| 1295 |
+
cond_embed=cond_embed,
|
| 1296 |
+
text_state=text_state,
|
| 1297 |
+
text_mask=text_mask,
|
| 1298 |
+
speaker_state=speaker_state,
|
| 1299 |
+
speaker_mask=speaker_mask,
|
| 1300 |
+
caption_state=caption_state,
|
| 1301 |
+
caption_mask=caption_mask,
|
| 1302 |
+
freqs_cis=freqs,
|
| 1303 |
+
self_mask=latent_mask,
|
| 1304 |
+
context_kv=context_kv_cache[i] if context_kv_cache is not None else None,
|
| 1305 |
+
)
|
| 1306 |
+
|
| 1307 |
+
x = self.out_norm(x)
|
| 1308 |
+
x = self.out_proj(x)
|
| 1309 |
+
return x.to(dtype=x_t.dtype)
|
| 1310 |
+
|
| 1311 |
+
def forward(
|
| 1312 |
+
self,
|
| 1313 |
+
x_t: torch.Tensor | None,
|
| 1314 |
+
t: torch.Tensor | None,
|
| 1315 |
+
text_input_ids: torch.Tensor,
|
| 1316 |
+
text_mask: torch.Tensor,
|
| 1317 |
+
ref_latent: torch.Tensor | None,
|
| 1318 |
+
ref_mask: torch.Tensor | None,
|
| 1319 |
+
caption_input_ids: torch.Tensor | None = None,
|
| 1320 |
+
caption_mask: torch.Tensor | None = None,
|
| 1321 |
+
latent_mask: torch.Tensor | None = None,
|
| 1322 |
+
text_condition_dropout: torch.Tensor | None = None,
|
| 1323 |
+
speaker_condition_dropout: torch.Tensor | None = None,
|
| 1324 |
+
caption_condition_dropout: torch.Tensor | None = None,
|
| 1325 |
+
duration_features: torch.Tensor | None = None,
|
| 1326 |
+
duration_has_speaker: torch.Tensor | None = None,
|
| 1327 |
+
duration_only: bool = False,
|
| 1328 |
+
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 1329 |
+
if duration_features is not None:
|
| 1330 |
+
(
|
| 1331 |
+
text_state,
|
| 1332 |
+
text_mask_full,
|
| 1333 |
+
speaker_state,
|
| 1334 |
+
speaker_mask_full,
|
| 1335 |
+
caption_state,
|
| 1336 |
+
caption_mask_full,
|
| 1337 |
+
) = self.encode_conditions(
|
| 1338 |
+
text_input_ids=text_input_ids,
|
| 1339 |
+
text_mask=text_mask,
|
| 1340 |
+
ref_latent=ref_latent,
|
| 1341 |
+
ref_mask=ref_mask,
|
| 1342 |
+
caption_input_ids=caption_input_ids,
|
| 1343 |
+
caption_mask=caption_mask,
|
| 1344 |
+
)
|
| 1345 |
+
if duration_only:
|
| 1346 |
+
return self.predict_duration_log_frames(
|
| 1347 |
+
text_state=text_state,
|
| 1348 |
+
text_mask=text_mask_full,
|
| 1349 |
+
speaker_state=speaker_state,
|
| 1350 |
+
speaker_mask=speaker_mask_full,
|
| 1351 |
+
duration_features=duration_features,
|
| 1352 |
+
has_speaker=duration_has_speaker,
|
| 1353 |
+
)
|
| 1354 |
+
|
| 1355 |
+
if x_t is None or t is None:
|
| 1356 |
+
raise ValueError("x_t and t are required unless duration_only=True.")
|
| 1357 |
+
text_mask_dit = text_mask_full
|
| 1358 |
+
speaker_mask_dit = speaker_mask_full
|
| 1359 |
+
caption_mask_dit = caption_mask_full
|
| 1360 |
+
if text_condition_dropout is not None:
|
| 1361 |
+
text_mask_dit = text_mask_dit.clone()
|
| 1362 |
+
text_mask_dit[text_condition_dropout] = False
|
| 1363 |
+
if speaker_condition_dropout is not None and speaker_mask_dit is not None:
|
| 1364 |
+
speaker_mask_dit = speaker_mask_dit.clone()
|
| 1365 |
+
speaker_mask_dit[speaker_condition_dropout] = False
|
| 1366 |
+
if caption_condition_dropout is not None and caption_mask_dit is not None:
|
| 1367 |
+
caption_mask_dit = caption_mask_dit.clone()
|
| 1368 |
+
caption_mask_dit[caption_condition_dropout] = False
|
| 1369 |
+
|
| 1370 |
+
v_pred = self.forward_with_encoded_conditions(
|
| 1371 |
+
x_t=x_t,
|
| 1372 |
+
t=t,
|
| 1373 |
+
text_state=text_state,
|
| 1374 |
+
text_mask=text_mask_dit,
|
| 1375 |
+
speaker_state=speaker_state,
|
| 1376 |
+
speaker_mask=speaker_mask_dit,
|
| 1377 |
+
caption_state=caption_state,
|
| 1378 |
+
caption_mask=caption_mask_dit,
|
| 1379 |
+
latent_mask=latent_mask,
|
| 1380 |
+
)
|
| 1381 |
+
duration_pred = self.predict_duration_log_frames(
|
| 1382 |
+
text_state=text_state,
|
| 1383 |
+
text_mask=text_mask_full,
|
| 1384 |
+
speaker_state=speaker_state,
|
| 1385 |
+
speaker_mask=speaker_mask_full,
|
| 1386 |
+
duration_features=duration_features,
|
| 1387 |
+
has_speaker=duration_has_speaker,
|
| 1388 |
+
)
|
| 1389 |
+
return v_pred, duration_pred
|
| 1390 |
+
|
| 1391 |
+
if duration_only:
|
| 1392 |
+
raise ValueError("duration_features is required when duration_only=True.")
|
| 1393 |
+
if x_t is None or t is None:
|
| 1394 |
+
raise ValueError("x_t and t are required for RF forward.")
|
| 1395 |
+
|
| 1396 |
+
(
|
| 1397 |
+
text_state,
|
| 1398 |
+
text_mask,
|
| 1399 |
+
speaker_state,
|
| 1400 |
+
speaker_mask,
|
| 1401 |
+
caption_state,
|
| 1402 |
+
caption_mask,
|
| 1403 |
+
) = self.encode_conditions(
|
| 1404 |
+
text_input_ids=text_input_ids,
|
| 1405 |
+
text_mask=text_mask,
|
| 1406 |
+
ref_latent=ref_latent,
|
| 1407 |
+
ref_mask=ref_mask,
|
| 1408 |
+
caption_input_ids=caption_input_ids,
|
| 1409 |
+
caption_mask=caption_mask,
|
| 1410 |
+
text_condition_dropout=text_condition_dropout,
|
| 1411 |
+
speaker_condition_dropout=speaker_condition_dropout,
|
| 1412 |
+
caption_condition_dropout=caption_condition_dropout,
|
| 1413 |
+
)
|
| 1414 |
+
return self.forward_with_encoded_conditions(
|
| 1415 |
+
x_t=x_t,
|
| 1416 |
+
t=t,
|
| 1417 |
+
text_state=text_state,
|
| 1418 |
+
text_mask=text_mask,
|
| 1419 |
+
speaker_state=speaker_state,
|
| 1420 |
+
speaker_mask=speaker_mask,
|
| 1421 |
+
caption_state=caption_state,
|
| 1422 |
+
caption_mask=caption_mask,
|
| 1423 |
+
latent_mask=latent_mask,
|
| 1424 |
+
)
|
| 1425 |
+
|
| 1426 |
+
def build_context_kv_cache(
|
| 1427 |
+
self,
|
| 1428 |
+
text_state: torch.Tensor,
|
| 1429 |
+
speaker_state: torch.Tensor | None,
|
| 1430 |
+
caption_state: torch.Tensor | None = None,
|
| 1431 |
+
) -> list[tuple[torch.Tensor, ...]]:
|
| 1432 |
+
"""
|
| 1433 |
+
Build per-layer projected conditioning KV tensors for faster repeated sampling steps.
|
| 1434 |
+
"""
|
| 1435 |
+
return [
|
| 1436 |
+
block.attention.project_context_kv(
|
| 1437 |
+
text_context=text_state,
|
| 1438 |
+
speaker_context=speaker_state,
|
| 1439 |
+
caption_context=caption_state,
|
| 1440 |
+
)
|
| 1441 |
+
for block in self.blocks
|
| 1442 |
+
]
|
| 1443 |
+
|
| 1444 |
+
@staticmethod
|
| 1445 |
+
def masked_mean(state: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
| 1446 |
+
mask_f = mask.unsqueeze(-1).to(dtype=state.dtype)
|
| 1447 |
+
denom = mask_f.sum(dim=1).clamp_min(1.0)
|
| 1448 |
+
return (state * mask_f).sum(dim=1) / denom
|
| 1449 |
+
|
| 1450 |
+
def predict_duration_log_frames(
|
| 1451 |
+
self,
|
| 1452 |
+
*,
|
| 1453 |
+
text_state: torch.Tensor,
|
| 1454 |
+
text_mask: torch.Tensor,
|
| 1455 |
+
speaker_state: torch.Tensor | None,
|
| 1456 |
+
speaker_mask: torch.Tensor | None,
|
| 1457 |
+
duration_features: torch.Tensor,
|
| 1458 |
+
has_speaker: torch.Tensor | None,
|
| 1459 |
+
) -> torch.Tensor:
|
| 1460 |
+
if self.duration_predictor is None:
|
| 1461 |
+
raise RuntimeError("Duration predictor is disabled for this model.")
|
| 1462 |
+
if duration_features.ndim != 2:
|
| 1463 |
+
raise ValueError(
|
| 1464 |
+
f"duration_features must have shape (B, D), got {tuple(duration_features.shape)}"
|
| 1465 |
+
)
|
| 1466 |
+
if duration_features.shape[1] != self.cfg.duration_aux_dim:
|
| 1467 |
+
raise ValueError(
|
| 1468 |
+
"duration_features dim mismatch: "
|
| 1469 |
+
f"expected {self.cfg.duration_aux_dim}, got {duration_features.shape[1]}"
|
| 1470 |
+
)
|
| 1471 |
+
|
| 1472 |
+
pred = self.duration_predictor(
|
| 1473 |
+
text_state=text_state.detach(),
|
| 1474 |
+
text_mask=text_mask,
|
| 1475 |
+
aux_features=duration_features,
|
| 1476 |
+
speaker_state=None if speaker_state is None else speaker_state.detach(),
|
| 1477 |
+
speaker_mask=speaker_mask,
|
| 1478 |
+
has_speaker=has_speaker,
|
| 1479 |
+
)
|
| 1480 |
+
return pred.float()
|
| 1481 |
+
|
| 1482 |
+
@property
|
| 1483 |
+
def device(self) -> torch.device:
|
| 1484 |
+
return next(self.parameters()).device
|
| 1485 |
+
|
| 1486 |
+
@property
|
| 1487 |
+
def dtype(self) -> torch.dtype:
|
| 1488 |
+
return next(self.parameters()).dtype
|
| 1489 |
+
|
| 1490 |
+
def as_dict(self) -> dict:
|
| 1491 |
+
return asdict(self.cfg)
|
irodori_tts/rf.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
from .model import TextToLatentRFDiT
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _make_rng(seed: int, device: torch.device) -> tuple[torch.Generator, torch.device]:
|
| 9 |
+
# MPS generators are not available on some PyTorch builds; use CPU generator as fallback.
|
| 10 |
+
try:
|
| 11 |
+
return torch.Generator(device=device).manual_seed(seed), device
|
| 12 |
+
except RuntimeError:
|
| 13 |
+
return torch.Generator(device="cpu").manual_seed(seed), torch.device("cpu")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def sample_logit_normal_t(
|
| 17 |
+
batch_size: int,
|
| 18 |
+
device: torch.device,
|
| 19 |
+
mean: float = 0.0,
|
| 20 |
+
std: float = 1.0,
|
| 21 |
+
t_min: float = 1e-3,
|
| 22 |
+
t_max: float = 0.999,
|
| 23 |
+
) -> torch.Tensor:
|
| 24 |
+
z = torch.randn(batch_size, device=device) * std + mean
|
| 25 |
+
t = torch.sigmoid(z)
|
| 26 |
+
return t.clamp(min=t_min, max=t_max)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def sample_stratified_logit_normal_t(
|
| 30 |
+
batch_size: int,
|
| 31 |
+
device: torch.device,
|
| 32 |
+
mean: float = 0.0,
|
| 33 |
+
std: float = 1.0,
|
| 34 |
+
t_min: float = 1e-3,
|
| 35 |
+
t_max: float = 0.999,
|
| 36 |
+
) -> torch.Tensor:
|
| 37 |
+
"""
|
| 38 |
+
Stratified sampling for logit-normal timesteps.
|
| 39 |
+
|
| 40 |
+
u ~ stratified U(0, 1), z = mean + std * Phi^{-1}(u), t = sigmoid(z)
|
| 41 |
+
"""
|
| 42 |
+
if batch_size <= 0:
|
| 43 |
+
return torch.empty((0,), device=device)
|
| 44 |
+
u = (
|
| 45 |
+
torch.arange(batch_size, device=device, dtype=torch.float32)
|
| 46 |
+
+ torch.rand(batch_size, device=device)
|
| 47 |
+
) / float(batch_size)
|
| 48 |
+
u = u.clamp(1e-6, 1.0 - 1e-6)
|
| 49 |
+
# Phi^{-1}(u) = sqrt(2) * erfinv(2u - 1)
|
| 50 |
+
z = torch.erfinv(2.0 * u - 1.0) * (2.0**0.5)
|
| 51 |
+
z = z * std + mean
|
| 52 |
+
t = torch.sigmoid(z)
|
| 53 |
+
# Randomize assignment order so dataset ordering does not correlate with t bins.
|
| 54 |
+
t = t[torch.randperm(batch_size, device=device)]
|
| 55 |
+
return t.clamp(min=t_min, max=t_max)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def rf_interpolate(x0: torch.Tensor, noise: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 59 |
+
# Straight line interpolation: x_t = (1-t) x0 + t z.
|
| 60 |
+
return (1.0 - t[:, None, None]) * x0 + t[:, None, None] * noise
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def rf_velocity_target(x0: torch.Tensor, noise: torch.Tensor) -> torch.Tensor:
|
| 64 |
+
# For x_t = (1-t) x0 + t z, velocity is d/dt x_t = z - x0.
|
| 65 |
+
return noise - x0
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def rf_predict_x0(x_t: torch.Tensor, v_pred: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 69 |
+
# x_t = x0 + t * v => x0 = x_t - t * v
|
| 70 |
+
return x_t - t[:, None, None] * v_pred
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def temporal_score_rescale(
|
| 74 |
+
v_pred: torch.Tensor,
|
| 75 |
+
x_t: torch.Tensor,
|
| 76 |
+
t: float | torch.Tensor,
|
| 77 |
+
rescale_k: float,
|
| 78 |
+
rescale_sigma: float,
|
| 79 |
+
) -> torch.Tensor:
|
| 80 |
+
"""
|
| 81 |
+
Temporal score rescaling from https://arxiv.org/pdf/2510.01184.
|
| 82 |
+
"""
|
| 83 |
+
t_value = float(t.item()) if isinstance(t, torch.Tensor) else float(t)
|
| 84 |
+
if t_value >= 1.0:
|
| 85 |
+
return v_pred
|
| 86 |
+
one_minus_t = 1.0 - t_value
|
| 87 |
+
snr = (one_minus_t * one_minus_t) / (t_value * t_value)
|
| 88 |
+
sigma_sq = float(rescale_sigma) * float(rescale_sigma)
|
| 89 |
+
ratio = (snr * sigma_sq + 1.0) / (snr * sigma_sq / float(rescale_k) + 1.0)
|
| 90 |
+
return (ratio * (one_minus_t * v_pred + x_t) - x_t) / one_minus_t
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def scale_speaker_kv_cache(
|
| 94 |
+
context_kv_cache: list[tuple[torch.Tensor, ...]],
|
| 95 |
+
scale: float,
|
| 96 |
+
max_layers: int | None = None,
|
| 97 |
+
) -> None:
|
| 98 |
+
"""
|
| 99 |
+
In-place scaling of speaker K/V tensors in precomputed context cache.
|
| 100 |
+
"""
|
| 101 |
+
if max_layers is None:
|
| 102 |
+
n_layers = len(context_kv_cache)
|
| 103 |
+
else:
|
| 104 |
+
n_layers = max(0, min(int(max_layers), len(context_kv_cache)))
|
| 105 |
+
for i in range(n_layers):
|
| 106 |
+
layer_kv = context_kv_cache[i]
|
| 107 |
+
if len(layer_kv) < 4:
|
| 108 |
+
raise ValueError(
|
| 109 |
+
f"Expected at least 4 tensors in context KV cache entry, got {len(layer_kv)}"
|
| 110 |
+
)
|
| 111 |
+
k_speaker = layer_kv[2]
|
| 112 |
+
v_speaker = layer_kv[3]
|
| 113 |
+
k_speaker.mul_(scale)
|
| 114 |
+
v_speaker.mul_(scale)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
@torch.inference_mode()
|
| 118 |
+
def sample_euler_rf_cfg(
|
| 119 |
+
model: TextToLatentRFDiT,
|
| 120 |
+
text_input_ids: torch.Tensor,
|
| 121 |
+
text_mask: torch.Tensor,
|
| 122 |
+
ref_latent: torch.Tensor | None,
|
| 123 |
+
ref_mask: torch.Tensor | None,
|
| 124 |
+
sequence_length: int,
|
| 125 |
+
caption_input_ids: torch.Tensor | None = None,
|
| 126 |
+
caption_mask: torch.Tensor | None = None,
|
| 127 |
+
num_steps: int = 40,
|
| 128 |
+
cfg_scale_text: float = 3.0,
|
| 129 |
+
cfg_scale_caption: float = 3.0,
|
| 130 |
+
cfg_scale_speaker: float = 5.0,
|
| 131 |
+
cfg_guidance_mode: str = "independent",
|
| 132 |
+
cfg_min_t: float = 0.5,
|
| 133 |
+
cfg_max_t: float = 1.0,
|
| 134 |
+
seed: int = 0,
|
| 135 |
+
cfg_scale: float | None = None,
|
| 136 |
+
truncation_factor: float | None = None,
|
| 137 |
+
rescale_k: float | None = None,
|
| 138 |
+
rescale_sigma: float | None = None,
|
| 139 |
+
use_context_kv_cache: bool = True,
|
| 140 |
+
speaker_kv_scale: float | None = None,
|
| 141 |
+
speaker_kv_max_layers: int | None = None,
|
| 142 |
+
speaker_kv_min_t: float | None = None,
|
| 143 |
+
) -> torch.Tensor:
|
| 144 |
+
"""
|
| 145 |
+
Euler sampling over RF ODE with text/reference/caption conditioning CFG.
|
| 146 |
+
|
| 147 |
+
Returns:
|
| 148 |
+
latent sequence in patched space, shape (B, sequence_length, patched_latent_dim)
|
| 149 |
+
"""
|
| 150 |
+
device = model.device
|
| 151 |
+
dtype = model.dtype
|
| 152 |
+
batch_size = text_input_ids.shape[0]
|
| 153 |
+
latent_dim = model.cfg.patched_latent_dim
|
| 154 |
+
|
| 155 |
+
rng, rng_device = _make_rng(seed=seed, device=device)
|
| 156 |
+
x_t = torch.randn(
|
| 157 |
+
(batch_size, sequence_length, latent_dim), device=rng_device, dtype=dtype, generator=rng
|
| 158 |
+
)
|
| 159 |
+
if rng_device != device:
|
| 160 |
+
x_t = x_t.to(device=device)
|
| 161 |
+
if truncation_factor is not None:
|
| 162 |
+
x_t = x_t * float(truncation_factor)
|
| 163 |
+
|
| 164 |
+
if cfg_scale is not None:
|
| 165 |
+
# Backward compatibility for old single-scale caller.
|
| 166 |
+
cfg_scale_text = float(cfg_scale)
|
| 167 |
+
cfg_scale_caption = float(cfg_scale)
|
| 168 |
+
cfg_scale_speaker = float(cfg_scale)
|
| 169 |
+
if not model.cfg.use_speaker_condition:
|
| 170 |
+
cfg_scale_speaker = 0.0
|
| 171 |
+
speaker_kv_scale = None
|
| 172 |
+
|
| 173 |
+
cfg_guidance_mode = str(cfg_guidance_mode).strip().lower()
|
| 174 |
+
if cfg_guidance_mode not in {"independent", "joint", "alternating"}:
|
| 175 |
+
raise ValueError(
|
| 176 |
+
f"Unsupported cfg_guidance_mode={cfg_guidance_mode!r}. "
|
| 177 |
+
"Expected one of: independent, joint, alternating."
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
init_scale = 0.999
|
| 181 |
+
t_schedule = torch.linspace(1.0, 0.0, num_steps + 1, device=device) * init_scale
|
| 182 |
+
use_independent_cfg = cfg_guidance_mode == "independent"
|
| 183 |
+
use_joint_cfg = cfg_guidance_mode == "joint"
|
| 184 |
+
use_alternating_cfg = cfg_guidance_mode == "alternating"
|
| 185 |
+
|
| 186 |
+
(
|
| 187 |
+
text_state_cond,
|
| 188 |
+
text_mask_cond,
|
| 189 |
+
speaker_state_cond,
|
| 190 |
+
speaker_mask_cond,
|
| 191 |
+
caption_state_cond,
|
| 192 |
+
caption_mask_cond,
|
| 193 |
+
) = model.encode_conditions(
|
| 194 |
+
text_input_ids=text_input_ids,
|
| 195 |
+
text_mask=text_mask,
|
| 196 |
+
ref_latent=ref_latent,
|
| 197 |
+
ref_mask=ref_mask,
|
| 198 |
+
caption_input_ids=caption_input_ids,
|
| 199 |
+
caption_mask=caption_mask,
|
| 200 |
+
)
|
| 201 |
+
text_state_uncond = torch.zeros_like(text_state_cond)
|
| 202 |
+
text_mask_uncond = torch.zeros_like(text_mask_cond)
|
| 203 |
+
speaker_state_uncond = None
|
| 204 |
+
speaker_mask_uncond = None
|
| 205 |
+
if model.cfg.use_speaker_condition:
|
| 206 |
+
if speaker_state_cond is None or speaker_mask_cond is None:
|
| 207 |
+
raise RuntimeError(
|
| 208 |
+
"Speaker conditioning is enabled but encoded speaker state is missing."
|
| 209 |
+
)
|
| 210 |
+
speaker_state_uncond = torch.zeros_like(speaker_state_cond)
|
| 211 |
+
speaker_mask_uncond = torch.zeros_like(speaker_mask_cond)
|
| 212 |
+
caption_state_uncond = None
|
| 213 |
+
caption_mask_uncond = None
|
| 214 |
+
if model.cfg.use_caption_condition:
|
| 215 |
+
if caption_state_cond is None or caption_mask_cond is None:
|
| 216 |
+
raise RuntimeError(
|
| 217 |
+
"Caption conditioning is enabled but encoded caption state is missing."
|
| 218 |
+
)
|
| 219 |
+
caption_state_uncond = torch.zeros_like(caption_state_cond)
|
| 220 |
+
caption_mask_uncond = torch.zeros_like(caption_mask_cond)
|
| 221 |
+
|
| 222 |
+
has_text_cfg = cfg_scale_text > 0
|
| 223 |
+
has_caption_cfg = (
|
| 224 |
+
model.cfg.use_caption_condition
|
| 225 |
+
and cfg_scale_caption > 0
|
| 226 |
+
and caption_mask_cond is not None
|
| 227 |
+
and bool(caption_mask_cond.any().item())
|
| 228 |
+
)
|
| 229 |
+
has_speaker_cfg = cfg_scale_speaker > 0
|
| 230 |
+
|
| 231 |
+
def _bundle(
|
| 232 |
+
*,
|
| 233 |
+
text_state: torch.Tensor,
|
| 234 |
+
text_mask_val: torch.Tensor,
|
| 235 |
+
speaker_state: torch.Tensor | None,
|
| 236 |
+
speaker_mask_val: torch.Tensor | None,
|
| 237 |
+
caption_state: torch.Tensor | None,
|
| 238 |
+
caption_mask_val: torch.Tensor | None,
|
| 239 |
+
) -> tuple[
|
| 240 |
+
torch.Tensor,
|
| 241 |
+
torch.Tensor,
|
| 242 |
+
torch.Tensor | None,
|
| 243 |
+
torch.Tensor | None,
|
| 244 |
+
torch.Tensor | None,
|
| 245 |
+
torch.Tensor | None,
|
| 246 |
+
]:
|
| 247 |
+
return (
|
| 248 |
+
text_state,
|
| 249 |
+
text_mask_val,
|
| 250 |
+
speaker_state,
|
| 251 |
+
speaker_mask_val,
|
| 252 |
+
caption_state,
|
| 253 |
+
caption_mask_val,
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
cond_bundle = _bundle(
|
| 257 |
+
text_state=text_state_cond,
|
| 258 |
+
text_mask_val=text_mask_cond,
|
| 259 |
+
speaker_state=speaker_state_cond,
|
| 260 |
+
speaker_mask_val=speaker_mask_cond,
|
| 261 |
+
caption_state=caption_state_cond,
|
| 262 |
+
caption_mask_val=caption_mask_cond,
|
| 263 |
+
)
|
| 264 |
+
enabled_cfg_names: list[str] = []
|
| 265 |
+
cfg_scales: dict[str, float] = {}
|
| 266 |
+
if has_text_cfg:
|
| 267 |
+
enabled_cfg_names.append("text")
|
| 268 |
+
cfg_scales["text"] = float(cfg_scale_text)
|
| 269 |
+
if has_speaker_cfg:
|
| 270 |
+
enabled_cfg_names.append("speaker")
|
| 271 |
+
cfg_scales["speaker"] = float(cfg_scale_speaker)
|
| 272 |
+
if has_caption_cfg:
|
| 273 |
+
enabled_cfg_names.append("caption")
|
| 274 |
+
cfg_scales["caption"] = float(cfg_scale_caption)
|
| 275 |
+
|
| 276 |
+
independent_bundles = [cond_bundle]
|
| 277 |
+
independent_names = ["cond"]
|
| 278 |
+
if use_independent_cfg:
|
| 279 |
+
for name in enabled_cfg_names:
|
| 280 |
+
independent_names.append(name)
|
| 281 |
+
independent_bundles.append(
|
| 282 |
+
_bundle(
|
| 283 |
+
text_state=text_state_uncond if name == "text" else text_state_cond,
|
| 284 |
+
text_mask_val=text_mask_uncond if name == "text" else text_mask_cond,
|
| 285 |
+
speaker_state=(
|
| 286 |
+
speaker_state_uncond if name == "speaker" else speaker_state_cond
|
| 287 |
+
),
|
| 288 |
+
speaker_mask_val=(
|
| 289 |
+
speaker_mask_uncond if name == "speaker" else speaker_mask_cond
|
| 290 |
+
),
|
| 291 |
+
caption_state=(
|
| 292 |
+
caption_state_uncond if name == "caption" else caption_state_cond
|
| 293 |
+
),
|
| 294 |
+
caption_mask_val=(
|
| 295 |
+
caption_mask_uncond if name == "caption" else caption_mask_cond
|
| 296 |
+
),
|
| 297 |
+
)
|
| 298 |
+
)
|
| 299 |
+
cfg_batch_mult = len(independent_bundles)
|
| 300 |
+
|
| 301 |
+
def _cat_optional_tensors(values: list[torch.Tensor | None]) -> torch.Tensor | None:
|
| 302 |
+
present = [value for value in values if value is not None]
|
| 303 |
+
if not present:
|
| 304 |
+
return None
|
| 305 |
+
if len(present) != len(values):
|
| 306 |
+
raise ValueError("Cannot concatenate optional condition tensors with mixed presence.")
|
| 307 |
+
return torch.cat(present, dim=0)
|
| 308 |
+
|
| 309 |
+
independent_text_state = torch.cat([bundle[0] for bundle in independent_bundles], dim=0)
|
| 310 |
+
independent_text_mask = torch.cat([bundle[1] for bundle in independent_bundles], dim=0)
|
| 311 |
+
independent_speaker_state = _cat_optional_tensors([bundle[2] for bundle in independent_bundles])
|
| 312 |
+
independent_speaker_mask = _cat_optional_tensors([bundle[3] for bundle in independent_bundles])
|
| 313 |
+
independent_caption_state = _cat_optional_tensors([bundle[4] for bundle in independent_bundles])
|
| 314 |
+
independent_caption_mask = _cat_optional_tensors([bundle[5] for bundle in independent_bundles])
|
| 315 |
+
|
| 316 |
+
joint_uncond_bundle = _bundle(
|
| 317 |
+
text_state=text_state_uncond,
|
| 318 |
+
text_mask_val=text_mask_uncond,
|
| 319 |
+
speaker_state=speaker_state_uncond,
|
| 320 |
+
speaker_mask_val=speaker_mask_uncond,
|
| 321 |
+
caption_state=caption_state_uncond,
|
| 322 |
+
caption_mask_val=caption_mask_uncond,
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
alternating_bundles: dict[
|
| 326 |
+
str,
|
| 327 |
+
tuple[
|
| 328 |
+
torch.Tensor,
|
| 329 |
+
torch.Tensor,
|
| 330 |
+
torch.Tensor | None,
|
| 331 |
+
torch.Tensor | None,
|
| 332 |
+
torch.Tensor | None,
|
| 333 |
+
torch.Tensor | None,
|
| 334 |
+
],
|
| 335 |
+
] = {
|
| 336 |
+
"text": _bundle(
|
| 337 |
+
text_state=text_state_uncond,
|
| 338 |
+
text_mask_val=text_mask_uncond,
|
| 339 |
+
speaker_state=speaker_state_cond,
|
| 340 |
+
speaker_mask_val=speaker_mask_cond,
|
| 341 |
+
caption_state=caption_state_cond,
|
| 342 |
+
caption_mask_val=caption_mask_cond,
|
| 343 |
+
),
|
| 344 |
+
"caption": _bundle(
|
| 345 |
+
text_state=text_state_cond,
|
| 346 |
+
text_mask_val=text_mask_cond,
|
| 347 |
+
speaker_state=speaker_state_cond,
|
| 348 |
+
speaker_mask_val=speaker_mask_cond,
|
| 349 |
+
caption_state=caption_state_uncond,
|
| 350 |
+
caption_mask_val=caption_mask_uncond,
|
| 351 |
+
),
|
| 352 |
+
}
|
| 353 |
+
if has_speaker_cfg:
|
| 354 |
+
alternating_bundles["speaker"] = _bundle(
|
| 355 |
+
text_state=text_state_cond,
|
| 356 |
+
text_mask_val=text_mask_cond,
|
| 357 |
+
speaker_state=speaker_state_uncond,
|
| 358 |
+
speaker_mask_val=speaker_mask_uncond,
|
| 359 |
+
caption_state=caption_state_cond,
|
| 360 |
+
caption_mask_val=caption_mask_cond,
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
# Force-speaker scaling operates on projected speaker K/V, so it requires context KV caches.
|
| 364 |
+
effective_use_context_kv_cache = bool(use_context_kv_cache or (speaker_kv_scale is not None))
|
| 365 |
+
|
| 366 |
+
context_kv_cond = None
|
| 367 |
+
context_kv_cfg = None
|
| 368 |
+
context_kv_joint_uncond = None
|
| 369 |
+
context_kv_alternating: dict[str, list[tuple[torch.Tensor, ...]]] = {}
|
| 370 |
+
if effective_use_context_kv_cache:
|
| 371 |
+
context_kv_cond = model.build_context_kv_cache(
|
| 372 |
+
text_state=text_state_cond,
|
| 373 |
+
speaker_state=speaker_state_cond,
|
| 374 |
+
caption_state=caption_state_cond,
|
| 375 |
+
)
|
| 376 |
+
if use_independent_cfg and cfg_batch_mult > 1:
|
| 377 |
+
context_kv_cfg = model.build_context_kv_cache(
|
| 378 |
+
text_state=independent_text_state,
|
| 379 |
+
speaker_state=independent_speaker_state,
|
| 380 |
+
caption_state=independent_caption_state,
|
| 381 |
+
)
|
| 382 |
+
elif use_joint_cfg:
|
| 383 |
+
if enabled_cfg_names:
|
| 384 |
+
context_kv_joint_uncond = model.build_context_kv_cache(
|
| 385 |
+
text_state=joint_uncond_bundle[0],
|
| 386 |
+
speaker_state=joint_uncond_bundle[2],
|
| 387 |
+
caption_state=joint_uncond_bundle[4],
|
| 388 |
+
)
|
| 389 |
+
elif use_alternating_cfg:
|
| 390 |
+
for name in enabled_cfg_names:
|
| 391 |
+
bundle = alternating_bundles[name]
|
| 392 |
+
context_kv_alternating[name] = model.build_context_kv_cache(
|
| 393 |
+
text_state=bundle[0],
|
| 394 |
+
speaker_state=bundle[2],
|
| 395 |
+
caption_state=bundle[4],
|
| 396 |
+
)
|
| 397 |
+
if speaker_kv_scale is not None:
|
| 398 |
+
scale_speaker_kv_cache(
|
| 399 |
+
context_kv_cache=context_kv_cond,
|
| 400 |
+
scale=float(speaker_kv_scale),
|
| 401 |
+
max_layers=speaker_kv_max_layers,
|
| 402 |
+
)
|
| 403 |
+
if context_kv_cfg is not None:
|
| 404 |
+
scale_speaker_kv_cache(
|
| 405 |
+
context_kv_cache=context_kv_cfg,
|
| 406 |
+
scale=float(speaker_kv_scale),
|
| 407 |
+
max_layers=speaker_kv_max_layers,
|
| 408 |
+
)
|
| 409 |
+
for cache in context_kv_alternating.values():
|
| 410 |
+
scale_speaker_kv_cache(
|
| 411 |
+
context_kv_cache=cache,
|
| 412 |
+
scale=float(speaker_kv_scale),
|
| 413 |
+
max_layers=speaker_kv_max_layers,
|
| 414 |
+
)
|
| 415 |
+
speaker_kv_active = speaker_kv_scale is not None
|
| 416 |
+
|
| 417 |
+
for i in range(num_steps):
|
| 418 |
+
t = t_schedule[i]
|
| 419 |
+
t_next = t_schedule[i + 1]
|
| 420 |
+
tt = torch.full((batch_size,), t, device=device, dtype=dtype)
|
| 421 |
+
|
| 422 |
+
use_cfg = bool(enabled_cfg_names) and (cfg_min_t <= t.item() <= cfg_max_t)
|
| 423 |
+
if use_cfg:
|
| 424 |
+
if use_independent_cfg:
|
| 425 |
+
x_t_cfg = torch.cat([x_t] * cfg_batch_mult, dim=0).to(dtype)
|
| 426 |
+
tt_cfg = tt.repeat(cfg_batch_mult)
|
| 427 |
+
v_out = model.forward_with_encoded_conditions(
|
| 428 |
+
x_t=x_t_cfg,
|
| 429 |
+
t=tt_cfg,
|
| 430 |
+
text_state=independent_text_state,
|
| 431 |
+
text_mask=independent_text_mask,
|
| 432 |
+
speaker_state=independent_speaker_state,
|
| 433 |
+
speaker_mask=independent_speaker_mask,
|
| 434 |
+
caption_state=independent_caption_state,
|
| 435 |
+
caption_mask=independent_caption_mask,
|
| 436 |
+
context_kv_cache=context_kv_cfg,
|
| 437 |
+
)
|
| 438 |
+
chunks = v_out.chunk(cfg_batch_mult, dim=0)
|
| 439 |
+
v = chunks[0]
|
| 440 |
+
for name, chunk in zip(independent_names[1:], chunks[1:], strict=True):
|
| 441 |
+
v = v + cfg_scales[name] * (chunks[0] - chunk)
|
| 442 |
+
else:
|
| 443 |
+
v_cond = model.forward_with_encoded_conditions(
|
| 444 |
+
x_t=x_t.to(dtype),
|
| 445 |
+
t=tt,
|
| 446 |
+
text_state=text_state_cond,
|
| 447 |
+
text_mask=text_mask_cond,
|
| 448 |
+
speaker_state=speaker_state_cond,
|
| 449 |
+
speaker_mask=speaker_mask_cond,
|
| 450 |
+
caption_state=caption_state_cond,
|
| 451 |
+
caption_mask=caption_mask_cond,
|
| 452 |
+
context_kv_cache=context_kv_cond,
|
| 453 |
+
)
|
| 454 |
+
if use_joint_cfg:
|
| 455 |
+
if len(enabled_cfg_names) > 1:
|
| 456 |
+
joint_scales = [cfg_scales[name] for name in enabled_cfg_names]
|
| 457 |
+
if max(joint_scales) - min(joint_scales) > 1e-6:
|
| 458 |
+
raise ValueError(
|
| 459 |
+
"cfg_guidance_mode='joint' expects equal enabled guidance scales; "
|
| 460 |
+
"set matching text/speaker/caption scales or use --cfg-scale."
|
| 461 |
+
)
|
| 462 |
+
joint_scale = cfg_scales[enabled_cfg_names[0]]
|
| 463 |
+
v_uncond_joint = model.forward_with_encoded_conditions(
|
| 464 |
+
x_t=x_t.to(dtype),
|
| 465 |
+
t=tt,
|
| 466 |
+
text_state=joint_uncond_bundle[0],
|
| 467 |
+
text_mask=joint_uncond_bundle[1],
|
| 468 |
+
speaker_state=joint_uncond_bundle[2],
|
| 469 |
+
speaker_mask=joint_uncond_bundle[3],
|
| 470 |
+
caption_state=joint_uncond_bundle[4],
|
| 471 |
+
caption_mask=joint_uncond_bundle[5],
|
| 472 |
+
context_kv_cache=context_kv_joint_uncond,
|
| 473 |
+
)
|
| 474 |
+
v = v_cond + joint_scale * (v_cond - v_uncond_joint)
|
| 475 |
+
elif use_alternating_cfg:
|
| 476 |
+
alt_name = enabled_cfg_names[i % len(enabled_cfg_names)]
|
| 477 |
+
alt_bundle = alternating_bundles[alt_name]
|
| 478 |
+
v_uncond_alt = model.forward_with_encoded_conditions(
|
| 479 |
+
x_t=x_t.to(dtype),
|
| 480 |
+
t=tt,
|
| 481 |
+
text_state=alt_bundle[0],
|
| 482 |
+
text_mask=alt_bundle[1],
|
| 483 |
+
speaker_state=alt_bundle[2],
|
| 484 |
+
speaker_mask=alt_bundle[3],
|
| 485 |
+
caption_state=alt_bundle[4],
|
| 486 |
+
caption_mask=alt_bundle[5],
|
| 487 |
+
context_kv_cache=context_kv_alternating.get(alt_name),
|
| 488 |
+
)
|
| 489 |
+
v = v_cond + cfg_scales[alt_name] * (v_cond - v_uncond_alt)
|
| 490 |
+
else:
|
| 491 |
+
raise RuntimeError(f"Unexpected cfg_guidance_mode: {cfg_guidance_mode}")
|
| 492 |
+
else:
|
| 493 |
+
v = model.forward_with_encoded_conditions(
|
| 494 |
+
x_t=x_t.to(dtype),
|
| 495 |
+
t=tt,
|
| 496 |
+
text_state=text_state_cond,
|
| 497 |
+
text_mask=text_mask_cond,
|
| 498 |
+
speaker_state=speaker_state_cond,
|
| 499 |
+
speaker_mask=speaker_mask_cond,
|
| 500 |
+
caption_state=caption_state_cond,
|
| 501 |
+
caption_mask=caption_mask_cond,
|
| 502 |
+
context_kv_cache=context_kv_cond,
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
if rescale_k is not None and rescale_sigma is not None:
|
| 506 |
+
v = temporal_score_rescale(
|
| 507 |
+
v_pred=v,
|
| 508 |
+
x_t=x_t,
|
| 509 |
+
t=t,
|
| 510 |
+
rescale_k=float(rescale_k),
|
| 511 |
+
rescale_sigma=float(rescale_sigma),
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
if (
|
| 515 |
+
speaker_kv_active
|
| 516 |
+
and speaker_kv_min_t is not None
|
| 517 |
+
and (t_next < speaker_kv_min_t)
|
| 518 |
+
and (t >= speaker_kv_min_t)
|
| 519 |
+
):
|
| 520 |
+
inv_scale = 1.0 / float(speaker_kv_scale)
|
| 521 |
+
scale_speaker_kv_cache(
|
| 522 |
+
context_kv_cache=context_kv_cond,
|
| 523 |
+
scale=inv_scale,
|
| 524 |
+
max_layers=speaker_kv_max_layers,
|
| 525 |
+
)
|
| 526 |
+
if context_kv_cfg is not None:
|
| 527 |
+
scale_speaker_kv_cache(
|
| 528 |
+
context_kv_cache=context_kv_cfg,
|
| 529 |
+
scale=inv_scale,
|
| 530 |
+
max_layers=speaker_kv_max_layers,
|
| 531 |
+
)
|
| 532 |
+
for cache in context_kv_alternating.values():
|
| 533 |
+
scale_speaker_kv_cache(
|
| 534 |
+
context_kv_cache=cache,
|
| 535 |
+
scale=inv_scale,
|
| 536 |
+
max_layers=speaker_kv_max_layers,
|
| 537 |
+
)
|
| 538 |
+
speaker_kv_active = False
|
| 539 |
+
|
| 540 |
+
x_t = x_t + v * (t_next - t)
|
| 541 |
+
|
| 542 |
+
return x_t
|
irodori_tts/text_normalization.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
import unicodedata
|
| 5 |
+
|
| 6 |
+
SIMPLE_REPLACE_MAP: dict[str, str] = {
|
| 7 |
+
"\t": "",
|
| 8 |
+
"[n]": "",
|
| 9 |
+
r"\[n\]": "",
|
| 10 |
+
" ": "",
|
| 11 |
+
"?": "?",
|
| 12 |
+
"!": "!",
|
| 13 |
+
"♥": "♡",
|
| 14 |
+
"●": "○",
|
| 15 |
+
"◯": "○",
|
| 16 |
+
"〇": "○",
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
REGEX_REPLACE_MAP = {
|
| 20 |
+
re.compile(r"[;▼♀♂《》≪≫①②③④⑤⑥]"): "",
|
| 21 |
+
re.compile(r"[\u02d7\u2010-\u2015\u2043\u2212\u23af\u23e4\u2500\u2501\u2e3a\u2e3b]"): "",
|
| 22 |
+
re.compile(r"[\uff5e\u301C]"): "ー",
|
| 23 |
+
re.compile(r"…{3,}"): "……",
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def strip_outer_brackets(text: str) -> str:
|
| 28 |
+
pairs = {"「": "」", "『": "』", "(": ")", "【": "】", "(": ")"}
|
| 29 |
+
|
| 30 |
+
while True:
|
| 31 |
+
if len(text) < 2:
|
| 32 |
+
break
|
| 33 |
+
|
| 34 |
+
start_char = text[0]
|
| 35 |
+
end_char = text[-1]
|
| 36 |
+
|
| 37 |
+
if start_char in pairs and pairs[start_char] == end_char:
|
| 38 |
+
depth = 0
|
| 39 |
+
is_enclosing_all = True
|
| 40 |
+
|
| 41 |
+
for i, char in enumerate(text):
|
| 42 |
+
if char == start_char:
|
| 43 |
+
depth += 1
|
| 44 |
+
elif char == end_char:
|
| 45 |
+
depth -= 1
|
| 46 |
+
|
| 47 |
+
if depth == 0 and i < len(text) - 1:
|
| 48 |
+
is_enclosing_all = False
|
| 49 |
+
break
|
| 50 |
+
|
| 51 |
+
if is_enclosing_all and depth == 0:
|
| 52 |
+
text = text[1:-1]
|
| 53 |
+
continue
|
| 54 |
+
|
| 55 |
+
break
|
| 56 |
+
|
| 57 |
+
return text
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def normalize_text(text: str) -> str:
|
| 61 |
+
for old, new in SIMPLE_REPLACE_MAP.items():
|
| 62 |
+
text = text.replace(old, new)
|
| 63 |
+
|
| 64 |
+
for pattern, replacement in REGEX_REPLACE_MAP.items():
|
| 65 |
+
text = pattern.sub(replacement, text)
|
| 66 |
+
|
| 67 |
+
text = strip_outer_brackets(text)
|
| 68 |
+
|
| 69 |
+
text = unicodedata.normalize("NFKC", text)
|
| 70 |
+
|
| 71 |
+
text = text.replace("...", "…")
|
| 72 |
+
text = text.replace("..", "…")
|
| 73 |
+
|
| 74 |
+
return text
|
irodori_tts/tokenizer.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections.abc import Iterable
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ByteTokenizer:
|
| 7 |
+
"""Simple byte-level tokenizer for text-to-speech."""
|
| 8 |
+
|
| 9 |
+
def __init__(self, bos_token: int = 256) -> None:
|
| 10 |
+
if bos_token < 0:
|
| 11 |
+
raise ValueError(f"bos_token must be >= 0, got {bos_token}")
|
| 12 |
+
self.bos_token = int(bos_token)
|
| 13 |
+
|
| 14 |
+
@classmethod
|
| 15 |
+
def for_vocab_size(cls, text_vocab_size: int) -> "ByteTokenizer":
|
| 16 |
+
if text_vocab_size < 256:
|
| 17 |
+
raise ValueError(
|
| 18 |
+
f"text_vocab_size must be >= 256 for byte-level tokenization, got {text_vocab_size}"
|
| 19 |
+
)
|
| 20 |
+
# Reserve a dedicated BOS token outside UTF-8 byte range when possible.
|
| 21 |
+
if text_vocab_size == 256:
|
| 22 |
+
return cls(bos_token=0)
|
| 23 |
+
return cls(bos_token=text_vocab_size - 1)
|
| 24 |
+
|
| 25 |
+
def encode(self, text: str, add_bos: bool = True) -> torch.Tensor:
|
| 26 |
+
tokens = list(text.encode("utf-8"))
|
| 27 |
+
if add_bos:
|
| 28 |
+
tokens.insert(0, self.bos_token)
|
| 29 |
+
return torch.tensor(tokens, dtype=torch.long)
|
| 30 |
+
|
| 31 |
+
def batch_encode(
|
| 32 |
+
self,
|
| 33 |
+
texts: Iterable[str],
|
| 34 |
+
max_length: int | None = None,
|
| 35 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 36 |
+
encoded = [self.encode(t) for t in texts]
|
| 37 |
+
if max_length is None:
|
| 38 |
+
max_length = max(x.numel() for x in encoded)
|
| 39 |
+
|
| 40 |
+
batch = torch.zeros((len(encoded), max_length), dtype=torch.long)
|
| 41 |
+
mask = torch.zeros((len(encoded), max_length), dtype=torch.bool)
|
| 42 |
+
for i, seq in enumerate(encoded):
|
| 43 |
+
n = min(max_length, seq.numel())
|
| 44 |
+
batch[i, :n] = seq[:n]
|
| 45 |
+
mask[i, :n] = True
|
| 46 |
+
return batch, mask
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class PretrainedTextTokenizer:
|
| 50 |
+
"""
|
| 51 |
+
Hugging Face tokenizer wrapper for text conditioning.
|
| 52 |
+
- right-padding for stable positional behavior
|
| 53 |
+
- optional explicit BOS prepend
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
def __init__(self, tokenizer, add_bos: bool = True) -> None:
|
| 57 |
+
self.tokenizer = tokenizer
|
| 58 |
+
self.add_bos = bool(add_bos)
|
| 59 |
+
# TTS collator uses fixed-length right-padding; enforce this regardless of pretrained defaults.
|
| 60 |
+
self.tokenizer.padding_side = "right"
|
| 61 |
+
|
| 62 |
+
if self.tokenizer.pad_token_id is None:
|
| 63 |
+
if self.tokenizer.eos_token_id is not None and self.tokenizer.eos_token is not None:
|
| 64 |
+
self.tokenizer.pad_token = self.tokenizer.eos_token
|
| 65 |
+
else:
|
| 66 |
+
raise ValueError(
|
| 67 |
+
"Tokenizer has no pad_token_id (and no eos_token fallback). "
|
| 68 |
+
"Set a pad token before training/inference."
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
if self.add_bos and self.tokenizer.bos_token_id is None:
|
| 72 |
+
raise ValueError("Tokenizer has no bos_token_id but add_bos=True.")
|
| 73 |
+
|
| 74 |
+
@classmethod
|
| 75 |
+
def from_pretrained(
|
| 76 |
+
cls,
|
| 77 |
+
repo_id: str,
|
| 78 |
+
add_bos: bool = True,
|
| 79 |
+
local_files_only: bool = False,
|
| 80 |
+
) -> "PretrainedTextTokenizer":
|
| 81 |
+
try:
|
| 82 |
+
from transformers import AutoTokenizer
|
| 83 |
+
except ImportError as exc:
|
| 84 |
+
raise RuntimeError(
|
| 85 |
+
"transformers is required for pretrained text tokenization. "
|
| 86 |
+
"Install with `pip install transformers sentencepiece`."
|
| 87 |
+
) from exc
|
| 88 |
+
|
| 89 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 90 |
+
repo_id,
|
| 91 |
+
use_fast=True,
|
| 92 |
+
trust_remote_code=False,
|
| 93 |
+
local_files_only=local_files_only,
|
| 94 |
+
)
|
| 95 |
+
return cls(tokenizer=tokenizer, add_bos=add_bos)
|
| 96 |
+
|
| 97 |
+
@property
|
| 98 |
+
def vocab_size(self) -> int:
|
| 99 |
+
return int(len(self.tokenizer))
|
| 100 |
+
|
| 101 |
+
@property
|
| 102 |
+
def bos_token_id(self) -> int | None:
|
| 103 |
+
return self.tokenizer.bos_token_id
|
| 104 |
+
|
| 105 |
+
@property
|
| 106 |
+
def pad_token_id(self) -> int:
|
| 107 |
+
pad_id = self.tokenizer.pad_token_id
|
| 108 |
+
if pad_id is None:
|
| 109 |
+
raise RuntimeError("pad_token_id is unexpectedly None.")
|
| 110 |
+
return int(pad_id)
|
| 111 |
+
|
| 112 |
+
def encode(self, text: str, add_bos: bool | None = None) -> torch.Tensor:
|
| 113 |
+
token_ids = self.tokenizer.encode(text, add_special_tokens=False)
|
| 114 |
+
use_bos = self.add_bos if add_bos is None else bool(add_bos)
|
| 115 |
+
if use_bos:
|
| 116 |
+
bos_id = self.bos_token_id
|
| 117 |
+
if bos_id is None:
|
| 118 |
+
raise ValueError("Tokenizer has no bos_token_id but BOS prepend was requested.")
|
| 119 |
+
token_ids.insert(0, int(bos_id))
|
| 120 |
+
return torch.tensor(token_ids, dtype=torch.long)
|
| 121 |
+
|
| 122 |
+
def batch_encode(
|
| 123 |
+
self,
|
| 124 |
+
texts: Iterable[str],
|
| 125 |
+
max_length: int | None = None,
|
| 126 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 127 |
+
encoded = [self.encode(t) for t in texts]
|
| 128 |
+
if max_length is None:
|
| 129 |
+
max_length = max(max(x.numel(), 1) for x in encoded)
|
| 130 |
+
if max_length <= 0:
|
| 131 |
+
raise ValueError(f"max_length must be > 0, got {max_length}")
|
| 132 |
+
|
| 133 |
+
batch = torch.full(
|
| 134 |
+
(len(encoded), max_length),
|
| 135 |
+
fill_value=self.pad_token_id,
|
| 136 |
+
dtype=torch.long,
|
| 137 |
+
)
|
| 138 |
+
mask = torch.zeros((len(encoded), max_length), dtype=torch.bool)
|
| 139 |
+
for i, seq in enumerate(encoded):
|
| 140 |
+
n = min(max_length, seq.numel())
|
| 141 |
+
if n > 0:
|
| 142 |
+
batch[i, :n] = seq[:n]
|
| 143 |
+
mask[i, :n] = True
|
| 144 |
+
return batch, mask
|
irodori_tts/watermark.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from collections.abc import Iterable
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
IRODORI_WATERMARK_PAYLOAD = (73, 82, 68, 84, 83) # "IRDTS"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _as_single_channel_vector(audio: torch.Tensor) -> torch.Tensor | None:
|
| 14 |
+
squeezed = audio.detach().float().squeeze()
|
| 15 |
+
if squeezed.ndim == 0 or squeezed.numel() == 0:
|
| 16 |
+
return None
|
| 17 |
+
if squeezed.ndim == 1:
|
| 18 |
+
return squeezed
|
| 19 |
+
return squeezed.reshape(-1)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _match_original_rank(audio: torch.Tensor, *, reference: torch.Tensor) -> torch.Tensor:
|
| 23 |
+
if reference.ndim == 2:
|
| 24 |
+
return audio.reshape(1, -1)
|
| 25 |
+
return audio.reshape(-1)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class SilentCipherWatermarker:
|
| 29 |
+
def __init__(self, *, device: str, model_type: str = "44.1k") -> None:
|
| 30 |
+
self.model = self._load_backend(device=device, model_type=model_type)
|
| 31 |
+
|
| 32 |
+
@staticmethod
|
| 33 |
+
def _load_backend(*, device: str, model_type: str):
|
| 34 |
+
try:
|
| 35 |
+
import silentcipher
|
| 36 |
+
except ImportError:
|
| 37 |
+
logger.warning(
|
| 38 |
+
"SilentCipher package is unavailable; generated audio will not be watermarked."
|
| 39 |
+
)
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
try:
|
| 43 |
+
return silentcipher.get_model(model_type=model_type, device=device)
|
| 44 |
+
except Exception as exc:
|
| 45 |
+
logger.warning(
|
| 46 |
+
"SilentCipher model could not be loaded (%s); generated audio will not be "
|
| 47 |
+
"watermarked.",
|
| 48 |
+
exc,
|
| 49 |
+
)
|
| 50 |
+
return None
|
| 51 |
+
|
| 52 |
+
@property
|
| 53 |
+
def ready(self) -> bool:
|
| 54 |
+
return self.model is not None
|
| 55 |
+
|
| 56 |
+
def encode_one(
|
| 57 |
+
self,
|
| 58 |
+
audio: torch.Tensor,
|
| 59 |
+
*,
|
| 60 |
+
sample_rate: int,
|
| 61 |
+
payload: Iterable[int] = IRODORI_WATERMARK_PAYLOAD,
|
| 62 |
+
) -> torch.Tensor:
|
| 63 |
+
if self.model is None:
|
| 64 |
+
return audio
|
| 65 |
+
|
| 66 |
+
vector = _as_single_channel_vector(audio)
|
| 67 |
+
if vector is None:
|
| 68 |
+
return audio
|
| 69 |
+
|
| 70 |
+
encoded, _ = self.model.encode_wav(
|
| 71 |
+
vector.to(self.model.device),
|
| 72 |
+
int(sample_rate),
|
| 73 |
+
list(payload),
|
| 74 |
+
calc_sdr=False,
|
| 75 |
+
)
|
| 76 |
+
encoded_audio = torch.as_tensor(encoded, dtype=torch.float32, device="cpu")
|
| 77 |
+
return _match_original_rank(encoded_audio, reference=audio)
|
| 78 |
+
|
| 79 |
+
def encode_batch(self, audios: list[torch.Tensor], *, sample_rate: int) -> list[torch.Tensor]:
|
| 80 |
+
if self.model is None:
|
| 81 |
+
return audios
|
| 82 |
+
return [self.encode_one(audio, sample_rate=sample_rate) for audio in audios]
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=2.5.1
|
| 2 |
+
torchaudio>=2.5.1
|
| 3 |
+
transformers<5
|
| 4 |
+
sentencepiece>=0.1.99,<0.2
|
| 5 |
+
safetensors>=0.7.0
|
| 6 |
+
soundfile>=0.12.0
|
| 7 |
+
huggingface-hub>=0.34.0,<1.0
|
| 8 |
+
gradio>=5.0.0
|
| 9 |
+
numpy
|
| 10 |
+
dacvae @ git+https://github.com/facebookresearch/dacvae
|
| 11 |
+
torchcodec>=0.10.0
|
| 12 |
+
silentcipher @ git+https://github.com/SesameAILabs/silentcipher.git@d46d7d0893a583d8968ab3a6626e2289faec9152
|