Compare commits

...
Author SHA1 Message Date
diodiogod 3a817a2526 Version 5.1.0
Add Dots TTS engine and refine language handling

Implementation details:
- integrate official Dots TTS text and SRT workflows
- improve Dots runtime lifecycle, caching, and progress reporting
- fix Dots language override behavior and refresh generated metadata
2026-06-15 10:39:50 -03:00
diodiogod 78d3e867ef Refine Dots README metadata
Documentation details:
- remove redundant zero-shot voice cloning note from Dots summaries
- replace vague language tags wording with language control
- normalize Dots size wording to ~6GB in generated docs
2026-06-15 10:39:29 -03:00
diodiogod db3a888b77 Refine Dots language handling and metadata 2026-06-15 10:00:13 -03:00
diodiogod 1f5882ba44 Add Dots instruction TTS mode 2026-06-15 08:51:00 -03:00
diodiogod e471cdbaa6 Refine Dots TTS runtime and logging 2026-06-14 16:32:26 -03:00
diodiogod cb6dfe3a11 Add Dots TTS engine integration 2026-06-14 15:27:58 -03:00
35 changed files with 2889 additions and 159 deletions
+11
View File
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [5.1.0] - 2026-06-15
### Added
- Add Dots TTS engine with multilingual cloning support
- Add official Dots TTS engine for text and SRT generation
- Refine language behavior, Portuguese metadata, and Dots engine documentation
### Changed
- Improve model unload, caching, and generation progress feedback
## [5.0.0] - 2026-06-14
### Added
+1
View File
@@ -42,6 +42,7 @@ The project code is MIT. Model weights carry their own licenses:
Granite ASR Apache-2.0 Yes
Step Audio EditX Apache-2.0 (verify before commercial use) Conditional
Echo-TTS CC-BY-NC-SA-4.0 No
Dots TTS Apache-2.0 Yes
MOSS-TTS Apache-2.0 Yes
RVC MIT (framework); community models vary Varies
──────────────── ──────────────────────────────────────────────────────── ──────────────
+5 -5
View File
@@ -96,11 +96,11 @@
## Node Implementations
### Unified Interface
- `nodes/unified/tts_text_node.py` - Universal TTS text node
- `nodes/unified/tts_srt_node.py` - Universal SRT node
- `nodes/unified/voice_changer_node.py` - Universal voice conversion
- `nodes/unified/asr_transcribe_node.py` - Universal ASR node
### Unified Interface
- `nodes/unified/tts_text_node.py` - Universal TTS text node
- `nodes/unified/tts_srt_node.py` - Universal SRT node
- `nodes/unified/voice_changer_node.py` - Universal voice conversion
- `nodes/unified/asr_transcribe_node.py` - Universal ASR node
### Shared / Special Nodes
- `nodes/shared/character_voices_node.py` - Character voice management (NARRATOR_VOICE output)
+4 -2
View File
@@ -7,7 +7,7 @@
[![Dynamic TOML Badge][version-shield]][version-url]
[![Ko-Fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/diogogo)
# TTS Audio Suite v5.0.0
# TTS Audio Suite v5.1.0
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/diogogo)
@@ -23,7 +23,7 @@ Subtitle workflows are still a core focus: the suite can transcribe to SRT, rebu
<!-- ENGINE_COMPARISON_START -->
## Quick Engine Comparison — 14 Engines
## Quick Engine Comparison — 15 Engines
| Engine | Languages | Size | Key Features |
|--------|-----------|------|--------------|
@@ -39,6 +39,7 @@ Subtitle workflows are still a core focus: the suite can transcribe to SRT, rebu
| **Granite ASR** | 🇺🇸​🇩🇪​🇪🇸​🇫🇷​🇯🇵​🇵🇹 | ~4.6GB | ASR (Automatic Speech Recognition), Custom timestamps/SRT via reused Qwen forced aligner |
| **Step Audio EditX** | 🇺🇸​🇨🇳​🇯🇵​🇰🇷 | ~7GB | Second Pass Speech Editing Node: 14 emotions, 32 speaking styles |
| **Echo-TTS** | 🇺🇸 | ~5.3GB + ~1.8GB | Diffusion-based (~30s best), Force Speaker KV (speaker drift control) |
| **Dots TTS** | 🇺🇸​🇨🇳​🇩🇪​🇪🇸​🇫🇷​🇮🇹 +13 | ~6GB | Official auto language detect / language control, SOAR and MeanFlow distilled variants |
| **MOSS-TTS** | 🇺🇸​🇨🇳​🇩🇪​🇪🇸​🇫🇷​🇮🇹 +10 | ~8.5GB tokenizer + ~6.1GB/17GB/18GB model | 20-language generation, Long-form generation (TTSD/Delay) |
| **RVC** | 🌐 Any | 100-300MB | Real-time VC, Integrated training workflow |
@@ -1432,6 +1433,7 @@ For offline/manual setup:
| MOSS-TTS | `ComfyUI/models/TTS/moss_tts/` | ✅ | Local/Delay/TTSD models plus shared MOSS-Audio-Tokenizer codec |
| Granite ASR | `ComfyUI/models/TTS/granite_asr/` | ✅ | Main Granite model; optional Qwen forced aligner reused lazily for timestamps/SRT |
| Echo-TTS | `ComfyUI/models/TTS/echo-tts-base/` | ✅ | ~7.1GB total (base + dac); CC-BY-NC-SA |
| Dots TTS | `ComfyUI/models/TTS/dots_tts/` | ✅ | Official base / soar / mf checkpoints with tokenizer, vocoder, speaker encoder |
*Generated from [tts_audio_suite_engines.yaml](docs/Dev%20reports/tts_audio_suite_engines.yaml).*
@@ -1142,6 +1142,84 @@ engines:
vram_efficient: { supported: "partial", notes: "(~7GB total)" }
speed_performance: { supported: true, notes: "Fast (diffusion, realtime-capable)" }
- id: dots-tts
name: Dots TTS
models: "dots.tts-base, dots.tts-soar, dots.tts-mf"
size: "~6GB"
license: "Apache-2.0"
commercial: true
capabilities:
tts: true
srt: true
vc: false
asr: false
training: false
special_features:
- "Official auto language detect / language control"
- "SOAR and MeanFlow distilled variants"
model_sources:
- component: "dots.tts-base"
source_name: "rednote-hilab/dots.tts-base"
source_url: "https://huggingface.co/rednote-hilab/dots.tts-base"
size: "~6GB"
auto_download: true
notes: "Official base checkpoint with tokenizer, vocoder, speaker encoder, and latent stats"
- component: "dots.tts-soar"
source_name: "rednote-hilab/dots.tts-soar"
source_url: "https://huggingface.co/rednote-hilab/dots.tts-soar"
size: "~6GB"
auto_download: true
notes: "Official SOAR checkpoint for higher-quality zero-shot cloning"
- component: "dots.tts-mf"
source_name: "rednote-hilab/dots.tts-mf"
source_url: "https://huggingface.co/rednote-hilab/dots.tts-mf"
size: "~6GB"
auto_download: true
notes: "Official MeanFlow-distilled checkpoint for faster inference"
languages:
en: { supported: true, flag: "🇺🇸", notes: "" }
zh: { supported: true, flag: "🇨🇳", notes: "(Mandarin; official also exposes YUE separately outside this matrix)" }
de: { supported: true, flag: "🇩🇪", notes: "" }
es: { supported: true, flag: "🇪🇸", notes: "" }
fr: { supported: true, flag: "🇫🇷", notes: "" }
it: { supported: true, flag: "🇮🇹", notes: "" }
ja: { supported: true, flag: "🇯🇵", notes: "" }
ko: { supported: true, flag: "🇰🇷", notes: "" }
ru: { supported: true, flag: "🇷🇺", notes: "" }
pt: { supported: true, flag: "🇵🇹", notes: "(Official PT tag is generic; upstream does not expose separate PT-BR/PT-PT tags and it may lean more European Portuguese than Brazilian Portuguese)" }
pl: { supported: true, flag: "🇵🇱", notes: "" }
hi: { supported: true, flag: "🇮🇳", notes: "" }
ar: { supported: true, flag: "🇦🇪", notes: "" }
tr: { supported: true, flag: "🇹🇷", notes: "" }
th: { supported: true, flag: "🇹🇭", notes: "" }
no: { supported: false, flag: "🇳🇴", notes: "" }
vi: { supported: true, flag: "🇻🇳", notes: "" }
hy: { supported: false, flag: "🇦🇲", notes: "" }
ka: { supported: false, flag: "🇬🇪", notes: "" }
da: { supported: false, flag: "🇩🇰", notes: "" }
fi: { supported: true, flag: "🇫🇮", notes: "" }
el: { supported: true, flag: "🇬🇷", notes: "" }
he: { supported: false, flag: "🇮🇱", notes: "" }
ms: { supported: false, flag: "🇲🇾", notes: "" }
nl: { supported: true, flag: "🇳🇱", notes: "" }
sv: { supported: false, flag: "🇸🇪", notes: "" }
sw: { supported: false, flag: "🇰🇪", notes: "" }
features:
voice_cloning: { supported: true, notes: "" }
native_multi_speaker: { supported: false, notes: "" }
voice_conversion: { supported: false, notes: "" }
asr_transcribe: { supported: false, notes: "" }
emotion_control: { supported: false, notes: "" }
native_long_form: { supported: false, notes: "(use suite chunking)" }
community_finetunes: { supported: false, notes: "" }
vram_efficient: { supported: "partial", notes: "(main env works; 2B-class model is not lightweight)" }
speed_performance: { supported: "partial", notes: "Moderate; mf variant is faster" }
- id: moss-tts
name: MOSS-TTS
models: "Local 1.7B, Delay 8B, TTSD 8B"
@@ -1508,6 +1586,10 @@ readme_model_download_table:
primary_model_path: "ComfyUI/models/TTS/echo-tts-base/"
auto_download: "✅"
notes: "~7.1GB total (base + dac); CC-BY-NC-SA"
- engine: "Dots TTS"
primary_model_path: "ComfyUI/models/TTS/dots_tts/"
auto_download: "✅"
notes: "Official base / soar / mf checkpoints with tokenizer, vocoder, speaker encoder"
model_layouts_markdown: |
# Model Folder Layouts
@@ -1826,3 +1908,37 @@ model_layouts_markdown: |
- Both components are required and auto-downloaded on first use.
- License: CC-BY-NC-SA (non-commercial).
## Dots TTS
```text
ComfyUI/models/TTS/dots_tts/
├── dots.tts-base/
├── dots.tts-soar/
└── dots.tts-mf/
```
Typical checkpoint contents:
```text
ComfyUI/models/TTS/dots_tts/dots.tts-soar/
├── added_tokens.json
├── chat_template.jinja
├── config.json
├── latent_stats.pt
├── llm_config.json
├── merges.txt
├── model.safetensors
├── speaker_encoder.safetensors
├── special_tokens_map.json
├── tokenizer.json
├── tokenizer_config.json
├── vocab.json
└── vocoder.safetensors
```
Notes:
- Downloads into a dedicated `dots_tts/` folder.
- Native sample rate is 48kHz.
- Main-environment support works on Transformers 5; on Windows, `normalize_text` falls back to no-op if `WeTextProcessing` is unavailable.
+1
View File
@@ -16,6 +16,7 @@
| **Granite ASR** | Main | granite-4.0-1b-speech | ~4.6GB | ❌ | ✅ | ❌ | ✅ | ❌ | Apache-2.0 | ASR (Automatic Speech Recognition), Custom timestamps/SRT via reused Qwen forced aligner, Speech translation (experimental), Optional forced aligner auto-routed through shared legacy T4 runtime | 6 |
| **Step Audio EditX** | Main | 3B LLM + CosyVoice | ~7GB | ✅ | ✅ | ❌ | ❌ | ❌ | Apache-2.0 (verify before commercial use) | Second Pass Speech Editing Node: 14 emotions, 32 speaking styles, Paralinguistic effects | 4 |
| **Echo-TTS** | Main | echo-tts-base + fish-s1-dac-min | ~5.3GB + ~1.8GB | ✅ | ✅ | ❌ | ❌ | ❌ | CC-BY-NC-SA-4.0 | Diffusion-based (~30s best), Force Speaker KV (speaker drift control) | 1 |
| **Dots TTS** | Main | dots.tts-base, dots.tts-soar, dots.tts-mf | ~6GB | ✅ | ✅ | ❌ | ❌ | ❌ | Apache-2.0 | Official auto language detect / language control, SOAR and MeanFlow distilled variants | 19 |
| **MOSS-TTS** | Main | Local 1.7B, Delay 8B, TTSD 8B | ~8.5GB tokenizer + ~6.1GB/17GB/18GB model | ✅ | ✅ | ❌ | ❌ | ✅ | Apache-2.0 | 20-language generation, Long-form generation (TTSD/Delay), Native 1-5 speaker TTSD dialogue, Duration token hint, Local/Delay/TTSD variants, Initial integrated LoRA training workflow (Delay 8B) | 16 |
| **RVC** | Main | Community .pth | 100-300MB | ❌ | ❌ | ✅ | ❌ | ✅ | MIT (framework); community models vary | Real-time VC, Integrated training workflow, Pitch shift (±14), 6 HuBERT models, Language-independent | Any |
+14 -14
View File
@@ -2,17 +2,17 @@
## Feature Comparison Matrix
| Feature | F5-TTS | ChatterBox | ChatterBox 23L | VibeVoice | Higgs Audio 2 | Higgs Audio v3 | IndexTTS-2 | CosyVoice3 | Qwen3-TTS | Granite ASR | Step Audio EditX | Echo-TTS | MOSS-TTS | RVC |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| **TTS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ |
| **SRT** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| **Voice Conversion** | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| **ASR (Transcribe)** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| **Training** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| **Voice Cloning** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (Base model) | ❌ | ✅ | ✅ | ✅ | ⚠️ (needs training) |
| **Native Multi-Speaker** | ❌ | ❌ | ❌ | ✅ (Base only, Kugel uses fallback) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (TTSD v1.0; 1-5 speakers) | ❌ |
| **Emotion Control** | ❌ | ❌ | ⚠️ (v2 tags - doesn't work) | ❌ | ⚠️ (via prompt) | ✅ (native inline tags) | ✅ (8 emotions) | ⚠️ (via instruct) | ⚠️ (via instruct) | ❌ | ✅ (14 emotions) | ❌ | ❌ | ❌ |
| **Native Long-form** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (TTSD/Delay long-form; use chunk orchestration for very long inputs) | N/A |
| **Community Finetunes** | ✅ | ✅ | ✅ | ✅ KugelAudio, Hindi | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (LoRA adapter inference supported; initial integrated LoRA training support added for MOSS-TTS Delay 8B) | ✅ |
| **VRAM Efficient** | ✅ | ✅ | ✅ | ⚠️ (5-18GB) | ⚠️ (9GB) | ⚠️ (~8-10GB) | ⚠️ (9-12GB) | ✅ (5.4GB) | ✅ (3-6GB) | ✅ (~4.6GB) | ⚠️ (7GB) | ⚠️ (~7GB total) | ⚠️ (Local 1.7B smaller; tokenizer is large) | ✅ |
| **Speed/Performance** | ✅ Very Fast | ✅ Fast | ✅ Fast | ⚠️ | ⚠️ | ⚠️ CUDA recommended | ⚠️ | ✅ Fast | ⚠️ | ⚠️ Moderate | ⚠️ | ✅ Fast (diffusion, realtime-capable) | ✅ Fast with CUDA/FlashAttention | ✅ Fast |
| Feature | F5-TTS | ChatterBox | ChatterBox 23L | VibeVoice | Higgs Audio 2 | Higgs Audio v3 | IndexTTS-2 | CosyVoice3 | Qwen3-TTS | Granite ASR | Step Audio EditX | Echo-TTS | Dots TTS | MOSS-TTS | RVC |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| **TTS** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ |
| **SRT** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| **Voice Conversion** | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| **ASR (Transcribe)** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Training** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| **Voice Cloning** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (Base model) | ❌ | ✅ | ✅ | ✅ | ✅ | ⚠️ (needs training) |
| **Native Multi-Speaker** | ❌ | ❌ | ❌ | ✅ (Base only, Kugel uses fallback) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (TTSD v1.0; 1-5 speakers) | ❌ |
| **Emotion Control** | ❌ | ❌ | ⚠️ (v2 tags - doesn't work) | ❌ | ⚠️ (via prompt) | ✅ (native inline tags) | ✅ (8 emotions) | ⚠️ (via instruct) | ⚠️ (via instruct) | ❌ | ✅ (14 emotions) | ❌ | ❌ | ❌ | ❌ |
| **Native Long-form** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (TTSD/Delay long-form; use chunk orchestration for very long inputs) | N/A |
| **Community Finetunes** | ✅ | ✅ | ✅ | ✅ KugelAudio, Hindi | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (LoRA adapter inference supported; initial integrated LoRA training support added for MOSS-TTS Delay 8B) | ✅ |
| **VRAM Efficient** | ✅ | ✅ | ✅ | ⚠️ (5-18GB) | ⚠️ (9GB) | ⚠️ (~8-10GB) | ⚠️ (9-12GB) | ✅ (5.4GB) | ✅ (3-6GB) | ✅ (~4.6GB) | ⚠️ (7GB) | ⚠️ (~7GB total) | ⚠️ (main env works; 2B-class model is not lightweight) | ⚠️ (Local 1.7B smaller; tokenizer is large) | ✅ |
| **Speed/Performance** | ✅ Very Fast | ✅ Fast | ✅ Fast | ⚠️ | ⚠️ | ⚠️ CUDA recommended | ⚠️ | ✅ Fast | ⚠️ | ⚠️ Moderate | ⚠️ | ✅ Fast (diffusion, realtime-capable) | ⚠️ Moderate; mf variant is faster | ✅ Fast with CUDA/FlashAttention | ✅ Fast |
+104 -104
View File
@@ -2,110 +2,110 @@
## Language Support by Engine
| Language | Code | F5-TTS | ChatterBox | ChatterBox 23L | VibeVoice | Higgs Audio 2 | Higgs Audio v3 | IndexTTS-2 | CosyVoice3 | Qwen3-TTS | Granite ASR | Step Audio EditX | Echo-TTS | MOSS-TTS | RVC |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 🇺🇸 **English** | EN | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 🇨🇳 **Chinese** | ZH | ❌ | ❌ | ✅ | ✅ | ✅ (Mandarin) | ✅ | ✅ | ✅ + 18 dialects | ✅ | ❌ | ✅ (Mandarin + Sichuanese, Cantonese) | ❌ | ✅ | ✅ |
| 🇩🇪 **German** | DE | ✅ | ✅ (×3) | ✅ | ✅ (Kugel) | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
| 🇪🇸 **Spanish** | ES | ✅ | ❌ | ✅ | ✅ (Kugel) | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
| 🇫🇷 **French** | FR | ✅ | ✅ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
| 🇮🇹 **Italian** | IT | ✅ | ✅ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 🇯🇵 **Japanese** | JA | ✅ | ✅ | ✅ | ✅ (Kugel) | ❌ | ✅ | ✅ ? | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| 🇰🇷 **Korean** | KO | ❌ | ✅ | ✅ | ✅ (Kugel) | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ |
| 🇷🇺 **Russian** | RU | ❌ | ✅ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 🇧🇷 **Portuguese** | PT | ✅ (BR) | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ✅ (EU/BR*) | ✅ | ❌ | ❌ | ✅ | ✅ |
| 🇵🇱 **Polish** | PL | ✅ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 🇮🇳 **Hindi** | HI | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇬 **Arabic** | AR | ❌ | ❌ | ✅ (Egyptian) | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 🇹🇷 **Turkish** | TR | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 🇹🇭 **Thai** | TH | ✅ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇳🇴 **Norwegian** | NO | ❌ | ✅ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇻🇳 **Vietnamese** | VI | ❌ | ❌ | ✅ (Viterbox) | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇦🇲 **Armenian** | HY | ❌ | ✅ | ❌ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇬🇪 **Georgian** | KA | ❌ | ✅ | ❌ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇩🇰 **Danish** | DA | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 🇫🇮 **Finnish** | FI | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇬🇷 **Greek** | EL | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 🇮🇱 **Hebrew** | HE | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇲🇾 **Malay** | MS | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇳🇱 **Dutch** | NL | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇸🇪 **Swedish** | SV | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 🇰🇪 **Swahili** | SW | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇿🇦 **Afrikaans** | AF | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇦🇱 **Albanian** | SQ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Assamese** | AS | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇸 **Asturian** | AST | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇦🇿 **Azerbaijani** | AZ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇷🇺 **Bashkir** | BA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇸 **Basque** | EU | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇧🇾 **Belarusian** | BE | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇧🇩 **Bengali** | BN | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇧🇦 **Bosnian** | BS | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇧🇬 **Bulgarian** | BG | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇸 **Catalan** | CA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇵🇭 **Cebuano** | CEB | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇶 **Central Kurdish** | CKB | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇭🇷 **Croatian** | HR | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇨🇿 **Czech** | CS | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇷🇺 **Eastern Mari** | MHR | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🌍 **Esperanto** | EO | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇪 **Estonian** | ET | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇸 **Galician** | GL | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Gujarati** | GU | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇭🇹 **Haitian Creole** | HT | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇳🇬 **Hausa** | HA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇭🇺 **Hungarian** | HU | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇩 **Indonesian** | ID | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇩 **Javanese** | JV | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Kannada** | KN | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇰🇿 **Kazakh** | KK | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇷🇼 **Kinyarwanda** | RW | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇰🇬 **Kyrgyz** | KY | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇱🇻 **Latvian** | LV | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇨🇩 **Lingala** | LN | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇱🇹 **Lithuanian** | LT | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇰🇪 **Luo** | LUO | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇲🇰 **Macedonian** | MK | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Malayalam** | ML | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇲🇹 **Maltese** | MT | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇳🇿 **Māori** | MI | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Marathi** | MR | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇲🇳 **Mongolian** | MN | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇳🇵 **Nepali** | NE | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇫🇷 **Occitan** | OC | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇷 **Persian** | FA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇷🇴 **Romanian** | RO | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇿🇦 **Sepedi** | NSO | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇷🇸 **Serbian** | SR | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇿🇼 **Shona** | SN | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇸🇰 **Slovak** | SK | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇸🇮 **Slovene** | SL | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇵🇭 **Tagalog** | TL | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇹🇯 **Tajik** | TG | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Tamil** | TA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Telugu** | TE | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇺🇦 **Ukrainian** | UK | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇵🇰 **Urdu** | UR | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇨🇳 **Uyghur** | UG | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇺🇿 **Uzbek** | UZ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇿🇦 **Xhosa** | XH | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇿🇦 **Zulu** | ZU | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇲🇼 **Chichewa/Nyanja** | NY | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Eastern Punjabi** | PA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇺🇬 **Ganda** | LG | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇸 **Icelandic** | IS | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇪 **Irish** | GA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇩🇿 **Kabyle** | KAB | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇨🇻 **Kabuverdianu** | KEA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇰🇪 **Kamba** | KAM | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇻🇦 **Latin** | LA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇱🇺 **Luxembourgish** | LB | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇹 **Oromo** | OM | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇦🇫 **Pashto** | PS | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇵🇰 **Sindhi** | SD | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇸🇴 **Somali** | SO | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇦🇴 **Umbundu** | UMB | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🏴 **Welsh** | CY | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Language | Code | F5-TTS | ChatterBox | ChatterBox 23L | VibeVoice | Higgs Audio 2 | Higgs Audio v3 | IndexTTS-2 | CosyVoice3 | Qwen3-TTS | Granite ASR | Step Audio EditX | Echo-TTS | Dots TTS | MOSS-TTS | RVC |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 🇺🇸 **English** | EN | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 🇨🇳 **Chinese** | ZH | ❌ | ❌ | ✅ | ✅ | ✅ (Mandarin) | ✅ | ✅ | ✅ + 18 dialects | ✅ | ❌ | ✅ (Mandarin + Sichuanese, Cantonese) | ❌ | ✅ (Mandarin; official also exposes YUE separately outside this matrix) | ✅ | ✅ |
| 🇩🇪 **German** | DE | ✅ | ✅ (×3) | ✅ | ✅ (Kugel) | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ |
| 🇪🇸 **Spanish** | ES | ✅ | ❌ | ✅ | ✅ (Kugel) | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ |
| 🇫🇷 **French** | FR | ✅ | ✅ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ |
| 🇮🇹 **Italian** | IT | ✅ | ✅ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| 🇯🇵 **Japanese** | JA | ✅ | ✅ | ✅ | ✅ (Kugel) | ❌ | ✅ | ✅ ? | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
| 🇰🇷 **Korean** | KO | ❌ | ✅ | ✅ | ✅ (Kugel) | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| 🇷🇺 **Russian** | RU | ❌ | ✅ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| 🇧🇷 **Portuguese** | PT | ✅ (BR) | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ✅ (EU/BR*) | ✅ | ❌ | ❌ | ✅ (Official PT tag is generic; upstream does not expose separate PT-BR/PT-PT tags and it may lean more European Portuguese than Brazilian Portuguese) | ✅ | ✅ |
| 🇵🇱 **Polish** | PL | ✅ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| 🇮🇳 **Hindi** | HI | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
| 🇪🇬 **Arabic** | AR | ❌ | ❌ | ✅ (Egyptian) | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| 🇹🇷 **Turkish** | TR | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| 🇹🇭 **Thai** | TH | ✅ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
| 🇳🇴 **Norwegian** | NO | ❌ | ✅ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇻🇳 **Vietnamese** | VI | ❌ | ❌ | ✅ (Viterbox) | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
| 🇦🇲 **Armenian** | HY | ❌ | ✅ | ❌ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇬🇪 **Georgian** | KA | ❌ | ✅ | ❌ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇩🇰 **Danish** | DA | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 🇫🇮 **Finnish** | FI | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
| 🇬🇷 **Greek** | EL | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| 🇮🇱 **Hebrew** | HE | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇲🇾 **Malay** | MS | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇳🇱 **Dutch** | NL | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
| 🇸🇪 **Swedish** | SV | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 🇰🇪 **Swahili** | SW | ❌ | ❌ | ✅ | ✅ (Kugel) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇿🇦 **Afrikaans** | AF | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇦🇱 **Albanian** | SQ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Assamese** | AS | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇸 **Asturian** | AST | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇦🇿 **Azerbaijani** | AZ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇷🇺 **Bashkir** | BA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇸 **Basque** | EU | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇧🇾 **Belarusian** | BE | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇧🇩 **Bengali** | BN | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇧🇦 **Bosnian** | BS | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇧🇬 **Bulgarian** | BG | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇸 **Catalan** | CA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇵🇭 **Cebuano** | CEB | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇶 **Central Kurdish** | CKB | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇭🇷 **Croatian** | HR | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇨🇿 **Czech** | CS | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇷🇺 **Eastern Mari** | MHR | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🌍 **Esperanto** | EO | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇪 **Estonian** | ET | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇸 **Galician** | GL | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Gujarati** | GU | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇭🇹 **Haitian Creole** | HT | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇳🇬 **Hausa** | HA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇭🇺 **Hungarian** | HU | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇩 **Indonesian** | ID | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇩 **Javanese** | JV | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Kannada** | KN | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇰🇿 **Kazakh** | KK | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇷🇼 **Kinyarwanda** | RW | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇰🇬 **Kyrgyz** | KY | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇱🇻 **Latvian** | LV | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇨🇩 **Lingala** | LN | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇱🇹 **Lithuanian** | LT | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇰🇪 **Luo** | LUO | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇲🇰 **Macedonian** | MK | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Malayalam** | ML | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇲🇹 **Maltese** | MT | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇳🇿 **Māori** | MI | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Marathi** | MR | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇲🇳 **Mongolian** | MN | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇳🇵 **Nepali** | NE | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇫🇷 **Occitan** | OC | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇷 **Persian** | FA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇷🇴 **Romanian** | RO | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇿🇦 **Sepedi** | NSO | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇷🇸 **Serbian** | SR | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇿🇼 **Shona** | SN | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇸🇰 **Slovak** | SK | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇸🇮 **Slovene** | SL | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇵🇭 **Tagalog** | TL | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇹🇯 **Tajik** | TG | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Tamil** | TA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Telugu** | TE | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇺🇦 **Ukrainian** | UK | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇵🇰 **Urdu** | UR | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇨🇳 **Uyghur** | UG | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇺🇿 **Uzbek** | UZ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇿🇦 **Xhosa** | XH | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇿🇦 **Zulu** | ZU | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (polished) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇲🇼 **Chichewa/Nyanja** | NY | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇳 **Eastern Punjabi** | PA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇺🇬 **Ganda** | LG | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇸 **Icelandic** | IS | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇮🇪 **Irish** | GA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇩🇿 **Kabyle** | KAB | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇨🇻 **Kabuverdianu** | KEA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇰🇪 **Kamba** | KAM | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇻🇦 **Latin** | LA | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇱🇺 **Luxembourgish** | LB | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇪🇹 **Oromo** | OM | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇦🇫 **Pashto** | PS | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇵🇰 **Sindhi** | SD | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇸🇴 **Somali** | SO | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🇦🇴 **Umbundu** | UMB | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 🏴 **Welsh** | CY | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (usable) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
**Notes:**
+8
View File
@@ -112,6 +112,14 @@ Use this as the canonical list of model repositories/links for offline setup.
| echo-tts-base (model + PCA state) | [jordand/echo-tts-base](https://huggingface.co/jordand/echo-tts-base) | ~5.3GB | ✅ | pytorch_model.safetensors + pca_state.safetensors |
| fish-s1-dac-min (audio codec) | [jordand/fish-s1-dac-min](https://huggingface.co/jordand/fish-s1-dac-min) | ~1.8GB | ✅ | pytorch_model.safetensors — audio codec required by Echo-TTS |
## Dots TTS
| Component | Source | Size | Auto-Download | Notes |
|---|---|---|---|---|
| dots.tts-base | [rednote-hilab/dots.tts-base](https://huggingface.co/rednote-hilab/dots.tts-base) | ~6GB | ✅ | Official base checkpoint with tokenizer, vocoder, speaker encoder, and latent stats |
| dots.tts-soar | [rednote-hilab/dots.tts-soar](https://huggingface.co/rednote-hilab/dots.tts-soar) | ~6GB | ✅ | Official SOAR checkpoint for higher-quality zero-shot cloning |
| dots.tts-mf | [rednote-hilab/dots.tts-mf](https://huggingface.co/rednote-hilab/dots.tts-mf) | ~6GB | ✅ | Official MeanFlow-distilled checkpoint for faster inference |
## MOSS-TTS
| Component | Source | Size | Auto-Download | Notes |
+34
View File
@@ -314,3 +314,37 @@ Notes:
- Both components are required and auto-downloaded on first use.
- License: CC-BY-NC-SA (non-commercial).
## Dots TTS
```text
ComfyUI/models/TTS/dots_tts/
├── dots.tts-base/
├── dots.tts-soar/
└── dots.tts-mf/
```
Typical checkpoint contents:
```text
ComfyUI/models/TTS/dots_tts/dots.tts-soar/
├── added_tokens.json
├── chat_template.jinja
├── config.json
├── latent_stats.pt
├── llm_config.json
├── merges.txt
├── model.safetensors
├── speaker_encoder.safetensors
├── special_tokens_map.json
├── tokenizer.json
├── tokenizer_config.json
├── vocab.json
└── vocoder.safetensors
```
Notes:
- Downloads into a dedicated `dots_tts/` folder.
- Native sample rate is 48kHz.
- Main-environment support works on Transformers 5; on Windows, `normalize_text` falls back to no-op if `WeTextProcessing` is unavailable.
@@ -43,6 +43,7 @@ Use Qwen3-TTS for:
- SRT processor structure that reuses the main TTS processor for generation.
- Interrupt handling.
- Progress feedback.
- Console observability parity, such as input-text echo and useful live generation progress.
- Unified model loading.
Secondary reference:
@@ -46,17 +46,19 @@ Text features:
- Did you wire parameter switching for real native parameters?
- Did you avoid adding fake unsupported parameters?
- Did you remove misleading controls like language selection if unsupported?
- If the engine supports language control, did you normalize the user-facing language UI/logs honestly and map them back to official backend codes without breaking segment/alias language switching?
- Did you reuse the suite chunking/chunk-combination controls instead of inventing duplicate engine-local chunk UI or chunk-silence parameters?
SRT features:
- If this is a TTS engine, did you implement the SRT path?
- Did you add interrupt checks in long SRT loops?
- Does SRT use the shared timing/assembly systems correctly?
- Does SRT use the shared timing/assembly/reporting systems correctly, using the real existing APIs instead of guessed helper names?
- Did you avoid unnecessary timing-mode restrictions?
- Does character switching work in SRT if text character tags are supported?
UX:
- Did you add progress feedback for long generation?
- Did you preserve useful console observability parity instead of making the engine run silently (for example input-text echo and live it/s/progress)?
- Are error messages clear when required inputs are missing?
- Are tooltips honest about limitations, license, VRAM, and expected text length?
@@ -75,7 +77,7 @@ Manual tests:
- Did character tags work?
- Did pause tags work?
- Did parameter switching work?
- Did Clear VRAM then regenerate work?
- Did Clear VRAM then regenerate work, and did unload actually tear down runtime/cache state instead of only moving weights to CPU?
- Did interrupt/cancel work in long generation?
```
@@ -61,6 +61,13 @@
- **Pattern**: F5-TTS and ChatterBox don't have `__del__`, only IndexTTS and StepAudio did (wrong)
- **Correct behavior**: Models stay in VRAM for reuse; only unload on explicit Clear VRAM button, engine switch, or parameter change
### Clear VRAM - Runtime Teardown vs CPU Offload
- **Wrong**: Assuming every engine is safe to unload with only `.to('cpu')`
- **Symptom**: Clear VRAM frees only part of VRAM; most memory disappears only after closing ComfyUI
- **Cause**: Runtime-heavy engines can keep compiled graphs, prompt caches, workspaces, or runtime objects alive after CPU offload
- **Correct**: Add an engine-specific unload handler that clears internal caches, drops runtime/model references, then runs `gc.collect()`, `torch.cuda.empty_cache()`, and `torch.cuda.ipc_collect()` when available
- **Prevention**: If the engine uses warmup, `torch.compile`, streaming caches, or a custom runtime object, explicitly test Clear VRAM and reload instead of assuming generic offload parity
### Generation Info Reports - Modular Pattern
- **CRITICAL - Consistency required**: ALL engines must use `ChunkCombiner` + `ChunkTimingHelper` for generation reports
- **Correct pattern** (from ChatterBox 23-Lang, F5-TTS):
@@ -74,10 +81,27 @@
### Segment Parameter Registration
- **Forgot to add**: New engines to `PARAMETER_ENGINES` in `utils/text/segment_parameters.py` for `[seed:X]` tag support
### Language UI Normalization
- **Wrong**: Exposing raw backend language codes in the engine node just because the official runtime accepts them
- **Symptom**: New engine UI looks inconsistent with the suite, logs show backend codes instead of readable names, and character-tag / alias languages do not map cleanly to node config values
- **Correct**: Keep user-facing language controls normalized to honest display names when the suite already does that, then map them back to the official backend codes in the adapter/runtime layer
- **Prevention**: Reuse or add a small engine-local language helper so node UI, processor logs, segment language overrides, and adapter normalization all agree on the same mapping
### SRT Processor Import Pattern
- **Wrong**: Using `from nodes.xxx import` - fails with `'nodes' is not a package`
- **Correct**: Use `importlib.util.spec_from_file_location()` for cross-module imports in dynamically loaded processors
### SRT Shared API Guessing
- **Wrong**: Inventing shared helper names like `AudioAssemblyEngine.assemble(...)`, `TimingReportGenerator`, or `AdjustedSRTGenerator`
- **Correct**: Open a working SRT processor first and match the exact API that already exists in `utils/timing/assembly.py`, `utils/timing/engine.py`, and `utils/timing/reporting.py`
- **Real pattern**:
- `stretch_to_fit` → `TimedAudioAssembler.assemble_timed_audio(...)`
- `pad_with_silence` → `AudioAssemblyEngine(...).assemble_with_overlaps(...)`
- `concatenate` → `TimingEngine.calculate_concatenation_adjustments(...)` + `assemble_concatenation(...)`
- `smart_natural` → `TimingEngine.calculate_smart_timing_adjustments(...)` + `assemble_smart_natural(...)`
- reporting / adjusted SRT → `SRTReportGenerator().generate_timing_report(...)` and `generate_adjusted_srt_string(...)`
- **Prevention**: Never assume shared helper names. Grep existing SRT processors and copy the real call pattern exactly.
### ComfyUI Audio Dict Format
- **Wrong**: Passing `audio_tensor` directly to functions expecting raw tensor
- **Correct**: Extract waveform with `audio_tensor.get('waveform')` - ComfyUI audio is `{"waveform": tensor, "sample_rate": int}`
+12 -1
View File
@@ -40,6 +40,15 @@ except Exception as e:
def __init__(self, *args, **kwargs):
raise ImportError(f"Echo-TTS adapter not available: {e}")
try:
from .dots_tts_adapter import DotsTTSEngineAdapter
DOTS_TTS_ADAPTER_AVAILABLE = True
except Exception as e:
DOTS_TTS_ADAPTER_AVAILABLE = False
class DotsTTSEngineAdapter:
def __init__(self, *args, **kwargs):
raise ImportError(f"Dots TTS adapter not available: {e}")
try:
from .moss_tts_adapter import MossTTSEngineAdapter
MOSS_TTS_ADAPTER_AVAILABLE = True
@@ -60,7 +69,9 @@ except Exception as e:
__all__ = [
'ChatterBoxEngineAdapter', 'F5TTSEngineAdapter', 'CosyVoiceAdapter', 'EchoTTSEngineAdapter',
'DotsTTSEngineAdapter',
'MossTTSEngineAdapter', 'HiggsAudioV3EngineAdapter',
'CHATTERBOX_ADAPTER_AVAILABLE', 'F5TTS_ADAPTER_AVAILABLE', 'COSYVOICE_ADAPTER_AVAILABLE',
'ECHO_TTS_ADAPTER_AVAILABLE', 'MOSS_TTS_ADAPTER_AVAILABLE', 'HIGGS_AUDIO_V3_ADAPTER_AVAILABLE'
'ECHO_TTS_ADAPTER_AVAILABLE', 'DOTS_TTS_ADAPTER_AVAILABLE',
'MOSS_TTS_ADAPTER_AVAILABLE', 'HIGGS_AUDIO_V3_ADAPTER_AVAILABLE'
]
+225
View File
@@ -0,0 +1,225 @@
"""
Dots TTS engine adapter.
Translates TTS Audio Suite processor calls into the official dots.tts runtime.
"""
import os
import sys
from typing import Any, Dict, Optional, Tuple
import torch
current_dir = os.path.dirname(__file__)
engines_dir = os.path.dirname(current_dir)
project_root = os.path.dirname(engines_dir)
if project_root not in sys.path:
sys.path.insert(0, project_root)
from utils.audio.audio_hash import generate_stable_audio_component
from utils.audio.cache import get_audio_cache
from utils.audio.processing import AudioProcessingUtils
from utils.models.factory_config import ModelLoadConfig
from utils.models.language_mapper import resolve_language_alias
from engines.dots_tts.languages import normalize_dots_language
class DotsTTSEngineAdapter:
"""Adapter for official Dots TTS inference."""
SAMPLE_RATE = 48000
def __init__(self, config: Optional[Dict[str, Any]] = None):
self.config = config.copy() if config else {}
self.audio_cache = get_audio_cache()
self._last_config: Optional[ModelLoadConfig] = None
self._load_signature = None
def update_config(self, new_config: Dict[str, Any]):
self.config = new_config.copy() if new_config else {}
def _build_load_signature(self) -> Tuple[Any, ...]:
return (
self.config.get("model_variant", "dots.tts-soar"),
self.config.get("device", "auto"),
self.config.get("precision", "auto"),
bool(self.config.get("optimize", False)),
int(self.config.get("max_generate_length", 500)),
)
def load_model(
self,
model_variant: str,
device: str = "auto",
precision: str = "auto",
optimize: bool = False,
max_generate_length: int = 500,
):
from utils.models.unified_model_interface import unified_model_interface
config = ModelLoadConfig(
engine_name="dots_tts",
model_type="tts",
model_name=model_variant,
model_path=model_variant,
device=device,
additional_params={
"precision": precision,
"optimize": bool(optimize),
"max_generate_length": int(max_generate_length),
},
)
self._last_config = config
unified_model_interface.load_model(config)
def _ensure_model_loaded(self):
signature = self._build_load_signature()
if signature == self._load_signature and self._last_config is not None:
return
self.load_model(
model_variant=self.config.get("model_variant", "dots.tts-soar"),
device=self.config.get("device", "auto"),
precision=self.config.get("precision", "auto"),
optimize=self.config.get("optimize", False),
max_generate_length=self.config.get("max_generate_length", 500),
)
self._load_signature = signature
def _get_engine(self):
if self._last_config is None:
self._ensure_model_loaded()
from utils.models.unified_model_interface import unified_model_interface
return unified_model_interface.load_model(self._last_config)
def _extract_voice_reference(self, voice_ref: Optional[Dict[str, Any]]) -> Tuple[Optional[str], str, str]:
if not voice_ref or not isinstance(voice_ref, dict):
return None, "", "default_voice"
prompt_text = (
voice_ref.get("reference_text")
or voice_ref.get("prompt_text")
or voice_ref.get("text")
or ""
).strip()
ref_audio = (
voice_ref.get("prompt_audio_path")
or voice_ref.get("audio_path")
or voice_ref.get("audio")
or voice_ref.get("waveform")
)
if ref_audio is None:
return None, prompt_text, "default_voice"
if isinstance(ref_audio, str):
return ref_audio, prompt_text, generate_stable_audio_component(audio_file_path=ref_audio)
if isinstance(ref_audio, dict) and "waveform" in ref_audio:
temp_path = AudioProcessingUtils.save_audio_to_temp_file(
ref_audio["waveform"],
ref_audio.get("sample_rate", self.SAMPLE_RATE),
)
return temp_path, prompt_text, generate_stable_audio_component(reference_audio=ref_audio)
if torch.is_tensor(ref_audio):
sample_rate = int(voice_ref.get("sample_rate", self.SAMPLE_RATE))
audio_dict = {"waveform": ref_audio, "sample_rate": sample_rate}
temp_path = AudioProcessingUtils.save_audio_to_temp_file(ref_audio, sample_rate)
return temp_path, prompt_text, generate_stable_audio_component(reference_audio=audio_dict)
raise TypeError(f"Unsupported Dots TTS voice reference type: {type(ref_audio)}")
@staticmethod
def _normalize_language(language: Optional[str]) -> Optional[str]:
if language is None:
return None
normalized = str(language).strip()
if not normalized:
return None
normalized_language = normalize_dots_language(normalized)
if normalized_language is not None:
return normalized_language
resolved = resolve_language_alias(normalized)
return normalize_dots_language(resolved)
def generate_single(
self,
text: str,
voice_ref: Optional[Dict[str, Any]],
seed: int = 0,
enable_audio_cache: bool = True,
character_name: Optional[str] = None,
) -> torch.Tensor:
stripped = (text or "").strip()
if not stripped:
return torch.zeros(1, 0, dtype=torch.float32)
self._ensure_model_loaded()
engine = self._get_engine()
prompt_audio_path, prompt_text, audio_component = self._extract_voice_reference(voice_ref)
language = self._normalize_language(self.config.get("language", "auto"))
model_variant = self.config.get("model_variant", "dots.tts-soar")
template_name = self.config.get("template_name", "tts")
num_steps = int(self.config.get("num_steps", 10))
guidance_scale = float(self.config.get("guidance_scale", 1.2))
speaker_scale = float(self.config.get("speaker_scale", 1.5))
normalize_text = bool(self.config.get("normalize_text", False))
precision = self.config.get("precision", "auto")
optimize = bool(self.config.get("optimize", False))
max_generate_length = int(self.config.get("max_generate_length", 500))
cache_key = None
if enable_audio_cache:
cache_key = self.audio_cache.generate_cache_key(
"dots_tts",
text=stripped,
audio_component=audio_component,
prompt_text=prompt_text,
model_variant=model_variant,
language=language or "none",
template_name=template_name,
num_steps=num_steps,
guidance_scale=guidance_scale,
speaker_scale=speaker_scale,
normalize_text=normalize_text,
max_generate_length=max_generate_length,
precision=precision,
optimize=optimize,
seed=int(seed or 0),
device=self.config.get("device", "auto"),
character=character_name or "narrator",
)
cached_audio = self.audio_cache.get_cached_audio(cache_key)
if cached_audio:
print(f"💾 Using cached Dots TTS audio for '{character_name or 'narrator'}': '{stripped[:30]}...'")
return cached_audio[0]
result = engine.generate(
text=stripped,
prompt_audio_path=prompt_audio_path,
prompt_text=prompt_text or None,
language=language,
num_steps=num_steps,
guidance_scale=guidance_scale,
speaker_scale=speaker_scale,
normalize_text=normalize_text,
template_name=template_name,
seed=int(seed or 0),
)
audio_tensor = result["audio"]
if not isinstance(audio_tensor, torch.Tensor):
audio_tensor = torch.tensor(audio_tensor, dtype=torch.float32)
audio_tensor = audio_tensor.detach().float().cpu()
if audio_tensor.dim() == 1:
audio_tensor = audio_tensor.unsqueeze(0)
if enable_audio_cache and cache_key:
duration = self.audio_cache._calculate_duration(audio_tensor, "dots_tts")
self.audio_cache.cache_audio(cache_key, audio_tensor, duration)
return audio_tensor
+6
View File
@@ -0,0 +1,6 @@
"""Dots TTS engine integration."""
from .dots_tts_engine import DotsTTSEngine
from .dots_tts_downloader import DotsTTSDownloader
__all__ = ["DotsTTSEngine", "DotsTTSDownloader"]
+160
View File
@@ -0,0 +1,160 @@
"""
Dots TTS model downloader.
Downloads official Rednote dots.tts checkpoints into
ComfyUI/models/TTS/dots_tts/ instead of hidden Hugging Face cache folders.
"""
import logging
import os
from contextlib import contextmanager
from typing import Dict, Optional
import folder_paths
from huggingface_hub import snapshot_download
from utils.models.extra_paths import get_all_tts_model_paths, get_preferred_download_path
@contextmanager
def _suppress_hf_http_logs():
# Keep HF download output readable by muting per-request chatter during snapshot downloads.
logger_names = ("httpx", "httpcore", "huggingface_hub")
original_levels = {}
try:
for name in logger_names:
logger = logging.getLogger(name)
original_levels[name] = logger.level
logger.setLevel(logging.WARNING)
yield
finally:
for name, level in original_levels.items():
logging.getLogger(name).setLevel(level)
class DotsTTSDownloader:
"""Resolve and download official dots.tts model folders."""
MODELS: Dict[str, Dict[str, str]] = {
"dots.tts-base": {
"repo_id": "rednote-hilab/dots.tts-base",
"description": "Official Dots TTS base checkpoint",
},
"dots.tts-soar": {
"repo_id": "rednote-hilab/dots.tts-soar",
"description": "Official Dots TTS SOAR checkpoint",
},
"dots.tts-mf": {
"repo_id": "rednote-hilab/dots.tts-mf",
"description": "Official Dots TTS MeanFlow checkpoint",
},
}
REQUIRED_FILES = [
"added_tokens.json",
"chat_template.jinja",
"config.json",
"latent_stats.pt",
"llm_config.json",
"merges.txt",
"model.safetensors",
"speaker_encoder.safetensors",
"special_tokens_map.json",
"tokenizer.json",
"tokenizer_config.json",
"vocab.json",
"vocoder.safetensors",
]
def __init__(self, base_path: Optional[str] = None):
if base_path is None:
try:
self.base_path = get_preferred_download_path(model_type="TTS", engine_name="dots_tts")
except Exception:
self.base_path = os.path.join(folder_paths.models_dir, "TTS", "dots_tts")
else:
self.base_path = base_path
os.makedirs(self.base_path, exist_ok=True)
def resolve_model_path(self, model_identifier: str) -> str:
"""Resolve local: names, absolute paths, or known official model identifiers."""
if not model_identifier:
model_identifier = "dots.tts-soar"
if os.path.isabs(model_identifier) and os.path.exists(model_identifier):
return model_identifier
if os.path.isdir(model_identifier):
return model_identifier
if model_identifier.startswith("local:"):
local_name = model_identifier[6:]
for base_path in get_all_tts_model_paths("TTS"):
for folder_name in ("dots_tts", "DOTS", ""):
candidate = os.path.join(base_path, folder_name, local_name) if folder_name else os.path.join(base_path, local_name)
if os.path.exists(candidate):
print(f"📁 Using local Dots TTS model: {candidate}")
return candidate
raise FileNotFoundError(f"Local Dots TTS model not found: {local_name}")
if model_identifier in self.MODELS:
return self.get_model_path(model_identifier)
raise FileNotFoundError(f"Unknown Dots TTS model identifier: {model_identifier}")
def get_model_path(self, model_name: str) -> str:
model_dir = os.path.join(self.base_path, model_name)
if not os.path.exists(model_dir) or not self._is_model_complete(model_dir):
return self.download_model(model_name, force=os.path.exists(model_dir))
return model_dir
def download_model(self, model_name: str, force: bool = False) -> str:
if model_name not in self.MODELS:
raise ValueError(f"Unknown Dots TTS model: {model_name}")
model_info = self.MODELS[model_name]
repo_id = model_info["repo_id"]
model_dir = os.path.join(self.base_path, model_name)
print(f"\n{'=' * 60}")
print("📦 Dots TTS Model Download")
print(f"{'=' * 60}")
print(f"Model: {model_name}")
print(f"Description: {model_info['description']}")
print(f"Repository: {repo_id}")
print(f"Target: {model_dir}")
print(f"{'=' * 60}\n")
try:
with _suppress_hf_http_logs():
snapshot_download(
repo_id=repo_id,
local_dir=model_dir,
local_dir_use_symlinks=False,
resume_download=True,
force_download=force,
)
except Exception as e:
raise RuntimeError(f"Failed to download {model_name} from {repo_id}: {e}") from e
if not self._is_model_complete(model_dir):
raise RuntimeError(f"Downloaded Dots TTS model is incomplete: {model_dir}")
print(f"✅ Dots TTS model ready: {model_dir}")
return model_dir
def _is_model_complete(self, model_dir: str) -> bool:
if not os.path.isdir(model_dir):
return False
missing = [
rel_path
for rel_path in self.REQUIRED_FILES
if not os.path.exists(os.path.join(model_dir, rel_path))
]
if missing:
print(f"❌ Dots TTS model incomplete. Missing {len(missing)} file(s):")
for rel_path in missing:
print(f" - {rel_path}")
return False
return True
+446
View File
@@ -0,0 +1,446 @@
"""
Dots TTS engine wrapper.
Wraps the official dots.tts runtime with ComfyUI-friendly model lifecycle hooks.
"""
import os
import sys
import types
import importlib
import logging
import time
import warnings
import gc
from contextlib import contextmanager
from typing import Any, Dict, Optional
import torch
current_dir = os.path.dirname(__file__)
engines_dir = os.path.dirname(current_dir)
project_root = os.path.dirname(engines_dir)
if project_root not in sys.path:
sys.path.insert(0, project_root)
from engines.dots_tts.dots_tts_downloader import DotsTTSDownloader
from engines.dots_tts.progress_callback import DotsTTSProgressTracker
from utils.device import resolve_torch_device
class _SuppressDotsModelTypeWarning(logging.Filter):
"""Drop the upstream bogus model-type warning while keeping real ones."""
def filter(self, record: logging.LogRecord) -> bool:
try:
message = record.getMessage()
except Exception:
return True
return "model of type `dots_tts` to instantiate a model of type" not in message
class DotsTTSEngine:
"""Thin wrapper around the official DotsTtsRuntime."""
SAMPLE_RATE = 48000
_normalizer_warning_shown = False
def __init__(
self,
model_name: str = "dots.tts-soar",
device: str = "auto",
precision: str = "auto",
optimize: bool = False,
max_generate_length: int = 500,
model_dir: Optional[str] = None,
):
self.model_name = model_name
self.device = resolve_torch_device(device)
self.precision = self._resolve_precision(precision, self.device)
self.optimize = bool(optimize)
self.max_generate_length = int(max_generate_length)
self.model_dir = model_dir or DotsTTSDownloader().resolve_model_path(model_name)
self._runtime = None
@staticmethod
def _resolve_precision(precision: str, device: str) -> str:
normalized = str(precision or "auto").lower()
if normalized in {"bfloat16", "float16", "float32"}:
if device == "cpu" and normalized != "float32":
return "float32"
return normalized
if device == "cpu":
return "float32"
if torch.cuda.is_available():
major, _minor = torch.cuda.get_device_capability()
if major >= 8:
return "bfloat16"
return "float16"
def _import_runtime(self):
self._ensure_text_normalizer_fallback()
importlib.invalidate_caches()
nodes_dir = os.path.join(project_root, "nodes")
blocked_paths = {
os.path.abspath(engines_dir),
os.path.abspath(nodes_dir),
}
original_sys_path = list(sys.path)
# ComfyUI can place the suite's `engines/` directory directly on sys.path.
# That makes our local `engines/dots_tts` package shadow the pip-installed
# official `dots_tts` package we actually need here.
sys.path[:] = [
path for path in sys.path
if os.path.abspath(path or os.getcwd()) not in blocked_paths
]
stale_modules = []
for module_name, module in list(sys.modules.items()):
if not (module_name == "dots_tts" or module_name.startswith("dots_tts.")):
continue
module_file = getattr(module, "__file__", "") or ""
if module_file and os.path.abspath(module_file).startswith(os.path.abspath(project_root)):
stale_modules.append((module_name, module))
del sys.modules[module_name]
try:
from dots_tts.runtime import DotsTtsRuntime
except Exception as e:
for module_name, module in stale_modules:
sys.modules.setdefault(module_name, module)
raise ImportError(
"Failed to import the official `dots_tts` package in the active ComfyUI Python "
"environment. This is usually either a missing install or an import-path "
f"collision. Active Python: {sys.executable}"
) from e
finally:
sys.path[:] = original_sys_path
return DotsTtsRuntime
@staticmethod
@contextmanager
def _suppress_noisy_runtime_logs():
"""Mute verbose Dots chatter while keeping actionable Transformers warnings."""
dots_logger = None
dots_logger_disabled = False
transformers_filter = _SuppressDotsModelTypeWarning()
transformer_loggers = []
try:
from loguru import logger as dots_logger
dots_logger.disable("dots_tts")
dots_logger_disabled = True
transformer_loggers = [
logging.getLogger("transformers"),
logging.getLogger("transformers.configuration_utils"),
logging.getLogger("transformers.modeling_utils"),
logging.getLogger("transformers.tokenization_utils_base"),
]
for logger in transformer_loggers:
logger.addFilter(transformers_filter)
except Exception:
pass
try:
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
message=r".*model of type `dots_tts` to instantiate a model of type.*",
)
yield
finally:
if dots_logger is not None and dots_logger_disabled:
try:
dots_logger.enable("dots_tts")
except Exception:
pass
for logger in transformer_loggers:
try:
logger.removeFilter(transformers_filter)
except Exception:
pass
@staticmethod
@contextmanager
def _patch_tokenizer_regex_fix():
"""Force the upstream Dots tokenizer load to opt into the Mistral regex fix."""
try:
from transformers import AutoTokenizer
except Exception:
yield
return
original_from_pretrained = AutoTokenizer.from_pretrained
def patched_from_pretrained(*args, **kwargs):
kwargs.setdefault("fix_mistral_regex", True)
return original_from_pretrained(*args, **kwargs)
AutoTokenizer.from_pretrained = patched_from_pretrained
try:
yield
finally:
AutoTokenizer.from_pretrained = original_from_pretrained
@staticmethod
def _ensure_text_normalizer_fallback():
"""Provide a no-op WeTextProcessing fallback when tn is unavailable."""
try:
import tn # noqa: F401
return
except Exception:
pass
class _NoOpNormalizer:
def normalize(self, text: str) -> str:
return text
tn_module = types.ModuleType("tn")
chinese_module = types.ModuleType("tn.chinese")
chinese_normalizer_module = types.ModuleType("tn.chinese.normalizer")
english_module = types.ModuleType("tn.english")
english_normalizer_module = types.ModuleType("tn.english.normalizer")
chinese_normalizer_module.Normalizer = _NoOpNormalizer
english_normalizer_module.Normalizer = _NoOpNormalizer
chinese_module.normalizer = chinese_normalizer_module
english_module.normalizer = english_normalizer_module
tn_module.chinese = chinese_module
tn_module.english = english_module
sys.modules.setdefault("tn", tn_module)
sys.modules.setdefault("tn.chinese", chinese_module)
sys.modules.setdefault("tn.chinese.normalizer", chinese_normalizer_module)
sys.modules.setdefault("tn.english", english_module)
sys.modules.setdefault("tn.english.normalizer", english_normalizer_module)
if not DotsTTSEngine._normalizer_warning_shown:
print("[Dots TTS] WeTextProcessing not available; normalize_text will use a no-op fallback")
DotsTTSEngine._normalizer_warning_shown = True
def _ensure_runtime_loaded(self):
if self._runtime is not None:
return
DotsTtsRuntime = self._import_runtime()
with self._patch_tokenizer_regex_fix(), self._suppress_noisy_runtime_logs():
self._runtime = DotsTtsRuntime.from_pretrained(
self.model_dir,
precision=self.precision,
optimize=self.optimize,
max_generate_length=self.max_generate_length,
)
self._ensure_runtime_device()
def _current_runtime_device(self) -> Optional[str]:
if self._runtime is None:
return None
try:
first_param = next(self._runtime.model.parameters())
return str(first_param.device)
except Exception:
return str(getattr(self._runtime, "device", self.device))
def _move_runtime_to(self, target_device: str):
if self._runtime is None:
return
device_obj = torch.device(target_device)
runtime = self._runtime
runtime.model = runtime.model.to(device_obj).eval()
runtime.device = device_obj
def _ensure_runtime_device(self):
if self._runtime is None:
return
target_device = resolve_torch_device(self.device)
current_device = self._current_runtime_device()
if self._devices_equivalent(current_device, target_device):
return
if current_device != target_device:
print(f"🔄 Dots TTS: moving runtime from {current_device} to {target_device}")
self._move_runtime_to(target_device)
@staticmethod
def _devices_equivalent(current_device: Optional[str], target_device: Optional[str]) -> bool:
if current_device == target_device:
return True
if not current_device or not target_device:
return False
current = str(current_device)
target = str(target_device)
if current.startswith("cuda") and target.startswith("cuda"):
return current in {"cuda", "cuda:0"} and target in {"cuda", "cuda:0"}
return False
@staticmethod
def _clear_runtime_caches(runtime: Any) -> None:
if runtime is None:
return
model = getattr(runtime, "model", None)
if model is not None:
try:
if hasattr(model, "set_optimize"):
model.set_optimize(False)
except Exception:
pass
for attr_name in (
"_compiled_models",
"_prompt_feature_cache",
"_static_generate_workspaces",
"_fm_decode_workspaces",
):
try:
cache = getattr(model, attr_name, None)
if hasattr(cache, "clear"):
cache.clear()
except Exception:
pass
for attr_name in (
"_double_streaming_prompt_g_cond_cache",
"_double_streaming_silence_audio_patch_cache",
):
try:
cache = getattr(runtime, attr_name, None)
if hasattr(cache, "clear"):
cache.clear()
except Exception:
pass
def unload_runtime(self):
"""Fully tear down the Dots runtime so Clear VRAM actually releases it."""
runtime = self._runtime
if runtime is None:
if torch.cuda.is_available():
torch.cuda.empty_cache()
if hasattr(torch.cuda, "ipc_collect"):
try:
torch.cuda.ipc_collect()
except Exception:
pass
gc.collect()
return
self._clear_runtime_caches(runtime)
model = getattr(runtime, "model", None)
try:
if model is not None and hasattr(model, "to"):
model.to("cpu")
except Exception:
pass
try:
runtime.model = None
except Exception:
pass
self._runtime = None
try:
import torch._dynamo as torch_dynamo
torch_dynamo.reset()
except Exception:
pass
del model
del runtime
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
if hasattr(torch.cuda, "ipc_collect"):
try:
torch.cuda.ipc_collect()
except Exception:
pass
def to(self, device):
"""ComfyUI unload/reload hook."""
self.device = str(device) if isinstance(device, str) else str(torch.device(device))
self._ensure_runtime_device()
return self
def unload(self):
"""Explicit unload hook used by engine-specific ComfyUI handlers."""
self.unload_runtime()
def generate(
self,
text: str,
prompt_audio_path: Optional[str] = None,
prompt_text: Optional[str] = None,
language: Optional[str] = None,
num_steps: int = 10,
guidance_scale: float = 1.2,
speaker_scale: float = 1.5,
normalize_text: bool = False,
template_name: Optional[str] = "tts",
seed: int = 42,
) -> Dict[str, Any]:
self._ensure_runtime_loaded()
self._ensure_runtime_device()
seed_value = int(seed or 0)
if seed_value > 0:
torch.manual_seed(seed_value)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed_value)
chunks = []
tracker = DotsTTSProgressTracker(
max_generate_length=self.max_generate_length,
sample_rate=self.SAMPLE_RATE,
)
start_time = time.time()
with self._patch_tokenizer_regex_fix(), self._suppress_noisy_runtime_logs():
stream = self._runtime.generate_stream(
text=text,
prompt_audio_path=prompt_audio_path,
prompt_text=prompt_text,
language=language,
template_name=template_name,
num_steps=int(num_steps),
guidance_scale=float(guidance_scale),
speaker_scale=float(speaker_scale),
normalize_text=bool(normalize_text),
)
for chunk in stream:
if not isinstance(chunk, torch.Tensor):
chunk = torch.tensor(chunk, dtype=torch.float32)
chunk = chunk.detach().float().cpu()
if chunk.dim() > 1:
chunk = chunk.squeeze()
chunks.append(chunk)
tracker.update(chunk.shape[-1] if chunk.numel() > 0 else 0)
tracker.end()
if chunks:
audio = torch.cat(chunks, dim=-1)
else:
audio = torch.zeros(0, dtype=torch.float32)
time_used = time.time() - start_time
duration_seconds = audio.shape[-1] / self.SAMPLE_RATE if audio.numel() > 0 else 0.0
rtf = time_used / duration_seconds if duration_seconds > 0 else float("inf")
return {
"fid": None,
"audio": audio,
"sample_rate": self.SAMPLE_RATE,
"time_used": time_used,
"rtf": rtf,
"profiling": None,
}
+137
View File
@@ -0,0 +1,137 @@
"""
Language helpers for Dots TTS.
"""
from typing import Optional
DOTS_LANGUAGE_CODE_BY_NAME = {
"arabic": "AR",
"czech": "CS",
"german": "DE",
"greek": "EL",
"english": "EN",
"spanish": "ES",
"finnish": "FI",
"french": "FR",
"hindi": "HI",
"indonesian": "ID",
"italian": "IT",
"japanese": "JA",
"korean": "KO",
"dutch": "NL",
"polish": "PL",
"portuguese": "PT",
"pt": "PT",
"pt-br": "PT",
"pt-pt": "PT",
"po": "PT",
"romanian": "RO",
"russian": "RU",
"thai": "TH",
"turkish": "TR",
"ukrainian": "UK",
"uk": "UK",
"vietnamese": "VI",
"cantonese": "YUE",
"yue": "YUE",
"chinese": "ZH",
"mandarin": "ZH",
"zh": "ZH",
"zh-cn": "ZH",
"zh-tw": "ZH",
}
DOTS_LANGUAGE_DISPLAY_BY_CODE = {
"AR": "Arabic",
"CS": "Czech",
"DE": "German",
"EL": "Greek",
"EN": "English",
"ES": "Spanish",
"FI": "Finnish",
"FR": "French",
"HI": "Hindi",
"ID": "Indonesian",
"IT": "Italian",
"JA": "Japanese",
"KO": "Korean",
"NL": "Dutch",
"PL": "Polish",
"PT": "Portuguese",
"RO": "Romanian",
"RU": "Russian",
"TH": "Thai",
"TR": "Turkish",
"UK": "Ukrainian",
"VI": "Vietnamese",
"YUE": "Cantonese",
"ZH": "Chinese",
}
DOTS_LANGUAGE_OPTIONS = [
"Auto",
"None",
"Arabic",
"Czech",
"German",
"Greek",
"English",
"Spanish",
"Finnish",
"French",
"Hindi",
"Indonesian",
"Italian",
"Japanese",
"Korean",
"Dutch",
"Polish",
"Portuguese",
"Romanian",
"Russian",
"Thai",
"Turkish",
"Ukrainian",
"Vietnamese",
"Cantonese",
"Chinese",
]
def normalize_dots_language(language: Optional[str]) -> Optional[str]:
if language is None:
return None
normalized = str(language).strip()
if not normalized:
return None
lowered = normalized.lower()
if lowered in {"auto", "auto detect", "auto_detect"}:
return "auto_detect"
if lowered in {"none", "off", "no tag", "no language tag"}:
return None
mapped = DOTS_LANGUAGE_CODE_BY_NAME.get(lowered)
if mapped:
return mapped
upper_value = normalized.upper()
if upper_value in DOTS_LANGUAGE_DISPLAY_BY_CODE:
return upper_value
if lowered == "yue":
return "YUE"
return upper_value
def format_dots_language_display(language: Optional[str]) -> str:
normalized = normalize_dots_language(language)
if normalized == "auto_detect":
return "Auto"
if normalized is None:
return "None"
return DOTS_LANGUAGE_DISPLAY_BY_CODE.get(normalized, str(language).strip() or "Auto")
+66
View File
@@ -0,0 +1,66 @@
"""
Dots TTS progress callback.
Provides suite-owned console progress output during streaming generation.
"""
import time
class DotsTTSProgressTracker:
"""Track Dots streaming progress with live it/s and RTF reporting."""
def __init__(self, max_generate_length: int, sample_rate: int):
self.max_generate_length = max(1, int(max_generate_length or 1))
self.sample_rate = max(1, int(sample_rate or 1))
self.generated_chunks = 0
self.emitted_samples = 0
self.start_time = time.time()
self.last_print_time = self.start_time
self.last_print_chunks = 0
def update(self, chunk_samples: int) -> None:
self.generated_chunks += 1
self.emitted_samples += max(0, int(chunk_samples or 0))
current_time = time.time()
should_print = (
self.generated_chunks == 1 or
current_time - self.last_print_time >= 0.5
)
if not should_print:
return
chunk_delta = self.generated_chunks - self.last_print_chunks
time_delta = current_time - self.last_print_time
its = chunk_delta / time_delta if time_delta > 0 else 0.0
elapsed = current_time - self.start_time
audio_seconds = self.emitted_samples / self.sample_rate
rtf = elapsed / audio_seconds if audio_seconds > 0 else 0.0
bar_width = 12
display_chunks = min(self.generated_chunks, self.max_generate_length)
filled = int(bar_width * display_chunks / self.max_generate_length)
filled = min(filled, bar_width)
progress_bar = f"[{'█' * filled}{'░' * (bar_width - filled)}] {display_chunks}/{self.max_generate_length}"
print(
f"\r Progress: {progress_bar} | {its:.1f} it/s | {elapsed:.0f}s | "
f"{audio_seconds:.1f}s audio | RTF {rtf:.2f} ",
end="",
flush=True,
)
self.last_print_time = current_time
self.last_print_chunks = self.generated_chunks
def end(self) -> None:
total_time = time.time() - self.start_time
avg_its = self.generated_chunks / total_time if total_time > 0 else 0.0
audio_seconds = self.emitted_samples / self.sample_rate
rtf = total_time / audio_seconds if audio_seconds > 0 else 0.0
print(
f"\r Complete: {self.generated_chunks} chunks in {total_time:.1f}s "
f"(avg {avg_its:.1f} it/s, {audio_seconds:.1f}s audio, RTF {rtf:.2f})"
+ " " * 20
)
+29 -1
View File
@@ -1130,7 +1130,34 @@ class TTSAudioInstaller:
"Installing Echo-TTS from GitHub (--no-deps)",
ignore_errors=True
)
def install_dots_tts(self):
"""Install official Dots TTS with minimal dependency impact."""
self.log("Installing Dots TTS engine", "INFO")
dependency_probes = [
("lingua", ["install", "lingua-language-detector"], "Installing lingua-language-detector"),
("langcodes", ["install", "langcodes"], "Installing langcodes"),
("loguru", ["install", "loguru"], "Installing loguru"),
("tn", ["install", "WeTextProcessing"], "Installing WeTextProcessing"),
]
for module_name, pip_args, description in dependency_probes:
if self.verify_python_import(module_name):
self.log(f"{module_name} import already satisfied - skipping", "SUCCESS")
continue
self.run_pip_command(pip_args, description, ignore_errors=True)
if self.verify_python_import("dots_tts.runtime"):
self.log("dots_tts.runtime already satisfied - skipping", "SUCCESS")
return
self.run_pip_command(
["install", "git+https://github.com/rednote-hilab/dots.tts.git", "--no-deps"],
"Installing official dots.tts from GitHub (--no-deps)",
ignore_errors=True
)
def install_f5tts_multilingual_support(self):
"""Install phonemization support for F5-TTS multilingual models (Polish, German, French, Spanish, etc.)"""
self.log("Installing F5-TTS multilingual phonemization support", "INFO")
@@ -1567,6 +1594,7 @@ def main():
installer.install_onnxruntime_with_gpu_support() # Install ONNX with GPU acceleration if available
installer.install_vibevoice() # Install VibeVoice with careful dependency management
installer.install_echo_tts() # Install Echo-TTS with minimal dependency impact
installer.install_dots_tts() # Install official Dots TTS in the main environment first
installer.install_f5tts_multilingual_support() # Install phonemization for Polish/multilingual F5-TTS
installer.install_indexts_text_processing() # Install IndexTTS-2 text normalization with fallback
installer.install_russian_text_stresser_support() # Install lightweight Russian stress package for Official 23-Lang
+13 -1
View File
@@ -11,7 +11,7 @@ except ImportError:
pass
# Version and constants
VERSION = "5.0.0"
VERSION = "5.1.0"
IS_DEV = False # Set to False for release builds
VERSION_DISPLAY = f"v{VERSION}" + (" (dev)" if IS_DEV else "")
SEPARATOR = "=" * 70
@@ -161,6 +161,14 @@ except Exception as e:
print(f"❌ Echo-TTS Engine failed: {e}")
ECHO_TTS_ENGINE_AVAILABLE = False
try:
dots_tts_engine_module = load_node_module("dots_tts_engine_node", "engines/dots_tts_engine_node.py")
DotsTTSEngineNode = dots_tts_engine_module.DotsTTSEngineNode
DOTS_TTS_ENGINE_AVAILABLE = True
except Exception as e:
print(f"❌ Dots TTS Engine failed: {e}")
DOTS_TTS_ENGINE_AVAILABLE = False
try:
chatterbox_official_23lang_engine_module = load_node_module("chatterbox_official_23lang_engine_node", "engines/chatterbox_official_23lang_engine_node.py")
ChatterBoxOfficial23LangEngineNode = chatterbox_official_23lang_engine_module.ChatterBoxOfficial23LangEngineNode
@@ -597,6 +605,10 @@ if ECHO_TTS_ENGINE_AVAILABLE:
NODE_CLASS_MAPPINGS["EchoTTSEngineNode"] = EchoTTSEngineNode
NODE_DISPLAY_NAME_MAPPINGS["EchoTTSEngineNode"] = "⚙️ Echo-TTS Engine"
if DOTS_TTS_ENGINE_AVAILABLE:
NODE_CLASS_MAPPINGS["DotsTTSEngineNode"] = DotsTTSEngineNode
NODE_DISPLAY_NAME_MAPPINGS["DotsTTSEngineNode"] = "⚙️ Dots TTS Engine"
if QWEN3_TTS_VOICE_DESIGNER_AVAILABLE:
NODE_CLASS_MAPPINGS["Qwen3TTSVoiceDesignerNode"] = Qwen3TTSVoiceDesignerNode
NODE_DISPLAY_NAME_MAPPINGS["Qwen3TTSVoiceDesignerNode"] = "🎨 Qwen3-TTS Voice Designer"
+343
View File
@@ -0,0 +1,343 @@
"""
Dots TTS processor.
Handles chunking, character switching, pause tags, and orchestration for Dots TTS.
"""
import os
import re
import sys
from typing import Any, Dict, List, Tuple, Union
import torch
current_dir = os.path.dirname(__file__)
nodes_dir = os.path.dirname(current_dir)
project_root = os.path.dirname(nodes_dir)
if project_root not in sys.path:
sys.path.insert(0, project_root)
from utils.audio.chunk_timing import ChunkTimingHelper
from utils.audio.edit_post_processor import process_segments as apply_edit_post_processing
from engines.dots_tts.languages import format_dots_language_display, normalize_dots_language
from utils.text.character_parser import character_parser
from utils.text.pause_processor import PauseTagProcessor
from utils.text.segment_parameters import ParameterValidator, apply_segment_parameters
from utils.text.step_audio_editx_special_tags import get_edit_tags_for_segment
from utils.voice.discovery import get_available_characters, get_character_mapping, voice_discovery
class DotsTTSProcessor:
"""Internal processor for Dots TTS generation."""
SAMPLE_RATE = 48000
def __init__(self, adapter, engine_config: Dict[str, Any]):
self.adapter = adapter
self.config = engine_config.copy() if engine_config else {}
def update_config(self, new_config: Dict[str, Any]):
self.config = new_config.copy() if new_config else {}
self.adapter.update_config(self.config)
@staticmethod
def _voice_log_note(voice_ref: Dict[str, Any]) -> str:
if not isinstance(voice_ref, dict):
return " [⚠️ No voice reference - will use default]"
audio_path = voice_ref.get("audio_path") or voice_ref.get("audio")
if not audio_path:
return " [⚠️ No voice reference - will use default]"
reference_text = voice_ref.get("reference_text")
if reference_text:
return f" [ref text: {len(reference_text)} chars]"
return ""
def _log_generation_text(
self,
character_name: str,
text_content: str,
language: str,
voice_ref: Dict[str, Any],
chunk_count: int,
parameter_log: str = "",
show_text_content: bool = True,
) -> None:
voice_note = self._voice_log_note(voice_ref)
print(f"🎭 Dots TTS - Generating for '{character_name}' (Language: {language}){voice_note}:")
if parameter_log:
print(f"🎛️ Dots TTS params: {parameter_log}")
if show_text_content:
print("=" * 60)
print(text_content)
print("=" * 60)
if chunk_count > 1:
print(
f"📝 Chunking {character_name}'s text into {chunk_count} chunks "
f"(Language: {language}){voice_note}"
)
@staticmethod
def _format_parameter_log(filtered_params: Dict[str, Any], current_config: Dict[str, Any], current_seed: int) -> str:
if not filtered_params:
return ""
ordered_parts = []
if "seed" in filtered_params:
ordered_parts.append(f"seed={current_seed}")
for key in ("num_steps", "guidance_scale", "speaker_scale", "max_generate_length"):
if key in filtered_params:
ordered_parts.append(f"{key}={current_config.get(key)}")
for key in ("normalize_text", "language"):
if key in filtered_params:
value = current_config.get(key)
if key == "language":
value = format_dots_language_display(value)
ordered_parts.append(f"{key}={value}")
remaining_keys = [
key for key in filtered_params.keys()
if key not in {"seed", "num_steps", "guidance_scale", "speaker_scale", "max_generate_length", "normalize_text", "language"}
]
for key in remaining_keys:
ordered_parts.append(f"{key}={current_config.get(key, filtered_params[key])}")
return ", ".join(ordered_parts)
def _setup_character_parser(self, text: str):
configured_language = str(self.config.get("language", "auto") or "auto").strip()
normalized_language = normalize_dots_language(configured_language)
if normalized_language is None:
resolved_language = "en"
elif normalized_language == "auto_detect":
resolved_language = "en"
else:
resolved_language = normalized_language.lower()
character_parser.language_resolver.default_language = resolved_language
character_parser.default_language = resolved_language
character_tags = re.findall(r"\[([^\]]+)\]", text or "")
characters_from_tags = []
for tag in character_tags:
if not tag.startswith("pause:"):
characters_from_tags.append(tag.split("|")[0].strip())
available_chars = get_available_characters()
character_aliases = voice_discovery.get_character_aliases()
all_available = set()
if available_chars:
all_available.update(available_chars)
for alias, target in character_aliases.items():
all_available.add(alias.lower())
all_available.add(target.lower())
for char in characters_from_tags:
all_available.add(char.lower())
all_available.add("narrator")
character_parser.set_available_characters(list(all_available))
char_lang_defaults = voice_discovery.get_character_language_defaults()
for char, lang in char_lang_defaults.items():
character_parser.set_character_language_default(char, lang)
character_parser.reset_session_cache()
@staticmethod
def _should_apply_segment_language(seg: Any, base_config: Dict[str, Any]) -> bool:
segment_language = getattr(seg, "language", None)
if not segment_language:
return False
if getattr(seg, "explicit_language", False):
return True
global_language = normalize_dots_language(base_config.get("language", "auto"))
parser_default = normalize_dots_language(character_parser.default_language)
segment_language_code = normalize_dots_language(segment_language)
# Character parser always resolves untagged text to its fallback default
# language. For Dots we must not feed that fallback back into generation,
# or Auto/None collapse into English cache/runtime behavior.
if segment_language_code == parser_default:
return False
return segment_language_code != global_language
def process_text(
self,
text: str,
voice_mapping: Dict[str, Any],
seed: int,
enable_chunking: bool = True,
max_chars_per_chunk: int = 400,
chunk_combination_method: str = "auto",
silence_between_chunks_ms: int = 100,
enable_audio_cache: bool = True,
apply_edit_postprocessing: bool = True,
show_text_logging: bool = True,
) -> List[Dict[str, Any]]:
self._setup_character_parser(text)
narrator_info = voice_mapping.get("narrator", {})
narrator_voice = narrator_info if isinstance(narrator_info, dict) else {"audio": narrator_info}
base_config = self.config.copy()
segment_objects = character_parser.parse_text_segments(text)
if not segment_objects:
segment_objects = character_parser.parse_text_segments("narrator " + text)
characters = list({seg.character for seg in segment_objects if seg.character})
character_mapping = get_character_mapping(characters, engine_type="audio_only")
segment_records: List[Dict[str, Any]] = []
for seg in segment_objects:
segment_text = (seg.text or "").strip()
if not segment_text:
continue
segment_params = seg.parameters if seg.parameters else {}
current_config = base_config
current_seed = seed
filtered_params: Dict[str, Any] = {}
if segment_params:
filtered_params = ParameterValidator.filter_parameters_for_engine(segment_params, "dots_tts")
current_config = apply_segment_parameters(base_config, segment_params, "dots_tts")
if "seed" in current_config:
current_seed = int(current_config.get("seed", seed))
if self._should_apply_segment_language(seg, base_config):
current_config = current_config.copy()
current_config["language"] = seg.language
global_language = normalize_dots_language(base_config.get("language", "auto"))
segment_language_code = normalize_dots_language(seg.language)
if segment_language_code != global_language:
print(
" 🌍 Dots TTS language switched to: "
f"{format_dots_language_display(seg.language)}"
)
self.adapter.update_config(current_config)
segment_language = format_dots_language_display(current_config.get("language", "Auto"))
parameter_log = self._format_parameter_log(filtered_params, current_config, current_seed)
voice_ref = narrator_voice.copy()
char_name = seg.character or "narrator"
if char_name != "narrator" and char_name in voice_mapping:
char_info = voice_mapping[char_name]
if isinstance(char_info, dict):
voice_ref = char_info.copy()
elif char_name != "narrator":
audio_path, ref_text = character_mapping.get(char_name, (None, None))
if audio_path:
voice_ref = {
"audio_path": audio_path,
"reference_text": ref_text or "",
}
def _generate_chunks(text_content: str, edit_tags: list):
if enable_chunking:
from utils.text.chunking import ImprovedChatterBoxChunker
max_chars = ImprovedChatterBoxChunker.validate_chunking_params(max_chars_per_chunk)
chunks = ImprovedChatterBoxChunker.split_into_chunks(text_content, max_chars=max_chars)
else:
chunks = [text_content]
self._log_generation_text(
character_name=char_name,
text_content=text_content,
language=segment_language,
voice_ref=voice_ref,
chunk_count=len(chunks),
parameter_log=parameter_log,
show_text_content=show_text_logging,
)
for chunk_idx, chunk in enumerate(chunks):
audio = self.adapter.generate_single(
text=chunk,
voice_ref=voice_ref,
seed=current_seed + chunk_idx,
enable_audio_cache=enable_audio_cache,
character_name=char_name,
)
if audio.dim() == 1:
audio = audio.unsqueeze(0)
segment_records.append(
{
"waveform": audio.cpu(),
"sample_rate": self.SAMPLE_RATE,
"text": chunk,
"edit_tags": edit_tags if chunk_idx == 0 else [],
}
)
if PauseTagProcessor.has_pause_tags(segment_text):
raw_pause_segments, _ = PauseTagProcessor.parse_pause_tags(segment_text)
for frag_type, frag_content in raw_pause_segments:
if frag_type == "text":
frag_clean, frag_edit_tags = get_edit_tags_for_segment(frag_content)
frag_clean = frag_clean.strip()
if frag_clean:
_generate_chunks(frag_clean, frag_edit_tags)
elif frag_type == "pause":
silence = PauseTagProcessor.create_silence_segment(
frag_content,
self.SAMPLE_RATE,
torch.device("cpu"),
torch.float32,
)
if silence.dim() == 1:
silence = silence.unsqueeze(0)
segment_records.append(
{
"waveform": silence,
"sample_rate": self.SAMPLE_RATE,
"text": f"[pause:{frag_content}s]",
"edit_tags": [],
}
)
else:
clean_text, edit_tags = get_edit_tags_for_segment(segment_text)
_generate_chunks(clean_text, edit_tags)
if apply_edit_postprocessing and segment_records and any(seg["edit_tags"] for seg in segment_records):
segment_records = apply_edit_post_processing(segment_records, engine_config=base_config)
return segment_records
def combine_audio_segments(
self,
segments: List[Dict[str, Any]],
method: str = "auto",
silence_ms: int = 100,
original_text: str = "",
return_info: bool = False,
) -> Union[torch.Tensor, Tuple[torch.Tensor, Dict[str, Any]]]:
if not segments:
empty = torch.zeros(0, dtype=torch.float32)
if return_info:
return empty, {}
return empty
audio_segments = [seg["waveform"] for seg in segments]
text_chunks = [seg.get("text", "") for seg in segments]
text_length = len(" ".join(text_chunks))
combined_audio, chunk_info = ChunkTimingHelper.combine_audio_with_timing(
audio_segments=audio_segments,
combination_method=method,
silence_ms=silence_ms,
crossfade_duration=0.1,
sample_rate=self.SAMPLE_RATE,
text_length=text_length,
original_text=original_text,
text_chunks=text_chunks,
)
if return_info:
return combined_audio, chunk_info
return combined_audio
+418
View File
@@ -0,0 +1,418 @@
"""
Dots TTS SRT processor.
Handles full SRT subtitle processing for Dots TTS.
"""
import importlib.util
import os
import sys
from typing import Any, Dict, List, Optional, Tuple
import comfy.model_management as model_management
import torch
current_dir = os.path.dirname(__file__)
nodes_dir = os.path.dirname(current_dir)
project_root = os.path.dirname(nodes_dir)
if project_root not in sys.path:
sys.path.insert(0, project_root)
from engines.adapters.dots_tts_adapter import DotsTTSEngineAdapter
class DotsTTSSRTProcessor:
"""Complete SRT processor for Dots TTS."""
SAMPLE_RATE = 48000
def __init__(self, node_instance, config: Dict[str, Any]):
self.node_instance = node_instance
self.config = config.copy() if config else {}
self.adapter = DotsTTSEngineAdapter(self.config)
self._tts_processor = None
self.srt_available = False
self.SRTParser = None
self.SRTSubtitle = None
self.SRTParseError = None
self._load_srt_modules()
def _load_srt_modules(self):
try:
from utils.system.import_manager import import_manager
success, modules, msg = import_manager.import_srt_modules()
if not success:
print(f"⚠️ Dots TTS SRT: SRT module not available: {msg}")
return
self.SRTParser = modules.get("SRTParser")
self.SRTSubtitle = modules.get("SRTSubtitle")
self.SRTParseError = modules.get("SRTParseError")
if self.SRTParser is None:
print("⚠️ Dots TTS SRT: SRT parser not available")
return
self.srt_available = True
except Exception as e:
print(f"⚠️ Dots TTS SRT: Failed to load SRT modules: {e}")
@property
def processor(self):
if self._tts_processor is None:
processor_path = os.path.join(current_dir, "dots_tts_processor.py")
spec = importlib.util.spec_from_file_location("dots_tts_processor_module", processor_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
self._tts_processor = module.DotsTTSProcessor(self.adapter, self.config)
return self._tts_processor
def update_config(self, new_config: Dict[str, Any]):
self.config = new_config.copy() if new_config else {}
self.adapter.update_config(self.config)
if self._tts_processor is not None:
self._tts_processor.update_config(self.config)
def process_srt_content(
self,
srt_content: str,
voice_mapping: Dict[str, Any],
seed: int,
timing_mode: str,
timing_params: Dict[str, Any],
enable_audio_cache: bool = True,
) -> Tuple[Dict[str, Any], str, str, str]:
if not self.srt_available:
raise ImportError("SRT support not available - missing required SRT parser")
self._check_interrupt()
srt_parser = self.SRTParser()
subtitles = srt_parser.parse_srt_content(srt_content, allow_overlaps=True)
from utils.timing.overlap_detection import SRTOverlapHandler
has_overlaps = SRTOverlapHandler.detect_overlaps(subtitles)
current_timing_mode, mode_switched = SRTOverlapHandler.handle_smart_natural_fallback(
timing_mode, has_overlaps, "Dots TTS SRT"
)
print(f"🚀 Dots TTS SRT: Processing {len(subtitles)} subtitles in {current_timing_mode} mode")
audio_segments, adjustments = self._process_all_subtitles(
subtitles,
voice_mapping,
seed,
enable_audio_cache=enable_audio_cache,
)
self._check_interrupt()
final_audio, final_adjustments, stretch_method = self._assemble_final_audio(
audio_segments,
subtitles,
current_timing_mode,
timing_params,
adjustments,
)
if final_adjustments is not None:
adjustments = final_adjustments
timing_report = self._generate_timing_report(
subtitles,
adjustments,
current_timing_mode,
has_overlaps,
mode_switched,
timing_mode if mode_switched else None,
stretch_method,
)
adjusted_srt_string = self._generate_adjusted_srt_string(subtitles, adjustments, current_timing_mode)
total_duration = final_audio.shape[-1] / self.SAMPLE_RATE
mode_info = current_timing_mode
if mode_switched:
mode_info = f"{current_timing_mode} (switched from {timing_mode} due to overlaps)"
info = (
f"Generated {total_duration:.1f}s Dots TTS SRT-timed audio from {len(subtitles)} subtitles "
f"using {mode_info} mode"
)
if final_audio.dim() == 1:
final_audio = final_audio.unsqueeze(0).unsqueeze(0)
elif final_audio.dim() == 2:
final_audio = final_audio.unsqueeze(0)
audio_output = {"waveform": final_audio, "sample_rate": self.SAMPLE_RATE}
return audio_output, info, timing_report, adjusted_srt_string
def _check_interrupt(
self,
subtitle_index: Optional[int] = None,
total_subtitles: Optional[int] = None,
character: Optional[str] = None,
):
if model_management.interrupt_processing:
if subtitle_index is not None and total_subtitles is not None:
if character:
raise InterruptedError(
f"Dots TTS SRT generation interrupted at subtitle "
f"{subtitle_index + 1}/{total_subtitles}, character '{character}'"
)
raise InterruptedError(
f"Dots TTS SRT generation interrupted at subtitle {subtitle_index + 1}/{total_subtitles}"
)
raise InterruptedError("Dots TTS SRT generation interrupted by user")
def _process_all_subtitles(
self,
subtitles: List,
voice_mapping: Dict[str, Any],
seed: int,
enable_audio_cache: bool = True,
) -> Tuple[List[torch.Tensor], List[Dict]]:
audio_segments: List[torch.Tensor] = []
adjustments: List[Dict[str, Any]] = []
all_segments_for_editing: List[Dict[str, Any]] = []
subtitle_lengths = [max(1, len((sub.text or "").strip())) for sub in subtitles]
for i, sub in enumerate(subtitles):
self._check_interrupt(i, len(subtitles))
text = sub.text.strip()
if not text:
target_duration = sub.duration
silence = torch.zeros(1, int(target_duration * self.SAMPLE_RATE))
audio_segments.append(silence)
adjustments.append(
{
"index": i,
"segment_index": i,
"sequence": sub.sequence,
"natural_duration": target_duration,
"target_start": sub.start_time,
"target_end": sub.end_time,
"target_duration": target_duration,
"start_time": sub.start_time,
"end_time": sub.end_time,
"stretch_factor": 1.0,
"needs_stretching": False,
"stretch_type": "none",
"adjustment": 0.0,
"adjusted_start": sub.start_time,
"adjusted_end": sub.end_time,
"adjusted_duration": target_duration,
}
)
continue
print(f"📖 Dots TTS SRT Subtitle {i + 1}/{len(subtitles)}")
segment_records = self.processor.process_text(
text=text,
voice_mapping=voice_mapping,
seed=seed + i,
enable_chunking=False,
enable_audio_cache=enable_audio_cache,
apply_edit_postprocessing=False,
)
subtitle_has_edit_tags = any(seg.get("edit_tags") for seg in segment_records)
if subtitle_has_edit_tags:
for seg_idx, seg in enumerate(segment_records):
waveform = seg["waveform"]
if waveform.dim() == 1:
waveform = waveform.unsqueeze(0)
elif waveform.dim() == 3:
waveform = waveform.squeeze(0)
all_segments_for_editing.append(
{
**seg,
"waveform": waveform.cpu(),
"subtitle_index": i,
"segment_index": seg_idx,
}
)
audio_segments.append(None)
else:
if len(segment_records) > 1:
audio, _ = self.processor.combine_audio_segments(
segments=segment_records,
method="auto",
silence_ms=0,
original_text=text,
return_info=True,
)
elif segment_records:
audio = segment_records[0]["waveform"]
else:
audio = torch.zeros(1, 0)
if audio.dim() == 1:
audio = audio.unsqueeze(0)
elif audio.dim() == 3:
audio = audio.squeeze(0)
audio_segments.append(audio.cpu())
target_start = sub.start_time
target_end = sub.end_time
target_duration = target_end - target_start
adjustments.append(
{
"index": i,
"segment_index": i,
"sequence": sub.sequence,
"natural_duration": 0.0,
"target_start": target_start,
"target_end": target_end,
"target_duration": target_duration,
"start_time": target_start,
"end_time": target_end,
"stretch_factor": 1.0,
"needs_stretching": False,
"stretch_type": "none",
"adjustment": 0.0,
"adjusted_start": target_start,
"adjusted_end": target_end,
"adjusted_duration": 0.0,
}
)
if not subtitle_has_edit_tags:
audio = audio_segments[i]
natural_duration = audio.shape[-1] / self.SAMPLE_RATE
stretch_factor = target_duration / natural_duration if natural_duration > 0 else 1.0
adjustments[i].update(
{
"natural_duration": natural_duration,
"stretch_factor": stretch_factor,
"needs_stretching": abs(stretch_factor - 1.0) > 0.05,
"stretch_type": "compress" if stretch_factor < 1.0 else "expand" if stretch_factor > 1.0 else "none",
"adjustment": natural_duration - target_duration,
"adjusted_duration": natural_duration,
}
)
print(f"✅ Dots TTS Subtitle {i + 1}/{len(subtitles)}: {natural_duration:.2f}s (expected {target_duration:.2f}s)")
else:
print(f"✅ Dots TTS Subtitle {i + 1}/{len(subtitles)}: queued for batch edit processing")
if all_segments_for_editing:
from utils.audio.edit_post_processor import process_segments as apply_edit_post_processing
processed_segments = apply_edit_post_processing(all_segments_for_editing, engine_config=self.config)
grouped_segments: Dict[int, List[Dict[str, Any]]] = {}
for seg in processed_segments:
grouped_segments.setdefault(seg["subtitle_index"], []).append(seg)
for subtitle_index, segments in grouped_segments.items():
segments = sorted(segments, key=lambda item: item.get("segment_index", 0))
if len(segments) > 1:
combined_audio, _ = self.processor.combine_audio_segments(
segments=segments,
method="auto",
silence_ms=0,
original_text=subtitles[subtitle_index].text,
return_info=True,
)
else:
combined_audio = segments[0]["waveform"]
if combined_audio.dim() == 1:
combined_audio = combined_audio.unsqueeze(0)
elif combined_audio.dim() == 3:
combined_audio = combined_audio.squeeze(0)
combined_audio = combined_audio.cpu()
audio_segments[subtitle_index] = combined_audio
target_duration = adjustments[subtitle_index]["target_duration"]
natural_duration = combined_audio.shape[-1] / self.SAMPLE_RATE
stretch_factor = target_duration / natural_duration if natural_duration > 0 else 1.0
adjustments[subtitle_index].update(
{
"natural_duration": natural_duration,
"stretch_factor": stretch_factor,
"needs_stretching": abs(stretch_factor - 1.0) > 0.05,
"stretch_type": "compress" if stretch_factor < 1.0 else "expand" if stretch_factor > 1.0 else "none",
"adjustment": natural_duration - target_duration,
"adjusted_duration": natural_duration,
}
)
return audio_segments, adjustments
def _assemble_final_audio(self, audio_segments, subtitles, timing_mode, timing_params, adjustments):
if timing_mode == "stretch_to_fit":
from engines.chatterbox.audio_timing import TimedAudioAssembler
assembler = TimedAudioAssembler(self.SAMPLE_RATE)
target_timings = [(sub.start_time, sub.end_time) for sub in subtitles]
fade_duration = timing_params.get("fade_for_StretchToFit", 0.01)
final_audio, stretch_method_used = assembler.assemble_timed_audio(
audio_segments,
target_timings,
fade_duration=fade_duration,
)
return final_audio, None, stretch_method_used
if timing_mode == "pad_with_silence":
from utils.timing.assembly import AudioAssemblyEngine
assembler = AudioAssemblyEngine(self.SAMPLE_RATE)
final_audio = assembler.assemble_with_overlaps(audio_segments, subtitles, torch.device("cpu"))
return final_audio, None, None
if timing_mode == "concatenate":
from utils.timing.assembly import AudioAssemblyEngine
from utils.timing.engine import TimingEngine
timing_engine = TimingEngine(self.SAMPLE_RATE)
assembler = AudioAssemblyEngine(self.SAMPLE_RATE)
new_adjustments = timing_engine.calculate_concatenation_adjustments(audio_segments, subtitles)
fade_duration = timing_params.get("fade_for_StretchToFit", 0.01)
final_audio = assembler.assemble_concatenation(audio_segments, fade_duration)
return final_audio, new_adjustments, None
from utils.timing.assembly import AudioAssemblyEngine
from utils.timing.engine import TimingEngine
timing_engine = TimingEngine(self.SAMPLE_RATE)
assembler = AudioAssemblyEngine(self.SAMPLE_RATE)
smart_adjustments, processed_segments = timing_engine.calculate_smart_timing_adjustments(
audio_segments,
subtitles,
timing_params.get("timing_tolerance", 2.0),
timing_params.get("max_stretch_ratio", 1.0),
timing_params.get("min_stretch_ratio", 0.5),
torch.device("cpu"),
)
final_audio = assembler.assemble_smart_natural(
audio_segments,
processed_segments,
smart_adjustments,
subtitles,
torch.device("cpu"),
)
return final_audio, smart_adjustments, None
def _generate_timing_report(
self,
subtitles,
adjustments,
current_timing_mode,
has_overlaps=False,
mode_switched=False,
original_mode=None,
stretch_method=None,
) -> str:
from utils.timing.reporting import SRTReportGenerator
reporter = SRTReportGenerator()
return reporter.generate_timing_report(
subtitles,
adjustments,
current_timing_mode,
has_overlaps,
mode_switched,
original_mode,
stretch_method,
)
def _generate_adjusted_srt_string(self, subtitles, adjustments, timing_mode: str) -> str:
from utils.timing.reporting import SRTReportGenerator
reporter = SRTReportGenerator()
return reporter.generate_adjusted_srt_string(subtitles, adjustments, timing_mode)
+161
View File
@@ -0,0 +1,161 @@
"""
Dots TTS engine configuration node.
"""
import importlib.util
import os
import sys
current_dir = os.path.dirname(__file__)
nodes_dir = os.path.dirname(current_dir)
project_root = os.path.dirname(nodes_dir)
if project_root not in sys.path:
sys.path.insert(0, project_root)
base_node_path = os.path.join(nodes_dir, "base", "base_node.py")
base_spec = importlib.util.spec_from_file_location("base_node_module", base_node_path)
base_module = importlib.util.module_from_spec(base_spec)
sys.modules["base_node_module"] = base_module
base_spec.loader.exec_module(base_module)
BaseTTSNode = base_module.BaseTTSNode
from engines.dots_tts.languages import DOTS_LANGUAGE_OPTIONS
class DotsTTSEngineNode(BaseTTSNode):
"""Dots TTS engine configuration node."""
MODEL_VARIANTS = [
"dots.tts-soar",
"dots.tts-mf",
"dots.tts-base",
]
LANGUAGE_OPTIONS = DOTS_LANGUAGE_OPTIONS
TEMPLATE_MODE_OPTIONS = [
"TTS",
"Instruction TTS",
]
TEMPLATE_NAME_BY_MODE = {
"TTS": "tts",
"Instruction TTS": "instruction_tts",
}
@classmethod
def NAME(cls):
return "⚙️ Dots TTS Engine"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model_variant": (cls.MODEL_VARIANTS, {
"default": "dots.tts-soar",
"tooltip": "Official Dots checkpoint.\n• dots.tts-soar: best zero-shot voice cloning\n• dots.tts-mf: fastest distilled MeanFlow checkpoint; use 4 steps\n• dots.tts-base: pretrained baseline"
}),
"device": (["auto", "cuda", "cpu"], {
"default": "auto",
"tooltip": "Device to run Dots TTS on.\n• auto: use the best available device\n• cuda: NVIDIA GPU\n• cpu: CPU-only (very slow for 2B model)"
}),
"language": (cls.LANGUAGE_OPTIONS, {
"default": "Auto",
"tooltip": "Official Dots language tag.\n• Auto: let Dots detect language from text\n• None: disable explicit language tagging\n• Full language names: force the model-side language tag\n\nVoice-cloning note: reference/narrator audio works best when the generated language matches the language spoken in the reference. Cross-language cloning can degrade quality, accent, and speaker similarity."
}),
"num_steps": ("INT", {
"default": 10,
"min": 1,
"max": 64,
"step": 1,
"tooltip": "Flow-matching sampling steps.\nOfficial recommendation: 10-32 for base/soar, 4 for mf."
}),
"guidance_scale": ("FLOAT", {
"default": 1.2,
"min": 0.0,
"max": 5.0,
"step": 0.1,
"tooltip": "Official CFG guidance scale.\nHigher values can increase energy and instability."
}),
"speaker_scale": ("FLOAT", {
"default": 1.5,
"min": 0.0,
"max": 5.0,
"step": 0.1,
"tooltip": "Official speaker embedding scale for reference audio conditioning."
}),
"max_generate_length": ("INT", {
"default": 500,
"min": 32,
"max": 1024,
"step": 32,
"tooltip": "Official maximum audio patch budget.\n500 is roughly 160 seconds of output budget."
}),
},
"optional": {
"template_mode": (cls.TEMPLATE_MODE_OPTIONS, {
"default": "TTS",
"tooltip": "Official non-standard Dots template mode from upstream.\n• TTS: standard Dots speech synthesis template\n• Instruction TTS: uses the same text field as normal TTS\nUpstream does not clearly document what behavior difference this mode is meant to produce.\nIt may yield different results, or little noticeable difference, versus standard TTS. Needs testing."
}),
"precision": (["auto", "bfloat16", "float16", "float32"], {
"default": "auto",
"tooltip": "Runtime precision.\n• auto: bfloat16 on newer CUDA GPUs, else float16, cpu -> float32"
}),
"normalize_text": ("BOOLEAN", {
"default": False,
"tooltip": "Use Dots native text normalization before inference."
}),
"optimize": ("BOOLEAN", {
"default": False,
"tooltip": "Enable official Dots runtime optimization / warmup.\nFirst load is slower; steady-state inference is faster."
}),
},
}
RETURN_TYPES = ("TTS_ENGINE",)
RETURN_NAMES = ("TTS_engine",)
FUNCTION = "create_engine_config"
CATEGORY = "TTS Audio Suite/⚙️ Engines"
def create_engine_config(
self,
model_variant: str,
device: str,
language: str,
num_steps: int,
guidance_scale: float,
speaker_scale: float,
max_generate_length: int,
template_mode: str = "TTS",
precision: str = "auto",
normalize_text: bool = False,
optimize: bool = False,
) -> tuple:
template_name = self.TEMPLATE_NAME_BY_MODE.get(template_mode, "tts")
config = {
"engine_type": "dots_tts",
"model_variant": model_variant,
"device": device,
"precision": precision,
"language": language,
"num_steps": int(num_steps),
"guidance_scale": float(guidance_scale),
"speaker_scale": float(speaker_scale),
"max_generate_length": int(max_generate_length),
"normalize_text": bool(normalize_text),
"optimize": bool(optimize),
"template_name": template_name,
}
print(f"⚙️ Dots TTS: Configured on {device}")
print(f" Model: {model_variant} | Language: {language} | Mode: {template_mode} | Precision: {precision}")
print(
f" Settings: steps={num_steps}, guidance_scale={guidance_scale}, "
f"speaker_scale={speaker_scale}, max_generate_length={max_generate_length}"
)
if normalize_text or optimize:
print(f" Advanced: normalize_text={normalize_text}, optimize={optimize}")
engine_data = {
"engine_type": "dots_tts",
"config": config,
"capabilities": ["tts"],
}
return (engine_data,)
+206
View File
@@ -0,0 +1,206 @@
"""
Unified Sound Effects Node - engine-agnostic text-to-sound generation.
"""
import hashlib
import importlib.util
import os
import sys
import time
from typing import Any, Dict, Tuple
import torch
# Add project root directory to path for imports
current_dir = os.path.dirname(__file__)
nodes_dir = os.path.dirname(current_dir)
project_root = os.path.dirname(nodes_dir)
if project_root not in sys.path:
sys.path.insert(0, project_root)
# Load base_node module directly
base_node_path = os.path.join(nodes_dir, "base", "base_node.py")
base_spec = importlib.util.spec_from_file_location("base_node_module", base_node_path)
base_module = importlib.util.module_from_spec(base_spec)
sys.modules["base_node_module"] = base_module
base_spec.loader.exec_module(base_module)
BaseTTSNode = base_module.BaseTTSNode
from engines.adapters import DotsTTSEngineAdapter
class UnifiedSoundEffectsNode(BaseTTSNode):
"""Unified text-to-sound node for engines that support non-speech audio generation."""
ENGINE_ADAPTERS = {
"dots_tts": DotsTTSEngineAdapter,
}
TEMPLATE_OVERRIDES = {
"dots_tts": "text_to_audio",
}
@classmethod
def NAME(cls):
return "🎧 Sound Effects"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"engine": ("TTS_ENGINE", {
"tooltip": "Engine configuration for a backend that supports sound-effects generation. Currently Dots text_to_audio is supported."
}),
"description": ("STRING", {
"multiline": True,
"default": "Heavy rain on a metal rooftop with distant thunder and occasional wind gusts.",
"tooltip": "Describe the non-speech audio you want. This node is for sound-effects / scene audio generation, not spoken TTS."
}),
"seed": ("INT", {
"default": 1, "min": 0, "max": 2**32 - 1,
"tooltip": "Seed for reproducible generation. Set 0 for random output."
}),
},
"optional": {
"enable_audio_cache": ("BOOLEAN", {
"default": True,
"tooltip": "Cache generated sound effects in memory so identical reruns are instant."
}),
}
}
RETURN_TYPES = ("AUDIO", "STRING")
RETURN_NAMES = ("audio", "generation_info")
FUNCTION = "generate_sound"
CATEGORY = "TTS Audio Suite/🎧 Sound Effects"
def __init__(self):
super().__init__()
self._cached_adapters = {}
def _validate_engine(self, engine: Dict[str, Any]) -> Dict[str, Any]:
if not isinstance(engine, dict):
raise ValueError("Engine input must be a configuration dict")
capabilities = engine.get("capabilities", [])
if capabilities and "sound_effects" not in capabilities:
raise ValueError("Engine does not support Sound Effects")
config = engine.get("config", engine)
engine_type = engine.get("engine_type") or config.get("engine_type")
if engine_type not in self.ENGINE_ADAPTERS:
raise ValueError(
f"Engine '{engine_type}' is not wired for Sound Effects yet. "
"This family exists now, but only Dots text_to_audio is implemented."
)
return config.copy()
def _get_adapter(self, engine_type: str, config: Dict[str, Any]):
stable_params = {
"engine_type": engine_type,
"model_variant": config.get("model_variant"),
"device": config.get("device"),
"precision": config.get("precision"),
"optimize": config.get("optimize"),
"max_generate_length": config.get("max_generate_length"),
"template_name": config.get("template_name"),
}
cache_key = hashlib.md5(str(sorted(stable_params.items())).encode()).hexdigest()[:12]
adapter = self._cached_adapters.get(cache_key)
if adapter is None:
adapter_cls = self.ENGINE_ADAPTERS[engine_type]
adapter = adapter_cls(config=config)
self._cached_adapters[cache_key] = adapter
else:
adapter.update_config(config)
return adapter
def _generate_with_dots(
self,
config: Dict[str, Any],
description: str,
seed: int,
enable_audio_cache: bool,
):
config["template_name"] = self.TEMPLATE_OVERRIDES["dots_tts"]
adapter = self._get_adapter("dots_tts", config)
return adapter.generate_single(
text=description,
voice_ref=None,
seed=seed,
enable_audio_cache=enable_audio_cache,
character_name="sound_effects",
)
def generate_sound(
self,
engine: Dict[str, Any],
description: str,
seed: int,
enable_audio_cache: bool = True,
) -> Tuple[Dict[str, Any], str]:
config = self._validate_engine(engine)
engine_type = config.get("engine_type", engine.get("engine_type", "unknown"))
clean_description = (description or "").strip()
if not clean_description:
raise ValueError("Sound Effects description cannot be empty")
print(f"🎧 Sound Effects: Starting {engine_type} generation")
print("============================================================")
print(clean_description)
print("============================================================")
start_time = time.time()
if engine_type == "dots_tts":
audio_tensor = self._generate_with_dots(
config=config,
description=clean_description,
seed=int(seed or 0),
enable_audio_cache=bool(enable_audio_cache),
)
else:
raise ValueError(f"Unsupported Sound Effects engine: {engine_type}")
elapsed = time.time() - start_time
sample_rate = 48000 if engine_type == "dots_tts" else 44100
audio_duration = 0.0
if hasattr(audio_tensor, "shape") and audio_tensor.shape[-1] > 0:
audio_duration = float(audio_tensor.shape[-1]) / float(sample_rate)
print(
f"✅ Sound Effects: {engine_type} generation complete "
f"({audio_duration:.2f}s audio in {elapsed:.1f}s)"
)
info = (
f"Sound Effects generated successfully\n"
f"Engine: {engine_type}\n"
f"Mode: {self.TEMPLATE_OVERRIDES.get(engine_type, 'custom')}\n"
f"Duration: {audio_duration:.2f}s\n"
f"Elapsed: {elapsed:.2f}s\n"
f"Seed: {int(seed or 0)}"
)
if not isinstance(audio_tensor, torch.Tensor):
audio_tensor = torch.tensor(audio_tensor, dtype=torch.float32)
audio_tensor = audio_tensor.detach().float().cpu()
if audio_tensor.dim() == 1:
audio_tensor = audio_tensor.unsqueeze(0).unsqueeze(0)
elif audio_tensor.dim() == 2:
audio_tensor = audio_tensor.unsqueeze(0)
return ({
"waveform": audio_tensor,
"sample_rate": sample_rate,
}, info)
NODE_CLASS_MAPPINGS = {
"UnifiedSoundEffectsNode": UnifiedSoundEffectsNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"UnifiedSoundEffectsNode": "🎧 Sound Effects"
}
+82 -6
View File
@@ -202,6 +202,12 @@ Hello! This is unified SRT TTS with character switching.
stable_params['use_cuda_graphs'] = config.get('use_cuda_graphs', False)
stable_params['compile_mode'] = config.get('compile_mode', 'default')
if engine_type == "dots_tts":
stable_params['model_variant'] = config.get('model_variant', 'dots.tts-soar')
stable_params['precision'] = config.get('precision', 'auto')
stable_params['optimize'] = config.get('optimize', False)
stable_params['max_generate_length'] = config.get('max_generate_length', 500)
if engine_type == "moss_tts":
stable_params['model_variant'] = config.get('model_variant', 'MOSS-TTS-Local-Transformer')
stable_params['multi_speaker_mode'] = config.get('multi_speaker_mode', 'Custom Character Switching')
@@ -492,12 +498,57 @@ Hello! This is unified SRT TTS with character switching.
'timestamp': time.time()
}
return engine_instance
elif engine_type == "vibevoice":
# Import and create the VibeVoice SRT processor using absolute import
vibevoice_srt_processor_path = os.path.join(nodes_dir, "vibevoice", "vibevoice_srt_processor.py")
vibevoice_srt_spec = importlib.util.spec_from_file_location("vibevoice_srt_processor_module", vibevoice_srt_processor_path)
vibevoice_srt_module = importlib.util.module_from_spec(vibevoice_srt_spec)
elif engine_type == "dots_tts":
dots_tts_srt_processor_path = os.path.join(nodes_dir, "dots_tts", "dots_tts_srt_processor.py")
dots_tts_srt_spec = importlib.util.spec_from_file_location("dots_tts_srt_processor_module", dots_tts_srt_processor_path)
dots_tts_srt_module = importlib.util.module_from_spec(dots_tts_srt_spec)
dots_tts_srt_spec.loader.exec_module(dots_tts_srt_module)
DotsTTSSRTProcessor = dots_tts_srt_module.DotsTTSSRTProcessor
class DotsTTSSRTWrapper:
def __init__(self, config):
self.config = config
self.processor = DotsTTSSRTProcessor(self, config)
def update_config(self, new_config):
self.config = new_config.copy()
self.processor.update_config(new_config)
def process_with_error_handling(self, func):
try:
return func()
except Exception as e:
raise e
def format_audio_output(self, audio_tensor, sample_rate):
if audio_tensor.is_cuda:
audio_tensor = audio_tensor.cpu()
if audio_tensor.dim() == 1:
audio_tensor = audio_tensor.unsqueeze(0).unsqueeze(0)
elif audio_tensor.dim() == 2:
audio_tensor = audio_tensor.unsqueeze(0)
return {"waveform": audio_tensor, "sample_rate": sample_rate}
def check_interrupt(self):
if model_management.interrupt_processing:
raise InterruptedError("Dots TTS SRT processing interrupted by user")
engine_instance = DotsTTSSRTWrapper(config)
import time
self._cached_engine_instances[cache_key] = {
'instance': engine_instance,
'timestamp': time.time()
}
return engine_instance
elif engine_type == "vibevoice":
# Import and create the VibeVoice SRT processor using absolute import
vibevoice_srt_processor_path = os.path.join(nodes_dir, "vibevoice", "vibevoice_srt_processor.py")
vibevoice_srt_spec = importlib.util.spec_from_file_location("vibevoice_srt_processor_module", vibevoice_srt_processor_path)
vibevoice_srt_module = importlib.util.module_from_spec(vibevoice_srt_spec)
vibevoice_srt_spec.loader.exec_module(vibevoice_srt_module)
VibeVoiceSRTProcessor = vibevoice_srt_module.VibeVoiceSRTProcessor
@@ -1109,6 +1160,31 @@ Hello! This is unified SRT TTS with character switching.
enable_audio_cache=enable_audio_cache
)
elif engine_type == "dots_tts":
timing_params = {
'fade_for_StretchToFit': fade_for_StretchToFit,
'max_stretch_ratio': max_stretch_ratio,
'min_stretch_ratio': min_stretch_ratio,
'timing_tolerance': timing_tolerance
}
voice_mapping = {}
if audio_tensor is not None or audio_path:
voice_mapping['narrator'] = {
'audio': audio_tensor,
'audio_path': audio_path,
'reference_text': reference_text if reference_text else "",
}
result = engine_instance.processor.process_srt_content(
srt_content=srt_content,
voice_mapping=voice_mapping,
seed=seed,
timing_mode=timing_mode,
timing_params=timing_params,
enable_audio_cache=enable_audio_cache
)
elif engine_type == "vibevoice":
# Use the VibeVoice SRT processor from the wrapper instance
voice_mapping = {"narrator": audio_tensor} if audio_tensor else {}
+88
View File
@@ -204,6 +204,12 @@ Back to the main narrator voice for the conclusion.""",
stable_params['use_cuda_graphs'] = config.get('use_cuda_graphs', False)
stable_params['compile_mode'] = config.get('compile_mode', 'default')
if engine_type == "dots_tts":
stable_params['model_variant'] = config.get('model_variant', 'dots.tts-soar')
stable_params['precision'] = config.get('precision', 'auto')
stable_params['optimize'] = config.get('optimize', False)
stable_params['max_generate_length'] = config.get('max_generate_length', 500)
# For MOSS-TTS, include model identity and load-time options.
if engine_type == "moss_tts":
stable_params['model_variant'] = config.get('model_variant', 'MOSS-TTS-Local-Transformer')
@@ -515,6 +521,36 @@ Back to the main narrator voice for the conclusion.""",
return engine_instance
elif engine_type == "dots_tts":
from engines.adapters.dots_tts_adapter import DotsTTSEngineAdapter
dots_tts_processor_path = os.path.join(nodes_dir, "dots_tts", "dots_tts_processor.py")
dots_tts_processor_spec = importlib.util.spec_from_file_location("dots_tts_processor_module", dots_tts_processor_path)
dots_tts_processor_module = importlib.util.module_from_spec(dots_tts_processor_spec)
dots_tts_processor_spec.loader.exec_module(dots_tts_processor_module)
DotsTTSProcessor = dots_tts_processor_module.DotsTTSProcessor
class DotsTTSWrapper:
def __init__(self, cfg):
self.config = cfg.copy()
self.adapter = DotsTTSEngineAdapter(self.config)
self.processor = DotsTTSProcessor(self.adapter, self.config)
def update_config(self, new_config):
self.config = new_config.copy()
self.adapter.update_config(new_config)
self.processor.update_config(new_config)
engine_instance = DotsTTSWrapper(config)
import time
self._cached_engine_instances[cache_key] = {
'instance': engine_instance,
'timestamp': time.time()
}
return engine_instance
elif engine_type == "qwen3_tts":
# Create Qwen3-TTS processor instance
# Use global nodes_dir (already defined at module level)
@@ -853,6 +889,8 @@ Back to the main narrator voice for the conclusion.""",
lang_code = "local"
elif language.lower() == "auto":
lang_code = "auto"
elif language.lower() == "none":
lang_code = "none"
else:
# Standard language codes - take first 2 chars
lang_code = language.lower()[:2] # en, fr, de, etc.
@@ -1283,6 +1321,56 @@ Back to the main narrator voice for the conclusion.""",
formatted_audio = AudioProcessingUtils.format_for_comfyui(combined_audio, 44100)
result = (formatted_audio, generation_info)
elif engine_type == "dots_tts":
import re
from utils.audio.chunk_timing import ChunkTimingHelper
if not hasattr(engine_instance, "processor"):
dots_tts_processor_path = os.path.join(nodes_dir, "dots_tts", "dots_tts_processor.py")
dots_tts_processor_spec = importlib.util.spec_from_file_location("dots_tts_processor_module", dots_tts_processor_path)
dots_tts_processor_module = importlib.util.module_from_spec(dots_tts_processor_spec)
dots_tts_processor_spec.loader.exec_module(dots_tts_processor_module)
DotsTTSProcessor = dots_tts_processor_module.DotsTTSProcessor
engine_instance.processor = DotsTTSProcessor(engine_instance.adapter, config)
engine_instance.processor.update_config(config)
voice_mapping = {}
if audio_tensor is not None or audio_path:
voice_mapping['narrator'] = {
'audio': audio_tensor,
'audio_path': audio_path,
'reference_text': reference_text or '',
}
segment_records = engine_instance.processor.process_text(
text=text,
voice_mapping=voice_mapping,
seed=seed,
enable_chunking=enable_chunking,
max_chars_per_chunk=max_chars_per_chunk,
chunk_combination_method=chunk_combination_method,
silence_between_chunks_ms=silence_between_chunks_ms,
enable_audio_cache=enable_audio_cache
)
combined_audio, chunk_info = engine_instance.processor.combine_audio_segments(
segments=segment_records,
method=chunk_combination_method,
silence_ms=silence_between_chunks_ms,
original_text=text,
return_info=True
)
total_duration = combined_audio.shape[-1] / 48000.0 if combined_audio.numel() else 0.0
clean_text = re.sub(r'\[.*?\]', '', text)
text_length = len(clean_text)
base_info = f"Generated {total_duration:.1f}s audio from {text_length} characters (Dots TTS, narrator: {char_display})"
base_info += "\n🎭 Character switching and pause tags enabled"
generation_info = ChunkTimingHelper.enhance_generation_info(f"✅ {base_info}", chunk_info)
formatted_audio = AudioProcessingUtils.format_for_comfyui(combined_audio, 48000)
result = (formatted_audio, generation_info)
elif engine_type == "moss_tts":
import re
from utils.audio.chunk_timing import ChunkTimingHelper
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "tts_audio_suite"
description = "TTS Audio Suite - Universal multi-engine TTS extension for ComfyUI with unified architecture supporting IndexTTS-2, ChatterBox, Chatterbox Multilingual TTS (Official 23-Lang), F5-TTS, Higgs Audio 2, VibeVoice, and RVC engines. It has character voice management, SRT subtitle TTS support, and audio processing capabilities."
version = "5.0.0"
version = "5.1.0"
license = {file = "LICENSE"}
[project.urls]
+8 -5
View File
@@ -27,12 +27,15 @@ sounddevice>=0.4.0
jieba
pypinyin
unidecode
phonemizer # IPA phonemization for multilingual TTS (requires espeak system dependency)
omegaconf>=2.3.0
phonemizer # IPA phonemization for multilingual TTS (requires espeak system dependency)
omegaconf>=2.3.0
transformers>=5.3.0,<6.0.0 # Main environment for Higgs Audio v3 / Step Audio EditX / MOSS-TTS. Legacy Transformers 4 engines use isolated runtimes.
# ML utilities (safe)
accelerate
loguru
langcodes
lingua-language-detector
# ML utilities (safe)
accelerate
datasets
requests
dacite
+42 -3
View File
@@ -521,6 +521,42 @@ class EchoTTSCacheKeyGenerator(CacheKeyGenerator):
return hashlib.md5(cache_string.encode()).hexdigest()
class DotsTTSCacheKeyGenerator(CacheKeyGenerator):
"""Cache key generator for Dots TTS engine."""
def generate_cache_key(self, **params) -> str:
guidance_scale = params.get('guidance_scale', 1.2)
speaker_scale = params.get('speaker_scale', 1.5)
if isinstance(guidance_scale, (int, float)):
guidance_scale = round(float(guidance_scale), 3)
if isinstance(speaker_scale, (int, float)):
speaker_scale = round(float(speaker_scale), 3)
cache_data = {
'text': params.get('text', ''),
'audio_component': params.get('audio_component', ''),
'prompt_text': params.get('prompt_text', ''),
'model_variant': params.get('model_variant', 'dots.tts-soar'),
'language': params.get('language', 'auto_detect'),
'template_name': params.get('template_name', 'tts'),
'num_steps': params.get('num_steps', 10),
'guidance_scale': guidance_scale,
'speaker_scale': speaker_scale,
'normalize_text': params.get('normalize_text', False),
'max_generate_length': params.get('max_generate_length', 500),
'precision': params.get('precision', 'auto'),
'optimize': params.get('optimize', False),
'seed': params.get('seed', 0),
'device': params.get('device', 'auto'),
'character': params.get('character', 'narrator'),
'engine': 'dots_tts',
}
cache_string = str(sorted(cache_data.items()))
return hashlib.md5(cache_string.encode()).hexdigest()
class AudioCache:
"""Unified audio cache manager for all TTS engines."""
@@ -536,6 +572,7 @@ class AudioCache:
'index_tts': IndexTTSCacheKeyGenerator(),
'cosyvoice': CosyVoiceCacheKeyGenerator(),
'qwen3_tts': Qwen3TTSCacheKeyGenerator(),
'dots_tts': DotsTTSCacheKeyGenerator(),
'moss_tts': MossTTSCacheKeyGenerator(),
'echo_tts': EchoTTSCacheKeyGenerator()
}
@@ -608,10 +645,12 @@ class AudioCache:
num_samples = audio_tensor.numel()
# Use engine-specific sample rates
if engine_type in ('f5tts', 'step_audio_editx', 'qwen3_tts', 'moss_tts', 'higgs_audio_v3'):
if engine_type == 'dots_tts':
sample_rate = 48000
elif engine_type in ('f5tts', 'step_audio_editx', 'qwen3_tts', 'moss_tts', 'higgs_audio_v3'):
sample_rate = 24000
elif engine_type in ('index_tts', 'cosyvoice'):
sample_rate = 22050
elif engine_type in ('index_tts', 'cosyvoice'):
sample_rate = 22050
else:
sample_rate = 44100
return num_samples / sample_rate
@@ -131,7 +131,7 @@ class StepAudioEditXHandler(BaseEngineHandler):
return 0
class HiggsAudioHandler(BaseEngineHandler):
class HiggsAudioHandler(BaseEngineHandler):
"""Specialized handler for Higgs Audio engine with CUDA graphs"""
def model_unload(self, wrapper, memory_to_free=None, unpatch_weights=True) -> bool:
@@ -141,18 +141,65 @@ class HiggsAudioHandler(BaseEngineHandler):
"""
result = super().model_unload(wrapper, memory_to_free, unpatch_weights)
if result:
# Mark model as invalid to force fresh CUDA graph initialization
wrapper._is_valid_for_reuse = False
print(f"🔄 Marked Higgs Audio model for cache refresh (CUDA graphs will be recreated)")
return result
# Engine registry
if result:
# Mark model as invalid to force fresh CUDA graph initialization
wrapper._is_valid_for_reuse = False
print(f"🔄 Marked Higgs Audio model for cache refresh (CUDA graphs will be recreated)")
return result
class DotsTTSHandler(BaseEngineHandler):
"""Handler for Dots TTS, which needs explicit runtime teardown to free compile caches."""
def partially_unload(self, wrapper, device: str, memory_to_free: int) -> int:
if not wrapper._is_loaded_on_gpu:
print(f"⚠️ Skipping unload: model already marked as not on GPU")
return 0
model = wrapper._model_ref() if wrapper._model_ref else None
if model is None:
print(f"⚠️ Model reference is None, cannot unload")
return 0
try:
if hasattr(model, "unload_runtime"):
model.unload_runtime()
print(
f"🔄 Unloaded {wrapper.model_info.model_type} model "
f"({wrapper.model_info.engine}) runtime and freed "
f"{wrapper._memory_size // 1024 // 1024}MB"
)
elif hasattr(model, "to"):
model.to(device)
print(
f"🔄 Moved {wrapper.model_info.model_type} model "
f"({wrapper.model_info.engine}) to {device}, freed "
f"{wrapper._memory_size // 1024 // 1024}MB"
)
wrapper.current_device = device
wrapper._is_loaded_on_gpu = False
if torch.cuda.is_available():
torch.cuda.empty_cache()
if hasattr(torch.cuda, "ipc_collect"):
try:
torch.cuda.ipc_collect()
except Exception:
pass
gc.collect()
return wrapper._memory_size
except Exception as e:
print(f"⚠️ Error unloading {wrapper.model_info.engine} model: {e}")
return 0
# Engine registry
_ENGINE_HANDLERS = {
'chatterbox': BaseEngineHandler(),
'chatterbox_official_23lang': BaseEngineHandler(),
'dots_tts': DotsTTSHandler(),
'f5tts': BaseEngineHandler(),
'echo_tts': BaseEngineHandler(),
'higgs_audio': HiggsAudioHandler(),
+7
View File
@@ -142,6 +142,13 @@ ENGINE_REGISTRY: Dict[str, EngineCapabilities] = {
default_runtime_profile="vibevoice_transformers4_shared",
),
"dots_tts": EngineCapabilities(
supports_voice_conversion=False,
multilingual_model_switching=False,
can_corrupt_on_reload=False,
fallback_languages=["English"],
),
"qwen3_asr": EngineCapabilities(
supports_voice_conversion=False,
multilingual_model_switching=False,
+32 -3
View File
@@ -1672,9 +1672,37 @@ def register_qwen3_tts_factory():
except Exception as e:
raise RuntimeError(f"Failed to load Qwen3-TTS model: {e}")
unified_model_interface.register_model_factory("qwen3_tts", "tts", qwen3_tts_factory)
unified_model_interface.register_model_factory("qwen3_tts", "tts", qwen3_tts_factory)
def register_dots_tts_factory():
"""Register Dots TTS model factory."""
def dots_tts_factory(config: ModelLoadConfig):
"""Factory for official Dots TTS models with ComfyUI integration."""
model_name = config.model_name or "dots.tts-soar"
device = config.device or "auto"
additional_params = config.additional_params or {}
precision = additional_params.get("precision", "auto")
optimize = bool(additional_params.get("optimize", False))
max_generate_length = int(additional_params.get("max_generate_length", 500))
from engines.dots_tts.dots_tts_engine import DotsTTSEngine
print(f"🔄 Loading Dots TTS model via unified interface: {model_name}")
engine = DotsTTSEngine(
model_name=model_name,
device=device,
precision=precision,
optimize=optimize,
max_generate_length=max_generate_length,
)
engine._ensure_runtime_loaded()
print(f"✅ Dots TTS model '{model_name}' loaded successfully")
return engine
unified_model_interface.register_model_factory("dots_tts", "tts", dots_tts_factory)
def register_qwen3_asr_factory():
"""Register Qwen3-ASR model factory"""
def qwen3_asr_factory(config: ModelLoadConfig):
@@ -1883,6 +1911,7 @@ def initialize_all_factories():
register_cosyvoice_factory()
register_moss_tts_factory()
register_qwen3_tts_factory()
register_dots_tts_factory()
register_qwen3_asr_factory()
register_qwen3_aligner_factory()
register_granite_asr_factory()
+23 -1
View File
@@ -22,6 +22,10 @@ PARAMETER_ALIASES = {
'cfg_weight': 'cfg', # cfg_weight alias -> cfg (more universal)
'cfgweight': 'cfg',
'num_steps': 'num_steps',
'guidance_scale': 'guidance_scale',
'guidance': 'guidance_scale',
'speaker_scale': 'speaker_scale',
'speaker_guidance': 'speaker_scale',
'cfg_text': 'cfg_scale_text',
'cfg_scale_text': 'cfg_scale_text',
'cfg_speaker': 'cfg_scale_speaker',
@@ -55,6 +59,8 @@ PARAMETER_ALIASES = {
'tokens': 'duration_tokens',
'duration_tokens': 'duration_tokens',
'max_new_tokens': 'max_new_tokens',
'max_generate_length': 'max_generate_length',
'max_audio_patches': 'max_generate_length',
'n_vq': 'n_vq_for_inference',
'n_vq_for_inference': 'n_vq_for_inference',
'instruction': 'instruction',
@@ -72,6 +78,7 @@ PARAMETER_ENGINES = {
'seed': {
'chatterbox', 'chatterbox_official_23lang', 'f5tts', 'higgs_audio',
'higgs_audio_v3', 'vibevoice', 'index_tts', 'step_audio_editx', 'cosyvoice', 'qwen3_tts',
'dots_tts',
'echo_tts', 'moss_tts'
},
'temperature': {
@@ -82,7 +89,13 @@ PARAMETER_ENGINES = {
'f5tts', 'vibevoice', 'index_tts', 'chatterbox', 'chatterbox_official_23lang'
},
'num_steps': {
'echo_tts'
'echo_tts', 'dots_tts'
},
'guidance_scale': {
'dots_tts'
},
'speaker_scale': {
'dots_tts'
},
'cfg_scale_text': {
'echo_tts'
@@ -150,6 +163,9 @@ PARAMETER_ENGINES = {
'max_new_tokens': {
'higgs_audio_v3', 'moss_tts'
},
'max_generate_length': {
'dots_tts'
},
'n_vq_for_inference': {
'moss_tts'
},
@@ -179,6 +195,8 @@ PARAMETER_VALIDATION = {
'temperature': (float, 0.1, 2.0, "Randomness/creativity control (lower=more deterministic)"),
'cfg': (float, 0.0, 20.0, "Classifier-free guidance strength"),
'num_steps': (int, 1, 200, "Number of inference steps"),
'guidance_scale': (float, 0.0, 5.0, "Dots TTS classifier-free guidance scale"),
'speaker_scale': (float, 0.0, 5.0, "Dots TTS speaker conditioning scale"),
'cfg_scale_text': (float, 0.0, 20.0, "CFG scale for text guidance (Echo-TTS)"),
'cfg_scale_speaker': (float, 0.0, 20.0, "CFG scale for speaker guidance (Echo-TTS)"),
'cfg_min_t': (float, 0.0, 1.0, "CFG minimum t value (Echo-TTS)"),
@@ -201,6 +219,7 @@ PARAMETER_VALIDATION = {
'audio_repetition_penalty': (float, 0.5, 3.0, "MOSS-TTS audio repetition penalty"),
'duration_tokens': (int, 0, 8192, "MOSS-TTS duration hint in audio tokens"),
'max_new_tokens': (int, 64, 16384, "Maximum generated tokens"),
'max_generate_length': (int, 32, 1024, "Dots TTS maximum audio patch budget"),
'n_vq_for_inference': (int, 0, 32, "MOSS-TTS Local Transformer RVQ layers for inference"),
'instruction': (str, None, None, "MOSS-TTS whole-segment instruction"),
'quality': (str, None, None, "MOSS-TTS whole-segment quality hint"),
@@ -217,6 +236,8 @@ PARAMETER_NODE_KEYS = {
'temperature': 'temperature',
'cfg': {'default': 'cfg_weight', 'f5tts': 'cfg_strength'}, # Engine-specific mapping
'num_steps': 'num_steps',
'guidance_scale': 'guidance_scale',
'speaker_scale': 'speaker_scale',
'cfg_scale_text': 'cfg_scale_text',
'cfg_scale_speaker': 'cfg_scale_speaker',
'cfg_min_t': 'cfg_min_t',
@@ -239,6 +260,7 @@ PARAMETER_NODE_KEYS = {
'audio_repetition_penalty': 'audio_repetition_penalty',
'duration_tokens': 'duration_tokens',
'max_new_tokens': 'max_new_tokens',
'max_generate_length': 'max_generate_length',
'n_vq_for_inference': 'n_vq_for_inference',
'instruction': 'instruction',
'quality': 'quality',