Files
diodiogod-TTS-Audio-Suite/docs/Dev reports/fails_to_avoid_TTS_Engine_Implementation.md
T
diodiogod fa74973ad5 Fix Qwen3-TTS audio hash error with nested dict structure
Fixed 'dict' object has no attribute 'cpu' error when using Voice Designer
voices with TTS Text node.

## Root Cause

Voice Designer and Character Voices nodes output unified NARRATOR_VOICE format:
{
    "audio": {"waveform": tensor, "sample_rate": 24000},
    "audio_path": "...",
    "reference_text": "...",
    ...
}

But TTS Text node was wrapping this AGAIN when creating voice_mapping:
{
    "waveform": audio_tensor,  # audio_tensor was already a dict!
    "sample_rate": 24000,
    "text": "..."
}

This created triple-nested structure that broke audio hashing.

## Fixes Applied

**TTS Text Node** (nodes/unified/tts_text_node.py):
- Extract actual waveform tensor from audio dict before creating voice_mapping
- Applied to all 6 places where audio_tensor is used:
  - Narrator voice mapping (line 1151)
  - Character ICL fallback (line 1183)
  - Character x_vector fallback (line 1217)
  - Character no-text fallback (line 1220)
  - Character no-audio fallback (line 1237)
- Pattern: Check if audio_tensor is dict, extract waveform/sample_rate if so

**Qwen3-TTS Adapter** (engines/adapters/qwen3_tts_adapter.py):
- Extract inner audio dict before passing to hash function
- Changed from passing full voice_ref to passing voice_ref['audio']

**Audio Hash Utility** (utils/audio/audio_hash.py):
- Added nested structure handling with fallback
- Detects Character Voices format {"audio": {...}} vs direct format
- Enhanced debug logging to diagnose structure issues

## Testing

- Voice Designer → TTS Text: Works without hash error
- Character Voices → TTS Text: Works without hash error
- Audio hashing generates stable keys for caching
- Both VoiceDesign and traditional voices function correctly
2026-01-28 00:15:21 -03:00

9.5 KiB

TTS Engine Implementation - Fails & Lessons Learned

Step Audio EditX Implementation

Import Errors

  • Missing import: Always add import folder_paths when using folder_paths.get_temp_directory() in node files
  • Bundled code imports: For complex bundled packages with internal cross-imports, add sys.path.insert(0, impl_dir) at top of main files instead of converting all imports

Audio Utility Functions

  • Temp file creation: Use AudioProcessingUtils.save_audio_to_temp_file() not save_audio() (doesn't exist)
  • ComfyUI audio format: Extract waveform with audio_tensor['waveform'] and audio_tensor.get('sample_rate') before passing to utilities

Download Configuration

  • HuggingFace file list: Always verify actual repo structure with curl before hardcoding file lists
  • Downloader API: Use dict format [{"remote": f, "local": f}] for unified_downloader, not plain strings
  • File existence: Check actual HuggingFace repo structure - don't assume file naming patterns (e.g., model-00001.safetensors not model-00001-of-00002.safetensors)
  • CRITICAL - No cache downloads: NEVER allow models to auto-download to cache directories. ALL models/weights must be downloaded via our downloader to organized models/TTS/ folders. Disable auto-download and add to downloader instead.

Factory Registration

  • Unified model interface: Always implement register_<engine>_factory() when using unified_model_interface.load_model()
  • Factory initialization: Add factory to initialize_all_factories() or it won't be registered

Node Registration

  • Engine branch: Add engine-specific branch in _create_engine_node_instance() for each new engine
  • Variable naming: Use consistent variable names (config not engine_config) throughout node code

Voice References

  • Reference text requirement: F5-TTS and Step Audio EditX REQUIRE prompt_text (transcript), ChatterBox/VibeVoice/Higgs don't
  • Narrator voice mapping: Map narrator from TTS Text node input by saving audio_tensor to temp file with reference_text
  • Voice discovery: Use get_character_mapping() not discover_voices_for_engine() (latter doesn't exist)

Pause Tag Format

  • Correct format: Use [pause:2], [pause:1.5s], [pause:500ms] - NOT <pause_X>
  • Parser usage: Use PauseTagProcessor.parse_pause_tags() which returns ('text', content) or ('pause', duration_seconds) tuples

Seed Control

  • Global torch state: Some engines (Step Audio EditX) use global torch.manual_seed() for reproducibility, not function parameters
  • CUDA seeds: Always set both torch.manual_seed() and torch.cuda.manual_seed_all() for GPU reproducibility

Quantization Support

  • Device movement: Bitsandbytes quantized models (int4/int8) CANNOT be moved with .to() - wrap in try-except
  • VRAM clearing: Quantized models stay on GPU; "Clear VRAM" must unload completely (not move to CPU)
  • Error handling: Catch ValueError with "is not supported for" and "8-bit"/"4-bit" in message

Character Switching Implementation

  • Missing voice fallback: When get_character_mapping() returns (None, None) for a character, MUST still add entry to voice_mapping with fallback to narrator/default voice
  • Voice mapping consistency: All characters in parsed segments MUST have entries in voice_mapping, even if empty/None
  • Adapter validation: Adapters that require voice references (Step Audio EditX, F5-TTS) need graceful fallback when voice_ref is None
  • Character parser interaction: Character parser changes unknown characters to "narrator" - need to ensure original character names are preserved for voice mapping lookup
  • Parser fix: Add text tag characters to available_characters (lowercase) + set language defaults like IndexTTS
  • Working pattern: all_available = set(get_available_characters()) + aliases + text tag chars (lowercase) + "narrator" → set_available_characters(list(all_available)) + set_character_language_default() for each char

Caching Implementation

  • CRITICAL - Planned but skipped: Caching listed in Phase 1 but often omitted during processor/adapter implementation. Verify with testing (run generation twice on same text - should show cache hit on second)
  • Required in adapters: Generate stable audio hash, create cache key with all params, check cache BEFORE generation, store after with duration calc
  • Cache key pattern: audio_cache.generate_cache_key(engine_type, text=..., audio_component=..., **all_params)
  • Duration calculation: Update _calculate_duration() with engine sample rate (e.g., 24000 for Step Audio EditX, F5-TTS)

Model Lifecycle - del Destructor

  • CRITICAL: Remove __del__ from engine classes - causes automatic unload after generation ends (when object goes out of scope)
  • 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

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):
    1. combine_audio_segments() method accepts return_info=True parameter
    2. Uses ChunkCombiner.combine_chunks() with return_info=True to get (audio, chunk_info) tuple
    3. In unified node: call ChunkTimingHelper.enhance_generation_info(base_info, chunk_info) for detailed timing
  • Wrong: Inline concatenation (torch.cat) without timing tracking, manual report formatting
  • Example: IndexTTS/StepAudio initially did inline concat - needed refactor to use ChunkCombiner
  • Required info: Duration, text length, segments/chunks count, chunk timing breakdown (start/end/text per chunk)

Segment Parameter Registration

  • Forgot to add: New engines to PARAMETER_ENGINES in utils/text/segment_parameters.py for [seed:X] tag support

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

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}
  • Also: Check AudioProcessingUtils.save_audio_to_temp_file(audio, sample_rate) signature - it returns path, doesn't take path as arg

ComfyUI Audio Tensor Shape

  • CRITICAL: ComfyUI expects 3D [batch, channels, samples] NOT 2D [channels, samples]
  • Error symptom: IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1) in save_audio()
  • Correct shape conversion:
    • 1D [samples] → 3D [1, 1, samples] (two unsqueeze)
    • 2D [channels, samples] → 3D [1, channels, samples] (one unsqueeze)
  • Wrong: Squeezing 3D to 2D, or only adding one dimension to 1D
  • Pattern: tensor.unsqueeze(0) adds batch dim to 2D; check VibeVoice processor for reference

Step Audio EditX Sample Rate

  • Symptom: Pitch shift in edit/clone modes with external audio (44100 Hz, 48000 Hz)
  • Cause: Saving audio with wrong sample rate metadata → torchaudio.load() misinterprets data
  • Fix: Resample to 24000 Hz before saving temp files (edit node, TTS text node, TTS SRT node)
  • Prevention: Always resample to engine's native rate (24000 Hz for Step Audio EditX)

Unified Model Interface - Adapter & Node Consistency

  • CRITICAL: ALL engine loading must use unified_model_interface.load_model() for smart caching
  • Symptom: Model loads twice (once for TTS, once for editing/other nodes) - wasting VRAM and time
  • Wrong pattern: Creating engine instances directly (engine = StepAudioEditXEngine()) in adapters or nodes
  • Correct pattern:
    1. Adapter load_base_model() uses unified_model_interface.load_model(config)
    2. Audio Editor node uses unified_model_interface.load_model(config)
    3. Both use same model_type (e.g., "tts") so cache keys match → engine reused
  • Cache key components: engine_name, model_type, model_name, device, quantization, etc.
  • Fix Step Audio EditX: Updated adapter and Audio Editor node to use unified interface with model_type="tts"
  • Prevention: When adding new engines, ALWAYS use unified interface in ALL code paths (adapters, nodes, processors)

Engine Wrapper Implementation - Always Compare First

  • CRITICAL: Before writing engine wrapper, open existing reference engine (Step Audio EditX, F5-TTS) side-by-side and compare patterns
  • Common mistakes when NOT comparing:
    • Missing required imports (folder_paths, resolve_torch_device, unified_model_interface, extra_paths)
    • Manual device/dtype resolution instead of using utils functions
    • Missing _find_model_directory() with extra_model_paths support
    • Using load_model() method instead of _ensure_model_loaded() pattern
    • Direct model instantiation instead of unified_model_interface.load_model()
    • Missing _model_config tracking (needed for unload)
    • Not using tts_model_manager.ensure_device() before fallback to .to()
    • Missing GPU capability detection for dtype auto-selection
  • Prevention: ALWAYS open Step Audio EditX engine file as reference when creating new engine wrapper, copy the patterns