Aratako commited on
Commit
12d8274
·
verified ·
1 Parent(s): 130d368

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -1,13 +1,18 @@
1
  ---
2
- title: Irodori TTS 500M V3 Demo
3
- emoji: 🐨
4
- colorFrom: red
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
  python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
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