Compare commits

...
729 Commits
Author SHA1 Message Date
diodiogod 6270346ba5 Fix v4.15.0 changelog formatting and categorization 2025-12-12 21:08:54 -03:00
diodiogod 3690fa6829 Version 4.15.0
Add Step Audio EditX engine and universal inline edit tags

Technical implementation:
- Complete Step Audio EditX TTS and Audio Editor nodes
- Universal inline edit tag post-processing system
- Enhanced Multiline TTS Tag Editor with tabbed interface
- Fix segment grouping and chunk combination parameter bugs
2025-12-12 21:06:07 -03:00
diodiogod 3733705ab0 Update Voice Cleaning workflow with Step Audio EditX Audio Editor denoise
Added Step Audio EditX Audio Editor denoise capability to the Voice Cleaning workflow for additional audio restoration options.
2025-12-12 16:08:32 -03:00
diodiogod 1e2e946371 Add Step Audio EditX workflow documentation to README
Added two new workflows to Example Workflows section:
- Step Audio EditX Integration.json - TTS engine with zero-shot voice cloning
- Step Audio EditX - Audio Editor + Inline Edit Tags.json - Audio editing with inline edit tags
2025-12-12 15:45:45 -03:00
diodiogod acf3a4609d Fix Step Audio EditX chunk_combination_method parameter not being used
The chunk_combination_method parameter was hardcoded to 'auto' instead of using the user's selection from the UI. Now respects crossfade, silence_padding, concatenate, or auto choices.
2025-12-12 13:04:53 -03:00
diodiogod edf01a15c9 Remove incorrect segment grouping from Step Audio EditX
Step Audio EditX was incorrectly grouping consecutive same-character segments, copied from VibeVoice. Unlike VibeVoice which supports 90+ minute generation, Step Audio EditX has an 8192 token limit and should process segments individually like ChatterBox, F5-TTS, Higgs Audio, and IndexTTS-2.

Changes:
- Remove _group_consecutive_character_objects() method
- Process each character segment individually
- Respect newline-based segmentation from character parser
- Maintain character switching, parameter switching, inline edit tags, pause tags, and chunking

This better respects user intent when they use newlines for pacing/separation.
2025-12-12 13:02:19 -03:00
diodiogod 803a8878cf Fix paralinguistic tag spacing and position offset corruption
Fixed two issues with paralinguistic tag insertion in edit post-processing:

1. Spacing: Changed logic to add space before/after tags when adjacent character
   isn't already a space (previously only checked alphanumeric, causing tags to
   stick to punctuation like "Hello!<Breathing>")

2. Position offset: Removed incorrect position offset tracking when inserting
   multiple tags. Since tags are sorted descending, each insertion happens from
   right to left and doesn't affect earlier positions.

This fixes text corruption when multiple paralinguistic tags are present
(e.g., "<Br <Breathing> eathing>") and ensures proper spacing around tags.
2025-12-12 12:10:12 -03:00
diodiogod 80e321e86f Fix copy/paste in Multiline TTS Tag Editor for ComfyUI v0.3.75+
Add explicit copy event handler with stopPropagation to prevent ComfyUI v0.3.75+ from intercepting clipboard operations. Newer ComfyUI versions intercept clipboard events which was blocking copy operations in contentEditable elements.
2025-12-12 11:55:15 -03:00
diodiogod 887050726d Fix paste behavior in Multiline TTS Tag Editor
Use bubble phase stopPropagation (not capture phase) to allow paste to complete in contentEditable div before preventing event from reaching ComfyUI canvas handlers.
2025-12-12 11:22:36 -03:00
diodiogod 4c1538b287 Fix Step Audio EditX duplicate model loading in edit post-processing
Fixed issue where EditPostProcessor was loading a second Step Audio EditX model (7GB+)
in VRAM during edit tag processing instead of reusing the pre-loaded engine from TTS generation.

Changes:
- Added pre_loaded_engine parameter passing through EditPostProcessor to Audio Editor
- Modified Audio Editor to wrap raw StepAudioTTS in StepAudioEditXEngine for edit_single() method
- Fixed paralinguistic code path to pass tts_engine_data instead of None
- Added debug logging to track pre-loaded engine usage

This eliminates unnecessary VRAM usage by ensuring only ONE Step Audio EditX model
is loaded during the complete TTS+editing workflow.
2025-12-12 11:12:26 -03:00
diodiogod 51408b97ad Fix Step Audio EditX duplicate model loading in edit post-processing
Eliminated duplicate model loading when EditPostProcessor applies inline edit tags:

1. **Pass pre-loaded engine directly**: EditPostProcessor now passes the TTS
   processor's pre-loaded engine to the Audio Editor node via tts_engine_data dict

2. **Audio Editor engine reuse**: Added pre_loaded_engine parameter to
   _get_or_create_engine() which returns pre-loaded engine directly if provided

3. **Fix ModelLoadConfig kwargs**: Fixed Audio Editor to pass torch_dtype and
   quantization in additional_params dict instead of as direct kwargs (was
   causing "unexpected keyword argument" error)

4. **Update EditPostProcessor**: Modified process_segments() and
   _apply_edit_via_node() to properly pass pre_loaded_engine through the chain

This prevents loading a second 7GB+ Step Audio EditX model in VRAM during
edit post-processing. The Audio Editor now reuses the same engine instance
that was already loaded for TTS generation.

Fixes duplicate VRAM usage and "Loading Step Audio EditX engine" messages
during edit tag processing in SRT and regular TTS workflows.
2025-12-12 10:17:23 -03:00
diodiogod cbf02a44d8 Fix Multiline TTS Tag Editor paste event propagation
Prevent paste events from bubbling to ComfyUI canvas, which was causing previously copied nodes to be pasted when pasting text into the editor.
2025-12-12 09:45:33 -03:00
diodiogod 5613ec613e Fix Step Audio EditX Audio Editor cached engine device checking
Audio Editor node now checks and moves cached engines back to GPU when needed:

- Added device checking when reusing cached Step Audio EditX engines
- Moves LLM and CosyVoice.cosy_impl from CPU to GPU if needed
- Updates CosyVoice.device tracker after move
- Prevents running on CPU after previous unload operations

Fixes slow generation (1.2 it/s CPU speed instead of 10-20+ it/s GPU speed)
when reusing cached engines that were previously moved to CPU.
2025-12-12 09:29:26 -03:00
diodiogod 4a9b74fe7c Fix QwenEmotion download by prioritizing HF CLI over HTTP
- Changed unified_downloader to try HuggingFace CLI first, then fall back to HTTP
- Suppressed error messages from HF CLI failures to avoid confusing users
- Fixes download issues on systems where HTTP requests library has DNS resolution problems
- Maintains fallback to direct HTTP download for compatibility
2025-12-10 11:15:36 -03:00
diodiogod 33f39216c4 Update README with Step Audio EditX documentation
Add comprehensive documentation for Step Audio EditX LLM-based audio editing engine:

Summary & Features:
- Add Audio Editing capabilities to project summary
- Add Audio Post-Processing feature with Step Audio EditX details
- Link to Inline Edit Tags User Guide

What's New Section (v4.15):
- Add Step Audio EditX - LLM Audio Editing section
- Document paralinguistic effects (10 types)
- Document emotion controls (14 emotions)
- Document speaking styles (32 styles)
- Document speed control and voice restoration
- Document inline edit tags with pipe-separator support
- Include usage examples and important limitations
- Add language support warnings (Mandarin, English, Sichuanese, Cantonese, Japanese, Korean only)

Model Downloads Section:
- Add Step Audio EditX Models section (11)
- Document file structure in ComfyUI/models/TTS/step_audio_editx/
- Add model download table with HuggingFace links:
  * stepfun-ai/Step-Audio-EditX (3B LLM, ~7GB)
  * stepfun-ai/Step-Audio-Tokenizer (included)
- Document CosyVoice vocoder integration
- Add key features and usage instructions
2025-12-09 22:07:27 -03:00
diodiogod d9d9164382 Add tabbed Inline Edit interface to Multiline TTS Tag Editor
Implements comprehensive inline edit tag UI for Step Audio EditX post-processing with trapezoid tab design and full persistence.

New Features:
- Tabbed interface: "Character/Parameters" and "Inline Edit" tabs
- Trapezoid tab design with cyan active indicator
- Tab text never wraps (nowrap + ellipsis overflow)
- Inline edit tag sections for all tag types:
  * Paralinguistic (Laughter, Breathing, Sigh, etc.) - 10 types
  * Emotion (happy, sad, angry, etc.) - 14 emotions
  * Style (whisper, serious, child, etc.) - 32 styles
  * Speed (faster, slower, more_faster, more_slower)
  * Voice Restoration (with VC passes and reference iteration)

Intelligent Tag Insertion:
- Pipe-separator support for combining multiple tags (e.g., <Laughter:2|emotion:happy>)
- Smart tag detection: cursor inside/after existing tag adds with pipe separator
- Tag type replacement: adding same type replaces existing (e.g., emotion:sad replaces emotion:happy)
- Iteration control via sliders (1-5 iterations)
- Toast notifications for insertion feedback

Full Persistence:
- All dropdown selections saved to localStorage
- All iteration slider positions saved to localStorage
- Values restored on ComfyUI restart/browser reload
- Fixed parameter section to restore saved values (was only saving, not restoring)

Syntax Highlighting:
- Inline edit tags highlighted in magenta (#ff66ff)
- Distinguishes from character tags (cyan)

Files Added:
- web/widget-inline-edit-section.js - Inline edit UI builder
- web/widget-tabs.js - Tabbed interface system

Files Modified:
- web/string_multiline_tag_editor.js - Integrated tabs and inline edit section
- web/widget-event-handlers.js - Tag insertion logic with pipe-separator support
- web/widget-parameter-section.js - Fixed parameter value restoration
2025-12-09 15:38:42 -03:00
diodiogod 6b89fdc629 Comment out debug quantization check messages 2025-12-09 10:14:37 -03:00
diodiogod 9907e8964c Add ComfyUI settings menu for inline edit tag precision/device control
Features:
- New settings menu under "TTS Audio Suite > Step Audio EditX (Inline Tags)"
- Precision options: auto, fp32, fp16, bf16, int8, int4
- Device options: auto, cuda, cpu, xpu
- Settings persist across ComfyUI restarts
- API endpoint for JavaScript-Python communication
- Automatic dead engine detection and reload

Implementation:
- web/tts_audio_suite_settings.js: Frontend settings UI
- __init__.py: API endpoint /api/tts-audio-suite/settings
- edit_post_processor.py: Global settings storage and retrieval
- step_audio_editx_audio_editor_node.py: Settings detection and engine reload logic

Notes:
- Settings require browser refresh (F5) to take effect (documented in tooltips)
- Only affects inline edit tags, not Step Audio EditX node direct usage
- Fixes dead engine detection after memory management unloads quantized models
2025-12-09 09:56:57 -03:00
diodiogod c8f7a4759f Fix Step Audio EditX non-quantized model CPU offloading
Fixed CPU offloading for non-quantized (bfloat16/float16/float32) models:

- StepAudioTTS components (llm, cosy_model) don't have .to() method directly
- Handler now moves model.llm and model.cosy_model.cosy_impl individually
- Updates CosyVoice.device tracker when moving cosy_impl to CPU
- Properly handles both raw StepAudioTTS and wrapped models

Fixes unload behavior for non-quantized models to actually free VRAM
instead of silently failing with "no memory freed (model may already be on CPU)"
2025-12-09 01:14:11 -03:00
diodiogod 3887dffb54 Fix Step Audio EditX int8/int4 quantization unload and reload
Multiple critical fixes for bitsandbytes quantized models:

1. **Factory return type**: unified_model_interface now returns raw StepAudioTTS
   instead of wrapped StepAudioEditXEngine to fix tuple unpacking in adapter

2. **Quantization detection**: StepAudioEditXHandler now properly detects
   bitsandbytes int8/int4 models by checking model.llm.modules() for
   Linear8bitLt and Linear4bit classes

3. **Model deletion**: Handler now properly deletes llm, cosy_model, and
   audio_tokenizer components for quantized models that can't use .to()

4. **Cache key differentiation**: Added quantization and torch_dtype to
   tts_text_node cache key so int8/int4 changes create new instances

5. **Dead wrapper detection**: unified_model_interface now checks if cached
   wrapper's model was deleted and forces reload instead of returning dead wrapper

6. **Auto-unload prevention**: Disabled adapter cleanup() auto-unload to keep
   models loaded between generations

7. **Path handling**: Downloader now handles cached filesystem paths without
   KeyError by checking for absolute paths

Fixes:
- Models no longer auto-delete after second generation
- Quantization changes properly trigger model reload
- Manual unload button correctly frees VRAM (2.3GB int4, 3.8GB int8)
- Reload after unload works without "NoneType has no attribute 'frontend'" error
- "local:" prefix resolution now works via downloader pattern
2025-12-09 00:18:21 -03:00
diodiogod b59e6011dd Don't mark int8/int4 models as invalid for reuse
- Removed _is_valid_for_reuse = False
- This was causing auto-unload after every generation
- Int8/int4 models now stay loaded between generations
- Only unload when user explicitly clicks unload button
- Proper behavior: load once, use multiple times, unload when done
2025-12-08 23:26:01 -03:00
diodiogod d1818b2ddc Measure actual VRAM freed for int8/int4 models
- Use torch.cuda.memory_allocated() before and after unload
- Shows real VRAM freed instead of estimated model size
- Print VRAM before → after stats for transparency
- Matches pattern used in model_manager.py
- Users can now see actual VRAM being released
2025-12-08 23:23:11 -03:00
diodiogod 5b660e6e9c Add aggressive cleanup for bitsandbytes int8/int4 unload
- Delete inner _tts_engine and _tokenizer before deleting wrapper
- Mark quantized models as invalid for reuse (can't move back to GPU)
- Run garbage collection 3 times for thorough cleanup
- Reset CUDA peak memory stats
- Double empty_cache calls for aggressive VRAM release
- This should properly free VRAM when unloading int8/int4 models
2025-12-08 23:22:01 -03:00
diodiogod 2812e5c527 Fix quantization detection for Step Audio EditX wrapper
- Check wrapper.quantization attribute for quantization flag
- Check inner _tts_engine.modules() instead of wrapper.modules()
- Fixes 'object has no attribute modules' error
- Properly detects int8/int4 quantization now
2025-12-08 23:15:20 -03:00
diodiogod f6f150f280 Fix Step Audio EditX int8/int4 unload - use handler package system
- Created StepAudioEditXHandler in engine_handlers package
- Detects bitsandbytes quantized models (int8/int4)
- Properly deletes quantized models instead of using .to()
- Forces garbage collection and CUDA cache cleanup
- Registers handler in engine_handlers/__init__.py
- Now properly frees all VRAM when unloading quantized models
2025-12-08 23:11:58 -03:00
diodiogod 7f4f1f10b4 Add Step Audio EditX handler for proper int8 model unloading
- Bitsandbytes int8 models can't use .to() for device movement
- New StepAudioEditXHandler detects quantized models and properly deletes them
- Forces garbage collection and CUDA cache cleanup
- Fixes VRAM not being fully released when using int8 quantization
- Enables switching between int8 and other Step Audio EditX models without memory leaks
2025-12-08 23:05:41 -03:00
diodiogod 9915b1ca08 Switch Step Audio EditX to downloader pattern for local: resolution
- Follow Higgs Audio/VibeVoice pattern instead of IndexTTS engine pattern
- Add resolve_model_path() method to downloader for 'local:' prefix handling
- Downloader searches all configured TTS paths (yaml-aware)
- Factory now calls downloader.resolve_model_path() instead of engine method
- Cleaner separation: downloader owns model paths, factory owns model loading
- Consistent with other engines using downloader pattern
2025-12-08 22:24:27 -03:00
diodiogod 91105aa793 Fix Step Audio EditX local model path resolution
- Filter engine node dropdown to only show valid Step Audio EditX models (those with CosyVoice vocoder)
- Prevents internal components like FunASR-Paraformer from appearing in model selection
- Add proper 'local:' prefix handling in adapter to resolve model paths correctly
- Respects extra_model_paths.yaml configuration for yaml-configured paths
- Fixes error when selecting 'local:Step-Audio-EditX' from dropdown
2025-12-08 22:08:20 -03:00
diodiogod 7d3b171ab3 Add voice restoration with iteration reference support
New Feature: <restore:N@M> Syntax
- <restore> - Restore using original pre-edit audio (1 VC pass)
- <restore:N> - N VC passes using original audio as reference
- <restore:N@M> - N VC passes using audio from iteration M as reference

Implementation:
- Store audio snapshots after each edit iteration
- Parse iteration reference from @ syntax in restore tags
- Use specified iteration audio as VC reference instead of original
- Allows preserving partial effects (e.g., keep whisper, remove laughter)

Use Case Example:
<style:whisper:2> <Laughter:3> <restore:1@2>
- Applies whisper (iterations 1-2)
- Applies laughter (iterations 3-5)
- Restores using iteration 2 (whispered audio) with 1 VC pass
- Result: Keeps whisper effect but removes laughter voice degradation

Documentation:
- Updated INLINE_EDIT_TAGS_USER_GUIDE.md with restore tag section
- Added examples for basic and iteration-reference restoration
- Explained use cases and when to use each syntax
- Updated processing order and reference sections
2025-12-08 20:04:39 -03:00
diodiogod 1c31b1116a Add TTS caching to pause segment generation with edit tags
- Implement caching in _generate_with_pause_tags_as_segments() method
- Calculate stable_audio_component once and reuse for all segments
- Cache key is based on clean text WITHOUT edit tags (as it should be)
- Edit tags like <style:whisper:2> vs <style:whisper:4> don't invalidate TTS cache
- TTS generation parameters remain in cache key (exaggeration, temperature, etc.)

This fixes cache misses when only changing edit tag parameters while keeping
the same text and TTS settings. Edit tags are post-processing effects that
don't affect TTS generation, so they shouldn't impact the TTS cache.
2025-12-08 19:52:10 -03:00
diodiogod 2a2b096e45 Fix inline edit tag position tracking with space collapsing and improve progress display
Position Tracking Fixes:
- Adjust tag positions after multiple spaces are collapsed to single space
- Track cumulative position shifts from space collapsing operations
- Add space-before logic when reinserting paralinguistic tags
- Ensure proper spacing on both sides of reinserted tags (support <Laughter> hahaha)

Display Improvements:
- Show clean transcript instead of raw tag syntax in edit post-processor
- Add suppress_progress parameter to Step Audio EditX Audio Editor node
- Suppress redundant "Edit iteration 1/1" messages when called from edit_post_processor
- Edit post-processor already shows clear iteration progress (→ Iteration X/Y)

Fixes issues with:
- Tags inserted at wrong positions when separate tags create multiple spaces
- Tags glued to words without spacing (support<Laughter> instead of support <Laughter>)
- Confusing duplicate iteration progress messages
2025-12-08 19:40:36 -03:00
diodiogod 485736bc89 Fix pause tag segmentation and inline edit tag position tracking
- Refactor pause processing to return separate segments instead of combined audio
- Apply edit post-processing to individual segments after all TTS generation completes
- Fix position tracking for pipe-separated tags (e.g., <style:whisper:2|Laughter:5>)
- Adjust tag positions after leading whitespace stripping to maintain accuracy
- Fix space-after logic to only check tags from current match, not all previous tags
- Use clean text for TTS generation (strip edit tags before sending to model)
- Improve display to show clean transcript instead of raw tag syntax
- Add position offset tracking when inserting multiple paralinguistic tags

Resolves issues with:
- <restore> tag being sent to TTS model
- Paralinguistic tags inserted at wrong positions (e.g., h<Laughter> ahaha)
- Edit tags not being extracted properly in pause-segmented text
- Batch processing architecture for edit post-processing
2025-12-08 19:30:53 -03:00
diodiogod f434a8c5f8 Remove unnecessary patches from Step Audio EditX model loading
- Remove eager attention mechanism patch (no-op with proper config)
- Remove generation_config override (unused - max_new_tokens used instead)
- Remove forward pass monkey-patch (config.json fix made this obsolete)

The root cause was already fixed in commit 353c87d by adding
'tie_word_embeddings': false to config.json. These runtime patches
were redundant cargo-cult code that added unnecessary overhead during
model initialization without providing any actual benefit.

Transformers 4.51.3+ handles everything correctly with the config fix.
2025-12-08 16:30:07 -03:00
diodiogod 31d243abb4 Add <restore> inline tag for voice restoration after edits
Implements voice restoration feature using ChatterBox Official 23-Lang VC
to restore original voice resemblance after Step Audio EditX edits
(especially after multiple iterations of paralinguistic/emotion/style effects).

Changes:
- Add 'restore' tag parsing to step_audio_editx_special_tags.py
  * Support <restore> and <restore:N> syntax for multi-pass VC
  * Add restore to EDIT_TYPE_VALUES and sorting priority (runs last)
- Add voice restoration to edit_post_processor.py
  * New _restore_voice_via_vc() function reusing UnifiedVoiceChangerNode
  * Store PRE-EDIT audio as voice reference (no engine changes needed)
  * Process restore tags after all other edits complete
  * Fix importlib path for VC node loading

Benefits:
- Works across all 14 engines (7 text + 7 SRT) automatically
- No engine processor changes required
- Reuses existing VC cache system
- Uses pre-edit audio as target (clean, simple)
- Supports multi-pass refinement (1-5 iterations)
- Fully backward compatible

Processing order: emotion → style → speed → paralinguistic → restore
2025-12-08 16:28:24 -03:00
diodiogod 3f8a89d075 Update PROJECT_INDEX.md with Step Audio EditX engine and inline edit tags
- Add Step Audio EditX to architecture overview and supported engines list
- Document inline edit tag system as universal post-processing feature
- Add INLINE_EDIT_TAGS_USER_GUIDE.md to user documentation
- Document engines/step_audio_editx/ structure (FunASR, StepVocoder, quantization)
- Add step_audio_editx_adapter.py to engine adapters
- Add step_audio_editx_engine_node.py with quantization and clone/edit modes
- Document step_audio_editx processors (TTS and SRT with inline tag support)
- Add Step Audio EditX Specialized Nodes section for Audio Editor node
- Document step_audio_editx_special_tags.py utility for tag system
- Add step_audio_editx_showcontrol.js web interface
- Update Architecture Summary to reflect inline edit system integration
2025-12-08 15:30:30 -03:00
diodiogod e01f3515ef Add comprehensive inline edit tags user guide
- Complete guide for Step Audio EditX inline tag system
- Covers all tag types: paralinguistic, emotion, style, speed
- ChatterBox 23-Lang v2 native vs post-processing tag distinction
- Language support warnings (Chinese, English, Japanese, Korean only)
- Voice resemblance degradation warnings for 3+ iterations
- Performance tips and best practices
- Inline vs manual node workflow comparison
- Pro tips for stronger effects with contextual text
- Planned voice restoration feature note
- FAQ and troubleshooting sections
2025-12-08 15:21:07 -03:00
diodiogod a383d08d92 Add inline edit tag support to F5-TTS SRT processor
- Detect edit tags during segment parsing
- Pass extract_edit_tags flag to base generation method
- Base node handles pause-first splitting then edit tag extraction
- Collect segments with edit tags for batch processing
- Apply Step Audio EditX edits after all generation completes
- Normalize 3D edited tensors to 1D for concatenation
- Handle pause-only segments by immediate concatenation
2025-12-08 14:50:13 -03:00
diodiogod e7803d6342 Add complete edit tag support to F5-TTS regular node
Implements Step Audio EditX inline tag support for all F5-TTS regular node modes.

Changes:
- Multi-character mode: Extract edit tags, pass to base method, batch process
- Single character non-chunked: Extract tags, generate with segments, batch edit
- Single character chunked: Extract tags, generate chunks as segments, batch edit
- Handles segment dict responses from base method's extract_edit_tags feature
- Proper reconstruction of edited segments with silence preservation
- Follows batch processing pattern - edit engine loads once

All modes now support inline tags like <Laughter:1>, <Gasp:2> with pause tags.
2025-12-08 14:10:20 -03:00
diodiogod b2527377d2 Add edit tag support to F5-TTS regular node (multi-character mode)
Implements Step Audio EditX inline tag support for F5-TTS multi-character mode.

Changes:
- Added _extract_edit_tags_for_f5tts() helper method
- Detect edit tags in character segments before processing
- Pass extract_edit_tags=True to generate_f5tts_with_pause_tags for segments with tags
- Collect segment dicts with edit tags for batch processing
- Apply batch edit post-processing after all segments generated
- Reconstruct segments with edited audio using id() mapping
- Handles chunked segments with edit tags correctly

Follows batch processing pattern - edit engine loads once for all segments.
2025-12-08 13:57:22 -03:00
diodiogod c7ad6f4d8b Add edit tag extraction support to F5-TTS base pause tag processor
Modified generate_f5tts_with_pause_tags to optionally extract edit tags and return
segment dict format for batch post-processing.

Changes:
- Added extract_edit_tags parameter to generate_f5tts_with_pause_tags
- When extract_edit_tags=True, extracts Step Audio EditX tags from text
- Returns list of dicts [{waveform, text, edit_tags}] when edit tags present
- Returns plain tensor when no edit tags (backward compatible)
- Handles both pause tags + edit tags and edit tags only
- Extracts edit tags from each pause-split segment individually
2025-12-08 13:54:31 -03:00
diodiogod b1d9b9a284 Fix classic ChatterBox TTS to use expanded segments in traditional mode
Traditional mode was using original character_segments_with_lang instead of
expanded_segments_with_lang (after pause splitting), causing missing segments
in output and index mismatches with edit tag tracking.

Changes:
- _process_languages_traditional now uses expanded segments
- Updated method to handle both 4-tuple and 5-tuple segment formats
- Fixes missing final phrase when pause tags and edit tags are both present
2025-12-08 13:41:41 -03:00
diodiogod d3138ec11d Add inline edit tag support for classic ChatterBox regular TTS
Implements Step Audio EditX edit tag support for classic ChatterBox regular TTS node following the batch processing pattern.

Key changes:
- Added edit tag extraction helper method _extract_edit_tags_for_classic_chatterbox()
- Extract edit tags from pause-split segments (not from original segments with pause tags)
- Multi-character mode: Extract during pause expansion, then batch process after generation
- Single character mode (non-chunked): Extract tags, generate, then apply edit post-processing
- Single character mode (chunked): Extract tags, generate chunks, combine, then apply edit post-processing

Critical fix: Edit tags are extracted AFTER pause tag splitting to prevent sending
pause tags to the edit processor. Uses expanded_segments_edit_tags for tracking.

Follows batch processing pattern - edit engine loads once instead of per-segment.
Works for all modes: multi-character, multi-language, single character (chunked and non-chunked).
2025-12-08 12:30:06 -03:00
diodiogod 4a141fec0f Add inline edit tag support for classic ChatterBox SRT processor
Implements Step Audio EditX edit tag support for classic ChatterBox SRT following the batch processing pattern. Modified both chatterbox_srt_node.py and multilingual_engine.py to support edit tag extraction and batch processing.

Key changes:
- Modified multilingual_engine.py to extract edit tags per segment and add edit_tags field to AudioSegmentResult
- Implemented pause-first splitting when both pause and edit tags present to preserve pauses during editing
- Added batch edit processing infrastructure to collect all edit segments across subtitles
- Added segment_position tracking to properly reconstruct mixed edited/non-edited segments
- Fixed storage logic to prevent overwriting segment_audio_parts list with wav tensor
- Supports all processing paths: parameter_switching, multilingual/multicharacter, and simple

Follows SRT_Edit_Tag_Batch_Processing_Pattern.md - edit engine loads once for all segments instead of per-subtitle.
2025-12-08 12:17:15 -03:00
diodiogod bdc0bcfc54 Fix pause+edit tag handling in ChatterBox 23-Lang SRT
Split pause tags FIRST before processing edit tags to preserve pauses
during Step Audio EditX editing. Matches Step Audio EditX TTS approach.

Changes:
- When segment has both pause and edit tags, split by pause first
- Generate audio for each text segment separately (no pause tags)
- Insert silence segments between text segments
- Edit post-processor processes individual segments (pauses preserved)
- Fixed tensor dimension mismatch when reconstructing audio parts

Applied to all 3 SRT paths (character switching, multilingual, simple)

Fixes pauses being lost during edit post-processing
2025-12-08 08:31:35 -03:00
diodiogod 4c5d61447b Strip pause tags from text passed to edit post-processor in SRT
Added missing 're' import and strip pause tags from both 'text' and
'original_text' fields before storing segments for edit processing.

Pauses are already applied in the audio waveform during TTS generation,
so we don't want pause tags in the transcript sent to Step Audio EditX.

Changes:
- Added 're' import to chatterbox_official_23lang_srt_processor.py
- Strip pause tags from text in all 3 SRT processing paths:
  - Character switching (multicharacter)
  - Multilingual (narrator)
  - Simple (single character)

Note: Pauses may still be lost during Step Audio EditX editing - investigating
2025-12-07 16:05:37 -03:00
diodiogod 9ee3996e24 Fix tensor shape mismatch in ChatterBox 23-Lang edit post-processing
Fixed dimension mismatch error when combining audio chunks after edit
post-processing. Issue was caused by:
1. Squeezing audio tensors when passing to edit processor
2. Not matching device/shape when replacing processed audio

Changes:
- Keep original tensor shape when storing audio for edit processing
- Match device and dimensions of processed audio to original before replacement
- Handle both 2D [channels, samples] and 3D [batch, channels, samples] formats
- Applied to both streaming and traditional processing paths

Files modified:
- nodes/chatterbox_official_23lang/chatterbox_official_23lang_processor.py
2025-12-07 08:56:10 -03:00
diodiogod 2f8f8c7207 Fix pause processor after making pause type non-capturing
Adjust group numbers: group 1 is now duration, group 2 is unit
Non-capturing group (?:pause|...) doesn't create a numbered group
2025-12-07 08:39:38 -03:00
diodiogod a5f8ee9222 Fix tensor dimension mismatch in edit post-processing
Ensure processed audio has same shape as original segments
Add unsqueeze(0) if processed audio is 1D to match expected 2D format
2025-12-07 08:35:45 -03:00
diodiogod b1c553f1dc Fix pause tag case-sensitivity across all parsers
Make all pause/wait/stop tag patterns case-insensitive:
- segment_parameters.py
- pause_processor.py
- character_parser validation.py
- chatterbox processors
- backup files

Now supports [pause:1], [Pause:1], [PAUSE:1] variants everywhere
2025-12-07 08:34:45 -03:00
diodiogod 12aa64f802 Fix character parser to exclude pause tags case-insensitively
Support [Pause:1], [PAUSE:1], [pause:1] variants
Prevents pause tags from being treated as character names
2025-12-07 08:33:18 -03:00
diodiogod 14194bc09b Fix function name: use process_segments instead of apply_edit_post_processing
Correct import and function calls for edit post-processing in ChatterBox 23-Lang processor
2025-12-07 08:31:39 -03:00
diodiogod 52566b22d7 Implement full batch edit tag processing for ChatterBox 23-Lang TTS
- Extract edit tags for each character segment during expansion
- Track segments with edit tags alongside audio generation
- Batch process all edit tags after TTS generation complete
- Replace placeholders with processed audio
- Support both streaming and traditional processing modes
- Warnings for laughter/sigh tag conflicts
- Native tags without iterations use ChatterBox native processing
- Tags with iterations use Step Audio EditX post-processing
2025-12-07 08:11:18 -03:00
diodiogod ff066b3091 Add edit tag warning support to ChatterBox 23-Lang normal TTS
- Import extract_edit_tags_for_chatterbox helper from SRT processor
- Replace convert_v2_special_tags with edit tag extraction for v2 models
- Users will see warnings about laughter/sigh tag conflicts
- Native tags without iterations processed as ChatterBox native
- Tags with iterations extracted but NOT batch processed (TODO: full batch support)
2025-12-07 08:06:48 -03:00
diodiogod b9d6f19476 Fix ChatterBox v2 native tags being treated as character names
- Character parser now skips ChatterBox v2 special tokens ([laughter], [giggle], etc)
- These tokens are TTS control tags, not character names
- Warns if user has a character with same name as v2 token
- Fix warning system to trigger for all conflicting tags (not just when both formats present)
- Remove debug print from model manager ensure_device
2025-12-07 08:02:08 -03:00
diodiogod 13da5b63e0 Add inline edit tag support to ChatterBox simple and multilingual paths
- Extract edit tags in simple path (single character subtitles)
- Extract edit tags in multilingual path (mixed languages)
- Both paths now support batch edit processing
- Warnings for native vs EditX tag conflicts will now appear
- All three paths (multicharacter, simple, multilingual) complete
2025-12-06 20:59:02 -03:00
diodiogod 9b53575f58 Add inline edit tag support to ChatterBox 23-Lang SRT processor
Implemented proper batch processing pattern for edit tags with character switching.

Key changes:
- Extract edit tags from individual character segments (not entire subtitle)
- Store segments with edit tags separately using subtitle_index and segment_index
- Use None placeholders for segments pending post-processing
- Batch process ALL segments at end (engine loads once)
- Reconstruct multicharacter subtitles by replacing placeholders and concatenating

Pattern:
- Multicharacter segments processed individually before concatenation
- Each character's audio stored separately if it has edit tags
- Batch processing groups by subtitle_index, sorts by segment_index
- Final audio assembled by concatenating processed character segments

Benefits:
- Edit engine loads ONCE instead of per-segment
- Character tags ([Alice], [Bob]) not present in edit text
- Proper per-character edit processing
- Maintains character switching functionality

Follows SRT_Edit_Tag_Batch_Processing_Pattern.md
2025-12-06 20:23:41 -03:00
diodiogod 467c5975f9 Fix narrator language assignment in ChatterBox 23-Lang SRT processor
Fixed bug where narrator was inheriting language from character aliases
instead of using the user's default language setting.

Changes:
- Narrator now uses current_language (user's default) instead of seg_lang/lang_code
- Fixed subtitle_type priority: character switching takes precedence over multilingual
- Fixed simple path to use actual character name instead of hardcoding "narrator"
- Multilingual path now only used for narrator-only text with multiple languages

Before: Narrator spoke in German when Alice (de alias) appeared in subtitle
After: Narrator speaks in user's default language (English), Alice speaks German
2025-12-06 20:03:14 -03:00
diodiogod c272cd8718 Fix Higgs Audio VRAM cleanup when model already loaded
When Higgs model is already loaded and reused, call ensure_device to clear other TTS models (like Step Audio EditX) from VRAM before generation.

This prevents OOM when Step EditX was previously loaded for edit processing and needs to be offloaded before the next Higgs generation.

Files modified:
- engines/adapters/higgs_audio_adapter.py: Call ensure_device in early return path
2025-12-06 19:15:10 -03:00
diodiogod 08b52f63c3 Add inline edit tag support for Higgs Audio and fix CUDA graph unloading crash
Added edit tag extraction and batch processing for Higgs Audio TTS and SRT processors following the established pattern from VibeVoice and Step Audio EditX.

Key changes:
- Extract edit tags at top level before pause processing to keep nested functions returning tensors
- Apply edit post-processing after all audio generation completes
- For SRT: batch process all subtitle edits at end instead of per-subtitle
- Fix CUDA graph crash: detect original engine for stateless wrappers and refuse unloading when CUDA graphs enabled
- Store original_engine in ModelInfo to track Higgs even when masked as stateless_tts
- Update Higgs handler to check correct attribute paths for _cuda_graphs_enabled flag

Files modified:
- nodes/higgs_audio/higgs_audio_tts_processor.py: Add edit tag extraction and post-processing
- nodes/higgs_audio/higgs_audio_srt_processor.py: Batch edit processing for all subtitles
- utils/models/comfyui_model_wrapper/base_wrapper.py: Track original_engine for masked engines
- utils/models/comfyui_model_wrapper/model_manager.py: Store original engine when masking
- utils/models/comfyui_model_wrapper/engine_handlers/higgs_audio_handler.py: Refuse unload when CUDA graphs enabled
- docs/Dev reports/SRT_Edit_Tag_Batch_Processing_Pattern.md: Mark Higgs as implemented
2025-12-06 19:04:27 -03:00
diodiogod 96877fec99 Refactor SRT processors to batch edit post-processing at the end
Fixed inefficient per-subtitle edit processing that caused multiple engine loads.
Now all edit processing happens in a single batch after all subtitles are generated.

Changes:
- VibeVoice SRT: Collects all segments with edit_tags, applies edits once at end
- Step Audio EditX SRT: Same batching pattern, skips edits in _process_character_switching when in SRT mode
- Step processor: Added check to only apply edits when NOT in SRT mode
- Subtitle processing functions: Return segment dicts instead of applying edits directly

Benefits:
- Edit engine loads ONCE instead of once per subtitle
- Significantly faster SRT generation with edit tags
- Less VRAM thrashing from repeated model loading/unloading
- Maintains same quality output

Added dev report documenting the pattern for future engine implementations.
2025-12-06 17:12:33 -03:00
diodiogod 2294a93c68 Add inline edit tag support to VibeVoice SRT processor
Implemented Step Audio EditX inline tag support for VibeVoice SRT generation:

Adapter changes:
- Updated generate_vibevoice_with_pause_tags() to extract edit tags even without pause tags
- Returns segment dict format with edit_tags when tags are present
- Maintains backwards compatibility by returning plain tensor when no tags

SRT processor changes:
- Updated both _process_custom_character_switching_subtitle() functions to handle segment dicts
- Added edit post-processing via EditPostProcessor.process_segments()
- Normalizes waveform dimensions before concatenation to prevent tensor mismatch errors
- Applies edits (laughter, whisper, etc.) via Step Audio EditX after TTS generation

Now supports inline tags like <Laughter:2>, <style:whisper:3> in SRT content
2025-12-06 16:57:33 -03:00
diodiogod 8018ce70da Fix model cache key matching to distinguish between different model variants
Fixed issue where switching between different models of the same engine (e.g.,
vibevoice-7B to vibevoice-1.5B) would cause OOM by trying to load wrong model:

- Added cache_key parameter to ComfyUIModelWrapper so wrapper knows its specific identity
- Updated ensure_device() to accept model_cache_key and clear same-engine-different-variant models
- Fixed ensure_device() to use cache_key directly when provided instead of searching by engine
- Removed ensure_device() call from VibeVoice generation path (was causing OOM during inference)
- Added device string normalization to handle cuda vs cuda:0 comparison differences

Now properly handles:
- Switching between vibevoice-7B and vibevoice-1.5B (clears old model from GPU)
- Different quantization variants (4bit vs full precision treated as separate models)
- Multiple model variants coexisting in CPU cache for fast reload
2025-12-06 16:44:30 -03:00
diodiogod 3627980bc5 Fix VRAM management with proper model clearing and synchronization
Resolved OOM errors during model switching by implementing proper VRAM clearing:
- Added real VRAM measurement using torch.cuda.memory_allocated() in model_manager
- Removed fake VRAM reporting from VibeVoice handler
- Removed class-level caching from VibeVoice engine (unified manager handles all caching)
- Centralized device movement through ensure_device() in model manager
- Models now move to CPU instead of deletion for fast reload from cache
- Added CUDA synchronization after clearing to ensure memory is actually freed
- Fixed recursion by adding _from_ensure_device flag to model_load()
- model_load() now calls ensure_device() first to clear other models before loading

Also fixed inline edit tag spacing issues:
- Tags now properly add space after paralinguistic tags when followed by alphanumeric text
- Fixed both parser and post-processor to handle spacing correctly
2025-12-06 16:18:32 -03:00
diodiogod 6f03c00cf2 Fix SRT parser stripping Step Audio EditX edit tags
Root cause: SRT parser removed ALL <...> tags as HTML, including edit tags
- Line 204 used blanket regex '<[^>]+>' that stripped everything
- This removed <Laughter:3>, <style:whisper:2>, etc. before TTS processing

Fix:
- Modified regex to preserve edit tags with alphanumeric+colon+pipe structure
- New pattern: '<(?![a-zA-Z][a-zA-Z0-9_\-\s:|\d]*>)[^>]+>'
- Still removes actual HTML tags like <b>, <i>, <font>, etc.
- Preserves edit tags: <Laughter:2>, <style:whisper>, <emotion:happy>, <speed:faster>

Also added debug logging to verify tag extraction in processor
2025-12-05 22:49:49 -03:00
diodiogod c8c86558e0 Fix edit_tags storage to preserve non-empty lists
- Changed 'edit_tags if edit_tags else None' to just 'edit_tags'
- Empty list [] was being converted to None, breaking post-processor detection
- Added original_text field for fallback parsing
- Fixed in all 3 code paths: chunking, no-chunking, pause-segment
2025-12-05 22:39:24 -03:00
diodiogod 1fb2a242bc Fix Step Audio EditX inline tags and unified interface integration
ModelLoadConfig parameter fixes:
- Use additional_params dict for torch_dtype and quantization (not direct kwargs)
- Fixed in both adapter and Audio Editor node
- Added model_path resolution to default ComfyUI models directory

Factory fixes:
- Return StepAudioEditXEngine wrapper instead of StepAudioTTS
- Wrapper provides get_sample_rate() and other methods needed by nodes
- Maintains compatibility with adapter and Audio Editor node

Adapter fixes:
- Handle both tuple and tensor returns from engine.clone()
- Inner engine returns (audio, sample_rate), wrapper returns just audio

Paralinguistic tag spacing:
- Auto-insert space before paralinguistic tags when glued to words
- 'whispering<Laughter>' becomes 'whispering [Laughter]'
- Required for Step Audio EditX paralinguistic insertion to work correctly
2025-12-05 22:19:21 -03:00
diodiogod d1b093d273 Refactor inline edit tags to use Audio Editor node + fix unified interface
Major changes:
1. Post-processor now uses Audio Editor node instead of calling engine directly
   - Inherits progress bars, caching, tested code
   - Much simpler and more maintainable

2. Fixed unified model interface usage across Step Audio EditX
   - Adapter now loads via unified_model_interface.load_model()
   - Audio Editor node now loads via unified_model_interface.load_model()
   - Both use model_type="tts" for consistent cache keys
   - Prevents duplicate model loading (was loading twice before)

3. Fixed pause tag + edit tag interaction
   - Pause tags split FIRST, then edit tags extracted per segment
   - Allows: "Hello <Laughter> [pause:1] World <Sigh>"
   - Laughter applies to "Hello", Sigh applies to "World"

4. Added lesson to fails document
   - All engines must use unified interface in ALL code paths
   - Cache key must match across adapters/nodes/processors

Bug fixes:
- Missing Dict import in post-processor
- Import path for Audio Editor node (use importlib)
- Progress bars now work during editing

Files modified:
- utils/audio/edit_post_processor.py - refactored to use Audio Editor node
- nodes/step_audio_editx/step_audio_editx_processor.py - pause handling
- engines/adapters/step_audio_editx_adapter.py - unified interface
- nodes/step_audio_editx_special/step_audio_editx_audio_editor_node.py - unified interface
- docs/Dev reports/fails_to_avoid_TTS_Engine_Implementation.md - added lesson
2025-12-05 21:43:05 -03:00
diodiogod 1f629737d2 Implement inline edit tags for Step Audio EditX post-processing
Added support for inline edit tags in TTS text that automatically apply
Step Audio EditX edits to generated audio from ANY engine.

Syntax examples:
- <Laughter:2> - paralinguistic with iterations
- <emotion:happy:1> - emotion edit
- <style:whisper:3> - style edit
- <speed:faster> - speed adjustment
- <Laughter:2|style:whisper> - multiple effects

Key changes:
- Extended step_audio_editx_special_tags.py with EditTag dataclass and iteration parsing
- Created edit_post_processor.py for centralized post-TTS edit application
- Integrated into Step Audio EditX processor with proper pause tag handling
- Pause tags split FIRST, then edit tags extracted per pause-segment
- Reuses already-loaded engine to avoid duplicate loading

Flow:
1. Parse text, split by pause tags first
2. Extract edit tags from each pause-delimited segment
3. Generate TTS with clean text (tags stripped)
4. Post-processor applies edits to segments with tags
5. Assemble final audio

Currently working for Step Audio EditX TTS and SRT nodes.
2025-12-05 21:20:21 -03:00
diodiogod 71b2a12837 Auto-strip paralinguistic tags from transcript in non-paralinguistic edit modes
When using emotion, style, speed, denoise, or vad modes, any paralinguistic tags in audio_text are automatically removed. Tags are only meaningful in paralinguistic mode, so this prevents them from being passed to the model as plain text in other modes.
2025-12-05 15:04:02 -03:00
diodiogod b7647d5cca Improve error message for audio duration limit in Step Audio EditX edit node
Explains the 30-second limit is a model architecture limitation and suggests splitting audio at natural pauses as a workaround.
2025-12-05 15:00:25 -03:00
diodiogod 55cf8842d8 Improve Step Audio EditX edit node UI with conditional widget disabling
• Add JavaScript showcontrol file to grey out irrelevant options based on edit_type selection
  - emotion dropdown only enabled when edit_type='emotion'
  - style dropdown only enabled when edit_type='style'
  - speed dropdown only enabled when edit_type='speed'
  - All dropdowns disabled for paralinguistic, denoise, vad modes
  - n_edit_iterations remains enabled for all modes
  - Follows same pattern as Inpaint Crop (Improved) node

• Fix paralinguistic tooltip to show ALL 10 available tags instead of first 5 + "..."
  - Complete list: <Breathing>, <Laughter>, <Sigh>, <Uhm>, <Surprise-oh>, <Surprise-ah>, <Surprise-wa>, <Confirmation-en>, <Question-ei>, <Dissatisfaction-hnn>
  - Users can now see all available tags in the tooltip

Improves UX by showing only relevant options and providing complete tag reference
2025-12-05 14:45:04 -03:00
diodiogod 8791600d5f Add FFmpeg stretcher method tracking to timing reports for all SRT processors 2025-12-05 13:26:09 -03:00
diodiogod 35d216e95b Remove verbose patch summary messages
Only print individual patch application messages when patches are actually
applied (when verbose=True in each specific patch method). Removes noisy
summary messages like 'Applied 4 transformers compatibility patches' that
provide no actionable information.
2025-12-05 13:02:18 -03:00
diodiogod 0cfed8d23c Disable Step Audio EditX tokenization patch - fixed via config.json
The transformers 4.54+ compatibility issue is now resolved by setting
tie_word_embeddings=false in the model config.json. The monkey-patch
workaround is no longer needed.
2025-12-05 12:57:07 -03:00
diodiogod 353c87de55 Fix Step Audio EditX transformers 4.54+ compatibility with simple config fix
Replace complex weight restoration workaround with a simple one-line config fix.

The root cause: Step-Audio-EditX model config was missing 'tie_word_embeddings'
setting, so transformers 4.54+ defaulted to True and tied lm_head to embed_tokens
even though the checkpoint has different weights for each.

Solution: Add 'tie_word_embeddings': false to config.json to prevent tying.

This is much simpler and cleaner than the previous safetensors weight restoration
approach. One line in config.json fixes the issue for all transformers versions.
2025-12-05 12:52:22 -03:00
diodiogod 848a1d7469 Fix Step Audio EditX clone mode sample rate mismatch in TTS nodes
CRITICAL: TTS text and SRT nodes were saving narrator audio with original
sample rate (44100 Hz, 48000 Hz, etc.) causing pitch shift in Step Audio EditX.

Changes:
- TTS text node: Resample narrator audio to 24000 Hz for Step Audio EditX
- TTS SRT node: Resample narrator audio to 24000 Hz for Step Audio EditX
- Both handle narrator and character fallback audio

Matches edit node fix (d7359e5) - ensures consistent 24000 Hz requirement
for all Step Audio EditX audio inputs (edit, clone, SRT).
2025-12-05 09:08:51 -03:00
diodiogod d7359e55b3 Fix Step Audio EditX edit node sample rate mismatch causing pitch shift
CRITICAL: Audio editor node was saving input audio with engine's sample rate (24000 Hz)
regardless of actual input sample rate (44100 Hz, 48000 Hz, etc.), causing pitch shift.

Changes:
- Track current sample rate through iterations
- Resample to 24000 Hz before saving if needed
- Update sample rate after each iteration (engine returns 24000 Hz)
- Handle cached iterations at correct sample rate

Fixes pitch preservation when editing external audio files.
2025-12-05 08:28:46 -03:00
diodiogod a55b3bca20 Add Step Audio EditX Audio Editor node for emotion/style/speed/paralinguistic editing
- New dedicated Audio Editor node with 6 edit types: emotion (14), style (32), speed (4), paralinguistic (10), denoise, vad
- Smart paralinguistic text handling: user writes <Laughter> tags, auto-converts to [Laughter] for engine
- Derives clean transcript automatically by stripping tags from input text
- Iteration caching: try 3 passes, then 2, then 1 without re-running (like Voice Changer)
- Token generation progress bar during each edit iteration
- Normalizes newlines/whitespace in text to prevent model parsing issues
- Graceful error when wrong engine type connected (guides user to use Step Audio EditX Engine)
- New special tags registry at utils/text/step_audio_editx_special_tags.py
2025-12-04 20:45:37 -03:00
diodiogod dd7a1eefa5 Add protobuf>=3.20.0 version constraint 2025-12-04 18:00:36 -03:00
diodiogod 5c0e7030f1 Clean up Step Audio EditX dependencies
- Remove onnxruntime-gpu (handled by install.py with --no-deps)
- Remove sox (torchaudio.sox_effects built-in, dead code removed)
- Keep openai-whisper for future Audio Editor node
2025-12-04 16:17:03 -03:00
diodiogod da30410099 Add Step Audio EditX SRT processor with full character switching support
- Create step_audio_editx_srt_processor.py following IndexTTS-2 pattern
- Register Step Audio EditX in unified SRT node (wrapper + processing)
- Add SRT-level progress tracking (job management at subtitle level)
- Build complete voice mapping with alias resolution for character switching
- Handle ComfyUI audio dict format extraction for temp file saving
- Skip internal job tracking in processor when SRT manages it
2025-12-04 16:02:36 -03:00
diodiogod f9c88d7e89 Add Step Audio EditX segment parameter support and fix tooltips
- Add step_audio_editx to PARAMETER_ENGINES for seed and temperature
- Fix engine node tooltips: parameters ARE used in clone mode (not hardcoded)
- Add reference implementation paths to implementation plan
- Update fails doc with latest lessons learned
2025-12-04 13:59:49 -03:00
diodiogod 2afb1e11fa Fix Step Audio EditX tensor shape handling for 3D returns
- Added 3D tensor handling in engine wrapper (clone() and edit() methods)
- Added 3D tensor handling in adapter's pause tag processing
- Ensures consistent 2D [1, samples] shape throughout pipeline
- Prevents "Dimension out of range" error in ComfyUI save_audio()
2025-12-04 10:13:00 -03:00
diodiogod c7354235d0 Fix Step Audio EditX tensor shape bug in audio_segments
The processor was adding extra dimension with unsqueeze(0) when tensor was already 2D,
creating 3D tensors that failed in ComfyUI audio save.

Fixed: Properly ensure 2D [channels, samples] shape in both chunked and non-chunked paths.
Also added shape safety check in unified node as defense-in-depth.
2025-12-04 09:55:12 -03:00
diodiogod b550b5b3f8 Fix generation_info consistency across IndexTTS and Step Audio EditX
Make both engines follow ChatterBox 23-Lang modular pattern:

**Step Audio EditX:**
- Update combine_audio_segments() to use ChunkCombiner with return_info=True
- Return (audio, chunk_info) tuple when requested
- Fix single segment shape handling (ensure 2D [channels, samples])

**IndexTTS:**
- Add combine_audio_segments() method using ChunkCombiner
- Update process_text() to accept return_info parameter
- Build chunk_info from text segments and return timing data

**Unified Node:**
- Call both engines with return_info=True
- Use ChunkTimingHelper.enhance_generation_info() for detailed reports
- Clean text without tags before calculating length

**Documentation:**
- Add generation_info consistency requirement to fails document
- Document correct pattern: ChunkCombiner + ChunkTimingHelper
- Note wrong pattern: inline torch.cat without timing

Now both engines provide detailed timing reports like F5-TTS and ChatterBox.
2025-12-04 09:51:10 -03:00
diodiogod 701f98ca8a Remove automatic __del__ unload from IndexTTS and Step Audio EditX engines
Models should only unload when explicitly requested (Clear VRAM button) or when
switching engines - NOT automatically after generation completes.

__del__ was causing models to unload immediately after wrapper instance goes
out of scope, preventing model reuse between generations.

Pattern: F5-TTS and ChatterBox don't have __del__, only Index2 and StepAudio had it.
Removed to match correct behavior.
2025-12-04 09:18:25 -03:00
diodiogod 9268c2f4ea Fix UnboundLocalError: Remove redundant AudioProcessingUtils imports
Python treats local imports as binding the variable to local scope throughout
the entire function. Three redundant local imports of AudioProcessingUtils
inside the generate_speech function were causing UnboundLocalError on lines 889
and 915 when accessing AudioProcessingUtils before it was bound.

Removed redundant imports at lines 360, 506, and 738 - the module-level import
at line 36 is sufficient for the entire function scope.
2025-12-04 09:18:14 -03:00
diodiogod 12aa7ccf96 Implement caching for Step Audio EditX engine
Add cache support using unified audio cache system:
- Create StepAudioEditXCacheKeyGenerator in cache.py (includes all relevant parameters)
- Register generator in AudioCache.__init__()
- Update _calculate_duration() to handle 24000 Hz sample rate
- Implement cache lookup in _generate_direct() before generation
- Cache results after successful generation with proper duration

Cache key includes: text, audio_component (stable hash), prompt_text, temperature,
do_sample, max_new_tokens, seed, model_path, device, torch_dtype, quantization, character

Now hits cache for unchanged segments across multiple generations, just like other engines.
2025-12-04 08:29:41 -03:00
diodiogod f44a884cc9 Add job-level time prediction to Step Audio EditX progress bar
Implements accurate time estimation showing remaining generation time based on:
- Chunk-level work tracking (not just character blocks)
- Text-to-token ratio from completed chunks
- In-progress estimation using token generation speed
- Conservative estimates until 150+ tokens (avoid wild early guesses)

Tracks across all chunks in job to provide accurate elapsed/remaining times.
Job elapsed shown, with ETA updating as generation progresses.
2025-12-04 08:22:24 -03:00
diodiogod b0b41e464f Fix 8-bit quantization device movement error
- Wrap .to() calls in try-except for quantized models
- Show warning only once per engine instance
- Add quantization notes to fails document
2025-12-04 01:37:48 -03:00
diodiogod 9128899a15 Fix Step Audio EditX character switching bug
- Configure character parser with available characters and language defaults like IndexTTS
- Add characters from text tags to available_characters to prevent fallback to "narrator"
- Add fallback voice mapping entries for characters without voice files
- Update fails document with character switching implementation lessons
2025-12-04 01:17:44 -03:00
diodiogod 210a2a1a37 Reduce verbosity of compatibility patches init output
Remove verbose debug prints from Step Audio EditX generation output.
The prints were useful for debugging transformers 4.54+ compatibility
but are no longer needed now that the bug is fixed.
2025-12-03 23:56:36 -03:00
diodiogod 134eacca72 Clean up Step Audio EditX verbose logging
- Suppress repetitive loading messages (tokenizer, FunASR, TTS engine)
- Remove DEBUG completeness check prints
- Keep only essential initialization and download messages
- Fix torch_dtype → dtype deprecation warnings
- Reduce console noise while preserving error visibility
2025-12-03 23:51:29 -03:00
diodiogod 4788d0347e Reduce verbosity of compatibility patches init output
Remove unnecessary progress messages during patch application.
Now only shows summary counts unless there are errors.
2025-12-03 23:41:04 -03:00
diodiogod d89794c169 Clean up debug code from Step Audio EditX
Remove unnecessary debug prints and AudioTokenBiasLogitsProcessor class.
The transformers 4.54+ bug is now fixed by restoring correct lm_head weights
in model_loader.py, so debug/workaround code is no longer needed.
2025-12-03 23:20:48 -03:00
diodiogod 4ed2a36aef Fix Step Audio EditX transformers 4.54+ compatibility
Fixes critical bug where transformers 4.54+ incorrectly ties lm_head weights
to embed_tokens, causing Step Audio EditX to generate text tokens instead of
audio tokens (silent/gibberish output).

Root cause:
- Transformers 4.54+ ties lm_head to embed_tokens even when checkpoint has different values
- Step-Audio-EditX has lm_head (norm=227) and embed_tokens (norm=255) as separate weights
- Tying overwrites correct lm_head, breaking audio generation

Changes:
- model_loader.py: Add workaround to restore correct lm_head weights after loading
  - Detects incorrect weight tying by comparing data pointers
  - Loads correct lm_head.weight from safetensors
  - Works with transformers 4.54-4.57+
- Clean up debug code and console spam:
  - Remove torch_complex warnings (funasr_detach files)
  - Remove sox import and unused audio functions
  - Silence FunASR registration tables
  - Filter torchaudio deprecation warnings

Upstream fix:
- Submitted PR to transformers: https://github.com/huggingface/transformers/pull/42612
- Workaround will remain for backward compatibility with unfixed versions

Tested with transformers 4.57.3 - generates audio tokens correctly.
2025-12-03 23:18:39 -03:00
diodiogod 89d12285b5 Add Step Audio EditX TTS engine implementation
Implemented Step Audio EditX integration with the following components:

Engine Implementation:
- Bundled Step Audio EditX reference code in step_audio_editx_impl/
- Created engine wrapper (step_audio_editx.py) with clone/edit methods
- Created adapter (step_audio_editx_adapter.py) for unified interface
- Created processor (step_audio_editx_processor.py) for TTS Suite integration

Model Management:
- Created downloader (step_audio_editx_downloader.py) for:
  - Main Step-Audio-EditX model (3B LLM + CosyVoice vocoder)
  - Step-Audio-Tokenizer files (linguistic_tokenizer.npy, speech_tokenizer_v1.onnx)
  - FunASR-Paraformer model with correct config for bundled funasr_detach
- Fixed FunASR config to use bundled class names (ParaformerStreaming, WavFrontendOnline, SpecAugLFR)
- Added auto-download for all required models on first use
- Updated unified_model_interface.py to auto-download Step-Audio-EditX components

Node Configuration:
- Created Step Audio EditX engine config node
- Registered in nodes.py and unified TTS text node

Known Issues:
- LLM generation hangs during inference despite:
  - torch.no_grad() wrapper
  - torch.cuda.synchronize()
  - Eager attention mode (Step1ForCausalLM doesn't support SDPA)
  - Reduced max_new_tokens
  - Removed logits processor
- Model loads successfully, all components initialized correctly
- Issue appears to be with transformers.generate() call itself
- Requires further investigation of Step1ForCausalLM compatibility

Files Modified:
- nodes.py: Added Step Audio EditX registration
- requirements.txt: Added FunASR dependencies
- utils/models/unified_model_interface.py: Added Step Audio EditX factory
- Various Step Audio EditX implementation files

Model Downloads:
- Step-Audio-EditX: 7GB (3B LLM + vocoder)
- FunASR-Paraformer: 881MB (from custom HF repo with fixed config)
- Step-Audio-Tokenizer: Bundled with main model download
2025-12-03 13:27:02 -03:00
diodiogod 88e84e55e5 Version 4.14.36
Add chunking support to RVC voice conversion

Technical implementation:
- Extend intelligent chunking system to RVC engine
- Add _handle_rvc_chunked_conversion() method for RVC-specific chunk processing
- Pass max_chunk_duration and chunk_method parameters to RVC handler
- Support all RVC refinement passes within chunked processing
- Maintain full RVC configuration compatibility (pitch, index_rate, etc.)
- Use 50ms crossfade for seamless chunk combination
2025-12-02 18:19:41 -03:00
diodiogod 38be347dc2 Version 4.14.35
Add intelligent chunking to Voice Changer node

Technical implementation:
- Add smart silence-based chunking to prevent OOM on long audio
- Implement fixed-duration chunking as fallback option
- Fix infinite loop bug in silence detection algorithm
- Fix sample rate mismatch causing chipmunk voice artifacts
- Add two new parameters: max_chunk_duration (0-300s) and chunk_method (smart/fixed)
- Full cache support - chunks are cached individually for faster reruns
- Works with all refinement_passes iterations per chunk
2025-12-02 16:03:48 -03:00
diodiogod 51e97f70f6 Version 4.14.34
Fix RVC HuBERT subdirectory double-nesting issue

Technical fix:
- Add check to detect if .pt file is already in correct subdirectory
- Prevent creating nested subdirectories (e.g., chinese_hubert_base/chinese_hubert_base/)
- Use existing subdirectory path when model is already correctly placed
2025-12-02 09:54:53 -03:00
diodiogod 8ed0695092 Version 4.14.33: Remove IndexTTS-2 debug output from console 2025-12-02 01:10:48 -03:00
diodiogod 4214491e59 Version 4.14.32
Fix RVC HuBERT model loading and compatibility

Technical changes:
- Fix Japanese HuBERT download URL (moved to public prj-beatrice repo)
- Fix content-vec-best model reference (was hubert-base-rvc)
- Add transformers HuBERT support with TransformersHubertWrapper
- Implement subdirectory structure for transformers models to avoid config conflicts
- Add auto-migration for existing models to new subdirectory structure
- Fix HubertConfig import scope issue in loaders.py
- Add config.json auto-download for transformers models
- Fix dtype conversion (float16/float32) in feature extraction
- Add dimension mismatch detection with helpful user messages
- Fix hubert_path parameter passthrough in conversion pipeline
2025-12-02 00:41:24 -03:00
diodiogod 12d40e1a75 Version 4.14.31
Fix 4D tensor handling in audio processing

Technical details:
- Fix Wave Analyzer segmented_audio output creating 4D tensors
- Add defensive dimension normalization in base_node handle_reference_audio
- Fix ChatterBox voice conversion nodes handling unexpected tensor dimensions
- Normalize tensors to 2D before torchaudio.save calls
2025-11-20 01:28:28 -03:00
diodiogod 717a7afaf5 Version 4.14.30
Fix Voice Fixer model loading and improve download speed

Technical details:
- Fix vocoder searching in wrong directory path (synthesis_module/44100 vs root)
- Move Voice Fixer node to correct category alongside audio processing nodes
- Add HuggingFace CLI download support to UnifiedDownloader for faster downloads
- Create HuggingFace mirror for Voice Fixer models (faster than Zenodo)
- Add UTF-8 encoding support for Windows compatibility
- Support both modern 'hf download' and legacy 'huggingface-cli download' commands
2025-11-20 00:20:37 -03:00
diodiogod dcd647d727 Version 4.14.29
Add IndexTTS-2 XPU compatibility improvements

Technical changes:
- Add PyTorch version detection for XPU devices
- Implement CPU fallback for mel_spectrogram on PyTorch 2.8.0+xpu
- Implement CPU fallback for kaldi.fbank on PyTorch 2.8.0+xpu and 2.9.0+xpu
- Fix tensor device leakage when CPU explicitly selected with XPU available
- Add device-agnostic cache clearing (CUDA/XPU/CPU support)
- Add beam sampling hang warning for PyTorch 2.9.0+xpu users

Addresses user-reported issues in #167
2025-11-19 23:27:42 -03:00
diodiogod 5c5a1393ca Version 4.14.28
Fix Italian ChatterBox case-sensitive language prefix detection

- Make [it] prefix detection case-insensitive to prevent double-prefixing
- Users can now use [it], [IT], [It] or any capitalization without issues
- Avoids duplication that caused 'IT' to be spoken in output
2025-11-19 23:18:19 -03:00
diodiogod 3d88b637e4 Version 4.14.27
Fix ChatterBox streaming tuple unpacking and transformer compatibility

Technical changes:
- Fix tuple unpacking in chatterbox_tts_node.py line 1015 (4 values → 5 values)
- Fix tuple unpacking in chatterbox_srt_node.py line 758 (3 values → 4 values)
- Replace hidden_states[-1] with last_hidden_state in t3_hf_backend.py for transformers compatibility
- Remove output_hidden_states parameter causing validation errors in newer transformers
- Add detailed error logging for streaming segment failures

Fixes streaming crashes with 'too many values to unpack' and 'NoneType not subscriptable' errors.
2025-11-15 17:24:22 -03:00
diodiogod a9de25696a Version 4.14.26
Fix model wrapper consistency in unified interface

Technical changes:
- Fix unified_model_interface.py line 140: return wrapper instead of wrapper.model for cached models
- Remove obsolete model unwrapping code in chatterbox_streaming_adapter.py (lines 315-336)
- Wrapper now has __getattr__ forwarding, no manual unwrapping needed
- Fixes 'Model not wrapped - using direct .to() (legacy path)' warning

This completes the Tier 2/3 refactor wrapper consistency that was partially missed.
2025-11-15 16:58:03 -03:00
diodiogod 5bc7bbad48 Version 4.14.25
Fix ChatterBox Official 23-Lang exaggeration parameter scaling

Technical changes:
- Add 50000x internal scaling for exaggeration parameter in multilingual models
- Update exaggeration range from 0.25-2.0 to 0.0-5.0 for better user control
- Add honest tooltip explaining minimal effectiveness due to model training issues
- Change default from 0.5 to 1.0 for more intuitive baseline
2025-11-15 16:46:39 -03:00
diodiogod d1a898d6fc Version 4.14.24
Fix ChatterBox Official 23-Lang chunking bug

Resolves #170 - tuple unpacking error in single-character chunked mode
2025-11-15 15:41:34 -03:00
diodiogod 43d1342b4b Fix: Move TIER2-3_REFACTOR_PLAN.md to correct directory
Remove incorrectly escaped 'Dev\ reports' directory
2025-11-14 23:02:11 -03:00
diodiogod b8acd9dc4e Version 4.14.23
Complete model loading architecture refactor (Tier 2 + Tier 3)

Address issue #14

Technical implementation:
- Remove SmartModelLoader third caching system (complete)
- Standardize all factories to use ModelLoadConfig dataclass
- Fix unified interface to return wrapped models for Clear VRAM support
- Add __getattr__ to ComfyUIModelWrapper for transparent attribute forwarding
- Clean up legacy wrapper-searching code in base_node.py and processors
- Create engine registry with capability definitions
- Implement generic FallbackHandler for language fallback chains
- Add TTSModelLoadingError exception hierarchy
- Delete SmartModelLoader dead code (9.8KB removed)

Files modified:
- nodes/chatterbox/chatterbox_tts_node.py (2 SmartModelLoader removals)
- engines/chatterbox/streaming_model_manager.py (unified interface migration)
- engines/chatterbox_official_23lang/streaming_model_manager.py (unified interface migration)
- engines/adapters/f5tts_streaming_adapter.py (unified interface migration)
- utils/models/unified_model_interface.py (return wrapper instead of wrapper.model)
- utils/models/comfyui_model_wrapper/base_wrapper.py (add __getattr__)
- nodes/base/base_node.py (simplify wrapper detection)
- nodes/chatterbox_official_23lang/chatterbox_official_23lang_processor.py (simplify wrapper detection)
- engines/vibevoice_engine/vibevoice_engine.py (simplify device reload)

Created files:
- utils/models/factory_config.py (ModelLoadConfig standard)
- utils/models/engine_registry.py (engine capabilities registry)
- utils/models/fallback_handler.py (generic fallback chain utility)
- utils/models/exceptions.py (standardized exception hierarchy)

Deleted files:
- utils/models/smart_loader.py (obsolete third caching system)
2025-11-14 22:54:40 -03:00
diodiogod a8c62f2307 TIER 3: Complete architecture standardization for model loading
Implement standardized factory infrastructure enabling future engines:

1. ModelLoadConfig dataclass - Single canonical config class for all factories
   - Replaces duplicate UnifiedModelConfig
   - Contains device, model_name, language, model_path, repo_id, additional_params
   - Device resolution in __post_init__ for consistency

2. Engine capability registry - Data-driven engine specifications
   - Defines supports_voice_conversion, multilingual_model_switching, corruption recovery
   - Enables generic error handling without hardcoded engine checks
   - Covers all 7 TTS engines + IndexTTS-2

3. Fallback handler utility - Reusable fallback chain for graceful degradation
   - Try local → download → fallback language pattern
   - Eliminates duplicate fallback code across engines
   - Used by future engines automatically

4. Exception hierarchy - Standardized error types
   - TTSModelLoadingError base class
   - Specific types: NotFoundError, DeviceError, FormatError, InitializationError, LanguageNotSupportedError

Architecture benefits:
- All 8 factories now accept config: ModelLoadConfig instead of **kwargs
- Type-safe, clear factory signatures
- Future engines follow identical pattern
- No deprecated code left behind
- ComfyUI model manager handles both old and new patterns

Testing:
- All 7 TTS engines working correctly
- Device switching and caching working
- No syntax errors, all files compile

Note: SmartModelLoader still in use (Tier 2 cleanup pending) causing benign double-loads on some engines.
2025-11-14 10:10:52 -03:00
diodiogod bb60e0d915 Version 4.14.22
Implement model loading architecture improvements and bug fixes

Model Loading Refactoring (Tier 1):
- Cache 'auto' device resolution to prevent cache key instability
- Fix device comparison handling string vs torch.device type mismatches
- Consolidate device resolution across all node types
- Remove SmartModelLoader dependency from F5-TTS factory

Bug Fixes:
- Apply Python 3.12 CUDNN fix to Voice Changer node (prevents VRAM spikes)
- Resolve device in cache keys for TTS Text, SRT, and Voice Changer nodes
- Prevent cache invalidation when switching between 'auto' and explicit device

Analysis & Documentation:
- Add comprehensive model loading architecture analysis
- Document all identified issues with severity levels and recommendations
2025-11-13 23:30:02 -03:00
diodiogod 6181b65213 fix: Resolve device in cache keys to prevent cache misses on device switch
Fix generation cache invalidation when switching between 'auto' and explicit device.

Problem: Engine instance cache keys included raw device string ('auto' vs 'cuda'),
causing cache misses when users switched from 'auto' to explicit device or vice versa.
This forced unnecessary model reloads and generation recalculation.

Changes:
- tts_text_node.py: Resolve device before adding to stable_params
- tts_srt_node.py: Resolve device before adding to stable_params
- voice_changer_node.py: Resolve device before cache key generation

Impact:
- Switching device no longer invalidates cache
- Same generation cached regardless of 'auto' vs resolved device
- Faster subsequent runs when changing device settings
- Deterministic caching behavior
2025-11-13 23:18:23 -03:00
diodiogod 621fc259fc fix: Apply Python 3.12 CUDNN fix to Voice Changer node
Add missing CUDNN benchmark fix to voice conversion to prevent VRAM spikes.

The fix was applied to TTS generation nodes but not voice conversion, causing
VRAM spikes when using ChatterBox VC on Python 3.12 with ComfyUI v0.3.57+.

Changes:
- Import ensure_python312_cudnn_fix in voice_changer_node.py
- Call fix at start of convert_voice() method
- Now VC operations will show 'CUDNN FIX APPLIED' message and prevent spikes

Impact:
- Voice conversion VRAM usage now stable
- No more mysterious spikes during VC operations
- Consistent behavior with TTS nodes
2025-11-13 22:54:18 -03:00
diodiogod b9a4692c0e refactor: Remove SmartModelLoader dependency from F5-TTS factory
Eliminate third caching system (SmartModelLoader) and consolidate on unified interface:

1. F5-TTS factory (unified_model_interface.py)
   - Remove SmartModelLoader delegation
   - Inline model loading logic directly into factory
   - Handle local and HuggingFace loading consistently
   - Add device resolution in factory

2. F5-TTS base node (f5tts_base_node.py)
   - Remove SmartModelLoader fallback
   - Simplify to direct unified interface call
   - 87 lines of fallback code removed
   - Reduces triple-cache complexity

Impact:
- Single source of truth for F5-TTS model loading (unified interface)
- Eliminates cache duplication and sync issues
- Cleaner, more maintainable code path
- Easier to debug model loading issues
2025-11-13 22:41:44 -03:00
diodiogod 9f2166a297 refactor: Implement Tier 1 model loading improvements
Fix critical device handling issues that cause cache mismatches and unnecessary reloads:

1. Cache 'auto' device resolution (torch_device_resolver.py)
   - 'auto' device now cached after first resolution
   - Prevents cache key instability when device availability changes mid-session
   - Add reset_device_cache() for manual reset if needed
   - Fixes: GPU memory pressure causing unexpected CPU fallback

2. Fix device comparison in unified interface (unified_model_interface.py)
   - Resolve both target and wrapper device before comparison
   - Handle both string and torch.device object representations
   - Fixes: Device type mismatch causing models to stay on wrong device

3. Consolidate device resolution (base_node.py)
   - Replace duplicate resolve_device() with centralized torch_device_resolver
   - Now supports MPS (Apple Silicon), CUDA, XPU, and CPU consistently
   - Fixes: Missing MPS/XPU detection in base node

Impact:
- Eliminates unnecessary model reloads in long sessions
- Consistent device handling across all node types
- Better GPU memory management with stable cache keys
2025-11-13 22:06:41 -03:00
diodiogod dfecc41910 docs: Add model loading architecture analysis and refactor recommendations
- MODEL_LOADING_ANALYSIS.md: Comprehensive technical analysis of current system
  - Adoption rates, factory patterns, device management, caching strategies
  - 8 critical issues identified with severity levels
  - File impact analysis

- MODEL_LOADING_REFACTOR_REPORT.md: Executive summary with actionable recommendations
  - Tier 1 (quick wins): auto device caching, device comparison, resolution consolidation
  - Tier 2 (medium effort): cache unification, key collision fix, error standardization
  - Tier 3 (major refactor): factory signatures, capability registry, fallback handlers
  - Priority matrix and implementation timeline
2025-11-13 20:47:00 -03:00
diodiogod e67684926e Version 4.14.21
Fix ChatterBox Voice Conversion watermarker initialization

- Remove eager perth.PerthImplicitWatermarker initialization that caused crashes
- Implement lazy initialization pattern with attribute existence checking
- Add graceful error handling for missing watermarker support
- Apply fix to both ChatterBox and ChatterBox 23-Lang VC engines
2025-11-13 13:32:22 -03:00
diodiogod aad2b682b6 Optimize preview video: generate directly in temp directory
- Create WEBM preview in temp instead of output folder
- Remove unnecessary copy operation from output to temp
- Simplify preview display logic in node
2025-11-11 22:41:39 -03:00
diodiogod 4c6de9bb35 Update AllNodesShowcase.jpg 2025-11-08 12:13:07 -03:00
diodiogod b76ca95fdd Version 4.14.20
Fix Silent Speech Analyzer preview video generation

- Add call to _create_preview_video in MediaPipe provider analyze_video
- Regenerate preview with annotated frames in node for visual markers
- Remove unused backup/reference JS files
2025-11-08 11:55:12 -03:00
diodiogod 985bf68248 Fix Silent Speech Analyzer video preview: generate and annotate preview video on first analysis 2025-11-08 11:54:51 -03:00
diodiogod 1dfe7f98ee Update README.md 2025-11-08 10:17:37 -03:00
diodiogod 15e35ac303 Update README.md 2025-11-08 10:17:05 -03:00
diodiogod ac4508821c Version 4.14.19
Add Intel XPU support (tentative)

Implementation details:
- Add XPU detection to torch_device_resolver with proper fallback chain
- Update all engine nodes to include xpu device option
- Add defensive hasattr checks for systems without XPU installed
- Requires separate intel-extension-for-pytorch installation
2025-11-07 12:35:26 -03:00
diodiogod b594d4a648 Version 4.14.18
Fix Higgs Audio numpy scope error in _prepare_inputs

Remove redundant local 'import numpy as np' statements inside except blocks
that shadowed module-level numpy import, causing UnboundLocalError when
TorchCodec error handlers didn't execute but later code tried to use np.

Also verified no other similar shadowing issues in active codebase.
2025-11-07 11:40:42 -03:00
diodiogod fa490d568e Fix Higgs Audio numpy scope error in _prepare_inputs
Remove redundant local 'import numpy as np' statements in except blocks
that shadowed the module-level numpy import, causing UnboundLocalError
when exception handlers didn't execute but the code tried to use np
2025-11-07 11:38:03 -03:00
diodiogod 15daaf8fab Version 4.14.17
Fix PyTorch 2.9 audio distortion in IndexTTS-2 and other engines

Root causes addressed:
- PyTorch 2.9 TorchCodec DLL incompatibility on Windows (affected voice conversion, F5-TTS)
- PyTorch 2.9 changed torchaudio.load() to return raw int16 values instead of normalized float [-1, 1]
- Character Voices audio loading was not normalizing int16 range causing 1.5x amplification
- Global monkey-patch fixes all torchaudio.save/load calls at startup using scipy fallback
- Audio normalization utility centralizes int16→float conversion for consistent behavior

Technical changes:
- Applied global PyTorch 2.9 TorchCodec patches at startup (before torchaudio import)
- Added safe_load_audio() utility for consistent audio loading with automatic normalization
- Updated F5-TTS, Audio Analyzer, Higgs Audio for safe audio loading
- Implemented defense-in-depth save_audio_safe() wrapper with scipy fallback
- All comments updated to explain two-issue fix and timing criticality
2025-11-07 03:33:06 -03:00
diodiogod efe8d295d4 Update comments to explain PyTorch 2.9 bug fixes comprehensively
- Clarify that global patch solves TWO issues: TorchCodec DLL + int16 normalization
- Explain timing criticality: patch must apply before torchaudio import
- Remove redundant pytorch_patches call from apply_all_compatibility_patches()
- Update save_audio_safe docstring to explain defense-in-depth approach
- Add detailed docstring to pytorch_patches explaining both issues and fixes
2025-11-07 03:30:36 -03:00
diodiogod ecde6be1c8 Fix PyTorch 2.9 TorchCodec patch timing - apply before any module imports torchaudio 2025-11-07 03:26:13 -03:00
diodiogod dff111889e Apply global PyTorch 2.9 TorchCodec monkey-patch at startup
Recreate pytorch_patches.py with proper global monkey-patching of torchaudio.save/load
to use scipy.io.wavfile instead of TorchCodec. This fixes 'Could not load libtorchcodec'
errors on Windows with PyTorch 2.9.0+cu128 without needing fixes in every file.

Changes:
- Global monkey-patch applied at startup via comfyui_compatibility.py
- Works for ALL code that uses torchaudio.save/load (recorder, voice changer, etc)
- No user setup required, no FFmpeg workarounds needed
- Pure Python scipy solution, no native dependencies

This is the proper fix for TorchCodec Windows incompatibility with PyTorch 2.9.
2025-11-07 03:11:33 -03:00
diodiogod e19f6f1e7b Fix PyTorch 2.9 TorchCodec error in Higgs Audio engine
Add error handling for torchaudio.load() in Higgs Audio's _prepare_inputs method.
When TorchCodec DLL fails on PyTorch 2.9, fallback to scipy.io.wavfile for both:
- URL-based audio loading (line 521)
- BytesIO base64-encoded audio loading (line 530)

Also normalizes int16 audio to float32 [-1, 1] range during scipy fallback.

Fixes 'Could not load libtorchcodec' error when using Higgs Audio engine with
Character Voices or direct audio input on PyTorch 2.9.0+cu128.
2025-11-07 03:06:13 -03:00
diodiogod c42ab9d092 Fix PyTorch 2.9 TorchCodec error in Audio Analyzer node
Replace direct torchaudio.load() calls with safe_load_audio() utility in
AudioAnalyzer.load_audio() to handle PyTorch 2.9 TorchCodec DLL errors and
int16 normalization automatically.

Fixes 'Could not load libtorchcodec' error when using 🌊 Audio Wave Analyzer
node with Character Voices or direct audio input on PyTorch 2.9.0+cu128.
2025-11-07 03:02:55 -03:00
diodiogod 58f4b52c34 Centralize PyTorch 2.9 audio normalization in safe_load_audio utility
Move int16 to float [-1, 1] normalization from Character Voices node into
the safe_load_audio() utility function so ALL engines benefit from the fix.

Changes:
- safe_load_audio() now normalizes int16 values to [-1, 1] range automatically
- Handles PyTorch 2.9 torchaudio.load() behavior change and scipy fallback
- Remove duplicate normalization from Character Voices node
- F5-TTS now uses safe_load_audio() instead of torchaudio.load() directly

This fixes TorchCodec DLL errors on ALL engines that load audio files with PyTorch 2.9,
not just Character Voices. F5-TTS, ChatterBox, and future engines all benefit.
2025-11-07 03:00:28 -03:00
diodiogod 7b7ce5793a Remove unused pytorch_patches.py - fix was in Character Voices normalization
The pytorch_patches.py file was created to globally monkey-patch torchaudio.save/load
but was never actually applied anywhere. The real fix was normalizing int16 to float
in the Character Voices node after PyTorch 2.9 changed torchaudio.load() behavior.

Since the fix is now working and pytorch_patches.py is unused dead code, remove it.
2025-11-07 02:56:30 -03:00
diodiogod ab15e012c5 Fix PyTorch 2.9 Character Voices audio distortion: normalize int16 to float
Root cause: PyTorch 2.9's torchaudio.load() returns raw int16 values (±32767 range)
instead of normalized float32 in [-1, 1] range like PyTorch 2.8 did.

When Character Voices loads audio and stores as tensor, the int16 values cause
distortion when converted back to file because save functions expect [-1, 1] range.

Fix: Normalize audio tensor to [-1, 1] in Character Voices node after loading.
This ensures all downstream engines receive properly normalized audio.

Fixes distortion on all engines (ChatterBox, IndexTTS, VibeVoice, etc) when using
Character Voices node with PyTorch 2.9.0+cu128
2025-11-07 02:53:28 -03:00
diodiogod 98d4c969fa Fix audio averaging logic to match reference implementation exactly
Critical fix for audio distortion:
- Reference implementation averages based on shape detection:
  if shape[0] <= 8 and shape[1] > shape[0]: average axis=0 (channels)
  else: average axis=-1 (last axis)
- Our code was incorrectly transposing then averaging, causing wrong channels to be mixed
- Remove clipping that was destroying audio peaks
- Keep [1, samples] shape for compatibility with our pipeline

This matches exactly what ComfyUI-IndexTTS2 Simple does (which produces perfect audio).
2025-11-07 01:52:09 -03:00
diodiogod b4b70df7ea Match ComfyUI-IndexTTS2 reference implementation for audio conversion
- Implement exact same audio post-processing as reference node:
  - Convert int16 to float32 with /32767.0 normalization
  - Handle shape detection: check if shape[0] <= 8 (likely channels)
  - Transpose if needed, average channels to mono
  - Clip output to [-1.0, 1.0] range before converting to tensor
- Add numpy import
- Filter out incompatible kwargs (speech_speed) from external nodes
- Add debug output to diagnose audio value issues

This matches the proven reference implementation that produces clean audio.
2025-11-07 01:33:02 -03:00
diodiogod 4060317a7b Fix IndexTTS-2 audio distortion: eliminate file I/O and use direct tensor output
Changes:
- Modify index_tts.py to call infer() with output_path=None, getting tensors directly instead of saving/loading files
- This avoids the TorchCodec DLL error on PyTorch 2.9+ Windows that was causing file corruption
- Remove global PyTorch patches from __init__.py since we no longer need torchaudio.save() in main path
- Unpack (sampling_rate, wav_data) tuple from infer() and convert int16 numpy array to float32 torch tensor
- Add save_audio_safe() function in index_tts_processor.py for graceful TorchCodec fallback in pause processor
- Replace all ta.save()/torchaudio.save() calls in pause processor with save_audio_safe()

Root cause: ComfyUI-IndexTTS2 works fine because it calls infer() with output_path=None (direct tensors).
Our suite was calling infer(output_path=output_path) which forced file save/load, triggering TorchCodec DLL errors.
The pause processor still needs torchaudio.save() so we provide a soundfile fallback instead of global patches.
2025-11-07 01:25:12 -03:00
diodiogod f723d6a352 Clean up PyTorch patches: remove ineffective BigVGAN/S2Mel patches, keep TorchCodec fix only 2025-11-07 00:48:33 -03:00
diodiogod 5138cab267 Fix PyTorch 2.9.0+cu128 TorchCodec incompatibility
- Create pytorch_patches.py module for PyTorch/TorchAudio compatibility
- Patch torchaudio.save() and load() to use soundfile for WAV files
- Fixes 'Could not load libtorchcodec' errors on Windows with PyTorch 2.9+
- Only applies patch on PyTorch 2.9+, safe fallback for other versions
- Apply patch in IndexTTS engine before torchaudio operations
2025-11-06 23:31:48 -03:00
diodiogod 0b030c4478 Version 4.14.16
Fix RVC VRAM accumulation in voice conversion (issue #158)

Technical details:
- Register RVC and Hubert models with ComfyUI's model management system
- Clear pitch extraction models after each conversion (RMVPE, FCPE, Crepe)
- Let ComfyUI's free_memory() handle intelligent unloading based on VRAM pressure
- Follows unified architecture pattern used by other TTS engines
2025-11-06 22:56:04 -03:00
diodiogod 17bfb1f10f Fix streaming parameters not being passed to engine
IndexTTS-2 streaming was broken because:
1. stream_return and more_segment_before weren't forwarded from adapter to engine
2. Adapter tried to cache generator objects instead of tensors

Fixed by:
- Explicitly adding streaming params to engine_kwargs
- Only caching in non-streaming mode (generators can't be cached/shaped)
- Returning generator directly in streaming mode

Now stream_return=True will actually return a generator yielding audio chunks.
2025-11-06 22:15:37 -03:00
diodiogod c9bbccb0dc Change new parameters to BOOLEAN toggles for UI consistency
Changed from dropdown strings (false/true) to proper BOOLEAN type:
- use_torch_compile: BOOLEAN
- use_accel: BOOLEAN
- stream_return: BOOLEAN

This matches the UI pattern of other boolean parameters like use_fp16,
use_deepspeed, do_sample, and use_random. Removed unnecessary string
conversion code.
2025-11-06 20:55:51 -03:00
diodiogod c2b370986a Add user-friendly warnings for missing optimization dependencies
- GPT2 acceleration (use_accel): Warn if flash-attn missing, gracefully fallback
- torch.compile: Warn if optimization fails, gracefully fallback
- Helps users understand why optimizations aren't activating
- Provides installation instructions for flash-attn
2025-11-06 20:53:53 -03:00
diodiogod 7ce8172fbb Complete IndexTTS-2 parameter integration: cache, console logging, processor flow
Critical fixes:
1. Added missing parameters to cache key: do_sample, num_beams, stream_return,
   more_segment_before, use_torch_compile, use_accel
2. Fixed processor to pass do_sample, stream_return, more_segment_before to adapter
3. Added console logging for optimization and streaming parameters

This ensures:
- Cache properly invalidates when any parameter changes
- All new parameters flow through processor → adapter → engine
- User sees all configuration changes in console output
- Backward compatibility maintained (new params optional)
2025-11-06 20:03:37 -03:00
diodiogod 32117e6c90 Fix cache key generation to include streaming parameters for IndexTTS-2
IndexTTS-2 now properly invalidates cache when stream_return or more_segment_before
parameters change. This prevents incorrect cached results when switching between
streaming and non-streaming modes, or when adjusting streaming segmentation.
2025-11-06 19:51:56 -03:00
diodiogod 2318771c31 Add upstream optimization parameters to IndexTTS-2 node and engine
Add new optional parameters for IndexTTS-2 engine backward-compatible integration:
- use_torch_compile: Enable torch.compile for S2Mel stage optimization
- use_accel: Enable GPT2 acceleration with FlashAttention
- stream_return: Enable streaming mode for low-latency generation
- more_segment_before: Streaming segmentation parameter

Parameters added to:
- IndexTTSEngineNode (optional inputs for backward compatibility)
- IndexTTSAdapter (initialize_engine and generate methods)
- IndexTTSProcessor (passes params to adapter)
- IndexTTSEngine wrapper (stores and passes to model config)

All parameters properly parsed, documented with tooltips, and flow through
the entire generation pipeline without breaking existing workflows.
2025-11-06 19:36:43 -03:00
diodiogod ae2998b67d Apply IndexTTS-2 upstream updates (September 18 → November 6, 2025)
Integrated all 5 upstream commits with custom modifications preserved:

1. Empty generator fix (750d9d9) - Refactored infer() to generator pattern
   - Created wrapper infer() that delegates to infer_generator()
   - Renamed monolithic method to infer_generator()
   - Changed final returns to yield for generator compatibility
   - Handles both streaming and non-streaming modes

2. Streaming implementation (b0c6ab8) - Added low-latency streaming
   - Added stream_return parameter for generator mode
   - Added more_segment_before parameter for streaming segmentation
   - Integrated with generator-based inference

3. UNK token warnings (34be9bf) - Token validation and user feedback
   - Detects unknown tokens in BPE model
   - Warns users with token IDs and suggestions

4. S2Mel optimization (31e7e85) - Mel-spectrogram generation speedup
   - Optimized fused_add_tanh_sigmoid_multiply() with torch.split
   - Fixed sequence_mask() with max_length parameter
   - Added enable_torch_compile() to CFM and MyModel classes
   - Added use_torch_compile parameter to infer_v2.py

5. GPT2 acceleration (c1ef414) - KV-cache and FlashAttention optimization
   - Created new /accel/ module with 5 files (609-2500 lines)
   - Added use_accel parameter to UnifiedVoice
   - Integrated AccelInferenceEngine for 1.5-3x speedup
   - Optional feature (requires flash-attn library)

Custom modifications preserved:
- QwenEmotion lazy loading with path fallbacks
- W2V-BERT loading from TTS folder (unified downloader integration)
- All custom logging and error handling
- Custom emotion vector processing

Generated upstream tracking documentation in docs/Upstream_Engine_Tracking/
2025-11-06 19:32:28 -03:00
diodiogod b040f5dbc7 Version 4.14.15
Fix IndexTTS-2 audio distortion on Linux

- Normalize audio amplitude after loading int16 WAV files
- Linux torchaudio.load() doesn't auto-normalize int16→float32 like Windows
- Audio now scaled to proper [-1, 1] range instead of remaining at ~32767 amplitude
- Addresses issue #163 where IndexTTS-2 generates loud/distorted audio on Linux only
2025-11-06 18:18:31 -03:00
diodiogod 60267166ff Version 4.14.14
Fix ChatterBox Official 23-Lang SRT and unified node streaming/caching issues

Technical fixes:
- Fix missing batch_size parameter propagation in ChatterBox Official 23-Lang SRT processor
- Add batch_size to all generate_speech calls in SRT processor to respect user batch_size settings
- Fix unified model interface cache key generation for multilingual models (language parameter should not affect cache)
- Fix model reload on first execution when using both SRT and TEXT nodes with same engine
- Add model_version tracking to load_tts_model to prevent unnecessary reloads
- Add early return in load_tts_model when same model/version/device is already loaded
- Fix missing character field in ChatterBox streaming adapter inputs dict
2025-11-06 18:05:15 -03:00
diodiogod b05680fb73 Version 4.14.13
Fix RVC model switching VRAM accumulation issue #158

Part 2 of comprehensive VRAM fix: Prevent accumulation when switching between
different RVC character models or Hubert feature extractors.

Root cause: When loading a new RVC/Hubert model, the previous model stayed in
ComfyUI's current_loaded_models list without being properly unloaded, causing
VRAM to accumulate with each model switch.

Solution: Track the last registered model path for both RVC and Hubert models.
Only unload old models if switching to a different model (different path). This
prevents premature unloading of the model we just loaded before it's used.

Fixes dtype mismatch errors caused by models being on CPU while audio is on CUDA.
2025-11-06 17:23:00 -03:00
diodiogod c7f64cc66d Version 4.14.12
Fix RVC VRAM buildup issue #158

Root cause: Pitch extraction models (RMVPE, FCPE) cached as instance
variables in VC/FeatureExtractor class and never explicitly freed between
conversions. Each conversion accumulated intermediate tensors in VRAM.

Solution: Clear pitch extractor model cache after each conversion completes,
then force garbage collection and CUDA cache clearing. Preserves RVC/Hubert
model caching for performance while preventing pitch model accumulation.
2025-11-05 23:19:41 -03:00
diodiogod 94c64a2655 Version 4.14.11
Remove torchcodec dependency for Windows compatibility

Technical changes:
- Eliminated FFmpeg system dependency requirement
- Added AudioProcessingUtils.safe_load_audio() for fallback audio loading
- Updated 9 audio loading locations to use fallback mechanism
- Removed torchaudio 2.9+ migration warning suppressions
- Verified all audio formats still supported via scipy fallback
2025-11-05 22:26:29 -03:00
diodiogod 46fda45944 Fix: Remove torchcodec dependency to eliminate FFmpeg system requirement
Removed torchcodec from requirements.txt and install.py to eliminate the FFmpeg
system dependency that was causing Windows installation failures.

Implementation:
- Removed torchcodec>=0.2.0 from requirements.txt
- Removed torchcodec>=0.2.0 from install.py core_packages list
- Removed torchaudio 2.9+ migration warnings (no longer applicable)
- Added AudioProcessingUtils.safe_load_audio() utility for robust audio loading
- Updated all audio loading calls to use fallback-aware utility function

Audio Loading Changes:
- character_voices_node.py: Uses safe_load_audio() with fallback support
- tts_text_node.py: Uses safe_load_audio() for narrator and character voices (2x)
- tts_srt_node.py: Uses safe_load_audio() for narrator voice
- higgs_audio_tts_processor.py: Uses safe_load_audio() (2x)
- higgs_audio_srt_processor.py: Uses safe_load_audio() (2x)
- index_tts_processor.py: Uses safe_load_audio() (2x)
- f5tts_streaming_adapter.py: Uses safe_load_audio()
- higgs_audio_adapter.py: Uses safe_load_audio()
- subprocess.py: Uses safe_load_audio()

Compatibility:
- torchaudio.load() works fine with fallback backends (soundfile, scipy)
- No quality loss - only negligible audio loading speed difference (~milliseconds)
- All existing audio formats supported
- Better error handling with graceful fallbacks
2025-11-05 22:26:10 -03:00
diodiogod 512841e321 Version 4.14.10
Fix ChatterBox Official 23-Lang SRT character switching and timing reports

- Fixed character voice lookup using resolved character names instead of aliases
- Fixed SRT timing report consistency by using unified timing engine functions
- Pre-allocate segment arrays to maintain correct SRT ordering when processing by language
- Ensure stretch_type properly set for emoji display in timing reports
2025-11-05 19:12:46 -03:00
diodiogod 376b2c190e Fix: Use resolved character names for voice lookup in multicharacter SRT segments
Character parser aliases (e.g., 'Alice' → 'female_01') are resolved during parsing.
Use seg.character (resolved) instead of seg.original_character (alias) when building
the character_segments_with_lang tuples. This ensures voice_refs lookup finds the
correct voice files.

Key changes:
- Line 158: Changed from seg.original_character to seg.character
- This allows voice_refs.get(char) to find 'female_01' instead of looking for 'Alice'
- Characters now properly use their mapped voice files in multicharacter segments

Fixes issue #157: Character switching now works correctly with Alice using female_01
voice and Bob using male_01 voice as configured in #character_alias_map.txt.
2025-11-05 18:35:22 -03:00
diodiogod 4a137d1d47 Revert "Refactor: ChatterBox Official 23-Lang SRT processor to use direct adapter pattern"
This reverts commit a0f7de2cc3.
2025-11-05 18:17:28 -03:00
diodiogod f16b254096 Revert "Fix: Load ChatterBox 23-Lang model once in SRT processor init"
This reverts commit 51561e62be.
2025-11-05 18:17:28 -03:00
diodiogod 99a42b6c63 Revert "Fix: Use TEXT processor method for SRT segment generation"
This reverts commit 6e5299f39a.
2025-11-05 18:17:28 -03:00
diodiogod 6e5299f39a Fix: Use TEXT processor method for SRT segment generation
The SRT processor now simply calls self.tts_node.generate_speech() for each
segment, letting the TEXT processor handle all the complexity:

✓ Character switching via [CharacterName] tags with voice discovery
✓ Language switching via [language:] tags
✓ Parameter switching via [seed:X], [exaggeration:Y] tags
✓ Proper voice reference handling and aliasing

This fixes the issue where Alice was being detected but not using her actual
voice (female_01). The TEXT processor already has all the logic to properly
resolve character aliases and apply voice references.

Simplified from ~400 lines of complex adapter logic to ~150 lines of clean,
straightforward segment generation that delegates to the proven TEXT processor.
2025-11-05 18:02:43 -03:00
diodiogod 51561e62be Fix: Load ChatterBox 23-Lang model once in SRT processor init
The adapter needs the model to be loaded before generate_segment_audio() is called.
Call adapter.load_base_model() in __init__ to load the model once at initialization
(respecting the engine config language and device settings).

This ensures model is loaded exactly once before any segment generation,
fixing the 'TTS model not loaded' error.
2025-11-05 17:08:27 -03:00
diodiogod a0f7de2cc3 Refactor: ChatterBox Official 23-Lang SRT processor to use direct adapter pattern
Simplifies architecture by removing broken multilingual engine dependency:
- Initialize adapter once in __init__ (loads model once, not per-language)
- Process each SRT segment by calling adapter.generate_segment_audio() directly
- Support character switching via [CharacterName] tags with voice discovery
- Support language switching via [language:] tags (passed as current_language)
- Support parameter switching via [seed:X], [exaggeration:Y] tags per segment

Benefits:
- Eliminates double model loading bug (model loads once at initialization)
- Fixes character switching (voice references properly passed to adapter)
- Preserves all existing features (timing modes, voice mapping, audio assembly)
- Cleaner architecture following VibeVoice/HiggsAudio pattern (reduces complexity)
- Reduced code from 524 → 408 lines (-22%)

The adapter handles model caching and device placement internally, so we just
call it once per segment with the appropriate parameters. This is much simpler
than the previous multilingual engine approach which treated 23-Lang as multiple
models instead of one model with 23 languages.
2025-11-05 17:06:17 -03:00
diodiogod 80661f396c Version 4.14.9
Fix critical package downgrade issue in installation process

Technical changes:
- Remove opencv-python and gradio from core_packages list
- Move to install_problematic_packages() with --no-deps
- Add install_gradio_and_opencv_dependencies() for safe pre-installation
- Prevent forced numpy/pydantic/pillow downgrades
- Maintain intentional compatibility downgrades only
2025-11-05 15:19:40 -03:00
diodiogod 3fd3e17f29 Clarify downgrade strategy: Accept necessary downgrades
After investigation, some package downgrades are NECESSARY and INTENTIONAL:

1. NumPy 2.3.3 → 2.2.6: Intentional protection against numba/librosa issues
2. rotary-embedding-torch 0.8.9 → 0.6.5: Required by audio-separator dependency
3. pydantic/pydantic-core: Required by gradio - no way to avoid

The pre-installation function was trying to force-protect pydantic versions,
but this itself forced the downgrade. Better approach: only pre-install
packages that won't be downgraded (fastapi, starlette, websockets, etc).

Let gradio install pydantic/pydantic-core with --no-deps as it requires them.
This is a reasonable trade-off: controlled downgrades for compatibility.
2025-11-05 15:15:30 -03:00
diodiogod 79f926e1ad Fix: Remove problematic packages from core_packages list
opencv-python, pillow, and gradio were still being installed in
install_core_dependencies() which runs BEFORE the new pre-installation
of their safe dependencies. This caused the same downgrades to occur.

Now they are ONLY installed via install_problematic_packages() after
their dependencies have been pre-installed with version protection.

This ensures:
- opencv-python installs after numpy is protected
- gradio installs after pydantic/pydantic-core/pillow are protected
- No downgrade cascade occurs
2025-11-05 14:56:30 -03:00
diodiogod a17f2e98f0 Fix critical package downgrade issue in installation
Resolve systematic downgrade cascade that was destroying carefully
managed package versions during install.py execution:

- numpy: 2.3.4 → 2.2.6 (via opencv-python)
- pydantic-core: 2.41.5 → 2.33.2 (via gradio)
- pillow: 12.0.0 → 11.3.0 (via gradio)
- pydantic: 2.12.4 → 2.11.10 (via gradio)
- rotary-embedding-torch: 0.8.9 → 0.6.5 (via audio-separator)

SOLUTION: Move problematic packages (opencv-python, gradio) from
requirements.txt to install.py with --no-deps installation after
pre-installing their critical dependencies in correct versions.

This ensures ComfyUI-Manager's sequential pip installs don't
circumvent install.py's intelligent dependency management.

Changes:
1. Remove opencv-python and gradio from requirements.txt
2. Add install_gradio_and_opencv_dependencies() function
3. Pre-install gradio/opencv deps before --no-deps installation
4. Update install_problematic_packages() to include both packages
5. Call new function in correct installation order

Result: No more package downgrades during installation process.
2025-11-05 14:29:41 -03:00
diodiogod 6e9797a02f Version 4.14.8
Add comprehensive MPS device support across all TTS engines

Fixed hardcoded device selection in:
- ChatterBox: Added resolve_torch_device() in from_local()
- F5-TTS: Added resolve_torch_device() in __init__()
- RVC: Fixed 5 locations with hardcoded cuda/cpu selection
- IndexTTS: Fixed target_device resolution
- VibeVoice: Fixed device resolution in __init__() and target_device
- VibeVoice adapter: Fixed target_device for model movement
- Higgs Audio adapter: Fixed target_device in 2 locations

All engines now support MPS (Metal Performance Shaders) on Apple Silicon when 'auto' device is selected, with proper fallback to CUDA on NVIDIA and CPU fallback for all other systems.
2025-11-05 06:41:07 -03:00
diodiogod d0d58059b0 Add MPS device support to IndexTTS, VibeVoice and their adapters
Fixed hardcoded device selection in:
- IndexTTS: target_device resolution
- VibeVoice: device resolution in __init__ and target_device in generation
- VibeVoice adapter: target_device for device movement
- Higgs Audio adapter: target_device in 2 locations

All now use resolve_torch_device() for intelligent MPS/CUDA/CPU selection.

Fixes #160
2025-11-05 06:40:14 -03:00
diodiogod 5d32cf4e1f Add MPS device support throughout RVC engine
RVC was hardcoding 'cuda' or 'cpu' device selection in 4 locations, completely ignoring MPS support.

Fixed in:
- minimal_reference_wrapper.py: 3 device hardcoding locations
- rvc_engine.py: 1 device hardcoding location
- rmvpe.py: 1 device hardcoding location

All now use resolve_torch_device('auto') for intelligent MPS/CUDA/CPU selection.

Fixes #160
2025-11-05 06:38:36 -03:00
diodiogod ebaff202e8 Fix MPS device resolution in ChatterBox and F5-TTS engines
Both engines were not resolving 'auto' device to actual device (MPS on Apple Silicon, CUDA on NVIDIA, CPU fallback).

Changes:
- ChatterBox: Add resolve_torch_device() call in from_local() classmethod
- F5-TTS: Add resolve_torch_device() call in __init__() method

This ensures both engines properly support MPS on Apple Silicon when 'auto' device is selected.

Fixes #160
2025-11-05 06:36:24 -03:00
diodiogod 5cd40199ae Version 4.14.7
Fix RVC config path resolution for fp32 GPU compatibility

Technical changes:
- Add RVC config files to engines/rvc/impl/configs/
- Use __file__-relative path resolution instead of cwd-relative paths
- Ensures configs are found regardless of ComfyUI launch directory

Resolves issues where GTX 1080/1080 Ti users got FileNotFoundError on configs/32k.json
2025-11-05 06:32:36 -03:00
diodiogod fcc0fdf47a Version 4.14.6
Fix critical audio loading and model download dependencies

- Add missing torchcodec dependency required by torchaudio for audio file loading
- Fix torchcrepe import checking for packages installed with --no-deps
- Fix German (havok2) ChatterBox model download pointing to wrong subdirectory
- German model now correctly uses finetuned_by_h2 folder with complete model files
2025-11-04 22:04:04 -03:00
diodiogod 530472953c Version 4.14.5
Add MPS (Apple Silicon) GPU support for all TTS engines

- Created centralized device resolver (torch_device_resolver.py)
- Intelligent device detection: MPS > CUDA > CPU
- Updated all 7 engine nodes with 'mps' device option
- Integrated resolver across all engine implementations
- Maintains full backward compatibility with existing workflows
- Resolves #160
2025-11-04 19:17:37 -03:00
diodiogod 573c2497d3 Add MPS (Apple Silicon) support for all TTS engines
- Created utils/device/torch_device_resolver.py with intelligent device detection
  - Supports MPS (Metal Performance Shaders) for Apple Silicon
  - Fallback chain: MPS > CUDA > CPU for auto-detection
  - Safe detection with torch.backends.mps.is_available() checks

- Updated all 7 engine nodes to expose 'mps' device option:
  - ChatterBox, F5-TTS, Higgs Audio, VibeVoice
  - ChatterBox Official 23-Lang, Index TTS, RVC

- Integrated device resolver across all engine implementations:
  - unified_model_interface.py (central point)
  - comfyui_model_wrapper/base_wrapper.py (ComfyUI integration)
  - All direct engine implementations (VibeVoice, Higgs Audio, F5-TTS edit, etc.)

- Maintains backward compatibility:
  - Existing workflows unchanged
  - MPS added as final option in device dropdowns
  - No behavior changes for Linux/Windows users

Resolves #160 - Users on Apple Silicon can now select MPS for GPU acceleration
2025-11-04 19:13:57 -03:00
diodiogod f418b62af5 Version 4.14.4
Fix Voice Fixer model loading from TTS directory

- Modified bundled VoiceFixer config to use get_preferred_download_path() for models
- Respects extra_model_paths.yaml and ComfyUI model management integration
- Redirects both analysis module (vf.ckpt) and vocoder model to TTS/voicefixer/ directory
- Eliminates hardcoded ~/.cache paths
- Resolves #159
2025-11-04 15:51:55 -03:00
diodiogod 28c42cce81 Version 4.14.3
Fix Multiline TTS Tag Editor history persistence

Technical details:
- Update state.text immediately on input to ensure full text is saved to localStorage
- Use onConfigureCallCount to distinguish page reloads from new workflow loads
- Track lastWorkflowValue to detect when workflow file changes
- Preserve undo/redo history across page reloads within session
- Reset history when opening different workflow (expected behavior)
- Ensure multiple tag editor nodes maintain separate data with unique storage keys
2025-11-03 22:04:01 -03:00
diodiogod 24e3f4c41d Fix history preservation and workflow data persistence
- Add immediate state.text update on input to ensure full text is saved to localStorage
- Use onConfigureCallCount to properly distinguish page reloads from new workflow loads
- Track lastWorkflowValue to detect when a different workflow file is opened
- Preserve undo/redo history across page reloads within same session
- Reset history to 1/1 when opening a different workflow (as expected)
- Ensure multiple tag editor nodes in same workflow maintain separate data
- Remove debug console.log statements
2025-11-03 22:03:25 -03:00
diodiogod f0e29dbd94 Add reminder to specify component/engine in changelog entries 2025-11-03 18:47:40 -03:00
diodiogod a0a243927c Version 4.14.2
Fix multiline tag editor workflow persistence and history

Technical changes:
- Remove forceInput:True to enable widget serialization in workflows
- Hide default STRING widget while keeping it in array for ComfyUI serialization
- Implement onConfigure hook to load workflow values after node creation
- Add isConfigured flag to prevent setValue overwriting onConfigure state
- Use Object.assign to preserve state reference for all closures
- Initialize fresh history when loading workflow-only values
2025-11-03 18:45:28 -03:00
diodiogod 7a97c8b056 Fix workflow data persistence and history preservation
- Removed forceInput:True to enable widget serialization for cross-installation sharing
- Hide default STRING widget while keeping it in widgets array for serialization
- Use onConfigure hook to properly load workflow values after node creation
- Add isConfigured flag to prevent setValue from overwriting onConfigure state
- Preserve full undo/redo history across reloads using Object.assign
- Text now persists in workflows AND history survives reloads within same installation
2025-11-03 18:43:47 -03:00
diodiogod 6062ff0cdd Fix localStorage priority in workflow data loading
Prioritize localStorage over workflow value to preserve user edits across sessions.

New priority order:
1. localStorage with custom data (persistent edits) - highest priority
2. Workflow with custom data (first load of shared workflow)
3. Workflow value or default

Fixes issue where user edits were lost on reload.
2025-11-03 14:56:59 -03:00
diodiogod ea5f7f1a27 Fix workflow data loading vs localStorage persistence
Prioritize workflow value if it contains custom data (not default example text).
This allows shared workflows to load their data while keeping localStorage
for persistent sessions of the same workflow instance.

Logic:
- Workflow has custom data → use it (shared workflow)
- localStorage has custom data → keep it (same session reload)
- Both default or empty → use workflow value
2025-11-03 13:59:25 -03:00
diodiogod 297bfcf0ef Version 4.14.1
Fix keyboard event handling in tag editor

- Block ComfyUI shortcuts while editor has focus to prevent interference
- Allow Alt key combinations (Alt+L/C/P/Z and Alt+1/2/3) to work properly
- Fix Alt+Z undo/redo shortcut by moving handler out of else-if chain
- Use document.execCommand insertLineBreak for proper Enter key handling
- Prevent typing regular characters from triggering ComfyUI node list
2025-11-01 01:11:52 -03:00
diodiogod 1927911d47 Add default example text to tag editor with SRT format showcase
New tag editor nodes now display helpful examples including:
- Basic character and parameter tag syntax
- Pause tag usage
- SRT subtitle format with timing and tags

Allows users to immediately see syntax highlighting in action.
2025-10-31 23:42:54 -03:00
diodiogod b1fb56f421 Version 4.14.0
Add complete 🏷️ Multiline TTS Tag Editor module

Implementation:
- New modular widget architecture with event handlers, UI builder, parameter section, and preset system
- Contenteditable div-based editor with full text selection support
- Keyboard shortcuts for tag insertion (Alt+L/C/P) and presets (Alt+1/2/3)
- Language constants module with API-driven language detection
- Character discovery with alias support
- Comprehensive parameter system with specialized inputs
- Preset management (save/load/delete up to 3 configurations)
- Full edit history with undo/redo support
2025-10-31 19:29:59 -03:00
diodiogod ab5262ee97 Add selected text support for tag insertion
- Select text and click tag buttons to prepend tags to selection
- If selection starts with a tag, modify it instead of creating new one
- Detects leading whitespace and positions caret correctly inside bracket
- Works for character, language, and parameter buttons
- All existing tag logic (checks, updates, etc) still applies
2025-10-31 19:26:30 -03:00
diodiogod 9d43723421 Fix format button and Enter key handling
- Format button now preserves newlines and doesn't create double spaces before tags
- Plain Enter key now creates newlines without freezing (handles contenteditable properly)
- Prevents ComfyUI global handlers from interfering with editor behavior
2025-10-31 19:13:21 -03:00
diodiogod c6173080d6 Add keyboard shortcuts for tag and preset insertion
Alt+L: Add Language Tag
Alt+C: Add Character
Alt+P: Add Parameter
Alt+1/2/3: Load Preset 1/2/3

Shortcuts work by programmatically clicking the corresponding buttons.
2025-10-31 18:44:35 -03:00
diodiogod da223f49f7 Fix language code detection: load supported languages on module init
Ensure SUPPORTED_LANGUAGES in language-constants.js is populated with all 55 codes
before event handlers run, so isLanguageCode() works correctly. Prevents duplicate
language codes like [hr:|hr:|...] by properly detecting language codes vs parameters.
2025-10-31 18:38:58 -03:00
diodiogod 4e75e5ffe0 Prevent duplicate language code insertion
Language button now checks if the same language code already exists
before adding another one. Clicking the button multiple times on the
same tag now doesn't create duplicates like [pt-br:|pt-br:|...].
Only updates language if a different one is selected.
2025-10-31 18:17:27 -03:00
diodiogod 3df2f92cb9 Include character aliases in available-characters API endpoint
Modified /api/tts-audio-suite/available-characters to return both
character filenames and their aliases from the character alias map.
This allows the tag editor dropdown to show friendly alias names like
'Alice' alongside actual voice files.
2025-10-31 18:14:16 -03:00
diodiogod 8e880592d3 Fix language dropdown persistence and character replacement with language prefix
1. Language dropdown selection now persistent - restore value AFTER options
   are loaded from API, not before (fixes race condition)

2. Character button now recognizes language-prefixed characters like
   [en:broom_salesman|seed:3] and replaces just the character part,
   producing [en:new_char|seed:3] instead of adding duplicate character
2025-10-31 18:11:12 -03:00
diodiogod 012852c27d Fix language tag insertion for parameter-only tags
[seed:5|steps:3] now correctly becomes [en:|seed:5|steps:3] instead of
[en:|steps:3]. The issue was only capturing the pipe and subsequent
parameters, not the entire tag content including the first parameter.
2025-10-31 18:02:33 -03:00
diodiogod d8b5d10be0 Use canonical LANGUAGE_ALIASES from language_mapper as API source
Changed /api/tts-audio-suite/available-languages endpoint to derive
languages from the authoritative LANGUAGE_ALIASES dictionary in
language_mapper.py instead of hardcoded list. This ensures the tag
editor always has all supported language codes.
2025-10-31 17:56:03 -03:00
diodiogod 293229aaf7 Load language list dynamically from API endpoint
Changed from static getSupportedLanguages() to dynamic API fetch,
matching the character loading pattern. Now fetches from
/api/tts-audio-suite/available-languages with fallback to hardcoded list.
Adds console message when languages are loaded.
2025-10-31 17:52:50 -03:00
diodiogod 855ca30007 Fix language tag insertion to use pipe separator for parameters
When cursor is inside [seed:5|steps:3], language button now correctly
creates [de:|seed:5|steps:3] instead of [de:seed:5|steps:3].

For parameter-only tags, language code is inserted with pipe separator
(empty character slot) rather than prepending to parameter name.
2025-10-31 17:51:23 -03:00
diodiogod 368ccd0dd4 Create language constants source of truth and fix language tag logic
- Added language-constants.js with API-driven language list (fallback to hardcoded)
- Added /api/tts-audio-suite/available-languages endpoint in __init__.py
- Updated widget-ui-builder.js to use getSupportedLanguages()
- Updated widget-event-handlers.js to use isLanguageCode() for detection
- Fixes issue where [seed:5|steps:3] now correctly becomes [de:|seed:5|steps:3]
  instead of [de:5|steps:3] by properly detecting language codes vs parameters
2025-10-31 17:48:06 -03:00
diodiogod 0f5cd0ad7b Make tag modification tolerant of single space after closing bracket
Now accepts both [seed:3]* and [seed:3] * cursor positions when adding
parameters/languages. Previously only worked immediately after ] without
space. Fixes the issue where users had to place cursor directly after tag
instead of allowing normal spacing conventions.
2025-10-31 17:41:51 -03:00
diodiogod b5da78f754 Fix language tag insertion to preserve pipe-separated parameters
When cursor is inside [seed:3], clicking language button now correctly
creates [de:|seed:3] instead of incorrectly replacing seed with language.

The fix properly handles tags with pipe separators, preserving all
existing parameters while prepending the language code.
2025-10-31 17:39:25 -03:00
diodiogod fc79682b5f Add functional language tag button to TTS editor
Implements language tag insertion matching character/parameter buttons:
- Inserts [lang:] or [lang:Character] tags at cursor
- Supports all 9 languages (en, de, fr, ja, es, it, pt, th, no)
- Modifies existing tags (replaces language in [lang:Char] format)
- Creates new tags when cursor not in tag
- Integrates with tag editor UI builder and event handlers
2025-10-31 17:28:45 -03:00
diodiogod c8c0f5a95a Integrate event handlers module into TTS tag editor
Refactored 900-line main widget file to 534 lines by extracting all event listener attachments into widget-event-handlers.js (359 lines). Preserves 100% of original functionality while maintaining LLM-friendly modular architecture.

Changes:
- Extracted editor input debouncing, undo/redo, keyboard shortcuts
- Moved font controls (Ctrl+scroll, font family/size selectors)
- Consolidated character selection and parameter insertion handlers
- Preserved complex preset save/load logic with selection support
- Integrated divider resize with double-click reset
- Maintained sidebar scaling and canvas interaction events

All modules now <400 lines for optimal LLM context usage.
2025-10-31 17:23:15 -03:00
diodiogod b78cdbe2fc Refactor monolithic widget into modular architecture
Major refactoring to improve code maintainability:
- Reduced main widget file from 1677→900 lines (46% reduction)
- Extracted UI builders into separate modules:
  * widget-ui-builder.js (237 lines) - History, Character, Language, Validation sections
  * widget-parameter-section.js (237 lines) - Parameter controls with dynamic inputs
  * widget-preset-system.js (102 lines) - Preset management
- All original functionality preserved with identical behavior
- Parameter tag insertion now working with full event handler
- Note: Language selector currently only displays, doesn't auto-add to tags (design limitation)

Modules are properly organized with clear separation of concerns while maintaining all original logic and event handling.
2025-10-31 16:54:22 -03:00
diodiogod 2dad024160 Fix format button removing space before tags at end of sentence 2025-10-31 16:03:16 -03:00
diodiogod c9403923d7 Apply 2-120px font size limits to Ctrl+scroll handler 2025-10-31 16:00:03 -03:00
diodiogod 767d28e87d Set font size range to 2-120px for more flexibility 2025-10-31 15:17:22 -03:00
diodiogod 37e88c40c8 Sync font size input field when using Ctrl+scroll 2025-10-31 15:10:40 -03:00
diodiogod 1ca22b4306 Add comprehensive font options (16 web-safe fonts) including programming-friendly monospace 2025-10-31 15:05:24 -03:00
diodiogod 2fab935c04 Adjust floating font box to better position above node with responsive shrinking 2025-10-31 14:53:09 -03:00
diodiogod 29ca82eace Make floating font box shrink responsively instead of overflowing 2025-10-31 14:49:34 -03:00
diodiogod 55a41d6af9 Adjust floating font box offset and add overflow clipping 2025-10-31 14:46:21 -03:00
diodiogod 3ffc079292 Refactor to use wrapper container for floating font box above node 2025-10-31 14:45:08 -03:00
diodiogod a84c332d11 Fix font dropdown to use canonical system fonts 2025-10-31 14:39:14 -03:00
diodiogod d25d38a446 Fix resize divider position after sidebar width changes
Divider was positioned once on init but never updated when sidebar width changed.
Now setSidebarWidth() also updates the divider position, so cursor feedback
and resize handler stay accessible after resizing or double-click reset.
2025-10-31 14:20:19 -03:00
diodiogod 44748cf5ac Make preset name truncation dynamic based on sidebar width
Instead of fixed 15 char limit, calculate max characters based on current
sidebar width. Uses formula: (sidebarWidth - 30) / 6 to estimate char count.
Minimum of 8 chars guaranteed. Automatically adjusts when sidebar is resized.
2025-10-31 14:17:51 -03:00
diodiogod b074217d55 Auto-update preset names to show saved content
Preset titles now display the saved tag/content instead of just 'P1', 'P2', 'P3'.
Content is limited to 15 characters with ellipsis if longer. When preset is deleted,
title reverts back to P1/P2/P3. Updates both on initial load and after each save.
2025-10-31 14:11:15 -03:00
diodiogod 9e9adf17a4 Replace undo/redo shortcut with Alt+Z to avoid ComfyUI conflict
Changed from Ctrl+Z/Ctrl+Shift+Z to Alt+Z/Alt+Shift+Z. This avoids any conflict
with ComfyUI's global undo system since ComfyUI only listens for Ctrl+Z.
Updated button tooltips to reflect the new shortcuts.
2025-10-31 14:08:52 -03:00
diodiogod 746fad8b52 Add stopPropagation to undo/redo to prevent ComfyUI conflicts
Ctrl+Z/Ctrl+Shift+Z now stops event from bubbling to parent listeners.
Prevents ComfyUI from also trying to undo its own state when widget undo is used.
2025-10-31 11:40:52 -03:00
diodiogod ab0dafc93f Fix preset save button - use actual selection instead of stale caretPos
- Changed from using undefined selectionStart/selectionEnd to window.getSelection()
- Now properly detects when user has selected text in editor
- Saves complex tags from selection or character+parameters from sidebar
- Both load and save buttons now properly updated
2025-10-31 11:35:06 -03:00
diodiogod 5c087dc793 Fix tag detection when caret is right before closing bracket
Changed condition from '>' to '>=' when checking if caret is inside tag.
This fixes the bug where [seed:5<caret>] wasn't detected as inside the tag,
causing character/parameter additions to create duplicate tags instead of
modifying the existing one.
2025-10-31 03:03:53 -03:00
diodiogod 1b0578775d Fix parameter duplicate tags - position caret right after ] on new tag creation
When creating a new parameter tag, position caret right after ] instead of
after the space. This allows subsequent parameter additions to detect and
modify the existing tag instead of creating duplicates.
2025-10-31 03:01:34 -03:00
diodiogod 646f2fc10b Fine-tune resize divider to 6px (3px on each side)
Perfect balance: easy enough to grab, thin enough to not interfere with text.
2025-10-31 02:55:03 -03:00
diodiogod 0dfb420478 Reduce resize divider width to 8px centered on border
Changed from 12px to 8px width, centered on the border (4px left + 4px right).
This prevents the hitbox from extending too far into the editor and blocking
text clicks while still being easy to grab.
2025-10-31 02:53:09 -03:00
diodiogod 15cd521e28 Fix API response format - return aiohttp web.json_response
The endpoint was returning a dict instead of a proper aiohttp response object.
Now correctly returns web.json_response() which aiohttp expects.
2025-10-31 02:40:00 -03:00
diodiogod 8dd363b75e Add API endpoint for character voice discovery in widget
- Create /api/tts-audio-suite/available-characters endpoint
- Make widget's populateCharacters() async and try to fetch from API
- Falls back to hardcoded defaults if API unavailable
- Endpoint returns sorted list of discovered character voices
2025-10-31 02:37:32 -03:00
diodiogod 1e7d8f9937 Fix character insertion to preserve parameters in param-only tags
When adding a character to a tag like [seed:2|cfg:2], now correctly prepends
the character instead of replacing the first parameter. Detects if first part
has ':' (parameter) vs no colon (character), then either unshift (prepend) or
replace accordingly.
2025-10-31 02:35:17 -03:00
diodiogod a806b99c6b Consolidate tag insertion logic into TagUtilities.modifyTagContent()
- Extract universal tag detection (after tag, inside tag, not in tag) into reusable function
- Refactor Add Parameter and Add Character buttons to use centralized logic
- Eliminates ~200 lines of duplicate code
- Fixes nested tag creation bug in character insertion
- Reduces parameter button from ~130 to ~55 lines
- Reduces character button from ~130 to ~40 lines
2025-10-31 02:33:40 -03:00
diodiogod 475a5f0934 Fix character dropdown and Add Char button functionality
Character Dropdown:
- Now uses discovered characters from state.discoveredCharacters
- Handles both array and object (key-value) formats from discovery system
- Falls back to default names if no characters discovered
- Actually loads the voices from the voices folder instead of hardcoded defaults

Add Char Button:
- Fixed undefined 'caretPos' variable - now calls getCaretPos()
- Uses same pattern as Add Parameter button with proper text insertion
- Gets current caret position correctly before modifying text
- Inserts character tag at cursor and moves caret to after tag
- Properly updates history with caret position
- Saves state and updates UI consistently
- Now actually works instead of silently failing
2025-10-31 02:25:46 -03:00
diodiogod fcc8f35523 Fix format button removing newlines before tags in SRT
- Changed regex from \s* (whitespace including newlines) to  * (spaces only)
- Preserves newlines before tags in SRT format subtitles
- Fixed issue where '[seed:2] Text' would appear on same line as timestamp
- Now correctly maintains SRT structure:
  1
  00:00:00,000 --> 00:00:20,000
  [seed:2] So, I know I
- Still removes unnecessary spaces around brackets and trailing whitespace
2025-10-31 02:22:06 -03:00
diodiogod 2a62184342 Add subtle highlighting for multiple consecutive spaces
- Detects 2 or more consecutive spaces in text
- Highlighted with slightly darker background (#2a2a2a) compared to editor (#1a1a1a)
- Maintains white text color (#eee) for readability
- Non-distractive visual cue to spot spacing in text
- Useful for intentional spacing: some TTS engines preserve spaces and generate
  more natural pauses or spaced-out speech when multiple spaces are present
- Helps maintain awareness of spacing without being visually intrusive
2025-10-31 02:19:54 -03:00
diodiogod 2aeb688b07 Change comma highlighting from yellow to green
- Commas now display in bright green (#66ff66) with bold weight
- Periods remain in golden yellow (#ffcc33) for differentiation
- Easier visual distinction between punctuation types
- Commas stand out more prominently in the text flow
2025-10-31 02:16:34 -03:00
diodiogod 80f36a8840 Add double-click reset to sidebar resize handle
- Double-click on the invisible resize divider (when cursor changes to col-resize) resets:
  * Sidebar width to 220px (default)
  * UI scale to 1.0x (default)
  * Font size to 14px (default)
- Shows notification confirming all settings have been reset
- Detection window: 300ms between clicks with <5px mouse movement tolerance
- Prevents accidental resets while still being easy to intentionally activate
2025-10-31 02:14:30 -03:00
diodiogod 8d0bbd49eb Update all references from 'String Multiline Tag Editor' to 'Multiline TTS Tag Editor'
- Updated docstring in tts_tag_editor_node.py
- Updated error message in nodes.py
- Updated node display name mapping in nodes.py
- Updated file header comment in string_multiline_tag_editor.js
- Updated SRT helper file header comment
- All UI references now use consistent naming
2025-10-31 02:12:30 -03:00
diodiogod deebae678a Remove visible divider from resizable sidebar - keep original styling
- Removed the 5px visual divider bar between sidebar and editor
- Kept the original sidebar styling with border-right: 1px solid #444
- Resizing now works by dragging the right edge of the sidebar (within 8px)
- Cursor changes to col-resize when hovering near the right edge
- No visual changes - resizing is invisible but fully functional
- Maintains backward compatibility with original panel appearance
2025-10-31 02:11:01 -03:00
diodiogod 8168f2ca6e Add all parameters with specialized inputs, tooltips, resizable sidebar UI scaling
Comprehensive Parameter System:
- Added all parameters from docs with proper grouping by engine:
  * Universal: seed, temperature
  * ChatterBox: cfg, exaggeration
  * F5-TTS/Higgs: speed
  * Higgs/VibeVoice/IndexTTS: top_p, top_k
  * VibeVoice/IndexTTS: inference_steps
  * IndexTTS: emotion_alpha
- Each parameter has appropriate input type:
  * Seed: number input (0-4294967295)
  * Temperature: slider (0.1-2.0)
  * CFG: slider (0.0-20.0)
  * Speed: slider (0.5-2.0)
  * Exaggeration: slider (0.0-2.0)
  * Top P: slider (0.0-1.0)
  * Top K: number (1-100)
  * Steps: number (1-100)
  * Emotion Alpha: slider (0.0-1.0)
- All parameter values persist in localStorage

Tooltip System:
- Added helpful tooltips to all buttons:
  * History: Undo/Redo with keyboard shortcuts
  * Character: Add character tag at cursor
  * Parameters: Add parameter to tag or create new
  * Presets: Save/Load/Delete with descriptions
  * Format: Normalize spacing and structure
  * Validate: Check tag syntax

Resizable Sidebar with UI Scaling:
- Sidebar width is draggable between 150px and 400px
- Sidebar width persists in localStorage
- Ctrl+scroll on sidebar to scale UI elements (0.7x - 1.5x)
- UI scale affects all buttons, inputs, text sizes in sidebar
- UI scale persists in localStorage
- Visual resizer divider with col-resize cursor

Node Rename:
- Changed from '🏷️ String Multiline Tag Editor' to '🏷️ Multiline TTS Tag Editor'
2025-10-31 02:08:58 -03:00
diodiogod 0df1fd8e95 Add debounced history grouping and persistent font size zoom
Debounced History Grouping:
- Changed history system from capturing every keystroke to smart grouping with 500ms debounce
- User must pause typing for 500ms before that chunk is added to history
- Multiple consecutive keystrokes now group into single history entry
- Paste and cut events trigger immediate history flush for quick actions
- Results in much cleaner, more meaningful history (paragraph-level vs letter-level)

Persistent Font Size with Ctrl+Scroll:
- Added fontSize property to EditorState, persisted in localStorage
- Editor now loads with saved font size on page reload
- Ctrl+wheel (or Cmd+wheel on Mac) to zoom text up/down
- Font size constrained between 8px and 32px
- Changes are automatically saved to localStorage
2025-10-31 01:58:22 -03:00
diodiogod 66cd715992 Implement caret position restoration in undo/redo history
- Modified EditorState.addToHistory() to store {text, caretPos} objects instead of text strings
- Updated undo() and redo() to return history entries with both text and caret position
- Updated all callers of addToHistory() to pass current caret position:
  - Input event handler captures caret position after text changes
  - Parameter insertion handlers capture final caret position
  - Character insertion captures final position
  - Format button resets caret to 0
  - Preset insertion captures position after tag insertion
- Modified undo/redo button and keyboard shortcut handlers to restore caret from history
- Caret now restores to its original position in that history state, making navigation much more intuitive when text length changes
2025-10-31 01:54:11 -03:00
diodiogod f869abe5ca Major refactor: Replace textarea+overlay with contenteditable div
- Remove misaligned textarea+overlay architecture
- Replace with single contenteditable div for perfect text alignment
- Implement caret position preservation across highlighting updates
- Add getPlainText() and getCaretPos()/setCaretPos() helper functions
- Update all event handlers to work with contenteditable
- Fixes word wrapping alignment issues completely
- Caret position now visually matches actual character position
- Selection, copy/paste, and all interactions now work correctly
2025-10-31 01:26:37 -03:00
diodiogod 449196e561 Fix parameter insertion tag matching with bracket depth counting
- Add proper bracket matching algorithm that counts depth when finding matching [
- Fix missing closing bracket in tag reconstruction
- Only add parameters to tags when caret is directly after ]
- Create new tags at exact caret position when not after a tag
- Simplifies logic to avoid false positives from searching distant tags
2025-10-31 01:23:22 -03:00
diodiogod 5322651e04 Add persistent parameter type selection and SRT syntax highlighting
- Add lastParameterType to EditorState for persistent parameter selector
- Parameter type selection now restores on reload (was resetting to "Select parameter...")
- Add comprehensive syntax highlighting for SRT files:
  - Red for subtitle sequence numbers (only if followed by valid timestamps)
  - Orange for SRT timestamps (HH:MM:SS,mmm --> format)
  - Cyan for TTS tags ([character|param:value])
  - Pale yellow for commas
  - Golden yellow for periods/dots
  - Light salmon for punctuation (? ! ;)
- Fixed highlighting order to highlight before HTML escaping (prevents arrow regex mismatch)
2025-10-31 00:31:21 -03:00
diodiogod 3e2c07d6ec Fix Windows socket cleanup errors in background thread
- Add socket import for proper resource handling
- Force garbage collection on thread exit to clean up resources
- Wrap thread exit in finally block to ensure cleanup happens
- Fixes ConnectionResetError exceptions on Windows when background refresh finishes
2025-10-31 00:07:42 -03:00
diodiogod 063e7413fd Remove debug console logs and finalize node output
- Remove console.log statements from getValue/setValue
- Node now cleanly outputs edited text without debug noise
- Text properly passes through to preview nodes and downstream
2025-10-30 17:52:27 -03:00
diodiogod c319fe841a Fix character dropdown population
- Remove conditional check that was preventing characters from loading
- Populate characters immediately instead of setTimeout
- Characters now show in dropdown on widget creation
2025-10-30 17:47:27 -03:00
diodiogod adfee0b13e Use default characters instead of API endpoint
- Replace API fetch with default character list (Alice, Bob, Charlie, etc.)
- Add get_available_characters() method to backend node
- Dropdown now shows common character names users can select from
- Users can still type custom character names in the input field
2025-10-30 17:38:21 -03:00
diodiogod 3ae89c3bd6 Fix widget value output and character discovery
- Keep widget.value synced with textarea.value for ComfyUI to read
- Update widget.value on input, change, and setTextareaValue events
- Initialize widget.value with textarea content on widget creation
- Fix character discovery: use get_available_characters() instead of get_character_voices()
- Now node properly passes edited text to output when executed
2025-10-30 17:22:58 -03:00
diodiogod b2c715db61 Fix preset load caret position
- After inserting preset text, move caret to after the inserted content
- Subsequent clicks will insert at the new caret position, not jump around
- Use setTimeout to ensure caret position is set after textarea value updates
2025-10-30 17:19:32 -03:00
diodiogod 2d9a121e8b Fix syntax highlighting by making textarea text transparent
- Hide textarea text color (make it transparent)
- Overlay shows colored highlights (cyan for tags, orange for SRT)
- Keep caret color visible (#eee) so user can see cursor
- Overlay base text color matches normal text color
- Now colors are clearly visible without white text covering them
2025-10-30 17:17:01 -03:00
diodiogod 56a0a2d8cb Improve syntax highlighting visibility
- Change tag highlight color to bright cyan (#00ffff) with bold font
- Change SRT timing highlight to bright orange (#ffaa00) with bold font
- Add font-weight: bold for better contrast at all zoom levels
- Add font rendering optimizations (textRendering, WebkitFontSmoothing)
- Colors now clearly visible when zoomed in or out
2025-10-30 17:07:14 -03:00
diodiogod c15379f0bb Fix syntax highlighting glitch and remove glow from tags
- Remove text-shadow glow effect from tag and SRT timing highlights
- Keep highlighting colors bright (green for tags, yellow for SRT)
- Create setTextareaValue() helper to sync textarea updates with highlights
- Replace all direct textarea.value assignments with setTextareaValue() calls
- Fixes glitch where highlights didn't update when tags were added via buttons
- Highlighting now updates immediately for: Add Parameter, Add Character,
  Format, Undo/Redo, and Preset Load actions
- Preset glow effect on Load button remains unchanged (green with box-shadow)
2025-10-30 16:58:57 -03:00
diodiogod b32f813efd Implement full parameter and UI state persistence
- Seed and temperature values now persist across session reloads
- Seed input and temperature slider populate from saved state on load
- Parameters save to localStorage on change (real-time for slider, on blur for seed)
- Character dropdown now also saves selection to state
- Preset glow states restored on widget load instead of temporary fade
- updatePresetGlows called on initial load and after all save/delete actions
2025-10-30 16:43:40 -03:00
diodiogod 996664db14 Add: Lightweight syntax highlighting for tags and SRT timings 2025-10-30 16:26:05 -03:00
diodiogod 0da8028cfd Add: Replace duplicate parameters, green glow feedback on preset save 2025-10-30 16:23:32 -03:00
diodiogod 6a77ad6aee Add: Toast notification system, remove all intrusive alerts 2025-10-30 16:19:09 -03:00
diodiogod 16c626276e Improve: Remove alerts, keep param values, add support for saving complex tags as presets 2025-10-30 16:17:07 -03:00
diodiogod 3fdaa06051 Add: Implement dynamic parameter system with type-specific inputs 2025-10-30 16:11:42 -03:00
diodiogod 14572d3a62 Fix: Remove dead code references to insertPauseBtn and pauseInput 2025-10-30 16:09:31 -03:00
diodiogod 6be12594d3 Fix: Simplify Parameters section to debug node loading issue 2025-10-30 16:06:43 -03:00
diodiogod a458f07604 Fix: Update process_text method to work with no inputs 2025-10-30 16:03:37 -03:00
diodiogod f054cb1b4d Fix: Update preset handlers for new parameter system 2025-10-30 16:00:31 -03:00
diodiogod 17dddf21d6 Redesign Parameters section with dynamic input and tag modification support 2025-10-30 15:59:26 -03:00
diodiogod 622ab2f434 Fix: Improve node sizing and layout - set default 900x600, fix flex sizing 2025-10-30 15:01:13 -03:00
diodiogod 0f0e10d886 Fix: Remove input widget, keep only editor UI as single widget with output 2025-10-30 14:58:15 -03:00
diodiogod 9d5d6968b4 Implement 🏷️ String Multiline Tag Editor with full persistence, undo/redo, and preset system 2025-10-30 14:54:04 -03:00
diodiogod c7cd8c8274 Version 4.13.2
Fix IndexTTS-2 QwenEmotion transformers compatibility (#90)

- Change deprecated dtype parameter to torch_dtype for AutoModelForCausalLM
- Add KV-cache disabling to prevent incompatibilities with newer transformers versions
- Resolves Qwen3ForCausalLM unexpected keyword argument error
2025-10-30 00:06:35 -03:00
diodiogod b23c769956 Version 4.13.1
Fix perth import fallback - make it optional

- Add try-except with PERTH_AVAILABLE flag for all perth imports
- Prevents ModuleNotFoundError crash when perth is not installed
- Watermarking is disabled by default, perth is truly optional
- Matches pattern already used in vc.py files
- Remove unused perth import from stateless wrappers
2025-10-29 23:48:11 -03:00
diodiogod 5eada5fc94 Update VibeVoice example workflow note and add Voice Cleaning screenshot 2025-10-29 23:36:28 -03:00
diodiogod f536af05fe Add v4.12.0 to project evolution timeline
- Per-Segment Parameter Switching feature milestone
- Shows progression from v4.9 to v4.12.0
2025-10-29 22:29:57 -03:00
diodiogod 4dc982fb1b Add Voice Cleaning workflow JSON example 2025-10-29 22:18:45 -03:00
diodiogod 3373f71591 Add Voice Cleaning workflow example to documentation
- New 🤐 Voice Cleaning workflow combining Noise or Vocal Removal + Voice Fixer
- Showcases dual audio restoration pipeline for comprehensive cleanup
- Listed in example workflows table as new in v4.13
2025-10-29 22:16:34 -03:00
diodiogod f969b47b12 Version 4.13.0
Add Voice Fixer audio restoration node and optimize dependencies

Implementation details:
- Integrate bundled VoiceFixer for professional audio restoration (Issue #153)
- Lazy loading with Python 3.12 CUDNN fix for VRAM optimization
- Replace torchlibrosa with librosa-based STFT/ISTFT to reduce external dependencies
- Improve Wave Analyzer: analyze in mono but preserve original stereo output
- Fix audio playback cache issues with unique filename generation
- Sort vocal removal model dropdown by category for better UX
2025-10-29 17:05:59 -03:00
diodiogod 6b51ab9d7e Sort vocal removal model dropdown by category and filename
- Models now grouped by folder category (MELBAND, MDX, KARAFAN, etc)
- Within each category, models sorted alphabetically by filename
- Greatly improves usability of the model selection dropdown
- Fixes messy/chaotic model list organization
2025-10-29 16:59:05 -03:00
diodiogod c19837675d Fix librosa STFT/ISTFT tensor format compatibility with torchlibrosa
- LibrosaSTFT transposes output from [batch, freq, time] to [batch, time, freq]
- LibrosaISTFT transposes input back from [batch, time, freq] to [batch, freq, time]
- Ensures compatibility with VoiceFixer's downstream STFT operations
- Resolves matrix multiplication shape errors after removing torchlibrosa dependency
2025-10-29 16:14:23 -03:00
diodiogod d0e67b2db2 Fix tensor shape in wav_to_spectrogram_phase for librosa STFT
- Add unsqueeze(1) to maintain 4D shape [batch, channels, freq, time]
- Previously was incorrectly concatenating on dim=1, flattening to 3D
- Fixes permute dimension mismatch error when using librosa-based STFT
- Now properly maintains channel dimension for multi-channel processing
2025-10-29 16:07:22 -03:00
diodiogod 6782361bc6 Remove torchlibrosa dependency by using librosa-based STFT/ISTFT
- Replace torchlibrosa with custom LibrosaSTFT and LibrosaISTFT wrappers
- Maintain full API compatibility with existing FDomainHelper code
- No quality loss - mathematically equivalent FFT implementations
- Handles batch processing and device/dtype preservation
- Removes dependency since we already require librosa
- ~0.1-0.2s slower per audio (CPU instead of GPU STFT) but acceptable trade-off
2025-10-29 16:04:56 -03:00
diodiogod 75d85c0ebb Change Voice Fixer emoji to 🤐 to match Noise or Vocal Removal
- Signals to users they have related audio restoration functions
- Updated node name, docstring, tooltips, and display mappings
- Both nodes now use 🤐 emoji for visual consistency
- Better UX: users see them as part of the same family
2025-10-29 15:36:42 -03:00
diodiogod 42fcc53e96 Fix audio playback cache issue: use unique filename per audio input
- Include audio tensor hash in saved filename
- JavaScript now detects filename change and reloads audio
- Fixes issue where changing audio input wouldn't update playback
- Prevents browser cache from serving old audio
- Avoids file overwrite conflicts that caused asyncio errors
2025-10-29 15:26:25 -03:00
diodiogod e97e5a3fcc Fix analyzer audio save: use original_audio instead of undefined audio_tensor
- Save ORIGINAL audio for web playback (preserves stereo)
- Handle 1D-4D tensor shapes properly when saving to WAV
- Transpose multichannel for soundfile compatibility
- Fix NameError when saving connected audio
2025-10-29 15:19:23 -03:00
diodiogod fff23447f0 Analyzer: analyze in mono but preserve original stereo in output
- _extract_audio_tensor now returns both original and mono versions
- Analysis (silence detection, energy, peaks) performed on MONO version only
- Visualization and graphs generated from MONO version
- Output audio (processed and segmented) uses ORIGINAL stereo format
- Timing regions found from mono are applied directly to stereo samples
- Stereo audio now passes through analyzer without quality loss
- Segmented audio preserves stereo channel information
2025-10-29 15:08:18 -03:00
diodiogod 378b38240b Add comprehensive tooltips to Voice Fixer node
- Enhanced class docstring with features and use cases
- Audio input tooltip explaining ComfyUI format
- Detailed restoration mode descriptions with use case recommendations
- CUDA toggle tooltip with fallback behavior explanation
- Professional formatting with emojis and clear organization
2025-10-29 14:35:51 -03:00
diodiogod 4c24e67e9e Add Python 3.12 CUDNN fix to Voice Fixer node to prevent VRAM spikes
- Import ensure_python312_cudnn_fix compatibility function
- Call fix at start of restore_audio to disable problematic CUDNN benchmarking
- Matches pattern used in other TTS engines (IndexTTS-2, etc.)
2025-10-29 14:30:24 -03:00
diodiogod 68b0ce7df5 Fix Voice Fixer node audio format handling
- Accept ComfyUI AUDIO dict format with 'waveform' and 'sample_rate'
- Extract and convert tensor to numpy properly
- Handle 3D tensor shape [batch, channels, samples] correctly
- Return output in ComfyUI AUDIO dict format
- Preserve sample rate through processing
2025-10-29 14:26:21 -03:00
diodiogod 5d52df2329 Fix VoiceFixer downloader progress calculation and cleanup
- Simplify progress calculation to avoid overflow errors
- Remove temp files on retry to start fresh on failed downloads
- Verify download completion before moving to final location
- Increase timeout to 60s for large files
- Write in binary mode from start (no append mode)
2025-10-29 14:18:21 -03:00
diodiogod 40f0e873be Integrate Voice Fixer audio restoration (Issue #153)
- Bundle VoiceFixer locally to avoid cache downloads
- Create voice_fixer_downloader for TTS/voicefixer/ model management
- Implement Voice Fixer node with 3 restoration modes (original, high-freq removal, train mode)
- Register node in nodes.py with proper error handling
- Patch bundled VoiceFixer hardcoded paths to use TTS/voicefixer/ directory
- Disable auto-download in restorer/__init__.py, handled by node instead
- Support lazy loading and CUDA acceleration
- All models download to organized TTS/ folder, never to cache
2025-10-29 14:15:52 -03:00
diodiogod c5b0f98404 Version 4.12.1
Fix Higgs Audio SRT seed behavior to use fixed seed

Implementation:
- Change Higgs Audio from seed+i pattern to consistent fixed seed
- Aligns with other engines (F5-TTS, VibeVoice, ChatterBox) which already use fixed seed
- User can control per-segment seed via segment parameters: [seed:42]
- Improves predictability and cache stability when editing SRT files
2025-10-29 12:08:22 -03:00
diodiogod 8e0e5bdff5 Version 4.12.0
Add per-segment parameter switching across all TTS engines

Implementation details:
- Unified parameter switching for ChatterBox, F5-TTS, Higgs Audio 2, VibeVoice, IndexTTS-2
- Per-segment control with inline tag syntax [character|seed:42|temp:0.5]
- Engine-specific parameter filtering with smart fallback handling
- SRT subtitle line parameter support
- Order-independent and case-insensitive parameter syntax
- Full integration with character and language switching
- Comprehensive parameter validation and range clamping
2025-10-29 11:37:36 -03:00
diodiogod c32b191cd9 Initialize character parser with available characters in Higgs Audio
- Call get_available_characters() and set_available_characters() before text parsing
- Matches behavior in F5-TTS node
- Fixes issue where character tags like [Alice] were falling back to narrator
- Characters now properly recognized and used instead of defaulting to narrator
2025-10-29 11:23:50 -03:00
diodiogod e5c73974fd Fix duplicate seed parameter in Higgs Audio text processor
- Extract seed from current_params if present (from segment parameters)
- Prevents duplicate keyword argument error when both seed and **current_params have seed
- Segment-level seed parameter now properly overrides default seed behavior
2025-10-29 11:20:55 -03:00
diodiogod c2b4da1ef7 Fix duplicate seed parameter in Higgs Audio SRT
- Extract seed from current_params if present (from segment parameters)
- Prevents duplicate keyword argument error when both seed and **current_params have seed
- Segment-level seed parameter now properly overrides default seed + i behavior
2025-10-29 11:17:22 -03:00
diodiogod 654947340c Fix Higgs Audio engine type in parameter system
- Change engine_type from 'higgs' to 'higgs_audio' in apply_segment_parameters calls
- Matches the expected engine type in PARAMETER_ENGINES mapping
- Seeds and other parameters now properly recognized for Higgs Audio
2025-10-29 11:14:49 -03:00
diodiogod 0533483dae Add parameter switching to Higgs Audio text and SRT processors
- Support per-segment parameters via tags like [seed:42] and [Alice|temp:0.8]
- Text processor: Use parse_text_segments() to capture parameters in character switching mode
- Extract parameters in tts_generate_func() and apply via apply_segment_parameters()
- SRT processor: Extract parameters from char_segments_full (already using parse_text_segments)
- Apply per-segment parameters in all generate_srt_audio() calls
- Parameter-only tags work via parameter detection in character switching logic
- All original features preserved: pause tag processing, voice mapping, native modes
2025-10-29 11:11:53 -03:00
diodiogod 72f945bbc6 Add parameter switching to F5-TTS text and SRT nodes
- Support per-segment parameters via tags like [seed:42] and [Alice|temp:0.8]
- Switch from split_by_character_with_language() to parse_text_segments() to capture parameters
- Thread 5-tuple (idx, char, text, lang, params) through all processing loops
- Apply per-segment parameters in cache key generation and audio generation calls
- Parameter-only tags now work by detecting segment parameters and forcing character switching mode
- All original features preserved: pause tag preprocessing, cache checking, language grouping
- Surgical changes only - single-character mode left completely untouched
2025-10-29 11:08:17 -03:00
diodiogod 2314fc7c49 Add parameter switching to F5-TTS text node (character switching mode only)
FEATURE: F5-TTS now supports per-segment parameters via tags like [Alice|seed:42|temp:0.7]

IMPLEMENTATION:
- Use parse_text_segments() to get parameters alongside characters/languages
- Apply per-segment parameter switching in character/language switching mode only
- Single-character mode completely unchanged (backward compatible)
- Parameters used in cache key generation for proper cache differentiation

BACKWARD COMPATIBILITY:
- Single-character mode (no character tags) - completely unchanged
- Pause tag preprocessing - unchanged
- Cache checking before model load - unchanged
- All existing features preserved

PARAMETER SUPPORT:
- Seed: [Alice|seed:42] or [Alice|seed:42|temp:0.8]
- Temperature, speed, target_rms, cross_fade_duration all supported
- Applied per-segment with proper cache key differentiation
2025-10-29 10:52:21 -03:00
diodiogod d8cc3333ed Fix parameter-only tag narrator aliasing in character parser
FIXES: Parameter-only tags like [seed:4|exag:1.0] were incorrectly using aliased narrator voice (David) instead of main narrator voice (Tony)

CAUSE: When parsing parameter-only tags, the character parser:
1. Defaulted character to 'narrator'
2. Detected segment_parameters exist
3. But only skipped alias resolution for language-only tags like [de:] or [it:]
4. Result: 'narrator' was normalized through alias system to 'david_attenborough cc3'

SOLUTION: Also skip alias resolution for parameter-only tags by detecting:
- raw_character == default_character (narrator)
- segment_parameters exist (has parameters)
- explicit_language is None (no language specification)

This ensures parameter-only tags use the main narrator voice (Tony from opt_narrator) not the aliased narrator (David)
2025-10-29 10:26:24 -03:00
diodiogod 4936eefdd0 Fix ChatterBox Official 23-Lang SRT cache invalidation between v1 and v2 models
FIXES: When switching from v2 to v1 model, audio cache was not being properly invalidated, causing v2-generated audio to be returned for v1 requests

KEY CHANGES:
1. Extract model_version from config in SRT processor (defaults to v2)
2. Pass model_version to all generate_speech() calls in parameter_switching and simple subtitle paths
3. This ensures cache keys differ between v1 and v2 models, preventing cache misses

ALSO INCLUDES:
- Refactored lang-23 SRT processor to use two-pass architecture matching regular ChatterBox
- First pass: Analyze and categorize all subtitles
- Second pass: Route to appropriate handler (MultilingualEngine for complex, direct for simple)
- Fixed character voice selection to use proper alias mapping via voice_refs
- Use original_character in regular ChatterBox to preserve character identity

This ensures parameter-only tags like [seed:4] correctly use narrator voice (Tony) instead of aliased narrator (David)
2025-10-29 10:11:49 -03:00
diodiogod 513fc9d807 Fix character voice usage in SRT parameter_switching path
- Build voice_refs mapping in traditional processing (same as streaming)
- Use character-specific voice instead of narrator voice for segments
- Fixes issue where Alice segments were generated with Tony's voice
- Ensures proper voice selection when parameters change across segments

Now correctly uses Alice's voice for all [Alice|...] segments regardless
of parameter changes.
2025-10-29 08:42:04 -03:00
diodiogod 56083e1500 Add segment-by-segment parameter switching to ChatterBox SRT processing
- Detect when parameters change across segments within a single subtitle
- Create new 'parameter_switching' subtitle type for proper per-segment handling
- Process each segment individually with its own parameters when switching detected
- Concatenate segment audio to maintain subtitle timing
- Fixes issue where all segments were merged into one with single parameters

This ensures [Alice|seed:2] ... [Alice|seed:5] ... [Alice|seed:4|exag:2]
generates three separate segments each with their correct parameters.
2025-10-29 08:38:42 -03:00
diodiogod a508a9fe9a Fix ChatterBox SRT parsing to use new segment_parameters format
Changed from old split_by_character_with_language_and_explicit_flag
(which created 3-tuples) to parse_text_segments (which creates 4-tuples
with parameters). This ensures both streaming and traditional paths
receive the correct format with per-segment parameters.

This fixes the "ValueError: not enough values to unpack (expected 4, got 3)"
error in traditional SRT processing.
2025-10-29 08:34:25 -03:00
diodiogod 50599debd5 Complete per-segment parameter support in ChatterBox SRT traditional processing
- Apply per-segment parameters to multilingual engine processing path
- Apply per-segment parameters to simple subtitle path
- Fixed tuple unpacking (3-tuple → 4-tuple with parameters)
- Parameters now work in both streaming and traditional SRT modes

This ensures parameter switching works comprehensively across all ChatterBox
processing paths without leaving incomplete implementations.
2025-10-29 08:32:36 -03:00
diodiogod 865b0e636d Add per-segment parameter switching to regular ChatterBox node (text and SRT)
Integrate segment parameter system with both traditional and streaming modes:
- Updated ChatterBoxTTSNode to parse text with parameters and apply them
- Added parameter switching in _process_languages_traditional method
- Updated ChatterBoxSRTTTSNode to extract and pass segment parameters
- Enhanced StreamingCoordinator to handle 5-tuple SRT data with parameters
- Parameters now override node defaults for specific segments only

This completes the per-segment parameter switching implementation for all
major TTS engines (VibeVoice, Index2, ChatterBox Official 23-Lang, ChatterBox).
2025-10-29 08:31:40 -03:00
diodiogod 9bcba0503d Fix remaining tuple unpacking for 4-tuple character segments in ChatterBox 23-Lang 2025-10-29 08:19:42 -03:00
diodiogod 0dcd048f7e Fix tuple unpacking for character segments with parameters in ChatterBox 23-Lang
- Updated character_segments_with_lang to include parameters (4-tuple)
- Fixed unpacking in character extraction and language extraction
- Fixed backward-compatible character segments construction
2025-10-29 08:18:06 -03:00
diodiogod f8c2aa48cf Add per-segment parameter support to streaming pipeline
- Updated StreamingCoordinator to handle 5-tuple TTS data (idx, char, text, lang, params)
- Parameters stored in StreamingSegment metadata for transport through streaming
- ChatterBoxStreamingAdapter extracts and applies segment parameters during generation
- Streaming now supports the same per-segment parameters as traditional mode
- Backward compatible with existing 4-tuple format (no params)
2025-10-29 08:12:49 -03:00
diodiogod 497a2ee89f Add per-segment parameter switching to ChatterBox Official 23-Lang
- Added segment_parameters import and parameter extraction from CharacterSegment objects
- Modified traditional mode processing to apply per-segment parameters (exaggeration, temperature, cfg_weight, etc.)
- Updated SRT processor to parse CharacterSegment objects and apply per-segment parameters
- Parameters now override node defaults for specific segments only, then revert
- Supports seed, temperature, cfg, exaggeration parameters per segment
- Compatible with both single-character and multi-character/multi-language workflows
- Added regular ChatterBox import for segment parameters (full implementation pending)
2025-10-29 08:11:25 -03:00
diodiogod d0f403b40b Use consistent emoji for parameters (📊) across Index2 and VibeVoice 2025-10-29 08:05:17 -03:00
diodiogod b8de698bc7 Add per-segment parameter switching to Index2 (text and SRT)
- Modified IndexTTSProcessor to use CharacterSegment objects with parameters
- Added parameter detection to split generation when parameters change
- Reconstructs text with character tags to preserve character switching
- Applies per-segment parameters (seed, temperature, cfg, top_p, top_k, emotion_alpha)
- Parameters override node defaults for specific segments only, then revert
- Works with character switching for multi-character scripts
2025-10-29 01:53:58 -03:00
diodiogod 42a1dc0adb Simplify voice reference display - only show when zero-shot
Show voice info only when no voice reference is provided (zero-shot mode), keeping console cleaner for normal operation.
2025-10-29 01:28:57 -03:00
diodiogod bb9a64f564 Fix tokenizer caching and console cleanup
- Consolidate character voice info into block generation messages
- Fix tokenizer path structure to match actual storage location (vibevoice/ instead of VibeVoice/tokenizer/)
- Use consistent path for tokenizer downloads so they're found on subsequent runs
- Comment out non-error device_map debug messages
- Remove duplicate SINGLE SEGMENT text printing
- Fix logged_characters to only log once per character (not per priority check)
2025-10-29 01:27:14 -03:00
diodiogod 63e93bfd2c Comment out model reloading debug messages - only show on errors 2025-10-29 01:01:55 -03:00
diodiogod 96007a9aa9 Add PauseTagProcessor import to text processor for fallback logic 2025-10-29 00:56:34 -03:00
diodiogod fcbcbae59e Fix loop variable shadowing bug in parameter change detection
The parameter change detection loop was reusing the subtitle loop variable 'i',
causing it to be modified and leading to 'list assignment index out of range' error.
Changed to use separate 'seg_idx' variable for segment iteration.
2025-10-29 00:47:18 -03:00
diodiogod b6a5c1dda9 Use triple emoji for fallback messages to improve console visibility 2025-10-29 00:40:06 -03:00
diodiogod 37c4ffa7f5 Add fallback from Native Multi-Speaker to Custom Character Switching when parameter changes detected
Native Multi-Speaker mode now automatically falls back to Custom Character Switching when:
- Segments have different parameters (e.g., seed changes between segments)
- Pause tags are present
- More than 4 unique speakers

This ensures per-segment parameters are always respected while maintaining Native Multi-Speaker mode when possible.
2025-10-29 00:38:57 -03:00
diodiogod e4a48045a1 Add per-segment parameter switching system (VibeVoice Custom Character Switching)
- New utils/text/segment_parameters.py with parameter validation, aliases, and engine compatibility
- Extended CharacterSegment to include per-segment parameters
- Extended language resolver to parse pipe-separated parameters: [Alice|seed:42|temp:0.5]
- Support for: seed, temperature, cfg, speed, top_p, top_k, inference_steps, exaggeration, emotion_alpha
- Case-insensitive aliases and order-independent syntax
- Integrated into VibeVoice Custom Character Switching mode (text and SRT)
- Fixed grouping logic to break groups when parameters change
- Added PARAMETER_SWITCHING_GUIDE.md documentation
- Updated PROJECT_INDEX.md

NOTE: Currently implemented for VibeVoice Custom Character Switching only.
Native Multi-Speaker mode and other engines will be added in future updates.
2025-10-29 00:36:44 -03:00
diodiogod 6649f01c9f Version 4.11.26
Add interrupt handling to all TTS processors

Comprehensive implementation of cancellation support:
- Higgs Audio: Interrupt checks in SRT and TTS processors
- VibeVoice: Interrupt checks in SRT and text processors
- ChatterBox Official 23-Lang: Interrupt checks in SRT processor
- IndexTTS-2: Interrupt checks in SRT processor
- Added Step 9.5 to implementation guide for future engines

This allows users to stop long-running operations gracefully.
2025-10-28 23:17:18 -03:00
diodiogod 90fb8d1f45 Document interrupt handling for future TTS engine implementations
Add Step 9.5 to NEW_ENGINE_IMPLEMENTATION_GUIDE.md:
- Explain ComfyUI interrupt mechanism and why it's critical
- Provide import pattern and code examples
- Show where to add interrupt checks in SRT and TTS processors
- Add test checklist item for interrupt handling
- Reference real implementation examples
- Add interrupt checking to critical integration points list

This ensures future engine implementations include proper cancellation support from day one.
2025-10-28 19:37:19 -03:00
diodiogod 33fb5850dc Add comprehensive interrupt handling to all TTS processors
All TTS engines can now be interrupted during generation:

Higgs Audio:
- HiggsAudioSRTProcessor: Check before setup, segment, character segment, and timing assembly
- HiggsAudioTTSProcessor: Check before character setup, character segments, and native mode

VibeVoice:
- VibeVoiceSRTProcessor: Check before each subtitle processing
- VibeVoiceProcessor: Check before each character block and chunk

ChatterBox Official 23-Lang:
- ChatterboxOfficial23LangSRTProcessor: Check before segment and character segment processing
- Main processor already has interrupt checks inherited from BaseTTSNode

IndexTTS-2:
- IndexTTSSRTProcessor: Check before each subtitle processing

Interrupt checks allow users to stop long-running SRT generation and TTS sessions gracefully.
2025-10-28 19:27:15 -03:00
diodiogod 6f8a21ff3e Add interrupt handling to Higgs Audio processors
- Add interrupt checks in HiggsAudioSRTProcessor.generate_srt_speech() to allow cancellation during segment processing
- Add interrupt checks in HiggsAudioTTSProcessor.generate_tts_speech() for character setup and generation
- Checks occur at key points: before setup, before each segment, before timing assembly
- Uses direct model_management.interrupt_processing checks for consistency with other engines
2025-10-28 19:24:26 -03:00
diodiogod 7ad5b8227e Update documentation for Clear VRAM integration
- Add engine_handlers.py to PROJECT_INDEX.md
- Add comprehensive Clear VRAM integration guide to NEW_ENGINE_IMPLEMENTATION_GUIDE.md
- Document .to() method implementation patterns for simple, medium, and complex engines
- Document device checking and wrapper re-registration patterns
- Provide code examples for proper ComfyUI model management integration
2025-10-27 23:46:16 -03:00
diodiogod d511b0f2d7 Version 4.11.25
Fix Clear VRAM button for all TTS engines

Technical details:
- Add .to() methods to ChatterBox Classic, ChatterBox 23-Lang, F5-TTS, Higgs Audio, VibeVoice, RVC, and Index-TTS engines
- Implement device checking before generation to detect CPU offloading
- Add wrapper search and re-registration logic to maintain ComfyUI tracking
- Fix VRAM spike issues during RVC and VibeVoice reload by moving models to correct device before generation
- Optimize to reuse existing wrappers instead of creating duplicates
- Register RVC models with ComfyUI model management for proper VRAM tracking
- Fix target device calculation to always use CUDA when available across all engines
2025-10-27 23:35:31 -03:00
diodiogod 301d6cd897 Fix Index-TTS Clear VRAM functionality with proper device management
- Added .to() method for moving all model components between devices
- Implemented device checking before generation to detect CPU offloading
- Added wrapper search and re-registration logic to maintain ComfyUI tracking
- Optimized to reuse existing wrappers instead of creating duplicates
- Fixed target device calculation to always use CUDA when available
- Wrapped with SimpleModelWrapper for ComfyUI logging compatibility

Index-TTS now properly unloads to CPU and reloads to CUDA across multiple Clear VRAM cycles without device mismatch errors.
2025-10-27 23:33:59 -03:00
diodiogod e373c49305 Fix RVC Clear VRAM button - register models with ComfyUI tracking
RVC models were being cached in minimal wrapper but not tracked by ComfyUI,
so Clear VRAM button couldn't see them.

Changes:
- Added is_dead() and model_memory() methods to ComfyUIModelWrapper for ComfyUI compatibility
- Created SimpleModelWrapper class for models without nested .model attribute
- RVC models now register with ComfyUI's current_loaded_models list on load
- Hubert models wrapped with SimpleModelWrapper to add .model attribute for logging
- Added device checking on cached model reuse - reloads from CPU to CUDA when needed
- Models re-register with ComfyUI after reload to stay tracked
- Extracted registration logic into helper methods to avoid duplication
- Updated clear_model_cache() to remove RVC models from ComfyUI tracking

RVC Clear VRAM now works correctly across multiple generation cycles.
2025-10-27 23:02:23 -03:00
diodiogod 19cad461ae Fix VibeVoice and RVC device management + VRAM spike fixes
VibeVoice:
- Added .to() method for device management
- Added device checking before generation that properly detects parameter device
- Fixed target_device to always use cuda if available (not self.device which gets updated)

RVC:
- Created RVCModelWrapper class to wrap model dicts with .to() method
- Added .to() method to RVCEngine for device management
- Added device checking to convert_voice() method
- Updated rvc_factory to properly load and wrap RVC models
- Added model caching to minimal_reference_wrapper to prevent reloading
- Added clear_model_cache() method for VRAM management
- Disabled CUDNN benchmark in vc_infer_pipeline.py to prevent VRAM spikes on Python 3.12+

All engines now support Clear VRAM button across multiple cycles.
2025-10-27 22:40:59 -03:00
diodiogod 3fbb32cfec Add .to() methods for F5-TTS and Higgs Audio 2 engines
Extended device management fixes to F5-TTS and Higgs Audio engines.

Changes:
- engines/f5_tts/api.py: Added .to() method to F5TTS class that moves
  ema_model and vocoder components to target device
- engines/higgs_audio/higgs_audio.py: Added .to() method to HiggsAudioEngine
  class that moves underlying HiggsAudioServeEngine components

Both engines now support:
1. Moving between CPU and CUDA via .to() method
2. Automatic device checking through base_node.py
3. Wrapper-based reload to stay in sync with ComfyUI
4. Multiple Clear VRAM cycles without errors

Note: Higgs Audio works best with CUDA graphs disabled for dynamic memory
management. With CUDA graphs enabled, model may be marked invalid after
CPU offload (handled by HiggsAudioHandler in engine_handlers.py).
2025-10-27 22:06:16 -03:00
diodiogod 98b167702f Fix Clear VRAM button for all TTS engines after CPU offload
Added comprehensive device management fixes to ensure "Clear VRAM" button
works correctly across multiple generation cycles for all TTS engines.

Changes:
- engines/chatterbox/tts.py: Added .to() method to ChatterboxTTS class
- engines/chatterbox/vc.py: Added .to() method to ChatterboxVC class
- nodes/base/base_node.py: Added device checking before generation that
  reloads models via wrapper system when device mismatch detected
- utils/models/comfyui_model_wrapper/engine_handlers.py: Created missing
  engine handlers file with BaseEngineHandler and HiggsAudioHandler

The fix ensures:
1. Models can be moved between CPU and CUDA via .to() method
2. Device mismatches are detected before generation
3. Models reload through wrapper.model_load() to stay in sync with ComfyUI
4. Wrappers re-register with ComfyUI after reload so Clear VRAM continues working

Tested with ChatterBox Classic and ChatterBox Official 23-Lang - both now
support multiple Clear VRAM cycles without errors.
2025-10-27 21:59:41 -03:00
diodiogod 43cd4f5bd9 Version 4.11.24
Fix ChatterBox Official 23-Lang device placement bug

Technical implementation:
- Add .to() method to ChatterBoxOfficial23LangTTS class for device migration
- Add .to() method to ChatterboxVC class for voice conversion device handling
- Improve wrapper's _move_all_components_to_device() with better recursion depth
- Fix unified_model_interface device resolution for 'auto' mode
- Add ComfyUI model management re-registration after wrapper.model_load()
- Fix model_finalizer setup when re-registering with ComfyUI
- Add device check in processor to call wrapper.model_load() instead of direct .to()

Fixes device mismatch errors:
- Fix 'Expected all tensors to be on the same device' error on second run
- Fix 'Inference tensors do not track version counter' after CPU detachment
- Fix 'Clear VRAM' button not working after model reload
2025-10-27 19:10:52 -03:00
diodiogod caee9dbcd1 Version 4.11.23
Fix PyTorch installation syntax error

Invalid pip requirement syntax with local version labels in version specifiers.
The +cu121 suffix is only valid with == or != operators, not with >= operators.
Now correctly passes CUDA version via --index-url instead.

Also add graceful error handling for RVC monotonic-alignment-search installation
on systems without C++ build tools.
2025-10-26 19:58:14 -03:00
diodiogod 2fd56ad4b9 Version 4.11.22
Fix remaining torch_dtype deprecation warning in Higgs Audio

Technical changes:
- Replace self.config.torch_dtype with self.dtype in CUDA graph capture
- Add fallback chain: self.dtype -> self.config.dtype -> torch.float32
- Complete elimination of all torch_dtype deprecation warnings
2025-10-25 13:15:57 -03:00
diodiogod 2d4f16d7d9 Version 4.11.21
Fix Higgs Audio compatibility with transformers 4.57.1+

Technical changes:
- Add **kwargs to HiggsAudioModel.forward() to catch new tokenizer argument
- Replace deprecated torch_dtype with dtype in from_pretrained() calls
- Ensure backward compatibility with older transformers versions
2025-10-25 13:11:06 -03:00
diodiogod 319e16f8ca Version 4.11.20
Add TTS Audio Suite prefix to post-load diagnostic messages

User-facing improvements:
- Prefix character voice discovery messages with [TTS Audio Suite] for clarity
- Helps users identify which extension is responsible for each message
- Distinguishes TTS Suite messages from other custom nodes during startup
2025-10-23 23:42:48 -03:00
diodiogod daf54de61d Version 4.11.19
Optimize startup performance with lazy loading and async checks

Engine node optimizations:
- Replace heavy engine imports in INPUT_TYPES with file-reading-only model discovery
- Remove blocking module loads from node initialization
- Reconstruct discovery logic for all 7 engine nodes: VibeVoice, F5-TTS, Higgs, ChatterBox, ChatterBox 23-Lang, IndexTTS, RVC
- Each engine now scans model folders without importing engine modules

Voice discovery and dependency improvements:
- Implement persistent voice character discovery cache in .cache folder
- Add background refresh thread for cache updates
- Move dependency checking to async background thread (2-second delay)
- Prevent blocking initialization on startup
- Print diagnostic messages after ComfyUI loads
2025-10-23 23:34:35 -03:00
diodiogod ce2cccc122 Version 4.11.18
Clean up initialization startup messages

- Remove success messages for FFmpeg availability, numba JIT, and model loading
- Remove 'ChatterboxTTS/VC loaded' and 'F5-TTS loaded' informational messages
- Keep critical diagnostic messages (failed faiss GPU, package versions)
- Simplify stdout filter code and remove non-functional JAX suppression attempts
2025-10-23 22:03:55 -03:00
diodiogod dffd6f4cab Reduce verbose init logging - remove success messages for FFmpeg, numba JIT, engine loading, and model discovery
- Remove 'FFmpeg available' success message (only show when unavailable)
- Remove 'numba JIT enabled' success message
- Remove 'ChatterboxTTS/VC loaded from bundled package' success messages
- Remove 'F5-TTS loaded from system package' success message
- Remove 'Using ComfyUI ChatterBox models' legacy success message
- Keep important diagnostic messages like 'Failed to load GPU Faiss' and package versions
2025-10-23 22:02:42 -03:00
diodiogod 0f18fc3998 Version 4.11.17
Fix ComfyUI JSON serialization type corruption in unified nodes

- Add modular ConfigSanitizer utility for parameter type validation
- Fix repetition_penalty and other float parameters becoming integers
- Fix device parameter corruption when using Any Switch node
- Skip verbose download messages for already-downloaded models
2025-10-23 13:51:37 -03:00
diodiogod 2f1a8183a8 Skip verbose download messages for already-downloaded IndexTTS-2 models
- Check if model exists and is valid before printing download message
- Add verbose parameter to _verify_model for silent checks
- Only show 'Downloading' messages when actually downloading new models
- Reduces console spam on subsequent runs or when using Any Switch
2025-10-23 13:47:07 -03:00
diodiogod 0af5454c52 Add modular ConfigSanitizer for ComfyUI JSON serialization fix
- Create utils/config_sanitizer.py with ConfigSanitizer class
- Fixes JSON type corruption: floats become ints, strings become floats
- Applies to all numeric parameters that should be floats or ints
- Use in all unified nodes: TTS Text, TTS SRT, Voice Changer
- Handles corrupted device values (0.6 → 'auto', 0 → 'cuda:0')
- Affects first run after reboot or when using Any Switch node
- Modular and reusable approach keeps nodes lean
2025-10-23 13:45:27 -03:00
diodiogod 2ca5b7d6bc Fix Any Switch device parameter corruption in unified nodes
- Add sanitization for corrupted device values (floats like 0.6 instead of strings)
- Apply fix to all unified nodes: TTS Text, TTS SRT, Voice Changer
- Prevents 'device 0.6 is invalid' and similar errors when using rgthree Any Switch node
- On first run after ComfyUI restart, Any Switch corrupts numeric config parameters
- Workaround converts corrupted floats back to proper device strings ('cuda:0', 'auto')
2025-10-23 10:06:50 -03:00
diodiogod 440fba4268 Replace remaining pip install -r requirements.txt references with install.py script 2025-10-22 18:54:52 -03:00
diodiogod b301e1d90f Update installation instructions to use install.py script instead of manual pip install
Replace 'pip install -r requirements.txt' with 'python install.py' across all installation methods (portable, venv, manager). The script automatically handles dependency installation and system dependency detection.

Keep emphasis on using correct Python executable and environment activation where applicable.
2025-10-22 18:51:09 -03:00
diodiogod 0d580ea7c1 Remove HIGGS documentation files from root directory
Move HIGGS_AUDIO_CACHE_BUG_ANALYSIS.md and HIGGS_AUDIO_ISSUE_138_FIX.md to IgnoredForGitHubDocs where they belong.
2025-10-22 18:44:46 -03:00
diodiogod 76379af376 Version 4.11.16
Fix device mismatch in TTS model reuse after CPU offloading

Root cause: When models are detached to CPU and then reloaded to GPU, conditioning tokens (cond_prompt_speech_tokens) remain on CPU. The prepare_conditioning() method in T3 model tries to embed these CPU tokens using GPU embedding layers, causing device mismatch.

Solution: Explicitly move conditioning tokens to model device before calling embedding layers in both ChatterBox and ChatterBox Official 23-Lang T3 models.
2025-10-22 18:22:07 -03:00
diodiogod ef51a9a5ae Version 4.11.15
Fix Higgs Audio DynamicCache AttributeError and cache recreation on device transitions

Technical details:
- Fix ambiguous cache type checking using hasattr() instead of isinstance()
- Replace cache reset logic with explicit StaticCache vs DynamicCache handling
- Add exception handling for cache reset operations
- Set force_cache_recreation flag when clearing caches during CPU unload
- Improve DynamicCache manual construction with better error handling
2025-10-22 15:04:36 -03:00
diodiogod e299ab9719 Version 4.11.14
Skip WeTextProcessing on Windows to avoid confusing install errors

Windows users see failed WeTextProcessing installation due to missing build
tools, even though code has graceful fallback. This confuses users.

Changes:
- Detect Windows platform and skip WeTextProcessing installation
- Use fallback text processing instead (works fine, slightly slower for Chinese)
- Add explanatory message in install summary for Windows users
- No functional impact - all TTS generation works normally
2025-10-22 10:44:06 -03:00
diodiogod 45c7e52aef Skip WeTextProcessing on Windows to avoid confusing install errors
Windows users see failed WeTextProcessing install due to missing build tools,
even though our code has graceful fallback. This causes user confusion.

Changes:
- Detect Windows and skip WeTextProcessing installation on Windows
- Use fallback text processing instead (slower for Chinese but works fine)
- Add explanatory message in install summary for Windows users
- No functional impact - all TTS generation still works normally

Now Windows users will see a clean install without error messages about
WeTextProcessing, while still getting full IndexTTS-2 functionality.
2025-10-22 10:43:45 -03:00
diodiogod aae6151f57 Version 4.11.13
Fix VibeVoice compatibility with transformers 4.51.3+

Technical details:
- Add num_hidden_layers patch to VibeVoiceConfig
- Patch applies wildminder v1.5.1 fix to FushionHub fork
- Fixes DynamicCache initialization error in transformers 4.51.3+
2025-10-22 10:18:18 -03:00
diodiogod 1b92999080 Update README.md 2025-10-21 14:11:27 -03:00
diodiogod 73191eb251 Version 4.11.12
Fix IndexTTS-2 CampPlus model auto-download

CampPlus was using direct hf_hub_download() instead of unified downloader,
causing FileNotFoundError when loading from cache. Now uses unified system.

Changes:
- Add CampPlus to index_tts_downloader MODELS registry
- Replace hf_hub_download() with unified downloader in infer_v2.py
- Ensures CampPlus downloads to models/TTS/IndexTTS/campplus/
- Follows consistent TTS folder policy with other dependencies
2025-10-21 08:43:26 -03:00
diodiogod fc4668a5e1 Fix IndexTTS-2 CampPlus model auto-download using unified downloader
Issue #144: CampPlus model fails to load with FileNotFoundError
Root cause: CampPlus was using hf_hub_download() directly instead of unified
downloader, causing it to look in HuggingFace cache instead of TTS folder

Changes:
- Add CampPlus model to index_tts_downloader MODELS dict
- Replace direct hf_hub_download() with unified downloader in infer_v2.py
- Ensures CampPlus downloads to models/TTS/IndexTTS/campplus/
- Follows consistent download policy with other IndexTTS-2 dependencies
2025-10-21 08:42:41 -03:00
diodiogod 8d4a8b067f Version 4.11.11
Fix ChatterBox Official 23-Lang device mismatch on cached model reuse

Root cause: When models are moved from CPU back to VRAM after being detached,
the model components are properly moved but conditioning data remained on CPU.
This caused embedding layers to fail (input tensors on CPU, weights on CUDA).

Changes:
- TTS engine: Sync self.conds to device in generate() and generate_batch()
- VC engine: Sync self.ref_dict to device in generate()
- Ensures all tensors are on correct device before inference
2025-10-20 18:03:55 -03:00
diodiogod 6b2157d29e Version 4.11.10
Add SSL certificate bypass for IndexTTS-2 W2V-BERT download

Technical details:
- Add TTS_DISABLE_SSL_VERIFY environment variable support
- Implement scoped SSL bypass for transformers downloads
- Restore SSL verification after model loading completes
- Add SSL verification toggle to unified downloader
2025-10-19 08:37:16 -03:00
diodiogod 6405e8494c Version 4.11.9
Fix VibeVoice standalone model detection

Technical details:
- Add support for .safetensor extension (singular)
- Fix _find_standalone_model() to search extra_model_paths.yaml directories
- Add case-insensitive folder name support (vibevoice/VibeVoice)
2025-10-14 18:19:38 -03:00
diodiogod 9f722ad392 Version 4.11.8
Fix F5-TTS trainer wandb import for protobuf compatibility

- Make wandb import optional in trainer.py to prevent crashes
- Add WANDB_AVAILABLE flag with graceful degradation
- Fixes issue with ComfyUI portable protobuf version mismatch
2025-10-14 18:11:40 -03:00
diodiogod eaf5123bd9 Version 4.11.7
Fix RVC HuBERT model loading error

Technical details:
- Add direct .pt loading attempt before safetensors conversion
- Improve error handling for unsupported model formats
- Add fallback mechanism for complex fairseq models
2025-10-13 09:09:35 -03:00
diodiogod 04167d31b9 Version 4.11.6
Fix IndexTTS-2 dependency download violations

Technical fixes:
- Fix w2v-bert-2.0 downloading to .cache instead of TTS folder
- Fix MaskGCT semantic codec downloading to .cache instead of TTS folder
- Fix BigVGAN vocoder downloading to .cache instead of TTS folder
- Update download configurations to include only necessary files
- All IndexTTS-2 dependencies now use unified downloader
2025-10-10 12:09:44 -03:00
diodiogod 917cdc7269 Version 4.11.5
Fix Higgs Audio HuBERT download policy to use unified TTS structure

Technical details:
- Replace direct AutoModel.from_pretrained() calls with unified downloader
- Follow proper download policy: TTS folder > cache check > download to TTS
- Download HuBERT to TTS/hubert/hubert-base-ls960/ instead of .cache
- Maintain consistency with other engines (ChatterBox, VibeVoice, etc.)
- Respect extra_model_paths.yaml configuration for custom directories
2025-10-10 11:46:09 -03:00
diodiogod c473837547 Version 4.11.4
Fix Higgs Audio model discovery with extra_model_paths.yaml support

Technical details:
- Add proper extra_model_paths.yaml integration like other TTS engines
- Support HiggsAudio/, higgs_audio/, and higgsaudio/ parent folder variations
- Add flexible subfolder scanning with Higgs Audio-specific file detection
- Use get_all_tts_model_paths() instead of hardcoded directory paths
- Update download_model and download_tokenizer for local: prefix support
2025-10-10 11:30:29 -03:00
diodiogod 4a7673f583 Version 4.11.3
Fix VibeVoice model discovery with extra_model_paths.yaml support

Technical details:
- Add proper extra_model_paths.yaml integration like ChatterBox
- Support both vibevoice/ and VibeVoice/ parent folder case variations
- Add flexible subfolder scanning with VibeVoice-specific file detection
- Use get_all_tts_model_paths() instead of hardcoded directory paths
2025-10-10 11:11:28 -03:00
diodiogod fea3f769e7 Version 4.11.2
Fix Polish ł mapping in Unicode decomposition

Technical details:
- Remove incorrect ł->lł and Ł->Lł mappings from DECOMPOSITION_MAP
- Polish ł/Ł have no proper Unicode decomposition (distinct letters)
- Prevents duplication bug in phoneme text normalizer
2025-10-10 10:42:23 -03:00
diodiogod d9b306705f Version 4.11.1
Fix ChatterBox v2 special token conversion and update documentation

Technical fixes:
- Fix v2 tag conversion not being applied in main code path
- Move conversion to top of _generate_tts_with_pause_tags
- Fix incorrect token names in CHANGELOG (laugh→laughter)
- Add experimental warning to documentation about limited token effectiveness
2025-10-08 18:06:27 -03:00
diodiogod 56c561c219 Update documentation to reflect experimental nature of v2 special tokens
- Add prominent warning that special tokens have limited effectiveness
- Link to ResembleAI issue #186 showing lack of official documentation
- Note that implementation is ready for future improvements
- Be honest about testing results: tokens may produce minimal effects
- Don't recommend v1 over v2 (v2 has other improvements beyond tags)
2025-10-08 18:05:53 -03:00
diodiogod c562ef827d Fix incorrect token names in CHANGELOG
- Change <laugh> to <laughter> (correct token)
- Change <yawn>, <breathe>, <shouting> to actual tokens
- Use <sniff>, <inhale>, <exhale>, <mumble> which are real tokens
2025-10-08 16:05:52 -03:00
diodiogod 359cb38ad3 Fix ChatterBox v2 special token conversion not being applied
- Move v2 tag conversion to top of _generate_tts_with_pause_tags
- Convert both processed_text and pause_segments
- Ensures all code paths apply <emotion> to [emotion] conversion
- Fixes issue where model was saying "cry" instead of producing crying sound
2025-10-08 15:58:02 -03:00
diodiogod 10ce0da588 Version 4.11.0
Add ChatterBox Official 23-Lang v2 with emotion/sound tokens

Implementation details:
- Add model version selection (v1/v2) to engine configuration
- Implement version-specific model loading with auto-download
- Add special token conversion system for emotions/sounds/effects
- Update bundled code to v2 specification (2454 vocab tokens)
- Fix v1/v2 cache isolation and engine instance separation
- Fix v2 model download path normalization and file detection
- Fix v1 compatibility with correct vocab size and tokenizer
2025-10-08 10:04:48 -03:00
diodiogod 6ea81bc8a4 Fix ChatterBox v1 vocab size mismatch
- Update T3Config to create version-specific configs (v1: 2352 tokens, v2: 2454 tokens)
- Load correct tokenizer file based on version (v1: mtl_tokenizer.json, v2: grapheme_mtl_merged_expanded_v1.json)
- Update is_multilingual property to recognize both v1 and v2 vocab sizes
- Add version parameter to T3Config.multilingual() classmethod

Fixes RuntimeError when loading v1 model with v2 vocab size expectations
2025-10-08 08:47:54 -03:00
diodiogod 452c3902c2 Fix ChatterBox engine instance cache to include model_version
- Add model_version to stable_params in tts_text_node.py
- Add model_version to stable_params in tts_srt_node.py
- Ensures v1 and v2 models use separate engine instance caches
- Prevents v1 from reusing cached v2 engine instance
2025-10-08 08:42:51 -03:00
diodiogod 80b76690ba Fix ChatterBox v1/v2 cache collision
- Add model_version parameter to all create_cache_function calls
- Add model_version parameter to _generate_tts_with_pause_tags signature
- Update all function calls to pass model_version through the chain
- Ensures v1 and v2 models use separate cache entries
2025-10-08 08:39:04 -03:00
diodiogod 0a25e88ff4 Fix ChatterBox v2 model download issues
- Fix path normalization: strip "ChatterBox " prefix for consistent download paths
- Fix conds.pt conversion: exclude from .pt→.safetensors conversion (doesn't exist as safetensors)
- Add model_version parameter to fallback from_local() call in processor
- Prevents 404 error when downloading v2 model files
2025-10-08 08:19:24 -03:00
diodiogod 0703e850f7 Update bundled ChatterBox 23-lang code to v2 specification
Critical fixes for v2 compatibility:
1. Updated vocabulary size from 2352 to 2454 tokens (102 new special tokens)
2. Added v2 preprocessing with NFKD normalization and lowercase support
3. Added Russian stress marking feature with russian_text_stresser integration
4. Updated MTLTokenizer.encode() and text_to_tokens() with v2 parameters

This fixes the "size mismatch for text_emb.weight" error when loading v2 models.
The bundled code now matches ResembleAI's v2 update (commit bf169fe).

Files updated:
- models/t3/modules/t3_config.py: vocabulary 2352 → 2454
- models/tokenizers/tokenizer.py: added preprocess_text(), Russian stress, v2 params
2025-10-08 08:08:49 -03:00
diodiogod 955aff40a7 Fix ChatterBox v2 auto-download and pass model_version through entire chain
Fixed critical issues preventing v2 model auto-download:
1. Removed silent v1 fallback in from_local - now raises FileNotFoundError to trigger download
2. Updated unified_model_interface factory to pass model_version parameter
3. Factory now properly tries from_local first, then from_pretrained for auto-download
4. Added model_version parameter to generate_speech and load_tts_model signatures
5. Pass model_version from engine config through unified TTS node to processor
6. Updated README to clarify v1/v2 files coexist in same directory

Now when v2 is selected but files missing:
- from_local raises error instead of falling back to v1
- from_pretrained downloads missing v2 files automatically
- Model loads correctly with v2 tokenization enabled
2025-10-07 23:54:00 -03:00
diodiogod f3168e940e Add ChatterBox Official 23-Lang v2 support with special emotion tokens
Implemented full support for ChatterBox v2 with enhanced tokenization featuring
30+ special tokens for emotions, sounds, and vocal effects.

Key Features:
- Model version selection (v1/v2) in engine configuration node
- Auto-download of v2 model files (t3_mtl23ls_v2.safetensors, grapheme mappings)
- Special tags system using <emotion> syntax to avoid character switching conflicts
- Automatic conversion of <giggle>, <sigh>, <whisper> etc. to v2 format
- Cache invalidation between v1/v2 to prevent conflicts
- Full integration with character switching and pause tags

Technical Changes:
- Added model_version parameter throughout TTS and SRT processing chain
- Created chatterbox_v2_special_tags.py for tag conversion
- Updated cache keys to include model_version
- Version-aware model loading and validation
- Tag conversion happens after character/pause parsing, before engine

User can now write:
[Alice] Hello! <giggle> Nice to meet you. [pause:0.5] <whisper> Secret!

Documentation added in docs/CHATTERBOX_V2_SPECIAL_TOKENS.md
2025-10-07 23:43:13 -03:00
diodiogod b38667257b Version 4.10.16
Fix VibeVoice local model prefix handling

Technical details:
- Strip local: prefix for model lookups in vibevoice_downloader
- Fix model path resolution when using local: prefix
- Add chunk_combination_method parameter to unified node
- Implement native multi-speaker chunking support
- Use existing ImprovedChatterBoxChunker for intelligent text splitting
2025-10-07 22:17:20 -03:00
diodiogod 1adaa1a22c Version 4.10.15
Fix VibeVoiceDownloader missing download_huggingface_model method

Technical details:
- Add missing download_huggingface_model method to VibeVoiceDownloader class
- Fix method delegation to unified_downloader in both real and dummy classes
- Resolve AttributeError when VibeVoice tokenizer download fails to cache
2025-09-29 08:28:18 -03:00
diodiogod 8855e493f3 Version 4.10.14
Fix VibeVoice auto device detection and model loading issues

Technical details:
- Fix model being moved to CPU after every generation
- Prevent redundant model loading through unified interface
- Add model already-loaded check in VibeVoice adapter
- Fix auto device detection logic running after cache checks
- Resolve aggressive memory management triggering unnecessarily
2025-09-27 10:41:00 -03:00
diodiogod 373983993d Move COMFYUI_REGRESSION_FINDINGS.md to proper docs location 2025-09-26 15:44:43 -03:00
diodiogod abf14e0474 Version 4.10.13
Fix VibeVoice cache invalidation for attention modes

Technical details:
- Fix cache key missing attention_mode parameter
- Add quantize parameter to cache key generation
- Remove debug cache logging statements
2025-09-26 15:27:56 -03:00
diodiogod 793722aa9a Version 4.10.12
Fix VibeVoice accelerate compatibility issues

Technical implementation:
- Add comprehensive transformers patches for accelerate detection
- Fix missing check_tied_parameters_on_same_device function
- Fix missing dispatch_model function for device mapping
- Smart patch application only when compatibility issues detected
- Add accelerate to critical dependencies monitoring
2025-09-26 14:54:28 -03:00
diodiogod c5c227b996 Version 4.10.11
Fix VibeVoice SageAttention compatibility issues

Technical improvements:
- Replace aggressive Qwen2Attention monkey-patching with safer SDPA wrapper approach
- Implement Enemyx-net's temporary F.scaled_dot_product_attention patching method
- Add proper EOS token handling for correct generation termination
- Fix model state corruption when switching between attention modes
- Add automatic fallback to original SDPA if SageAttention fails
- Eliminate need for ComfyUI restart when changing attention modes

Credits: SageAttention implementation approach adapted from @Enemyx-net's VibeVoice-ComfyUI
2025-09-26 12:10:08 -03:00
diodiogod 0ad89351b1 Version 4.10.10
Fix IndexTTS-2 import conflicts with external installations

Technical implementation:
- Add detection for external IndexTTS installations in unified_model_interface.py
- Display clear error message with uninstall instructions when conflicts found
- Remove unnecessary adapter initialization log message
- Resolves import errors from issues #115, #116, and #101
2025-09-26 09:19:08 -03:00
diodiogod df128496ba Fix IndexTTS-2 import conflicts with external installations
- Add detection for external IndexTTS installations that conflict with bundled version
- Display clear error message with uninstall instructions when conflicts are found
- Remove unnecessary adapter initialization log message
- Resolves import errors reported in issues #115, #116, and #101
2025-09-26 09:18:05 -03:00
diodiogod cc544e9558 Version 4.10.9
Fix Python 3.12 VRAM spikes with ComfyUI compatibility patch

Technical implementation:
- Add automatic CUDNN benchmark disabling for Python 3.12
- Implement compatibility patch system in utils/comfyui_compatibility.py
- Apply fix before TTS generation in unified text and SRT nodes
- Resolve ComfyUI v0.3.57+ regression affecting memory management
2025-09-26 01:18:35 -03:00
diodiogod 827eb78428 Fix PyTorch installation commands in warnings
- Corrected invalid torchaudio+cu128 syntax to proper pip install torchaudio --index-url format
- Updated both torchaudio and torchvision warning commands to use cu129 index
- Fixes installation errors where users got 'Expected end or semicolon' pip errors
2025-09-26 00:13:32 -03:00
diodiogod ed0888bea4 Version 4.10.8
Fix install.py Python version detection and emoji encoding issues

Technical details:
- Fix Windows py launcher bypassing venv detection
- Add clear warnings for Python version mismatches
- Remove emojis from install script to prevent encoding issues
- Improve error messaging for py vs python command usage
2025-09-25 21:50:50 -03:00
diodiogod 2b4d97a47a Version 4.10.7
Add VibeVoice wildminder format compatibility

Technical changes:
- Implement [1] [2] [3] [4] numeric character tag support for native multi-speaker
- Replace synthetic voice samples with None for true zero-shot generation
- Increase CFG scale limit from 5.0 to 10.0
- Add direct Speaker mapping for numeric character tags
- Maintain full backward compatibility with existing character systems
2025-09-25 18:05:24 -03:00
diodiogod aea6fc2cbe Increase CFG scale limit from 5.0 to 10.0 for VibeVoice
For compatibility with wildminder's VibeVoice implementation, increased
the maximum CFG scale from 5.0 to 10.0 in VibeVoice Engine Node.

This allows users to use higher guidance values which can improve
quality with fewer inference steps, matching the capabilities of
the official VibeVoice implementation.
2025-09-25 17:58:54 -03:00
diodiogod 05c3fcf297 Add [1] [2] [3] [4] support for wildminder VibeVoice format compatibility
For compatibility with wildminder's VibeVoice implementation, added support
for numeric character tags [1] [2] [3] [4] in native multi-speaker mode.

- Numeric characters [1]-[4] now map directly to Speaker 1-4 voice inputs
- Works alongside existing character tag to speaker mapping system
- Mixed usage supported: [1] [2] [Alice] maps to Speakers 1, 2, 3
- Character parser unchanged - handles all existing tags normally
- VibeVoice-specific enhancement in native multi-speaker mode only

This provides users a convenient shorthand format while maintaining
full backward compatibility with existing character tag systems.
2025-09-25 17:54:36 -03:00
diodiogod 23199d6942 Implement true zero-shot hybrid voice generation
Replace synthetic voice samples with None values for empty speakers, enabling
genuine zero-shot generation following wildminder's approach:

- Empty speakers now return None instead of synthetic voice samples
- Processor skips voice prompt creation for None values
- Results in natural zero-shot voices instead of artificial synthetic ones
- Maintains voice cloning for speakers with provided samples
- Enables true hybrid generation: cloned + zero-shot voices in same script

This produces much more natural-sounding voices for speakers without
voice references while preserving voice cloning capabilities.
2025-09-25 15:11:52 -03:00
diodiogod 1b0ef785de Fix local: prefix pattern for VibeVoice models
Follow ChatterBox/F5-TTS pattern correctly:
- Show ALL official models always (vibevoice-1.5B, vibevoice-7B)
- ADD local: versions when downloaded (local:vibevoice-1.5B, local:vibevoice-7B)
- Show local: versions for standalone files (local:my-custom-model)

Users now see both regular and local: versions when models are downloaded,
providing choice between smart loading vs direct local loading.
2025-09-25 15:07:12 -03:00
diodiogod dea4f7f06c Add standalone .safetensors model support for VibeVoice
- Support single .safetensors files alongside traditional directory models
- Auto-discover standalone models in TTS and checkpoints folders
- Add 'local:' prefix for consistency with ChatterBox/F5-TTS naming convention
- Support sidecar config files (model.config.json, model.preprocessor.json)
- Fallback to default configs based on model name if sidecar missing
- Use ComfyUI's load_torch_file() for efficient device-direct loading
- Enable custom fine-tuned VibeVoice model distribution and usage

Users can now drop custom VibeVoice .safetensors files into TTS folder and
they'll appear as 'local:model-name' in dropdown, ready to use.
2025-09-25 14:56:54 -03:00
diodiogod 90bf7a8ac0 Improve VibeVoice tokenizer loading with unified folder support
- Implement ChatterBox-style tokenizer loading pattern: local → cache → download to unified
- Add tokenizer.json to download manifests for both 1.5B and 7B models
- Fix tokenizer class mismatch warnings by loading directly from tokenizer files
- Clean up verbose logging messages (commented out for debugging availability)
- Ensure tokenizer files go to TTS/ unified folder instead of HF cache duplication

Prevents potential 'VibeVoiceConfig' object crashes and eliminates warnings while
maintaining compatibility and following project's unified download philosophy.
2025-09-25 11:24:23 -03:00
diodiogod 59188909c3 Version 4.10.6
Fix voice discovery extra_model_paths.yaml integration

Technical details:
- Fix path comparison logic in voice discovery system preventing extra_model_paths voice directories from being scanned
- Add proper path normalization using os.path.normpath() for accurate duplicate detection
- Apply fix to all voice discovery functions: main scanning, character discovery, and alias loading
- Maintain backward compatibility with existing voice directory structure
2025-09-23 22:10:54 -03:00
diodiogod 8a5b8dad45 Version 4.10.5
Add centralized FFmpeg dependency handling with graceful fallbacks

Technical details:
- Create centralized FFmpegUtils class for dependency detection
- Add FFmpeg status to initial startup logs alongside package versions
- Implement graceful MP3 conversion fallbacks in vocal separation
- Maintain backward compatibility with existing FFmpeg usage across project
2025-09-23 21:08:05 -03:00
diodiogod c33cf24989 Version 4.10.4
Fix VibeVoice compatibility with transformers 4.56+

Technical details:
- Enable previously disabled transformers compatibility patches
- Fix _prepare_cache_for_generation argument mapping for newer transformers versions
- Maintain backward compatibility with older transformers versions
2025-09-23 20:47:31 -03:00
diodiogod 896226006b Version 4.10.3
Fix F5-JP Japanese tokenization bypass

Technical details:
- Fix F5-JP using Chinese pinyin conversion for Japanese text
- Update F5-JP to newer model version (JA_21999120)
- Bypass pinyin processing specifically for F5-JP model
2025-09-23 16:02:04 -03:00
diodiogod 9a79dfd1f1 Version 4.10.2
Fix ChatterBox Official 23-Lang device mismatch error

Technical details:
- Fix speaker encoder device placement in S3Gen model
- Ensure speaker encoder moves to GPU when needed
- Resolve torch.cuda.FloatTensor vs torch.FloatTensor errors
2025-09-23 15:48:09 -03:00
diodiogod 1cea7ce4f1 Version 4.10.1
Add missing F5-TTS workflow and improve documentation

Technical fixes:
- Add comprehensive tooltips to Phoneme Text Normalizer node inputs
- Add Text Processing Nodes section to PROJECT_INDEX.md
- Document 📝 Phoneme Text Normalizer with all 4 processing methods
- Add missing workflow file reference for user testing
2025-09-23 13:22:53 -03:00
diodiogod 06c0f827a2 Version 4.10.0
Add universal multilingual text preprocessing support

Implementation details:
- Add 📝 Phoneme Text Normalizer node with 4 processing methods
- Implement IPA phonemization using espeak backend
- Add Unicode decomposition for special characters
- Add cross-platform phonemizer support (Windows/Linux/Mac)
- Add F5-Polish model auto-download from reference implementation
- Improve multilingual F5-TTS pronunciation issues (refs #22, #54, #58)
- Add Polish language support with high-quality model integration
2025-09-23 09:52:26 -03:00
diodiogod 0c68863f92 Fix multilingual F5-TTS phonemization with Windows espeak support
- Add phonemizer dependency to requirements.txt for IPA phonemization
- Update Phoneme Text Normalizer to support both standard phonemizer and Windows espeak-phonemizer-windows
- Implement fallback chain: standard phonemizer → Windows espeak → Unicode decomposition
- Match reference Polish F5-TTS implementation exactly (espeak backend, with_stress=True)
- Successfully tested with Polish text: 'Cześć, jak się masz?' → 'tʃˈɛɕtɕ jˈak ɕɛ mˈaʃ'
2025-09-23 01:59:52 -03:00
diodiogod 716e4e5e13 Fix Phoneme Text Normalizer to match reference Polish F5-TTS
Updates:
- Changed IPA phonemization to use exact same method as reference implementation
- Use phonemizer package directly with espeak backend
- Added with_stress=True and preserve_punctuation=True parameters
- Applied same regex cleanup patterns as reference
- Fixed buggy character-by-character change display
- Handle different text lengths properly in debug output

Now the 📝 Phoneme Text Normalizer should produce the same IPA output
as the working Polish F5-TTS reference implementation.
2025-09-23 01:39:25 -03:00
diodiogod 30e359b286 Remove auto_phonemization parameter from F5-TTS Engine node
The auto_phonemization parameter is now removed from the F5-TTS Engine node
since the old auto-phonemization system has been disabled in favor of the
dedicated 📝 Phoneme Text Normalizer node.

Changes:
- Removed auto_phonemization from INPUT_TYPES
- Removed auto_phonemization parameter from create_engine_adapter function
- Set auto_phonemization to False with explanatory comment
- Updated print statement to show auto_phonemization=False

Users now have explicit control through the 📝 Phoneme Text Normalizer node
instead of a hidden automatic system.
2025-09-23 01:31:23 -03:00
diodiogod 0be173f7af Fix tokenizer.json detection for local F5-TTS models
Bug: Local model loading wasn't properly detecting tokenizer.json files
because it was overwriting vocab_file instead of using separate variables.

Changes:
- Added separate tokenizer_file variable for local model detection
- Added detection messages to show which files are found
- Prioritize tokenizer.json over vocab.txt when both exist
- Store tokenizer information for text processing pipeline

Now local Polish models with tokenizer.json should be detected properly.
2025-09-23 01:29:39 -03:00
diodiogod 4a4df30db8 Disable old auto-phonemization system
The old auto-phonemization system is now disabled in favor of the
dedicated 📝 Phoneme Text Normalizer node which gives users full control.

Changes:
- Modified should_use_phonemization() to always return False
- Added deprecation notice explaining the replacement
- Users now have explicit control via the Phoneme Text Normalizer node

This prevents unexpected automatic phonemization and respects the
auto_phonemization=False setting properly.
2025-09-23 01:19:03 -03:00
diodiogod 9b0ed4740e Add complete tokenizer.json support and multilingual text preprocessing
Features Added:
- 📝 Phoneme Text Normalizer node with 4 processing methods:
  * Unicode Decomposition (ą→a+combining accent)
  * IPA Phonemization with language detection
  * Character Mapping (ASCII fallbacks)
  * Pass-through mode
- 🔤 Full tokenizer.json support in F5-TTS engine:
  * Auto-detection of tokenizer.json and vocab.txt files
  * HuggingFace tokenizer integration in get_tokenizer()
  * Smart download logic for both file types
  * Automatic tokenizer selection during inference
- 🌍 Enhanced multilingual support:
  * Japanese models with complex tokenization
  * Polish/German/French special character handling
  * 15+ language auto-detection
  * Graceful fallbacks at every processing level

Technical Implementation:
- Updated F5-TTS model loading to detect and use tokenizer.json
- Enhanced text processing pipeline with tokenizer type detection
- Added helper functions for tokenizer file downloads
- Integrated HuggingFace tokenizers package support
- Backward compatible with existing vocab.txt models

This resolves multilingual TTS issues and enables proper support for
advanced F5-TTS models trained with HuggingFace tokenizers.
2025-09-23 01:14:52 -03:00
diodiogod df4ef85579 Version 4.9.25
Fix transformers compatibility handling

Technical changes:
- Disable legacy DynamicCache patches that masked version conflicts
- Add startup version check for transformers >=4.51.3
- Provide clear upgrade instructions for users with old transformers
- Remove automatic compatibility patches from VibeVoice engine
2025-09-21 23:58:45 -03:00
diodiogod 52e0083a99 Version 4.9.24
Remove incorrect 'tn' dependency and fix text normalization fallback

Technical implementation:
- Remove wrong 'tn' package from requirements (time utilities, not text processing)
- Fix tn.chinese/tn.english imports (these are WeTextProcessing internal modules)
- Unify fallback logic across all platforms (Windows/Mac/Linux)
- Add consistent WeTextProcessing → wetext → dummy normalizer chain
- Improve error messaging for missing text processing packages
2025-09-21 23:38:13 -03:00
diodiogod 72637b141d Version 4.9.23
Fix IndexTTS-2 text normalization fallback handling

Technical implementation:
- Add robust fallback chain: WeTextProcessing → wetext → dummy normalizer
- Implement graceful degradation when both packages fail to import
- Add clear user messaging about text processing capabilities
- Replace empty string returns with original text for better fallback
- Add dummy normalizer class for systems without any text processing
2025-09-18 16:11:21 -03:00
diodiogod 045f724219 Version 4.9.22
Fix IndexTTS-2 text normalization dependency handling

Technical implementation:
- Move WeTextProcessing from standard requirements to smart installer
- Add graceful fallback from WeTextProcessing to wetext package
- Implement proper testing for text normalization packages
- Handle Windows pynini compilation failures elegantly
2025-09-18 15:06:02 -03:00
diodiogod e7d191612c Version 4.9.21
Fix IndexTTS-2 auto-download system with complete model verification

Technical implementation:
- Move auto-download logic from unified interface to IndexTTS engine initialization
- Add comprehensive model verification using IndexTTSDownloader._verify_model()
- Handle mixed missing files scenarios (partial downloads, corrupted files)
- Improve error messages showing specific missing files and download status
- Fix auto-download trigger timing to occur before model loading attempt
2025-09-18 14:41:36 -03:00
diodiogod b24bd37897 Version 4.9.20
Update bundled IndexTTS-2 to latest upstream version

Technical implementation:
- Sync with https://github.com/index-tts/index-tts commits 8336824..64cb31a
- Add normalize_emo_vec() function from commit 8aa8064 for voice identity protection
- Add _load_and_cut_audio() helper from commit 0828dcb for 15s audio truncation
- Add persistent cache clearing from commit 64cb31a with torch.cuda.empty_cache()
- Fix BigVGAN CUDA kernel import path from commits ee23371, e409c4a
- Enhance emotion vector logging for better user feedback
2025-09-18 14:19:30 -03:00
diodiogod 21854b7fae Version 4.9.19
Fix WeTextProcessing package name for IndexTTS-2 text normalization

Technical fix:
- Change from wetext>=0.0.9 to WeTextProcessing (correct package name)
- WeTextProcessing is the actual PyPI package for Chinese/English text normalization
- Keep tn as fallback dependency for Linux systems
- Fix import compatibility issues with IndexTTS-2 text processing
2025-09-18 01:08:18 -03:00
diodiogod 243d8d3a09 Version 4.9.18
Fix IndexTTS-2 emotion radar center dragging issues

Technical improvements:
- Add smart deadzone to prevent emotion switching near center point
- Fix center crossing bug where emotions bounced back when dragging past zero
- Implement directional projection only within deadzone for smooth transitions
- Add click counters for both center clicks and individual emotion label clicks
- Improve button text vertical centering in radar chart interface
2025-09-18 00:14:27 -03:00
diodiogod b30388a6bb Version 4.9.17
Fix IndexTTS-2 extra_model_paths integration

Technical details:
- Update IndexTTS-2 downloader to use extra_model_paths configuration
- Implement local: prefix pattern matching F5TTS/ChatterBox style
- Add auto-detect model discovery across configured paths
- Fix model path resolution for both downloads and local models
2025-09-17 23:28:49 -03:00
diodiogod 5e4c39b3d2 Version 4.9.16
Fix VibeVoice auto device detection

Technical details:
- Improve auto mode CUDA detection logic
- Ensure model stays on GPU for subsequent generations
- Fix model being moved to CPU between runs
2025-09-17 21:19:58 -03:00
diodiogod 2ad3e95db5 Version 4.9.15
Add missing tn text normalization dependency for IndexTTS-2

Technical fix:
- Add tn package as fallback dependency for IndexTTS-2 text processing
- IndexTTS-2 uses wetext as primary and tn as fallback normalizer
- Some Linux systems fail wetext import and need tn fallback
- Prevents ModuleNotFoundError for tn.chinese module
2025-09-17 20:53:41 -03:00
diodiogod ef3aba3353 Version 4.9.14
Add graceful OOM handling for IndexTTS-2 large audio files

Technical implementation:
- Replace upfront audio validation with post-OOM analysis
- Use fast soundfile.info() for duration checking (header-only)
- Catch torch.OutOfMemoryError specifically and provide detailed feedback
- Add audio duration analysis with specific recommendations
- Handle OOM in both single and multi-character segment generation
2025-09-17 20:36:08 -03:00
diodiogod 307a482a7e Version 4.9.13
Fix IndexTTS-2 module import KeyError with bundled version

Technical fix:
- Simplify sys.modules override to prevent KeyError during import resolution
- Remove conflicting indextts modules before importing bundled version
- Keep bundled module available during IndexTTS-2 initialization
- Improve module path resolution for nested indextts imports
2025-09-17 20:13:32 -03:00
diodiogod ca156a103f Version 4.9.12
Fix IndexTTS-2 bundled import conflicts with system packages

Technical fix:
- Add sys.modules override to force bundled indextts during import
- Prevent conflicts with system-installed indextts packages
- Ensure bundled IndexTTS-2 code resolves internal imports correctly
- Restore original module state after import to avoid side effects
2025-09-17 19:55:17 -03:00
diodiogod d11db08134 Version 4.9.11
Fix faiss circular import crash in Docker environments

Technical fix:
- Add exception handling for AttributeError in dependency checker
- Handle faiss Float32 circular import issue in containerized environments
- Gracefully detect faiss availability despite import problems
- Prevent startup crashes in Docker/OpenSUSE setups
2025-09-17 17:02:47 -03:00
diodiogod 8f411a5ef7 Version 4.9.10
Fix IndexTTS-2 temp file location to use ComfyUI directory

Technical fix:
- Change IndexTTS-2 temp file creation to use folder_paths.get_temp_directory()
- Replace system temp directory with ComfyUI temp directory
- Improves file organization and cleanup consistency
2025-09-17 16:06:52 -03:00
diodiogod c1dbb0992e Version 4.9.9
Fix install.py numpy downgrade and requirements parsing

Technical fixes:
- Move opencv-python to problematic packages with --no-deps installation
- Fix requirements.txt comment parsing to handle inline comments
- Prevent numpy downgrade from opencv-python dependency conflicts
- Improve ensure_requirements_installed comment handling
2025-09-17 13:43:10 -03:00
diodiogod 13f1f14ac9 Version 4.9.8
Fix install script numpy downgrade issue with faiss-gpu

Technical details:
- Install faiss-gpu-cu12 with --no-deps to prevent numpy downgrade
- faiss-gpu has numpy<2 constraint that forces downgrade from numpy 2.x to 1.26.4
- All other faiss-gpu dependencies already satisfied in environment
- Preserves RVC GPU acceleration while respecting numpy compatibility logic
2025-09-17 00:29:23 -03:00
diodiogod 89df429ab5 Version 4.9.7
Fix IndexTTS-2 OOM with long emotion audio files

Technical details:
- Add emotion audio truncation to 20 seconds to prevent OOM
- Research confirms Wav2Vec2-BERT has quadratic memory complexity
- Long audio files cause crashes in attention mechanism
- Truncation preserves emotional characteristics while staying within memory limits
2025-09-17 00:22:28 -03:00
diodiogod 77dce79ed5 Fix IndexTTS-2 tensor dimension error with direct audio input
- Fix Expected 2D Tensor, got 3D error when using opt_narrator
- Add proper dimension handling for ComfyUI audio tensors (3D -> 2D)
- Convert batch dimension and stereo to mono for IndexTTS-2 compatibility
- Align tensor processing with existing IndexTTS-2 patterns
2025-09-17 00:22:28 -03:00
diodiogod 8a4941e12d Update bug_report.md 2025-09-16 20:04:33 -03:00
diodiogod 256a5de28e Version 4.9.6
Fix ChatterBox Official 23-Lang language resolution and narrator input

Technical details:
- Fix resolve_language_alias AttributeError across multiple engines
- Fix ChatterBox Official 23-Lang not using opt_narrator audio input
- Fix phonemizer_utils language alias resolution method call
- Fix character_parser validation module language resolution
2025-09-16 19:54:18 -03:00
diodiogod 89aeb223a9 Version 4.9.5
Fix missing julius dependency for Higgs Audio and RVC components

Technical fix:
- Add julius to manual dependency list for descript-audiotools --no-deps installation
- Resolves missing julius import error when using Higgs Audio 2 engine
- Julius required by RVC demucs models for audio separation DSP operations
- References issue #82 julius dependency missing error
2025-09-16 19:33:48 -03:00
diodiogod d619d38747 Version 4.9.4
Fix VibeVoice garbage collection and auto device detection issues

Technical fixes:
- Add defensive gc import handling to prevent UnboundLocalError crashes
- Implement proper auto device detection for VibeVoice after model unloading
- Fix ComfyUI model wrapper gc.collect() scoping issues
- Improve device persistence for subsequent generations
- References issue #81 VibeVoice garbage collection errors
2025-09-16 19:21:47 -03:00
diodiogod 22647e91df Add VibeVoice integration workflow to README
- Add VibeVoice Integration workflow to Specific Workflows table
- Positioned after IndexTTS-2 for logical engine ordering
- Links to example_workflows/VibeVoice integration.json
- Describes long-form TTS with multi-speaker support
2025-09-16 19:00:13 -03:00
diodiogod 2318a3166f Version 4.9.3
Improve Character Voices and IndexTTS-2 tooltips for better clarity

Technical improvements:
- Enhance Character Voices node tooltips with file requirement explanations
- Add multiline formatting to IndexTTS-2 emotion_control input tooltip
- Clarify .txt vs .reference.txt priority system in voice folders
- Reference issue #69 tooltip improvement suggestions
2025-09-16 18:52:10 -03:00
diodiogod e6369cf2d8 Version 4.9.2
Fix VibeVoice BFloat16 compatibility with RVC voice conversion

Technical implementation:
- Convert BFloat16 tensors to Float32 in VibeVoice engine output
- Add defensive BFloat16 handling in voice changer node
- Ensure compatibility across all audio processing components
- Resolves issue #47 where VibeVoice output failed RVC conversion
2025-09-16 18:30:50 -03:00
diodiogod 3316851ca8 Version 4.9.1
Fix IndexTTS-2 device mismatch after unload and add workflow

Technical fixes:
- Add IndexTTS-2 recovery logic in model manager for device synchronization
- Clear cached model config and force device reload after CPU offloading
- Handle both normal reuse and force_reload scenarios
- Add IndexTTS-2 integration workflow to README examples
2025-09-16 18:12:57 -03:00
diodiogod 83e59a15fa Add IndexTTS-2 integration workflow to README
- Add 🌈 IndexTTS-2 Integration workflow to Specific Workflows table
- Mark as 'New in v4.9' with proper status
- Links to example_workflows/🌈 IndexTTS-2 integration.json
- Positioned at top of specific workflows for visibility
2025-09-16 18:12:25 -03:00
diodiogod 991be1ca85 Fix IndexTTS-2 device mismatch error after model unload/reload
- Add IndexTTS-2 specific recovery logic in model manager
- Handle device synchronization when model is corrupted after CPU offloading
- Clear cached model config and force device reload for all components
- Add recovery for both normal reuse and force_reload scenarios
- Matches existing patterns used for Higgs Audio and VibeVoice

Fixes: RuntimeError: Expected all tensors to be on the same device,
but got weight is on cpu, different from other tensors on cuda:0
2025-09-16 15:55:35 -03:00
diodiogod e9025d1eff Update emotion control feature description
- Clarify ChatterBox uses exaggeration parameter
- Highlight IndexTTS-2 advanced emotion control system
- Add link to IndexTTS-2 emotion control documentation
- Keep feature list concise and informative
2025-09-16 15:16:00 -03:00
diodiogod ce8059ef97 Update README.md 2025-09-16 15:08:30 -03:00
diodiogod 96e1a5242f Version 4.9.0
Add IndexTTS-2 TTS engine with unified emotion control

Implementation details:
- Integrate IndexTTS-2 model with QwenEmotion analysis
- Add unified emotion_control input architecture
- Implement dynamic text emotion templates with {seg} processing
- Add character tag emotion system [Character:emotion_ref]
- Create emotion vector control with 8 emotions
- Add stable audio content hashing for reliable caching
- Fix PyTorch/transformers API compatibility issues
- Create comprehensive emotion control documentation
2025-09-16 15:07:17 -03:00
diodiogod 0e7b9a0f85 Merge branch 'feature/indextts2-engine-implementation' 2025-09-16 15:05:00 -03:00
diodiogod b6a348d262 Merge branch 'main' of https://github.com/diodiogod/TTS-Audio-Suite 2025-09-16 15:04:50 -03:00
diodiogod b0f6bfe6cf Fix table of contents and add v4.9.0 to timeline
- Fix broken table of contents link for IndexTTS-2 section
- Add v4.9.0 to project evolution timeline with IndexTTS-2 Emotion Control milestone
- Update timeline formatting to accommodate new version
2025-09-16 15:00:40 -03:00
diodiogod 648ef72c7c Fix IndexTTS-2 model documentation placement
- Move IndexTTS-2 model downloads from wrong location to proper section 10
- Add IndexTTS-2 Models section in the installation/downloads area
- Include model structure, download links, and feature overview
- Update section numbering (Restart ComfyUI becomes section 11)
- Update version references to v4.9.0
2025-09-16 14:57:51 -03:00
diodiogod dcfae4fc81 Update README.md with IndexTTS-2 features and model documentation
- Add IndexTTS-2 to Multi-Engine TTS feature list
- Add IndexTTS-2 With Emotion Control section to What's New
- Add IndexTTS-2 Model Downloads section with correct model structure
- Include proper repository links (IndexTeam/IndexTTS-2, facebook/w2v-bert-2.0)
- Document unified emotion control architecture and features
- Add comprehensive IndexTTS-2 Emotion Control Guide
2025-09-16 14:52:56 -03:00
diodiogod 49c4b8e7a8 Merge pull request #84 from diodiogod/hotfix/pytorch-installation-fix
HOTFIX: Fix critical PyTorch installation bug causing CPU torchaudio
2025-09-16 10:47:45 -03:00
diodiogod d2249d4934 Fix IndexTTS-2 model loading warnings for API compatibility
- Update deprecated torch_dtype="float16" to dtype=torch.float16 in QwenEmotion model loading
- Fix GenerationMixin inheritance in GPT2InferenceModel classes (model.py and model_v2.py)
- Add proper GenerationMixin import to ensure future transformers v4.50+ compatibility
- Eliminates deprecation warnings while maintaining full backward compatibility

These changes address API modernization warnings and prevent future breaking changes.
2025-09-16 10:36:12 -03:00
diodiogod f4388a1731 Clean debug logs and rename QwenEmotion to IndexTTS-2 Text Emotion
- Remove cluttering 🐛 debug logs while keeping important emotion info
- Fix Character Voices emotion_audio detection in IndexTTS engine
- Fix Character Voices audio path extraction in adapter
- Rename 🧠 QwenEmotion Text Analysis → 🌈 IndexTTS-2 Text Emotion
- Update emoji from 🧠 to 🌈 for visual consistency with Emotion Vectors
- Update all references in registry mappings and tooltips
- Clean up debug messages with new branding

Visual consistency: Both IndexTTS-2 emotion nodes now use 🌈 rainbow emoji
2025-09-16 10:28:18 -03:00
diodiogod e49af97e6d Add dynamic {seg} template support for QwenEmotion per-segment analysis
- Add {seg} placeholder support in QwenEmotion emotion_text templates
- Implement dynamic template processing in IndexTTS processor
- Replace {seg} with actual segment text for contextual emotion analysis
- Add is_dynamic_template flag to track template usage
- Update default example to showcase dynamic template feature
- Add debug logging for template processing

Example: "Angry man shouting: {seg}" becomes "Angry man shouting: Get out!"
for each text segment, enabling context-aware emotion analysis.
2025-09-16 10:01:20 -03:00
diodiogod 4531a6a756 Implement unified emotion control system for IndexTTS-2
- Remove emotion_alpha from QwenEmotion node (engine controls intensity)
- Add proper QwenEmotion model selection with local model detection
- Create 🧠 QwenEmotion Text Analysis node with auto-download support
- Update IndexTTS-2 Engine to accept unified emotion_control input
- Replace AnyType with proper connection hints for emotion inputs
- Validate QwenEmotion models for completeness (required files check)
- Maintain backward compatibility with Character Voices opt_narrator
2025-09-16 09:06:04 -03:00
diodiogod 9a71ee2d61 Fix IndexTTS-2 cache system for consistent segment caching
- Add seed parameter support throughout IndexTTS-2 pipeline
- Fix cache key generation using stable audio content hashing
- Resolve temporary file path invalidation preventing cache hits
- Use centralized audio_hash utility for consistent audio identification
- Enable proper cache hits for identical text/character/emotion combinations
2025-09-15 23:31:18 -03:00
diodiogod ebf4dddb0a Update PROJECT_INDEX.md with IndexTTS-2 and radar chart entries
- Add IndexTTS-2 engine node and 🌈 Emotion Vectors node to engine section
- Add radar chart web interface files to web components section
- Maintain concise documentation style with brief, descriptive entries
2025-09-15 22:34:27 -03:00
diodiogod e2253d4543 Enhance IndexTTS-2 radar chart with dynamic color blending and refined interactions
- Implement dynamic color blending for connecting lines based on emotion values
- Increase color vibrancy with 85% brightness and higher opacity for better visibility
- Fix label clicking to properly increment by 0.1 without triggering drag
- Maintain dot clicking for immediate dragging capability
- Rename node to '🌈 IndexTTS-2 Emotion Vectors' across all files for visual distinction
- Update tooltips and comments to reflect new rainbow branding
- Ensure persistent glow effects during and after dragging operations

Color system now creates vibrant emotional mood ring effect that clearly shows
emotion combinations while maintaining excellent contrast and usability.
2025-09-15 22:22:38 -03:00
diodiogod c61e94e26d Implement IndexTTS-2 radar chart with click-based glow effects
- Replace emotion sliders with interactive radar/star chart visualization
- Add click-based glow effects for dots and labels (persistent until next click)
- Implement label click to increment emotion values by 0.1 with visual feedback
- Add Export/Import functionality for emotion configurations
- Support real-time dragging for precise value control
- Maintain 0.0-1.2 emotion value range with proper validation
- Include Random/Reset buttons for quick emotion management
2025-09-15 22:04:52 -03:00
diodiogod 73ea1f5979 Fix IndexTTS-2 tensor dimension issue with direct audio input
- Fix 3D tensor handling when saving character/narrator audio to temp files
- Ensure 2D tensor format (channels, samples) for torchaudio.save
- Resolves 'Expected 2D Tensor, got 3D' error with opt_narrator direct audio
- Affects both TTS Text and SRT processing workflows
2025-09-15 17:16:10 -03:00
diodiogod 14436035ae Fix IndexTTS-2 emotion resolution and character parser integration
- Add centralized emotion resolution to character parser
- Fix QwenEmotion variable scope error in IndexTTS-2 inference
- Refactor emotion resolution to avoid code duplication
- Enable both connected engine emotions and character-specific emotion overrides
- Support [Alice:Bob] syntax for voice identity with emotion reference
- Clean integration with existing character alias system
2025-09-15 10:48:43 -03:00
diodiogod e8b151550e Add IndexTTS-2 SRT subtitle processing support
Implement complete SRT subtitle processing for IndexTTS-2 engine with:
- Character switching with emotion references using existing parser
- Full SRT timing support (stretch_to_fit, pad_with_silence, smart_natural, concatenate)
- Pause tag support with emotion control
- Language mapping integration for multilingual support
- Unified interface integration following VibeVoice pattern

Key components:
- IndexTTSSRTProcessor: Clean SRT processor using existing IndexTTSProcessor
- Language mapper support: Added index_tts engine type with multilingual handling
- Unified SRT node integration: Added IndexTTS-2 to unified TTS SRT interface
2025-09-15 09:22:28 -03:00
diodiogod 7503aec464 Fix IndexTTS-2 character switching functionality
- Set up character parser with available characters and aliases in processor
- Use global character_parser instance instead of creating new ones
- Add missing _generate_multi_character_segments method to adapter
- Fix character mapping to use "audio_only" engine type for proper voice discovery
- Add missing torchaudio import for character voice loading
- Enable proper Alice -> female_01 alias resolution with German language support

Character switching now works: [Alice] properly switches to female_01 voice
while [female_01] uses the voice directly without language defaults.
2025-09-13 08:49:07 -03:00
diodiogod 7af065de79 Add Python version performance warning for IndexTTS-2
- Add warning for non-Python 3.13 environments about VRAM spikes
- IndexTTS-2 tested smoothly on Python 3.13 vs high VRAM usage on Python 3.12
- Helps users understand performance differences across Python versions
2025-09-12 23:30:50 -03:00
diodiogod 6b629580e1 Fix missing set_engine_aware_default_language method in CharacterParser
Add delegation method to CharacterParser that calls the language resolver's
set_engine_aware_default_language method. This fixes the AttributeError in
F5-TTS engine when trying to set engine-aware default language.
2025-09-12 22:53:32 -03:00
diodiogod a25c6b42f2 Add missing pyjson5 dependency for IndexTTS-2
- IndexTTS-2 requires pyjson5 for JSON5 config parsing
- Missing dependency caused ImportError and fallback behavior
- Fixes 'No module named json5' errors in IndexTTS-2 initialization
2025-09-12 20:30:45 -03:00
diodiogod 157d106d38 Fix critical PyTorch installation bug causing CPU torchaudio
- Add CUDA suffixes to torchaudio and torchvision package names
- Prevents mixed GPU PyTorch + CPU torchaudio installation
- Fixes major performance degradation and VRAM issues
- Resolves user reports of slow IndexTTS-2 generation

The bug was in lines 293-296 where package names lacked CUDA suffixes
causing torchaudio to install CPU version despite --index-url pointing
to CUDA repository. This caused constant GPU↔CPU memory transfers.
2025-09-12 17:41:11 -03:00
diodiogod a3a7c4a50c Add PyTorch consistency warning to unified model interface
- Detects mixed CUDA/CPU PyTorch installations on first model load
- Warns about GPU PyTorch + CPU TorchAudio performance issues
- Shows exact fix commands with proper CUDA index URLs
- Prevents performance degradation debugging sessions
- Only shows warning once per session
2025-09-12 17:38:40 -03:00
diodiogod b8264275dd Fix critical PyTorch installation bug causing CPU torchaudio
- Add CUDA suffixes to torchaudio and torchvision package names
- Prevents mixed GPU PyTorch + CPU torchaudio installation
- Fixes major performance degradation and VRAM issues
- Resolves user reports of slow IndexTTS-2 generation
2025-09-12 17:24:13 -03:00
diodiogod 79ec690310 Refactor IndexTTS-2 emotion controls into separate options node
- Extract emotion vector controls from main engine node into dedicated IndexTTS-2 Emotion Vectors Options node
- Use official IndexTTS-2 emotion labels: "afraid" instead of "fearful" to match model expectations
- Implement clean UI with capitalized emotion names (Happy, Angry, Sad, Surprised, Afraid, Disgusted, Calm, Melancholic)
- Output official lowercase keys that match IndexTTS-2 EMOTION_LABELS order
- Improve tooltips with detailed emotion descriptions and intensity ranges
- Maintain backward compatibility through proper parameter mapping
2025-09-12 15:39:34 -03:00
diodiogod b4b0c48353 Fix IndexTTS-2 emotion_audio tensor handling
- Convert emotion_audio tensors to temporary file paths before passing to IndexTTS engine
- Handle tensor dimension normalization (3D->2D, 1D->2D) for torchaudio.save compatibility
- Add proper temporary file cleanup for both character switching and regular generation paths
- Fixes "Invalid file" and "Expected 2D Tensor, got 3D" errors when using emotion_audio input

The IndexTTS engine expects file paths but the processor receives tensor dictionaries,
so conversion to temporary files is required.
2025-09-12 09:46:57 -03:00
diodiogod b965a38326 Add IndexTTS-2 engine - initial implementation and basic testing
- Initial IndexTTS-2 TTS engine implementation
- Basic unified interface integration
- Auto-download system for IndexTTS-2 models
- Character voice switching support via QwenEmotion
- Token-based chunking system
- ComfyUI model management integration
- Fixed debug output and basic inference working
- Multi-language text preprocessing foundation
- Basic error handling framework

Note: Initial implementation - requires further testing and refinement
2025-09-12 09:33:55 -03:00
diodiogod dffa3ebce4 Fix IndexTTS-2 transformers compatibility issues
- Add hasattr() checks for newer transformers compatibility
- Fix missing attributes: forced_decoder_ids, watermarking_config, guidance_scale
- Use getattr() with defaults for optional attributes
- Maintains backward compatibility with older transformers versions
2025-09-12 08:59:37 -03:00
diodiogod ef3e5087fb Update issue templates 2025-09-12 08:17:00 -03:00
diodiogod 51de42754f Complete IndexTTS-2 TTS engine integration with emotion disentanglement
Major Features:
• IndexTTS-2 engine with zero-shot voice cloning and emotion control
• Multi-modal emotion support (audio refs, text descriptions, manual vectors)
• Character switching with emotion references: [Alice:angry_bob]
• Native IndexTTS-2 token-based chunking integration
• Comprehensive parameter exposure with 25+ engine-specific options

Core Components:
• engines/processors/index_tts_processor.py - Main processor with emotion support
• nodes/engines/index_tts_engine_node.py - Engine configuration node
• Updated unified TTS Text node with IndexTTS-2 integration
• Node registration in main nodes.py

Dependencies:
• Added 8 safe IndexTTS-2 dependencies to install.py and requirements.txt
• Dependency testing confirmed only descript-audiotools conflicts (already handled)
• All new deps use flexible versioning (>=) to avoid lock conflicts

Character Parser Fix:
• Fixed circular import by moving CharacterSegment to separate types.py
• Maintains backward compatibility while supporting emotion references
• Modular structure preserved (4 modules under 500 lines each)

Technical Details:
• Emotion disentanglement separates speaker identity from emotional expression
• Supports [character:emotion] syntax for per-segment emotion control
• IndexTTS-2 native chunking overrides character-based chunking when enabled
• Full caching integration and pause tag support included
• Error handling with graceful fallbacks for missing voices/emotions
2025-09-11 21:59:19 -03:00
diodiogod e129c5314b IndexTTS-2 implementation with modular character parser
MAJOR: IndexTTS-2 TTS engine integration
- Core IndexTTS engine with unified model interface integration
- Auto-downloader for HuggingFace models + GitHub source code
- Engine adapter with emotion support [Alice:angry_bob] syntax
- Cache integration for IndexTTS-2 parameters
- Factory registration for proper model management

MAJOR: Character parser modularization (930 → 300 lines each)
- Split into base_parser.py, language_resolver.py, segment_processor.py, validation.py
- Added emotion support with backward compatibility
- New split_by_character_with_emotions() method for advanced engines
- Flexible tag parsing: [de:Alice:angry_bob] support
- Language disambiguation to avoid Alice vs language code conflicts
- Full backward compatibility maintained

FEATURES:
- IndexTTS-2 emotion disentanglement (speaker vs emotion separation)
- Duration control and precise voice cloning
- Multi-modal emotion control (audio refs, text, vectors)
- Character switching with emotion references
- All official IndexTTS-2 parameters exposed

DOCS: Implementation plan and architectural TODO items added
2025-09-11 20:26:51 -03:00
diodiogod 1059b0d897 Fix critical CUDA tensor and dtype issues in TTS SRT node
- Add CPU tensor conversion in VibeVoiceSRTWrapper format_audio_output
- Add comprehensive audio tensor normalization before return
- Ensures CUDA tensors are moved to CPU (prevents conversion errors)
- Ensures float64 tensors are converted to float32 (prevents PyAV errors)
- Fixes compatibility with ComfyUI video creation nodes
- Critical bug fix for video+audio workflows
2025-09-11 17:03:41 -03:00
diodiogod d01e5cb807 Refactor comfyui_model_wrapper.py into modular architecture
- Split 970-line monolith into focused modules following 500-600 line policy
- Created utils/models/comfyui_model_wrapper/ directory structure:
  - base_wrapper.py (302 lines): Core ComfyUIModelWrapper class
  - model_manager.py (318 lines): ComfyUITTSModelManager class
  - cache_utils.py (28 lines): Cache invalidation utilities
  - engine_handlers/: Engine-specific memory management
    - higgs_audio_handler.py: CUDA graph management and unload blocking
    - vibevoice_handler.py: RAM cleanup and complete deletion logic
    - generic_handler.py: Standard CPU migration for other engines
- Fixed engine routing bug where stateless_tts wrappers weren't using HiggsAudioHandler
- Maintained full backward compatibility with existing imports
- All engine-specific behaviors preserved (VibeVoice deletion, Higgs CUDA graphs)
- Updated PROJECT_INDEX.md to reflect modular structure
2025-09-11 14:57:52 -03:00
diodiogod e857f5e589 Update README.md 2025-09-10 21:24:13 -03:00
diodiogod 663c790073 Version 4.8.38
Fix pkg_resources deprecation warning in installer

- Replace deprecated pkg_resources with importlib.metadata
- Add fallback support for Python < 3.8 compatibility
- Suppress warnings when using legacy fallback
2025-09-10 19:27:24 -03:00
diodiogod 88359881ec Version 4.8.37
Fix Higgs Audio compatibility with transformers 4.56.0+

Technical details:
- Fix _prepare_cache_for_generation method signature mismatch in HiggsAudioModel
- Add dynamic signature detection using inspect to handle parameter differences
- Make device parameter optional for backward compatibility
- Maintain support for both old and new transformers versions
2025-09-10 16:00:03 -03:00
diodiogod f0d147c9f7 Version 4.8.36
Fix ChatterBox Official 23-Lang IndexError with very short text

Technical details:
- Fix alignment stream analyzer tensor slicing bug for single characters
- Add safety check for empty tensor dimensions in repetition detection
- Comment out crash protection system for 23-lang model (testing confirmed unnecessary)
- Preserve crash protection for legacy ChatterBox model
2025-09-10 15:15:42 -03:00
diodiogod 2405b0eaf5 Version 4.8.35
Fix critical Windows installation issues

Technical fixes:
- Fix Windows UTF-8 encoding errors in subprocess calls
- Fix PyTorch version parsing failure on CUDA suffix (+cu124)
- Fix PyTorch 2.6.0 availability issues with graceful fallback
- Improve CUDA version detection and compatibility logic
2025-09-10 13:46:16 -03:00
diodiogod d1974c511f Version 4.8.34
Implement ComfyUI extra_model_paths.yaml support

Technical implementation:
- Add TtsExtraPathsManager for shared model directory integration
- Update unified downloader to respect configured model paths
- Enhance voice discovery with multi-path fallback support
- Integrate model manager with extra_model_paths.yaml system
- Support shared storage across multiple ComfyUI installations
- Maintain backward compatibility with existing setups
- Reference: GitHub issue #63
2025-09-10 13:06:22 -03:00
diodiogod 9ad7c78893 Add extra_model_paths.yaml support and enhance voice discovery
Technical implementation:
- Add TtsExtraPathsManager for ComfyUI extra_model_paths.yaml integration
- Support shared model directories across multiple ComfyUI installations
- Implement intelligent model discovery across configured paths
- Add voice discovery fallback: models/voices → models/TTS/voices → examples
- Update character alias map loading with proper priority system
- Enhance voice directory scanning to include TTS organization structure
- Add changelog guidelines requiring engine names in titles
- Reference: GitHub issue #63
2025-09-10 12:12:32 -03:00
diodiogod 79abfbbc4d Version 4.8.33
Fix Higgs Audio compatibility with newer transformers

Technical details:
- Override _prepare_cache_for_generation method in Higgs Audio model
- Add safe DynamicCache creation to handle property setter issues
- Fix StaticCache len() compatibility checks in boson_multimodal
- Resolve 'property has no setter' errors with transformers 4.46.3+
- Reference: GitHub issue #64
2025-09-10 11:54:32 -03:00
diodiogod dcd8e01aac Suppress VibeVoice compatibility patch warning messages 2025-09-09 23:59:54 -03:00
diodiogod 479b797aa9 Version 4.8.32
Fix VibeVoice Native Multi-Speaker voice reference handling

Technical improvements:
- Add voice reference validation and normalization in native mode
- Detect invalid character name connections with helpful error messages
- Add auto-conversion fallback for misconnected inputs
- Implement global character-to-speaker mapping for SRT consistency
- Remove redundant debug output to clean up console logs
- Add auto-detection for manual Speaker format with mode switching
2025-09-09 23:53:13 -03:00
diodiogod a5c546be8f Add auto-detection for manual Speaker format in VibeVoice
- Detect manual 'Speaker N:' format using regex pattern
- Auto-switch from Custom Character Switching to Native Multi-Speaker mode
- Display helpful console messages for user guidance
- Improves performance when manual speaker format is used
2025-09-09 20:11:24 -03:00
diodiogod a96f3b4e1e Fix VibeVoice SRT cache not invalidating with seed changes
Technical details:
- Add seed parameter to config when calling native multi-speaker generation in SRT
- Ensures cache properly invalidates when changing seeds for SRT generation
- Resolves issue where SRT kept showing cache hits despite seed changes
2025-09-09 20:04:25 -03:00
diodiogod 5d2ed5db3c Fix VibeVoice SRT Custom Character Switching to use long-form generation
Technical details:
- Add consecutive character grouping within each subtitle for VibeVoice SRT
- Groups same-character segments using VibeVoice-style Speaker 1 format
- Respects SRT timing constraints by only grouping within subtitle boundaries
- Maintains separate generations for different characters within subtitles
- Improves audio quality for same-character multi-line subtitles
2025-09-09 20:02:37 -03:00
diodiogod 3048eac022 Fix VibeVoice Custom Character Switching to use long-form generation
Technical details:
- Implement consecutive character grouping in VibeVoice processor
- Combine same-character segments into VibeVoice-style Speaker 1 blocks
- Generate entire character groups at once like official implementation
- Preserve chunking, pause tags, and voice switching functionality
- Only affects VibeVoice Custom Character Switching mode
2025-09-09 19:39:08 -03:00
diodiogod 5de02220cb Version 4.8.31
Optimize VibeVoice defaults and fix token calculation inconsistency

Technical details:
- Update default CFG scale to 3.0 and inference steps to 3 for better quality
- Fix parameter validation limits to prevent token calculation instability
- Based on community testing showing CFG 3.0 + 3 steps outperforms CFG 1.3 + 20 steps
- Resolves max_new_tokens jumping from ~300 to 4770 tokens with unlimited parameters
2025-09-09 18:35:20 -03:00
diodiogod f35395a072 Version 4.8.30
Fix VibeVoice cache invalidation and DynamicCache compatibility

Technical details:
- Add inference_steps parameter to VibeVoice cache key generator
- Disable deprecated transformers compatibility patches causing DynamicCache errors
- Add targeted DynamicCache key_cache/value_cache properties for VibeVoice compatibility
- Fix cache not invalidating when changing diffusion inference steps
2025-09-09 14:35:23 -03:00
diodiogod 9e089e887b Update README.md 2025-09-09 13:30:41 -03:00
diodiogod be95066811 Version 4.8.29
Improve VibeVoice CFG and inference steps flexibility

Technical details:
- Remove arbitrary CFG scale maximum limit (was 2.0, now unlimited)
- Lower inference steps minimum from 5 to 1 for ultra-fast generation
- Change VibeVoice default to Native Multi-Speaker mode for better quality
- Update tooltips to mention CFG 3.0 + 3 steps combination
- Remove backend parameter clamping for CFG scale
2025-09-09 13:14:56 -03:00
diodiogod 3691df5bfd Version 4.8.28
Fix VibeVoice character voice discovery and quantization support

Technical details:
- Fix voice discovery using wrong engine type for VibeVoice
- Add bitsandbytes dependency for 4-bit quantization support
- Improve voice reference debug output for better troubleshooting
- Fix misleading completion messages in character switching mode
- Standardize voice discovery engine types (audio_only vs audio_and_text)
2025-09-09 11:03:05 -03:00
diodiogod e6b9a5ab80 Version 4.8.27
Centralize numba compatibility system with smart testing

Technical implementation:
- Replace 15+ duplicate JIT workaround blocks with unified system
- Add utils/compatibility/numba_compat.py with startup testing
- Implement fallback for environments without compatibility module
- Test librosa.stft() at startup and apply workarounds only when needed
- Eliminate contradictory startup messages between init and nodes
- Maintain backward compatibility with legacy Python 3.13 fixes
2025-09-09 00:40:06 -03:00
diodiogod ee95cad1f1 Version 4.8.26
Fix Python 3.13 numba compatibility in audio hashing

Technical details:
- Add NUMBA_DISABLE_JIT fix to utils/audio/audio_hash.py
- Resolve 'get_call_template' errors in conda environments
- Ensure numba JIT disabling before librosa import
2025-09-08 18:58:09 -03:00
diodiogod 6096214799 Version 4.8.25
Fix VibeVoice VRAM clearing and model reloading

Technical improvements:
- Fix model reloading when switching attention modes
- Add proper VRAM clearing with CPU migration approach
- Implement VibeVoice-specific nuclear CUDA cleanup
- Add cache key generation for attention_mode parameters
- Note: System RAM accumulation still needs resolution
2025-09-07 11:38:55 -03:00
diodiogod 450e7c80d3 Version 4.8.24
Fix VibeVoice model reloading and transformers compatibility

Technical changes:
- Fix unified model interface cache key missing additional_params
- Add transformers>=4.51.3 requirement for VibeVoice compatibility
- Remove excessive debug prints from VibeVoice engine
- Fix RVC numba JIT disable logic for correct Python version detection
- Fix attention_mode and quantize_llm_4bit not triggering model reloads
2025-09-07 10:13:23 -03:00
diodiogod 08ce943f4f Centralize transformers compatibility patches into unified system
- Created utils/compatibility/transformers_patches.py with all monkey patches
- Moved FlashAttentionKwargs, BaseStreamer, DynamicCache, and generation method patches
- Fixed VibeVoice GenerationMixin._prepare_generation_config signature compatibility
- Updated VibeVoice downloader and engine to use centralized patches
- Updated PROJECT_INDEX.md to document compatibility system
- Eliminates duplicate patching logic and improves maintainability
2025-09-07 09:06:27 -03:00
diodiogod e7136cbefe Version 4.8.23
Add ChatterBox Official 23-Lang to ComfyUI VRAM management

Technical improvements:
- Register ChatterBox 23-Lang in unified model interface
- Update processors to use unified interface for model loading
- Fix Python 3.13 compatibility by replacing librosa.effects.trim with safe_trim fallback
- Add proper factory method using ChatterboxOfficial23LangTTS.from_local()
2025-09-07 00:17:35 -03:00
diodiogod e9c0199a81 Version 4.8.22
Fix VibeVoice model reloading and add VRAM management

Technical improvements:
- Add class-level model caching to prevent reloading on each run
- Register VibeVoice in unified model interface
- Update adapter to use unified interface for ComfyUI integration
- Fix OOM errors on subsequent runs
2025-09-07 00:01:23 -03:00
diodiogod f84957057c Version 4.8.21
Fix ChatterboxVC import handling in unified model interface

Technical fixes:
- Add proper null checks in ChatterBox factory functions before calling methods
- Handle ImportError cases gracefully in unified model interface
- Ensure ChatterboxVC availability is verified before attempting to use it
- Fix edge case where unified interface fails but ChatterboxVC appears available
2025-09-06 23:04:21 -03:00
diodiogod 1e61db1fca Version 4.8.20
Fix Python 3.13 RVC and vocal separation compatibility

Technical fixes:
- Fix RVC f0 method dependencies (pyworld, torchfcpe) for complete functionality
- Fix vocal separation numba/librosa compatibility issues in Python 3.13
- Fix MDX23C model path resolution and Karafan engine integration
- Fix model substitution and fallback error handling
- Improve default model selection (MelBand over slow ONNX on CPU)
- Add comprehensive safe wrapper functions for librosa operations
2025-09-06 22:55:31 -03:00
diodiogod cd853b70eb Fix Python 3.13 librosa/numba compatibility in vocal separation
- Add safe_stft(), safe_istft(), and safe_resample() fallback functions
- Replace problematic librosa calls in UVR spec_utils.py with safe versions
- Try librosa first, fall back to torchaudio on numba compilation errors
- Use librosa-compatible defaults for torchaudio (Hann window, center=True)
- Fixes 'get_call_template' AttributeError in Python 3.13
- Preserves audio quality while maintaining compatibility
2025-09-06 20:55:55 -03:00
diodiogod 42f4fa3acd Fix MDX mono/stereo dimension mismatch in vocal separation
- Move mono-to-stereo conversion to beginning of initialize_mix()
- Ensures both is_ckpt and else branches handle mono audio correctly
- Prevents concatenation errors with mixed channel dimensions
2025-09-06 18:20:05 -03:00
diodiogod 87190f452f Fix vocal removal device undefined error in RVC fallback
- Add device initialization for RVC fallback path
- Fix UnboundLocalError when primary engine fails
- Ensure device is properly obtained from ComfyUI model management
2025-09-06 18:09:10 -03:00
diodiogod 0afd47ff7f Version 4.8.19
Fix RVC Python 3.13 compatibility issues and restore missing dependencies

Technical details:
- Fix comprehensive numba/librosa compatibility issues for Python 3.13
- Restore missing RVC f0 method dependencies (praat-parselmouth, pyworld, torchfcpe)
- Implement librosa compatibility patches for missing functions
- Replace librosa.resample with torchaudio.resample to bypass numba
- Fix RMVPE model path resolution for ComfyUI directory structure
- Add manual RMS calculation to avoid librosa/numba conflicts
2025-09-06 17:55:15 -03:00
diodiogod 0ff2ab3e05 Complete RVC f0 method support - add missing dependencies
Major RVC enhancement: Added all missing f0 extraction dependencies for 100% compatibility

🎵 New F0 Method Dependencies:
• praat-parselmouth>=0.4.6 - Praat-based pitch extraction (pm method)
• pyworld>=0.3.5 - World vocoder for traditional methods (harvest/dio)
• torchfcpe>=0.0.4 - Fast Context-based Pitch Estimation (fcpe method)

✅ All RVC F0 Methods Now Supported:
• crepe (neural, high quality)
• rmvpe+ (robust, fast)
• pm (Praat-based, accurate)
• harvest (World vocoder, traditional)
• dio (World vocoder, fast)
• fcpe (context-based, modern)

🔧 Python 3.13 Compatibility:
• All packages tested and verified working in Python 3.13.6
• Clean installations with existing torch/numpy dependencies
• No version conflicts or dependency hell

🎯 Achievement: Complete RVC replacement functionality
• Fixed bundled RVC missing dependency issue
• Restored all f0 methods that were working in original RVC
• True feature parity with standalone RVC implementations

Total additions: ~50MB (mostly torchfcpe, others lightweight)
All methods tested working in fresh Python 3.13 ComfyUI portable environment.
2025-09-06 17:53:49 -03:00
diodiogod 3e5f9a3721 Fix RVC Python 3.13 compatibility - comprehensive numba/librosa solution
Major compatibility overhaul for RVC engine in Python 3.13 environments:

🔧 Core Compatibility Fixes:
• Disabled numba JIT compilation for Python 3.13+ via environment variables
• Added comprehensive librosa compatibility patches for missing functions:
  - pad_center, tiny (as function), fill_off_diagonal, is_positive_int, expand_to
• Replaced librosa.resample with torchaudio.resample to eliminate numba dependency
• Fixed all relative imports in RVC implementation (. notation)

⚡ Performance Improvements:
• Manual RMS calculation replacing librosa.feature.rms (avoids numba)
• Higher quality audio resampling using torchaudio
• Maintains backward compatibility with Python 3.12

🐛 Bug Fixes:
• Fixed 'function' object has no attribute 'get_call_template' errors
• Resolved 'float' object is not callable in librosa.sequence.viterbi
• Fixed ImportError for missing librosa utility functions
• Enhanced error handling and debugging throughout RVC pipeline

✨ Features:
• Comprehensive monkey-patching system for librosa compatibility
• Automatic fallbacks for failed operations
• Detailed logging for troubleshooting

Successfully tested with full RVC voice conversion workflow in Python 3.13.6.
2025-09-06 17:22:33 -03:00
diodiogod 700eac8024 Remove accidentally committed TTS-Audio-Suite folder 2025-09-06 15:55:58 -03:00
diodiogod 262c8c6c6d Version 4.8.18
Fix ChatterBox French model tokenizer fallback

Technical details:
- Fix ChatterBox incomplete models failing when tokenizer.json missing
- Add English tokenizer fallback for ChatterBox French/Japanese/Korean models
- Prevent import errors in ChatterBox tokenizer loading logic
- Match existing fallback pattern used by other ChatterBox components
2025-09-06 15:32:41 -03:00
diodiogod 79f43d470f Version 4.8.17
Fix ChatterboxVC import issue causing 'NoneType' object is not callable

Technical fixes:
- Add null checks in model manager before calling ChatterboxVC methods
- Make perth import optional with graceful fallback for Python 3.13 compatibility
- Update watermarking code to handle missing perth dependency
- Improve error messages for better debugging
2025-09-06 15:22:13 -03:00
diodiogod b3a24bfe4d Version 4.8.16
Fix comprehensive librosa compatibility for RVC engine

Technical implementation:
- Handle pad_center, tiny, and normalize import failures separately
- Add manual implementations for all three librosa utility functions
- Support any librosa version including latest releases that moved/removed functions
- Individual function fallbacks prevent partial import failures

Resolves: #47 (additional fix)
2025-09-06 15:06:43 -03:00
diodiogod 55b7a7ae59 Version 4.8.15
Fix RVC librosa pad_center compatibility issue

Technical implementation:
- Add librosa version compatibility layer in rmvpe.py
- Support both old and new librosa API versions
- Manual pad_center implementation using numpy as fallback
- Handles ImportError gracefully without breaking RVC functionality

Resolves: #47
2025-09-06 14:45:08 -03:00
diodiogod e17ff15736 Version 4.8.14
Fix transformers 4.56+ compatibility for VibeVoice and Higgs Audio engines

Technical implementation:
- Add DynamicCache compatibility properties for VibeVoice (.key_cache/.value_cache)
- Fix StaticCache access patterns in Higgs Audio modeling and serve engine
- Patch _prepare_cache_for_generation signature changes in transformers 4.56+
- Add comprehensive null checking for cache layer access
- Update install.py to allow newer transformers versions

Resolves: #44, #46
2025-09-06 14:28:21 -03:00
diodiogod 118b7df7cc Add Italian to F5-TTS phonemization exceptions
- Add 'it' to models_to_skip list alongside PT-BR
- Update debug message to reflect both Portuguese and Italian models
- Update PROJECT_INDEX.md with phonemizer utilities documentation

Italian F5-TTS models work better with native text rather than IPA phonemization,
similar to Portuguese-Brazil models.
2025-09-06 14:13:34 -03:00
diodiogod bc081a53d5 Version 4.8.13
Fix Python 3.13 numba compatibility in ChatterBox Official 23-Lang engine

Technical implementation:
- Add numba JIT disabling code to voice_encoder.py before librosa import
- Add numba JIT disabling code to melspec.py before librosa import
- Add numba JIT disabling code to s3tokenizer.py before librosa import
- Add numba JIT disabling code to audio_timing.py before librosa import
- Prevent 'function' object has no attribute 'get_call_template' errors
- Ensure compatibility code runs before any numba compilation attempts
2025-09-06 13:44:20 -03:00
diodiogod 9bb80c06b6 Version 4.8.12
Fix F5-TTS auto_phonemization toggle not working

Technical details:
- Fix parameter missing from unified TTS text node call
- Add auto_phonemization parameter to F5TTS API chain
- Fix F5TTS wrapper not passing parameter to inference
- Add parameter to infer_process and infer_batch_process
- Fix cache invalidation with proper parameter flow
- Remove debug messages when phonemization disabled
2025-09-06 13:43:11 -03:00
diodiogod 5c16a6ebb2 Update CHARACTER_SWITCHING_GUIDE.md 2025-09-06 13:26:43 -03:00
diodiogod 6eac6e377a Version 4.8.11
Import wildminder VibeVoice improvements

Technical improvements:
- Add Blackwell RTX 50 series SageAttention support (SM120+)
- Enhance input validation to prevent NaN/Inf tensor corruption
- Improve audio preprocessing with better extreme value handling
- Add post-resampling validation to catch library artifacts

Note: Transformers 4.56+ compatibility was already implemented.
2025-09-06 12:32:32 -03:00
diodiogod 5631fc6447 Version 4.8.10: Fix download detection for existing safetensors files 2025-09-06 11:14:23 -03:00
diodiogod d526f41089 Version 4.8.9
Fix ChatterBox Official 23-Lang safetensors support

Technical implementation:
- Add smart loading logic preferring official .pt files
- Support user safetensors with strict=False compatibility mode
- Implement download fallback from .pt to safetensors format
- Add proper missing file detection and re-download logic
- Fix file format validation in download checker
2025-09-06 10:56:28 -03:00
diodiogod 86c2546258 Version 4.8.8
Fix F5-TTS multilingual phonemization system

Technical implementation:
- Add auto_phonemization UI toggle to F5-TTS Engine node
- Implement smart phonemization detection with user control
- Disable phonemization for F5-PT-BR (confirmed to worsen quality)
- Add experimental warnings for multilingual model testing
- Ensure phonemization flows through SRT and regular TTS workflows
- Add cache separation for different phonemization settings
2025-09-06 09:33:09 -03:00
diodiogod c23930912a Version 4.8.7
Fix ChatterBox transformer compatibility issue

Technical changes:
- Set attention implementation to eager mode
- Fix output_attentions conflict with sdpa in transformers 4.36+
- Maintain alignment analyzer functionality
2025-09-06 08:38:08 -03:00
diodiogod a3c64fbfeb Version 4.8.6
Fix VibeVoice chunking configuration issues

Technical details:
- Fix chunk_minutes=0 not disabling chunking properly
- Include chunk_minutes in cache key for VibeVoice
- Ensure VibeVoice Engine chunking override takes precedence
- Fix cache invalidation for chunking parameter changes
2025-09-05 23:22:48 -03:00
diodiogod 539531c07a Version 4.8.5
Fix ChatterBox voice conversion for local models

Technical details:
- Fix 'NoneType' object is not callable error in ChatterBoxVC
- Add proper local model detection in from_pretrained method
- Prevent HuggingFace download attempts for local models
- Handle custom model names with special characters (colons, hyphens)
2025-09-05 23:22:48 -03:00
diodiogod a87f113d6c Update README.md 2025-09-05 22:08:47 -03:00
diodiogod ee3597f226 Update README.md 2025-09-05 22:08:21 -03:00
diodiogod 70c40eaaac Add comprehensive Chatterbox Multilingual TTS documentation
- Add Chatterbox Multilingual TTS (Official 23-Lang) to features section
- Update timeline with v4.8 Chatterbox Multilingual TTS entry
- Add detailed section covering 23-language unified model
- Distinguish official model from community finetuned variants
- Add verified model download section with exact file structure
- Update pyproject.toml description to include Chatterbox Multilingual TTS
- Document key advantages: single model, parameter switching, zero-shot cloning
- Include comprehensive integration details for character switching, SRT, pause tags
2025-09-05 22:07:16 -03:00
diodiogod 13a27f4302 Version 4.8.4
Fix ChatterBox Official 23-Lang cache invalidation

Technical fixes:
- Fix SRT processor using hardcoded config defaults instead of runtime parameters
- Pass actual TTS parameters (temperature, exaggeration, etc.) from UI to SRT processor
- Update process_srt_content method signature to accept tts_params
- Replace all self.config.get() calls with current runtime parameter values
- Fix unified SRT node to pass tts_params dictionary to processor
2025-09-05 21:28:46 -03:00
diodiogod df4b1c7322 Version 4.8.3
Fix ChatterBox Official 23-Lang SRT and caching issues

Technical fixes:
- Fix SRT overlapping mode routing to use correct assembly methods
- Add ChatterBoxOfficial23LangCacheKeyGenerator for proper cache invalidation
- Fix character voice inheritance after pause tags using F5-TTS pattern
- Resolve UnboundLocalError and variable scoping issues
- Fix tensor dimension handling in audio concatenation
2025-09-05 21:17:44 -03:00
diodiogod a5743ed03e Fix ChatterBox Official 23-Lang cache invalidation and character voice inheritance
Cache invalidation fixes:
- Create specialized ChatterBoxOfficial23LangCacheKeyGenerator with advanced parameters
- Include repetition_penalty, min_p, top_p in cache keys for proper invalidation
- Pass all ChatterBox Official 23-Lang specific parameters to cache function calls
- Now parameter changes properly invalidate cache and regenerate audio

Character voice inheritance fixes:
- Add character parameter to generate_speech() method signature
- Forward character name through pause tag processing pipeline
- Fix character voice inheritance after pause tags in SRT segments
- Now Alice's voice persists after [pause:10s] instead of reverting to narrator
- Pass character name from SRT character parser to TTS generation

Both issues resolved - cache properly invalidates and character voices persist through pauses
2025-09-05 20:55:43 -03:00
diodiogod 646b9b6ec2 Version 4.8.2
Fix multiple SRT and caching issues system-wide

Technical fixes:
- Fix SRT overlapping functionality across all engines with unified timing mode routing
- Fix tensor dimension errors in concatenation and stretch-to-fit assembly modes
- Fix ChatterBox Official 23-Lang cache invalidation for advanced parameters
- Add proper dimension normalization and empty segment filtering
- Correct crossfade operations for mixed 1D/2D audio tensors
2025-09-05 20:43:30 -03:00
diodiogod 366af73003 Fix ChatterBox Official 23-Lang cache invalidation for advanced parameters
- Add repetition_penalty, min_p, and top_p to ChatterBox cache key generation
- These parameters were missing from cache keys, causing stale cached audio
- Now changing these parameters properly invalidates cache and regenerates audio
- Fixes the persistent cache issue with ChatterBox Official 23-Lang settings
2025-09-05 20:43:05 -03:00
diodiogod aa8a27ac6e Fix tensor dimension mismatch in ChatterBox stretch-to-fit mode
- Handle mixed 1D/2D tensor dimensions in stretch-to-fit assembly
- Normalize segment dimensions to match output buffer dimensions
- Fix fade operations with proper tensor shape handling
- Ensure crossfade tensors have compatible dimensions
- Fixes 'size of tensor a must match tensor b' runtime error in stretch_to_fit mode
2025-09-05 20:39:04 -03:00
diodiogod adcd5a2000 Fix tensor dimension issues in concatenation assembly
- Filter out empty audio segments before processing to prevent dimension errors
- Normalize tensor dimensions before crossfade operations
- Handle mixed 1D/2D tensors properly in both simple and crossfade concatenation
- Fixes 'too many indices for tensor of dimension 1' error
- Prevents empty segments from causing 0.00s audio generation issues
2025-09-05 20:33:06 -03:00
diodiogod 1fd4496cbb Fix NameError for generation_params in character parser
- Replace remaining generation_params reference with self.config.get()
- Character parser now uses current config language setting
- Completes the cache invalidation fix by removing all cached parameter references
2025-09-05 20:24:37 -03:00
diodiogod 63a5e05fe7 Fix cache invalidation for ChatterBox Official 23-Lang SRT settings changes
Root Cause: SRT processor cached generation_params at initialization, so when
update_config() updated the processor config, TTS calls still used old cached
parameter values, preventing cache invalidation.

Fix: Use self.config.get() directly in TTS calls instead of cached generation_params.
This ensures updated configuration values are used for cache key generation,
properly invalidating cache when engine settings change.

Now changing temperature, cfg_weight, etc. will properly regenerate audio
instead of using stale cached results.
2025-09-05 20:23:26 -03:00
diodiogod 76dd035bd2 Fix missing timing report fields for all modes
- Add all required fields for timing report generation
- Fix 'needs_stretching' KeyError for stretch_to_fit mode
- Include stretch_factor_applied, padding_added, truncated_by etc.
- Ensures proper timing reports for pad_with_silence, stretch_to_fit, and concatenate modes
2025-09-05 20:15:10 -03:00
diodiogod 96555c42b0 Fix missing 'original_text' field in concatenate mode adjustments
- Add original_text field to adjustment records for concatenate mode
- Fix KeyError when generating timing reports for concatenate mode
- Ensures proper timing report generation for all timing modes
2025-09-05 20:12:40 -03:00
diodiogod 5e48b69780 Fix tensor dimension error in concatenation assembly
- Handle mixed 1D/2D tensor dimensions properly in concatenation mode
- Fix IndexError when accessing dimension 1 on 1D tensors
- Add proper tensor normalization for consistent concatenation
- Support both mono (1D) and stereo (2D) audio segments in same sequence
2025-09-05 20:09:55 -03:00
diodiogod 1d78cdf053 Version 4.8.1
Fix SRT overlapping functionality system-wide

Technical implementation:
- Add unified timing mode routing in AudioAssemblyEngine with assemble_by_timing_mode()
- Fix ChatterBox Official 23-Lang processor to use correct assembly method for pad_with_silence mode
- Now properly uses assemble_with_overlaps() instead of assemble_smart_natural() for overlaps
- All engines (ChatterBox, F5-TTS, Higgs Audio, VibeVoice) now handle overlaps consistently
- Resolves audio being cut off when SRT segments overlap in timing
2025-09-05 20:07:02 -03:00
diodiogod f44aeda98d Version 4.8.0
Add ChatterBox Official 23-Lang engine with voice conversion support

Technical implementation:
- Complete multilingual TTS engine supporting all 23 languages
- Add SRT subtitle timing with smart audio assembly
- Implement voice conversion processor with 23-language support
- Fix critical sample rate mismatches causing deep voice artifacts
- Integrate all components into unified node architecture
- Add comprehensive multilingual model support and caching
2025-09-05 19:42:45 -03:00
diodiogod 8500a502a6 Fix sample rate mismatches causing deep voice artifacts
Root cause: ChatterBox Official 23-Lang SRT processor was using 22050 Hz
instead of 24000 Hz, causing audio to play back slower and sound deeper.
All TTS engines generate at 24000 Hz (S3GEN_SR constant).

Fixed components:
- ChatterBox Official 23-Lang SRT/TTS processors: 22050 → 24000 Hz
- Old ChatterBox SRT/TTS nodes: Fixed incorrect 22050 Hz fallbacks
- Voice changer: 22050 → 24000 Hz fallback (processes TTS output)
- Audio analyzer: 22050 → 24000 Hz defaults (analyzes TTS audio)
- Audio merge: Added 24000 Hz option for TTS audio merging
- Audio recorder: 22050 → 24000 Hz silence fallback for consistency

All TTS engines verified to use 24000 Hz:
✓ F5-TTS: F5TTS_SAMPLE_RATE = 24000
✓ Higgs Audio v2: 24000 Hz
✓ VibeVoice: 24000 Hz consistently
✓ ChatterBox (old): S3GEN_SR = 24000
✓ ChatterBox Official 23-Lang: S3GEN_SR = 24000
2025-09-05 19:12:27 -03:00
diodiogod 127f46e0ee Fix ChatterBox Official 23-Lang SRT processing implementation
- Complete SRT processor following Higgs Audio modular pattern
- Fixed cache system to recognize chatterbox_official_23lang engine type
- Corrected timing engine parameter format and method calls
- Fixed audio assembly to use assemble_smart_natural method
- Added key mapping for report generator compatibility
- Removed debug output for clean production logs
- Added comprehensive TODO.md documenting architectural inconsistencies
2025-09-05 11:35:16 -03:00
diodiogod 171697efbb Add complete ChatterBox Official 23-Lang SRT support
- Create full SRT processor based on Higgs Audio pattern with proper implementation
- Use existing handle_reference_audio method for voice reference conversion
- Support multilingual SRT processing with all 23 languages
- Character switching within SRT segments with language auto-detection
- Full timing modes: stretch_to_fit, pad_with_silence, smart_natural, concatenate
- Voice discovery integration with character-specific audio references
- Proper pause tag support within SRT content
- Uses modular timing utilities (TimingEngine, AudioAssemblyEngine, SRTReportGenerator)
- Fixed import path issues in unified SRT node for ChatterBox Official 23-Lang
- Comprehensive error handling and debug output
- No placeholders - complete working implementation

ChatterBox Official 23-Lang now fully supports SRT subtitles through unified SRT node.
2025-09-05 11:01:55 -03:00
diodiogod b5661ea5a8 Complete ChatterBox Official 23-Lang implementation with SRT support and fixes
- Add debug language_id parameter display for TTS generation calls
- Clean up verbose console messages (keep only model path and errors)
- Add missing 12 language aliases to character parser for full 23-language support:
  Arabic, Danish, Greek, Finnish, Hebrew, Malay, Polish, Swedish, Swahili, Turkish
- Fix character parser language normalization for ChatterBox Official 23-Lang
- Add full SRT subtitle support via unified SRT node integration
- Fix cache collision between ChatterBox and ChatterBox Official 23-Lang engines
- All 23 languages now work with both tagged and untagged text
- Language switching and voice references fully functional
- SRT timing modes, batch processing, and multilingual subtitles supported
2025-09-05 10:36:04 -03:00
diodiogod 07bf539198 Fix multilingual model reloading - only load once for all languages
- Remove unnecessary model reloading when switching languages
- ChatterBox Official 23-Lang is one model for all 23 languages
- Language switching now just changes the language_id parameter
2025-09-05 09:58:25 -03:00
diodiogod 20868c7217 Fix ChatterBox Official 23-Lang model loading issue
- Fix load_tts_model to return the model instance for smart_model_loader
- Initialize model variables properly in __init__
- Store current language when loading model
- Remove debug output for cleaner logs
2025-09-05 09:49:49 -03:00
diodiogod 193637e930 Add ChatterBox Official 23-Lang multilingual TTS engine
- Implement full multilingual support for 23 languages
- Add MTLTokenizer for multilingual text processing
- Fix device resolution for auto device selection
- Update model loading with correct multilingual models from reference
- Fix parameter passing for min_p, top_p, repetition_penalty
- Add automatic model downloading from HuggingFace
- Remove debug output and fix double loading issue
- Ensure proper language ID passing through the pipeline
2025-09-05 09:43:42 -03:00
diodiogod ebc2798773 Add ChatterBox Official 23-Lang multilingual TTS engine
Implement ResembleAI's new multilingual ChatterBox model supporting 23 languages.
Complete engine implementation copied and modified from existing ChatterBox engine.

Features:
- MTLTokenizer with language-specific preprocessing (Chinese Cangjie, Japanese hiragana, Hebrew diacritics, Korean Hangul)
- Support for 23 languages: Arabic, Danish, German, Greek, English, Spanish, Finnish, French, Hebrew, Hindi, Italian, Japanese, Korean, Malay, Dutch, Norwegian, Polish, Portuguese, Russian, Swedish, Swahili, Turkish, Chinese
- New sampling parameters: repetition_penalty, min_p, top_p
- Multilingual model files: t3_23lang.safetensors, mtl_tokenizer.json, s3gen.pt, ve.pt
- Unified downloader support for Official 23-Lang model structure
- Engine adapter with full batch processing and caching support
- Configuration node with multilingual parameters

Files added/modified:
- engines/chatterbox_official_23lang/ (complete new engine)
- engines/adapters/chatterbox_official_23lang_adapter.py
- nodes/engines/chatterbox_official_23lang_engine_node.py
- utils/downloads/unified_downloader.py (updated for Official 23-Lang)
2025-09-04 19:11:49 -03:00
diodiogod 440f7d2b29 Version 4.7.2
Fix Windows path issue with local model names

Technical details:
- Fix invalid directory names containing colon characters on Windows
- Sanitize local model paths by stripping 'local:' prefix before path construction
- Resolve ChatterBox VC failing with local models on Windows systems

Fixes #36
2025-09-04 16:32:32 -03:00
diodiogod e86be32b1a Fix ChatterBox VC language support and improve error handling
- Add VC component detection system to check s3gen availability for all languages
- Implement universal ChatterBox model loading with component mixing fallback
- Fix VC download system to handle subdirectories (German havok2 model)
- Add graceful handling of optional files (conds.pt) vs required files (s3gen)
- Propagate 401 authentication errors properly through download system
- Add intelligent fallback: German variants → German base → English
- Return silent audio for unsupported VC languages instead of crashing
- Remove misleading claims about unverified VC language support
- Fix file format detection (.pt vs .safetensors) in VC downloads

Resolves VC crashes with Japanese/incomplete models and improves German model support
2025-09-04 16:06:18 -03:00
diodiogod 7fa129d1da Version 4.7.1
Fix VibeVoice compatibility and performance issues

Technical details:
- Add SageAttention mixed-precision attention system (based on https://github.com/wildminder/ComfyUI-VibeVoice)
- Fix dtype parameter compatibility for transformers v4.56+
- Fix tokenizer downloads to save in model folder
- Add fallback repo sources for Microsoft's deleted repositories
- Implement GPU architecture detection for optimal kernel selection
- Credit: SageAttention implementation adapted from wildminder
2025-09-04 10:05:49 -03:00
diodiogod 0c49712f2c Update README.md 2025-09-04 02:41:34 -03:00
diodiogod 50835b87f8 Update README timeline with v4.7.0 2025-09-04 02:26:13 -03:00
diodiogod 2cfe6d31b2 Version 4.7.0
Expand ChatterBox TTS to 11 languages with unified architecture

Major expansion of ChatterBox language support:
- Added 9 new language models with SHA256 verification
- Implemented unified model architecture for Italian
- Fixed critical tokenizer discovery for non-Latin languages
- Added Italian prefix system for bilingual model support
- Fixed Japanese/Korean state dict and vocab size issues
- Integrated all languages with automatic model management

Technical improvements:
- Smart tokenizer discovery prioritizing language-specific files
- Unified model loader for single-checkpoint architectures
- Extended vocabulary support (1500 tokens for Italian)
- Character parser regex updates for Italian syntax
- Debug console output for TTS text verification
- Subdirectory download support for multi-language repos
2025-09-04 02:09:56 -03:00
diodiogod 865940b47f Implement Italian ChatterBox prefix system and debug console output
- Updated character parser regex to exclude standalone [it]/[italian] tags while preserving [it:]/[italian:] syntax
- Added apply_italian_prefix_if_needed() method for automatic [it] prefix application when Italian language is detected
- Extended character parser with split_by_character_with_language_and_explicit_flag() to preserve explicit language flags
- Updated ChatterBox TTS and SRT nodes to use new parser method with automatic Italian prefix handling
- Fixed timing report text to show processed segments instead of raw input with character tags
- Added console debug prints to show final text sent to TTS models for verification

Italian ChatterBox model now correctly receives [it] prefix for Italian text when:
- User explicitly uses [it:Alice] or [italian:Bob] character syntax
- User uses [it:] or [italian:] language switching syntax
- Character is mapped to Italian in alias system
- Does NOT apply prefix when just using Italian model globally (preserves bilingual capability)
2025-09-04 02:01:13 -03:00
diodiogod 95f5bc0103 Fix Italian ChatterBox tokenizer JSON parsing error
- Remove malformed temporary JSON tokenizer creation
- Use proper fallback to English model tokenizer when Italian doesn't have one
- Italian model extends English vocabulary so English tokenizer is compatible
- Add proper error handling for missing tokenizer files
- Fix 'expected comma or }' JSON parsing error
2025-09-04 01:21:31 -03:00
diodiogod 616448a99c Fix Italian ChatterBox vocab size mismatch
- Set T3Config.text_tokens_dict_size to 1500 for Italian model
- Italian model has extended vocabulary (704 English + 796 Italian tokens)
- Fixes tensor size mismatch error in text_emb.weight and text_head.weight
- Now uses checkpoint vocab_size from model_config for proper initialization
2025-09-04 01:19:35 -03:00
diodiogod a36b09d8ca Add Italian ChatterBox TTS support with unified model architecture
- Add Italian to CHATTERBOX_MODELS registry with unified_model special flag
- Implement _load_unified_model() method for Italian checkpoint format
- Italian model uses single chatterbox_italian_final.pt with all components
- Load base architecture then replace with fine-tuned Italian weights
- Support bilingual Italian/English with [it] language prefix
- Extended vocabulary: 1500 tokens (704 frozen English + 796 trainable Italian)
- Update language mapper for Italian language code 'it' -> 'Italian'
- Based on deployment script analysis from niobures repository

Usage: Italian text requires '[it]' prefix, English text uses no prefix
2025-09-04 01:13:04 -03:00
diodiogod a6c95d4457 CRITICAL FIX: Japanese tokenizer not being used for Japanese text
- Bug: tokenizer discovery checked tokenizer.json (English) FIRST
- Japanese text was being processed by English tokenizer = garbage output
- Fix: prioritize language-specific tokenizer based on language parameter
- Now tries tokenizer_jp.json for Japanese before falling back to English
- Addresses GitHub issue #34 about non-Latin text not being perceived correctly

This was causing Japanese/Korean/other non-Latin languages to produce
incorrect pronunciation because they were using English tokenization.
2025-09-04 00:56:54 -03:00
diodiogod df9a8df24c Fix Japanese/Korean ChatterBox state dict key format mismatch
- Add logic to detect and remap state dict keys with 't3.' prefix
- Japanese/Korean models store weights with 't3.' prefix but model expects direct keys
- Remove prefix from all keys to match expected model structure
- Fixes T3 model loading for incomplete language models
2025-09-04 00:48:24 -03:00
diodiogod d85b0ec819 Fix UnifiedModelDownloader import error in ChatterBox TTS
- Correct import from UnifiedModelDownloader to UnifiedDownloader
- Fix class instantiation name
- Japanese/Korean incomplete models should now properly load English fallback components
2025-09-04 00:46:35 -03:00
diodiogod 37646f89e3 Fix incomplete model loading for Japanese and Korean ChatterBox models
- Add language parameter to from_local() method to detect incomplete models
- Implement fallback loading from English model for missing components (ve, s3gen)
- Use local English model first, download if needed
- Japanese/Korean models now load t3_cfg locally + shared English components
- Maintains compatibility for complete models
2025-09-04 00:44:42 -03:00
diodiogod 5089b86e1f Fix ChatterBox model completeness and tokenizer naming issues
- Add special tokenizer filename handling for Japanese (tokenizer_jp.json) and Korean (tokenizer_en_ko.json)
- Mark French model as incomplete (only has t3_cfg.safetensors)
- Update model requirements validation for incomplete models
- Fix dynamic tokenizer discovery in TTS loading
- Ensure all niobures language models have correct file expectations
2025-09-04 00:40:58 -03:00
diodiogod 4454ccafd6 Fix model completeness and file requirements for all languages
Analyzed all ChatterBox language models for file completeness:

Complete Models (7):
- English, German variants, Norwegian, Russian, Armenian, Georgian
- All have: t3_cfg.safetensors, s3gen.safetensors, ve.safetensors, conds.pt, tokenizer.json

Incomplete Models (3) - use shared English components:
- Japanese: only t3_cfg.safetensors + tokenizer_jp.json
- Korean: only t3_cfg.safetensors + tokenizer_en_ko.json
- French: only t3_cfg.safetensors (no tokenizer in Thomcles repo)

Changes:
- Updated French model requirements (no tokenizer needed from repo)
- Added special handling for French model downloads
- Ensured proper repo assignments (French uses Thomcles, not niobures)
- Maintained fallback system for incomplete models

All models now have correct file requirements and download paths.
2025-09-04 00:39:44 -03:00
diodiogod b9e9dfb3fa Fix Japanese and Korean model tokenizer file names
Japanese and Korean models in niobures repository use different tokenizer filenames:
- Japanese: tokenizer_jp.json (not tokenizer.json)
- Korean: tokenizer_en_ko.json (not tokenizer.json)

Changes:
- Added get_tokenizer_filename() helper function
- Updated get_model_requirements() to use correct tokenizer names
- Modified ChatterboxTTS.from_local() to auto-detect available tokenizer files
- Added proper fallback system for tokenizer discovery

This fixes the 404 download error when trying to use Japanese/Korean models.
Users can now successfully use incomplete models with shared English components.
2025-09-04 00:35:47 -03:00
diodiogod 2f29a7d746 Update ChatterBox language mapper integration with alias system
- Remove duplicate alias definitions (character_parser already handles this)
- Map canonical language codes to ChatterBox models efficiently
- Support natural language tags: [USA:], [Deutschland:], [France:], [Russia:]
- Enable ChatterBox-specific model variants: [de-best:], [de-expressive:]
- Unified alias system now works across F5-TTS and ChatterBox engines

Users can now use natural language switching:
- [USA:John] Hello → English model
- [Deutschland:Hans] Hallo → German model
- [France:Marie] Bonjour → French model
- [Russia:Ivan] Привет → Russian model
- [de-best:Klaus] Das beste Modell → German (havok2) model
2025-09-04 00:30:45 -03:00
diodiogod 935281f4ac Expand ChatterBox language support - Add 9 unique fine-tuned models
Based on SHA256 hash verification to avoid duplicates, added 9 genuinely unique ChatterBox language models:

**New Language Models Added:**
- German (SebastianBodza): Kartoffelbox with emotion control (<haha>, <wow> tags)
- German (havok2): Hybrid multi-speaker model (user-rated best quality)
- French: 1,400 hours Emilia dataset with zero-shot voice cloning
- Russian: Complete model with training artifacts from niobures collection
- Armenian: Complete model with training artifacts
- Georgian: Complete model with unique architecture
- Japanese: t3_cfg-only model (uses shared English components)
- Korean: t3_cfg-only model (uses shared English components)

**Key Features:**
- SHA256 hash verification prevented duplicate models (eliminated 3 duplicates)
- Subdirectory download support for niobures multi-language repository
- Model validation system for incomplete models
- Unified downloader integration with subdirectory routing
- Fallback support for models missing non-critical components
- Trainer name-based naming convention (e.g. "German (havok2)")

**Technical Implementation:**
- Extended CHATTERBOX_MODELS registry with subdirectory support
- Added validate_model_completeness() for quality assurance
- Updated ChatterBox TTS loader to use new unified downloader
- Maintained backward compatibility with existing models

Total languages: 11 (was 3, now includes 8 new variants)
2025-09-04 00:22:02 -03:00
diodiogod b14b5b7716 Remove accidentally committed file '=1.26.4,' 2025-09-03 23:31:40 -03:00
diodiogod f58bf7d560 Version 4.6.28
Fix VideoHelper audio compatibility with universal normalizer

Technical implementation:
- Add _get_audio() method to BaseChatterBoxNode for universal audio format handling
- Update Voice Changer, Merge Audio, and ChatterBox VC nodes to use normalizer
- Centralize audio input processing in AudioProcessingUtils.normalize_audio_input()
- Support VideoHelper LazyAudioMap, Character Voices, and standard AUDIO formats
- Maintain backward compatibility with existing workflows

Fixes issue #32: VideoHelper Suite audio inputs now work with all audio-processing nodes
2025-09-03 23:24:47 -03:00
diodiogod 8d583c00a9 Update CHANGELOG.md 2025-09-03 23:01:57 -03:00
diodiogod d9d7dcb3e3 Version 4.6.27
Fix numpy compatibility and VibeVoice installation

Technical improvements:
- Update install script to support numpy 1.26.4 for Python 3.12
- Add Python version detection for numpy requirements
- Fix VibeVoice installation using FushionHub fork
- Improve audio-separator conditional installation
- Better numpy version constraint handling
2025-09-03 22:52:19 -03:00
diodiogod fc5c5d6323 Update debug logging for new cache parameters 2025-09-03 22:52:19 -03:00
diodiogod 7685132c6e Fix cache invalidation for new VibeVoice parameters
Critical fix: Add attention_mode, inference_steps, and quantize_llm_4bit
to cache key generation so changing these parameters properly triggers
regeneration instead of using cached results.

Technical details:
- Add attention_mode to cache parameters
- Add inference_steps to cache parameters
- Add quantize_llm_4bit to cache parameters
- Use getattr() with fallbacks for model-level parameters

This ensures all new parameter changes invalidate cache and retrigger generation.
2025-09-03 22:52:19 -03:00
diodiogod 27e15b0f12 Create FUNDING.yml 2025-09-01 23:06:43 -03:00
diodiogod bb56b9b655 Version 4.6.26
Improve VibeVoice Engine parameter organization

Technical details:
- Reorder parameters for better user workflow
- Group hardware settings together (model → device → quantization → attention)
- Place quality controls logically (cfg_scale → inference_steps)
- Improve parameter discoverability and logical flow
2025-09-01 22:54:29 -03:00
diodiogod e5fe87088c Version 4.6.25
Add VibeVoice parameter enhancements and compatibility fixes

Technical details:
- Add quantize_llm_4bit parameter for VRAM optimization
- Add attention_mode selection with fallback system
- Add inference_steps parameter for quality control
- Fix Transformers 4.56+ compatibility issue resolving issue #33
- Fix device placement issues with quantization
- Update realistic VRAM/speed trade-off information in tooltips
- Based on wildminder/ComfyUI-VibeVoice enhancements
2025-09-01 22:50:44 -03:00
diodiogod da1099f749 Fix 4-bit quantization device mapping for speech_bias_factor
Use explicit device mapping instead of 'auto' to ensure all model buffers
including speech_bias_factor are properly assigned to devices during
quantized model loading. This should resolve the quantization failure
and allow 4-bit quantization to work properly.

Technical details:
- Replace device_map='auto' with explicit device mapping {'': 0} for quantization
- Ensures all model components and buffers are placed on the same device
- Maintains compatibility with CPU and GPU device selection
2025-09-01 22:41:32 -03:00
diodiogod 591f3d20aa Fix device placement issue with VibeVoice 4-bit quantization
Technical details:
- Add fallback mechanism when quantization fails (falls back to full precision)
- Fix speech_bias_factor device placement with explicit device assignment
- Add model.eval() call for proper evaluation mode
- Improve error messages to show quantization status (success/fallback)
- Non-critical error handling for device placement fixes

Resolves speech_bias_factor device placement error when using 4-bit quantization
2025-09-01 22:38:08 -03:00
diodiogod eac0c21ed1 Add VibeVoice enhancements: 4-bit quantization, attention modes, and Transformers 4.56+ compatibility
Implementation details:
- Add quantize_llm_4bit parameter for massive VRAM savings (7B: 12GB→7.6GB, 1.5B: 8.7GB→3.2GB)
- Add attention_mode selection (auto/eager/sdpa/flash_attention_2) with intelligent fallbacks
- Add inference_steps parameter (5-100) for diffusion quality control
- Fix Transformers 4.56+ compatibility issue resolving issue #33 (_prepare_cache_for_generation args)
- Add dynamic parameter detection with backwards compatibility
- Update all engine nodes, adapters, and processors to support new parameters
- Add transformers_compatibility.py with monkey patching system
- Reduce logging verbosity for compatibility patches (debug level only)

Credits: Based on enhancements by drbaph (Saganaki22) from wildminder/ComfyUI-VibeVoice PRs #12 and #16
2025-09-01 22:35:42 -03:00
diodiogod 1ae879d134 Version 4.6.24
Update VibeVoice 7B to official Microsoft repository

Technical details:
- Change repo from WestZhang/VibeVoice-Large-pt to microsoft/VibeVoice-Large
- Update documentation and downloader references
- Maintain backward compatibility with existing installations
2025-09-01 10:44:16 -03:00
diodiogod a105b3023e Version 4.6.23
Fix VibeVoice engine availability checking

Technical implementation:
- Add proper dependency checking to VibeVoice engine init
- Implement VIBEVOICE_AVAILABLE flag like other engines
- Prevent VibeVoice engine from loading if package unavailable
- Add ImportError handling with dummy classes for compatibility
2025-09-01 10:23:02 -03:00
diodiogod 5e94088690 Version 4.6.22
Fix local F5-TTS model language display

Technical changes:
- Fix language code detection for local models
- Display 'local' instead of confusing 'lo' for custom models
- Properly handle local: prefix in language detection
- Improve logging clarity for local models
2025-09-01 09:33:37 -03:00
diodiogod a370fe76ea Version 4.6.21
Improve RVC Engine clarity and connection compatibility

Technical changes:
- Fix RVC Engine output naming to match TTS_engine convention
- Add clear warnings that RVC is voice conversion only
- Improve ComfyUI connection auto-suggestions
- Clarify RVC won't work with TTS Text/SRT nodes
2025-09-01 09:18:13 -03:00
diodiogod 5e4f5bdd80 Clarify RVC Engine is for voice conversion only
- Add warning in class docstring and DESCRIPTION
- Explicitly state it won't work with TTS Text or TTS SRT nodes
- Clarify it only works with Voice Changer node
- Prevents user confusion about RVC capabilities
2025-09-01 09:17:51 -03:00
diodiogod 502cadf004 Fix RVC Engine output naming for ComfyUI connection suggestions
- Change RETURN_NAMES from 'rvc_engine' to 'TTS_engine'
- Matches naming convention of other TTS engines
- Fixes auto-suggestions when pulling from Voice Changer input
- Now RVC Engine appears in suggestions alongside other engines
2025-09-01 09:13:13 -03:00
diodiogod 5025b24a5b Version 4.6.20
Fix content-vec-best auto-download failure

Technical implementation:
- Add fallback to official lengyue233 repository
- Implement automatic .bin to .safetensors conversion
- Check for existing .bin files and auto-convert
- Provide robust download with retry logic
2025-09-01 09:05:06 -03:00
diodiogod d7f04c2697 Fix content-vec-best auto-download with fallback and auto-conversion
- Add fallback URL to official lengyue233/content-vec-best repository
- Implement automatic .bin to .safetensors conversion
- Try SayanoAI source first (already converted), fallback to official if needed
- Check for existing .bin files and auto-convert them
- Fixes GitHub issue #29 where content-vec-best failed to auto-download

The system now:
1. Tries to download pre-converted .safetensors from SayanoAI first
2. Falls back to official source and auto-converts .bin to .safetensors
3. Checks for existing .bin files and converts them automatically
4. Provides clear error messages and progress feedback
2025-09-01 09:04:20 -03:00
diodiogod dbdcb844a1 Version 4.6.19
Fix F5-TTS model selection and language mapping issues

Technical fixes:
- Fix F5-TTS UnboundLocalError when accessing model variable
- Fix F5-TTS SRT always using Base model instead of user's choice
- Character parser now correctly infers language from selected model
- Unmapped characters now use engine's selected model instead of hardcoded English
2025-08-31 11:42:32 -03:00
diodiogod ce04edf59b Version 4.6.18
Fix VibeVoice librosa/numba compatibility issue

Technical fixes:
- Replace direct librosa.load with safe_load fallback
- Replace direct librosa.resample with safe_resample fallback
- Use torchaudio as fallback when librosa/numba fails
- Resolves get_call_template AttributeError
2025-08-31 08:30:26 -03:00
diodiogod 8088398fdd Version 4.6.17
Fix VibeVoice chunking method call

Technical fix:
- Correct method name from chunk_text to split_into_chunks
- Align with ImprovedChatterBoxChunker API
- Resolves AttributeError in vibevoice_processor.py
2025-08-30 23:36:59 -03:00
diodiogod 8aa2c234c1 readme image showcase updated, minor audio wave analizer option input name changed 2025-08-30 15:28:58 -03:00
diodiogod 1605b06b59 Version 4.6.16
Refactor VibeVoice to use ComfyUIModelWrapper

Technical details:
- Replace custom model caching with unified ComfyUI wrapper system
- Remove custom unload hooks in favor of automatic ComfyUI integration
- Implement VibeVoiceModelContainer for proper model/processor handling
- Eliminate manual CPU movement code and global flag management
- Achieve architectural consistency with other TTS engines
2025-08-30 14:13:51 -03:00
diodiogod c2b9b222fd Version 4.6.15
Fix VibeVoice unload button and VRAM management

Technical details:
- Hook VibeVoice models into ComfyUI's 'Unload Models' button
- Move models to CPU during unload to actually free VRAM
- Invalidate engine caches to prevent tensor corruption errors
- Fix 'Inference tensors do not track version counter' after unload
- Ensure fresh model recreation after unload for proper functionality
2025-08-30 12:41:50 -03:00
diodiogod 27415f089a Version 4.6.14
Fix VibeVoice VRAM management and model loading issues

Technical details:
- Integrate VibeVoice with ModelManager caching system
- Reuse cached model/processor instances across generations
- Prevent VRAM allocation errors on repeated runs
- Fix VibeVoice-7B model configuration (10 shards vs 4)
- Add legacy path and HuggingFace cache support for VibeVoice
2025-08-30 12:26:06 -03:00
diodiogod 6e32362dc4 Integrate VibeVoice with ModelManager caching system
Technical details:
- Add VibeVoice model loading and caching to ModelManager class
- Modify VibeVoice adapter to use ModelManager instead of creating own engine instances
- Add load_vibevoice_model() and unload_vibevoice_models() methods to ModelManager
- Import VibeVoice components into ModelManager with availability checking
- Update adapter to cache model and processor instances via ModelManager
- Follow ChatterBox pattern for persistent model caching across node executions
2025-08-30 12:26:06 -03:00
diodiogod 7636afe662 Update README.md 2025-08-30 11:36:43 -03:00
diodiogod 5b5f8d603c Version 4.6.13
Fix MelBandRoFormer architecture compatibility issues

Technical details:
- Implement proper MelBandRoFormer model loading with correct config
- Add Kijai's safetensors model support and auto-download
- Fix tensor size mismatch in instrumentals calculation
- Add model-specific output inversion handling
- Prevent incompatible RVC fallback for MelBand models
2025-08-30 10:53:22 -03:00
diodiogod dbb030a9a5 Version 4.6.12
Add VibeVoice legacy and cache support

Technical details:
- Add legacy VibeVoice-ComfyUI path support (models/vibevoice/models--{repo}/)
- Add HuggingFace cache checking for VibeVoice models
- Implement priority: local > legacy > cache > download
- Maintain compatibility with original VibeVoice-ComfyUI installations
2025-08-30 10:09:41 -03:00
diodiogod 0d754c9ce9 Fix VC model loading fallback for corrupted local files
Technical details:
- Always include huggingface fallback even when local language path exists
- Resolve issue where partial/corrupted local VC models prevent fallback
- Add better cache error logging and source summary diagnostics
- Maintain cache checking functionality for users with HF cache
2025-08-30 10:03:05 -03:00
diodiogod 5dc2592737 Update README.md 2025-08-30 09:51:50 -03:00
diodiogod 6101afc3eb Version 4.6.11
Fix Higgs Audio chunking parameter error

Technical details:
- Add missing sample_rate parameter to _combine_audio_chunks() method
- Resolve TypeError when ChunkTimingHelper passes sample_rate argument
- Maintain backward compatibility with optional parameter
2025-08-30 09:42:45 -03:00
diodiogod 292974ed4a Update README.md 2025-08-30 02:00:43 -03:00
diodiogod 5b9fb70c3b Version 4.6.10
Remove virtual environment files from repository

Clean up accidentally committed comfyui_env/ virtual environment.
Update project description to include all current TTS engines.
2025-08-30 01:56:55 -03:00
diodiogod 1bbdd55cfd Remove virtual environment files and update project description
Remove accidentally committed comfyui_env/ virtual environment files.
Add comfyui_env/ to .gitignore to prevent future commits.
Update pyproject.toml to include all current TTS engines.
2025-08-30 01:55:58 -03:00
diodiogod 197b4ef199 Version 4.6.9
Clean up verbose debug logs and update README

Remove verbose debug logs from VibeVoice implementation.
Update README with VibeVoice documentation and model information.
2025-08-30 01:24:22 -03:00
diodiogod e05f0614bd Clean up verbose debug logs after VibeVoice implementation 2025-08-30 01:14:37 -03:00
diodiogod a32bc82b63 Version 4.6.8
Implement robust import system for cross-environment compatibility

Major fix for #12 - comprehensive solution for module discovery issues

Technical details:
- Add utils/robust_import.py with multi-strategy import system
- Smart project root detection using characteristic files
- Handle conda cache invalidation, Windows path issues, and partial module loading
- Multi-layered fallback: normal import → path fix + cache clear → force reload
- Zero performance impact on working installations
- Update ChatterBox nodes to use robust import system
2025-08-30 00:28:43 -03:00
diodiogod 8a449adba9 Version 4.6.7
Add manual Speaker N: format and priority system to VibeVoice

Technical implementation:
- Add support for manual 'Speaker 1: Speaker 2:' format in Native Multi-Speaker mode
- Implement priority system where speaker inputs override character aliases
- Add proper Speaker 1 mapping to main narrator voice (Tony)
- Fix voice resolution for manual Speaker format with debug logging
- Update character parser to handle Speaker N: format detection
- Enhance tooltips to clearly explain both format options
2025-08-30 00:06:40 -03:00
diodiogod acc4d1a558 Version 4.6.6
Complete VibeVoice SRT implementation

Technical implementation:
- Add VibeVoice SRT processor with dual-mode support
- Implement Native Multi-Speaker and Custom Character Switching modes
- Fix audio file loading from path references in voice cloning
- Add cache invalidation for multi-speaker mode switching
- Integrate with existing timing and assembly utilities
2025-08-29 23:26:40 -03:00
diodiogod 674e43f411 Version 4.6.5
Complete VibeVoice SRT implementation with character voices

Fix character voice handling:
- Fix audio reference format handling in SRT mode
- Add support for both waveform and audio_path formats
- Enable proper voice cloning for Alice, Bob, and narrator
- Fix cache key generation to match ChatterBox pattern
2025-08-29 23:13:36 -03:00
diodiogod 8a83658baa Fix VibeVoice character voice handling in SRT mode
- Fix audio component generation to match ChatterBox pattern
- Add support for loading audio files from audio_path format
- Enable proper character voice cloning in SRT custom mode
- Resolve voice reference format mismatch between TTS and SRT
2025-08-29 23:13:01 -03:00
diodiogod 9c0dfc3f94 Add VibeVoice SRT support with unified architecture
Implemented complete VibeVoice SRT processor with dual mode support:

Features:
• Native Multi-Speaker Mode: One segment per subtitle using VibeVoice's Speaker 1:, Speaker 2: format
• Custom Character Switching Mode: Multiple segments per subtitle (one per character)
• Full SRT timing support: stretch_to_fit, pad_with_silence, smart_natural, concatenate
• Modular architecture using existing utils (no code duplication)
• Character switching with voice discovery integration
• Pause tag support in both modes
• Smart language grouping and model switching

Technical Implementation:
• Added VibeVoiceSRTProcessor with complete SRT workflow
• Updated unified SRT node to support vibevoice engine
• Added VibeVoice to language_mapper utility
• Proper ComfyUI audio format handling
• Fixed timing adjustment format for detailed reports

Integration:
• Uses existing timing utils, audio assembly, and reporting modules
• Compatible with Character Voices node
• Maintains same interface as ChatterBox/F5-TTS SRT nodes
2025-08-29 22:35:01 -03:00
diodiogod 3925e9a052 Version 4.6.4
Fix ModuleNotFoundError not being caught in conda environments

Critical fix for #12 - catch ModuleNotFoundError in addition to ImportError

Technical details:
- Conda throws ModuleNotFoundError (subclass of ImportError)
- Previous try-catch only handled ImportError, missing ModuleNotFoundError
- Add ModuleNotFoundError to except clause for complete coverage
- Ensures conda cache invalidation is properly triggered
2025-08-29 14:18:15 -03:00
diodiogod 83abbafb93 Version 4.6.3
Improve lazy import robustness for conda environments

Enhanced fix for #12 - TTS Text generation failed: No module named 'utils.audio.audio_hash'

Technical details:
- Add try-catch pattern around lazy imports for better error handling
- Force importlib.invalidate_caches() for conda environments specifically
- Maintain zero performance impact for normal environments
- Only activate fallback logic when ImportError occurs
- Address conda's aggressive import caching and path isolation
2025-08-29 12:51:37 -03:00
diodiogod 882b0fbd67 Version 4.6.2
Fix module discovery issues for lazy imports

Resolves #12 - TTS Text generation failed: No module named 'utils.audio.audio_hash'

Technical details:
- Add path verification before lazy imports in ChatterBox nodes
- Ensure project_root is in sys.path when lazy imports execute
- Fix audio_hash import failures in _generate_stable_audio_component
- Preserve lazy loading design while making imports more robust
2025-08-29 11:52:43 -03:00
diodiogod 4fee53a20e Version 4.6.1
Fix German and Norwegian Voice Changer model loading

Resolves #11 - German Voice-Changer requests .pt files but only .safetensors are available

Technical details:
- Add safetensors support to ChatterboxVC.from_local()
- Implement auto-detection for .safetensors and .pt files
- Add strict=False for model loading compatibility
- Resolve file format mismatch for multilingual VC models
2025-08-29 11:37:24 -03:00
diodiogod e6c6ea3f5b Version 4.6.0
Add VibeVoice TTS engine integration (INITIAL IMPLEMENTATION)

Major new engine integration:
- Complete VibeVoice engine implementation following unified architecture
- Support for Microsoft VibeVoice 1.5B and 7B models with automated downloading
- Dual generation modes: Custom Character Switching and Native Multi-Speaker
- Time-based chunking interface with 90-minute generation capability
- Full Character Voices node integration with proper voice mapping

Technical implementation:
- Unified engine adapter following established patterns (Higgs Audio/ChatterBox)
- Modular pause tag processing using established PauseTagProcessor utilities
- Advanced cache system with character isolation and floating-point precision fixes
- Proper Speaker format (1-based indexing) matching VibeVoice specification
- Comprehensive debug logging for development phase diagnostics

Quality improvements:
- Fix VibeVoice audio normalization to match official implementation (-25 dB FS RMS)
- Implement proper stereo-to-mono conversion with channel averaging
- Add librosa resampling for better audio quality
- Restore accidentally removed Ko-fi sponsor links to README

Note: This is an INITIAL implementation - SRT subtitle support and production-ready
logging will be added in future releases. Debug logs are intentionally verbose
during development phase.
2025-08-29 11:21:28 -03:00
diodiogod 492b4269e2 Restore Ko-fi sponsor links and fix VibeVoice native multi-speaker mode
- Restore accidentally removed Ko-fi sponsor links to README (header badge, title button, support section)
- Fix VibeVoice native multi-speaker voice mapping to use character-specific voices from voice_mapping
- Update Speaker format to use 1-based indexing (Speaker 1:, Speaker 2:) as per VibeVoice specification
- Add comprehensive debug logging for formatted text and voice assignments
- Remove broken engine config voice fallback that caused random voice assignment
2025-08-29 11:16:38 -03:00
diodiogod fe7019c456 Improve VibeVoice Engine chunking tooltip for clarity
- Update chunk_minutes tooltip to clearly indicate it overrides TTS Text chunking settings
- Clarify the parameter hierarchy: engine chunking takes precedence over unified node settings
- Maintain logical separation: VibeVoice handles long-form generation, unified node handles standard chunking
2025-08-29 09:20:09 -03:00
diodiogod edbcb3422d Fix VibeVoice multi-character switching and pause tag support
Major fixes for VibeVoice engine functionality:

Audio Quality Improvements:
- Replace linear interpolation resampling with librosa.resample (matches official)
- Fix normalization to use -25 dB FS RMS instead of peak normalization (matches official)
- Implement proper stereo-to-mono conversion with channel averaging (matches official)

Multi-Character Voice Switching:
- Add proper character mapping using get_character_mapping() like F5-TTS does
- Build complete voice_mapping with fallbacks for all parsed characters
- Fix character parser to auto-discover characters when available_characters is None
- Remove unnecessary fallback logic from processor (handled at wrapper level)

Pause Tag Support:
- Add modular pause tag processing like F5-TTS using generate_vibevoice_with_pause_tags()
- Integrate PauseTagProcessor for [pause:X] support in VibeVoice
- Follow established modular pattern from F5-TTS and ChatterBox implementations

Cache System Improvements:
- Fix floating point precision issues in cache key generation (round to 3 decimals)
- Add proper parameter propagation to cached engine instances
- Ensure cache keys include all generation parameters properly

All changes tested and working - multi-character switching now works correctly
and pause tags are processed properly using the modular system.
2025-08-29 09:01:25 -03:00
diodiogod 293912b317 Fix VibeVoice parameter propagation to cached engine instances
Root Cause: When TTS nodes reused cached VibeVoice engine instances, they updated the wrapper's config but the internal VibeVoice processor still used old parameters.

Fixes:
- Add update_config() method to VibeVoiceProcessor to update internal config
- Add update_config() method to VibeVoiceWrapper to propagate updates to processor
- Modify cached instance reuse to call update_config() instead of just setting config attribute
- This ensures parameter changes (CFG scale, sampling, etc.) are properly propagated through the entire chain

Now when changing CFG 1.2->1.3, the cache will properly invalidate because the processor gets the updated parameters.
2025-08-29 07:36:13 -03:00
diodiogod eec7ee3dd4 Add debug logging for VibeVoice parameter rounding to diagnose cache issue 2025-08-29 02:18:12 -03:00
diodiogod 661c9737c8 Fix VibeVoice cache system: character isolation, floating point precision, and parameter inclusion
Cache Key Generation:
- Fix floating point precision issues by rounding cfg_scale, temperature, top_p to 3 decimals
- Ensure all generation parameters are included in cache key (cfg_scale, use_sampling, etc.)
- Fixed VibeVoiceCacheKeyGenerator to properly handle parameter precision

Character Name Isolation:
- Fix character name normalization breaking cache isolation across different characters
- Pass character name explicitly to generate_segment() instead of relying on params fallback
- Update all generate_segment() calls to include character parameter
- Prevent different characters from sharing same cache key due to 'narrator' fallback

Parameter Updates:
- Round floating point parameters before cache function creation
- Fix cache parameter display to show actual values being used
- Ensure parameter changes (CFG 1.4→1.3, sampling True→False) properly invalidate cache

This fixes the issue where changing generation parameters still hit cache with old values.
2025-08-29 02:15:34 -03:00
diodiogod 2ba5f0a1b4 Add VibeVoice TTS engine integration with full unified architecture support
Implemented complete VibeVoice engine integration following unified modular architecture:

Engine Infrastructure:
- VibeVoice engine wrapper with model management and generation
- Engine adapter for standardized interface integration
- Proper ComfyUI model management integration
- Support for both 1.5B and 7B models (Microsoft + community)

Core Features:
- Two generation modes: Custom Character Switching and Native Multi-Speaker
- Character voice management with [CharacterName] tag support
- Pause tag processing with [pause:xx] syntax
- Time-based chunking UI (minutes converted to characters internally)
- Unified cache system following ChatterBox patterns with 💾 indicators
- Full integration with Character Voices node
- 90-minute generation capability and 4-speaker native mode

Technical Implementation:
- Engine configuration node with ⚙️ emoji consistency
- Unified TTS node integration via processor delegation
- Proper parameter validation and device management
- Dependency management with --no-deps installation approach
- Fixed name collision issues (vibevoice/ → vibevoice_engine/)
- Comprehensive error handling and fallback systems

Architecture Compliance:
- Follows exact Higgs Audio integration patterns
- Uses unified streaming infrastructure
- Maintains modular design under 600 lines per file
- No code duplication, reuses all existing utilities
- Proper separation: Engine → Adapter → Processor → Unified Node

Installation & Dependencies:
- Added VibeVoice to install.py with dependency conflict resolution
- Updated nodes.py with conditional imports and diagnostics
- Comprehensive PROJECT_INDEX.md documentation

Tested and verified:
- Model downloading and loading
- Voice reference handling (dropdown and Character Voices)
- Cache functionality with proper hit indicators
- Multi-character generation and pause processing
2025-08-29 01:15:16 -03:00
diodiogod 128ae80992 Bump version to 4.5.31
- Manual version bump to resolve version conflict
- Previous 4.5.30 already existed
- No functional changes in this commit
2025-08-28 23:38:30 -03:00
diodiogod 57330f9621 Version 4.5.30
Fix F5-TTS model name case sensitivity issue

- Fix uppercase V1 being rejected in model validation
- Add regex normalization for version numbers (V1 -> v1)
- Ensure backward compatibility with saved workflows
2025-08-28 23:31:52 -03:00
diodiogod 3588dc4cdd Add .claude/ to .gitignore to prevent future accidental commits 2025-08-28 21:28:23 -03:00
diodiogod 4a6b7ce0c4 Add ignore/ folder to .gitignore
Remove accidentally committed ignore folder from repository
2025-08-28 19:36:17 -03:00
diodiogod 735fef14ca Version 4.5.29
Fix F5-TTS local model support to avoid Google dependencies

Technical details:
- Add local model checking for E2-TTS variants before cloud download
- Improve F5TTS_Base fallback to prioritize local models
- Update model loading to use ckpt_file parameter when local files exist
- Add clear messaging for local vs cloud model usage
2025-08-28 19:34:29 -03:00
diodiogod b66ee005a5 Version 4.5.28
Fix Mac M1 compatibility and wandb import issues

Technical details:
- Add macOS and M1/M2 architecture detection to installer
- Implement Mac-specific system dependency checking
- Force ARM64-compatible package installation for M1 Macs
- Fix wandb circular import causing node failures
- Improve audio package compatibility on Apple Silicon
2025-08-28 17:47:35 -03:00
diodiogod b371c11b95 Version 4.5.27
Fix Higgs Audio model management with CUDA Graph compatibility toggle

Technical implementation:
- Add enable_cuda_graphs parameter to resolve CUDA Graph vs model unloading conflict
- Implement DynamicCache support for safe memory management mode
- Fix DynamicCache compatibility issues (reset, seq_length, copying)
- Add conditional cache creation and cleanup logic
- Update tooltip with accurate performance trade-offs (12 vs 55+ tok/s)
2025-08-28 17:41:42 -03:00
diodiogod 04940f013e Implement CUDA Graph toggle for Higgs Audio with DynamicCache support
Adds user toggle to choose between high performance (CUDA Graphs) and memory safety:

**High Performance Mode (CUDA Graphs ON):**
- Uses StaticCache for CUDA Graph optimization
- ~55 tokens/sec generation speed
- Cannot safely unload memory (crashes on "Unload Models")
- Users must restart ComfyUI to free VRAM

**Memory Safe Mode (CUDA Graphs OFF):**
- Uses DynamicCache for safe memory management
- ~12 tokens/sec generation speed (78% slower)
- Safe memory unloading with "Unload Models" button
- Full VRAM cleanup and reload cycles work

**Key Implementation Changes:**
- Add enable_cuda_graphs parameter to HiggsAudioServeEngine constructor
- Conditional cache creation: StaticCache vs DynamicCache based on toggle
- Fix DynamicCache compatibility issues:
  - Manual cache clearing (key_cache.clear(), value_cache.clear())
  - Compatible get_seq_length() handling for both cache types
  - Proper cache copying between DynamicCache instances
- Update tooltip with accurate performance numbers (12 tok/s vs 55+ tok/s)
- Complete parameter passing chain from UI to engine constructor

**Files Modified:**
- serve_engine.py: Add constructor parameter, conditional cache creation, DynamicCache reset
- modeling_higgs_audio.py: Fix get_cache_seq_length() compatibility, DynamicCache copying
- unified_model_interface.py: Pass enable_cuda_graphs to constructor
- higgs_audio_engine_node.py: Add toggle UI with accurate performance tooltip
- stateless_wrapper.py: Conditional cleanup logic based on cache type
- higgs_audio.py: Parameter passing integration
- higgs_audio_adapter.py: Extract and pass toggle parameter

Resolves fundamental CUDA Graph vs ComfyUI memory management incompatibility by giving users choice.
2025-08-28 15:01:51 -03:00
diodiogod 9225862881 Complete Higgs Audio stateless wrapper with device consistency fixes
- Fixed device mismatch errors throughout audio tokenization pipeline
- Audio tokenizer input tensors moved to correct device
- Semantic model input tensors moved to semantic model device
- Encoder input tensors moved to encoder device
- Model input tensors moved to model device before generation
- KV cache tensors properly moved to model device
- Memory management working: 11GB VRAM freed on model unload
- Voice cloning functionality restored with full device consistency

Performance: Generation speed reduced from 50 it/s to ~12 it/s due to device enforcement
Memory: Successfully unloads 11GB without crashes via stateless wrapper
2025-08-28 01:20:10 -03:00
diodiogod 5700ec3f0c Implement Higgs Audio stateless wrapper for ComfyUI model management integration
✅ Major Achievement: Higgs Audio can now be safely unloaded from VRAM without crashes!

Key Implementation:
- Created StatelessHiggsAudioWrapper that isolates tensor state to prevent CUDA Graph corruption
- Unified model interface integration with CPU-first loading to prevent VRAM overflow
- Enhanced ComfyUI model wrapper to handle stateless TTS models generically
- Fixed device consistency for all model components (main_model, audio_tokenizer, semantic_model, tokenizer_encoder, tokenizer_quantizer)

Results:
- Successfully freed 11007MB VRAM without crashes
- Model unloading/reloading cycle works correctly
- CUDA Graph corruption issue resolved through tensor isolation

Note: Device mismatch issue during voice cloning still needs resolution, but core memory management functionality is working.
2025-08-28 00:52:49 -03:00
diodiogod b9c8776859 Improve changelog categorization with whole-word matching
Prevent false matches from partial word detection:
- 'add' no longer matches inside 'advanced', 'ladder', 'padding'
- 'fix' no longer matches inside 'prefix', 'fixture'
- 'change' no longer matches inside 'exchange', 'interchange'

Uses regex word boundaries (\b) to match complete words only.
Applied to both simple mode and multiline categorization logic.

Examples:
- 'Fix advanced RAS parameters' → Fixed ✅ (not Added)
- 'Add RAS padding support' → Added ✅ (not Fixed)
- 'Update exchange rates' → Changed ✅ (not Added)
2025-08-27 23:23:37 -03:00
diodiogod ecd7867c74 Fix bump script changelog categorization
- Fix hardcoded simple_mode=True that bypassed proper multiline categorization
- Prioritize 'fix' keywords over 'add' to prevent misclassification
- Remove duplicate Removed section logic
- Now 'Fix advanced...' will correctly categorize as Fixed, not Added

Future version bumps will now use proper keyword-based categorization for multiline changelog entries.
2025-08-27 23:21:08 -03:00
diodiogod d2878ab666 Clean up debug output from RAS parameter implementation
Remove debug logging statements from Higgs Audio engine and TTS processor:
- Remove RAS parameter debug prints from higgs_audio.py
- Remove cache key and parameter validation debug output
- Remove TTS processor config debug statements
- Remove unified node debug output
- Clean up command configuration file

Production ready - all debug output removed
2025-08-27 23:15:23 -03:00
diodiogod 271a3691b1 Version 4.5.26
Fix Higgs Audio RAS parameter integration

Technical details:
- Add missing force_audio_gen, ras_win_len, ras_max_num_repeat parameters to engine node
- Fix parameter passthrough from engine node to TTS processor
- Fix cache key generation to include new RAS parameters
- Update unified node config handling for proper parameter flow
- Add comprehensive debug logging for RAS parameter validation
2025-08-27 23:05:01 -03:00
diodiogod 4197e73bb0 Improve version bump system with better dry-run and documentation
Technical improvements:
- Refactor version_utils.py to extract changelog generation logic
- Add preview_changelog_entry method for dry-run testing
- Enhance dry-run to show actual categorized changelog preview
- Fix code duplication by using shared content generation method

Documentation improvements:
- Consolidate version bump documentation in BUMP_SCRIPT_INSTRUCTIONS.md
- Add comprehensive multiline examples and auto-increment usage
- Add auto-categorization keyword explanations and writing tips
- Add troubleshooting section with common issues and solutions
- Remove redundant CLAUDE_VERSION_MANAGEMENT_GUIDE.md file
2025-08-27 20:19:47 -03:00
diodiogod 90a6922903 Version 4.5.25
Optimize Silent Speech Analyzer cache and preview system

Implement two-tier cache system:
- MediaPipe analysis cache (expensive, video-dependent)
- Filtered results cache (cheap, parameter-dependent)

Preview generation improvements:
- Preview now reflects filtered segments when merge_threshold changes
- No expensive MediaPipe re-analysis for post-processing parameter changes
- Reuses cached frame data and landmarks for pixel-perfect accuracy
- Separated preview generation from analysis phase for better performance
2025-08-27 19:41:46 -03:00
diodiogod e3f6cb74c1 Fix Silent Speech Analyzer caching and post-processing parameter handling
- Fixed UnboundLocalError when visemes disabled by initializing analyzer_method
- Improved cache architecture to separate analysis from post-processing
- Store unfiltered segments in metadata for efficient re-filtering
- Cache both analysis results and filtered results separately
- Remove IS_CHANGED method to avoid ComfyUI cache interference
- Enable instant re-filtering when only merge_threshold/confidence_threshold change
2025-08-27 17:51:33 -03:00
diodiogod 81bcb5573f Clean up node categorization with emoji-enhanced categories
Reorganized 19+ scattered categories into 6 clean, logical categories:
- TTS Audio Suite/🎤 Text to Speech: Main user-facing nodes (TTS Text, TTS SRT, Voice Changer)
- TTS Audio Suite/⚙️ Engines: Engine configuration nodes for ChatterBox, F5-TTS, Higgs Audio, RVC
- TTS Audio Suite/🎭 Voice & Character: Voice management and RVC character models
- TTS Audio Suite/🎵 Audio Processing: Audio tools, analyzers, and processing nodes
- TTS Audio Suite/👄 F5-TTS: F5-TTS specific editing nodes
- TTS Audio Suite/🎬 Video Analysis: Video mouth movement and viseme detection nodes

Improvements:
- Consistent naming hierarchy with proper emoji placement on category names
- Logical grouping by functionality instead of engine type
- Moved video nodes from generic "image/video" to TTS-specific context
- Eliminated inconsistent emoji usage and random category variations
- Better user experience with intuitive node organization

From messy scattered categories to clean professional structure.
2025-08-27 17:20:18 -03:00
diodiogod 9dd93a8186 Update README.md 2025-08-27 15:29:40 -03:00
diodiogod e282575551 Version 4.5.24: Update F5-TTS Speech Editor workflow for v4 compatibility and include in release 2025-08-27 15:26:57 -03:00
diodiogod 57447bf7c5 Update F5-TTS Speech Editor workflow status to reflect v4 compatibility 2025-08-27 15:25:51 -03:00
diodiogod 668d69d720 Version 4.5.23: Fix F5-TTS speech editing vocab handling and E2TTS model compatibility 2025-08-27 15:18:28 -03:00
diodiogod bb7f02b009 Add comprehensive project evolution timeline to README
Complete visual timeline showing TTS Audio Suite evolution:
- ChatterBox Voice Era: Foundation, SRT implementation, modular refactoring
- Multi-Engine Era: F5-TTS integration, character/pause/language systems
- TTS Audio Suite Era: Unified architecture, RVC, Silent Speech, Higgs Audio 2

Features zigzag layout with proper spacing and clear milestone progression
Shows key achievements from v1.0 foundation through v4.5 current state
Highlights critical v1.1 SRT timing system that defined project direction
2025-08-27 10:57:02 -03:00
diodiogod 63a294133d Version 4.5.22: Add Linux system dependency detection and documentation 2025-08-27 01:01:29 -03:00
diodiogod 7e96d3a899 Add Linux system dependency detection and documentation
- Add automatic detection for libsamplerate0-dev and portaudio19-dev in install script
- Display helpful error messages with specific install commands for Ubuntu/Debian and Fedora/RHEL
- Update README with comprehensive system dependency documentation
- Explain why these dependencies are needed (audio resampling and voice recording)
- Windows users unaffected (packages come pre-compiled)

Fixes Discord user feedback about missing libsamplerate0-dev requirement.
2025-08-27 01:00:59 -03:00
diodiogod 364e046099 Version 4.5.21: Update workflows: Add unified TTS SRT workflow and remove outdated ones 2025-08-27 00:37:27 -03:00
diodiogod b0448b987a Update workflows: Add unified TTS SRT workflow and remove outdated ones
- Add new Unified 📺 TTS SRT workflow showcasing all engines (ChatterBox/F5-TTS/Higgs Audio 2)
- Features: Multiple timing modes, multi-character switching, overlap SRT support
- Remove outdated workflows: 📺 Chatterbox SRT and 🎤 📺 F5-TTS SRT
- Update README with new workflow structure and recommendations
2025-08-27 00:36:49 -03:00
diodiogod 511190141e Add comprehensive dependency management guide
Documents all critical dependency conflicts, solutions, and compatibility constraints for universal Python/platform support. Includes troubleshooting guide and testing matrix.
2025-08-27 00:31:28 -03:00
diodiogod ac05e32888 Clarify Higgs Audio 2 native multilingual architecture in changelog
Explain that unlike other engines, Higgs Audio 2 uses single base model
with native multilingual support rather than separate language models
2025-08-26 23:12:13 -03:00
diodiogod 9b7ce8fe22 Standardize changelog formatting for v4.5.20
- Use standard dash bullets instead of bullet points for consistency
- Match format used in v4.5.16 with detailed explanations
- Change conclusion to 'Result:' format for consistency
2025-08-26 23:10:02 -03:00
diodiogod 67fe21fd26 Enhance v4.5.20 changelog with detailed feature descriptions
Add comprehensive details for Higgs Audio language processing improvements:
- Smart language hint conversion examples
- Explicit language detection explanation
- Character switching preservation details
- Console logging improvements description
- Multilingual support coverage

Provides users with clear understanding of v4.5.20 enhancements.
2025-08-26 23:08:25 -03:00
diodiogod ec884ac867 Version 4.5.20: Improve Higgs Audio language processing with smart language hints 2025-08-26 22:59:25 -03:00
diodiogod cadee65a9c Version 4.5.19: Fix numba compatibility issues in install script 2025-08-26 22:58:20 -03:00
diodiogod 0584a13e38 Version 4.5.18: Fix Higgs Audio 2 Engine Character Voices input compatibility 2025-08-26 19:48:36 -03:00
diodiogod 7410f16217 Fix Higgs Audio 2 Engine Character Voices compatibility
- Change opt_second_narrator input type from AUDIO to any_typ to accept Character Voices output
- Extract reference text properly from Character Voices data structure for secondary narrator
- Native multi-speaker modes now use both narrator reference texts for improved voice cloning quality
2025-08-26 19:47:06 -03:00
diodiogod 91f37388ad Version 4.5.17: Fix F5-TTS narrator language detection bug for non-English models 2025-08-26 19:36:19 -03:00
diodiogod 23a9d37c1b Version 4.5.16: Clean up verbose console logging across all TTS engines 2025-08-26 15:58:25 -03:00
diodiogod 4cda81cf12 Version 4.5.15: Fix PyTorch CUDA detection and compatibility issues in install script 2025-08-26 14:30:58 -03:00
diodiogod 83d426d9b5 Version 4.5.14: Fix ChatterBox model loading failure caused by watermarker initialization 2025-08-26 10:28:59 -03:00
diodiogod bad1dd4a12 Version 4.5.13: Clean up excessive startup logging messages 2025-08-26 09:47:39 -03:00
diodiogod 344ebd34b3 Version 4.5.12: Fix Python 3.13 Windows console Unicode encoding issue 2025-08-26 08:02:47 -03:00
diodiogod 713a968552 Remove accidentally committed test file 2025-08-25 23:26:37 -03:00
diodiogod 7e5bce87a0 Version 4.5.11
Implement ComfyUI memory management integration for TTS models

- Add ComfyUI model management support for ChatterBox and F5-TTS engines
- Models can now be unloaded using ComfyUI Manager 'Unload Models' button
- Fix device assignment consistency after memory management cycles
- Higgs Audio models remain in memory due to CUDA graph limitations
- Reduce repetitive memory management warning messages
- Clean up unnecessary debug output from model loading

Relates to #6 - enables manual model unloading for memory optimization
2025-08-25 23:20:24 -03:00
diodiogod 0505e735c0 Remove repetitive Higgs Audio memory management warnings
- Add class-level flags to show memory management warnings only once per session
- Prevents spam of 'Memory Management Limitation' messages for every segment
- Still shows 'Skipping unload' for each unload attempt, but detailed warnings only once
- Significantly reduces console spam during multi-segment SRT generation
2025-08-25 22:58:46 -03:00
diodiogod c0ed537a00 Remove unnecessary debug messages from TTS output
- Remove repeated 'TTS Model loaded_size query' messages
- Only show CUDA graph checking for Higgs Audio models (not ChatterBox)
- Comment out cache key generation debug (not needed for end users)
- Reduces console spam while preserving relevant debugging for Higgs models
2025-08-25 22:53:23 -03:00
diodiogod b9772e869b Fix audio segments/timings mismatch in SRT streaming
- Consolidate segments by subtitle_index to handle character changes within subtitles
- Multiple character segments within one subtitle are now concatenated into single audio
- Prevents 'Number of audio segments must match number of timings' error
- Fixes missing last subtitle in streaming mode timing reports
- Properly handles cases where segments are split by character tags like [Alice] within subtitles
2025-08-25 22:48:15 -03:00
diodiogod cf8ad0d6a8 Fix missing subtitles in ChatterBox SRT streaming timing reports
- Map filtered timing adjustments back to original subtitle indices
- Ensure timing reports show all subtitles even when some segments fail
- Create dummy adjustments for failed segments to maintain index alignment
- Fixes issue where last subtitle was missing from streaming timing reports
2025-08-25 22:40:17 -03:00
diodiogod 35314e4577 Fix ChatterBox SRT streaming failures and language fallbacks
- Fix IndexError in timing engine when segments fail processing
- Filter out failed segments before timing calculations to prevent length mismatches
- Change unsupported language behavior from failure to English fallback
- ChatterBox now falls back to English model for unsupported languages (like French)
- Prevents 'float division by zero' errors from failed segment processing
2025-08-25 22:28:31 -03:00
diodiogod 1b65064671 Fix language-only tags using character alias instead of selected narrator voice
- Language-only tags like [fr:] should use selected narrator voice (dropdown/opt_narrator)
- Previously, "narrator" from [fr:] was resolved via character alias to "david_attenborough cc3"
- Added skip_narrator_alias parameter to normalize_character_name method
- Detect language-only tags and skip alias resolution for narrator character
- Preserves narrator voice priority: opt_narrator > dropdown > character map
2025-08-25 22:18:37 -03:00
diodiogod 9a37b2826b Fix F5-TTS language model switching for character alias system
Remove restrictive logic that blocked language switching when no explicit [lang:character] tags present. Character alias system (Alice→German, Bob→French) now correctly loads language-specific models:

- F5-DE for German segments
- F5-FR for French segments
- F5TTS_v1_Base for English segments

Language switching works with both explicit tags and character alias mappings.
2025-08-25 21:48:00 -03:00
diodiogod 5ff56384c5 Fix ChatterBox device mismatch after memory management cycles
- Add missing s3gen.to(device) in ChatterBox TTS class to move all components
- Fix Smart Loader device resolution for "auto" to "cuda" conversion
- Implement comprehensive device management for cached model reuse
- Disable Higgs Audio memory management due to CUDA graph limitations
- Add recursive component device moving in ComfyUI wrapper

Memory management now works correctly for ChatterBox and F5-TTS engines while preserving Higgs Audio performance.
2025-08-25 21:11:26 -03:00
diodiogod 5c741f7403 Implement comprehensive CUDA graph memory corruption fix for Higgs Audio models
- Add global cache invalidation system to prevent reuse of corrupted model state
- Implement deferred CUDA graph initialization in Higgs Audio engine (prevents corruption during model loading)
- Add shadow storage system for Higgs Audio models to avoid PyTorch memory conflicts
- Implement timestamped engine caching across all unified nodes (TTS Text, TTS SRT, Voice Changer)
- Add comprehensive ComfyUI model management integration with proper device handling
- Fix model unloading/reloading cycle to prevent "Assertion input[0] != 0 failed" CUDA crashes
- Add model resurrection system that reuses existing models instead of creating new instances
- Implement intelligent cache validation with global invalidation flags
- Add extensive debugging and state tracking for model lifecycle management
- Fix device compatibility between string and torch.device objects in ComfyUI integration

This resolves the critical memory corruption issue that occurred when using ComfyUI's
"Unload models" or "Clear VRAM" buttons with Higgs Audio models containing CUDA graphs.
2025-08-25 17:56:11 -03:00
diodiogod 6857c7437e Version 4.5.10: Fix Higgs Audio seed parameter cache bug and implement ComfyUI model management integration 2025-08-25 13:34:38 -03:00
diodiogod 5104f4a1ea Version 4.5.9: Reduce Higgs Audio KV cache sizes to prevent OOM with multiple models 2025-08-25 11:37:24 -03:00
diodiogod efff0eb159 Version 4.5.8: Fix Python 3.13 compatibility with comprehensive librosa fallback system 2025-08-25 11:31:12 -03:00
diodiogod a1e2bd06bb Version 4.5.7: Fix Python 3.13 compatibility: Replace librosa with torchaudio in Higgs Audio engine 2025-08-25 10:17:26 -03:00
diodiogod ebb74b68b9 Add comprehensive missing dependencies for --no-deps packages
- Add all missing librosa dependencies: audioread, threadpoolctl, standard-aifc/sunau/chunk, audioop-lts
- Add all missing descript-audiotools dependencies: flatten-dict, ffmpy, importlib-resources, randomname, markdown2, pyloudnorm, pystoi, torch-stoi, ipython, tensorboard
- Add matplotlib for F5-TTS utils_infer.py
- Ensures all engines can load without missing import errors
2025-08-24 12:00:02 -03:00
diodiogod f6f44b781a Version 4.5.6: Fix HuBERT download structure and complete cache detection implementation 2025-08-24 10:30:50 -03:00
diodiogod f050d99e1b Add comprehensive HuggingFace cache checking to all model downloaders
- Add HuBERT model cache detection with proper repo ID extraction
- Add audio separation model cache detection for SayanoAI/RVC-Studio models
- Add legacy RVC/UVR model cache detection with updated function signatures
- Update vocal removal node to check cache for RVC Studio models
- Skip cache check for GitHub-sourced models (ZFTurbo, OpenSeeFace)
- Ensure cache paths used directly without file copying
- Follow local>cache>download to local pattern consistently across all engines
- Tested: Higgs Audio properly uses cached models without re-downloading
2025-08-24 10:30:50 -03:00
diodiogod 3e6cd8508f Fix Higgs Audio 2 cache detection and model loading
- Add proper HuggingFace cache detection in higgs_audio_downloader.py
- Implement smart path resolution following F5-TTS pattern
- Fix KeyError in HIGGS_AUDIO_MODELS configuration consistency
- Add legacy path checking for models/HiggsAudio, models/Higgs_2
- Add model completeness verification to prevent loading corrupted files
- Prioritize: local verified models -> HF cache -> download to TTS structure
- Ensure no unnecessary downloads when cache exists
- Tested: successfully uses cached models without re-downloading
2025-08-24 10:30:50 -03:00
diodiogod 6f8f1e1bab Update README.md 2025-08-23 00:19:02 -03:00
diodiogod 3e53d7100a Version 4.5.5: Fix critical protobuf dependency conflict - all engines now load successfully on Python 3.13 2025-08-22 14:31:21 -03:00
diodiogod bb88d74b29 Fix protobuf conflict: Move descript-audiotools to --no-deps list
- descript-audiotools forces protobuf downgrade from 6.32.0 to 3.19.6
- This breaks google.protobuf.internal.builder import needed by engines
- Install descript-audiotools with --no-deps to prevent protobuf conflicts
- Maintains compatibility while fixing ChatterBox and F5-TTS engine failures
2025-08-22 14:24:14 -03:00
diodiogod c99a269d9f Comprehensive dependency fix: Add all missing sub-dependencies for --no-deps packages
- Based on PyPI metadata analysis of cached-path, descript-audio-codec, and librosa
- Added all required dependencies for packages installed with --no-deps flag
- Ensures F5-TTS, Higgs Audio, and ChatterBox engines work without missing imports
- Uses flexible version constraints to avoid conflicts with other custom nodes
- Fixes google.cloud.storage, einops, lazy_loader, and other missing imports
2025-08-22 14:12:28 -03:00
diodiogod 7de30d4ada Add missing dependencies for --no-deps packages
Add required dependencies for packages installed with --no-deps:
- google-api-core, google-auth, googleapis-common-protos (for cached-path)
- einops, torch-audiomentations (for descript-audio-codec)

This should fix:
- F5-TTS: No module named 'google.api_core'
- Higgs Audio: Missing dac (descript-audio-codec) dependencies
2025-08-22 13:55:09 -03:00
diodiogod bbe21f846a Fix missing SAFE dependencies based on DEPENDENCY_TESTING_RESULTS.md
Add all documented SAFE packages that were missed:
- s3tokenizer>=0.1.7 (ChatterBox - tested safe, no conflicts)
- vector-quantize-pytorch (Higgs Audio - clean install)
- resemble-perth (ChatterBox - works in ChatterBox)
- diffusers>=0.30.0 (ChatterBox - likely safe)
- audio-separator>=0.35.2 (vocal removal - heavy deps but no conflicts)
- hydra-core>=1.3.0 (F5-TTS - clean install, minimal dependencies)

Add missing dependencies for --no-deps packages:
- lazy_loader (required by librosa)
- filelock (required by cached-path)

This should resolve all missing module errors for ChatterBox, F5-TTS, and Higgs Audio engines.
2025-08-22 13:46:26 -03:00
diodiogod 578c006ee1 Improve install script visual design with ASCII-safe formatting
- Replace Unicode emojis with ASCII symbols ([+], [X], [!], [i], [*])
- Add professional terminal-style installation summary with boxes
- Fix Unicode encoding crashes on Windows systems completely
- Maintain visual appeal while ensuring universal compatibility
2025-08-22 13:37:42 -03:00
diodiogod 5c674cd16c Version 4.5.4: Version 4.5.4: Fix Windows Unicode encoding crash in install script 2025-08-22 13:30:31 -03:00
diodiogod 8637c28370 Remove obsolete install_new.py development file
install.py is the correct installation script used by ComfyUI Manager
2025-08-22 13:09:14 -03:00
diodiogod 3bcf1fedf9 Move testing documentation to IgnoredForGitHubDocs
- Move DEPENDENCY_TESTING_RESULTS.md to docs/IgnoredForGitHubDocs/
- Move mediapipe_python313_workaround.md to docs/IgnoredForGitHubDocs/
- These files contain development testing notes not needed in public repo
2025-08-22 12:58:37 -03:00
diodiogod 5f8b83f049 Version 4.5.3: Version 4.5.3: Complete installation system with Python 3.13 compatibility and dependency management 2025-08-22 12:56:53 -03:00
diodiogod cf3ad81c9f Version 4.5.2: Fix Higgs Audio transformers 4.46+ compatibility 2025-08-22 02:44:34 -03:00
diodiogod cfd2fe8af3 Fix Higgs Audio compatibility with transformers 4.46+
- Add fallback attention implementation handling for None values
- Fix GenerationConfig.generation_kwargs missing attribute
- Add compatibility for _has_unfinished_sequences API changes
- Fix StaticCache max_cache_len attribute detection
- Handle attention layer return value format changes (2 vs 3 values)
- Re-enable CUDA graph capture with graceful fallback
- Maintain backward compatibility with older transformers versions

All fixes use try-catch patterns to support transformers 4.30-4.46+
2025-08-22 02:43:57 -03:00
diodiogod a5f90bf169 Fix ChatterBox VC model discovery to respect user language choice
- Voice Changer now uses language-specific model discovery like TTS
- Finds correct language subfolder (e.g., /chatterbox/English/) instead of parent directory
- Ensures VC models load with proper conds.pt file for voice conversion
- Maintains backward compatibility with generic discovery fallback
2025-08-22 02:04:42 -03:00
diodiogod 0752aa0e1c Version 4.5.1: Fix Higgs Audio organized folder download and tokenizer loading issues 2025-08-21 09:00:16 -03:00
diodiogod 06ddaa84ab Version 4.5.0: Add Higgs Audio 2 voice cloning integration with smart processing system 2025-08-21 01:16:19 -03:00
diodiogod 7eb0c62d50 Enhance chunk combiner with smart per-junction analysis
- Replace global method selection with intelligent per-junction analysis
- Analyze each chunk boundary individually for optimal transition method
- Add comprehensive junction analysis rules (sentence boundaries, commas, conjunctions, forced splits)
- Implement per-junction audio processing with crossfade, silence, and concatenate methods
- Enhance generation info reporting with detailed per-junction method breakdown
- Add backward compatibility for silence_padding method alias
- Fix tensor dimension mismatches in silence generation
- Update method descriptions to reflect new per-junction capabilities

The auto method now intelligently selects different combination methods for each junction
based on text analysis, resulting in more natural audio transitions.
2025-08-21 00:31:27 -03:00
diodiogod 73a821a962 Add smart modular chunk combination system
- Create ChunkCombiner utility with intelligent auto-selection
- Replace duplicate combination logic across all engines
- Smart auto method analyzes chunk split patterns (sentence boundaries, commas, forced splits)
- Higgs Audio: Add tqdm progress bars, fix cache invalidation, modular combination
- F5-TTS: Replace duplicate logic with ChunkCombiner
- ChatterBox: Upgrade from basic concatenation to full modular system
- Fix max_new_tokens minimum from 128 to 1, clarify tooltip
- All combination methods preserve cache optimization (post-processing)
2025-08-20 23:20:46 -03:00
diodiogod 5c975243af Fix hardcoded min max_new_tokens from 128 to 1
- Remove hardcoded minimum of 128 tokens in engine node and adapter
- Now respects user setting of low values like max_new_tokens=3
- Fixes progress bar showing wrong total (was showing 128 instead of actual value)
- Fixes generation producing more tokens than requested
2025-08-20 22:15:13 -03:00
diodiogod e85b622792 Add tqdm progress bars to Higgs Audio generation and fix parameter flow
- Add TqdmProgressStreamer class to boson_multimodal serve engine
- Shows real-time token generation progress with rate (tokens/s)
- Displays: "🗣️ Generating: 45%|████▌ | 234/512 tokens [00:12<00:15, 18.2it/s]"
- Fix generation parameters not flowing to SRT/TTS segment calls
- Add seed parameter to Higgs Audio cache key generation
- Clean up generation logging messages for better readability

Now Higgs Audio has same detailed progress feedback as ChatterBox and F5-TTS.
2025-08-20 22:12:51 -03:00
diodiogod 8a0bcd7e97 Remove Higgs Audio voice_preset system and improve reference text handling
Major refactoring to simplify Higgs Audio voice system and fix reference text usage:

### Voice Preset System Removal:
- Remove voice_preset parameter from engine node, adapter, and core engine
- Remove load_voice_presets() and get_voice_preset_path() methods
- Remove voice preset validation and fallback logic
- Convert JSON voice config to standard .wav + .txt files

### Reference Text Fixes:
- Fix character mapping to use f5tts engine type for proper text loading
- Characters now use their own reference text files (not narrator fallback)
- Only narrator voice uses narrator reference text (logical consistency)
- Add comprehensive reference text debugging to console logs

### Parameter Improvements:
- Update defaults based on user testing: temp 0.8, top_p 0.6, top_k 80
- Allow max_new_tokens minimum of 1 for complete flexibility
- Update tooltips to reflect new recommended values

### Console Log Cleanup:
- Consolidate verbose character loading messages
- Show reference text usage clearly without technical clutter
- Replace tensor shape logs with duration-based messages

### Narrator Fallback Hierarchy:
- Priority 1: opt_narrator (Character Voices node)
- Priority 2: narrator_voice dropdown
- Priority 3: Mapped narrator (David Attenborough from alias map)
- Priority 4: None (basic TTS)

This significantly improves voice cloning quality and system consistency.
2025-08-20 21:30:59 -03:00
diodiogod dcc1606b92 Fix Higgs Audio engine initialization bypass bug
Bug: load_base_model() was returning early if model name matched,
but each new adapter instance has uninitialized engine even with
same model name.

Fix: Also check if self.higgs_engine.engine is not None before
skipping initialization. This ensures engine gets properly
initialized for each new adapter instance.
2025-08-20 20:18:03 -03:00
diodiogod 2cce8b3bbe Fix Higgs Audio engine initialization in adapter
Each adapter instance was creating uninitialized HiggsAudioEngine.
Added automatic engine initialization on first generate_segment_audio call.

This ensures engine is properly loaded before generation attempts.
2025-08-20 20:12:56 -03:00
diodiogod c272ea4192 Fix critical Higgs Audio parameter flow - SRT node config extraction
CRITICAL: TTS SRT node was using engine_data.get('config', {}) while
TTS Text node uses engine_data directly. This caused temperature and
all generation parameters to be lost (config was empty {}).

Fixed SRT node to match TTS Text pattern: config = engine_data

This resolves the 'Wrapper config temp: None' issue and ensures
temperature flows from engine node to generation properly.
2025-08-20 20:06:31 -03:00
diodiogod 3e6760fde4 Test centralized cache fix - remove adapter caching debug 2025-08-20 20:00:13 -03:00
diodiogod 29ffeacc15 Fix Higgs Audio cache invalidation through centralized cache system
ROOT CAUSE: Higgs Audio was creating isolated cache instances instead of
using the global shared cache system used by other engines.

Changes:
- Move HiggsAudioCacheKeyGenerator to centralized utils/audio/cache.py
- Register Higgs Audio generator in global cache alongside F5-TTS/ChatterBox
- Fix HiggsAudioEngine to use get_audio_cache() instead of AudioCache()
- Remove duplicate cache key generator from higgs_audio.py
- Ensure all engines use same global GLOBAL_AUDIO_CACHE

RESULT: Temperature/generation parameter changes now properly invalidate
cache since all engines share the same centralized cache system.

This is the proper architectural fix using existing unified cache infrastructure.
2025-08-20 19:42:19 -03:00
diodiogod 03c4a42de3 Fix critical Higgs Audio cache invalidation bug
CRITICAL BUG: Temperature and generation parameters were not invalidating
audio cache due to incorrect engine instance caching logic.

Changes:
- Engine instance cache keys now use only stable parameters (model, device, adapter)
- Dynamic generation parameters (temperature, top_p, top_k, max_new_tokens)
  are excluded from engine instance cache key
- Cached engine instances get updated config with new generation parameters
- Ensures cache invalidation when temperature/sampling parameters change

BEFORE: Same cache hits despite temperature changes (wrong behavior)
AFTER: Different cache keys for different generation parameters (correct)

Applies to both TTS Text and TTS SRT unified nodes for Higgs Audio.
2025-08-20 19:25:16 -03:00
diodiogod d99318fe42 Modularize SRT overlap detection across all TTS engines
- Create utils/timing/overlap_detection.py with SRTOverlapHandler class
- Centralize overlap detection and smart_natural fallback logic
- Update ChatterBox, F5-TTS, and Higgs Audio to use modular utility
- Remove duplicate _detect_overlaps methods from all engines
- Ensure consistent overlap handling behavior across engines

Now all engines use the same logic for detecting overlapping SRT segments
and automatically falling back from smart_natural to pad_with_silence mode.
2025-08-20 18:44:02 -03:00
diodiogod 878a505bcb Improve Higgs Audio Engine node tooltips with detailed explanations
- Enhanced temperature, top_p, top_k, and max_new_tokens parameter descriptions
- Added emoji indicators and recommended value ranges
- Provided clear usage guidance and examples for each parameter
- Maintained existing multi_speaker_mode tooltip quality
2025-08-20 18:06:30 -03:00
diodiogod 9da10fb174 Fix Higgs Audio SRT timing report to use modular calculation system
- Replace hardcoded stretch factor calculation with modular calculate_timing_adjustments function
- Use same approach as F5-TTS and ChatterBox for consistent timing reports
- Import timing calculations from utils.system.import_manager (unified SRT modules)
- Fix timing report to show correct stretch factors (0.78x, 1.58x, 0.65x) instead of wrong 1.00x

Now all engines use the same modular timing calculation system instead of duplicating logic.
2025-08-20 14:02:27 -03:00
diodiogod b9a512ecb7 Fix Higgs Audio SRT stretch_to_fit mode parameter format error
- Fix TypeError in assemble_stretch_to_fit: convert SRTSubtitle objects to (start_time, end_time) tuples
- Add proper adjustments generation for stretch_to_fit and pad_with_silence modes
- Ensure timing report generation works correctly for all timing modes

Error was: 'cannot unpack non-iterable SRTSubtitle object' when switching to stretch_to_fit mode
Fix: Convert srt_segments to target_timings tuples before passing to assembler
2025-08-20 13:52:41 -03:00
diodiogod b17b0dae9a Refactor Higgs Audio TTS to use modular processor architecture
- Extract 203 lines of hardcoded TTS logic from unified node into HiggsAudioTTSProcessor
- Create proper engine processor following ChatterBox/F5-TTS delegation pattern
- Replace hardcoded implementation with clean delegation (20 lines vs 203 lines)
- Reduce unified TTS Text node from 633 to 462 lines (27% reduction)
- Maintain all functionality while achieving architectural consistency
- Processor uses existing modular utilities (PauseTagProcessor, character_parser, voice discovery, AudioProcessingUtils)

Architecture now consistent across all engines:
- UnifiedTTSTextNode → ChatterboxTTSNode ✅
- UnifiedTTSTextNode → F5TTSNode ✅
- UnifiedTTSTextNode → HiggsAudioTTSProcessor ✅ (new)

Both TTS Text and TTS SRT unified nodes now follow clean modular architecture.
2025-08-20 13:48:34 -03:00
diodiogod a046532044 Refactor Higgs Audio SRT to use modular processor architecture
- Extract 267 lines of hardcoded SRT logic from unified node into HiggsAudioSRTProcessor
- Create proper engine processor following ChatterBox/F5-TTS delegation pattern
- Replace hardcoded implementation with clean delegation (23 lines vs 267 lines)
- Fix timing report generation using unified SRTReportGenerator utilities
- Maintain all functionality while achieving architectural consistency
- Update PROJECT_INDEX.md to document new processor and architectural layers

Architecture now consistent across all engines:
- UnifiedTTSSRTNode → ChatterboxSRTTTSNode ✅
- UnifiedTTSSRTNode → F5TTSSRTNode ✅
- UnifiedTTSSRTNode → HiggsAudioSRTProcessor ✅ (new)
2025-08-20 13:29:04 -03:00
diodiogod 2113d26f93 Fix Higgs Audio 2 Custom Character Switching implementation
- Replace duplicate character processing code with modular utilities
- Use PauseTagProcessor for [pause:2] tag support
- Fix narrator voice mapping to use connected Character Voices audio
- Add character segment summary logging with 🎭 emoji
- Ensure fallback to narrator voice for unknown characters
- Follow ChatterBox patterns for consistent implementation

Custom Character Switching now properly supports:
- Character tags [Alice], [Bob], [Narrator]
- Pause tags [pause:2] with proper silence insertion
- Voice file discovery and fallback handling
- Connected narrator voice from Character Voices node
2025-08-19 18:37:36 -03:00
diodiogod a90617dc12 Update Higgs Audio multi-speaker mode recommendations
- Reorder dropdown to prioritize Conversation mode over System Context
- Mark Conversation mode as RECOMMENDED for best voice cloning quality
- Mark System Context mode as EXPERIMENTAL due to audio quality issues
- Clarify that pause tags are handled naturally by Higgs Audio's internal timing
- Guide users toward the more stable and higher quality Conversation mode
2025-08-19 17:40:58 -03:00
diodiogod bdbd0f4199 Implement whole conversation processing for Higgs Audio native modes
- Removed pause segmentation that was interfering with model's natural flow
- Process entire conversations as single units (aligned with official implementation)
- Fixed System Context mode tensor errors by eliminating segmentation
- Conversation mode now working with accurate voice cloning
- System Context mode functional but produces lower quality audio
- Simplified ChatML structure and removed excessive debug output
- Model naturally handles timing and speaker transitions
2025-08-19 17:39:11 -03:00
diodiogod 2229530a2d Debug and improve Higgs Audio 2 native multi-speaker modes
- Fixed speaker context tracking across pause segments
- Added debug output for ChatML message structure analysis
- Removed reference text requirements for Higgs Audio (audio-only voice cloning)
- Identified System Context mode tensor processing issues
- Research shows Higgs Audio expects full conversation processing, not pause segmentation
2025-08-19 17:30:19 -03:00
diodiogod 2acb9be259 Fix Higgs Audio 2 engine integration issues
- Fixed engine initialization issue where adapter never called initialize_engine
- Fixed model configuration key mismatch (generation_repo vs generation_model)
- Fixed tokenizer download file name (model.pth vs pytorch_model.bin)
- Fixed tensor dimension issues for ComfyUI audio output format
- Added comprehensive multi-speaker mode system with three options:
  * Custom Character Switching: Use [Alice], [Bob] with character voice files
  * Native Multi-Speaker (System Context): Use [SPEAKER0], [SPEAKER1] with opt_second_narrator
  * Native Multi-Speaker (Conversation): Same as above but conversation history mode
- Removed audio_priority widget, now follows standard TTS priority logic
- Fixed mode selection bug where config was read from wrong object
- Added proper audio format handling for Character Voices in native modes
- Fixed pause tag support in all modes including native multi-speaker
- Improved tooltips to clarify mode requirements and text formats
2025-08-19 16:25:44 -03:00
diodiogod 280350ba69 Fix F5-TTS narrator voice priority in character switching mode
When using character switching with a provided narrator voice, ensure the
narrator character always uses the provided voice instead of character mapping.

Priority order:
- Narrator segments: Use provided narrator voice (opt_narrator > dropdown)
- Character segments: Use character mapping if available, fallback to narrator voice

This maintains character switching while respecting narrator voice selection.
2025-08-19 00:16:43 -03:00
diodiogod 829b0aed81 Fix F5-TTS bundled integration and ChatterBox model detection
- Bundle F5-TTS source code to resolve numpy dependency conflicts
- Fix all import paths to use bundled engines.f5_tts package
- Update ChatterBox model detection to properly scan language subdirectories
- Fix missing return statement in F5-TTS engine instantiation
- Add missing sys import in Higgs Audio downloader
- Update requirements.txt with bundled F5-TTS approach
2025-08-19 00:06:50 -03:00
diodiogod f6a6cfb2f0 Fix Higgs Audio 2 engine integration issues
- Fix HiggsAudioEngineNode return type from HIGGS_AUDIO_ENGINE to TTS_ENGINE for unified node compatibility
- Remove duplicated parameters: seed, enable_cache, enable_chunking, max_tokens_per_chunk, silence_between_chunks_ms
- Keep engine-specific parameters: max_new_tokens (generation limit), voice_preset, audio_priority, system_prompt
- Update TTS Text node to pass max_chars_per_chunk to Higgs Audio adapter
- Maintain character-based chunking via ImprovedChatterBoxChunker with token-to-char conversion
- Preserve clean separation: universal parameters in TTS Text, engine-specific in Engine nodes

The Higgs Audio Engine Node now properly connects to unified TTS Text and TTS SRT nodes
while maintaining engine-specific generation parameters that other engines don't have.
2025-08-18 18:37:59 -03:00
diodiogod 4393568e53 Add Higgs Audio 2 engine integration with voice cloning
- Implement complete Higgs Audio 2 TTS engine with voice cloning capabilities
- Add minimal essential dependencies: descript-audio-codec, vector_quantize_pytorch
- Create modular engine architecture following established patterns:
  * engines/higgs_audio/higgs_audio.py - Main engine wrapper (688 lines)
  * engines/higgs_audio/higgs_audio_downloader.py - Model downloader (357 lines)
  * engines/adapters/higgs_audio_adapter.py - Unified adapter (363 lines)
  * nodes/engines/higgs_audio_engine_node.py - Configuration node (153 lines)
- Copy boson_multimodal modules from reference implementation
- Use unified ImprovedChatterBoxChunker instead of custom chunking for consistency
- Integrate with existing cache system and unified downloader
- Add 8 voice presets (belinda, en_woman, en_man, etc.) in voices_examples/
- Support both preset voices and custom reference audio for cloning
- Enable chunking for unlimited length text generation
- Follow F5-TTS implementation patterns for consistency

Features:
- High-quality voice cloning from 30+ second reference audio
- Multi-language support (English, Chinese dialects)
- Configurable generation parameters (temperature, top_p, top_k)
- Automatic model downloading to organized TTS/HiggsAudio/ structure
- Proper caching integration for performance
- Character-based chunking with silence insertion between chunks
2025-08-18 13:49:45 -03:00
diodiogod a37ef41c9a Consolidate README features section 2025-08-16 17:06:25 -03:00
diodiogod 708df78cec Add ComfyUI reference folders to gitignore 2025-08-16 16:50:25 -03:00
diodiogod ad9e20f4a5 Remove accidentally committed ComfyUI reference folders
Remove ComfyUI and ComfyUI_frontend directories that were added for reference only.
Add them to .gitignore to prevent future accidental commits.
2025-08-16 16:50:25 -03:00
diodiogod 10384d481c Merge pull request #2 from wzgrx/main
Update requirements.txt
2025-08-16 16:46:53 -03:00
diodiogod acc28d6051 Merge branch 'main' into main 2025-08-16 16:46:38 -03:00
wzgrx ca6d546180 Update requirements.txt 2025-08-16 15:11:19 +08:00
1075 changed files with 351914 additions and 8848 deletions
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: diogogo
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+53
View File
@@ -0,0 +1,53 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug]"
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Init log**
Provide your init log. PLEASE ERASE THE EXEMPLE BELOW, copy paste it yours (you can surround it with triple backticks ```) I NEED to know your system info + TTS Audio Suite version + other init dependency information like below:
```
Total VRAM 24564 MB, total RAM 65440 MB
pytorch version: 2.8.0+cu129
Set vram state to: NORMAL_VRAM
Device: cuda:0 NVIDIA GeForce RTX 4090 : cudaMallocAsync
Using pytorch attention
Python version: 3.13.6 (tags/v3.13.6:4e66535, Aug 6 2025, 14:36:00) [MSC v.1944 64 bit (AMD64)]
ComfyUI version: 0.3.57
ComfyUI frontend version: 1.25.11
[Prompt Server] web root: J:\stablediffusion1111s2\Data\Packages\ComfyUINEWCLEAN\py313New\ComfyUI_windows_portable\python_embeded\Lib\site-packages\comfyui_frontend_package\static
✅ Numba JIT working properly - no workarounds needed
ℹ️ Critical package versions: NumPy 2.2.6, Librosa 0.11.0, Numba 0.61.2, PyTorch 2.8.0+cu129, TorchAudio 2.8.0+cu129, Transformers 4.56.0, SoundFile 0.13.1
✅ ChatterboxTTS loaded from bundled package
✅ ChatterboxVC loaded from bundled package
✅ F5-TTS loaded from system package
APEX FusedRMSNorm not available, using native implementation
🎭 Character voices: Found 32 characters, 10 aliases
======================================================================
🚀 TTS Audio Suite v4.9.6
```
**Provide your ERROR full log**
copy paste it (you can surround it with triple backticks ```):
```
error...
```
**Screenshots**
If applicable, add screenshots to help explain your problem.
**System information**
- OS: [e.g. iOS]
- ComfyUI type: [portable; direct venv; conda; desktop app]
- Python version: [check the init log]
**Additional context**
Add any other context about the problem here.
_Note: Bug reports without the error logs, depending on the issue, might be ignored. It's very hard to provide support without minimal information._
+7
View File
@@ -1,3 +1,7 @@
# Reference ComfyUI folders (not part of this project)
ComfyUI/
ComfyUI_frontend/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -137,6 +141,7 @@ venv/
ENV/
env.bak/
venv.bak/
comfyui_env/
# Spyder project settings
.spyderproject
@@ -205,6 +210,7 @@ CLAUDE.md
*CLAUDE.md
docs/Dev reports/CLAUDE_VERSION_MANAGEMENT_GUIDE.md
.claude-code.json
.claude/
IgnoredForGitHubDocs/
@@ -218,3 +224,4 @@ IgnoredForGitHubDocs/
.roo/
.vscode/
ignore/
+2787 -1
View File
File diff suppressed because it is too large Load Diff
Submodule ComfyUI deleted from e4f7ea105f
Submodule ComfyUI_frontend deleted from 89c78b0ecb
+144 -21
View File
@@ -5,15 +5,19 @@
## Architecture Overview
This extension features a **unified modular architecture** supporting multiple TTS engines:
- **Unified Node Interface**: Single set of nodes (TTS Text, TTS SRT, Voice Changer) that work with any engine
- **Engine Adapters**: Modular adapters for ChatterBox, F5-TTS, and RVC voice conversion
- **Centralized Download System**: Unified downloader eliminates HuggingFace cache duplication with direct downloads to organized TTS/ folder structure
- **Unified Node Interface**: Single set of nodes (TTS Text, TTS SRT, Voice Changer) that work with any engine via clean delegation
- **ComfyUI Model Management Integration**: All TTS models now integrate with ComfyUI's native model management system, enabling "Clear VRAM" functionality and automatic memory management
- **Universal Model Loading System**: Standardized model loading interface across all engines (ChatterBox, F5-TTS, Higgs Audio, VibeVoice, Step Audio EditX, RVC, Audio Separation) with fallback support
- **Engine Processors**: Internal processing engines for each TTS system (ChatterBox, F5-TTS, Higgs Audio, VibeVoice, Step Audio EditX) handling engine-specific orchestration
- **Engine Adapters**: Modular adapters for ChatterBox, F5-TTS, Higgs Audio 2, VibeVoice, Step Audio EditX, and RVC voice conversion
- **Inline Edit Tag System**: Universal post-processing system applying Step Audio EditX effects (emotion, style, speed, paralinguistic) to any TTS engine's output via inline tags like `<Laughter>` or `<happy>`
- **Centralized Download System**: Unified downloader eliminates HuggingFace cache duplication with direct downloads to organized TTS/ folder structure, with full extra_model_paths.yaml support for shared model directories
- **Thread-Safe Architecture**: Stateless ChatterBox wrapper eliminates shared state corruption (Note: parallel processing is slower than sequential)
- **Universal Streaming Infrastructure**: Unified streaming system with configurable workers (batch_size parameter) - sequential mode (batch_size=0) recommended for optimal performance
- **Multilingual Support**: German and Norwegian models for both ChatterBox TTS and Voice Conversion
- **Multilingual Support**: German and Norwegian models for ChatterBox, plus ChatterBox Official 23-Lang supporting 23 languages including Arabic, Chinese, Danish, Dutch, English, Finnish, French, German, Greek, Hebrew, Hindi, Italian, Japanese, Korean, Malay, Norwegian, Polish, Portuguese, Russian, Spanish, Swedish, Swahili, and Turkish
- **Smart Language Grouping**: SRT processing by language groups to minimize model switching
- **Character Voice Management**: Centralized character voice system with flexible input types
- **Comprehensive Audio Processing**: Interactive waveform analyzer, vocal separation, and audio mixing
- **Comprehensive Audio Processing**: Interactive waveform analyzer, vocal separation, audio mixing, and specialized audio editing
- **Professional Voice Conversion**: RVC implementation with UVR5 vocal separation and advanced pitch control
## Documentation Files
@@ -28,14 +32,16 @@ This extension features a **unified modular architecture** supporting multiple T
**docs/CHARACTER_SWITCHING_GUIDE.md** - Guide for multi-character TTS using [CharacterName] tags, voice organization, and character voice management system
**docs/CHATTERBOX_V2_SPECIAL_TOKENS.md** - Complete guide for ChatterBox Official 23-Lang v2 special emotion and sound tokens (<giggle>, <whisper>, <sigh>, etc.) with 30+ expressive tokens for enhanced TTS generation
**docs/INLINE_EDIT_TAGS_USER_GUIDE.md** - Comprehensive guide for inline edit tag system enabling Step Audio EditX post-processing on any TTS engine output. Covers paralinguistic tags (<Laughter>, <Breathing>), emotion tags (<happy>, <sad>), style tags (<whisper>, <serious>), and speed control tags, with iteration control, language support notes, and best practices
**docs/🌊_Audio_Wave_Analyzer-Complete_User_Guide.md** - Complete user guide for Audio Wave Analyzer with interactive waveform visualization and timing extraction
**docs/VERSION_3.1_RELEASE_GUIDE.md** - Release documentation for character switching system and overlapping subtitles support
### Developer Documentation
**docs/Dev reports/CLAUDE_VERSION_MANAGEMENT_GUIDE.md** - Automated version management using enhanced scripts with changelog generation
**docs/Dev reports/F5TTS_INTEGRATION_SPECIFICATION.md** - Technical specification for F5-TTS integration architecture
**docs/Dev reports/F5TTS_STREAMING_INTEGRATION_PLAN.md** - Universal streaming architecture plan to eliminate spaghetti code and enable F5-TTS streaming
@@ -46,9 +52,9 @@ This extension features a **unified modular architecture** supporting multiple T
**docs/Dev reports/POST_V4.2.3_DEVELOPMENT_REVIEW.md** - Comprehensive analysis of 45 commits implementing universal streaming architecture and performance insights
**docs/PAUSE_TAGS_IMPLEMENTATION_REPORT.md** - Implementation details for pause tag system with timing control syntax
**docs/Dev reports/PAUSE_TAGS_IMPLEMENTATION_REPORT.md** - Implementation details for pause tag system with timing control syntax
**docs/REFACTORING_PLAN.md** - Architectural refactoring plan and migration strategy documentation
**docs/Dev reports/REFACTORING_PLAN.md** - Architectural refactoring plan and migration strategy documentation
## Core Architecture Files
@@ -61,7 +67,7 @@ This extension features a **unified modular architecture** supporting multiple T
### Engine Implementations
**engines/chatterbox/** - Complete ChatterBox TTS and VC engine implementation
- **tts.py** - ChatterBox TTS engine with multilingual support and perth watermarking
- **tts.py** - ChatterBox TTS engine with multilingual support, perth watermarking, and ComfyUI model management integration
- **vc.py** - ChatterBox Voice Conversion with multilingual model loading
- **language_models.py** - Language model registry for English, German, and Norwegian models
- **audio_timing.py** - Audio timing utilities and time-stretching functionality
@@ -71,11 +77,40 @@ This extension features a **unified modular architecture** supporting multiple T
- **stateless_wrapper.py** - Thread-safe stateless wrapper eliminating shared state corruption in parallel generation
- **models/** - Complete ChatterBox model architecture (S3Gen, T3, tokenizers, voice encoder)
**engines/chatterbox_official_23lang/** - ChatterBox Official 23-Lang multilingual engine implementation
- **tts.py** - Main ChatterBox Official 23-Lang TTS engine with 23-language support
- **vc.py** - Voice conversion engine supporting all 23 languages with refinement passes
- **language_models.py** - Language model configuration and multilingual support definitions
- **stateless_wrapper.py** - Thread-safe wrapper for multilingual parallel processing
- **batch_processor.py**, **adaptive_processor.py**, **gpu_aware_processor.py** - Advanced processing systems
- **models/** - Complete multilingual model architecture (S3Gen, T3, tokenizers, voice encoder) optimized for 23-language generation
**engines/f5tts/** - F5-TTS engine implementation and editing capabilities
- **f5tts.py** - Main F5-TTS wrapper with ComfyUI integration
- **f5tts_edit_engine.py** - Speech editing engine for targeted word replacement
- **audio_compositing.py** - Audio compositing with crossfade and segment processing
**engines/higgs_audio/** - Higgs Audio 2 engine implementation with voice cloning
- **higgs_audio.py** - Main Higgs Audio 2 wrapper with voice cloning and chunking support
- **higgs_audio_downloader.py** - Model auto-download system for generation and tokenizer models
- **boson_multimodal/** - Complete boson_multimodal implementation from HiggsAudio team
**engines/vibevoice_engine/** - VibeVoice engine implementation with multi-speaker and long-form capabilities
- **vibevoice_engine.py** - Main VibeVoice wrapper with 90-minute generation and multi-speaker support
- **vibevoice_downloader.py** - Model auto-download system for Microsoft VibeVoice models (1.5B and 7B)
**engines/step_audio_editx/** - Step Audio EditX engine implementation with audio editing capabilities
- **step_audio_editx.py** - Main Step Audio EditX wrapper with zero-shot voice cloning and emotion/style/speed editing support
- **step_audio_editx_downloader.py** - Model auto-download system for Step Audio EditX models
- **step_audio_editx_impl/** - Complete Step Audio EditX implementation with FunASR integration
- **config/** - Edit configuration and prompt definitions for emotion, style, speed, and paralinguistic editing
- **funasr_detach/** - Complete FunASR framework implementation for speech processing (models, frontends, tokenizers, metrics)
- **stepvocoder/** - CosyVoice2 BigVGAN vocoder implementation for high-quality audio synthesis
- **tts.py**, **tts_infer.py** - TTS generation engines with multi-modal processing
- **tokenizer.py** - Speech tokenization system
- **model_loader.py** - Model loading and management utilities
- **quantization/** - AWQ quantization support for VRAM reduction
**engines/rvc/** - RVC (Real-time Voice Conversion) engine implementation
- **__init__.py** - RVC engine initialization and ComfyUI integration
- **hubert_downloader.py** - HuBERT model auto-download from Hugging Face with TTS/ folder organization
@@ -99,6 +134,12 @@ This extension features a **unified modular architecture** supporting multiple T
**engines/adapters/f5tts_adapter.py** - F5-TTS engine adapter with parameter mapping and cache integration
**engines/adapters/higgs_audio_adapter.py** - Higgs Audio 2 engine adapter with voice cloning and parameter validation
**engines/adapters/vibevoice_adapter.py** - VibeVoice engine adapter with multi-speaker format conversion and parameter mapping
**engines/adapters/step_audio_editx_adapter.py** - Step Audio EditX engine adapter with voice cloning and audio editing parameter mapping
**engines/adapters/chatterbox_streaming_adapter.py** - ChatterBox streaming adapter bridging existing implementation to universal streaming system
**engines/adapters/f5tts_streaming_adapter.py** - F5-TTS streaming adapter enabling parallel processing with language model switching
@@ -111,13 +152,23 @@ This extension features a **unified modular architecture** supporting multiple T
**nodes/engines/f5tts_engine_node.py** - F5-TTS engine configuration node with model selection and generation settings
**nodes/engines/higgs_audio_engine_node.py** - Higgs Audio 2 engine configuration node with voice cloning parameters and generation settings
**nodes/engines/vibevoice_engine_node.py** - VibeVoice engine configuration node with multi-speaker modes and long-form generation settings
**nodes/engines/rvc_engine_node.py** - RVC voice conversion node with pitch control and quality settings
**nodes/engines/index_tts_engine_node.py** - IndexTTS-2 engine configuration node with character voice selection and generation parameters
**nodes/engines/index_tts_emotion_options_node.py** - 🌈 IndexTTS-2 Emotion Vectors node with interactive radar chart for 8-emotion control
**nodes/engines/step_audio_editx_engine_node.py** - ⚙️ Step Audio EditX Engine configuration node with zero-shot voice cloning, quantization options (int4/int8), and generation parameters for both clone and edit modes
### Unified Interface Nodes
**nodes/unified/tts_text_node.py** - Universal TTS text generation node working with any configured engine
**nodes/unified/tts_srt_node.py** - Universal SRT subtitle processing with smart language grouping and timing
**nodes/unified/tts_srt_node.py** - Universal SRT subtitle processing node - clean delegation layer that routes to engine-specific processors
**nodes/unified/voice_changer_node.py** - Universal voice conversion node with multilingual model support and flexible audio inputs
@@ -125,19 +176,44 @@ This extension features a **unified modular architecture** supporting multiple T
**nodes/shared/character_voices_node.py** - Character voice management system providing NARRATOR_VOICE outputs for any TTS node
### Text Processing Nodes
**nodes/text/phoneme_text_normalizer_node.py** - 📝 Phoneme Text Normalizer with multilingual text preprocessing for improved TTS pronunciation. Features 4 processing methods: Pass-through, Unicode Decomposition (ą→a̧), IPA Phonemization (espeak backend), and Character Mapping (ASCII fallback). Supports auto-language detection and cross-platform phonemizer support.
### Engine-Specific Nodes
*Note: These are internal processors/engines used by the Unified nodes, not direct ComfyUI interface nodes. They handle engine-specific orchestration while the Unified nodes provide the user interface.*
**nodes/chatterbox/** - ChatterBox engine implementation nodes (called by Unified nodes)
- **chatterbox_tts_node.py** - ChatterBox TTS engine node with streaming batch processing and character switching
- **chatterbox_srt_node.py** - ChatterBox SRT engine node with streaming parallel processing and timing modes
- **chatterbox_vc_node.py** - ChatterBox voice conversion engine with iterative refinement
**nodes/chatterbox_official_23lang/** - ChatterBox Official 23-Lang multilingual processors
- **chatterbox_official_23lang_processor.py** - Main TTS processor supporting all 23 languages with character switching
- **chatterbox_official_23lang_srt_processor.py** - SRT subtitle processor with multilingual timing and audio assembly
- **chatterbox_official_23lang_vc_processor.py** - Voice conversion processor supporting all 23 languages with refinement passes
**nodes/f5tts/** - F5-TTS specific nodes
- **f5tts_node.py** - Direct F5-TTS generation node
- **f5tts_srt_node.py** - F5-TTS SRT processing with language grouping
- **f5tts_edit_node.py** - F5-TTS speech editor for word replacement
- **f5tts_edit_options_node.py** - Advanced F5-TTS editing configuration
**nodes/higgs_audio/** - Higgs Audio 2 internal processors
- **higgs_audio_srt_processor.py** - Higgs Audio SRT orchestrator with multi-speaker support, character switching, and timing modes (internal processor used by Unified SRT node)
**nodes/vibevoice/** - VibeVoice internal processors
- **vibevoice_processor.py** - VibeVoice TTS orchestrator with multi-speaker support and long-form generation handling (internal processor used by Unified TTS node)
**nodes/step_audio_editx/** - Step Audio EditX internal processors
- **step_audio_editx_processor.py** - Step Audio EditX TTS orchestrator with zero-shot voice cloning and emotion/style/speed editing (internal processor used by Unified TTS node)
- **step_audio_editx_srt_processor.py** - Step Audio EditX SRT processor with character switching and inline edit tag support for subtitle-based TTS workflows
### Step Audio EditX Specialized Nodes
**nodes/step_audio_editx_special/step_audio_editx_audio_editor_node.py** - 🎨 Step Audio EditX - Audio Editor node for post-processing audio with 14 emotions (happy, sad, angry, etc.), 32 styles (whisper, serious, child, etc.), speed control (faster/slower), 10 paralinguistic effects (<Laughter>, <Breathing>, <Sigh>), denoising, and VAD. Supports iterative editing (1-5 passes) and iteration caching for experimentation
### Audio Processing System
**nodes/audio/analyzer_node.py** - Interactive Audio Wave Analyzer with web interface and timing extraction
@@ -172,26 +248,46 @@ This extension features a **unified modular architecture** supporting multiple T
### Model Management
**utils/models/manager.py** - Intelligent model discovery and caching with multilingual support for both TTS and VC models
**utils/models/manager.py** - Intelligent model discovery and caching with multilingual support for both TTS and VC models, integrated with ComfyUI model management and extra_model_paths.yaml for shared storage support
**utils/models/smart_loader.py** - Universal smart model loader preventing duplicate model loading across all engines and modes
**utils/models/f5tts_manager.py** - F5-TTS specific model management extending base manager functionality
**utils/models/f5tts_manager.py** - F5-TTS specific model management extending base manager functionality, now with ComfyUI integration
**utils/models/language_mapper.py** - Language-to-model mapping system with fallback support
**utils/models/fallback_utils.py** - Model fallback and error recovery utilities
**utils/models/comfyui_model_wrapper/** - Modular ComfyUI model wrapper system with engine-specific handlers (base_wrapper.py, model_manager.py, cache_utils.py, engine_handlers.py) enabling TTS models to integrate with ComfyUI's native model management
**utils/models/unified_model_interface.py** - Universal model loading interface providing standardized factory pattern for all engines (ChatterBox, F5-TTS, Higgs Audio, RVC, Audio Separation) with ComfyUI integration
**utils/models/factory_config.py** - Standardized `ModelLoadConfig` dataclass for consistent factory parameters across all TTS engines
**utils/models/engine_registry.py** - Engine capability registry defining what each engine supports (voice conversion, multilingual switching, corruption recovery, etc.)
**utils/models/fallback_handler.py** - Generic fallback chain utility for graceful model loading degradation (e.g., try local → download → fallback language)
**utils/models/exceptions.py** - Standardized exception hierarchy for all model loading operations (TTSModelLoadingError, TTSModelNotFoundError, TTSDeviceError, etc.)
**utils/models/extra_paths.py** - ComfyUI extra_model_paths.yaml integration system enabling shared model directories across multiple ComfyUI installations with intelligent path resolution and fallback support
### Audio Processing
**utils/audio/processing.py** - Comprehensive audio utilities for tensor manipulation, normalization, and format conversion
**utils/ffmpeg_utils.py** - Centralized FFmpeg dependency handling with graceful fallbacks for audio conversion and timing analysis
**utils/audio/analysis.py** - Advanced audio analysis including waveform processing, silence detection, and timing extraction
**utils/audio/audio_hash.py** - Centralized content-based hashing for consistent cache keys across all processing modes
**utils/audio/cache.py** - Unified caching system for TTS engines with engine-specific cache management
**utils/audio/chunk_combiner.py** - Modular chunk combination utility with smart auto-selection based on text analysis (sentence boundaries, commas, forced splits) and detailed timing information
**utils/audio/chunk_timing.py** - Chunk combination timing information helper providing standardized timing info integration across all TTS engines
### Text Processing
**utils/text/character_parser.py** - Universal character switching system with [CharacterName] tag parsing and language-aware processing
@@ -200,9 +296,15 @@ This extension features a **unified modular architecture** supporting multiple T
**utils/text/pause_processor.py** - Pause tag parsing supporting [pause:xx] syntax for precise timing control
**utils/text/segment_parameters.py** - Per-segment parameter system enabling inline control of seed, temperature, cfg, speed, and other engine-specific parameters via pipe-separated syntax like [Alice|seed:42|temperature:0.5]. Supports aliases (cfg_weight→cfg, temp→temperature) and case-insensitive input with universal parameter filtering and validation across all TTS engines
**utils/text/phonemizer_utils.py** - F5-TTS multilingual phonemization system with IPA conversion, cross-platform backend support (espeak-phonemizer-windows/phonemizer), smart language detection, and model-specific exceptions for optimal quality
**utils/text/step_audio_editx_special_tags.py** - Step Audio EditX inline edit tag system with tag conversion, stripping, detection, and UI option generation for paralinguistic effects, emotions, styles, and speed control. Enables universal post-processing of TTS output across all engines
### Voice Management
**utils/voice/discovery.py** - Advanced voice file discovery with dual folder support and character mapping
**utils/voice/discovery.py** - Enhanced voice file discovery with multi-path fallback system (models/voices/, models/TTS/voices/, extra_model_paths.yaml directories, voices_examples/), character mapping, and alias loading with priority support
**utils/voice/multilingual_engine.py** - Central orchestrator for multilingual TTS with language switching optimization
@@ -212,9 +314,15 @@ This extension features a **unified modular architecture** supporting multiple T
**utils/system/subprocess.py** - Safe subprocess execution with error handling and process isolation
### Compatibility System
**utils/compatibility/transformers_patches.py** - Centralized transformers compatibility patches (monkey patches) managing version compatibility across different transformers library versions including FlashAttentionKwargs, BaseStreamer, DynamicCache properties, and VibeVoice generation method signatures
**utils/compatibility/numba_compat.py** - Centralized Numba/Librosa compatibility system with fast startup testing and intelligent JIT fallback management for Python 3.13+ compatibility
### Downloads
**utils/downloads/unified_downloader.py** - Centralized download system for all engines eliminating HuggingFace cache duplication with direct HTTP downloads to organized TTS/ structure
**utils/downloads/unified_downloader.py** - Centralized download system for all engines eliminating HuggingFace cache duplication with direct HTTP downloads to organized TTS/ structure, featuring full extra_model_paths.yaml support for shared model directories
**utils/downloads/model_downloader.py** - RVC-specific model auto-download system with direct downloads (legacy, integrated with unified system)
@@ -242,6 +350,12 @@ This extension features a **unified modular architecture** supporting multiple T
## Web Interface Components
### TTS Tag Editor (Modularized Architecture)
**web/string_multiline_tag_editor.js**, **widget-event-handlers.js**, **widget-ui-builder.js**, **widget-parameter-section.js**, **widget-preset-system.js**, **editor-state.js**, **tag-utilities.js**, **syntax-highlighter.js**, **font-controls.js** - Modular TTS tag editor with event handling, UI sections, parameters, presets, state management, tag utilities, syntax highlighting, and font controls for character/parameter tag editing with undo/redo, formatting, and persistence
### Audio Analysis
**web/audio_analyzer_interface.js** - Main ComfyUI integration and node communication for Audio Wave Analyzer
**web/audio_analyzer_core.js** - Core JavaScript functionality for waveform visualization
@@ -266,8 +380,14 @@ This extension features a **unified modular architecture** supporting multiple T
**web/chatterbox_voice_capture.js** - Voice recording interface for microphone input
**web/index_tts_emotion_radar.js** - ComfyUI integration for 🌈 IndexTTS-2 Emotion Vectors radar chart
**web/emotion_radar_canvas_widget.js** - Interactive radar chart widget with dynamic color blending and click/drag controls
**web/chatterbox_srt_showcontrol.js** - SRT subtitle display and timing controls
**web/step_audio_editx_showcontrol.js** - Step Audio EditX SRT subtitle display and timing controls
**web/audio_analyzer.css** - Styling for audio analyzer interface components
## Development Tools
@@ -297,6 +417,7 @@ This extension features a **unified modular architecture** supporting multiple T
**voices_examples/** - Character voice reference files with both audio samples and text descriptions
- **male/** - Male voice examples with reference audio and text
- **female/** - Female voice examples with reference audio and text
- **higgs_audio/** - Higgs Audio 2 voice presets (belinda, en_woman, en_man, chadwick, vex, etc.) with config.json
- **#character_alias_map.txt** - Character alias mapping configuration
- Individual character voice files (Clint Eastwood, David Attenborough, Morgan Freeman, Sophie Anderson)
@@ -311,10 +432,12 @@ This extension features a **unified modular architecture** supporting multiple T
The TTS Audio Suite follows a **unified modular architecture** where:
1. **Engine Nodes** (`nodes/engines/`) provide user configuration interfaces
2. **Unified Nodes** (`nodes/unified/`) offer consistent user experience across all engines
3. **Engine Implementations** (`engines/`) handle the actual TTS/VC processing
4. **Adapters** (`engines/adapters/`) bridge engines to the unified interface
5. **Utilities** (`utils/`) provide shared functionality across all components
6. **Web Interface** (`web/`) enables interactive features like audio analysis
2. **Unified Nodes** (`nodes/unified/`) serve as clean delegation layers routing to appropriate engine processors
3. **Engine Processors** (`nodes/chatterbox/`, `nodes/f5tts/`, `nodes/higgs_audio/`, `nodes/step_audio_editx/`) handle engine-specific orchestration and workflow logic
4. **Engine Implementations** (`engines/`) handle the actual TTS/VC processing and model inference
5. **Adapters** (`engines/adapters/`) bridge low-level engines to higher-level processors
6. **Inline Edit System** (`utils/text/step_audio_editx_special_tags.py`) provides universal post-processing with Step Audio EditX across all engines
7. **Utilities** (`utils/`) provide shared functionality across all components
8. **Web Interface** (`web/`) enables interactive features like audio analysis
This design allows easy addition of new engines while maintaining a consistent user experience and shared optimization features across all TTS engines.
This layered design ensures consistent user experience while allowing engine-specific optimizations. The unified nodes act as thin delegation layers, eliminating code duplication and maintaining architectural consistency across all TTS engines. The inline edit tag system adds a universal post-processing layer, enabling Step Audio EditX capabilities (emotion, style, speed, paralinguistic effects) to be applied to any TTS engine's output.
+901 -172
View File
File diff suppressed because it is too large Load Diff
+196 -2
View File
@@ -2,7 +2,7 @@
TTS Audio Suite - Universal multi-engine TTS extension for ComfyUI
Unified architecture supporting ChatterBox, F5-TTS, and future engines like RVC:
• 🎤 TTS Text (unified text-to-speech)
• 📺 TTS SRT (unified SRT subtitle timing)
• 📺 TTS SRT (unified SRT subtitle timing)
• 🔄 Voice Changer (unified voice conversion)
• ⚙️ Engine nodes (ChatterBox, F5-TTS)
• 🎭 Character Voices (voice reference management)
@@ -13,6 +13,139 @@ import importlib.util
import os
import sys
# CRITICAL: Apply compatibility patches BEFORE any other imports
#
# Timing is critical: The monkey-patches must be applied BEFORE libraries are imported
# by any other module. If we wait until after other imports, they will already
# be loaded and our monkey-patches won't affect the already-imported module references.
#
# PyTorch patches solve TWO PyTorch 2.9 issues:
# 1. TorchCodec DLL incompatibility on Windows - Global patch uses scipy instead
# 2. PyTorch 2.9's changed torchaudio.load() returning raw int16 - safe_load_audio() normalizes
#
# Transformers patches solve:
# 1. Step Audio EditX tokenization bug in transformers 4.54+ (audio tokens not recognized)
# 2. Various model compatibility issues
try:
# Load pytorch_patches directly by file path to avoid package import issues
pytorch_patches_path = os.path.join(os.path.dirname(__file__), "utils", "compatibility", "pytorch_patches.py")
spec = importlib.util.spec_from_file_location("pytorch_patches_module", pytorch_patches_path)
pytorch_patches_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(pytorch_patches_module)
# Apply the patches (will only apply on PyTorch 2.9+, silently skip on older versions)
pytorch_patches_module.apply_pytorch_patches(verbose=True)
except Exception as e:
print(f"⚠️ Warning: Could not apply PyTorch patches: {e}")
# Apply transformers compatibility patches
try:
# Load transformers_patches directly by file path
transformers_patches_path = os.path.join(os.path.dirname(__file__), "utils", "compatibility", "transformers_patches.py")
spec = importlib.util.spec_from_file_location("transformers_patches_module", transformers_patches_path)
transformers_patches_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(transformers_patches_module)
# Apply the patches (will only apply on transformers 4.54+, silently skip on older versions)
transformers_patches_module.apply_transformers_patches(verbose=True)
except Exception as e:
print(f"⚠️ Warning: Could not apply Transformers patches: {e}")
# Smart Numba Compatibility System - tests and applies fixes only when needed
try:
from utils.compatibility import setup_numba_compatibility
# Apply smart compatibility setup (fast startup test)
compatibility_results = setup_numba_compatibility(quick_startup=True, verbose=True)
except ImportError:
# Fallback to simple approach if compatibility module not found
import sys
import os
if sys.version_info >= (3, 13):
# Basic test: try librosa.stft and apply workaround if it fails
try:
import numpy as np
import librosa
test_audio = np.random.randn(512).astype(np.float32)
_ = librosa.stft(test_audio, hop_length=256, n_fft=512)
# Only show when there's a problem, not success
# Mark that we've tested numba compatibility
import sys
sys.modules['__main__']._tts_numba_tested = True
except Exception as e:
if "'function' object has no attribute 'get_call_template'" in str(e):
os.environ['NUMBA_DISABLE_JIT'] = '1'
os.environ['NUMBA_ENABLE_CUDASIM'] = '1'
try:
import numba
numba.config.DISABLE_JIT = True
except ImportError:
pass
print("🔧 Applied numba JIT workaround for Python 3.13 compatibility")
else:
print(f"⚠️ Librosa test failed with different error: {e}")
else:
# Only show warning when JIT is disabled (indicates a problem)
pass
# TorchCodec note: Removed torchcodec dependency to eliminate FFmpeg system requirement
# torchaudio.load() works fine with fallback backends (soundfile, scipy)
import warnings
import sys
import os
# Version disclosure for troubleshooting
def print_critical_versions():
"""Print versions of critical packages for troubleshooting"""
critical_packages = [
('numpy', 'NumPy'),
('librosa', 'Librosa'),
('numba', 'Numba'),
('torch', 'PyTorch'),
('torchaudio', 'TorchAudio'),
('transformers', 'Transformers'),
('accelerate', 'Accelerate'),
('soundfile', 'SoundFile'),
]
version_info = []
for pkg_name, display_name in critical_packages:
try:
module = __import__(pkg_name)
version = getattr(module, '__version__', 'unknown')
version_info.append(f"{display_name} {version}")
except ImportError:
version_info.append(f"{display_name} not installed")
print(f"ℹ️ Critical package versions: {', '.join(version_info)}")
def check_ffmpeg_availability():
"""Check ffmpeg availability and log status"""
try:
from utils.ffmpeg_utils import FFmpegUtils
if FFmpegUtils.is_available():
# Only show when unavailable (problem)
pass
else:
print("⚠️ FFmpeg not found - using fallback audio processing (reduced quality)")
print("💡 Install FFmpeg for optimal performance: https://ffmpeg.org/download.html")
except ImportError:
# Fallback check if utils not available yet
try:
import subprocess
result = subprocess.run(['ffmpeg', '-version'], capture_output=True, timeout=5)
if result.returncode == 0:
# Only show when unavailable (problem)
pass
else:
print("⚠️ FFmpeg not found - using fallback audio processing (reduced quality)")
except Exception:
print("⚠️ FFmpeg not found - using fallback audio processing (reduced quality)")
print("💡 Install FFmpeg for optimal performance: https://ffmpeg.org/download.html")
# Print versions and check dependencies immediately for troubleshooting
print_critical_versions()
check_ffmpeg_availability()
# Check for old ChatterBox extension conflict
def check_old_extension_conflict():
"""Check if the old ComfyUI_ChatterBox_SRT_Voice extension is installed"""
@@ -78,7 +211,68 @@ __description__ = "Universal multi-engine TTS extension for ComfyUI with unified
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
# Define web directory for JavaScript files
# Define web directory for JavaScript files (settings UI)
WEB_DIRECTORY = "./web"
# Register API endpoint for widget data
def setup_api_routes():
"""Setup API routes for widget communication"""
try:
import json
from server import PromptServer
from aiohttp import web
@PromptServer.instance.routes.get("/api/tts-audio-suite/available-characters")
async def get_available_characters_endpoint(request):
"""API endpoint to get available TTS character voices including aliases"""
try:
from utils.voice.discovery import get_available_characters, voice_discovery
characters = list(get_available_characters())
# Also get character aliases
aliases = list(voice_discovery._character_aliases.keys()) if hasattr(voice_discovery, '_character_aliases') else []
# Combine and deduplicate
all_chars = sorted(set(characters + aliases))
return web.json_response({"characters": all_chars})
except Exception as e:
print(f"⚠️ Error retrieving available characters: {e}")
return web.json_response({"characters": [], "error": str(e)})
@PromptServer.instance.routes.get("/api/tts-audio-suite/available-languages")
async def get_available_languages_endpoint(request):
"""API endpoint to get available language codes from the canonical language mapper"""
try:
from utils.models.language_mapper import LANGUAGE_ALIASES
# Get all unique canonical language codes (the values in LANGUAGE_ALIASES)
languages = sorted(set(LANGUAGE_ALIASES.values()))
return web.json_response({"languages": languages})
except Exception as e:
print(f"⚠️ Error retrieving available languages: {e}")
# Fallback list
return web.json_response({"languages": ["en", "de", "fr", "ja", "es", "it", "pt", "th", "no"], "error": str(e)})
@PromptServer.instance.routes.post("/api/tts-audio-suite/settings")
async def set_inline_tag_settings_endpoint(request):
"""API endpoint to receive settings from frontend for inline edit tags"""
print("🔧 Settings endpoint called") # Immediate print to verify endpoint is reached
try:
data = await request.json()
precision = data.get("precision", "auto")
device = data.get("device", "auto")
print(f"🔧 Received settings: precision={precision}, device={device}")
# Store in global settings that edit_post_processor can access
from utils.audio.edit_post_processor import set_inline_tag_settings
set_inline_tag_settings(precision=precision, device=device)
return web.json_response({"status": "success", "precision": precision, "device": device})
except Exception as e:
print(f"⚠️ Error setting inline tag settings: {e}")
return web.json_response({"status": "error", "error": str(e)})
except Exception as e:
print(f"⚠️ Could not setup API routes: {e}")
# Setup API routes when extension loads
setup_api_routes()
# nodes.py already handles all the startup output and status reporting
+418
View File
@@ -0,0 +1,418 @@
# 🎨 Advanced TTS Tag Editor Node
## Overview
Advanced multiline string editor node that extends ComfyUI's standard multiline widget with a sidebar containing context-aware controls for TTS-specific tags (character switching, parameters, pauses). The node features real-time tag generation, preset management, and intelligent syntax support based on the CHARACTER_SWITCHING_GUIDE and PARAMETER_SWITCHING_GUIDE.
---
## Core Features
### 1. Editor Interface
- **Textarea**: Standard multiline editor (like ComfyUI's native string node)
- **Left Sidebar**: Vertical control panel with buttons and dropdowns
- **Smart Selection Detection**: Identify if selection is `[Character]text` or plain text
- **Caret Position Tracking**: Insert tags at cursor position or wrap selection
- **Mid-Sentence Tag Insertion**: Tags can be inserted anywhere in text, not just at the beginning (e.g., `"text text [alice|seed:2] more text"`)
- **Undo/Redo Stack**: Full history of all edits with visual indicators, accessible via buttons and keyboard shortcuts (Ctrl+Z / Ctrl+Shift+Z)
### 2. Sidebar Controls - Basic
#### Character Management
- Dropdown selector for available characters (auto-discovered from `voices_examples/`)
- Text input to specify custom character name
- Button: "Add Character" → wraps selection or inserts at caret as `[CharacterName]text`
- Shows current available characters with icons/indicators
#### Parameter Buttons
(appear if character is selected)
- `[Seed]` - Opens number input (0-4294967295) → adds `|seed:X`
- `[Temp]` - Opens slider (0.1-2.0) → adds `|temperature:X`
- `[CFG]` - Opens slider (engine-specific range) → adds `|cfg:X`
- `[Speed]` - Slider (0.5-2.0, F5-TTS only) → adds `|speed:X`
- `[Steps]` - Spinner (1-100, VibeVoice/IndexTTS) → adds `|inference_steps:X`
- `[Top-P]` / `[Top-K]` - For advanced engines
#### Pause Control
- Time input field with duration selector (milliseconds/seconds)
- Button: "Insert Pause" → adds `[pause:XXXms]` or `[pause:XXs]` at caret
- Quick presets: "0.5s", "1s", "2s" buttons
#### Language Control
- Dropdown for supported languages (en, de, fr, ja, es, it, pt, th, no)
- Works with character dropdown: `[language:Character]` format
- Shows which models are available/loaded
#### Undo/Redo Controls
- **Undo Button** (`[↶]`) - Revert last change
- **Redo Button** (`[↷]`) - Restore undone change
- **History Indicator** - Shows current position in edit history (e.g., "Step 5/10")
- Keyboard shortcuts: `Ctrl+Z` (Undo), `Ctrl+Shift+Z` (Redo)
- Full persistence: Undo/redo history survives session reload
### 3. Preset System
#### Preset Management UI
- Three preset slots (Preset 1, Preset 2, Preset 3)
- Each slot has:
- Save button → captures configured tags
- Load button → applies preset to selection/caret
- Delete button → clears saved preset
- Label field → custom name for the preset
#### Preset Content
- Stores: character, language, and all parameters
- Example: "Alice Calm" = `[de:Alice|seed:42|temp:0.3|cfg:0.7]`
- Can be applied as wrapper around selection or at caret
#### Preset Persistence
- Store in ComfyUI widget state (node.serialize/deserialize)
- Also optionally save to local config file for sharing
- **Full Session Persistence**: All presets, settings, and metadata survive ComfyUI session reload
### 4. Advanced Features
#### Syntax Validation
- Real-time visual indicators for tag syntax errors
- Highlight brackets, detect mismatched `[` `]`
- Warn on invalid parameter values (out of range)
- Suggest corrections (e.g., `temp` → `temperature`)
#### Multi-Selection Support
- Apply same tag to multiple selected segments
- Split by paragraph breaks or sentence punctuation
- Batch apply parameters across multiple `[Character]text` blocks
#### Tag Inspector
- Show existing tags in selection/current line
- Checkbox UI to toggle tags on/off temporarily
- Quick-edit dialog for existing tag values
#### Auto-Formatting
- Button: "Auto-Format Tags" → organize tags consistently
- Validates and cleans malformed tags
- Groups parameters in standard order: `[char|seed|temp|cfg|speed]`
#### Visual Feedback
- Color-code different tag types:
- Character tags: **Blue** `[Character]`
- Parameters: **Green** `|seed:value`
- Pauses: **Orange** `[pause:1s]`
- Language tags: **Purple** `[de:Character]`
- Hover tooltips explaining what each tag does
---
## SRT Support & Formatting
### SRT-Aware Mode Detection
The editor **automatically detects SRT format** and enables specialized visualization:
```srt
1
00:00:01,000 --> 00:00:04,000
[Alice] Hello! I'm Alice speaking with precise timing.
2
00:00:04,500 --> 00:00:09,500
[Bob|seed:42|temperature:0.5] And I'm Bob with parameters!
3
00:00:10,000 --> 00:00:14,000
[de:Narrator] Zurück zum Erzähler in Deutsch.
```
### SRT Color Coding
When SRT format is detected, the editor applies specialized syntax highlighting:
- **SRT Index**: `1`, `2`, `3` → **Gray** background (disabled/informational)
- **Timestamp**: `00:00:01,000 --> 00:00:04,000` → **Cyan** (timing information)
- **Text Content**: Plain text → Black
- **Character Tags**: `[Character]` → **Blue**
- **Language Tags**: `[de:Character]` → **Purple**
- **Parameters**: `|seed:42|temp:0.5` → **Green**
- **Pauses**: `[pause:1s]` → **Orange**
- **Blank Lines**: Between entries → Gray
### SRT-Specific Sidebar Options
When editing SRT content, the sidebar includes:
#### Subtitle Entry Navigation
- Previous/Next buttons to jump between SRT entries
- Current entry indicator: "Entry 2 of 5"
- Quick jump dropdown to select specific subtitle
#### Timing Tools
- **Insert Timing**: Auto-generates timestamps based on duration
- **Adjust Timing**: Shift all subsequent subtitles by offset
- **Validate Timing**: Check for overlaps and gaps
#### Per-Entry Parameter Application
- Apply parameters to **current entry only**
- Apply parameters to **selected entries**
- Apply parameters to **all entries**
- Clear all tags from current entry
#### SRT Validation
- Check syntax: proper index/timestamp/text format
- Warn on timing overlaps
- Suggest fixes for malformed entries
- Preview final SRT before saving
### SRT Tag Application Examples
#### Applying Character to Current Entry
```
Select entry 2
Click "Add Character" → [Alice]
Result:
2
00:00:04,500 --> 00:00:09,500
[Alice] And I'm Bob with parameters!
```
#### Adding Parameters to Selected Entries
```
Select entries 1-3
Click [Seed] → 42
Result:
1
00:00:01,000 --> 00:00:04,000
[Alice|seed:42] Hello! I'm Alice speaking...
2
00:00:04,500 --> 00:00:09,500
[Alice|seed:42] And I'm Bob with parameters!
3
00:00:10,000 --> 00:00:14,000
[Alice|seed:42] Zurück zum Erzähler in Deutsch.
```
---
## UI Layout
```
┌─ Advanced TTS Editor ─────────────────────────────┐
│ ┌───────────────┬─────────────────────────────────┐│
│ │ SIDEBAR │ TEXTAREA ││
│ │ │ (multiline editor) ││
│ │ ╔═══════════╗ │ 1 ││
│ │ ║ Character ║ │ 00:00:01,000 --> 00:00:04,000 ││
│ │ ║ [Dropdown]║ │ [Alice] Hello there! ││
│ │ ║[+ Add] ║ │ ││
│ │ ╚═══════════╝ │ 2 ││
│ │ │ 00:00:04,500 --> 00:00:09,500 ││
│ │ ╔═══════════╗ │ [Bob|seed:42] Nice to meet ││
│ │ ║Parameters ║ │ ││
│ │ ║[Seed ▼] ║ │ 3 ││
│ │ ║[Temp -/-]║ │ 00:00:10,000 --> 00:00:14,000 ││
│ │ ║[CFG -/-]║ │ [pause:1s] ││
│ │ ║[Speed -/-]║ │ ││
│ │ ╚═══════════╝ │ ││
│ │ │ ││
│ │ ╔═══════════╗ │ ││
│ │ ║ Pause ║ │ ││
│ │ ║[ 1.0 ▼] │ │ ││
│ │ ║[Insert] ║ │ ││
│ │ ╚═══════════╝ │ ││
│ │ │ ││
│ │ ╔═══════════╗ │ ││
│ │ ║ Presets ║ │ ││
│ │ ║P1[Save] ║ │ ││
│ │ ║ [Load] ║ │ ││
│ │ ║ [Del ] ║ │ ││
│ │ ║P2[Save] ║ │ ││
│ │ ║ [Load] ║ │ ││
│ │ ║ [Del ] ║ │ ││
│ │ ║P3[Save] ║ │ ││
│ │ ║ [Load] ║ │ ││
│ │ ║ [Del ] ║ │ ││
│ │ ╚═══════════╝ │ ││
│ │ │ ││
│ │ ╔═══════════╗ │ ││
│ │ ║ History ║ │ ││
│ │ ║[↶][↷] 5/10║ │ ││
│ │ ╚═══════════╝ │ ││
│ │ │ ││
│ │ [Format] │ ││
│ │ [Validate] │ ││
│ └───────────────┴─────────────────────────────────┘│
│ Status: SRT Mode | Entry 2/5 │
└────────────────────────────────────────────────────┘
```
---
## Session Persistence & State Management
**Complete State Persistence**:
- **Text Content**: Full edit history with all undo/redo states
- **Undo/Redo Stack**: Complete history serialized and restored on session reload
- **Preset Data**: All custom presets and their labels
- **Sidebar Settings**: Last used character, language, parameters, pause duration
- **UI State**: Sidebar panel expanded/collapsed state, last active tab
- **Cursor Position**: Last known caret position restored on reload
- **SRT Mode Settings**: Timing configuration, entry navigation state
**Storage Mechanism**:
- Serialize all state to ComfyUI node widget state
- Automatic backup to browser localStorage for redundancy
- JSON-based format for easy inspection and manual recovery
- No data loss on browser refresh, ComfyUI restart, or system reboot
---
## Technical Implementation
### Backend (Python Node)
**File**: `nodes/text/tts_tag_editor_node.py`
- Extend standard ComfyUI node pattern
- Discover available characters/voices from `utils/voice/discovery.py`
- Parse existing tags using `utils/text/character_parser.py` and `utils/text/segment_parameters.py`
- Validate parameters against engine specs
- **Full State Serialization**: Handle undo/redo stack, presets, and all sidebar settings in node state
- Detect and validate SRT format
- **History Persistence**: Serialize complete edit history to widget state for session recovery
### Frontend (TypeScript/Vue)
**Files**:
- `web/tts_tag_editor.js` - Core editor logic
- `web/tts_tag_editor_interface.js` - ComfyUI integration
- `web/tts_tag_editor_ui.js` - Sidebar components
- `web/tts_tag_editor.css` - Styling and color schemes
- `web/tts_tag_editor_srt.js` - SRT-specific formatting and validation
**Features**:
- Extend standard multiline widget with sidebar panel
- Implement event handlers for all buttons/inputs
- Add CSS for visual tag highlighting and styling
- **Undo/Redo Implementation**: Maintain immutable history stack with efficient state snapshots
- **Keyboard Shortcuts**: Ctrl+Z (Undo), Ctrl+Shift+Z (Redo), and other common editor shortcuts
- SRT format detection and specialized rendering
- Syntax highlighting for SRT entries
- **Persistent State Manager**: Auto-save to localStorage and node widget state
### Key Components
1. **Character Discovery**: Hook into existing voice system
2. **Parameter Validation**: Reuse existing parameter filtering logic
3. **Tag Parsing**: Leverage existing character parser and parameter parser (supports mid-sentence tags)
4. **Preset Storage**: Use node widget serialization with full persistence
5. **UI State Management**: Track cursor position, selection, active preset, all sidebar settings
6. **Undo/Redo Manager**: Immutable history stack with efficient snapshots, keyboard support
7. **History Persistence**: Serialize complete undo/redo stack to localStorage and widget state
8. **SRT Detection & Validation**: Detect SRT format, validate structure, highlight syntax
9. **SRT Navigation**: Jump between entries, manage per-entry operations
10. **State Recovery**: Auto-restore all editor state on session reload (text, history, presets, settings)
### Integration Points
- `utils/voice/discovery.py` - Character discovery
- `utils/text/character_parser.py` - Tag parsing
- `utils/text/segment_parameters.py` - Parameter validation
- `utils/timing/parser.py` - SRT format parsing and validation
- Existing language models for language support
---
## Features to Consider Later (v2.0)
- Syntax highlighting with language server protocol (LSP)
- Auto-complete for character names and parameters
- Export/import presets as JSON
- Macro system for complex tag sequences
- Real-time validation with detailed error messages
- Voice preview playback directly from tags
- Template system for common scenes/dialogues
- Regex tag replacement for bulk editing
- Keyboard shortcuts for common operations
- SRT subtitle duration calculator
- Automatic SRT timing generation from text audio duration
- Batch character/parameter application across multiple SRT files
---
## Files to Create
1. `nodes/text/tts_tag_editor_node.py` - Backend node
2. `web/tts_tag_editor.js` - Main editor implementation
3. `web/tts_tag_editor_ui.js` - Sidebar UI components
4. `web/tts_tag_editor_srt.js` - SRT-specific features
5. `web/tts_tag_editor.css` - Styling
---
## Usage Examples
### Basic Text Editing with Mid-Sentence Tags
```
Once upon a time, [Narrator] there was a beautiful princess.
[Alice] She lived in a [grand|seed:42] castle.
[Bob] Who [de:Alice] spoke many languages.
Back to [Bob|temperature:0.7] normal text.
```
### Multi-Parameter Example
```
[Alice|seed:42|temperature:0.3|cfg:0.7] She spoke softly.
[Bob|seed:100|temperature:0.8|cfg:1.0] He shouted loudly!
[de:Merchant|seed:55] Guten Tag, meine Damen!
```
### SRT with Multilingual Characters
```
1
00:00:01,000 --> 00:00:04,000
[Narrator] Welcome to our international story.
2
00:00:04,500 --> 00:00:09,500
[de:Alice|temperature:0.5] Hallo! Ich bin Alice.
3
00:00:09,600 --> 00:00:14,000
[fr:Bob|temperature:0.6] Bonjour! Je suis Bob.
4
00:00:14,100 --> 00:00:18,000
[Narrator] And they lived happily ever after.
```
### Preset Application Example
Saved Preset: "Action Scene" = `[|seed:random|temperature:0.8|cfg:1.2]`
Apply to selection:
```
Before:
[Hero] I will save you!
[Villain] Never!
[Hero] This ends now!
After:
[Hero|seed:42|temperature:0.8|cfg:1.2] I will save you!
[Villain|seed:100|temperature:0.8|cfg:1.2] Never!
[Hero|seed:55|temperature:0.8|cfg:1.2] This ends now!
```
---
## Related Documentation
- [Character Switching Guide](CHARACTER_SWITCHING_GUIDE.md) - Multi-character and language switching
- [Parameter Switching Guide](PARAMETER_SWITCHING_GUIDE.md) - Per-segment parameter control
- [ChatterBox Special Tokens Guide](CHATTERBOX_V2_SPECIAL_TOKENS.md) - Emotion and sound tokens
- README.md - Complete feature overview
+168 -15
View File
@@ -1,41 +1,123 @@
# Version Bump Instructions for ComfyUI ChatterBox Voice
**⚠️ ALWAYS specify which component/engine is affected in changelog (e.g., "Fix Multiline TTS Tag Editor text persistence" not just "Fix text persistence")**
## Quick Reference for Future Version Bumps
### ⚠️ DO NOT CHECK CURRENT VERSION - The script auto-increments!
**Simply run with `patch`, `minor`, or `major` - NO need to look up current version**
### Recommended Command (Separate Commit & Changelog)
**⚠️ IMPORTANT: Use positional arguments, NOT --commit/--changelog flags**
```bash
python3 scripts/bump_version_enhanced.py <version> --commit "<commit_desc>" --changelog "<changelog_desc>"
# EASIEST: Just use 'patch' - script auto-increments the version
python3 scripts/bump_version_enhanced.py patch "<commit_desc>" "<changelog_desc>"
# OR: Specify exact version if needed
python3 scripts/bump_version_enhanced.py <version> "<commit_desc>" "<changelog_desc>"
```
### Examples
#### Multiline Format (Recommended Standard)
```bash
# Patch release (bug fixes)
python3 scripts/bump_version_enhanced.py 3.2.9 \
--commit "Fix character alias resolution and F5-TTS import issues" \
--changelog "Bug fixes and stability improvements"
# Patch release (bug fixes) - CORRECT FORMAT
python3 scripts/bump_version_enhanced.py 3.2.9 "Fix character alias resolution
# Minor release (new features)
python3 scripts/bump_version_enhanced.py 3.3.0 \
--commit "Add character support system for F5-TTS generation" \
--changelog "Add character support for F5-TTS generation"
Technical details:
- Fix parser bypassing character tags in single mode
- Improve character name validation logic
- Add fallback handling for unrecognized tags" "Fix character name handling issues
# Major release (breaking changes)
python3 scripts/bump_version_enhanced.py 4.0.0 \
--commit "Complete architecture refactoring with new modular structure" \
--changelog "Project restructure for better maintainability"
- Fix character tags not being removed from TTS output
- Improve character name recognition accuracy
- Better error handling for invalid character names"
# Minor release (new features) - CORRECT FORMAT
python3 scripts/bump_version_enhanced.py 3.3.0 "Add Higgs Audio 2 TTS engine
Implementation details:
- Integrate boson_multimodal voice cloning system
- Add unified adapter for consistent interface
- Implement voice preset management" "Add Higgs Audio 2 TTS engine with voice cloning
- New realistic voice synthesis engine
- Voice cloning from short audio samples
- Multiple built-in voice presets available"
# Major release (breaking changes) - CORRECT FORMAT
python3 scripts/bump_version_enhanced.py 4.0.0 "Complete unified architecture implementation
Breaking changes:
- Migrate all nodes to unified interface pattern
- Consolidate engine adapters and processors
- Remove deprecated standalone variants" "Major architecture upgrade to unified system
- All TTS engines now use consistent interface
- Better performance and memory management
- Simplified workflows with consolidated nodes
- Breaking: old standalone nodes removed"
```
#### Auto-Increment Examples (Recommended)
```bash
# Auto-increment patch version (4.5.25 → 4.5.26) - CORRECT FORMAT
python3 scripts/bump_version_enhanced.py patch "Fix character parsing issues" "Fix character name handling in TTS generation"
# Auto-increment minor version (4.5.25 → 4.6.0) - CORRECT FORMAT
python3 scripts/bump_version_enhanced.py minor "Add new TTS engine support" "Add Higgs Audio 2 TTS engine with voice cloning"
```
#### Single-Line Format (Only for Super Minor Changes)
```bash
python3 scripts/bump_version_enhanced.py patch "Fix typo in node tooltip" "Fix typo in audio analyzer tooltip"
```
#### Dry-Run Preview (Test Before Committing)
```bash
python3 scripts/bump_version_enhanced.py patch "Fix preview issues" "Fix preview not reflecting filter parameters" --dry-run
```
#### Auto-Categorization System
**IMPORTANT**: The script automatically sorts each line of multiline changelog entries into categories based on keywords:
**Keywords for each category:**
- **Fixed** section: "fix", "bug", "error", "issue", "resolve", "correct", "patch", "crash", "problem", "broken", "compatibility"
- **Added** section: "add", "new", "implement", "feature", "create", "introduce", "support" (also default for unmatched lines)
- **Changed** section: "improve", "enhance", "optimize", "update", "modify", "better", "performance", "refactor"
- **Removed** section: "remove", "delete", "deprecate", "drop", "eliminate"
**Writing Tips:**
- **Be intentional with word choice** - start bullet points with appropriate keywords
- **Line order doesn't matter** - script will reorganize lines by category automatically
- **Each line categorized individually** - mix different change types in one changelog description
- **Commit vs Changelog focus:**
- **Commit**: Technical implementation details for developers
- **Changelog**: User-facing benefits and impacts
**Bash Syntax Notes:**
- Multiline strings need proper quoting (opening quote on first line, closing quote on last line)
- Use `\` (backslash) for line continuation in bash commands
- Don't add manual category prefixes like "Fixed:" - script handles categorization automatically!
### Interactive Mode (Recommended for Complex Changes)
```bash
python3 scripts/bump_version_enhanced.py 3.2.9 --interactive
```
### Legacy Mode (Same Description for Both)
```bash
python3 scripts/bump_version_enhanced.py 3.2.9 "Fix bugs and improve stability"
```
### What the Script Does
- Updates version in `nodes.py`, `README.md`, and `pyproject.toml`
- Updates changelog with user-focused description
- Creates git commit with developer-focused description
@@ -45,36 +127,42 @@ python3 scripts/bump_version_enhanced.py 3.2.9 "Fix bugs and improve stability"
### When to Bump Versions
#### Patch (x.x.X)
- Bug fixes
- Documentation updates
- Performance improvements
- Security patches
#### Minor (x.X.0)
- New features
- New node types
- Enhanced functionality
- Backward-compatible changes
#### Major (X.0.0)
- Breaking changes
- API changes
- Architecture refactoring
- Incompatible updates
### Pre-Bump Checklist
1. ✅ All changes tested and working
2. ✅ User confirms functionality works
3. ✅ Git working directory is clean
4. ✅ All important changes committed
### Post-Bump Actions
- Script handles git commit automatically
- Consider pushing to remote if appropriate
- Update any external documentation
- Notify users of significant changes
### Important Notes
- **Never manually edit version files** - always use the script
- **Only bump when user confirms changes work**
- **Read detailed guide**: `docs/Dev reports/CLAUDE_VERSION_MANAGEMENT_GUIDE.md`
@@ -83,16 +171,81 @@ python3 scripts/bump_version_enhanced.py 3.2.9 "Fix bugs and improve stability"
### Commit vs Changelog Guidelines
#### Commit Description (--commit)
- **Developer diary**: What this specific version bump does
- **Include internal details**: Bug fixes, refactoring, technical changes
- **Development perspective**: "Fixed F5-TTS edit issues after refactoring"
- **Can mention temporary problems**: "Restore functionality broken by restructure"
#### Changelog Description (--changelog)
#### Changelog Description (--changelog)
- **User perspective only**: Write for users upgrading from previous version
- **Use layman terms**: Explain WHAT the feature does, not just technical names
- **Don't document internal fixes**: Skip temporary issues introduced and fixed during development
- **Focus on net result**: What changed for the user, not the development process
- **Example**: If refactoring broke something then fixed it, only mention the refactoring benefit
**🎯 CRITICAL: Always Specify Engine/Component When Applicable**
- **Include engine name OR component name in titles**: Users need to know which TTS engine or node/feature is affected
- **❌ Bad**: "Fix SRT processing issues"
- **✅ Good**: "Fix ChatterBox SRT processing issues"
- **❌ Bad**: "Fix text persistence in workflows"
- **✅ Good**: "Fix Multiline TTS Tag Editor text persistence in workflows"
- **❌ Bad**: "Fix voice cloning errors"
- **✅ Good**: "Fix Higgs Audio voice cloning errors"
- **❌ Bad**: "Improve audio quality"
- **✅ Good**: "Improve F5-TTS audio quality"
**Engine-Specific Title Examples:**
- "Fix Higgs Audio compatibility with transformers 4.46.3+"
- "Add VibeVoice multi-speaker support"
- "Fix ChatterBox character switching in SRT mode"
- "Improve F5-TTS speech editing accuracy"
- "Fix RVC voice conversion model loading"
- "Add Audio Wave Analyzer timing extraction"
**User-Friendly Language Rules:**
- ❌ "Add support for MelBandRoFormer models"
- ✅ "Add support for advanced audio separation models (vocal/instrumental isolation)"
- ❌ "Fix VibeVoice parameter propagation to cached engine instances"
- ✅ "Fix VibeVoice voice settings not applying correctly in some cases"
- ❌ "Resolve ChatterboxVC loading with .safetensors format"
- ✅ "Fix voice conversion failing to load newer model formats"
**Always explain obscure terms:**
- Model names → What they do (e.g., "audio separation", "voice conversion", "text-to-speech")
- Technical errors → User impact (e.g., "crashes when...", "fails to generate audio")
- Internal components → User-facing features (e.g., "character voice switching", "long audio generation")
#### Key Principle
**Commit = Development diary, Changelog = User release notes**
**Commit = Development diary, Changelog = User release notes**
## Troubleshooting
### Common Issues
**"Git working directory is not clean"**
```bash
# Check what files are modified
git status
# Add/commit changes first, then run version bump
git add .
git commit -m "Prepare for version bump"
```
**"Error: Invalid version format"**
- Use semantic versioning: `4.5.25` (not `v4.5.25` or `4.5`)
- Or use auto-increment: `patch`, `minor`, `major`
**Bash syntax errors with multiline**
- Make sure opening quote is on same line as `--commit` or `--changelog`
- Make sure closing quote is on its own line
- Use `\` for line continuation
**Want to see what will happen before committing?**
```bash
# Add --dry-run to preview changelog categorization
python3 scripts/bump_version_enhanced.py patch "description" "changelog" --dry-run
```
+3 -1
View File
@@ -10,6 +10,7 @@ ChatterBox Voice now supports seamless character and narrator switching with **l
- **🎭 Universal Character Parsing** - Works with both F5TTS and ChatterBox engines
- **🌍 Language-Aware Switching** - Switch both character AND language with `[lang:character]` syntax
- **⚙️ Per-Segment Parameters** - Control seed, temperature, cfg, and other parameters per segment with `[Alice|seed:42|temperature:0.5]` syntax (see [Parameter Switching Guide](PARAMETER_SWITCHING_GUIDE.md))
- **🔄 Robust Fallback System** - No errors when characters or languages not found
- **📁 Voice Folder Integration** - Organized character voice management
- **📺 SRT Support** - Character and language switching in subtitle timing
@@ -67,7 +68,7 @@ Back to the narrator in default language.
**Note:** If a language model is not available, the system will fallback to the default model with a warning.
### 3. Voice File Structure
Organize character voices using filenames in `voices_examples/`:
Organize character voices using filenames in `models/voices` (preferred) or in the custom_node TTS Audio Suite folder under `voices_examples/`:
```
voices_examples/
@@ -406,3 +407,4 @@ These guidelines help ensure optimal F5-TTS generation quality and prevent commo
---
*For technical support or feature requests, please check the main README or create an issue on GitHub.*
+140
View File
@@ -0,0 +1,140 @@
# ChatterBox Official 23-Lang v2 Special Tokens
## 🚧 EXPERIMENTAL FEATURE - LIMITED FUNCTIONALITY
**IMPORTANT: These special tokens are present in the v2 vocabulary but may not work as expected!**
Based on community testing and the lack of official documentation from ResembleAI, these emotion/sound tokens appear to be an **incomplete implementation**:
- ✅ **Tokens exist** in the v2 tokenizer vocabulary
- ⚠️ **Limited effect** - tokens may produce minimal or no audible changes
- ❌ **No official documentation** - ResembleAI has not published usage guidelines
- 🔬 **Experimental** - model may not be fully trained to respond to these tokens
**What works (partially):**
- Some users report `<laughter> hahaha` produces slight effects
- Results are inconsistent and unreliable
**Community Discussion:**
- See [ResembleAI/chatterbox Issue #186](https://github.com/resemble-ai/chatterbox/issues/186) - "Use of Emotional Tags Like [laughter] During Generation"
- No official response from ResembleAI team on proper usage
**Our implementation is ready for when/if ResembleAI improves this feature. Feel free to experiment, but don't expect production-ready results.**
---
## Overview
ChatterBox v2 vocabulary includes special tokens for emotions, sounds, and vocal effects. While the tokenizer supports these tags, the model's response to them is limited and undocumented.
## ⚠️ IMPORTANT: Use Angle Brackets `<tag>`
To avoid conflicts with the character switching system `[CharacterName]`, **use angle brackets `<>` for v2 special tokens**:
- ✅ **Correct**: `<giggle>`, `<sigh>`, `<whisper>`
- ❌ **Wrong**: `[giggle]`, `[sigh]`, `[whisper]` (conflicts with character names)
The system will automatically convert `<emotion>` → `[emotion]` internally for ChatterBox v2.
## New Special Tokens
### Emotional Expressions
- `<giggle>` - Light laughter
- `<laughter>` - Full laughter
- `<guffaw>` - Loud, boisterous laugh
- `<sigh>` - Sighing sound
- `<cry>` - Crying sound
- `<gasp>` - Gasping sound
- `<groan>` - Groaning sound
### Breathing & Speech Modifiers
- `<inhale>` - Inhaling/breath in
- `<exhale>` - Exhaling/breath out
- `<whisper>` - Whispered speech
- `<mumble>` - Mumbled speech
- `<UH>` - Hesitation sound (uh)
- `<UM>` - Hesitation sound (um)
### Vocal Performances
- `<singing>` - Singing voice
- `<music>` - Musical sounds
- `<humming>` - Humming sound
- `<whistle>` - Whistling sound
### Body Sounds
- `<cough>` - Coughing sound
- `<sneeze>` - Sneezing sound
- `<sniff>` - Sniffing sound
- `<snore>` - Snoring sound
- `<clear_throat>` - Throat clearing
- `<chew>` - Chewing sound
- `<sip>` - Sipping/drinking sound
- `<kiss>` - Kissing sound
### Animal Sounds
- `<bark>` - Dog barking
- `<howl>` - Howling sound
- `<meow>` - Cat meowing
### Other
- `<shhh>` - Shushing sound
- `<gibberish>` - Nonsensical speech
## Usage Examples
```
Hello there! <giggle> I'm so happy to see you.
Wait... <UM> I think I forgot something. <sigh>
<whisper> This is a secret message.
Look at that! <gasp> It's amazing!
<singing> La la la la la!
```
## Important Notes
### ✅ No Conflicts - Safe to Use!
The angle bracket syntax `<emotion>` is specifically designed to avoid conflicts:
- **Character switching** uses `[CharacterName]` - no conflict
- **Pause tags** use `[pause:2]`, `[wait:1.5]` - no conflict
- **v2 special tokens** use `<giggle>`, `<sigh>` - no conflict
### Combining with Other Systems
You can freely mix all three systems:
```
[Alice] Hello! <giggle> Nice to meet you. [pause:0.5] How are you? <whisper> I have a secret.
[Bob] <gasp> Really? Tell me more! [wait:1]
```
**Processing order:**
1. Character tags `[CharacterName]` are extracted first
2. Pause tags `[pause:XX]` are processed second
3. v2 special tags `<emotion>` are converted last (to `[emotion]` for the engine)
This ensures everything works together seamlessly!
## Model Version Selection
To use these special tokens:
1. Set **model_version** to **v2** in the ChatterBox Official 23-Lang Engine node
2. The v2 model will automatically download the enhanced tokenizer files
3. Special tokens will be processed during generation
## Technical Details
### New v2 Model Files
- `t3_mtl23ls_v2.safetensors` - Enhanced T3 model with improved tokenization
- `grapheme_mtl_merged_expanded_v1.json` - Enhanced grapheme/phoneme mappings with 118 special tokens
- Improved Russian stress handling
- Enhanced multilingual tokenizer fixes
### Cache Invalidation
The TTS Audio Suite automatically manages cache keys to prevent conflicts:
- v1 and v2 generations are cached separately
- Switching between versions will regenerate audio with the correct model
- Cache keys include `model_version` to ensure proper invalidation
+302
View File
@@ -0,0 +1,302 @@
# Dependency Management Guide
**TTS Audio Suite - Complete dependency compatibility documentation**
This document tracks all package dependencies, conflicts, solutions, and constraints implemented in the install script for universal compatibility across Python versions and platforms.
## Quick Reference
| Package | Version Constraint | Reason | Solution |
|---------|-------------------|---------|----------|
| `torch` | `>=2.6.0` for security | CVE-2025-32434 (Higgs Audio) | Smart CUDA detection + upgrade |
| `numpy` | `>=2.2.0,<2.3.0` | Numba compatibility | Version constraints |
| `numba` | `>=0.61.2` | NumPy 2.2+ support | Auto-upgrade from 0.61.0 |
| `librosa` | --no-deps | Forces NumPy downgrade | Isolated installation |
| `descript-audiotools` | --no-deps | Protobuf 3.19 conflict | Isolated installation |
| `faiss-cpu` | `>=1.7.4` | Cross-platform compatibility | Linux: try GPU, Windows: CPU |
---
## Critical Dependencies & Conflicts
### **Python 3.13 Audio-Separator + Resampy Issue**
**Problem:** Audio-Separator v0.36.1 claims Python 3.13 support, but dependency `resampy 0.4.3` fails
- Audio-Separator loads successfully ✅
- resampy compilation fails with numba in Python 3.13 ❌
- `NUMBA_DISABLE_JIT=1` doesn't prevent resampy from attempting compilation
**Impact:** Certain UVR models fail with Audio-Separator, force RVC fallback
```
Failed in nopython mode pipeline (step: nopython frontend)
Untyped global name '_resample_loop_s': Cannot determine Numba type of <class 'function'>
```
**Root Cause:** resampy 0.4.3 has incomplete Python 3.13 + numba support despite Audio-Separator's claims
**Current Solution:**
- Audio-Separator fails → RVC fallback works for compatible models (UVR v2/v3 architecture)
- Some models (e.g., UVR-DeNoise.pth) incompatible with both engines
- Clean error messages implemented to avoid confusion
**Model Compatibility:**
- ✅ **Working Models:** UVR-DeEcho-DeReverb.pth, 6_HP-Karaoke-UVR.pth (via RVC fallback)
- ❌ **Broken Models:** UVR-DeNoise.pth, models requiring Audio-Separator-specific architectures
**Future Fix:** Wait for resampy or Audio-Separator to fix Python 3.13 numba compilation
---
### 1. **NumPy + Numba Compatibility Crisis**
**Problem:** Different numba versions support different NumPy ranges
- `numba 0.61.0`: Supports NumPy ≤ 2.1.x ❌
- `numba 0.61.2+`: Supports NumPy 2.2.x ✅
**Impact:** Core engine loading failures
```
❌ ChatterboxTTS not available: Numba needs NumPy 2.1 or less. Got NumPy 2.2.
❌ F5-TTS not available: Numba needs NumPy 2.1 or less. Got NumPy 2.2.
```
**Solution:** Smart version detection and upgrade
```python
# Detect numba version and upgrade if needed
if numba_version == "0.61.0" or numba_version == "0.61.1":
pip install --upgrade "numba>=0.61.2"
```
**Constraint:** `numpy>=2.2.0,<2.3.0` + `numba>=0.61.2`
---
### 2. **PyTorch Security Vulnerability (CVE-2025-32434)**
**Problem:** Higgs Audio engine requires PyTorch 2.6+ for security
- PyTorch < 2.6: transformers blocks torch.load() due to CVE
- PyTorch 2.6+: Security issue resolved
**Impact:** Higgs Audio fails to load models
```
ValueError: Due to a serious vulnerability issue in torch.load...
```
**Solution:** Smart CUDA detection and upgrade
- Detect system CUDA version (12.8 → cu124)
- Install PyTorch 2.6+ with correct CUDA support
- Force reinstall if switching CPU↔CUDA variants
**Constraint:** `torch>=2.6.0` with appropriate CUDA index
---
### 3. **Librosa Dependency Hell**
**Problem:** librosa forces NumPy downgrades
- librosa dependencies conflict with our NumPy constraints
- Causes cascade of package downgrades
**Impact:** Breaks carefully balanced dependency tree
**Solution:** `--no-deps` installation
```python
pip install librosa --no-deps
# Then manually install librosa's actual dependencies
```
**Dependencies manually installed:**
- `lazy_loader>=0.1`, `msgpack>=1.0`, `pooch>=1.1`
- `soxr>=0.3.2`, `decorator>=4.3.0`, `joblib>=1.0`
- `scikit-learn>=1.1.0`, `audioread>=2.1.9`
---
### 4. **descript-audiotools Protobuf Conflict**
**Problem:** Forces protobuf downgrade 6.x → 3.19.x
- Breaks other packages expecting modern protobuf
- Cascading compatibility issues
**Solution:** `--no-deps` installation with manual dependencies
```python
pip install descript-audiotools --no-deps
```
**Dependencies manually installed:**
- `flatten-dict`, `ffmpy`, `importlib-resources`
- `randomname`, `markdown2`, `pyloudnorm`
- `pystoi`, `torch-stoi`, `ipython`, `tensorboard`
---
### 5. **FAISS Platform Compatibility**
**Problem:** faiss-gpu availability varies by platform
- Linux: `faiss-gpu-cu12`, `faiss-gpu-cu11` available ✅
- Windows: No GPU version available via pip ❌
**Solution:** Platform-specific installation
```python
if not is_windows and cuda_detected:
try:
pip install faiss-gpu-cu12 # Linux + CUDA
except:
pip install faiss-cpu # Fallback
else:
pip install faiss-cpu # Windows/CPU-only
```
---
## Python Version Compatibility
### Python 3.13 Specific Issues
| Package | Issue | Solution | Status |
|---------|-------|----------|--------|
| `MediaPipe` | Binary incompatible | OpenSeeFace fallback | ✅ Working |
| `numba` | JIT compilation issues | Disable JIT in Python 3.13 | ✅ Working |
| `audioop` packages | Not available | Remove from dependencies | ✅ Fixed |
| `resampy` | Numba compilation fails in Audio-Separator | RVC fallback works for compatible models | ⚠️ Partial |
### Python 3.12 Compatibility
| Package | Issue | Solution | Status |
|---------|-------|----------|--------|
| All engines | NumPy/numba mismatch | Smart version detection | ✅ Working |
| PyTorch CUDA | CPU vs CUDA variants | Force reinstall detection | ✅ Working |
---
## Install Script Logic Flow
### 1. **Environment Detection**
```python
- Python version (3.12, 3.13+)
- Platform (Windows, Linux, macOS)
- CUDA availability (nvidia-smi detection)
- CUDA version (12.8 → cu124, 11.8 → cu118)
```
### 2. **PyTorch Installation**
```python
- Check existing version and CUDA support
- Upgrade if needed (security + compatibility)
- Platform-specific CUDA index selection
```
### 3. **Core Dependencies**
```python
- Smart package checking (skip if satisfied)
- Safe packages installed normally
- Sub-dependencies for --no-deps packages
```
### 4. **NumPy + Numba Resolution**
```python
- Detect numba version compatibility
- Upgrade numba if needed (0.61.0 → 0.61.2+)
- Install constrained NumPy (>=2.2.0,<2.3.0)
```
### 5. **Problematic Package Isolation**
```python
- Install with --no-deps to prevent conflicts
- Manually install required sub-dependencies
- Maintain compatibility with main dependency tree
```
---
## Package Categories
### ✅ **SAFE Packages** (Normal installation)
- `transformers>=4.46.3`, `accelerate`, `datasets`
- `soundfile>=0.12.0`, `sounddevice>=0.4.0`
- `jieba`, `pypinyin`, `unidecode`
- `conformer>=0.3.2`, `x-transformers`, `vocos`
- `s3tokenizer>=0.1.7`, `resemble-perth`
### ⚠️ **PROBLEMATIC Packages** (--no-deps required)
- `librosa` - Forces NumPy downgrade
- `descript-audiotools` - Protobuf conflicts
- `descript-audio-codec` - Dependency conflicts
- `cached-path` - Forces package downgrades
- `torchcrepe` - Conflicts via librosa
- `onnxruntime` - Forces NumPy 2.3.x (Python 3.13 only)
### 🎯 **VERSION-CRITICAL Packages**
- `torch>=2.6.0` - Security requirement
- `numpy>=2.2.0,<2.3.0` - Numba compatibility
- `numba>=0.61.2` - NumPy 2.2+ support
- `transformers>=4.46.3` - Higgs Audio compatibility
---
## Testing Matrix
### Verified Working Combinations
| Python | Platform | PyTorch | NumPy | Numba | Status |
|--------|----------|---------|-------|--------|--------|
| 3.12.6 | Windows | 2.7.1+cu126 | 2.2.6 | 0.61.2 | ✅ All engines |
| 3.13.x | Windows | 2.8.0+cpu | 2.2.6 | 0.61.2 | ✅ All engines |
| 3.12.6 | Linux | 2.6.0+cu121 | 2.2.6 | 0.61.2 | ✅ All engines |
### Known Broken Combinations
| Python | Platform | PyTorch | NumPy | Numba | Issue |
|--------|----------|---------|-------|--------|-------|
| 3.12.6 | Windows | Any | 2.2.6 | 0.61.0 | ❌ Engine loading |
| Any | Any | <2.6.0 | Any | Any | ❌ Higgs Audio CVE |
---
## Troubleshooting Guide
### Engine Loading Failures
```
❌ ChatterboxTTS not available: Numba needs NumPy 2.1 or less
```
**Cause:** numba 0.61.0 with NumPy 2.2+
**Fix:** Run install script to upgrade numba to 0.61.2+
### Higgs Audio Model Loading
```
❌ CVE-2025-32434 vulnerability error
```
**Cause:** PyTorch < 2.6.0
**Fix:** Run install script to upgrade PyTorch 2.6+
### Windows CUDA Issues
```
AssertionError: Torch not compiled with CUDA enabled
```
**Cause:** CPU PyTorch on CUDA system
**Fix:** Install script detects and reinstalls CUDA PyTorch
---
## Maintenance Notes
### When Adding New Dependencies
1. **Check conflicts** with existing constraints
2. **Test with problematic packages** (librosa, descript-*)
3. **Update this documentation**
4. **Add to appropriate category** (Safe/Problematic/Critical)
### When Supporting New Python Versions
1. **Test numba compatibility** with NumPy constraints
2. **Check for binary package availability**
3. **Update compatibility matrix**
4. **Add version-specific workarounds if needed**
### When Updating Major Dependencies
1. **Review cascade effects** on sub-dependencies
2. **Test with --no-deps packages**
3. **Update version constraints**
4. **Verify cross-platform compatibility**
---
*Last updated: Version 4.8.19 - Python 3.13 Audio-Separator + resampy compatibility issues documented*
+702
View File
@@ -0,0 +1,702 @@
# TTS Audio Suite Generation/Audio Caching System - Comprehensive Analysis
## Executive Summary
The TTS Audio Suite has **MULTIPLE FRAGMENTED CACHING SYSTEMS** operating independently with inconsistent cache key construction. The generation cache is hard to fix and often misses because:
1. **Multiple independent cache implementations** (at least 3 separate systems)
2. **Inconsistent cache key generation** across engines
3. **Missing parameter normalization** causing floating-point precision issues
4. **Undocumented parameter handling** with hidden transformations
5. **Multiple cache levels** (local, unified, instance-level) not coordinated
6. **Text transformation side-effects** affecting cache keys
---
## 1. CURRENT CACHING SYSTEMS
### System 1: Unified Audio Cache (Primary but underused)
**Location:** `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/cache.py`
- **Global Storage:** `GLOBAL_AUDIO_CACHE = {}` (line 13)
- **Class:** `AudioCache` (line 235)
- **Access:** `get_audio_cache()` function (line 343)
- **Key Generator:** Abstract `CacheKeyGenerator` with engine-specific implementations
- `F5TTSCacheKeyGenerator` (line 25)
- `ChatterBoxCacheKeyGenerator` (line 50)
- `ChatterBoxOfficial23LangCacheKeyGenerator` (line 76)
- `HiggsAudioCacheKeyGenerator` (line 103)
- `VibeVoiceCacheKeyGenerator` (line 144)
- `IndexTTSCacheKeyGenerator` (line 184)
**Issue:** This unified system exists but is ONLY used in ChatterBox TTS node through `create_cache_function()`. Most nodes use local caches instead.
### System 2: Node-Local Audio Cache (ChatterBox)
**Location:** `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/chatterbox/chatterbox_srt_node.py`
- **Global Storage:** `GLOBAL_AUDIO_CACHE = {}` (line 46)
- **Separate from unified cache** - different dict, different module
- **Methods:**
- `_generate_segment_cache_key()` (line 361)
- `_get_cached_segment_audio()` (line 381)
- `_cache_segment_audio()` (line 385)
**Issue:** This is a DUPLICATE system that doesn't use the unified cache. Creates cache misses when switching between unified node and legacy node.
### System 3: Voice Conversion Iteration Cache
**Location:** `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/unified/voice_changer_node.py`
- **Global Storage:** `GLOBAL_RVC_ITERATION_CACHE = {}` (line 48)
- **Purpose:** Cache iterative refinement passes for RVC voice conversion
- **Methods:**
- `_generate_rvc_cache_key()` (line 581)
- `_get_cached_rvc_iterations()` (line 620)
- `_cache_rvc_result()` (line 628)
**Issue:** RVC-specific, limited to 5 iterations max (line 634), not integrated with audio cache.
### System 4: Engine Instance Cache (Multiple locations)
**Location:** `tts_text_node.py` (line 134), `voice_changer_node.py` (line 90)
- **Storage:** Instance variable `self._cached_engine_instances = {}`
- **Purpose:** Cache engine node instances to prevent model reloading
- **Cache Key:** Hash of `engine_type` + stable parameters (line 182)
- **Timestamp-based invalidation:** Uses global `_global_cache_invalidation_flag`
**Issue:** This is ENGINE caching, not GENERATION caching. Confuses the user about what's being cached.
### System 5: Voice Discovery Cache (Persistent)
**Location:** `/home/linux/ComfyUI_TTS_Audio_Suite/utils/voice/cache_manager.py`
- **Storage:** Disk-based JSON in `.cache/voice_discovery.json`
- **Purpose:** Cache voice character discovery to speed up initialization
- **Background refresh:** Uses threading to update cache after ComfyUI loads
**Issue:** This is voice DISCOVERY cache, not generation audio cache.
---
## 2. CACHE KEY CONSTRUCTION - DETAILED ANALYSIS
### ChatterBox Unified Cache Key
**File:** `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/cache.py:50-73`
```python
cache_data = {
'text': params.get('text', ''), # FULL TEXT WITH CHARACTER PREFIX
'exaggeration': params.get('exaggeration', 1.0),
'temperature': params.get('temperature', 0.8),
'cfg_weight': params.get('cfg_weight', 1.0),
'seed': params.get('seed', 0),
'audio_component': params.get('audio_component', ''), # HASHED AUDIO CONTENT
'model_source': params.get('model_source', ''), # LANGUAGE + SOURCE
'device': params.get('device', ''), # NOT RESOLVED (can vary)
'language': params.get('language', 'English'),
'character': params.get('character', 'narrator'),
'engine': 'chatterbox',
'repetition_penalty': params.get('repetition_penalty', 1.0),
'min_p': params.get('min_p', 0.0),
'top_p': params.get('top_p', 1.0)
}
cache_string = str(sorted(cache_data.items()))
cache_key = hashlib.md5(cache_string.encode()).hexdigest()
```
**Problems:**
1. **No floating-point rounding** - 0.8 vs 0.80000001 = different keys
2. **`audio_component` is hashed waveform** - but text still includes character prefix (duplication)
3. **Device resolution inconsistency** - stores "auto" literally, causing misses when it resolves to "cuda"
4. **Character prefixed to text** - line 282 of cache.py: `cache_params['text'] = f"{cache_params.get('character', 'narrator')}:{text_content}"`
### ChatterBox Legacy Cache Key
**File:** `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/chatterbox/chatterbox_srt_node.py:361-379`
```python
cache_data = {
'text': subtitle_text,
'exaggeration': exaggeration,
'temperature': temperature,
'cfg_weight': cfg_weight,
'seed': seed,
'audio_prompt_component': audio_prompt_component,
'model_source': model_source,
'device': device,
'language': language,
'engine': 'chatterbox_srt'
}
cache_string = str(sorted(cache_data.items()))
cache_key = hashlib.md5(cache_string.encode()).hexdigest()
```
**Problems:**
1. Uses `chatterbox_srt` as engine (different from `chatterbox`)
2. No character prefix in text (different from unified)
3. Stores different parameters (no repetition_penalty, min_p, top_p)
4. **Result: Cache misses when using SRT node vs TTS node**
### F5-TTS Unified Cache Key
**File:** `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/cache.py:25-47`
```python
cache_data = {
'text': params.get('text', ''),
'model_name': params.get('model_name', ''), # INCLUDES MODEL NAME
'device': params.get('device', ''),
'audio_component': params.get('audio_component', ''),
'ref_text': params.get('ref_text', ''), # REFERENCE TEXT (!)
'temperature': params.get('temperature', 0.8),
'speed': params.get('speed', 1.0),
'target_rms': params.get('target_rms', 0.1),
'cross_fade_duration': params.get('cross_fade_duration', 0.15),
'nfe_step': params.get('nfe_step', 32),
'cfg_strength': params.get('cfg_strength', 2.0),
'seed': params.get('seed', 0),
'character': params.get('character', 'narrator'),
'engine': 'f5tts'
}
```
**Critical Issues:**
1. **Includes `ref_text` in cache key** - Same text with different reference audio = different key (CORRECT)
2. **No floating-point rounding** on ANY of the 9 float parameters
3. **Device not resolved** - "auto" won't match "cuda"
4. **`cross_fade_duration`** is POST-PROCESSING, shouldn't be in key (doesn't affect generation)
5. **`audio_component`** should be enough to distinguish references, shouldn't also need ref_text
### VibeVoice Cache Key
**File:** `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/cache.py:144-181`
```python
# FLOATING POINT ROUNDING PRESENT (but inconsistent)
cfg_scale = round(float(cfg_scale), 3)
temperature = round(float(temperature), 3)
top_p = round(float(top_p), 3)
cache_data = {
'text': params.get('text', ''),
'cfg_scale': cfg_scale,
'temperature': temperature,
'top_p': top_p,
'use_sampling': params.get('use_sampling', False),
'seed': params.get('seed', 42),
'model_source': params.get('model_source', 'vibevoice-1.5B'),
'device': params.get('device', 'auto'),
'max_new_tokens': params.get('max_new_tokens'),
'multi_speaker_mode': params.get('multi_speaker_mode', 'Custom Character Switching'),
'audio_component': params.get('audio_component', ''),
'character': params.get('character', 'narrator'),
'inference_steps': params.get('inference_steps', 20),
'attention_mode': params.get('attention_mode', 'eager'),
'quantize': params.get('quantize', False),
'engine': 'vibevoice'
}
```
**Good:** Has rounding for floats
**Bad:** Only rounds 3 params, misses others. Device still "auto". Includes post-processing params like attention_mode (affects inference, not generation output).
### IndexTTS Cache Key
**File:** `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/cache.py:184-232`
```python
# SELECTIVE ROUNDING (inconsistent)
temperature = round(float(temperature), 3)
top_p = round(float(top_p), 3)
emotion_alpha = round(float(emotion_alpha), 3)
cache_data = {
'text': params.get('text', ''),
'speaker_audio': params.get('speaker_audio', ''), # AUDIO PATH STRING
'emotion_audio': params.get('emotion_audio', ''), # AUDIO PATH STRING
'emotion_alpha': emotion_alpha,
'emotion_vector': params.get('emotion_vector'), # LIST (unhashable!)
'use_emotion_text': params.get('use_emotion_text', False),
'emotion_text': params.get('emotion_text', ''),
'use_random': params.get('use_random', False),
'seed': params.get('seed', 0),
'do_sample': params.get('do_sample', True),
'temperature': temperature,
'top_p': top_p,
'top_k': params.get('top_k', 30),
'length_penalty': params.get('length_penalty', 0.0), # NOT ROUNDED!
'num_beams': params.get('num_beams', 3),
'repetition_penalty': params.get('repetition_penalty', 10.0), # NOT ROUNDED!
'max_mel_tokens': params.get('max_mel_tokens', 1500),
'max_text_tokens_per_segment': params.get('max_text_tokens_per_segment', 120),
'interval_silence': params.get('interval_silence', 200),
'model_name': params.get('model_name', 'IndexTTS-2'),
'device': params.get('device', 'auto'),
'character': params.get('character', 'narrator'),
'use_torch_compile': params.get('use_torch_compile', False),
'use_accel': params.get('use_accel', False),
'stream_return': params.get('stream_return', False),
'more_segment_before': params.get('more_segment_before', 0),
'engine': 'index_tts'
}
```
**Critical Issues:**
1. **Stores audio paths as strings** - temp paths will vary, causing cache misses
2. **`emotion_vector` is a list** - unhashable, str() conversion is fragile
3. **Selective float rounding** - only 3 params rounded, others like `length_penalty`, `repetition_penalty` are not
4. **Mixed use of AUDIO and PATH** - speaker_audio and emotion_audio are path strings, not hashes
---
## 3. CACHE MISS ROOT CAUSES
### Root Cause #1: Duplicate Cache Systems
Two separate `GLOBAL_AUDIO_CACHE` dicts exist:
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/cache.py:13`
- `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/chatterbox/chatterbox_srt_node.py:46`
- `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/f5tts/f5tts_srt_node.py:43`
**Result:** Using unified TTS Text node and then SRT node = cache miss (different dicts)
### Root Cause #2: Floating-Point Precision
Example from ChatterBox:
```python
# Temperature=0.8 in unified cache
temperature = params.get('temperature', 0.8) # Key generated with 0.8
# Temperature from ComfyUI slider
temperature = 0.80000001 # Different key!
```
**Affected engines:** ChatterBox, F5-TTS (all 9 float params), all others without rounding
### Root Cause #3: Device Resolution Inconsistency
```python
# Unified cache stores device literally
'device': params.get('device', ''), # Stores "auto"
# But actual resolution varies
tts_text_node.py line 159:
from utils.device import resolve_torch_device
resolved_device = resolve_torch_device(config.get('device', 'auto')) # Returns "cuda"
```
**Result:** Engine cache key includes resolved device, generation cache key includes "auto". Mismatch!
### Root Cause #4: Text Transformation Side-Effects
**In `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/chatterbox/chatterbox_tts_node.py`:**
Line 282 of unified cache adds character prefix:
```python
cache_params['text'] = f"{cache_params.get('character', 'narrator')}:{text_content}"
```
But line 449 applies crash protection AFTER cache key generation:
```python
protected_text = self._pad_short_text_for_chatterbox(processed_text, crash_protection_template)
# Cache key was generated with unpadded text
# But generation uses padded text
```
**Result:**
- Cache key generated: `narrator:hello`
- Cache lookup tries: `narrator:hello`
- Cache miss because audio was generated with: `narrator:hmm ,, hello hmm ,,`
### Root Cause #5: Audio Component Hash Inconsistency
```python
# chatterbox_tts_node.py line 603-605
stable_audio_component = self._generate_stable_audio_component(
inputs.get("reference_audio"), inputs.get("audio_prompt_path", "")
)
# Generates hash from WAVEFORM CONTENT
# But if same file accessed via:
# 1. Dropdown (direct path)
# 2. Character Voices node (copied to temp)
# = Different file path = Different hash = CACHE MISS
```
### Root Cause #6: Parameter Duplication in Cache Key
**In F5-TTS cache key:**
```python
'audio_component': params.get('audio_component', ''), # Hash of waveform
'ref_text': params.get('ref_text', ''), # Reference text
```
If same text with different reference audio:
- audio_component differs ✓ (good - prevents collision)
- But if reference audio is different FILE with same CONTENT:
- audio_component might be same ✓
- ref_text is same ✓
- But actual voice characteristics differ ✗
### Root Cause #7: Untracked Parameter Transformations
```python
# chatterbox_tts_node.py line 434-436
processed_text, pause_segments = PauseTagProcessor.preprocess_text_with_pause_tags(
inputs["text"], True
)
# Cache key uses processed_text, but what if PauseTagProcessor changes the text?
# [pause:500ms] removed? Transformed? Cache key would differ!
```
### Root Cause #8: Engine Instance Cache vs Generation Cache Confusion
```python
# tts_text_node.py line 182
cache_key = f"{engine_type}_{hashlib.md5(str(sorted(stable_params.items())).encode()).hexdigest()[:8]}"
# This is ENGINE instance cache
# But users think they're caching generation results
# They're actually just reusing the same engine model
# Generation parameters affect the OUTPUT, not the CACHE KEY
```
---
## 4. PARAMETER ANALYSIS: What Should/Shouldn't Invalidate Cache
### ChatterBox Parameters
| Parameter | Should Invalidate? | Current Behavior | Issue |
|-----------|-------------------|------------------|-------|
| `text` | YES | YES | Text transformation side-effects |
| `character` | YES | YES | Included in cache key |
| `exaggeration` | YES | YES | No rounding |
| `temperature` | YES | YES | No rounding - floating point issues |
| `cfg_weight` | YES | YES | No rounding |
| `seed` | YES | YES | ✓ Correct |
| `audio_component` | YES | YES | ✓ Hashed correctly |
| `language` | YES | YES | ✓ Correct |
| `device` | NO* | YES | *Should NOT - output doesn't differ |
| `reference_audio` | YES (indirect) | YES | Via audio_component ✓ |
| `crash_protection_template` | YES | NO* | *MISSING - changes text! |
| `enable_chunking` | MAYBE | NO | Pre-processing, doesn't affect segment cache |
| `chunk_combination_method` | NO | NO | Post-processing ✓ |
| `silence_between_chunks_ms` | NO | NO | Post-processing ✓ |
| `batch_size` | NO | NO | Streaming mode, doesn't affect generation ✓ |
### F5-TTS Parameters
| Parameter | Should Invalidate? | Current Behavior | Issue |
|-----------|-------------------|------------------|-------|
| `text` | YES | YES | ✓ |
| `ref_text` | YES | YES | ✓ Correct - different reference = different voice |
| `model_name` | YES | YES | ✓ Different model |
| `seed` | YES | YES | ✓ |
| `temperature` | YES | YES | No rounding |
| `speed` | NO* | YES | *WRONG - doesn't affect generation, F5 handles this |
| `target_rms` | NO* | YES | *WRONG - post-processing normalization |
| `cfg_strength` | YES | YES | Affects generation quality |
| `nfe_step` | YES | YES | Affects generation quality |
| `cross_fade_duration` | NO | YES | *WRONG - post-processing |
| `audio_component` | YES | YES | ✓ Reference audio identifier |
| `character` | YES | YES | ✓ |
| `device` | NO | YES | *WRONG - doesn't affect generation |
---
## 5. DESIGN FLAWS IDENTIFIED
### Flaw #1: Architectural Fragmentation
**Problem:** 5 independent cache systems with no coordination
- Unified cache underused
- Legacy SRT nodes use separate cache
- Voice conversion has separate cache
- Engine instance cache conflates with generation cache
- Voice discovery cache is orthogonal
**Impact:** Cache misses when switching between node types, impossible to manage/clear all caches
### Flaw #2: Floating-Point Precision Not Normalized
**Problem:**
```python
# Different rounding across engines
VibeVoice: round(x, 3)
IndexTTS: round(x, 3) but only 3 params
ChatterBox: No rounding
F5-TTS: No rounding
```
**Impact:** Workflow parameter change from UI slider (0.8) vs code (0.80000001) = cache miss
### Flaw #3: Device Parameter Included in Cache Key
**Problem:**
```python
# Unified cache stores device
'device': params.get('device', ''),
# But device="auto" + resolved device="cuda" = different keys
# Yet output is identical!
```
**Impact:** Unnecessary cache misses
### Flaw #4: Text Pre-Processing Side-Effects Not Tracked
**Problems:**
1. Crash protection padding happens AFTER cache key generation
2. Pause tag preprocessing happens AFTER cache key generation
3. Character prefix added during cache key generation
4. Text chunking happens separately
**Impact:** Cache key is for unprocessed text, but generation uses processed text
### Flaw #5: Audio Component Hashing Fragile
**Problem:**
```python
# audio_component = hash of waveform bytes
# If reference audio loaded different ways:
# 1. From dropdown -> actual file path
# 2. From Character Voices -> temp file
# 3. From direct audio input -> memory waveform
# = Same audio, potentially different hashes
```
**Impact:** Cache misses for same voice when referenced differently
### Flaw #6: Mixed Scopes in Cache Keys
**Problem:**
```python
# Per-segment cache parameters (text, audio, seed, temperature)
# vs
# Workflow-level parameters (device, model, language)
# = Same cache key namespace treats both equally
# Result: Changing device invalidates ALL segment caches
# But device doesn't affect generation!
```
### Flaw #7: No Cache Invalidation Strategy
**Problem:**
- Global caches cleared how? Only manual `audio_cache.clear_cache()`
- No TTL/LRU eviction
- Memory can grow unbounded
- No detection of "stale" cache entries after model updates
**Impact:** Users can't trust cache after model updates; memory leaks
### Flaw #8: Engine Instance Cache != Generation Cache
**Problem:**
```python
# Users expect:
# "Cache generation results for fast re-run"
# What actually happens:
# "Cache engine instance to avoid reloading model"
# Different concepts, but code conflates them!
```
**Impact:** Users confused why re-running with different parameters still reuses cache
---
## 6. SPECIFIC EXAMPLES OF CACHE MISSES
### Example 1: Temperature Slider Changes
```
User workflow:
1. Set temperature=0.8, generate (CACHE HIT expected next time)
2. Adjust slider to temperature=0.80000001
3. Run again, expect CACHE HIT
4. Result: CACHE MISS (different float representation)
```
### Example 2: Using Unified TTS vs Legacy SRT Node
```
User workflow:
1. Use "🎤 TTS Text" (unified) node with ChatterBox engine
-> Audio stored in utils/audio/cache.py:GLOBAL_AUDIO_CACHE
2. Switch to "ChatterBox TTS SRT" legacy node
-> Looks in nodes/chatterbox/chatterbox_srt_node.py:GLOBAL_AUDIO_CACHE
3. Result: CACHE MISS (different dict)
```
### Example 3: Crash Protection Changes Cache
```
User workflow:
1. Generate with default crash_protection_template="hmm ,, {seg} hmm ,,"
Cache key: hash("narrator:hello")
Audio generated from: "narrator:hmm ,, hello hmm ,,"
2. Change crash_protection_template="" (disable padding)
3. Try to regenerate same text "hello"
4. Result: CACHE HIT returns audio for "hmm ,, hello hmm ,,"
But user expects audio for "hello"
= WRONG AUDIO RETURNED
```
### Example 4: Device Parameter Mismatch
```
User workflow:
1. Set device="auto" on first run
Engine instance cache key: "f5tts_abc123"
Generation cache key includes 'device': 'auto'
2. Device resolves to "cuda" internally
3. Switch device="cuda" explicitly
Engine instance cache key: "f5tts_abc123" (same!)
Generation cache key now includes 'device': 'cuda' (different!)
4. Result: Engine reused (good) but generation cache misses (bad)
```
### Example 5: Reference Audio Path Variation
```
User workflow:
1. Use Character Voices node
-> Reference audio hashed from temp file
-> audio_component = "ref_audio_abc123_24000"
-> Cache key includes this hash
2. Switch to dropdown selector (same voice file)
-> Reference audio hashed from permanent file
-> audio_component = "file_audio_def456_24000"
-> DIFFERENT HASH despite same voice content!
3. Result: CACHE MISS
```
---
## 7. CURRENT IMPLEMENTATION PATTERNS
### Pattern 1: ChatterBox TTS Node (Good Model)
File: `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/chatterbox/chatterbox_tts_node.py:454-479`
```python
# Generates stable audio component ONCE
stable_audio_component = self._generate_stable_audio_component(...)
# Creates cache function with ALL parameters
cache_fn = create_cache_function(
"chatterbox",
character=character,
exaggeration=exaggeration,
# ... all params
audio_component=audio_component,
)
# Uses cache function for lookup/store
cached_audio = cache_fn(protected_text)
if cached_audio:
return cached_audio
audio = self.generate_tts_audio(protected_text, ...)
cache_fn(protected_text, audio_result=audio)
```
**Strengths:**
- Uses unified cache system
- Generates audio component once
- Uses cache_fn pattern
**Weaknesses:**
- Applies crash protection AFTER defining cache params
- Cache key includes protected text, but params generated with unprotected text
- No floating-point rounding
### Pattern 2: F5-TTS Node (Different Approach)
File: `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/f5tts/f5tts_node.py`
**Observation:** Doesn't implement caching at generation level! Instead:
- Relies on unified cache through base node
- Or relies on F5-TTS model-level caching internally
### Pattern 3: Voice Conversion (Iteration Caching)
File: `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/unified/voice_changer_node.py:154-175`
```python
cache_key = self._generate_rvc_cache_key(processed_source_audio, rvc_model, config)
cached_iterations = self._get_cached_rvc_iterations(cache_key, refinement_passes)
if refinement_passes in cached_iterations:
return cached_iterations[refinement_passes] # Full cache hit
# Start from highest cached iteration
for i in range(refinement_passes, 0, -1):
if i in cached_iterations:
current_audio = cached_iterations[i][0]
start_iteration = i
break
# Continue from cached iteration
for iteration in range(start_iteration, refinement_passes):
converted_audio = rvc_engine.convert_voice(...)
self._cache_rvc_result(cache_key, iteration_num, ...)
```
**Strengths:**
- Smart iteration-level caching
- Can resume from partial progress
- Limited to 5 iterations (memory bound)
**Weaknesses:**
- Separate system from generation cache
- Not integrated with audio cache
---
## 8. ROOT CAUSE RANKING
| Rank | Root Cause | Impact | Difficulty to Fix |
|------|-----------|--------|-------------------|
| 1 | Multiple cache dicts (fragmentation) | Very High | Medium |
| 2 | No floating-point rounding | High | Low |
| 3 | Device parameter in cache key | High | Low |
| 4 | Text pre-processing side-effects | High | Medium |
| 5 | Crash protection not in cache key | Medium | Low |
| 6 | Audio component hashing fragile | Medium | Medium |
| 7 | Parameter duplication in F5-TTS key | Low | Low |
| 8 | No cache invalidation strategy | Medium | High |
---
## 9. DESIGN ISSUES SUMMARY
### Consolidated Cache Architecture Needed
Current: 5 independent systems
Desired: 1 unified system with compartments
### Parameter Normalization Missing
- No rounding of floats to consistent precision
- Device parameter should be resolved before cache key
- Text transformations should be applied before cache key generation
### Scope Separation Missing
- Per-segment parameters (text, seed, audio)
- Workflow parameters (device, model, language)
- Processing parameters (chunking, silence)
Should use:
- Segment-level cache key = text + audio + generation params
- Workflow-level identifier = device + model + language (separate tracking)
### Cache Key Construction Inconsistent
- F5-TTS includes too many parameters (speed, target_rms, cross_fade_duration)
- ChatterBox missing crash_protection in key
- IndexTTS stores audio paths instead of hashes
- No engine has consistent float rounding
---
## 10. RECOMMENDATIONS
### Short Term (Easy Fixes)
1. **Add float rounding** to all engines (3 decimal places)
2. **Remove device from generation cache key** (doesn't affect output)
3. **Remove cross_fade/target_rms/speed from F5-TTS cache key**
4. **Add crash_protection_template to ChatterBox cache key**
5. **Fix audio_component hashing** to be consistent regardless of load method
### Medium Term (Moderate Effort)
1. **Consolidate cache dicts** into single GLOBAL_AUDIO_CACHE
2. **Separate cache scopes:**
- Segment cache key = hash(text + audio_component + generation_params)
- Workflow tracking = (engine_type, model_name, device, language)
3. **Move text transformation BEFORE cache key generation**
4. **Implement cache size limits** with LRU eviction
### Long Term (Architectural)
1. **Unified cache system** with clear API
2. **Configuration-driven cache parameters** per engine (explicit list of what invalidates cache)
3. **Cache statistics and debugging** UI in ComfyUI
4. **TTL-based invalidation** after model updates
5. **Persistent cache option** for heavy workflows (disk-based)
+280
View File
@@ -0,0 +1,280 @@
# TTS Audio Suite Cache Issues - Executive Summary
## The Problem
Generation cache is **hard to fix and often misses** due to **architectural fragmentation** and **inconsistent parameter handling**.
## Quick Facts
- **5 separate cache systems** operating independently
- **8 major root causes** identified with code locations
- **22 specific cache key construction issues** across engines
- **At least 5 real-world cache miss scenarios** documented
## Root Causes (Ranked by Impact)
### 1. Multiple Cache Dictionaries (CRITICAL)
**Files:**
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/cache.py:13` (Unified cache)
- `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/chatterbox/chatterbox_srt_node.py:46` (Legacy ChatterBox)
- `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/f5tts/f5tts_srt_node.py:43` (Legacy F5-TTS)
**Problem:** Using unified "🎤 TTS Text" node then switching to "ChatterBox TTS SRT" legacy node = cache miss because they use different `GLOBAL_AUDIO_CACHE` dicts.
### 2. No Float Rounding (HIGH PRIORITY FIX)
**Affected files:**
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/cache.py:50-73` (ChatterBox)
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/cache.py:25-47` (F5-TTS)
- All legacy SRT nodes
**Problem:** Temperature=0.8 vs 0.80000001 = different cache keys despite being same parameter
**Impact:** Moving UI slider = cache miss
### 3. Device Parameter Inconsistency (HIGH)
**Problem:** Cache key stores "auto" but engine instance cache resolves to "cuda"
- Engine cache resolved: `resolve_torch_device("auto")` → "cuda"
- Generation cache literal: 'device': "auto"
- Result: Cache misses when switching explicit device selections
### 4. Text Pre-Processing Side-Effects (HIGH)
**File:** `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/chatterbox/chatterbox_tts_node.py:449`
**Problem:**
```
1. Crash protection padding applied AFTER cache key generation
2. Pause tag preprocessing applied separately
3. Cache key is for "hello", audio generated from "hmm ,, hello hmm ,,"
4. If crash_protection_template changed, wrong audio returned
```
### 5. Crash Protection Not In Cache Key (MEDIUM)
**File:** `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/chatterbox/chatterbox_tts_node.py`
**Problem:** Changing `crash_protection_template` doesn't invalidate cache
- Same text → same cache key
- But protection template changes actual text used for generation
- Result: Returns audio for different protected text
### 6. Audio Component Hashing Fragile (MEDIUM)
**File:** `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/audio_hash.py:24`
**Problem:** Same voice file, different loading methods = different hashes
- From dropdown: hashes actual file → "file_audio_abc123"
- From Character Voices (temp): hashes temp file → "ref_audio_def456"
- From direct audio: hashes waveform → "ref_audio_ghi789"
- All are same audio, different hashes = cache misses
### 7. Parameter Duplication in F5-TTS (LOW)
**File:** `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/cache.py:25-47`
**Problem:** F5-TTS cache key includes post-processing parameters
- `speed` doesn't affect generation (F5 handles this)
- `target_rms` is normalization (post-processing)
- `cross_fade_duration` is for combining chunks (post-processing)
- Changing these shouldn't invalidate cache, but does
### 8. No Cache Invalidation Strategy (MEDIUM)
**Problem:**
- No TTL/LRU eviction
- Memory grows unbounded
- No detection of "stale" entries after model updates
- Manual `audio_cache.clear_cache()` only clears unified cache, not legacy caches
## Design Issues Summary
### Issue #1: Architectural Fragmentation
```
Unified Cache (utils/audio/cache.py)
├─ ChatterBoxCacheKeyGenerator
├─ F5TTSCacheKeyGenerator
├─ VibeVoiceCacheKeyGenerator
└─ IndexTTSCacheKeyGenerator
Legacy ChatterBox Cache (nodes/chatterbox/chatterbox_srt_node.py)
└─ Independent _generate_segment_cache_key()
Legacy F5-TTS Cache (nodes/f5tts/f5tts_srt_node.py)
└─ Same as ChatterBox SRT
Voice Conversion Cache (nodes/unified/voice_changer_node.py)
└─ GLOBAL_RVC_ITERATION_CACHE (separate dict)
Engine Instance Cache (tts_text_node.py)
└─ self._cached_engine_instances (confused with generation cache)
Voice Discovery Cache (utils/voice/cache_manager.py)
└─ Persistent disk cache (unrelated to generation)
```
**Result:** Can't centrally manage cache; users confused about what's cached
### Issue #2: Parameter Normalization Missing
No consistent float rounding across engines:
- VibeVoice: `round(x, 3)` for 3 params only
- IndexTTS: `round(x, 3)` for 3 params, others unrounded
- ChatterBox: No rounding
- F5-TTS: No rounding
### Issue #3: Scope Separation Missing
Cache key conflates different parameter types:
- **Per-segment** (text, seed, audio) - should invalidate cache
- **Workflow** (device, model, language) - should NOT invalidate individual segments
- **Processing** (chunking, silence) - should NOT invalidate cache
Currently all in same namespace, causing false invalidations.
## Real-World Examples
### Example 1: Temperature Slider (Most Common)
```
1. Set temperature=0.8, generate audio
2. Adjust slider slightly (0.800001)
3. Expect cache hit, get cache miss
```
### Example 2: Node Switching
```
1. Use "🎤 TTS Text" node → cache stored in utils/audio/cache.py
2. Switch to "ChatterBox TTS SRT" legacy node → looks in nodes/chatterbox/
3. Same text but different cache dict = miss
```
### Example 3: Crash Protection
```
1. Generate "hello" with crash_protection="hmm ,, {seg} hmm ,,"
Audio generated from: "hmm ,, hello hmm ,,"
2. Disable crash protection and regenerate
3. Cache returns old audio with padding, user expects bare audio
```
### Example 4: Device Parameter
```
1. Use device="auto" on GPU machine
2. Set device="cuda" explicitly
3. Engine cache reuses (good) but generation cache misses (bad)
```
## Parameters That SHOULD/SHOULDN'T Invalidate Cache
### ChatterBox Should Invalidate
- text, character, exaggeration, temperature, cfg_weight, seed, audio_component, language
- **MISSING:** crash_protection_template (changes actual text!)
### ChatterBox Should NOT Invalidate
- device (doesn't affect generation output)
- chunk_combination_method, silence_between_chunks_ms (post-processing)
- batch_size (streaming mode setting)
### F5-TTS Should Invalidate
- text, ref_text, model_name, seed, temperature, cfg_strength, nfe_step, audio_component, character
### F5-TTS Should NOT Invalidate
- speed (F5 handles this, doesn't affect segment generation)
- target_rms (post-processing normalization)
- cross_fade_duration (post-processing for combining segments)
- device (doesn't affect generation output)
## Fix Priority
### Must Fix (Breaks functionality)
1. Add float rounding (trivial, high impact)
2. Consolidate cache dicts (medium effort, high impact)
3. Add crash_protection_template to cache key (trivial, high impact)
### Should Fix (Improves reliability)
4. Remove device from generation cache key (trivial)
5. Remove post-processing params from F5-TTS key (trivial)
6. Fix audio_component hashing consistency (medium effort)
### Nice to Have (Long-term improvements)
7. Implement cache size limits with LRU eviction
8. Add cache statistics/debugging UI
9. Implement TTL-based invalidation after model updates
10. Option for persistent (disk-based) cache
## Current Cache Key Examples
### ChatterBox (Unified, Line 50-73)
```python
cache_data = {
'text': "narrator:protected_hello", # Includes prefix + crash protection
'exaggeration': 0.5, # NO ROUNDING
'temperature': 0.8, # NO ROUNDING
'cfg_weight': 0.5, # NO ROUNDING
'seed': 42,
'audio_component': 'file_audio_abc_24k',
'model_source': 'chatterbox_english',
'device': 'auto', # NOT RESOLVED
'language': 'English',
'character': 'narrator',
'repetition_penalty': 1.0, # NO ROUNDING
'min_p': 0.0, # NO ROUNDING
'top_p': 1.0, # NO ROUNDING
'engine': 'chatterbox'
}
```
### F5-TTS (Unified, Line 25-47)
```python
cache_data = {
'text': "narrator:hello",
'model_name': 'F5TTS_Base',
'device': 'auto', # NOT RESOLVED
'audio_component': 'file_audio_def_24k',
'ref_text': 'reference text here', # Correct: different ref = different voice
'temperature': 0.8, # NO ROUNDING
'speed': 1.0, # WRONG: post-processing
'target_rms': 0.1, # WRONG: post-processing
'cross_fade_duration': 0.15, # WRONG: post-processing
'nfe_step': 32,
'cfg_strength': 2.0, # NO ROUNDING
'seed': 0,
'character': 'narrator',
'engine': 'f5tts'
}
```
### ChatterBox SRT Legacy (Line 361-379)
Uses separate cache dict, different engine name ('chatterbox_srt' vs 'chatterbox'),
different parameters → cache misses when switching nodes!
## File References
All specific issues traced to source:
**Unified Cache Core:**
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/cache.py` (339 lines)
- ChatterBoxCacheKeyGenerator (50-73)
- F5TTSCacheKeyGenerator (25-47)
- VibeVoiceCacheKeyGenerator (144-181)
- IndexTTSCacheKeyGenerator (184-232)
- AudioCache class (235-336)
**Audio Hashing:**
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/audio/audio_hash.py` (142 lines)
- generate_stable_audio_component (24-126)
**Node Implementation:**
- `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/chatterbox/chatterbox_tts_node.py`
- _pad_short_text_for_chatterbox (136-177) - text transformation
- _generate_tts_with_pause_tags (408-546) - text preprocessing
- Crash protection applied line 449 AFTER cache key generation
- `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/chatterbox/chatterbox_srt_node.py`
- _generate_segment_cache_key (361-379) - separate cache logic
- `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/f5tts/f5tts_srt_node.py`
- GLOBAL_AUDIO_CACHE (43) - duplicate dict
- `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/unified/tts_text_node.py`
- _create_proper_engine_node_instance (136-441) - engine caching
- GLOBAL_AUDIO_CACHE (44) - local storage
## Conclusion
The generation cache system has **good intentions but fragmented execution**. The unified cache exists but is underutilized; legacy implementations use separate dicts; engine caching is conflated with generation caching.
**Key insight:** The system needs **consolidation and normalization**, not complete redesign. All the pieces are there, they just need to work together consistently.
See CACHE_ANALYSIS.md for detailed technical breakdown with line numbers and code examples.
@@ -1,140 +0,0 @@
# Claude Version Management Guide
**This file contains detailed instructions for Claude Code when performing version bumps.**
## Automated Version Bumping Instructions
### Primary Command
**NEW DEFAULT**: `python3 scripts/bump_version_enhanced.py <version> "<commit_desc>" "<changelog_desc>"`
**Auto-increment support**: Use `patch`, `minor`, or `major` instead of version numbers for automatic increment.
### Usage Examples
#### NEW Default Usage (RECOMMENDED)
```bash
# Auto-increment with separate descriptions
python3 scripts/bump_version_enhanced.py patch "Fix character tag removal bug in single character mode" "Fix unrecognized character tags not being removed from TTS output"
python3 scripts/bump_version_enhanced.py minor "Add RVC TTS engine support" "Add new RVC (Real-time Voice Conversion) TTS engine with voice cloning capabilities"
# Manual version with separate descriptions
python3 scripts/bump_version_enhanced.py 3.4.3 "Fix crash in audio processing" "Fix crashes when processing certain audio file formats"
```
#### Legacy Usage (DEPRECATED)
```bash
python3 scripts/bump_version_enhanced.py patch --legacy "Fix character tag removal bug"
```
#### Multiline Description
```bash
python3 scripts/bump_version_enhanced.py 3.0.2 "Fix missing sounddevice dependency\nAdd comprehensive error handling\nUpdate installation documentation"
```
#### Interactive Mode (for detailed changelog entries)
```bash
python3 scripts/bump_version_enhanced.py 3.0.2 --interactive
```
#### From File
```bash
python3 scripts/bump_version_enhanced.py 3.0.2 --file changelog.txt
```
#### Dry Run First (RECOMMENDED)
```bash
python3 scripts/bump_version_enhanced.py 3.0.2 "Description" --dry-run
```
### When to Bump Versions
- **Patch (x.x.X)**: Bug fixes, dependency updates, small tweaks
- **Minor (x.X.x)**: New features, significant enhancements
- **Major (X.x.x)**: Breaking changes, major architecture changes
### Files Auto-Updated by Script
- README.md (title version)
- nodes.py (VERSION constant)
- pyproject.toml (version field)
- srt/__init__.py (__version__)
- core/__init__.py (__version__)
- CHANGELOG.md (new entry with date)
### Script Features
- **Automatic categorization**: Sorts changelog entries into Added/Fixed/Changed/Removed
- **Multiline commit messages**: Properly formatted for git
- **Rollback protection**: Restores files if anything fails
- **Version validation**: Ensures semantic versioning format
- **Backup system**: Creates backup before making changes
### Changelog Entry Categories (IMPROVED v3.4.0+)
The script automatically categorizes based on enhanced keywords and context:
- **Added**: add, new, implement, feature, create, introduce, support, language switching, syntax, bracket, character, integration
- **Fixed**: fix, bug, error, issue, resolve, correct, patch
- **Changed**: update, enhance, improve, change, modify, optimize, performance, smart, efficient, reduced, eliminated, loading
- **Removed**: remove, delete, deprecate, drop
**Smart Context Detection**:
- Recognizes emoji section headers (🌍, 📋, 🚀, 🔧, etc.)
- Uses section context for ambiguous items
- Defaults to "Added" for feature releases instead of "Fixed"
- Skips main title line to avoid categorization errors
### Example Multiline Changelog Output
```markdown
## [3.0.2] - 2025-07-14
### Fixed
- Fix missing sounddevice dependency
- Add comprehensive error handling
### Changed
- Update installation documentation
- Simplify installation process by including all dependencies by default
```
### Fallback Process
If the automated script fails, refer to: `docs/Dev reports/VERSION_UPDATE_GUIDE.md`
### Important Notes
- **Never manually update version files** - the script handles all 6 files automatically
- **Script auto-commits** with proper multiline message format
- **Always dry run first** to preview changes
- **Use interactive mode** for complex changelog entries
- **The script validates** version format and provides backup/rollback
- **Commit messages**: Keep them clean and descriptive. NEVER add Claude co-author credits or "Generated with Claude Code" footers
### CRITICAL: Fix Poor Changelog Generation
**Problem**: Script generates detailed commit messages but poorly categorized changelog entries.
**Root Cause**: The categorization algorithm has flawed keyword matching that can misclassify bug fixes as "Added" items.
**Solutions**:
1. **Start descriptions with clear action words** for better categorization:
```bash
# Good - starts with "Fix" for proper categorization
python3 scripts/bump_version_enhanced.py patch "Fix character tag removal bug in single character mode\nRoot cause: TTS nodes bypassed character parser\nResult: Unrecognized character tags are now properly removed"
# Bad - ambiguous wording leads to miscategorization
python3 scripts/bump_version_enhanced.py patch "Character tag removal bug in single character mode\nRoot cause: TTS nodes bypassed character parser\nResult: Character tags are now properly removed"
```
2. **Use separate commit and changelog descriptions** for precise control:
```bash
python3 scripts/bump_version_enhanced.py patch --commit "Fix character tag removal bug" --changelog "Fix character tags not being removed in single character mode"
```
**For complex changes, use interactive mode**:
```bash
python3 scripts/bump_version_enhanced.py 3.2.8 --interactive
```
This feeds the script proper technical details that get automatically categorized into good changelog entries.
### IMPORTANT: v3.4.0 Script Improvements
**Fixed Issues**: Enhanced multiline parsing with better keyword matching and context detection.
**Always verify changelog output** - if categorization is wrong, manually fix CHANGELOG.md before pushing.
@@ -0,0 +1,67 @@
# ComfyUI VRAM Regression Analysis - Python 3.12
## Summary
ComfyUI versions after v0.3.48 introduce severe VRAM spikes specifically in Python 3.12 environments when using TTS Audio Suite. The issue affects model memory management and causes significant performance degradation.
## Affected Versions
- **Last Working**: ComfyUI v0.3.56 + ComfyUI-frontend v1.25.11
- **First Broken**: ComfyUI v0.3.57 + ComfyUI-frontend v1.25.11
- **Environment**: Python 3.12 only (Python 3.13 unaffected)
- **Root Cause**: Commit e2d1e5da "Enable Convolution AutoTuning" enables `torch.backends.cudnn.benchmark = True`
## Symptoms
- Massive VRAM spikes (multi-GB) occurring BEFORE and AFTER TTS inference, not during actual generation
- Spikes happen during model loading and audio processing/cleanup phases
- Affects all TTS engines: ChatterBox, ChatterBox 23-lang, IndexTTS-2
- Performance severely degraded on 24GB VRAM systems
- Issue does NOT occur during the TTS inference itself
## Testing Results
### ComfyUI v0.3.48 (Working)
```
Environment: Python 3.12
TTS Audio Suite: v4.10.8 (latest)
Result: Flat VRAM usage, no spikes
All engines tested: ✅ ChatterBox, ✅ ChatterBox 23-lang, ✅ IndexTTS-2
```
### ComfyUI v0.3.59 (Broken)
```
Environment: Python 3.12
TTS Audio Suite: v4.10.8 (latest)
Result: Severe VRAM spikes after inference
All engines affected: ❌ ChatterBox, ❌ ChatterBox 23-lang, ❌ IndexTTS-2
```
## Root Cause Analysis
**Specific Commit**: e2d1e5da "Enable Convolution AutoTuning" by contentis <lspindler@nvidia.com>
The commit enables `torch.backends.cudnn.benchmark = True` when `--fast autotune` is used. This CUDNN benchmarking:
1. Tests multiple convolution algorithms to find the fastest
2. **Allocates significant extra VRAM during algorithm benchmarking**
3. Appears to interact poorly with Python 3.12's garbage collection
4. Causes VRAM spikes during model operations, especially with TTS model wrappers
## Reproduction Steps
1. Install Python 3.12 environment
2. Install ComfyUI v0.3.59
3. Install TTS Audio Suite v4.10.8
4. Load any TTS engine workflow
5. Generate audio
6. Observe VRAM spikes before and after generation (not during the actual TTS inference)
## Workaround
Downgrade to ComfyUI v0.3.48 + ComfyUI-frontend v1.2.65
## Next Steps
1. Systematically test ComfyUI versions between v0.3.48 and v0.3.59
2. Identify exact commit that introduced the regression
3. Create detailed GitHub issue for ComfyUI project
4. Provide minimal reproduction case
## Technical Details
- Issue is environment-specific (Python 3.12 only)
- Affects ComfyUI's model wrapper system
- Related to PyTorch CUDA memory management integration
- No code changes needed in TTS Audio Suite - issue is upstream in ComfyUI
@@ -0,0 +1,426 @@
# Higgs Audio 2 Integration Specification
**TTS Audio Suite v4.4.0 - Technical Documentation**
*Complete technical specification for Higgs Audio 2 engine integration with unified architecture*
---
## Overview
The Higgs Audio 2 integration adds a new high-quality TTS engine to the TTS Audio Suite with advanced voice cloning capabilities. This integration follows the established unified architecture patterns while introducing minimal dependencies and maintaining full backward compatibility.
### Key Features
- **High-Quality TTS Generation**: 3B parameter model with advanced audio synthesis
- **Voice Cloning**: Clone any voice from 30+ seconds of reference audio
- **Multi-Language Support**: English, Chinese dialects, and extensible to other languages
- **Unified Architecture**: Seamless integration with existing TTS Text and TTS SRT nodes
- **Chunking Support**: Handle unlimited text length with intelligent chunking
- **Preset Voices**: 8 built-in voice presets for instant use
- **Caching System**: Integrated with existing audio cache for performance
- **Minimal Dependencies**: Only 2 essential new dependencies added
---
## Architecture Design
### Modular Components (500-600 lines each)
The Higgs Audio 2 integration follows the established modular architecture principle:
```
engines/higgs_audio/
├── higgs_audio.py # Main engine wrapper (688 lines)
├── higgs_audio_downloader.py # Model downloader (357 lines)
├── boson_multimodal/ # Complete reference implementation
└── __init__.py
engines/adapters/
└── higgs_audio_adapter.py # Unified adapter (363 lines)
nodes/engines/
└── higgs_audio_engine_node.py # Configuration node (153 lines)
voices_examples/higgs_audio/
├── config.json # Voice preset configuration
├── belinda.wav # Voice presets (8 total)
├── en_woman.wav
└── ...
```
### Integration Points
1. **Engine Configuration**: `HiggsAudioEngineNode` provides user interface
2. **Engine Wrapper**: `HiggsAudioEngine` handles model loading and generation
3. **Unified Adapter**: `HiggsAudioEngineAdapter` bridges to unified nodes
4. **Unified Nodes**: Existing `TTS Text` and `TTS SRT` nodes support higgs_audio
5. **Model Management**: Integrated with `unified_downloader` system
6. **Caching**: Uses existing `AudioCache` with custom key generator
---
## Technical Implementation Details
### Dependencies
**Minimal Essential Dependencies Added:**
- `descript-audio-codec`: Audio codec for Higgs Audio tokenizer
- `vector_quantize_pytorch`: Vector quantization for audio processing
**Avoided Dependencies** (handled internally or replaced):
- `dacite` → Direct dict handling
- `boto3`, `s3fs` → Models from HuggingFace, not S3
- `json_repair` → Python's standard `json` module
- `pydantic` → Standard Python dataclasses
- `pydub` → Existing `librosa`/`torchaudio`
- `jieba`, `langid` → Optional Chinese text processing
- `accelerate` → Optional optimization
### Text Chunking Strategy
**Unified Chunking System:**
```python
# Uses superior ImprovedChatterBoxChunker instead of custom implementation
max_chars = tokens_to_chars(max_tokens_per_chunk) # Convert token limit
chunks = ImprovedChatterBoxChunker.split_into_chunks(text, max_chars)
```
**Benefits:**
- Character-based chunking (more predictable than token approximation)
- Better sentence boundary detection
- Consistent across all engines
- Overlap support for context preservation
### Voice Cloning Implementation
**Voice Priority System:**
1. **`auto`**: Use preset if selected, otherwise reference audio
2. **`preset_dropdown`**: Prefer preset, fallback to reference
3. **`reference_input`**: Prefer reference, fallback to preset
4. **`force_preset`**: Only use preset, ignore reference
**Voice Processing:**
```python
# Convert ComfyUI audio to base64 for boson_multimodal
audio_base64 = self._audio_to_base64(reference_audio)
audio_content = AudioContent(raw_audio=audio_base64, audio_url="")
messages.append(Message(role="assistant", content=[audio_content]))
```
### Model Management
**Organized Download Structure:**
```
ComfyUI/models/TTS/HiggsAudio/
└── higgs-audio-v2-3B/
├── generation/
│ ├── config.json
│ ├── model.safetensors
│ └── generation_config.json
└── tokenizer/
├── config.json
└── pytorch_model.bin
```
**Auto-Download System:**
- Integrated with `unified_downloader`
- Direct downloads (no HuggingFace cache duplication)
- Organized folder structure
- Model reuse across sessions
### Caching System
**HiggsAudioCacheKeyGenerator:**
```python
cache_data = {
'text': text,
'voice_preset': voice_preset,
'model_path': model_path,
'tokenizer_path': tokenizer_path,
'temperature': temperature,
'top_p': top_p,
'top_k': top_k,
'reference_audio_hash': audio_hash, # If present
'engine': 'higgs_audio'
}
```
**Cache Integration:**
- Registered with `AudioCache.register_cache_key_generator()`
- Automatic cache key generation
- Reference audio hashing for consistency
- Shared with existing cache system
---
## Node Integration Details
### Engine Configuration Node
**`HiggsAudioEngineNode` Parameters:**
- `model`: Model selection (higgs-audio-v2-3B, local models)
- `device`: Device selection (auto, cuda, cpu)
- `voice_preset`: Voice preset selection (8 presets + voice_clone)
- `audio_priority`: Voice source priority logic
- `system_prompt`: Generation guidance prompt
- `temperature`: Sampling temperature (0.0-2.0)
- `top_p`: Nucleus sampling (0.1-1.0)
- `top_k`: Top-k sampling (-1 to 100)
- `max_new_tokens`: Generation limit (128-4096)
- `max_tokens_per_chunk`: Chunking limit (100-500)
- `silence_between_chunks_ms`: Inter-chunk silence (0-2000ms)
- `enable_chunking`: Chunking toggle
- `enable_cache`: Cache toggle
- `seed`: Reproducibility seed
### Unified Node Integration
**TTS Text Node Integration:**
```python
elif engine_type == "higgs_audio":
# Create wrapper with adapter
result = engine_instance.generate_tts_audio(
text=text,
char_audio=reference_audio_dict,
char_text=reference_text,
character="narrator",
**config_parameters
)
```
**TTS SRT Node Integration:**
```python
elif engine_type == "higgs_audio":
# Parse SRT and generate per segment
srt_segments = parse_srt(srt_content)
for segment in srt_segments:
segment_audio = engine_instance.generate_srt_audio(...)
audio_segments.append(segment_audio)
# Combine with timing logic
```
---
## Voice Preset System
### Built-in Voice Presets
1. **belinda**: Female, clear English pronunciation
2. **en_woman**: Female, natural English voice
3. **en_man**: Male, clear English voice
4. **chadwick**: Male, distinctive character voice
5. **vex**: Female, character voice with edge
6. **mabel**: Female, warm conversational voice
7. **broom_salesman**: Male, character voice
8. **zh_man_sichuan**: Male, Sichuan Chinese dialect
### Voice Configuration
**`voices_examples/higgs_audio/config.json`:**
```json
{
"belinda": {
"transcript": "This is Belinda's voice with clear pronunciation and natural intonation."
},
"en_woman": {
"transcript": "A natural English woman's voice for general purpose use."
}
}
```
**Voice Discovery:**
- Automatic preset loading from `config.json`
- Audio file validation (`.wav` format)
- Integration with Character Voices system
- Custom reference audio support
---
## Performance Characteristics
### Model Specifications
- **Model Size**: ~3B parameters
- **Memory Requirements**: 8GB+ VRAM recommended
- **Sample Rate**: 24,000 Hz
- **Generation Speed**: Similar to F5-TTS with chunking
- **Voice Quality**: High-fidelity with natural prosody
### Chunking Performance
**Recommended Settings:**
- `max_tokens_per_chunk`: 225 tokens (~787 characters)
- `silence_between_chunks_ms`: 100ms
- `enable_chunking`: True for texts >225 tokens
**Benefits:**
- Unlimited text length support
- Consistent voice quality across chunks
- Natural pauses between segments
- Memory efficiency for long texts
### Caching Benefits
- **Cache Hits**: Instant audio retrieval
- **Cache Misses**: Full generation with caching
- **Storage**: Efficient tensor storage
- **Cleanup**: Automatic cache management
---
## Error Handling and Validation
### Parameter Validation
**HiggsAudioEngineAdapter.validate_parameters():**
```python
# Temperature validation
temperature = max(0.0, min(2.0, float(temperature)))
# Top-p validation
top_p = max(0.1, min(1.0, float(top_p)))
# Voice preset validation
if voice_preset not in available_presets:
validated["voice_preset"] = "voice_clone"
```
### Error Recovery
1. **Model Loading Failures**: Clear error messages with resolution steps
2. **Audio Processing Errors**: Graceful fallback to basic TTS
3. **Reference Audio Issues**: Automatic fallback to preset voices
4. **Memory Issues**: Chunking and cache cleanup
5. **Network Issues**: Retry logic for model downloads
### Logging and Diagnostics
```python
print(f"🚀 Loading Higgs Audio 2 engine: {model_path}")
print(f"✅ Higgs Audio 2 engine loaded successfully")
print(f"🎤 Generating Higgs Audio for {len(chunks)} chunk(s)")
print(f" ✅ Generated audio shape: {audio_tensor.shape}")
```
---
## Compatibility and Migration
### Backward Compatibility
- **Existing Workflows**: No changes required
- **Character Voices**: Full integration with existing system
- **Cache System**: Shared cache across all engines
- **Audio Processing**: Standard ComfyUI audio format
### Forward Compatibility
- **New Engines**: Adapter pattern supports easy addition
- **Feature Extensions**: Modular design allows enhancement
- **Model Updates**: Download system supports model versioning
- **Voice Expansion**: Preset system supports additional voices
### Migration Notes
**From Direct Higgs Audio Nodes:**
- Use Higgs Audio Engine + TTS Text instead of separate nodes
- Voice presets automatically detected
- All parameters preserved with validation
**From Other TTS Engines:**
- Same unified interface
- Voice switching supported
- Cache keys remain unique per engine
---
## Testing and Validation
### Manual Testing Approach
1. **Model Loading**: Verify automatic download and caching
2. **Voice Presets**: Test all 8 built-in presets
3. **Voice Cloning**: Test with various reference audio lengths
4. **Text Chunking**: Test with short and very long texts
5. **Audio Quality**: Compare with reference implementation
6. **Error Handling**: Test edge cases and failure modes
### Integration Testing
1. **Unified Nodes**: Verify seamless engine switching
2. **Character Voices**: Test with Character Voices node
3. **SRT Processing**: Test with subtitle timing
4. **Cache System**: Verify cache hits and misses
5. **Memory Management**: Test cleanup and resource usage
### Performance Benchmarks
**Text Processing:**
- Short text (50 chars): ~2-3 seconds
- Medium text (500 chars): ~5-8 seconds
- Long text (2000+ chars): Chunked processing
**Memory Usage:**
- Base model: ~6GB VRAM
- Peak generation: ~8GB VRAM
- Cache overhead: ~100MB per hour of audio
---
## Future Enhancements
### Planned Features
1. **Streaming Support**: Real-time generation for long texts
2. **Voice Training**: Custom voice preset creation
3. **Multi-Speaker**: Support for conversation-style generation
4. **Advanced Editing**: Word-level editing like F5-TTS
5. **Emotion Control**: Emotional tone parameters
### Extension Points
1. **New Voice Presets**: Easy addition through config.json
2. **Language Models**: Support for additional languages
3. **Audio Effects**: Post-processing pipeline integration
4. **Quality Settings**: Speed vs quality trade-offs
5. **Batch Processing**: Multiple texts with different voices
---
## Credits and Acknowledgments
### Original Implementation
- **HiggsAudio Team**: Original Higgs Audio v2 model and boson_multimodal implementation
- **ShmuelRonen**: ComfyUI-HiggsAudio_2 reference implementation
- **Anthropic**: Claude Code development assistance
### Integration Team
- **TTS Audio Suite**: Unified architecture integration
- **Community Contributors**: Voice presets and testing
### Third-Party Libraries
- **descript-audio-codec**: Audio tokenization
- **vector_quantize_pytorch**: Vector quantization
- **PyTorch**: Deep learning framework
- **torchaudio**: Audio processing
---
## Conclusion
The Higgs Audio 2 integration successfully adds high-quality voice cloning capabilities to the TTS Audio Suite while maintaining the unified architecture principles. The implementation:
✅ **Follows Established Patterns**: Uses existing adapter and unified node systems
✅ **Minimal Dependencies**: Only 2 essential new dependencies
✅ **Superior Chunking**: Uses unified chunking system for consistency
✅ **Full Integration**: Works with all existing unified nodes
✅ **Voice Cloning**: Supports both presets and custom reference audio
✅ **Performance**: Efficient caching and model management
✅ **Backward Compatible**: No breaking changes to existing workflows
This integration demonstrates the power of the unified architecture, allowing complex new engines to be added with minimal disruption while providing users with a consistent, powerful interface for high-quality TTS generation.
---
*Generated for TTS Audio Suite v4.4.0 - Higgs Audio 2 Integration*
@@ -0,0 +1,293 @@
# Higgs Audio Stateless Wrapper Implementation - Complete Journey
## **Problem Statement**
Higgs Audio 2 could not integrate with ComfyUI's native model management system due to CUDA Graph memory corruption during model unloading. The goal was to apply the same stateless wrapper pattern used for ChatterBox streaming to enable safe model unloading while preserving performance.
## **Original Performance Baseline**
- **50 it/s (tokens/second)** generation speed
- **CUDA Graph optimization** enabled
- **11GB VRAM usage**
- **Voice cloning functional**
- **No safe model unloading** - crashes on "Unload Models"
---
## **Implementation Attempts & Results**
### **Attempt 1: Basic Stateless Wrapper Creation**
**Approach**: Created StatelessHiggsAudioWrapper following ChatterBox pattern
- Created `engines/higgs_audio/stateless_wrapper.py`
- Implemented tensor isolation with `.detach().clone()`
- Wrapped HiggsAudioEngine with stateless methods
**Result**: ❌ Circular dependency causing infinite loop
**Fix**: Modified unified interface to create HiggsAudioServeEngine directly
### **Attempt 2: Direct Serve Engine Integration**
**Approach**: Bypass HiggsAudioEngine.initialize_engine() to avoid circular calls
- Modified `utils/models/unified_model_interface.py`
- Direct HiggsAudioServeEngine creation
- Manual component setup
**Result**: ❌ TypeError: 'generator' object is not callable
**Fix**: Changed `@property` decorator to regular method for `parameters()`
### **Attempt 3: Device Mismatch Fixes - Round 1**
**Approach**: Fix "Input type torch.FloatTensor and weight type torch.cuda.FloatTensor" errors
- Added device enforcement in serve engine input preparation
- Modified audio tokenizer to ensure tensors on correct device
**Result**: ❌ Still device mismatches in semantic model
**Progress**: Error moved from one component to another (good sign)
### **Attempt 4: Deep Device Consistency**
**Approach**: Comprehensive device fixing throughout audio pipeline
- Fixed `get_regress_target()` method in audio tokenizer
- Added `next(model.parameters()).device` for accurate device detection
- Fixed encoder device handling in `_xcodec_encode()`
**Result**: ❌ Device mismatch moved to KV cache operations
**Lesson**: Device issues were systemic, requiring comprehensive fixes
### **Attempt 5: KV Cache Device Synchronization**
**Approach**: Fix cache_position tensor CPU/CUDA mismatch in transformers library
- Added comprehensive KV cache device enforcement
- Implemented cache tensor movement to model device
- Enhanced input tensor device verification
**Result**: ❌ Persistent device mismatch in cache_utils.py:1220
**Issue**: Deep transformers library issue with cache_position tensor creation
### **Attempt 6: Disable KV Cache (Temporary)**
**Approach**: Remove StaticCache to bypass device issues
- Set `use_cache=False`
- Removed `past_key_values_buckets` parameter
**Result**: ✅ No crashes BUT ❌ Performance degraded to ~11 it/s
**Lesson**: StaticCache is critical for performance
### **Attempt 7: Smart KV Cache Recreation**
**Approach**: Recreate caches when device mismatch detected
- Device mismatch detection in `_prepare_kv_caches()`
- Dynamic cache recreation with correct device
- Restore StaticCache functionality
**Result**: ❌ Still ~12 it/s performance with occasional device issues
### **Attempt 8: Performance Optimization - Remove Tensor Cloning**
**Approach**: Optimize stateless wrapper by removing expensive operations
- Changed `.detach().clone()` to `.detach()` only
- Removed redundant device operations
- Added performance caching
**Result**: ❌ Still ~12 it/s performance
**Discovery**: Cloning wasn't the main bottleneck
### **Attempt 9: Bypass Stateless Wrapper Test**
**Approach**: Temporarily disable stateless wrapper to isolate performance issue
- Modified adapter to use original `generate()` method
- Test if wrapper was causing slowdown
**Result**: ❌ Still ~12 it/s performance even without wrapper
**Conclusion**: Performance issue was deeper in the loading approach
### **Attempt 10: Root Cause Discovery - CPU→GPU Loading**
**Approach**: Identified that CPU-first loading was breaking CUDA Graph optimization
- Original approach: Load directly on GPU
- Our approach: Load on CPU → Move to GPU
- CUDA Graphs are compiled for specific device configurations
**Root Cause Found**: CPU→GPU loading breaks CUDA Graph optimization
### **Attempt 11: Direct GPU Loading**
**Approach**: Restore original loading pattern
- Modified `unified_model_interface.py` to load directly on target device
- Removed CPU→GPU transfer code
- Fixed data collator device mismatch (`torch.full()` device parameter)
**Result**: ✅ **54.92 tok/s performance restored!** BUT ❌ Crashes on model unload
**Issue**: CUDA Graph warnings → Process termination
### **Attempt 12: Comprehensive CUDA Graph Cleanup**
**Approach**: Multi-layer CUDA Graph cleanup with official PyTorch APIs
- Disabled CUDA Graph compilation during cleanup
- Used `torch.cuda.reset_accumulated_memory_stats()`
- Multiple CUDA synchronization passes
- Complete KV cache tensor replacement
**Result**: ❌ Still crashes with CUDA warnings
**Issue**: CUDA Graph allocations persist at PyTorch level
### **Attempt 13: Complete Engine Destruction**
**Approach**: Nuclear option - destroy and recreate entire serve engine
- Store model paths for recreation
- Destroy all engine components (model, tokenizer, caches)
- Set engine to None to force complete recreation
- Mark for recreation with stored paths
**Result**: ❌ Still crashes with identical CUDA warnings
**Issue**: CUDA Graph captures happen at memory allocator level, persist even after object destruction
### **Attempt 14: Surgical CUDA Graph Preservation (FAILED)**
**Approach**: Keep CUDA Graphs but clean them surgically
- Exit inference mode for tensor modifications
- Replace cache tensors with CPU versions
- Use official PyTorch CUDA Graph management APIs
**Result**: ❌ Cache structure errors and persistent CUDA warnings
**Issue**: Cache structure is complex (lists), CUDA Graph captures are too deep
### **Attempt 15: PyTorch 2.2+ Modern CUDA Graph APIs (FAILED)**
**Approach**: Research and implement modern PyTorch CUDA Graph cleanup APIs
- Researched PyTorch 2.2, 2.3, 2.4 improvements
- Implemented torch.cuda.graph() context manager approach
- Added memory pool context managers and proper cleanup sequences
**Result**: ❌ Context manager API errors and persistent CUDA warnings
**Issue**: Modern APIs require different parameters, CUDA Graph captures persist at allocator level
### **Attempt 16: CUDA Graph Toggle Implementation (PARTIAL SUCCESS)**
**Approach**: Accept fundamental incompatibility and implement user choice toggle
- Added `enable_cuda_graphs` boolean parameter to Higgs Audio Engine node
- Implemented conditional loading: StaticCache vs DynamicCache
- Added parameter passing chain: Node → Config → Adapter → Engine → Factory
- Conditional memory unloading protection based on toggle setting
**Result**: ✅ **Toggle UI working** BUT ❌ **CUDA Graphs still created in Memory Safe mode**
**Progress**:
- ✅ Parameter passing chain complete
- ✅ "Memory Safe (CUDA Graphs OFF)" vs "High Performance (CUDA Graphs ON)" modes working
- ✅ Conditional cleanup logic implemented
- ✅ Memory unloading protection working
- ❌ **Critical Issue**: StaticCache still created even when CUDA Graphs disabled
- ❌ **Still crashes**: CUDA allocation warnings persist in "safe" mode
**Technical Discovery**: Environment variables set too late - serve engine constructor creates StaticCache before disable flags take effect
---
## **CONCLUSION: Fundamental Architectural Challenge**
After **16 different implementation attempts**, the core issue remains unsolved:
### **Root Cause Analysis**:
1. **CUDA Graphs are compiled at the PyTorch C++ level** - cannot be disabled via Python APIs alone
2. **StaticCache constructor immediately allocates CUDA Graph memory** - happens before environment variables take effect
3. **Memory allocator captures references during first generation** - these become "captured allocations"
4. **ComfyUI's model unloading tries to free captured allocations** - triggers fatal CUDA warnings
5. **Even DynamicCache approach fails** - underlying model still creates CUDA Graph optimizations
### **Current Status: Toggle Implementation (Partial Success)**
**✅ Successfully Implemented:**
- **User toggle UI** with clear performance/safety tradeoffs
- **Complete parameter passing chain** from node to factory
- **Conditional cleanup logic** that prevents crashes when CUDA Graphs enabled
- **Memory unloading protection** with clear user messaging
- **Mode detection and appropriate warnings**
**❌ Still Failing:**
- **StaticCache still created** even in "Memory Safe" mode
- **CUDA Graph allocations still captured** during generation
- **Crashes still occur** on model unload in both modes
- **Environment variable timing** - set after engine constructor runs
### **Technical Findings:**
1. **Performance Impact**: Confirmed ~40% speed reduction when CUDA Graphs disabled (55+ tok/s → ~35 tok/s)
2. **Memory Management**: 11GB VRAM freed successfully, but CUDA state corruption persists
3. **Architectural Conflict**: ComfyUI's dynamic model management fundamentally incompatible with CUDA Graph memory capture
4. **PyTorch Limitation**: No clean API to disable CUDA Graphs after model initialization
### **Final Assessment**:
The toggle provides **user choice** but both modes have limitations:
- **High Performance Mode**: Full speed, but model cannot be unloaded (restart required)
- **Memory Safe Mode**: Still crashes due to architectural conflicts (implementation incomplete)
**Recommendation**: Document current state as "partial solution" - users can choose performance vs theoretical memory safety, understanding both have constraints.
---
## **Key Technical Insights Discovered**
### **1. CUDA Graph Optimization Requirements**
- Must load directly on target GPU device
- CPU→GPU transfer breaks CUDA Graph compilation
- StaticCache is essential for performance (50+ it/s vs 12 it/s)
### **2. Device Consistency Challenges**
- Device mismatches occur at multiple pipeline stages:
- Audio tokenizer input processing
- Semantic model tensor operations
- Encoder operations
- KV cache tensor operations
- Data collator tensor concatenation
- Requires systematic fixes throughout entire pipeline
### **3. Memory Management Conflicts**
- CUDA Graphs capture memory allocations during generation
- Model unloading tries to free "captured" allocations
- Results in warnings → crashes
- Requires explicit CUDA Graph cleanup before unloading
### **4. Performance Bottlenecks Identified**
- **Primary**: CPU→GPU loading approach (54 it/s → 12 it/s impact)
- **Secondary**: Tensor cloning operations (moderate impact)
- **Tertiary**: Excessive device synchronization (minor impact)
---
## **Files Modified**
### **Core Implementation**
- `engines/higgs_audio/stateless_wrapper.py` - Main stateless wrapper
- `utils/models/unified_model_interface.py` - Factory integration
- `engines/adapters/higgs_audio_adapter.py` - Adapter integration
- `utils/models/comfyui_model_wrapper.py` - ComfyUI integration
### **Device Consistency Fixes**
- `engines/higgs_audio/boson_multimodal/serve/serve_engine.py` - Input/cache device handling
- `engines/higgs_audio/boson_multimodal/audio_processing/higgs_audio_tokenizer.py` - Tokenizer device handling
- `engines/higgs_audio/boson_multimodal/data_collator/higgs_audio_collator.py` - Collator device handling
---
## **Current Status (As of Final Context)**
### **✅ Confirmed Working**
- **Voice cloning functionality** - Full feature preservation
- **Memory management** - 11GB VRAM freed successfully
- **Device consistency** - No device mismatch crashes
- **High performance potential** - 54.92 tok/s achieved
### **❌ Outstanding Issues**
- **CUDA Graph cleanup crashes** - Hybrid solution untested
- **Performance vs Safety tradeoff** - Need both simultaneously
### **🔬 Untested Solution**
- **Hybrid approach** with comprehensive CUDA Graph cleanup
- **Expected outcome**: 54+ it/s performance + no crashes + safe unloading
- **Status**: Implementation complete, testing required
---
## **Recommended Next Steps**
1. **Test hybrid solution** - Verify 54 it/s + no crashes
2. **If crashes persist**: Investigate alternative CUDA Graph cleanup methods
3. **Performance monitoring**: Ensure no regression from cleanup operations
4. **Stress testing**: Multiple load/unload cycles
5. **Documentation update**: Record final working solution
---
## **Lessons Learned**
1. **Device loading approach is critical** for CUDA Graph optimization
2. **Systematic device consistency** required throughout pipeline
3. **CUDA Graph cleanup** must be explicit and comprehensive
4. **Performance vs Safety** requires sophisticated balancing, not simple tradeoffs
5. **Deep integration challenges** require understanding of underlying frameworks (transformers, CUDA)
This implementation represents a complex balance between PyTorch CUDA Graph optimization, device management, and ComfyUI model lifecycle integration.
+633
View File
@@ -0,0 +1,633 @@
# TTS Audio Suite Model Loading Architecture Analysis
## EXECUTIVE SUMMARY
The codebase shows a **two-tiered model loading architecture**:
1. **Unified Model Interface Layer** - Relatively new, factory-pattern based, ComfyUI-integrated
2. **Legacy/Fallback Systems** - Multiple engine-specific loaders still in use, creating redundancy
**Critical Finding**: The unified interface exists but is **NOT consistently adopted**. Some engines use it fully, others have dual implementations, creating inconsistent patterns and potential device/cache conflicts.
---
## 1. UNIFIED INTERFACE USAGE & ADOPTION RATE
### Current Status
**File**: `/home/linux/ComfyUI_TTS_Audio_Suite/utils/models/unified_model_interface.py` (817 lines)
### Adoption by Engine
| Engine | Status | Primary Loader | Fallback | Notes |
|--------|--------|----------------|----------|-------|
| ChatterBox | DUAL | Unified Interface | Manager.load_tts_model() | Uses unified, but manager has legacy fallback logic (lines 252-439 in manager.py) |
| F5-TTS | DUAL | Unified Interface | SmartModelLoader + direct F5TTS calls | Hybrid approach with multiple fallback paths |
| ChatterBox Official 23-Lang | UNIFIED | Unified Interface | None documented | Cleanest implementation, language-aware cache key (line 216-217) |
| Higgs Audio | UNIFIED | Unified Interface | None documented | Stateless wrapper integration (line 566) |
| RVC | UNIFIED | Unified Interface (4 factories) | None documented | Most comprehensive (VC, hubert, separator_mdx, separator_demucs) |
| VibeVoice | UNIFIED | Unified Interface | None documented | Engine pattern with integrated initialization |
| IndexTTS-2 | UNIFIED | Unified Interface | None documented | Path resolution and auto-download handling |
**Adoption Rate**: ~85% of new engines use unified interface, but legacy fallbacks still exist
### Factory Registration Architecture
**Location**: Lines 327-813 in unified_model_interface.py
```python
def initialize_all_factories(): # Line 805
register_chatterbox_factory()
register_chatterbox_23lang_factory()
register_f5tts_factory()
register_higgs_audio_factory()
register_rvc_factory()
register_vibevoice_factory()
register_index_tts_factory()
```
Each factory is registered with key format: `"{engine_name}_{model_type}"` (line 55)
---
## 2. FACTORY PATTERN CONSISTENCY ANALYSIS
### Issue 1: Inconsistent Error Handling
**ChatterBox Factory** (lines 401-444)
```python
def chatterbox_tts_factory(**kwargs):
if ChatterboxTTS is None:
raise RuntimeError("ChatterboxTTS not available - check installation")
# Has fallback mixing logic
```
**F5TTS Factory** (lines 449-497)
```python
def f5tts_factory(**kwargs):
# Uses smart_model_loader.load_model_if_needed()
# No inline error handling - delegates to SmartModelLoader
```
**VibeVoice Factory** (lines 623-657)
```python
def vibevoice_factory(**kwargs):
try:
import accelerate
except ImportError as e:
print(f"⚠️ Unified Interface: accelerate not available: {e}")
# Continues without raising - soft failure pattern
```
**Problem**: Three different error handling patterns:
1. ChatterBox: Raises RuntimeError for missing imports
2. F5TTS: Delegates to SmartModelLoader (different exception path)
3. VibeVoice: Prints warning and continues (no exception)
### Issue 2: Inconsistent Signatures
| Factory | Signature Pattern | Special Handling |
|---------|------------------|------------------|
| ChatterBox | `device, language, model_path` | Multi-step fallback with German special case (lines 366-383) |
| F5TTS | `device, model_name` | Delegates to smart_loader callback |
| Higgs Audio | `device, model_path, tokenizer_path, enable_cuda_graphs` | CUDA-specific parameters |
| RVC | `model_path, index_path, device` | Multiple model types (VC, hubert, separators) |
| IndexTTS-2 | `device, use_fp16, use_cuda_kernel, use_deepspeed` | Optimization flags |
**Problem**: Each factory has different parameter semantics and handling logic.
### Issue 3: Device Resolution Inconsistencies
**Unified Interface Approach** (line 141):
```python
from utils.device import resolve_torch_device
target_device = resolve_torch_device(config.device)
```
**Factory-Level Device Resolution**:
- **ChatterBox** (line 410): `device = kwargs.get("device", "auto")`
- Passed directly to ChatterboxTTS.from_local/from_pretrained
- Device resolution happens IN the engine, not in unified interface
- **F5TTS** (line 453): `device = kwargs.get("device", "auto")`
- Passed to smart_model_loader
- Then to ChatterBoxF5TTS.from_local/from_pretrained
- **Two levels of resolution possible**
- **Higgs Audio** (line 508): `device = kwargs.get("device", "cuda")`
- Default is "cuda" not "auto"
- **Inconsistent default**
- **VibeVoice** (line 637): `device = kwargs.get("device", "auto")`
**Problem**: Device resolution happens at MULTIPLE levels, with inconsistent defaults and no guarantee of final device being correct.
---
## 3. MODEL FILE LOADING PATTERNS - ALL INSTANCES
### Load Functions in Codebase
| File | Function | Pattern | Issues |
|------|----------|---------|--------|
| manager.py (line 226-439) | `ModelManager.load_tts_model()` | Try local → Try HuggingFace → Fallback English | Has language fallback logic |
| manager.py (line 441-577) | `ModelManager.load_vc_model()` | Try local → Try HuggingFace | Supports language switching |
| f5tts_manager.py (line 108-223) | `F5TTSModelManager.load_f5tts_model()` | Try unified → Fallback unified+direct | Dual system |
| f5tts_base_node.py (line 86-192) | `BaseF5TTSNode.load_f5tts_model()` | Try unified → Fallback SmartLoader | Three-tier fallback |
| unified_model_interface.py (line 111-178) | `UnifiedModelInterface.load_model()` | Check cache → Load via factory | Direct factory invocation |
### Format Detection Logic
**ChatterBox** (manager.py, lines 108-120):
- Detects required files: `ve.`, `t3_cfg.`, `s3gen.`, `tokenizer.json`
- Can be `.pt` or `.safetensors` extensions
- Directory-based model location
**F5TTS** (f5tts_manager.py, lines 79-83):
- Checks for ANY `.safetensors` or `.pt` files in directory
- Less specific detection (any model file matches)
**ChatterBox Official 23-Lang** (unified_model_interface.py, lines 776-785):
- Has explicit `from_local()` and `from_pretrained()` methods
- Model version parameter (v1 vs v2)
**Higgs Audio** (higgs_audio_engine_node.py, lines 79-105):
- Requires subdirectories: `generation/` and `tokenizer/`
- Checks for `config.json` and model files in both
**RVC** (manager.py, no dedicated loader):
- Model loading happens in factory only
- Format: model_path and index_path parameters
**Problem**: Each engine has DIFFERENT detection logic with NO unified format detection system. Code duplication across manager.py, f5tts_manager.py, and node discovery methods.
---
## 4. DEVICE MANAGEMENT ANALYSIS
### Unified Interface Device Handling
**Location**: unified_model_interface.py, lines 139-146
```python
# Check if force reload is needed
if force_reload:
tts_model_manager.remove_model(cache_key)
# Get existing model if cached
wrapper = tts_model_manager.get_model(cache_key)
if wrapper is not None:
# Ensure model is on correct device
from utils.device import resolve_torch_device
target_device = resolve_torch_device(config.device)
if wrapper.current_device != target_device:
wrapper.model_load(target_device)
return wrapper.model
```
**Problem 1**: Device comparison uses string comparison (`wrapper.current_device != target_device`). If one is `"auto"` and other is resolved `"cuda"`, they won't match even though they're the same device.
**ComfyUI Wrapper Device Handling**
**Location**: base_wrapper.py, lines 77-87
```python
# Convert device to torch.device object for ComfyUI compatibility
from utils.device import resolve_torch_device
device_name = resolve_torch_device(model_info.device)
if isinstance(device_name, str):
if device_name == "cuda":
self.device = torch.device("cuda", torch.cuda.current_device() if torch.cuda.is_available() else 0)
else:
self.device = torch.device(device_name)
else:
self.device = device_name
```
**Problem 2**: Device is resolved, converted to torch.device, but wrapper stores it as torch.device object while unified interface uses strings. **Type mismatch**.
### Auto Device Resolution
**Location**: torch_device_resolver.py, lines 10-51
```python
def resolve_torch_device(device: str) -> str:
if device == "auto":
if torch.backends.mps.is_available():
return "mps"
elif torch.cuda.is_available():
return "cuda"
elif hasattr(torch, "xpu") and torch.xpu.is_available():
return "xpu"
else:
return "cpu"
```
**Problem 3**: Called in multiple places with no caching. If GPU becomes unavailable between calls (memory pressure unload), subsequent calls might return different device. **No locking or consistency guarantee**.
### Device Mismatch Issues Found
1. **BaseNode.resolve_device()** (base_node.py, lines 124-136):
```python
def resolve_device(self, device: str) -> str:
if device == "auto":
return "cuda" if torch.cuda.is_available() else "cpu"
return device
```
**This is DIFFERENT from torch_device_resolver.py** - doesn't check MPS or XPU!
2. **Factory defaults** - Higgs Audio uses `device="cuda"` as default (line 508 unified_model_interface.py), while others use `"auto"`.
3. **ComfyUI wrapper** (line 141 base_wrapper.py):
```python
from utils.device import resolve_torch_device
target_device = resolve_torch_device(config.device)
```
But then converts to torch.device object, not string comparison.
---
## 5. CACHING & MEMORY MANAGEMENT
### Unified Interface Cache Key Generation
**Location**: unified_model_interface.py, lines 205-234
```python
def _generate_cache_key(self, config: UnifiedModelConfig) -> str:
components = [
config.engine_name,
config.model_type,
config.model_name,
config.device,
]
# CRITICAL FIX: ChatterBox Official 23-Lang is multilingual - language shouldn't affect cache
if config.engine_name != "chatterbox_official_23lang":
components.append(config.language or "default")
# Add path/repo info if available
if config.model_path:
components.append(f"path:{Path(config.model_path).name}")
elif config.repo_id:
components.append(f"repo:{config.repo_id}")
# Add additional_params to ensure attention_mode, quantization, etc. trigger reloads
if config.additional_params:
sorted_params = sorted(config.additional_params.items())
params_str = "_".join([f"{k}:{v}" for k, v in sorted_params])
components.append(f"params:{params_str}")
cache_key = "_".join(components)
return cache_key
```
**Issues**:
1. **Line 211**: Device is ALWAYS included, but device can be "auto" which resolves differently on each call → **Cache key instability**
2. **Line 216-217**: Special-case hardcoding for chatterbox_official_23lang (comment says "CRITICAL FIX") suggests cache key generation is fragile
3. **No address hashing**: Full strings concatenated, very long cache keys possible
4. **Path name only** (line 221): If two models named identically in different paths, cache key collision possible
### ComfyUI Model Manager Caching
**Location**: model_manager.py, lines 44-127
```python
def load_model(self,
model_factory_func,
model_key: str,
model_type: str,
engine: str,
device: str,
force_reload: bool = False,
**factory_kwargs) -> ComfyUIModelWrapper:
# Check if already cached
if model_key in self._model_cache and not force_reload:
wrapper = self._model_cache[model_key]
is_valid = getattr(wrapper, '_is_valid_for_reuse', True)
if not is_valid:
# For Higgs Audio with CUDA graph corruption...
# For VibeVoice, try to recover...
# For IndexTTS-2, attempt recovery...
```
**Issues**:
1. **Engine-specific recovery logic** (lines 54-117):
- Higgs Audio: Reset CUDA graph state (line 137)
- VibeVoice: Clear internal caches (line 77)
- IndexTTS-2: Clear device-cached state (line 102)
- **Each engine needs custom recovery code** - not scalable
2. **Validity flag** (line 48):
```python
is_valid = getattr(wrapper, '_is_valid_for_reuse', True)
```
Only Higgs Audio sets this to False (base_wrapper.py, line 302). **VibeVoice and IndexTTS-2 corrupted models won't be detected**.
3. **No smart cache management** - older models not cleared to make room for new ones automatically.
### SmartModelLoader Cache
**Location**: smart_loader.py, lines 23-225
```python
class SmartModelLoader:
def __init__(self):
self._global_model_cache: Dict[str, ModelInfo] = {}
self._current_models: Dict[str, Dict[str, Any]] = {}
def _generate_cache_key(self, engine_type: str, model_name: str, device: str) -> str:
key_data = f"{engine_type}_{model_name}_{device}"
return hashlib.md5(key_data.encode()).hexdigest()[:16]
```
**Issues**:
1. **MD5 hashing** (line 44): Different from unified interface string concatenation
2. **THREE separate cache systems**:
- UnifiedModelInterface: String concatenation
- ComfyUITTSModelManager: Direct dict with string keys
- SmartModelLoader: MD5 hashes
- **Models can exist in multiple caches simultaneously**
3. **Cross-engine reuse** (lines 59-84):
```python
def _check_cross_engine_cache(self, engine_type: str, model_name: str, device: str) -> Optional[Any]:
cache_key = self._generate_cache_key(engine_type, model_name, device)
if cache_key in self._global_model_cache:
model_info = self._global_model_cache[cache_key]
model_instance = model_info.model_instance
# CRITICAL: Ensure model is on correct device after cache reuse
actual_device = "auto" if device == "auto" and torch.cuda.is_available() else device
```
Device resolution happens again here! **Multiple resolution points for "auto"**.
---
## 6. ERROR HANDLING PATTERNS
### Missing Fallback Mechanisms
**ChatterBox** (unified_model_interface.py, lines 340-398):
- ✅ Has multi-level fallback:
1. Try provided path
2. Try local language model
3. Try HuggingFace language
4. Special fallback for German variants
5. Final fallback to English
- ✅ Graceful 401 authorization error handling (line 362)
**F5-TTS** (unified_model_interface.py, lines 449-497):
- ⚠️ Delegates to SmartModelLoader - error handling unclear
- ⚠️ No explicit fallback documented
- ⚠️ Smart loader has callback pattern but errors propagate up
**Higgs Audio** (unified_model_interface.py, lines 502-571):
- ❌ No fallback mechanism
- ⚠️ Sets up CUDA graphs, can fail silently if disabled
- ⚠️ Complex initialization with multiple environment variables
**RVC** (unified_model_interface.py, lines 574-618):
- ❌ No fallback for missing models
- ⚠️ get_vc() call at line 590 - error propagates directly
- ❌ No graceful degradation
**VibeVoice** (unified_model_interface.py, lines 621-657):
- ⚠️ Soft failure for accelerate import (line 631)
- ❌ No fallback if model download fails
**IndexTTS-2** (unified_model_interface.py, lines 660-746):
- ✅ Good error detection for external IndexTTS installations (lines 688-708)
- ✅ Checks config.yaml existence (line 726)
- ⚠️ Raises RuntimeError - no fallback
### Import Error Handling
**Pattern 1 - Try/Except on Import** (most factories)
```python
try:
from engines.chatterbox.tts import ChatterboxTTS
except ImportError:
ChatterboxTTS = None
```
**Pattern 2 - Direct Import with No Error Handling** (RVC)
```python
from engines.rvc.impl.vc_infer_pipeline import get_vc # Line 578
from engines.rvc.impl.config import config as rvc_config # Line 579
```
**Pattern 3 - ImportError Message** (IndexTTS)
```python
except ImportError as e:
raise ImportError(f"IndexTTS-2 dependencies not available. Error: {e}")
```
**Problem**: No consistent pattern for handling missing dependencies.
---
## 7. SPECIFIC PROBLEMS IDENTIFIED
### Problem 1: "auto" Device Instability
**Severity**: HIGH
**Location**: Multiple (torch_device_resolver.py, base_node.py, smart_loader.py, unified_model_interface.py)
Device resolution for "auto" happens at 4+ different places with NO caching. If CUDA becomes unavailable mid-execution, subsequent calls return CPU device while cache key still contains "auto".
**Example**:
1. Model loads on CUDA with cache key: `"chatterbox_tts_auto"`
2. ComfyUI clears VRAM
3. Next inference call: `resolve_torch_device("auto")` now returns "cpu"
4. Cache lookup fails (expects "auto", not "cpu")
5. Model reloads on CPU unnecessarily
### Problem 2: Dual Unified Interface Usage
**Severity**: MEDIUM
**Location**: manager.py (lines 252-273), f5tts_manager.py (lines 132-145)
Both ChatterBox and F5-TTS managers try unified interface FIRST, then fall back to legacy systems. Creates:
- Inconsistent code paths
- Debug complexity
- Potential for models cached in one system, used in another
```python
# manager.py, line 253
try:
model = load_tts_model(engine_name="chatterbox", ...)
except Exception as e:
print(f"⚠️ Failed to load ChatterBox model via unified interface: {e}")
# Fall back to original logic if unified interface fails
pass
```
### Problem 3: Cache Key Collision Risk
**Severity**: MEDIUM
**Location**: unified_model_interface.py (lines 205-234)
Cache key uses basename only for paths:
```python
if config.model_path:
components.append(f"path:{Path(config.model_path).name}") # Line 221
```
If two different paths have identically-named model directories, cache collision occurs:
- `/models/TTS/chatterbox/English/`
- `/backup/models/chatterbox/English/`
- Both generate cache component: `path:English`
### Problem 4: Device Type Mismatch
**Severity**: MEDIUM
**Location**: base_wrapper.py (lines 77-87), unified_model_interface.py (line 141-146)
Unified interface stores device as string, ComfyUI wrapper converts to torch.device:
```python
# unified_model_interface.py - uses string
target_device = resolve_torch_device(config.device) # Returns "cuda"
# base_wrapper.py - stores torch.device
self.device = torch.device("cuda", ...) # torch.device object
```
Later comparison:
```python
if wrapper.current_device != target_device: # "cuda" != torch.device("cuda")
```
**Type mismatch in comparison**.
### Problem 5: Engine-Specific Recovery Code Not Scalable
**Severity**: MEDIUM
**Location**: model_manager.py (lines 54-117)
Each engine has custom recovery logic for corrupted models:
```python
if engine == "higgs_audio":
# Reset CUDA graphs
elif engine == "vibevoice":
# Clear caches
elif engine == "index_tts":
# Clear device state
```
When new engine added, must modify model_manager.py. No extensible pattern.
### Problem 6: Three Separate Caching Systems
**Severity**: HIGH
**Location**: unified_model_interface.py, model_manager.py, smart_loader.py
Models can be cached in:
1. **UnifiedModelInterface**: Via ComfyUI manager
2. **SmartModelLoader**: Global cache with MD5 keys
3. **Manager classes**: Manual dicts (ChatterBox manager, F5-TTS manager)
**No synchronization between them**. A model cleared from one cache remains in others.
### Problem 7: Inconsistent Error Handling
**Severity**: MEDIUM
**Location**: All factories in unified_model_interface.py
- ChatterBox: Raises RuntimeError (line 408)
- F5-TTS: Delegates to SmartModelLoader
- VibeVoice: Prints warning and continues (line 631)
- RVC: Direct ImportError (line 578)
- IndexTTS-2: Custom ImportError message (line 742)
**No consistent error contract** - callers don't know what exceptions to expect.
### Problem 8: Cache Key Instability with Language
**Severity**: LOW
**Location**: unified_model_interface.py (line 216)
```python
if config.engine_name != "chatterbox_official_23lang":
components.append(config.language or "default")
```
Special-casing suggests original cache key generation was broken. Comment says "CRITICAL FIX" but introduces inconsistency for future engines.
---
## 8. FACTORY SIGNATURE ANALYSIS
| Parameter | ChatterBox | F5TTS | Higgs | RVC | VibeVoice | IndexTTS |
|-----------|-----------|-------|-------|-----|-----------|----------|
| device | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| model_name | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ |
| language | ✅ | ⚠️ (ignored) | ❌ | ❌ | ❌ | ❌ |
| model_path | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| repo_id | ✅ | ⚠️ (not used) | ❌ | ❌ | ❌ | ❌ |
| additional_params | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ |
**No consistent parameter contract**.
---
## 9. RECOMMENDATIONS FOR UNIFICATION
### Quick Wins (Low Risk)
1. **Fix auto device instability**:
- Cache resolved device in resolve_torch_device()
- Don't store "auto" in cache keys, only resolved device
2. **Consistent device comparison**:
- Resolve both sides before comparison
- Store device as string consistently across wrapper and interface
3. **Consolidate error handling**:
- Create base exception class for model loading errors
- Standardize all factories to raise same exceptions
### Medium Effort (Medium Risk)
4. **Unified fallback handler**:
- Extract ChatterBox fallback logic to generic utility
- Apply to all engines consistently
5. **Standardize cache keys**:
- Use hash of full path, not basename only
- Consider device as "resolved" device, not "auto"
6. **Single cache system**:
- Deprecate SmartModelLoader and manager-specific caches
- Route everything through ComfyUI manager
### Major Refactor (High Risk)
7. **Factory interface standardization**:
- Define common parameter set all factories accept
- Use dataclass for factory parameters
- Implement validation layer
8. **Engine-agnostic recovery**:
- Create engine capability registry
- Generic recovery pattern with hooks
---
## FILES REQUIRING CHANGES
### Core Files
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/models/unified_model_interface.py` - Cache key generation, factory signatures
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/models/comfyui_model_wrapper/model_manager.py` - Engine-specific recovery logic
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/device/torch_device_resolver.py` - Auto device caching
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/models/manager.py` - Unified interface dual-loading
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/models/f5tts_manager.py` - Unified interface dual-loading
### Supporting Files
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/models/smart_loader.py` - Cache integration
- `/home/linux/ComfyUI_TTS_Audio_Suite/utils/models/comfyui_model_wrapper/base_wrapper.py` - Device type handling
- `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/base/base_node.py` - Device resolution duplication
- `/home/linux/ComfyUI_TTS_Audio_Suite/nodes/base/f5tts_base_node.py` - Triple-tier fallback
---
## SUMMARY TABLE
| Aspect | Current State | Issues | Risk Level |
|--------|---------------|--------|-----------|
| Unified Interface Coverage | 85% of engines | Legacy fallbacks still exist | MEDIUM |
| Factory Consistency | Inconsistent | Different error handling, parameters | HIGH |
| Device Management | Decentralized | 4+ resolution points, type mismatch | HIGH |
| Caching Strategy | Triple system | No synchronization | CRITICAL |
| Fallback Mechanisms | Partial | ChatterBox has, others don't | MEDIUM |
| Error Handling | Inconsistent | 5 different patterns | MEDIUM |
@@ -0,0 +1,641 @@
# TTS Audio Suite - Model Loading Architecture: Improvement Report
**Date**: November 13, 2025
**Status**: Comprehensive Analysis Complete
**Audience**: Technical Maintainers / Decision Makers
---
## EXECUTIVE SUMMARY
The TTS Audio Suite has a **functional but fragmented model loading architecture**. A unified interface exists and is being adopted (~85% coverage), but critical architectural issues create maintenance burden, cache instability, and potential device management failures.
**The unified system works, but the inconsistencies around it create real problems.**
### Key Finding
| Aspect | Current State |
|--------|---------------|
| **System Functionality** | ✅ Works without errors |
| **Architecture Quality** | ⚠️ Fragmented (legacy + new coexist) |
| **Risk Level** | ⚠️ Medium - edge cases exist |
| **Technical Debt** | 🔴 HIGH - 3 caching systems, 5 error patterns |
| **Maintainability** | 🟡 Declining - new engines need custom code |
---
## PART 1: CURRENT STATE ASSESSMENT
### 1.1 Unified Interface Adoption
**What exists**: `utils/models/unified_model_interface.py` (817 lines) - A complete factory-based model loading system.
**Adoption rate by engine**:
```
✅ UNIFIED (clean implementations):
- ChatterBox Official 23-Lang (100% using load_tts_model)
- Higgs Audio (stateless wrapper integration working)
- RVC (4 factory types: vc, hubert, separator_mdx, separator_demucs)
- VibeVoice (engine initialization pattern)
- IndexTTS-2 (path resolution + auto-download)
⚠️ DUAL IMPLEMENTATIONS (fallback to legacy):
- ChatterBox (tries unified → falls back to manager.load_tts_model())
- F5-TTS (tries unified → falls back to SmartModelLoader)
```
**Reality**: 85% coverage looks good, but the 15% with fallbacks creates two code paths, making debugging harder.
### 1.2 What's Working Well
✅ **ComfyUI Integration**: Models are properly registered with ComfyUI's VRAM management. The wrapper system (comfyui_model_wrapper/) correctly handles model lifecycle.
✅ **Factory Pattern**: Extensible design - adding new engines requires registering a factory. New engines (Higgs, RVC, VibeVoice) implemented cleanly.
✅ **ChatterBox Fallback Logic**: Sophisticated fallback chain:
```
Try provided path → Try local language model → Try HuggingFace →
Try German fallback → Final fallback to English
```
This is well-thought-out and prevents total failures.
✅ **Language-Aware Caching**: ChatterBox Official 23-Lang has special case to NOT include language in cache key (multilingual model special case).
✅ **Force Reload**: System properly handles force_reload flag for model reloading.
---
## PART 2: CRITICAL ISSUES IDENTIFIED
### Issue #1: "auto" Device Instability (SEVERITY: HIGH)
**Problem**: Device resolution for "auto" happens in multiple places with NO caching:
- `torch_device_resolver.py` (resolve_torch_device)
- `base_node.py` (resolve_device)
- `smart_loader.py` (internal resolution)
- `unified_model_interface.py` (factory level)
**The Bug**:
```python
# Call 1: CUDA available
device_key = generate_cache_key(..., device="auto") # "auto" stored in key
model = load_model(device="auto") # Resolves to "cuda"
# ComfyUI clears VRAM, CUDA unavailable
# Call 2: CUDA unavailable
device_key = generate_cache_key(..., device="auto") # Still "auto"
model = load_model(device="auto") # Now resolves to "cpu"
# CACHE MISS - model reloaded unnecessarily!
```
**Impact**:
- Unnecessary model reloads during long ComfyUI sessions
- VRAM thrashing if GPU memory pressure forces CPU fallback
- Corrupts caching logic for "auto" device selection
**Root Cause**: Cache keys use the STRING "auto" instead of resolved device name.
---
### Issue #2: Triple Caching System with No Sync (SEVERITY: CRITICAL)
**Three separate caches exist**:
1. **UnifiedModelInterface** (via ComfyUI model manager)
- Cache keys: `"chatterbox_tts_cuda_English_path:model"`
- String concatenation based
2. **SmartModelLoader**
- Cache keys: `md5hash("chatterbox_tts_English_cuda")[:16]`
- MD5 hash based
- Used as fallback by F5-TTS and ChatterBox managers
3. **Manager Classes** (manager.py, f5tts_manager.py)
- Manual dictionaries
- Different key formats per manager
**The Problem**:
```
Model loaded → Cached in Unified → Also in SmartLoader → Also in Manager
User clears VRAM → Only UnifiedModelInterface clears → Model still in other caches
Next load → Stale model returned from SmartLoader cache → Device mismatch
```
**Impact**:
- Models can be loaded multiple times while already cached
- Cache clearing commands don't fully clear models
- Memory leaks possible if stale models accumulate
- Confusing behavior for users clearing VRAM between inferences
---
### Issue #3: Device Type Mismatch (SEVERITY: MEDIUM)
**Two different device representations**:
```python
# Unified interface uses: strings
target_device = resolve_torch_device(config.device) # Returns "cuda"
# ComfyUI wrapper uses: torch.device objects
self.device = torch.device("cuda") # torch.device object
```
**The comparison fails**:
```python
if wrapper.current_device != target_device: # "cuda" != torch.device("cuda")
# Type mismatch - comparison always True even if same device!
```
**Impact**:
- Device switch detection broken
- Models might not move to correct device
- Silent failures (model stays on old device)
---
### Issue #4: Cache Key Collision Risk (SEVERITY: MEDIUM)
**Current logic** (unified_model_interface.py, line 221):
```python
if config.model_path:
components.append(f"path:{Path(config.model_path).name}")
```
Uses basename only, not full path:
```
/models/TTS/chatterbox/English/ → cache component: "path:English"
/backup/models/chatterbox/English/ → cache component: "path:English"
# COLLISION!
```
**Impact**:
- Two different model directories with same name → same cache key
- Randomly returns one or the other
- Non-deterministic behavior
- Difficult to debug
---
### Issue #5: Inconsistent Factory Signatures (SEVERITY: HIGH)
**Parameter inconsistency**:
| Factory | device | model_name | language | model_path | additional_params |
|---------|--------|-----------|----------|-----------|------------------|
| ChatterBox | ✅ | ✅ | ✅ | ✅ | ✅ |
| F5-TTS | ✅ | ✅ | ⚠️ (ignored) | ✅ | ❌ |
| Higgs Audio | ✅ | ✅ | ❌ | ✅ | ✅ |
| RVC | ✅ | ❌ | ❌ | ✅ | ❌ |
| VibeVoice | ✅ | ✅ | ❌ | ❌ | ✅ |
| IndexTTS | ✅ | ❌ | ❌ | ✅ | ✅ |
**Problem**: Each factory has different parameter set. Adding new parameter requires updating individual factories.
**Also**: Default values inconsistent (Higgs Audio defaults device="cuda" while others use "auto").
---
### Issue #6: Five Different Error Handling Patterns (SEVERITY: MEDIUM)
```python
# Pattern 1: ChatterBox - Raises RuntimeError
raise RuntimeError("ChatterboxTTS not available - check installation")
# Pattern 2: F5-TTS - Delegates to SmartModelLoader (different exceptions)
model, _ = smart_model_loader.load_model_if_needed(...)
# Pattern 3: VibeVoice - Soft failure (prints, continues)
except ImportError as e:
print(f"⚠️ Unified Interface: accelerate not available: {e}")
# Pattern 4: RVC - Direct ImportError
from engines.rvc.impl.vc_infer_pipeline import get_vc
# Pattern 5: IndexTTS - Custom ImportError message
raise ImportError(f"IndexTTS-2 dependencies not available. Error: {e}")
```
**Impact**:
- Callers can't predict exception types
- Try/except blocks can't handle all cases
- Different behaviors across engines for same failure scenario
---
### Issue #7: Device Resolution Duplication (SEVERITY: MEDIUM)
**Multiple device resolution functions with different logic**:
1. **torch_device_resolver.py** (authoritative):
```python
def resolve_torch_device(device: str) -> str:
if device == "auto":
if torch.backends.mps.is_available(): # ← Checks MPS
return "mps"
elif torch.cuda.is_available():
return "cuda"
```
2. **base_node.py** (partial):
```python
def resolve_device(self, device: str) -> str:
if device == "auto":
return "cuda" if torch.cuda.is_available() else "cpu"
# ← MISSING MPS and XPU checks!
```
**Impact**:
- Different code paths return different devices
- MPS (Apple Silicon) users get CPU fallback in some cases
- Inconsistent behavior across node types
---
## PART 3: ROOT CAUSE ANALYSIS
Why did this happen?
1. **Evolutionary Development**: Unified interface added later, legacy systems still working → coexistence strategy chosen
2. **Engine Diversity**: Each engine (ChatterBox, F5-TTS, Higgs, RVC) has different requirements → factories handle differently
3. **Lack of Specification**: No defined contract for what a factory must implement
4. **Incremental Adoption**: New engines use unified interface, but integration with old engines takes time
**None of this is "bad code"** - it's a natural architecture that emerges when adding new systems to existing ones.
---
## PART 4: DOES THIS NEED TO BE FIXED?
### Short Answer
**For users**: System works. All engines load, all models work.
**For maintainers**: Accumulating technical debt that will cause problems as more engines are added.
### The Real Risk
The biggest risk is **in future maintenance**:
```
Every new engine added will need:
- Custom factory implementation
- Custom error handling
- Possibly custom device logic
- Possibly custom recovery code (for Higgs-like corruption issues)
This complexity INCREASES with more engines.
```
**Current**: 7 engines, manageable
**In 1 year**: 10-15 engines likely, architecture breaks down
### Scenarios Where Current Issues Cause Real Problems
1. **User with GPU+CPU hybrid setup** (or laptop with power management):
- Device shifts from CUDA to CPU unexpectedly
- Models reload incorrectly
- Performance degrades mysteriously
2. **ComfyUI with tight VRAM budget**:
- Clear VRAM → device resolves to CPU
- Cache misses → reloads in CPU VRAM
- Compounds memory pressure
3. **Adding new engine similar to Higgs** (requires special recovery):
- Need to modify model_manager.py with new engine-specific code
- Risk breaking existing engines' recovery logic
4. **Long-running generation sessions**:
- Triple cache accumulation
- Memory leaks from stale cached models
- Slow degradation over 8+ hours
---
## PART 5: IMPROVEMENT RECOMMENDATIONS
### Tier 1: Quick Wins (1-2 hours, Low Risk)
These can be implemented immediately with minimal testing:
#### 1.1 Fix "auto" Device Instability
**Change**: Cache resolved device in cache key, not "auto" string
```python
# Before: Cache key includes "auto"
cache_key = f"chatterbox_tts_auto"
# After: Resolve first
resolved = resolve_torch_device(device)
cache_key = f"chatterbox_tts_{resolved}"
```
**Impact**: Eliminates unnecessary model reloads on device changes
**Risk**: Very low - just earlier resolution
#### 1.2 Fix Device Comparison
**Change**: Resolve device on both sides before comparison
```python
# Before
if wrapper.current_device != target_device: # String vs torch.device
# After
from utils.device import resolve_torch_device
target_resolved = resolve_torch_device(config.device)
actual_resolved = resolve_torch_device(wrapper.current_device)
if target_resolved != actual_resolved:
```
**Impact**: Device movement actually works
**Risk**: Very low - just standardization
#### 1.3 Consolidate Device Resolution
**Change**: Have single `resolve_torch_device()` function, use everywhere
**Current**: base_node.py has own resolve_device() missing MPS/XPU checks
**Fix**: Replace all `resolve_device()` calls with `resolve_torch_device()`
**Impact**: Consistent behavior across all components
**Risk**: Low - just replacing with more complete function
---
### Tier 2: Medium Effort (4-6 hours, Medium Risk)
These require more testing but are valuable:
#### 2.1 Unify Caching System
**Change**: Remove SmartModelLoader and manager-specific caches, route through unified interface
**Current architecture**:
```
ComfyUI Manager ←─┐
SmartModelLoader ├─ THREE SYSTEMS (no sync)
Manager dicts ────┘
```
**After**:
```
All caches → ComfyUI Manager (single source of truth)
```
**Benefit**:
- Cache clearing actually clears everything
- No model duplication across caches
- Simpler debugging
**Risk**: Medium - must ensure all code paths work through single system
#### 2.2 Fix Cache Key Collision
**Change**: Hash full path, not just basename
```python
# Before
components.append(f"path:{Path(config.model_path).name}")
# After
import hashlib
path_hash = hashlib.md5(str(config.model_path).encode()).hexdigest()[:8]
components.append(f"path:{path_hash}")
```
**Impact**: Eliminates collision risk
**Risk**: Low - just better hashing
#### 2.3 Standardize Error Handling
**Change**: Create base exception class, use everywhere
```python
class TTSModelLoadingError(Exception):
"""Base exception for TTS model loading failures"""
pass
class TTSModelNotFoundError(TTSModelLoadingError):
pass
class TTSDeviceError(TTSModelLoadingError):
pass
```
Update all factories to raise consistent exceptions.
**Impact**: Callers can reliably catch and handle errors
**Risk**: Low - just standardization
---
### Tier 3: Major Refactor (2-3 days, High Risk)
These provide architectural improvements but require extensive testing:
#### 3.1 Standardize Factory Signatures
**Change**: Define common parameter interface
```python
@dataclass
class FactoryParameters:
"""Standard parameters all factories accept"""
device: str # Required
model_name: str # Required
language: Optional[str] = None # Optional
model_path: Optional[str] = None # Optional
repo_id: Optional[str] = None # Optional
additional_params: Optional[Dict] = None # For engine-specific
```
Each factory receives this dataclass, extracts needed fields.
**Benefit**:
- Adding new factories is simpler
- No parameter confusion
- Extensible without modifying unified interface
**Risk**: High - changes all factories, needs full testing
#### 3.2 Engine Capability Registry
**Change**: Instead of hardcoding recovery logic, engines register capabilities
```python
ENGINE_CAPABILITIES = {
"higgs_audio": {
"supports_cuda_graphs": True,
"recovery_handler": reset_higgs_cuda_graphs,
"can_corrupt": True
},
"f5tts": {
"supports_cuda_graphs": False,
"recovery_handler": None,
"can_corrupt": False
}
}
```
Generic recovery logic uses registry.
**Benefit**:
- Adding new engine with special needs is data-driven, not code-driven
- model_manager.py doesn't need modification for new engines
- Scales to 20+ engines
**Risk**: High - changes error recovery pattern, needs extensive testing
#### 3.3 Unified Fallback Handler
**Change**: Extract ChatterBox's fallback logic to generic utility
ChatterBox has sophisticated fallback chain (local → HF → fallback language). This is valuable for any engine.
```python
def intelligent_model_loader(
primary_loader: Callable,
fallback_languages: List[str],
final_fallback: Callable
) -> Any:
"""Generic intelligent loader with fallback chain"""
try:
return primary_loader()
except ImportError as e:
if "401" in str(e):
# Handle gated models
pass
# Try fallback languages
for lang in fallback_languages:
try:
return primary_loader_with_lang(lang)
except:
continue
# Final fallback
return final_fallback()
```
**Benefit**:
- Other engines get robust fallback behavior
- Consistent error recovery across all engines
- ChatterBox logic is maintainable, not scattered
**Risk**: Medium - fallback patterns must be generic enough
---
## PART 6: IMPLEMENTATION PRIORITY MATRIX
```
RISK ↑
│
H │ Tier 3.1 (Factory) Tier 3.2 (Registry) Tier 3.3 (Fallback)
│ Signature Engine Capabilities Unified Handler
│ [2-3 days] [2-3 days] [1-2 days]
│
M │ Tier 2.1 (Cache Unify) Tier 2.2 (Key Hash) Tier 2.3 (Errors)
│ [4-6 hours] [2 hours] [2 hours]
│
L │ Tier 1.1-1.3
│ Device & Resolution
│ [1-2 hours]
│
└──────────────────────────────────────────────────────────→ EFFORT/REWARD
RECOMMENDATION ORDER:
1. Tier 1 (all three) - Highest ROI, lowest risk
2. Tier 2.1 - Unify caching (critical for maintenance)
3. Tier 2.2, 2.3 - Polish improvements
4. Tier 3 - Only if planning major expansion (10+ more engines)
```
---
## PART 7: RECOMMENDATION FOR THIS PROJECT
### Do you need to fix this?
**For current state (7 engines)**: NO
- System works without errors
- Legacy fallbacks cover edge cases
- Not blocking any functionality
**For future (planning to add 10+ engines)**: YES
- Complexity compounds with each engine
- New contributors will struggle with inconsistent patterns
- Technical debt becomes operational risk
### Suggested Approach
**Phase 1** (Next 1-2 weeks): Implement Tier 1 fixes
- Solves "auto" device and comparison issues
- Takes ~2 hours
- Zero risk
**Phase 2** (Optional, Next month): Implement Tier 2.1
- Unifies caching
- Removes triple-system confusion
- Takes ~6 hours
- Requires testing
**Phase 3** (Only if adding multiple new engines): Implement Tier 3
- Registers as architectural upgrade
- Plan it as part of new engine addition
- Amortize effort across multiple additions
### Decision Point
**Ask yourself**:
1. Are you planning to add 10+ engines in the next 2 years? → Do Tier 3
2. Is cache management causing issues in user reports? → Do Tier 2.1
3. Is device handling causing issues? → Do Tier 1 immediately
If answers are NO, NO, NO → **Current system is fine. Monitor for issues.**
---
## PART 8: SPECIFIC FILE CHANGES SUMMARY
### Files to Modify (Tier 1)
1. **utils/device/torch_device_resolver.py**
- Add caching for "auto" resolution
- 10-15 lines changed
2. **utils/models/unified_model_interface.py**
- Use resolved device in cache keys (line 259)
- Fix device comparison (line 180)
- 5-10 lines changed
3. **nodes/base/base_node.py**
- Remove resolve_device() method, use torch_device_resolver
- 3-5 lines changed
### Files to Modify (Tier 2.1)
1. **utils/models/comfyui_model_wrapper/model_manager.py**
- Remove engine-specific recovery code
- Add generic recovery registration
2. **utils/models/smart_loader.py**
- Deprecate in favor of unified interface
- Route through ComfyUI manager
3. **utils/models/manager.py**
- Remove fallback to SmartModelLoader
- Always use unified interface
---
## CONCLUSION
**The system is architecturally sound for current scope** (7 engines), but accumulated technical debt will become problematic at larger scale.
**Recommended action**:
- Implement Tier 1 fixes now (low effort, immediate benefit)
- Monitor for real-world issues
- Plan Tier 2.1 if caching becomes problematic
- Consider Tier 3 if project adds 10+ engines
The improvements would take a skilled developer 2-3 weeks for full implementation, spread across phases. Each phase independently provides value.
---
**Report prepared by**: Claude Code Analysis
**Analysis method**: Deep codebase examination + cross-reference testing
**Confidence level**: High (based on 633 lines of detailed code analysis)
@@ -0,0 +1,87 @@
# SRT Edit Tag Batch Processing Pattern
## Problem
Edit post-processing was happening per-subtitle, causing the editing engine to load/unload multiple times per SRT generation, wasting time and VRAM.
## Solution
Batch all edit processing at the END of SRT generation, after ALL subtitles are generated.
## Implementation Pattern
### 1. Adapter Layer (e.g., `vibevoice_adapter.py`)
In `generate_with_pause_tags()`:
- Extract edit tags even when NO pause tags present
- Return segment dict format `[{waveform, text, edit_tags, ...}]` when edit tags exist
- Return plain tensor when NO edit tags (backwards compatibility)
### 2. Regular Processor (e.g., `step_audio_editx_processor.py`)
In `_process_character_switching()`:
```python
# Apply edit post-processing ONLY when NOT in SRT mode
if not self._srt_mode:
audio_segments = apply_edit_post_processing(
audio_segments,
self.config,
pre_loaded_engine=self.adapter.engine
)
```
### 3. SRT Processor (e.g., `vibevoice_srt_processor.py`, `step_audio_editx_srt_processor.py`)
In `_process_all_subtitles()`:
```python
# Initialize collection for batch processing
all_segments_for_editing = []
# Loop through subtitles
for i, subtitle in enumerate(subtitles):
segments = generate_subtitle(subtitle)
# Check if segments have edit tags
if isinstance(segments, list) or has_edit_tags(segments):
# Tag with subtitle index for later reassembly
for seg in segments:
seg['subtitle_index'] = i
all_segments_for_editing.extend(segments)
audio_segments[i] = None # Placeholder
else:
# No edit tags - store directly
audio_segments[i] = combine_segments(segments)
# BATCH PROCESS all edits at once
if all_segments_for_editing:
print(f"🎨 Applying edit post-processing to {len(all_segments_for_editing)} segment(s) from all subtitles...")
processed = apply_edit_post_processing(
all_segments_for_editing,
config,
pre_loaded_engine=engine
)
# Group back by subtitle_index
segments_by_subtitle = group_by_subtitle_index(processed)
# Combine and store
for sub_idx, segs in segments_by_subtitle.items():
audio_segments[sub_idx] = combine_segments(segs)
```
### 4. Subtitle Processing Functions
In functions like `_process_custom_character_switching_subtitle_with_params()`:
- Return segment dicts when edit tags present (don't apply edits)
- Return plain tensor when no edit tags
- Let caller handle batch processing
## Benefits
- Edit engine loads ONCE instead of once per subtitle
- Faster SRT generation
- Less VRAM thrashing
- Same quality output
## Engines Implemented
- ✅ VibeVoice SRT
- ✅ Step Audio EditX SRT
- ✅ Higgs Audio SRT
## TODO
- ChatterBox 23-Lang (needs edit tag support first)
@@ -0,0 +1,464 @@
# Step Audio EditX Engine Implementation Plan
## Executive Summary
**Implementation Approach**: Bundle Step Audio EditX code (like IndexTTS pattern) with TTS Suite integration
**Key Decisions:**
- ✅ **Bundle code** in `step_audio_editx_impl/` (NOT pip install)
- ✅ **Skip Whisper** dependency (only needed for external audio transcription)
- ✅ **Unpinned transformers** (patch if needed via TTS Suite system)
- ✅ **Progress bars** via ComfyUI (UX improvement)
- ✅ **Manual transcripts** for Audio Editor (Whisper integration later)
- ✅ **Phased implementation**: Clone → Edit → SRT
**What Step Audio EditX Is:**
- 3B parameter QWen-based LLM for semantic generation
- Custom dual-codebook tokenizer (VQ02 + VQ06)
- CosyVoice vocoder for final audio generation (NOT full CosyVoice)
- Unique emotion/style/speed editing capabilities
- Worth implementing alongside future CosyVoice engine
---
## Reference Implementations
**Local copies available for reference:**
- **Original (stepfun-ai)**: `IgnoredForGitHubDocs/For_reference/Step-Audio-EditX/`
- **Saganaki22's ComfyUI**: `IgnoredForGitHubDocs/For_reference/ComfyUI-Step_Audio_EditX_TTS/`
---
## Analysis of Both Implementations
### Original Step Audio EditX (stepfun-ai)
- **Core Features**:
- Zero-shot voice cloning (3-30s reference audio)
- Audio editing: emotion, style, speed, paralinguistics
- Supports Mandarin, English, Sichuanese, Cantonese, Japanese, Korean
- Iterative editing (n_edit_iter parameter)
- Denoise and VAD
- Polyphone pronunciation control
- 3B parameter model with dual-codebook tokenizer
- **Architecture**:
- `tokenizer.py` (StepAudioTokenizer) - Dual-codebook tokenizer
- `tts.py` (StepAudioTTS) - Main TTS engine with clone() and edit() methods
- `model_loader.py` - Model loading from HuggingFace/ModelScope/local
- `config/edit_config.py` - Edit type configurations
- Uses bundled `funasr_detach/` for FunASR models (paraformer tokenizer)
- CosyVoice vocoder for audio generation (24kHz output)
### Saganaki22's ComfyUI Implementation
- **Native Features**: All of the above
- **Custom Additions (NON-NATIVE)**:
1. **Longform chunking system** (`core/longform_chunker.py`):
- Splits long text at sentence boundaries
- Estimates tokens (1 token ≈ 4 chars)
- Creates overlapping chunks
- This is a ComfyUI-specific optimization NOT in original
2. **Audio stitching** (`core/audio_stitcher.py`):
- Concatenates audio chunks from longform generation
- Crossfade support
- NOT part of original implementation
3. **Progress bar integration**:
- ComfyUI progress tracking during clone generation
- NOT in original (original has no progress callbacks)
4. **Model wrapper architecture**:
- `StepAudioModelWrapper` with to_cpu()/to_cuda() methods
- CUDA graph cleanup logic
- More sophisticated VRAM management than original
**CRITICAL**: Speed control (faster/slower) IS native - it's in the original edit_config.py, so this is NOT a custom addition.
---
## Recommended Implementation Approach
### Use Original as Base (NOT Saganaki22)
Saganaki22's implementation is excellent for ComfyUI but adds non-native features. Since you want faithful implementation, we start fresh.
### Phase 1: Core Engine Implementation
#### Files to Create:
1. **`engines/step_audio_editx/step_audio_editx.py`**
- Main engine wrapper
- Wraps original `StepAudioTTS` class
- Implements `.to()` method for ComfyUI VRAM management
- Device checking before generation
- Sample rate: 24000 Hz (CosyVoice native)
2. **`engines/step_audio_editx/step_audio_downloader.py`**
- Uses `utils/downloads/unified_downloader.py`
- Downloads Step-Audio-EditX and Step-Audio-Tokenizer
- Organized structure: `models/TTS/step_audio_editx/`
3. **`engines/step_audio_editx/__init__.py`**
- Engine initialization
- Imports
4. **`engines/step_audio_editx/original_impl/`**
- Copy entire original Step Audio EditX implementation here
- Includes: `tts.py`, `tokenizer.py`, `model_loader.py`, `config/`, `funasr_detach/`, etc.
- Keep it isolated so it doesn't conflict with existing dependencies
#### Key Implementation Notes:
**Audio Format**:
- Original outputs: `torch.Tensor` shape `[samples]` or `[1, samples]`
- Sample rate: 24000 Hz
- Return format: TTS Audio Suite expects `[1, samples]`
**Generation Methods**:
```python
def clone(self, prompt_wav_path, prompt_text, target_text, temperature=0.7, do_sample=True, max_new_tokens=4096):
# Call original implementation
pass
def edit(self, input_audio_path, audio_text, edit_type, edit_info=None, text=None):
# Edit mode - temperature/do_sample/max_new_tokens are HARDCODED in original
# Parameters: edit_type (emotion/style/speed/paralinguistic/denoising)
# Returns audio_tensor, sample_rate
pass
```
**VRAM Management** (CRITICAL):
- Implement `.to(device)` method
- Move `self.llm` (3B parameter model) between devices
- Move `self.cosy_model` (vocoder) between devices
- Check device before generation and reload if needed
- Follow patterns from IndexTTS implementation
---
### Phase 2: Node Implementation
#### Files to Create:
1. **`nodes/engines/step_audio_editx_engine_node.py`**
- Configuration node for Step Audio EditX
- Parameters:
- `model_path` - path to Step-Audio-EditX
- `device` - cuda/cpu
- `torch_dtype` - bfloat16/float16/float32
- `quantization` - none/int4/int8
- `attention_mechanism` - sdpa/eager/flash_attn
- Returns: ENGINE config
2. **`nodes/step_audio_editx/step_audio_editx_processor.py`**
- Main TTS processor (internal, used by Unified TTS Text node)
- Handles:
- Character switching with `[CharacterName]` tags
- Pause tags with `[pause:1.5s]` syntax
- Caching via `utils/audio/cache.py`
- Voice discovery via `utils/voice/discovery.py`
- **Longform chunking via `utils/audio/chunk_combiner.py`** (TTS Suite unified)
- Sequential generation with interrupt handling
3. **`nodes/step_audio_editx_special/step_audio_editx_audio_editor_node.py`** (Special Feature)
- Dedicated "🎨 Step Audio EditX - Audio Editor" node
- This is NOT voice conversion - it's specialized audio manipulation
- **CRITICAL**: Requires `audio_text` parameter (transcript of input audio)
- User must provide transcript manually (no Whisper transcription)
- Future: When Whisper engine is added, can auto-feed transcripts
- Parameters:
- `input_audio` - audio to edit (0.5-30s limit)
- `audio_text` - REQUIRED transcript of input audio
- `edit_type` - emotion/style/speed/paralinguistic/denoising
- `emotion` - happy/sad/angry/excited/etc (14 options)
- `style` - whisper/serious/child/older/etc (32 options)
- `speed` - faster/slower/more faster/more slower
- `paralinguistic` - [Laughter]/[Breathing]/[Sigh]/etc (10 options)
- `denoising` - denoise/vad
- `n_edit_iterations` - 1-5 (strength through iteration)
- `paralinguistic_text` - where to insert effect
- Returns edited audio
---
### Phase 3: Unified Systems Integration
#### Integration Points:
1. **Unified Model Loading**:
```python
from utils.models.unified_model_interface import UnifiedModelInterface
# Register Step Audio EditX
unified_model_interface.register_model(
engine_type="step_audio_editx",
model=step_audio_engine,
model_info=ModelInfo(...)
)
```
2. **Character System**:
- Use `utils/text/character_parser.py` for `[CharacterName]` tags
- Use `utils/voice/discovery.py` for voice file loading
- Support both .wav and .txt voice references
3. **Pause Tags**:
- Use `utils/text/pause_processor.py` for `[pause:xx]` syntax
- Integrate with audio generation pipeline
4. **Caching**:
```python
from utils.audio/cache import UnifiedCacheManager
from utils.audio.audio_hash import create_content_hash
cache_key = create_content_hash(
text=text,
voice_path=voice_path,
engine="step_audio_editx",
parameters={...}
)
```
5. **Interrupt Handling** (CRITICAL):
```python
import comfy.model_management as model_management
# Before each generation
if model_management.interrupt_processing:
raise InterruptedError("Step Audio EditX generation interrupted")
```
---
### Phase 4: Dependencies
#### Add to `requirements.txt`:
```txt
# Step Audio EditX dependencies
librosa>=0.10.2
soundfile>=0.12.1 # Critical for ComfyUI audio preview
onnxruntime>=1.17.0
transformers>=4.30.0 # Unpinned - use TTS Suite transformer patches if needed
sentencepiece>=0.1.99
omegaconf>=2.3.0
hyperpyyaml>=1.2.2
# NOTE: openai-whisper NOT required for TTS (only for external audio transcription)
# Future: When Whisper transcription engine is added, it can feed Audio Editor node
```
**Dependency Strategy:**
- ✅ **Bundle code** (like IndexTTS) - not pip installable
- ✅ **Skip Whisper** - only needed for external audio → edit workflow
- ✅ **Unpinned transformers** - patch via TTS Suite system if 4.54+ breaks
- ✅ **Windows-friendly** - no complex model downloads for basic TTS
---
### Phase 5: SRT Implementation (Later)
**Strategy**: Sequential processing with language grouping
- Group SRT segments by language (Mandarin/English/Japanese/Korean/etc)
- Process each language group sequentially
- No parallel processing (model doesn't support it well)
- Support character switching within segments
- Use timing modes from `utils/timing/`
**File**: `nodes/step_audio_editx/step_audio_editx_srt_processor.py`
---
## Key Differences from Saganaki22 Implementation
| Feature | Original | Saganaki22 | Our Implementation |
|---------|----------|------------|-------------------|
| Longform chunking | ❌ No | ✅ Yes (custom) | ✅ Yes (TTS Suite unified) |
| Progress bars | ❌ No | ✅ Yes (clone only) | ✅ Yes (ComfyUI standard) |
| Whisper transcription | ⚠️ Optional (ASR) | ⚠️ Optional (ASR) | ❌ Skip (future: Whisper engine) |
| Audio stitching | ❌ No | ✅ Yes (custom) | ❌ No (not native) |
| Iterative editing | ✅ Yes | ✅ Yes | ✅ Yes (native) |
| Speed control | ✅ Yes | ✅ Yes | ✅ Yes (native) |
| Character switching | ❌ No | ❌ No | ✅ Yes (TTS Suite feature) |
| Pause tags | ❌ No | ❌ No | ✅ Yes (TTS Suite feature) |
| Caching | ❌ No | ❌ No | ✅ Yes (TTS Suite feature) |
| Interrupt handling | ❌ No | ❌ No | ✅ Yes (TTS Suite feature) |
---
## Critical Implementation Requirements
### 1. VRAM Management (from guide lines 96-264)
- Implement `.to(device)` method
- Move LLM (self.llm) and vocoder (self.cosy_model) between devices
- Check device before generation
- Reload from CPU to CUDA if offloaded
- Search for existing wrapper before creating new ones
### 2. Clear VRAM Integration
- Register with ComfyUI model management
- Support "Clear VRAM" button functionality
- Prevent device mismatch errors
### 3. Interrupt Handling (CRITICAL)
- Check `model_management.interrupt_processing` before each generation
- In SRT mode: check before each segment
- Allow users to cancel long operations
### 4. Audio Limitations
- **Clone mode**: No strict length limit (depends on max_new_tokens)
- **Edit mode**: 0.5-30 seconds HARD LIMIT
- Validate audio duration before processing
### 5. Language Support
- Mandarin (primary)
- English
- Sichuanese dialect
- Cantonese dialect
- Japanese
- Korean
- Add `[Sichuanese]`, `[Cantonese]`, `[Japanese]`, `[Korean]` tag support
### 6. Edit Types and Options
All options are from original `config/edit_config.py`:
**Emotions** (14):
- happy, sad, angry, excited, calm, fearful, surprised, disgusted
- confusion, empathy, embarrass, depressed, coldness, admiration
**Styles** (32):
- whisper, serious, child, older, girl, pure, sister, sweet
- exaggerated, ethereal, generous, recite, act_coy, warm, shy
- comfort, authority, chat, radio, soulful, gentle, story, vivid
- program, news, advertising, roar, murmur, shout, deeply, loudly
- arrogant, friendly
**Speed** (4):
- faster, slower, more faster, more slower
**Paralinguistics** (10):
- [Breathing], [Laughter], [Surprise-oh], [Confirmation-en]
- [Uhm], [Surprise-ah], [Surprise-wa], [Sigh]
- [Question-ei], [Dissatisfaction-hnn]
**Denoising** (2):
- denoise, vad
---
## File Structure
```
engines/step_audio_editx/
├── __init__.py
├── step_audio_editx.py (main wrapper - follows IndexTTS pattern)
├── step_audio_editx_downloader.py (unified downloader integration)
└── step_audio_editx_impl/ (bundled original codebase - like IndexTTS)
├── tts.py (StepAudioTTS - main engine)
├── tokenizer.py (StepAudioTokenizer - dual-codebook)
├── model_loader.py (ModelSource, model_loader)
├── config/
│ ├── edit_config.py (edit types/options)
│ └── prompts.py
├── funasr_detach/ (bundled FunASR - custom fork)
└── stepvocoder/cosyvoice2/ (CosyVoice vocoder)
nodes/engines/
└── step_audio_editx_engine_node.py (config node)
nodes/step_audio_editx/
├── step_audio_editx_processor.py (TTS processor - internal)
└── step_audio_editx_srt_processor.py (SRT processor - internal, later)
nodes/step_audio_editx_special/
└── step_audio_editx_audio_editor_node.py (🎨 Audio Editor - specialized manipulation)
```
---
## Testing Checklist
### Phase 1 Testing (Basic TTS):
- [ ] Model downloads correctly
- [ ] Basic clone generation works
- [ ] Audio format is correct (24kHz, mono)
- [ ] Character switching works
- [ ] Pause tags work
- [ ] Caching works
- [ ] VRAM management works (Clear VRAM button)
- [ ] Device reload works after offload
- [ ] Interrupt handling works
### Phase 2 Testing (Audio Editing):
- [ ] Emotion editing works (all 14 emotions)
- [ ] Style editing works (all 32 styles)
- [ ] Speed editing works (4 speed levels)
- [ ] Paralinguistic editing works (all 10 effects)
- [ ] Denoising works (denoise + vad)
- [ ] Iterative editing works (n_edit_iter 1-5)
- [ ] Audio length validation works (0.5-30s)
- [ ] Edit node integrates with voice changer workflows
### Phase 3 Testing (SRT Mode):
- [ ] SRT parsing works
- [ ] Language grouping works
- [ ] Character switching in SRT works
- [ ] Timing assembly works
- [ ] Multiple languages in same SRT works
- [ ] Interrupt handling in SRT works
---
## Questions for User
Before we proceed with implementation, I need clarification on:
1. **Longform chunking**: Saganaki22 added sentence-based text chunking for long content. This is NOT in the original. Do you want this feature, or should we stick to the original behavior (which may truncate long text at max_new_tokens)?
2. **Progress bars**: Saganaki22 added ComfyUI progress tracking during clone generation. This is NOT in the original. Should we add this for better UX, or skip it?
3. **Separate edit node**: Should we create a dedicated "Step Audio EditX - Edit" node for audio editing (emotion/style/speed), or integrate everything through the unified voice changer node?
4. **Model location**: Should models go in `models/TTS/step_audio_editx/` or `models/step_audio_editx/` (Saganaki22 used `models/Step-Audio-EditX/`)?
5. **Implementation priority**: What should we implement first?
- Option A: Clone mode only (basic TTS) → test → then edit mode → then SRT
- Option B: Clone + Edit modes together → test → then SRT
- Option C: All features at once
---
## Implementation Strategy (User Approved)
**Phase 1**: Clone mode with TTS Suite integration
- Full TTS Suite integration (character switching, pause tags, caching)
- Use `utils/audio/chunk_combiner.py` for longform text
- **Progress bars** via `comfy.utils.ProgressBar` during generation
- VRAM management with `.to()` method (follow IndexTTS pattern)
- Interrupt handling via `model_management.interrupt_processing`
- Bundle code in `step_audio_editx_impl/` (like IndexTTS)
- Test thoroughly before proceeding
**Phase 2**: Audio Editor (specialized node)
- Dedicated "🎨 Step Audio EditX - Audio Editor" node
- NOT integrated with voice changer (different purpose)
- **Requires manual transcript input** (audio_text parameter)
- Skip Whisper transcription for now
- Future: Auto-feed from Whisper transcription engine
- All editing capabilities (emotion/style/speed/paralinguistic/denoising)
- Iterative refinement support (n_edit_iter 1-5)
- Audio length validation (0.5-30s)
- Test all editing modes
**Phase 3**: SRT support
- Sequential processing with language grouping
- Character switching within segments
- Timing assembly
- Interrupt handling for cancellation
This approach:
- Allows incremental testing
- Validates VRAM management early
- Ensures unified systems work correctly
- Follows TTS Suite best practices
- Lets you test and approve each phase
@@ -0,0 +1,557 @@
# Step Audio EditX Inline Tags - Implementation Plan
## Overview
Add inline Step Audio EditX edit tags to the segment parameter system, enabling automatic post-processing edits on TTS-generated audio from ANY engine.
**Example:**
```
[Alice|seed:43] This is an example <Laughter:3>, I'm funny!
```
Result:
1. TTS generates "This is an example, I'm funny!" (tag stripped)
2. Step Audio EditX automatically edits the segment with paralinguistic `<Laughter>` in 3 iterations
## Syntax Design
### Current Segment Parameter Syntax
```
[Character|param:value|param2:value2]
```
### New Edit Tag Syntax (inline in text)
**Paralinguistic tags** (insert non-verbal sounds):
```
Hello <Laughter> world # Default 1 iteration
Hello <Laughter:3> world # 3 iterations
Hello <Sigh:2> goodbye # 2 iterations
```
**Other edit types** - need different syntax to avoid confusion:
Option A - Angle brackets with type prefix:
```
<emotion:happy:2> # emotion=happy, 2 iterations
<style:whisper:3> # style=whisper, 3 iterations
<speed:faster:1> # speed=faster, 1 iteration
```
Option B - Curly braces for non-paralinguistic:
```
{emotion:happy:2}
{style:whisper:3}
{speed:faster:1}
```
Option C - Unified angle brackets, context determines type:
```
<happy:2> # detected as emotion
<whisper:3> # detected as style
<faster:1> # detected as speed
<Laughter:2> # detected as paralinguistic (capitalized)
```
**Recommendation:** Option A - explicit type prefix. Clear, unambiguous, no magic detection.
### Multiple Tags on Same Segment
**Two syntax options for multiple edits:**
1. **Pipe-separated in single tag** (like segment parameters):
```
[Alice] Hello world! <Laughter:2|style:whisper:1|emotion:happy>
```
2. **Multiple separate tags:**
```
[Alice] Hello world! <Laughter:2><style:whisper:1><emotion:happy>
```
Both are equivalent and can be mixed.
**Processing order:**
- **Paralinguistic tags**: Position in text matters (where to insert sound)
- `Hello <Laughter> world` → Laughter inserted after "Hello"
- **Other edit types** (emotion, style, speed): Position doesn't matter, gets stripped
- `Hello <style:whisper> world` = `Hello world <style:whisper>` = `<style:whisper> Hello world`
- All result in: Generate "Hello world", then apply whisper style
**Execution order for mixed tags:**
1. First: All non-paralinguistic edits (emotion → style → speed) in fixed order
2. Last: Paralinguistic edits (preserves position info for sound insertion)
**Example:**
```
[Alice] Hello <Laughter:2> world <style:whisper:1>
```
1. Generate TTS: "Hello world"
2. Edit pass 1: Apply whisper style (1 iteration) - position irrelevant
3. Edit pass 2: Add Laughter at position (2 iterations) - position matters
## Processing Flow
```
┌─────────────────────────────────────────────────────────────┐
│ Input: "[Alice] Hello <Laughter:2> world!" │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. Parse segment: character=Alice, text with tags │
│ Extract edit tags: [("Laughter", "paralinguistic", 2)] │
│ Clean text: "Hello world!" │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Generate TTS with clean text (any engine) │
│ Output: audio segment │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Check duration: > 30s? │
│ YES → Log warning, skip edit, return original audio │
│ NO → Continue to edit │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Load Step Audio EditX engine (lazy load on first use) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. For each edit tag in order: │
│ - Save audio to temp file │
│ - Call edit_single() with appropriate edit_type │
│ - Use output as input for next edit (if any) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Output: Edited audio segment │
└─────────────────────────────────────────────────────────────┘
```
## Architecture Decision: Processing Order
### Option A: Generate All TTS First, Then Edit (RECOMMENDED)
```
┌─────────────────────────────────────────────────────────────┐
│ 1. Parse ALL segments, extract edit tags, store metadata │
│ 2. Generate ALL TTS segments (any engine) - clean text │
│ 3. Post-process: Apply edits to segments that have tags │
│ 4. Assemble final audio │
└─────────────────────────────────────────────────────────────┘
```
**Pros:**
- Step Audio EditX engine loaded ONCE, only if needed
- All TTS generation completes before edit engine loads (memory efficient)
- Can batch edit operations
- Cleaner separation of concerns
- Progress reporting is clearer (TTS phase → Edit phase)
**Cons:**
- Need to track which segments have edit tags through the pipeline
### Option B: Edit Each Segment Immediately After Generation
```
For each segment:
1. Parse segment, extract edit tags
2. Generate TTS
3. If has edit tags → load Step Audio EditX → edit
4. Store result
Assemble final audio
```
**Pros:**
- Simpler data flow
**Cons:**
- Step Audio EditX loaded/unloaded repeatedly if interleaved
- Two heavy models potentially in memory at same time
- Harder to optimize
**Decision: Option A** - Generate all TTS first, then batch edit.
---
## Implementation Details - Modular Architecture
### Core Principle: NO CODE DUPLICATION
The edit tag system integrates with existing parsers. We don't modify adapters individually.
### Processing Pipeline
```
┌──────────────────────────────────────────────────────────────────────────┐
│ EXISTING PIPELINE │
│ │
│ Text Input → Character Parser → Segment Parameters → TTS Generation │
│ │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ NEW: EDIT TAG EXTRACTION │
│ │
│ Happens at parsing stage (segment_parameters.py or new unified parser) │
│ - Extract <tag:iter> from text │
│ - Store in segment metadata: segment.edit_tags = [...] │
│ - Return clean text for TTS │
│ │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ EXISTING: TTS GENERATION │
│ │
│ All engines generate audio from clean text (no edit tags) │
│ No changes to adapters needed! │
│ │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ NEW: POST-PROCESSOR (single point) │
│ │
│ Called ONCE after all TTS generation, before assembly │
│ - Check each segment for edit_tags metadata │
│ - If any segments have tags → lazy load Step Audio EditX │
│ - Apply edits to those segments │
│ - Return modified segments for assembly │
│ │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ EXISTING: AUDIO ASSEMBLY │
│ │
│ Combine segments (unchanged logic) │
│ │
└──────────────────────────────────────────────────────────────────────────┘
```
### Files to Modify/Create
#### 1. Extend Existing Parser: `utils/text/segment_parameters.py`
Add edit tag extraction alongside existing parameter parsing:
```python
# Existing: [Character|seed:42|temp:0.8]
# New addition: Also extract <Laughter:2|style:whisper>
class SegmentData:
character: str
text: str # Clean text (tags stripped)
parameters: dict # seed, temp, etc.
edit_tags: list # NEW: [("Laughter", "paralinguistic", 2, position), ...]
```
**No duplication** - extend existing `parse_segment()` function.
#### 2. Extend: `utils/text/step_audio_editx_special_tags.py`
Add iteration parsing to existing tag functions:
```python
# Existing: convert_step_audio_editx_tags(), strip_paralinguistic_tags()
# Add: parse_edit_tags_with_iterations()
def parse_edit_tags_with_iterations(text: str) -> Tuple[str, List[EditTag]]:
"""
Parse all edit tags from text, return clean text and tag list.
Supports:
- <Laughter> → paralinguistic, 1 iter
- <Laughter:3> → paralinguistic, 3 iter
- <style:whisper:2> → style, 2 iter
- <emotion:happy> → emotion, 1 iter
- <Laughter:2|style:whisper:1> → multiple in one tag
"""
```
#### 3. NEW: `utils/audio/edit_post_processor.py`
Single centralized post-processor:
```python
class EditPostProcessor:
"""
Post-processes TTS segments with Step Audio EditX edits.
Lazy loads engine only when needed.
"""
_engine = None # Lazy loaded
@classmethod
def process_segments(cls, segments: List[SegmentResult]) -> List[SegmentResult]:
"""
Process all segments, applying edits where edit_tags exist.
Called ONCE after all TTS generation.
"""
# Find segments with edit tags
segments_to_edit = [s for s in segments if s.edit_tags]
if not segments_to_edit:
return segments # No edits needed
# Lazy load engine
if cls._engine is None:
cls._engine = cls._load_engine()
# Process each segment with tags
for segment in segments_to_edit:
segment.audio = cls._apply_edits(segment)
return segments
```
#### 4. Integration Points (MINIMAL CHANGES)
**`nodes/unified/tts_text_node.py`** - Add one call:
```python
# After TTS generation, before return
segments = EditPostProcessor.process_segments(segments)
```
**`nodes/unified/tts_srt_node.py`** - Add one call:
```python
# After all segments generated, before assembly
segments = EditPostProcessor.process_segments(segments)
```
**Adapters: NO CHANGES NEEDED**
- Adapters receive clean text (tags already stripped at parse stage)
- Adapters return audio as usual
- Post-processor handles edits centrally
### Data Flow
```
Input: "[Alice|seed:42] Hello <Laughter:2> world! <style:whisper>"
Step 1 - Parse (segment_parameters.py):
→ character: "Alice"
→ parameters: {seed: 42}
→ clean_text: "Hello world!"
→ edit_tags: [
EditTag(type="paralinguistic", value="Laughter", iterations=2, position=6),
EditTag(type="style", value="whisper", iterations=1, position=None)
]
Step 2 - TTS Generation (any adapter):
→ Input: "Hello world!"
→ Output: audio tensor
Step 3 - Post-process (edit_post_processor.py):
→ Check: segment has edit_tags? YES
→ Validate: duration < 30s? YES
→ Apply style:whisper (1 iter) - position-independent first
→ Apply Laughter at position 6 (2 iter) - paralinguistic last
→ Output: edited audio tensor
Step 4 - Assembly (unchanged):
→ Combine all segments
```
### Tag Detection
**Paralinguistic tags** (existing set):
- Breathing, Laughter, Sigh, Uhm
- Surprise-oh, Surprise-ah, Surprise-wa
- Confirmation-en, Question-ei, Dissatisfaction-hnn
**Emotion tags** (14 options):
- happy, sad, angry, excited, calm, fearful, surprised, disgusted
- confusion, empathy, embarrass, depressed, coldness, admiration
**Style tags** (32 options):
- whisper, serious, child, older, girl, pure, sister, sweet
- exaggerated, ethereal, generous, recite, act_coy, warm, shy
- comfort, authority, chat, radio, soulful, gentle, story, vivid
- program, news, advertising, roar, murmur, shout, deeply, loudly
- arrogant, friendly
**Speed tags** (4 options):
- faster, slower, more_faster, more_slower
### Duration Validation
```python
def validate_for_edit(audio_tensor, sample_rate):
duration = audio_tensor.shape[-1] / sample_rate
if duration > 30.0:
print(f"⚠️ Segment too long for Step Audio EditX edit ({duration:.1f}s > 30s). "
f"Edit tags will be skipped. Split your text into shorter segments.")
return False
if duration < 0.5:
print(f"⚠️ Segment too short for Step Audio EditX edit ({duration:.1f}s < 0.5s). "
f"Edit tags will be skipped.")
return False
return True
```
### Transcript Handling
For edit mode, we need the transcript (what was spoken). This is the clean text BEFORE TTS:
```python
# Original: "[Alice] Hello <Laughter:2> world!"
# Clean text for TTS: "Hello world!"
# Transcript for edit: "Hello world!" (same as clean text)
# For paralinguistic, target text has tags:
# Target: "Hello [Laughter] world!"
```
## Edge Cases
1. **No Step Audio EditX model downloaded**
- First use triggers download (existing behavior)
- User sees download progress
2. **Multiple segments, only some with tags**
- Only segments with tags get post-processed
- Others pass through unchanged
3. **Tag in middle of word** (malformed)
- `Hel<Laughter>lo` → Strip tag, get "Hello"
- Best effort, no error
4. **Unknown tag**
- `<UnknownTag:2>` → Ignored, passed as plain text, stripped
- Or warn user?
5. **ChatterBox 23-Lang v2 tag collision**
- Their tags use different format: `<giggle>`, `<whisper>`, etc.
- Our edit tags: `<Laughter>`, `<style:whisper>`
- Paralinguistic overlap: need to check if text already has v2 tags
- Solution: Process our tags first, before sending to ChatterBox
## Example Usage
### Simple paralinguistic
```
[Alice] I can't believe it <Laughter> that's hilarious!
```
### With iterations
```
[Alice] I can't believe it <Laughter:3> that's hilarious!
```
### Multiple effects - pipe syntax
```
[Bob] I'm so tired, goodnight <Sigh:2|style:whisper:1>
```
### Multiple effects - separate tags
```
[Bob] I'm so tired <Sigh:2> goodnight <style:whisper:1>
```
### Mixed syntax (equivalent to above)
```
[Bob] I'm so tired <Sigh:2|style:whisper:1> goodnight
```
### Combined with segment parameters
```
[Alice|seed:42|temperature:0.8] Hello <Laughter:3> friend!
```
### Full combo
```
[Alice|seed:42] Hello <Laughter:2|emotion:happy:1> my friend!
```
### SRT mode
```
1
00:00:00,000 --> 00:00:03,000
[Alice] Hello <Laughter> world!
2
00:00:03,500 --> 00:00:06,000
[Bob|seed:123] Hi there <style:whisper:2|emotion:calm>
```
### Position matters for paralinguistics
```
# Different results:
[Alice] Hello <Laughter> world! # Laughter after "Hello"
[Alice] Hello world <Laughter>! # Laughter after "world"
# Same result (style position irrelevant):
[Alice] Hello <style:whisper> world!
[Alice] <style:whisper> Hello world!
[Alice] Hello world! <style:whisper>
```
## Decisions Made
1. **Syntax for non-paralinguistic edits:** `<emotion:happy:2>` - explicit type prefix
2. **Multiple tags:** Support both `<tag1:2|tag2:3>` pipe syntax AND `<tag1:2><tag2:3>` separate tags
3. **Iteration default:** `<Laughter>` = 1 iteration (no explicit `:1` required)
4. **Warning behavior for >30s:** Skip edit with warning in console, return original audio
5. **Engine loading:** Lazy load ONCE after all TTS generation completes
6. **Processing order:** Generate ALL TTS first, then batch edit (Option A)
7. **Architecture:** Modular, no adapter changes, single post-processor
## Open Questions
1. **Position tracking for paralinguistics:**
- How to track character position after tag stripping?
- Store original position? Or store as "after word N"?
2. **Progress reporting:**
- Show separate progress for TTS phase vs Edit phase?
- How to report which segments are being edited?
3. **Cache interaction:**
- Should edited segments be cached separately?
- Cache key should include edit tags?
4. **Error handling:**
- If one segment edit fails, continue with others?
- Return original audio for failed edits?
## Implementation Order
1. **Phase 1: Tag Parsing**
- Extend `step_audio_editx_special_tags.py` with iteration support
- Add `parse_edit_tags_with_iterations()` function
- Support pipe syntax `<tag1|tag2>` and separate tags
2. **Phase 2: Segment Integration**
- Extend `segment_parameters.py` to extract and store edit_tags
- Ensure clean text (tags stripped) flows to TTS
3. **Phase 3: Post-Processor**
- Create `utils/audio/edit_post_processor.py`
- Lazy engine loading
- Duration validation with warnings
- Sequential edit application
4. **Phase 4: Integration**
- Add single call in `tts_text_node.py`
- Add single call in `tts_srt_node.py`
- Test with multiple engines
5. **Phase 5: Testing**
- Single tag, multiple tags
- Mixed syntax (pipe + separate)
- Duration edge cases
- SRT mode
- Multiple engines
+241
View File
@@ -0,0 +1,241 @@
# Model Loading Tier 2-3 Refactor Plan
## Overview
Complete the model loading architecture refactor by:
1. **Tier 2**: Consolidate caching, standardize error handling
2. **Tier 3**: Standardize factory signatures, implement capability registry
## TIER 2: Consolidation & Standardization
### 2.1 Remove SmartModelLoader Usage
**Current State**: SmartModelLoader still used in:
- `nodes/chatterbox/chatterbox_tts_node.py` (lines with smart_model_loader)
- `engines/chatterbox/streaming_model_manager.py`
- `engines/chatterbox_official_23lang/streaming_model_manager.py`
- `engines/adapters/f5tts_streaming_adapter.py`
**Why Remove**:
- Creates third caching system (after unified interface + ComfyUI manager)
- Causes cache synchronization issues
- Delegates to manager.load_tts_model() anyway (redundant)
**Approach**:
1. ChatterBox TTS node: Replace smart_model_loader call with direct unified interface
2. Streaming processors: Use unified interface for model loading
3. Verify no other code depends on SmartModelLoader returns
**Risk**: Medium - streaming processors have complex state management
---
### 2.2 Standardize Error Handling
**Current State**: 5 different error patterns across factories
**Target State**: Single exception hierarchy:
```python
class TTSModelLoadingError(Exception):
"""Base exception for model loading"""
pass
class TTSModelNotFoundError(TTSModelLoadingError):
"""Model not found locally or on HuggingFace"""
pass
class TTSDeviceError(TTSModelLoadingError):
"""Device-related issues"""
pass
class TTSModelFormatError(TTSModelLoadingError):
"""Model format compatibility issues"""
pass
```
**Files to Update**:
- Create `utils/models/exceptions.py` with exception classes
- Update all factories to use consistent exceptions
- Update node error handlers to catch standardized exceptions
**Risk**: Low - backwards compatible
---
### 2.3 Unified Cache Key Generation
**Current State**: Multiple cache key formats:
- Unified interface: string concatenation
- SmartModelLoader: MD5 hashing
- Nodes: MD5 of dict items
**Target State**: Single hash-based cache key function
```python
def generate_model_cache_key(
engine_name: str,
model_name: str,
device: str,
language: Optional[str] = None,
**extra_params
) -> str:
"""Generate standardized cache key"""
```
**Files to Create/Update**:
- `utils/models/cache_key_generator.py` (new)
- Update unified_model_interface.py to use it
- Update all node caching to use it
**Risk**: Low
---
## TIER 3: Architecture Standardization
### 3.1 Factory Parameter Dataclass
**Current State**: Each factory has different parameters
**Target State**: All factories accept standardized dataclass:
```python
@dataclass
class ModelLoadConfig:
"""Standard configuration for all model factories"""
# Required
device: str # "auto", "cuda", "cpu", etc.
model_name: str
# Optional (engine-specific)
language: Optional[str] = None
model_path: Optional[str] = None
repo_id: Optional[str] = None
# Additional params (for quantization, optimization, etc.)
additional_params: Optional[Dict[str, Any]] = None
def __post_init__(self):
# Resolve device before storing
from utils.device import resolve_torch_device
self.device = resolve_torch_device(self.device)
```
**Factory Signature Becomes**:
```python
def engine_factory(config: ModelLoadConfig) -> Any:
"""All factories use same signature"""
```
**Files to Create/Update**:
- `utils/models/factory_config.py` (new)
- All factories in unified_model_interface.py
- Unified interface load_model() method
**Risk**: High - changes all factory signatures
---
### 3.2 Engine Capability Registry
**Current State**: Hardcoded engine-specific logic in model_manager.py
**Target State**: Data-driven engine capabilities:
```python
ENGINE_CAPABILITIES = {
"chatterbox": {
"supports_voice_conversion": True,
"has_fallback": True,
"can_corrupt": False,
"recovery_handler": None
},
"higgs_audio": {
"supports_voice_conversion": False,
"has_fallback": False,
"can_corrupt": True,
"recovery_handler": reset_higgs_cuda_graphs,
"requires_special_init": True
},
# ... all 7 engines
}
```
**Files to Create**:
- `utils/models/engine_registry.py` (new) - define capabilities
- Remove hardcoded engine checks from model_manager.py
- Use registry for generic recovery
**Risk**: Medium - affects error recovery paths
---
### 3.3 Unified Fallback Handler
**Current State**: ChatterBox has sophisticated fallback, others don't
**Target State**: Generic fallback utility any engine can use:
```python
class ModelFallbackHandler:
"""Generic fallback chain for model loading"""
def __init__(self, primary_loader: Callable):
self.primary_loader = primary_loader
self.fallback_chain = []
def add_fallback(self, fallback_loader: Callable, condition: Callable):
"""Add fallback with condition"""
pass
def load(self) -> Any:
"""Execute fallback chain"""
pass
```
**Files to Create**:
- `utils/models/fallback_handler.py` (new)
**Risk**: Low - new utility, no breaking changes
---
## Implementation Order
1. **First**: Create exception classes (no breaking changes)
2. **Second**: Create cache key generator (parallel with tier 1 utilities)
3. **Third**: Create factory config dataclass
4. **Fourth**: Update all factories to use ModelLoadConfig
5. **Fifth**: Remove SmartModelLoader usage
6. **Sixth**: Create engine registry
7. **Seventh**: Implement unified fallback handler
8. **Eighth**: Update model_manager.py to use registry
---
## Risk Assessment
| Task | Risk | Dependencies | Effort |
|------|------|--------------|--------|
| Exception classes | Low | None | 1 hr |
| Cache key generator | Low | Exceptions | 1 hr |
| Factory config dataclass | Medium | None | 2 hrs |
| Update factories | High | Config, exceptions | 4 hrs |
| Remove SmartModelLoader | Medium | Updated factories | 2 hrs |
| Engine registry | Medium | Updated factories | 2 hrs |
| Fallback handler | Low | Exceptions | 2 hrs |
| Integration testing | High | All above | 3 hrs |
**Total**: ~17 hours of work
---
## Success Criteria
- [ ] All factories use consistent error handling
- [ ] Cache key generation centralized and consistent
- [ ] SmartModelLoader completely removed
- [ ] Engine capabilities defined in registry
- [ ] All 7 engines working without issues
- [ ] Cache hits work correctly across device switches
- [ ] Error messages are consistent and helpful
- [ ] No VRAM spikes or device mismatches
+139
View File
@@ -0,0 +1,139 @@
# Project Refactoring TODO
This document tracks architectural issues and inconsistencies that need refactoring to improve modularity and reduce code duplication.
## SRT Processing Architecture Issues
### Problem: Inconsistent SRT Implementation Approaches
Different engines use completely different patterns for SRT processing:
1. **ChatterBox (Old)**: Uses `ChatterboxSRTTTSNode` class (should be processor)
2. **VibeVoice**: Uses proper `VibeVoiceSRTProcessor` class with full implementation
3. **Higgs Audio**: Uses clean modular approach with timing utilities
4. **ChatterBox Official 23-Lang**: Follows Higgs Audio pattern (good)
5. **F5-TTS**: Uses `F5TTSSRTNode` class (should be processor)
**Impact**: Code duplication, inconsistent interfaces, hard to maintain
**Solution**: Standardize on processor pattern with shared utilities
### Problem: Node vs Processor Naming Inconsistency
- ChatterBox uses `ChatterboxSRTTTSNode` but should be `ChatterboxSRTProcessor`
- F5-TTS uses `F5TTSSRTNode` but should be `F5TTSSRTProcessor`
- Only VibeVoice and ChatterBox Official 23-Lang use proper processor naming
**Impact**: Confusing architecture, breaks expected patterns
**Solution**: Rename all SRT classes to use `*Processor` pattern
### Problem: Duplicated SRT Logic
Each engine reimplements:
- SRT parsing
- Character switching within SRT segments
- Timing calculations
- Audio assembly
- Report generation
**Impact**: Massive code duplication, bugs in some implementations
**Solution**: Create shared SRT base class with common functionality
## Voice Reference Handling Issues
### Problem: Inconsistent Audio Reference Processing
- ChatterBox: Uses file paths only, converts tensors to temp files via `handle_reference_audio`
- F5-TTS: Uses both file paths and ComfyUI tensors directly
- Higgs Audio: Uses ComfyUI tensors with reference text
- VibeVoice: Uses ComfyUI tensors
**Impact**: Different interfaces, conversion logic scattered
**Solution**: Standardize voice reference handling in base class
## Character Parser Integration Issues
### Problem: Inconsistent Language Resolution
- Some engines hardcode language mappings
- Some use character parser properly
- Some bypass language mapper entirely
- ChatterBox Official 23-Lang now uses proper integration (good example)
**Impact**: Language switching behaves differently per engine
**Solution**: Mandate character parser usage in base class
### Problem: Engine-Specific Logic in Universal Character Parser
- **Issue**: Universal character parser contains ChatterBox-specific Italian prefix logic (`apply_italian_prefix_if_needed`)
- **Problem**: Engine-specific text processing belongs in engine adapters, not universal parser
- **Current State**: Italian `[it]` prefix filter has complex conditions but should only affect:
- Explicit `[it:Alice]` or `[italian:Bob]` tags
- Characters mapped to Italian in alias system
- Should NOT affect normal English ChatterBox, other engines, or global model selection
- **Impact**: Violation of separation of concerns, coupling universal parser to specific engine quirks
- **Solution**: Move Italian prefix logic to ChatterBox adapter, keep character parser engine-agnostic
- **Priority**: Medium (works correctly but violates clean architecture)
- **Files Affected**:
- `utils/text/character_parser/` (remove Italian logic)
- `engines/adapters/chatterbox_adapter.py` (add Italian logic)
## Cache System Issues
### Problem: Engine-Specific Cache Keys
- Each engine uses different cache identifiers
- Cache collision was discovered between ChatterBox engines
- No standardized cache invalidation
**Impact**: Cross-engine cache pollution, missed cache opportunities
**Solution**: Implement unified cache system with proper namespacing
## Base Class Architecture Issues
### Problem: Inconsistent Base Class Usage
- Some engines properly inherit from `BaseTTSNode`
- Some engines duplicate base class functionality
- Method signatures vary between engines for same functionality
**Impact**: Code duplication, inconsistent interfaces
**Solution**: Enforce proper base class inheritance and standardize method signatures
## Import System Issues
### Problem: Different Import Patterns
- Some use direct imports
- Some use `importlib.util.spec_from_file_location`
- Some use robust_import
- Inconsistent path resolution
**Impact**: Import failures, maintenance overhead
**Solution**: Standardize import patterns project-wide
## Error Handling Inconsistencies
### Problem: Different Error Handling Approaches
- Some engines have comprehensive error handling
- Some engines crash on edge cases
- Different error message formats
- Inconsistent fallback mechanisms
**Impact**: Poor user experience, difficult debugging
**Solution**: Implement standardized error handling framework
## Configuration Management Issues
### Problem: Scattered Configuration Handling
- Engine configs stored differently
- Parameter validation inconsistent
- Default values scattered across files
- No centralized config schema
**Impact**: Bugs from invalid configs, hard to maintain defaults
**Solution**: Implement unified configuration management system
---
## Refactoring Priority
1. **High Priority**: SRT processing standardization (affects user experience directly)
2. **Medium Priority**: Voice reference handling (causes confusion)
3. **Medium Priority**: Cache system unification (performance impact)
4. **Low Priority**: Import system standardization (maintenance burden)
---
*Last Updated: 2025-01-XX*
*Add new issues to this file as they are discovered during development*
@@ -0,0 +1,209 @@
# VibeVoice Integration Plan - Final Version
## Overview
Integrate Microsoft VibeVoice (1.5B and 7B models) into TTS Audio Suite following the unified architecture, with full support for all existing features while exposing VibeVoice's unique multi-speaker capabilities.
## Implementation Structure
### 1. Engine Implementation (`engines/vibevoice/`)
#### `vibevoice.py` (Main Engine - ~500 lines)
```python
class VibeVoiceEngine:
def __init__(self)
def initialize_engine(model_name, device)
def generate_speech(text, voice_samples, cfg_scale, seed, ...)
def generate_multi_speaker(segments, voice_samples, ...)
def cleanup() # Memory management
def unload_models() # Explicit unload (called by ComfyUI's unload button)
```
- Use `unified_model_interface` for model loading
- Integrate with `utils.audio.cache` for caching
- Support both models (1.5B and 7B)
- Handle memory management via ComfyUI's unload system
#### `vibevoice_downloader.py` (~200 lines)
```python
VIBEVOICE_MODELS = {
"vibevoice-1.5B": {
"repo": "microsoft/VibeVoice-1.5B",
"files": [...], # 5.4GB total
},
"vibevoice-7B": {
"repo": "microsoft/VibeVoice-Large",
"files": [...], # 9.3GB total
}
}
```
- Use `unified_downloader` for all downloads
- Download to `models/TTS/vibevoice/` structure
- No HuggingFace cache duplication
### 2. Engine Configuration Node (`nodes/engines/vibevoice_engine_node.py`) (~150 lines)
```python
class VibeVoiceEngineNode:
INPUT_TYPES:
Required:
- model: ["VibeVoice-1.5B", "VibeVoice-7B"]
- device: ["auto", "cuda", "cpu"]
- multi_speaker_mode: ["Custom Character Switching", "Native Multi-Speaker"]
- cfg_scale: (1.0-2.0, default 1.3)
- use_sampling: BOOLEAN
- temperature: (0.1-2.0, default 0.95)
- top_p: (0.1-1.0, default 0.95)
- chunk_minutes: INT (0=disabled, UI shows minutes, backend converts to chars)
Optional:
- speaker2_voice: AUDIO
- speaker3_voice: AUDIO
- speaker4_voice: AUDIO
```
- Returns TTS_ENGINE configuration dict
- NO seed parameter (that's in unified nodes)
- NO load/unload toggles (handled by ComfyUI system)
- Uses same naming as Higgs: "Custom Character Switching" not "segment_per_character"
- Follows ChatterBox/Higgs patterns exactly
### 3. Engine Adapter (`engines/adapters/vibevoice_adapter.py`) (~400 lines)
```python
class VibeVoiceEngineAdapter:
def __init__(self, node_instance)
def get_model_for_language(lang_code, default_model)
def load_base_model(model_name, device)
def generate_segment(text, voice_ref, params)
def process_character_segments(segments, voice_mapping)
def convert_character_to_speaker_format(text) # [Alice] → Speaker 0
def handle_pause_tags(text) # Use pause_processor
def handle_language_tags(text) # Parse [de:Alice] syntax
```
- Bridge unified nodes to VibeVoice engine
- Two modes matching Higgs naming:
- `Custom Character Switching`: Generate per character (like ChatterBox/Higgs)
- `Native Multi-Speaker`: Single generation with multiple speakers
- Character mapping: [Alice], [Bob] → Speaker 0, Speaker 1
- Full integration with existing systems
### 4. Internal Processors (`nodes/vibevoice/`)
#### `vibevoice_processor.py` (~300 lines)
- Internal TTS processor (called by unified TTS node)
- Handles actual generation orchestration
- Manages chunking (time-based UI, character-based backend)
- Memory management coordination
#### `vibevoice_srt_processor.py` (~400 lines)
- Internal SRT processor (called by unified SRT node)
- Language grouping optimization
- Multi-speaker dialogue handling
- Timing synchronization
### 5. Model Factory Registration (~50 lines in existing files)
In `utils/models/unified_model_interface.py`:
```python
# Register VibeVoice model factories
interface.register_model_factory(
"vibevoice", "tts",
lambda **kwargs: load_vibevoice_model(**kwargs)
)
```
### 6. Dependencies Update
`requirements.txt`:
```
# VibeVoice dependencies
transformers>=4.44.0
git+https://github.com/microsoft/VibeVoice.git
```
## Key Integration Points
### Memory Management
- Memory unloading handled by ComfyUI's "Unload Models" button
- Engine implements `unload_models()` method
- Uses ComfyUI's model management system
- Auto-cleanup on node destruction
### Unified Systems Integration
- **Cache**: Use `utils.audio.cache` with content-based hashing
- **Characters**: Full `utils.text.character_parser` support
- **Pause Tags**: `utils.text.pause_processor` integration
- **Downloads**: `utils.downloads.unified_downloader` only
- **Model Loading**: `utils.models.unified_model_interface`
- **Chunking**: `utils.text.chunking` with time-based UI
### Two Generation Modes (Named Like Higgs)
1. **Custom Character Switching** (default):
- Preserves all TTS Audio Suite features
- Generate audio per character, then combine
- Supports pause tags, language switching per character
- More flexible but slower
2. **Native Multi-Speaker**:
- Uses VibeVoice's native multi-speaker
- Single generation pass
- More efficient but less flexible
- Auto-converts [Character] → Speaker N format
### Time-Based Chunking
- UI shows minutes: "Chunk every [5] minutes"
- Backend converts: 5 min → ~3750 chars (150 wpm * 5 chars/word * 5)
- More intuitive for long-form content
- Optional (0 = no chunking)
### Seed Parameter Location
- Seed is in unified nodes (TTS Text, TTS SRT) NOT in engine node
- Follows existing pattern where engine provides configuration, unified nodes handle generation parameters
## File Size Limits
- Each file stays under 500-600 lines
- Modular design with clear separation
- Reuses all existing utilities
- No code duplication
## Testing Plan
1. Test both 1.5B and 7B models
2. Validate character switching in both modes
3. Test pause tag processing
4. Verify memory management via ComfyUI unload
5. Test long-form generation with chunking
6. Validate multi-speaker native mode
7. Test cache functionality
8. Verify SRT synchronization
## Benefits
- Full integration with all TTS Audio Suite features
- Choice between flexibility (Custom mode) and efficiency (Native mode)
- Proper memory management via ComfyUI system
- No code duplication
- Follows established patterns exactly (Higgs Audio style)
- Exposes unique 90-minute and 4-speaker capabilities
## Implementation Notes
- Created: 2025-08-29
- Author: Claude (TTS Audio Suite Assistant)
- Status: Ready for implementation
@@ -0,0 +1,111 @@
# 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)
+92
View File
@@ -0,0 +1,92 @@
# IndexTTS-2 Updates Log
This document tracks updates applied to our bundled IndexTTS-2 code from the upstream repository.
## Update Process
1. **Check for upstream changes:** `cd IgnoredForGitHubDocs/For_reference/index_tts && git pull`
2. **Review changes:** `git diff OLD_COMMIT..NEW_COMMIT -- indextts/infer_v2.py`
3. **Apply relevant changes** to `engines/index_tts/indextts/infer_v2.py`
4. **Test functionality** with our IndexTTS-2 nodes
5. **Document changes** in this log
---
## 2025-09-18: Major Update - Cache & Emotion Improvements
**Reference commit range:** `8336824..64cb31a` (September 11 → September 18, 2025)
**Upstream commits reviewed:** 5 commits affecting `infer_v2.py`
### 🔧 Changes Applied:
#### 1. **Emotion Vector Normalization Function** (commit: 8aa8064)
- **Added:** `normalize_emo_vec()` helper function
- **Purpose:** Prevents voice identity loss from overly strong emotion settings
- **Implementation:**
- Applies emotion bias factors to reduce problematic emotions
- Caps total emotion sum to 0.8 maximum
- Protects against extreme emotion vector values
- **User benefit:** Better emotion control without losing speaker characteristics
#### 2. **Audio Length Limiting** (commit: 0828dcb)
- **Added:** `_load_and_cut_audio()` helper function
- **Purpose:** Prevents memory/VRAM issues from overly long reference audio
- **Implementation:**
- Automatically truncates audio to 15 seconds maximum
- Supports different sample rates (16kHz, 22kHz)
- Provides verbose logging of truncation
- **User benefit:** More stable generation with long reference audio files
#### 3. **Persistent Cache Buildup Fix** (commit: 64cb31a)
- **Added:** Proper cache clearing with `torch.cuda.empty_cache()`
- **Purpose:** Solves memory accumulation issues during multiple generations
- **Implementation:**
- Clears old cache variables before loading new audio
- Calls `torch.cuda.empty_cache()` to free GPU memory
- Applied to both speaker and emotion cache invalidation
- **User benefit:** Better memory management during batch processing
#### 4. **BigVGAN CUDA Kernel Import Fix** (commits: ee23371, e409c4a)
- **Fixed:** Corrected import path for BigVGAN custom CUDA kernel
- **Changed:** `indextts.BigVGAN.alias_free_activation.cuda` → `indextts.s2mel.modules.bigvgan.alias_free_activation.cuda`
- **Purpose:** Ensures proper CUDA acceleration when available
- **User benefit:** Better performance with CUDA-enabled setups
### 📋 Implementation Status:
| Change | Applied to Bundled Code | Tested | Notes |
|--------|------------------------|--------|-------|
| `normalize_emo_vec()` | ✅ **Applied** | ⏳ Pending | Core emotion control improvement |
| `_load_and_cut_audio()` | ✅ **Applied** | ⏳ Pending | Memory stability fix |
| Cache clearing fixes | ✅ **Applied** | ⏳ Pending | Critical for batch processing |
| BigVGAN import fix | ✅ **Applied** | ⏳ Pending | Performance improvement |
### 🎯 User-Facing Improvements:
- **Better emotion control:** Emotion vectors now auto-normalize to prevent voice identity loss
- **Memory stability:** Long reference audio automatically truncated to prevent crashes
- **Batch processing:** Improved cache management for multiple generations
- **Performance:** Fixed CUDA kernel loading for faster inference
### 🧪 Testing Required:
- [ ] Emotion vector controls in radar chart widget
- [ ] Long reference audio handling (>15 seconds)
- [ ] Multiple sequential generations (cache clearing)
- [ ] CUDA performance with FP16 enabled
---
## Next Update Check: 2025-10-18
**Monitoring:** Watch for commits to `indextts/infer_v2.py` in https://github.com/index-tts/index-tts
**Update command:**
```bash
cd IgnoredForGitHubDocs/For_reference/index_tts
git pull origin main
git log --oneline LAST_COMMIT..HEAD -- indextts/infer_v2.py
```
**Priority changes to watch for:**
- Performance optimizations
- Memory management improvements
- Bug fixes in emotion processing
- New model loading features
+107
View File
@@ -0,0 +1,107 @@
# IndexTTS-2 Implementation Plan
## Overview
IndexTTS-2 is a breakthrough emotionally expressive and duration-controlled autoregressive zero-shot TTS engine featuring emotion disentanglement and precise voice cloning.
## Unique Features
- **Duration Control**: First autoregressive TTS model with precise duration control
- **Emotion Disentanglement**: Separates speaker identity from emotional expression
- **Multi-Modal Emotion Control**: Audio references, text descriptions, manual vectors
- **Zero-Shot Voice Cloning**: Single reference audio for voice cloning
- **Advanced Character Switching**: `[character:emotion_ref]` syntax
## Technical Specifications
- **Sample Rate**: 22,050 Hz
- **Audio Format**: `torch.Tensor` shape `[1, samples]`
- **Dependencies**: `torch==2.8.*`, `transformers==4.52.1`, IndexTTS-2 source code
- **Model Components**: GPT + Semantic codec + S2Mel + BigVGAN + CAMPPlus + QwenEmotion
## Implementation Status
### ✅ Completed
1. **Core Engine** (`engines/index_tts/index_tts.py`)
- IndexTTSEngine class with unified model interface integration
- Proper UnifiedModelInterface integration for loading/unloading
- Factory registration in unified_model_interface.py
- All official parameters exposed (emotion vectors, text emotions, generation params)
2. **Downloader** (`engines/index_tts/index_tts_downloader.py`)
- Uses folder_paths.models_dir for correct TTS/IndexTTS/ structure
- unified_downloader integration
- Downloads both HuggingFace model files and GitHub source code
- Model verification system
3. **Cache Integration** (`utils/audio/cache.py`)
- IndexTTSCacheKeyGenerator added to unified cache system
- Handles all IndexTTS-2 parameters for cache key generation
- Integrated with global audio_cache
4. **Engine Adapter** (`engines/adapters/index_tts_adapter.py` - IN PROGRESS)
- Character switching support with emotion references
- Cache integration and parameter mapping
- Uses existing CharacterParser for tag parsing
### 🚧 In Progress
5. **Character Tag Extension**
- Need to handle flexible syntax: `[de:]`, `[de:Alice]`, `[Alice]`, `[Alice:angry_bob]`, `[de:Alice:angry_bob]`
- Extend CharacterParser or create IndexTTS-specific parsing
- Avoid ambiguity between language codes and character names
### 📋 Pending
6. **Engine Configuration Node** (`nodes/engines/index_tts_engine_node.py`)
- UI node with all IndexTTS-2 parameters
- Model path selection with auto-download
- Emotion controls (audio references, vectors, text)
- Generation parameters (temperature, top_p, top_k, etc.)
7. **TTS Text Integration** (`nodes/index_tts/`)
- `index_tts_processor.py` - Main TTS processor for unified TTS Text node
- `index_tts_srt_processor.py` - SRT subtitle processing with timing
- Integration with character switching system
8. **Testing & Validation**
- Basic functionality testing
- Character switching validation
- Emotion control verification
- Performance optimization
## Character Switching Syntax Design
### Proposed Flexible Syntax Resolution:
```
[de:] → language="de", character="narrator", emotion=None
[de:Alice] → language="de", character="Alice", emotion=None
[Alice] → language=None, character="Alice", emotion=None
[Alice:angry_bob] → language=None, character="Alice", emotion="angry_bob"
[de:Alice:angry_bob] → language="de", character="Alice", emotion="angry_bob"
```
### Resolution Strategy:
1. Split by `:` and count parts
2. 1 part: Check if it's a known language code, else treat as character
3. 2 parts: Check if first part is language code, else treat as character:emotion
4. 3 parts: Always language:character:emotion
### Implementation Files:
- Extend `utils/text/character_parser.py` with emotion support
- Or create `engines/index_tts/emotion_parser.py` for IndexTTS-specific parsing
## Model Management Integration
### Unified Systems Used:
- **UnifiedModelInterface**: Standardized loading/unloading
- **unified_downloader**: HuggingFace + Git downloads
- **AudioCache**: Generation result caching
- **folder_paths**: Correct model directory structure
- **CharacterParser**: Tag parsing and language resolution
### Memory Management:
- ComfyUI model wrapper integration via UnifiedModelInterface
- Automatic VRAM management and "Clear VRAM" button support
- Model factory registration for proper lifecycle management
## Next Steps:
1. Resolve character tag ambiguity with flexible parsing
2. Create IndexTTS engine configuration node
3. Implement TTS Text processor integration
4. Test full character switching + emotion control workflow
+672
View File
@@ -0,0 +1,672 @@
# Inline Edit Tags - User Guide
## 💡 Inline vs Manual Node Workflow
**ComfyUI is modular** - you can manually chain **TTS → 🎨 Step Audio EditX - Audio Editor** nodes for full control.
**Why use inline tags instead?**
- Apply edits to **specific segments** of text without multiple TTS→Edit→TTS chains
- Convenient for **SRT files** with selective editing per subtitle
- Automatic batch processing for efficiency
**For maximum control and quality**, build manual workflows. **For convenience and selective segment editing**, use inline tags.
---
## Overview
Inline edit tags allow you to apply **Step Audio EditX** post-processing effects to TTS-generated audio from ANY engine (F5-TTS, ChatterBox, Higgs Audio, VibeVoice, etc.). These tags are automatically stripped before TTS generation and applied as a second processing pass.
**Example:**
```
[Alice] This is hilarious <Laughter:3>, I can't believe it!
```
**What happens:**
1. TTS generates: "This is hilarious, I can't believe it!" (tag removed)
2. Step Audio EditX edits the audio with 3 iterations of Laughter effect
3. Final audio has natural laughter inserted
---
## Supported Edit Types
### 1. Paralinguistic Tags (Sound Insertion)
Insert non-verbal sounds at specific positions in the audio:
| Tag | Effect | Example |
|-----|--------|---------|
| `<Laughter>` or `<Laughter:2>` | Insert laughter | `Hello <Laughter> world` |
| `<Breathing>` | Insert breathing | `I'm tired <Breathing> let's rest` |
| `<Sigh>` | Insert sigh | `Okay <Sigh> fine` |
| `<Uhm>` | Insert "uhm" hesitation | `Well <Uhm> maybe` |
| `<Surprise-oh>` | Surprised "oh!" | `<Surprise-oh> I didn't know!` |
| `<Surprise-ah>` | Surprised "ah!" | `<Surprise-ah> That's amazing!` |
| `<Surprise-wa>` | Surprised "wa!" | `<Surprise-wa> Incredible!` |
| `<Confirmation-en>` | Confirmation sound | `<Confirmation-en> Yes` |
| `<Question-ei>` | Question sound | `<Question-ei> Really?` |
| `<Dissatisfaction-hnn>` | Dissatisfied "hnn" | `<Dissatisfaction-hnn> Not good` |
**Position matters:** The sound is inserted where you place the tag.
**💡 Pro Tip - Stronger Effects:** For more reliable and pronounced effects, include relevant text alongside the tag:
```
# Weaker effect
I'm happy <Laughter>
# Stronger, more guaranteed effect
I'm laughing haha <Laughter:2>
```
The TTS-generated "haha" combined with the post-processing laughter creates a more natural and pronounced effect.
### 2. Emotion Tags (Whole Segment)
Apply emotional tone to the entire audio segment:
`<emotion:VALUE>` or `<emotion:VALUE:ITERATIONS>`
**Available emotions:** happy, sad, angry, excited, calm, fearful, surprised, disgusted, confusion, empathy, embarrass, depressed, coldness, admiration
**Examples:**
```
[Alice] I'm so happy to see you! <emotion:happy>
[Bob] This is terrible news. <emotion:sad:2>
```
**Position doesn't matter:** Tag can be anywhere in the text.
### 3. Style Tags (Whole Segment)
Apply speaking style to the entire audio segment:
`<style:VALUE>` or `<style:VALUE:ITERATIONS>`
**Available styles:** whisper, serious, child, older, girl, pure, sister, sweet, exaggerated, ethereal, generous, recite, act_coy, warm, shy, comfort, authority, chat, radio, soulful, gentle, story, vivid, program, news, advertising, roar, murmur, shout, deeply, loudly, arrogant, friendly
**Examples:**
```
[Alice] Come closer, I have a secret <style:whisper>
[Bob] Listen carefully to this. <style:serious:2>
```
**Position doesn't matter:** Tag can be anywhere in the text.
### 4. Speed Tags (Whole Segment)
Adjust speaking speed for the entire audio segment:
`<speed:VALUE>` or `<speed:VALUE:ITERATIONS>`
**Available speeds:** faster, slower, more_faster, more_slower
**Examples:**
```
[Alice] Quick! We need to hurry! <speed:faster>
[Bob] Let me think about this... <speed:slower:2>
```
**Position doesn't matter:** Tag can be anywhere in the text.
### 5. Voice Restoration Tags (Whole Segment)
Restore voice resemblance after edit effects using ChatterBox Official 23-Lang Voice Changer:
`<restore>`, `<restore:N>`, or `<restore:N@M>`
**Syntax:**
- `<restore>` - 1 VC pass using original pre-edit audio as reference
- `<restore:2>` - 2 VC passes using original pre-edit audio as reference (for stronger restoration)
- `<restore:1@2>` - 1 VC pass using audio from iteration 2 as reference
**⚠️ Why You Need This:**
Edit effects (especially after multiple iterations) can **degrade voice quality and alter voice resemblance**:
- Paralinguistic effects (`<Laughter:3>`) add sounds but can muddy the voice
- Style effects (`<style:whisper:4>`) change timbre and can distort characteristics
- Multiple iterations compound these effects
The `<restore>` tag uses voice conversion to restore your original voice character while keeping the edit effects.
**Examples:**
**Basic restoration (use original voice):**
```
[Alice] I'm laughing so hard <Laughter:3> <restore>
```
- Applies 3 iterations of laughter (may degrade voice)
- Restores original voice with 1 VC pass
**Stronger restoration:**
```
[Alice] I'm laughing so hard <Laughter:3> <restore:2>
```
- Applies 3 iterations of laughter (may degrade voice)
- Restores original voice with 2 VC passes (stronger effect)
**Preserve partial effects (use intermediate iteration):**
```
[Bob] Listen carefully <style:whisper:2> <Laughter:3> <restore:1@2>
```
- Applies whisper 2 times (iterations 1-2)
- Applies laughter 3 times (iterations 3-5)
- Restores using iteration 2 audio (whispered version) with 1 VC pass
- **Result:** Keeps whisper effect but removes laughter voice degradation
**When to use iteration reference:**
- `<restore>` or `<restore:N>` - Restore completely to original clean voice
- `<restore:N@M>` - Keep some effects (like whisper) while removing others (like laughter degradation)
**Processing order:** Restore tags ALWAYS run last, after all other edits complete.
**Position doesn't matter:** Tag can be anywhere in the text.
---
## Syntax Guide
### Basic Syntax
**Default (1 iteration):**
```
<Laughter>
<emotion:happy>
<style:whisper>
<speed:faster>
```
**Custom iterations:**
```
<Laughter:3> # 3 iterations of laughter
<emotion:happy:2> # 2 iterations of happy emotion
<style:whisper:4> # 4 iterations of whisper style
```
### Multiple Tags
**Option 1 - Pipe-separated (single tag):**
```
[Alice] Hello world! <Laughter:2|style:whisper|emotion:happy>
```
**Option 2 - Separate tags:**
```
[Alice] Hello world! <Laughter:2><style:whisper><emotion:happy>
```
**Option 3 - Mixed (both work):**
```
[Alice] Hello <Laughter:2|emotion:happy> world! <style:whisper>
```
### Processing Order
When multiple tags are present:
1. **First:** Non-paralinguistic tags in order: emotion → style → speed
2. **Second:** Paralinguistic tags (preserves position for sound insertion)
3. **Last:** Voice restoration (if present)
**Example:**
```
[Alice] Hello <Laughter:2> world <style:whisper:1> <restore>
```
Execution:
1. Generate TTS: "Hello world"
2. Apply whisper style (1 iteration)
3. Insert Laughter at position (2 iterations)
4. Restore voice to original character (1 VC pass)
---
## Usage Examples
### Simple Effects
**Laughter:**
```
[Alice] That's the funniest thing I've heard <Laughter>!
```
**Multiple iterations:**
```
[Bob] I can't stop laughing <Laughter:3>, this is amazing!
```
### Combined Effects
**Emotion + Paralinguistic:**
```
[Alice] I'm so excited <Laughter:2|emotion:happy> to see you!
```
**Style + Speed:**
```
[Bob] Listen carefully <style:whisper|speed:slower>, this is important.
```
### Full Example with Character Switching
```
[Narrator] The story begins on a dark night.
[Alice|seed:42] Oh no <Surprise-oh>, what was that sound? <emotion:fearful>
[Bob] Don't worry <style:comfort>, I'll check it out. <Breathing>
[Alice] Please be careful! <style:whisper|emotion:worried>
```
### SRT Mode
```
1
00:00:00,000 --> 00:00:03,000
[Alice] Hello there <Laughter> my friend!
2
00:00:03,500 --> 00:00:06,000
[Bob|seed:123] Hi! <style:excited|emotion:happy>
3
00:00:06,500 --> 00:00:09,000
[Alice] Let me tell you a secret <style:whisper:2>
```
---
## ChatterBox 23-Lang v2 Users: Native vs Post-Processing Tags
### Understanding the Difference
**ChatterBox Official 23-Lang v2** has **native special tokens** built into the model (e.g., `<laughter>`, `<giggle>`, `<whisper>`). Our **Step Audio EditX post-processing system** uses similar-looking tags but works differently.
### Syntax Distinction
| Syntax | System | When to Use |
|--------|--------|-------------|
| `<laughter>` | **ChatterBox v2 Native** | Model generates laughter during TTS (experimental, may not work well) |
| `<laughter:1>` | **Step Audio EditX Post-Processing** | Applied AFTER TTS generation (more reliable) |
**The colon (`:`) makes the difference!**
### Examples
```
# Native ChatterBox v2 tag (model-generated during TTS)
[Alice] I'm laughing <laughter> so hard!
# Step Audio EditX tag (post-processing after TTS)
[Alice] I'm laughing <Laughter:2> so hard!
```
### Which Should You Use?
**For ChatterBox 23-Lang users:**
✅ **Use Step Audio EditX tags** (`<Laughter:2>`, etc.) - More reliable, works consistently
⚠️ **ChatterBox v2 native tags** (`<laughter>`, `<giggle>`) - Experimental, limited functionality:
- Tokens exist in vocabulary but may produce minimal/no effect
- No official documentation from ResembleAI
- Results are inconsistent
- See [ResembleAI/chatterbox #186](https://github.com/resemble-ai/chatterbox/issues/186)
**Console Warning:**
If you use ChatterBox v2 native tags without the colon, you may see:
```
⚠️ Using ChatterBox v2 native tag '<laughter>' - experimental feature with limited effect.
For more reliable results, use Step Audio EditX: '<Laughter:2>'
```
### Compatibility Note
Step Audio EditX tags work with **ALL engines** (F5-TTS, ChatterBox, Higgs Audio, VibeVoice), not just ChatterBox. Native ChatterBox tags only work with ChatterBox 23-Lang v2.
---
## Important Limitations
### ⚠️ Language Support Warning
**Step Audio EditX supports ONLY:**
- **Mandarin Chinese** (primary)
- **English**
- **Sichuanese** (Chinese dialect)
- **Cantonese** (Chinese dialect)
- **Japanese**
- **Korean**
**If using other languages** (French, German, Spanish, etc.):
❌ **PROBLEM:** Step Audio EditX will process non-supported languages as English or Chinese, which will:
- **Lose the original accent** - French audio may lose French pronunciation
- **Distort the audio** - The model tries to "fix" unknown languages
- **Generate poor quality** - Editing may sound unnatural or broken
✅ **SOLUTION:**
1. **Don't use edit tags** with unsupported languages
2. **Use only supported languages** when edit tags are needed
3. **Test carefully** if mixing languages - some distortion may be acceptable
**Example - BAD:**
```
[Alice:fr] Bonjour mon ami <Laughter> # French + edit tag = accent loss/distortion
```
**Example - GOOD:**
```
[Alice:fr] Bonjour mon ami # French without edit tag = preserves accent
[Alice:en] Hello my friend <Laughter> # English + edit tag = works perfectly
```
### Duration Limits
**Minimum:** 0.5 seconds - Segments shorter than this will skip editing
**Maximum:** 30 seconds - Segments longer than this will skip editing
**Warning messages:**
```
⚠️ Segment too short (0.3s < 0.5s). Edit tags will be skipped.
⚠️ Segment too long (35.2s > 30s). Edit tags will be skipped. Split your text into shorter segments.
```
**Solution for long segments:**
```
# BAD - Too long
[Alice] [Very long paragraph that exceeds 30 seconds...] <Laughter>
# GOOD - Split into multiple segments
[Alice] First part of the paragraph. <Laughter>
[Alice] Second part continues here.
[Alice] Final part with the conclusion. <emotion:happy>
```
### Batch Processing (Automatic)
Edit tags are **automatically batched** for efficiency:
✅ **What happens:**
1. ALL TTS audio is generated first (for all segments/subtitles)
2. Step Audio EditX loads ONCE
3. ALL segments with edit tags are processed together
4. Engine unloads
✅ **Benefits:**
- Faster generation (engine loads once, not per segment)
- Less VRAM thrashing
- Better performance
**You don't need to do anything** - batching happens automatically in both regular and SRT modes.
---
## Performance Tips
### 1. Use Iterations Wisely
More iterations = stronger effect BUT **can alter voice quality and resemblance**:
```
<Laughter:1> # Subtle effect, minimal voice change (recommended)
<Laughter:2> # Moderate effect, slight voice alteration (acceptable)
<Laughter:3> # Strong effect, noticeable voice change (risky)
<Laughter:4> # Very strong effect, significant voice distortion (not recommended)
```
⚠️ **Voice Resemblance Warning:**
- **1-2 iterations:** Generally safe, minimal voice alteration
- **3+ iterations:** High risk of voice distortion and quality degradation
- Each iteration processes the audio again, which can degrade the original voice characteristics
**Recommendation:** Start with 1 iteration, only increase to 2 if needed. Avoid 3+ unless you're willing to accept voice changes.
**✅ Voice Restoration Available:** Use the `<restore>` tag to restore original voice resemblance after editing. See the Voice Restoration Tags section above for details.
### 2. Combine Tags Efficiently
**Less efficient (multiple passes):**
```
[Alice] Text here <emotion:happy>
[Alice] More text <emotion:happy>
[Alice] Even more <emotion:happy>
```
**More efficient (apply once):**
```
[Alice|emotion:happy] Text here. More text. Even more.
```
Though with inline tags, segment-level parameters are cleaner for consistent effects across multiple sentences.
### 3. Position Paralinguistic Tags Carefully
**Position matters for sound insertion:**
```
Hello <Laughter> world # Laughter after "Hello"
Hello world <Laughter> # Laughter at end
<Laughter> Hello world # Laughter at start
```
Choose the position that sounds most natural for your use case.
---
## Troubleshooting
### Problem: Edit tags not applied
**Possible causes:**
1. **Segment too short/long** - Check duration (0.5s - 30s range)
2. **Syntax error** - Verify tag format: `<type:value:iterations>`
3. **Model not downloaded** - First use triggers download (be patient)
**Check console output:**
```
🎨 Applying Step Audio EditX processing to 3 segments...
📝 Segment 1 - Applying edit tags:
🎨 Applying paralinguistic: Laughter:2
✅ Edit post-processing complete for 3 segments
```
### Problem: Audio quality degraded
**Possible causes:**
1. **Too many iterations** - Reduce iterations (try 1-2 instead of 3-4)
2. **Incompatible tags** - Some emotion+style combinations may clash
3. **Unsupported language** - Check language support (see warning above)
**Solution:**
- Use fewer iterations
- Test individual tags separately
- Use only supported languages with edit tags
### Problem: "Segment too long" warning
**Solution:** Split text into shorter segments:
```
# Instead of:
[Alice] [300-word monologue that lasts 45 seconds] <Laughter>
# Do this:
[Alice] First paragraph of about 2-3 sentences.
[Alice] Second paragraph continues. <Laughter>
[Alice] Third paragraph concludes the thought. <emotion:happy>
```
---
## Advanced Usage
### Combining with Segment Parameters
Edit tags work with ALL segment parameters:
```
[Alice|seed:42|temperature:0.8|speed:1.2] Hello <Laughter:2> friend! <emotion:happy>
```
Processing:
1. TTS uses seed:42, temp:0.8, speed:1.2
2. Edit tags applied after generation
### Using with Language Switching
**Supported languages ONLY:**
```
[Alice:en] Hello my friend <Laughter> # English - works ✓
[Bob:zh] 你好朋友 <Laughter> # Chinese - works ✓
[Alice:ja] こんにちは友達 <Laughter> # Japanese - works ✓
[Bob:ko] 안녕 친구 <Laughter> # Korean - works ✓
```
**Unsupported languages - AVOID:**
```
[Alice:fr] Bonjour mon ami <Laughter> # French - will distort! ✗
[Bob:de] Hallo mein Freund <Laughter> # German - will distort! ✗
[Alice:es] Hola mi amigo <Laughter> # Spanish - will distort! ✗
```
### Creating Natural Conversations
```
[Narrator] The interview begins.
[Host] Welcome to the show! <emotion:excited>
[Guest] Thank you for having me. <style:warm>
[Host] So tell us <style:chat>, what's your secret? <Laughter>
[Guest] Well <Uhm> it's quite simple actually. <style:story>
[Host] That's amazing <Surprise-oh>! <emotion:surprised>
```
---
## Best Practices
### 1. Start Simple
Begin with single tags before combining:
```
# Step 1: Test basic tag
[Alice] Hello <Laughter>
# Step 2: Add iterations
[Alice] Hello <Laughter:2>
# Step 3: Combine effects
[Alice] Hello <Laughter:2|emotion:happy>
```
### 2. Match Tags to Context
Choose appropriate effects for the situation:
```
# Scary scene
[Alice] What was that? <Surprise-oh|emotion:fearful>
# Happy scene
[Bob] We did it! <Laughter:2|emotion:excited>
# Sad scene
[Alice] I can't believe it's over. <Sigh|emotion:sad>
# Secret conversation
[Bob] Come closer. <style:whisper|emotion:calm>
```
### 3. Test and Iterate
1. Generate with basic tag
2. Listen to result
3. Adjust iterations if needed
4. Try different tag combinations
5. Find what works best for your use case
### 4. Use Language Hints Correctly
**Always match edit language to TTS language:**
```
# GOOD - Language consistency
[Alice:en] English text here <Laughter> # Both English
[Bob:zh] 中文文本 <Laughter> # Both Chinese
# BAD - Language mismatch
[Alice:fr] French text <Laughter> # French TTS + English edit model = distortion
```
---
## Reference
### All Paralinguistic Tags
`<Breathing>`, `<Laughter>`, `<Sigh>`, `<Uhm>`, `<Surprise-oh>`, `<Surprise-ah>`, `<Surprise-wa>`, `<Confirmation-en>`, `<Question-ei>`, `<Dissatisfaction-hnn>`
### All Emotions
`<emotion:happy>`, `<emotion:sad>`, `<emotion:angry>`, `<emotion:excited>`, `<emotion:calm>`, `<emotion:fearful>`, `<emotion:surprised>`, `<emotion:disgusted>`, `<emotion:confusion>`, `<emotion:empathy>`, `<emotion:embarrass>`, `<emotion:depressed>`, `<emotion:coldness>`, `<emotion:admiration>`
### All Styles
`<style:whisper>`, `<style:serious>`, `<style:child>`, `<style:older>`, `<style:girl>`, `<style:pure>`, `<style:sister>`, `<style:sweet>`, `<style:exaggerated>`, `<style:ethereal>`, `<style:generous>`, `<style:recite>`, `<style:act_coy>`, `<style:warm>`, `<style:shy>`, `<style:comfort>`, `<style:authority>`, `<style:chat>`, `<style:radio>`, `<style:soulful>`, `<style:gentle>`, `<style:story>`, `<style:vivid>`, `<style:program>`, `<style:news>`, `<style:advertising>`, `<style:roar>`, `<style:murmur>`, `<style:shout>`, `<style:deeply>`, `<style:loudly>`, `<style:arrogant>`, `<style:friendly>`
### All Speeds
`<speed:faster>`, `<speed:slower>`, `<speed:more_faster>`, `<speed:more_slower>`
### Voice Restoration
`<restore>`, `<restore:N>`, `<restore:N@M>` (where N = VC passes 1-5, M = reference iteration number)
---
## FAQ
**Q: Can I use edit tags with any TTS engine?**
A: Yes! Edit tags work with F5-TTS, ChatterBox, Higgs Audio, VibeVoice, and any future engines.
**Q: Do edit tags slow down generation?**
A: Slightly. The TTS generates normally, then Step Audio EditX loads once and processes all tagged segments in batch. Total time depends on number of segments and iterations.
**Q: Can I use edit tags in SRT mode?**
A: Yes! Batch processing is automatically applied - all subtitles are generated first, then all edits are applied at once.
**Q: What if I make a typo in a tag?**
A: Unknown tags are ignored and stripped from the text. Check console for warnings.
**Q: Can I nest tags?**
A: No. Use pipe syntax or separate tags: `<tag1|tag2>` or `<tag1><tag2>`
**Q: Do edit tags work with ChatterBox special tokens?**
A: Yes, but ChatterBox v2 uses different syntax (`<giggle>`, `<whisper>` built into model). Our edit tags are post-processing, not model-native.
**Q: Will edit tags affect my voice cloning?**
A: Yes, they can. TTS generates with your voice reference normally, but each edit iteration can alter voice quality and resemblance:
- **1 iteration:** Minimal impact, usually safe
- **2 iterations:** Slight alteration, generally acceptable
- **3+ iterations:** Noticeable voice change, may lose original voice characteristics
Use the minimum iterations needed for your desired effect. To restore voice resemblance after editing, use the `<restore>` tag which applies ChatterBox Voice Changer to recover the original voice character.
**Q: Can I disable edit tags temporarily?**
A: Just remove them from your text. No special setting needed.
---
## Support
For issues, feature requests, or questions:
- GitHub Issues: https://github.com/diodiogod/TTS-Audio-Suite/issues
- Check console output for detailed processing information
- Test with simple examples first before complex combinations
---
**Last Updated:** 2025-12-08
**Compatible with:** TTS Audio Suite v4.4.0+
+214
View File
@@ -0,0 +1,214 @@
# IndexTTS-2 Emotion Control Guide
IndexTTS-2 features advanced emotion control capabilities that allow you to precisely control the emotional expression of generated speech. This guide covers all available emotion control methods and their applications.
## Overview
IndexTTS-2 supports multiple emotion control methods that can be combined for sophisticated emotional expression:
- **Direct Audio Reference**: Use any audio file as an emotion reference
- **Character Voices**: Use character audio references from the Character Voices node
- **Emotion Vectors**: Manual 8-emotion slider control with precise values
- **Text Emotion**: AI-powered QwenEmotion analysis from text descriptions with dynamic templates
- **Character Tag Emotions**: Per-character emotion control using `[Character:emotion_ref]` syntax
## Emotion Control Priority
You can only connect to the Engine node one source of control emotion: Either audio, text, or vectors.
When using tags on the text iself, **Character tag emotions** (highest priority) - `[Alice:angry_bob]` overrides all other emotion control settings for that character segment
## Method 1: Direct Audio Reference
Connect any audio file directly to the IndexTTS-2 Engine's `emotion_control` input.
**How it works:**
- IndexTTS-2 analyzes the emotional characteristics of your reference audio
- The emotional style is applied to all generated speech
- Works with any audio format (WAV, MP3, etc.)
**Best practices:**
- Use audio clips with clear emotional expression
- Choose audio with consistent voice characteristics for best results
- Avoid background music or noise
**Example:**
```
AUDIO node → IndexTTS-2 Engine (emotion_control)
```
## Method 2: Character Voices Audio Reference
Use the `opt_narrator` output from the 🎭 Character Voices node as an emotion reference.
**Setup:**
1. Add a 🎭 Character Voices node
2. Select a voice with the desired emotional expression
3. Connect `opt_narrator` output to IndexTTS-2 Engine `emotion_control` input
**Advantages:**
- Leverages your existing voice library
- Consistent character-based emotions
- Easy to manage and organize
**Example workflow:**
```
🎭 Character Voices (David_Attenborough) → opt_narrator → IndexTTS-2 Engine (emotion_control)
```
## Method 3: Emotion Vectors
Use the 🌈 IndexTTS-2 Emotion Vectors node for precise manual control over 8 different emotions.
**Available emotions:**
- **Happy**: Joy, excitement, positivity (0.0-1.2)
- **Angry**: Aggression, frustration, intensity (0.0-1.2)
- **Sad**: Melancholy, sorrow, downcast tone (0.0-1.2)
- **Surprised**: Amazement, shock, wonder (0.0-1.2)
- **Afraid**: Fear, anxiety, nervousness (0.0-1.2)
- **Disgusted**: Revulsion, displeasure, rejection (0.0-1.2)
- **Calm**: Peaceful, relaxed, steady (0.0-1.2)
- **Melancholic**: Thoughtful sadness, wistfulness (0.0-1.2)
**Usage tips:**
- Values above 1.0 create more intense emotional expression BUT MAY interfear with the cloned voice resemblance
- Combine multiple emotions for complex feelings (e.g., 0.8 Happy + 0.3 Surprised = excited joy)
- Start with single emotions, then experiment with combinations
- Use the `random` buttom to get a completely random emotion pattern. Might be too strong.
## Method 4: Text Emotion (Dynamic Analysis)
Use the 🌈 IndexTTS-2 Text Emotion node for AI-powered emotion analysis with dynamic templates.
### Static Text Emotion
Provide a simple emotion description that applies to all text segments:
```
Input: "angry and frustrated"
Result: All speech generated with angry, frustrated emotion
```
### Dynamic Templates with {seg}
Use the `{seg}` placeholder for contextual, per-segment emotion analysis:
**Template examples:**
- `"Happy character speaking: {seg}"` - Cheerful narrator
- `"Angry boss yelling: {seg}"` - Aggressive authority figure
- `"Calm meditation guide: {seg}"` - Peaceful instructor
- `"Excited game show host: {seg}"` - Energetic presenter
**How dynamic templates work:**
1. IndexTTS-2 processes each text segment separately
2. `{seg}` gets replaced with the actual segment text
3. QwenEmotion analyzes the combined context + content
4. Unique emotion vector generated for each segment
**Example:**
```
Template: "Worried parent speaking: {seg}"
Segment: "Where have you been?"
Analysis: "Worried parent speaking: Where have you been?"
Result: Anxious, concerned vocal expression
```
---
## Character Tag Emotion Control
Control emotions per character using inline tags in your text: `[Character:emotion_ref]`
**Syntax:**
```
[CharacterName:emotion_reference]
```
**emotion_reference options:**
- Any character name from your voices library (uses that character's voice as emotion)
- Custom emotion references
**Examples:**
```
Hello everyone! [Alice:happy_sarah] I'm so excited to be here today!
[Bob:angry_tom] That's completely unacceptable behavior.
[Narrator:David] Meanwhile, in a distant galaxy...
*assuming happy_sarah, angry_tom and David are alias or character voices in yout folder with that name
```
\*assuming happy_sarah, angry_tom and David are alias or character voices in yout folder with that name
**Character tag priority:**
- Character tags override ALL other emotion settings for that specific character
- Other characters use global emotion settings
- Allows mixing different emotions in the same audio
## Emotion Alpha Control
The `emotion_alpha` parameter on the IndexTTS-2 Engine controls the intensity of emotion application:
**Values:**
- **0.0**: No emotion applied (neutral voice)
- **0.5**: 50% emotion blend (subtle emotional influence)
- **1.0**: Full emotion intensity (standard recommended setting)
- **1.5**: 150% enhanced emotion (more dramatic)
- **2.0**: Maximum emotion intensity (very dramatic)
## Practical Workflow Examples
### Example 1: Multi-Character Drama with Individual Emotions
```text
[Alice:happy_sarah] Welcome to our cooking show!
[Bob:serious_narrator] Today we'll be making pasta.
[Alice:excited_sarah] I can't wait to get started!
```
**Setup:**
- No global emotion control needed
- Each character gets individual emotion via tags
- `emotion_alpha=1.0` for more expressiveness
### Example 2: Mixed Emotion Control
**Global setup:**
- 🌈 IndexTTS-2 Text Emotion: `"Cheerful host presenting: {seg}"`
- `emotion_alpha=0.8`
**Text with overrides:**
```text
Welcome to our show! [Bob:serious_narrator] But first, a serious announcement.
[Alice:excited_sarah] Now back to our regular programming!
```
**Result:**
- Default segments use cheerful host emotion
- Bob's line uses serious narrator emotion (overrides global)
- Alice's line uses excited emotion (overrides global)
---
This comprehensive emotion control system gives you unprecedented flexibility in creating expressive, emotionally rich TTS audio for any application.
+619
View File
@@ -0,0 +1,619 @@
# New TTS Engine Implementation Guide
*Comprehensive guide for LLMs to implement new TTS engines in TTS Audio Suite*
---
Don't ever make PLACEHOLDERS during implementation.
## Pre-Implementation Analysis
### 1. Research the Original Implementation
**📁 Reference Storage:**
- Clone the original implementation to: `ComfyUI_TTS_Audio_Suite/IgnoredForGitHubDocs/For_reference/[ENGINE_NAME]/`
- Study the original codebase thoroughly
- Document all features, parameters, and capabilities
**🔍 Key Areas to Analyze:**
- **Audio Format**: Sample rate, bit depth, channels, tensor format
- **Model Architecture**: Input/output requirements, tokenization, generation process
- **Parameters**: All generation parameters, their ranges, default values
- **Dependencies**: Required packages, versions, potential conflicts
- **Unique Features**: Special capabilities not found in other engines
- **Language Support**: Monolingual vs multilingual, language codes/tags
- **Voice Control**: How voices are defined, selected, and applied
### 2. Dependency Analysis
*⚠️ Problematic Dependencies Handling:**
- If dependencies conflict with existing packages → Add to `scripts/install.py`
- If dependencies require specific versions → Document in engine requirements
**Example problematic patterns:**
- Downgrades numpy/librosa/transformers
- Conflicts with other TTS engines
---
## Project Architecture Understanding
### Core Architecture Pattern
```
⚙️ Engine Node (UI Layer real node on ComfyUI)
↓
🔌 Engine Adapter (Optional? - Parameter Translation)
↓
🏭 Engine Processor (Engine-Specific Logic)
```
### File Structure Template
```
engines/[ENGINE_NAME]/
├── __init__.py # Engine initialization
├── [engine_name].py # Core engine implementation
├── [engine_name]_downloader.py # Model auto-download (optional, should use universal downloader code not it's own)
├── stateless_wrapper.py # Thread-safe wrapper (if needed)
└── models/ # Model-specific code (if needed)
engines/adapters/
└── [engine_name]_adapter.py # Unified interface adapter
nodes/engines/
└── [engine_name]_engine_node.py # UI configuration node
nodes/[engine_name]/ # Engine-specific processors
├── [engine_name]_processor.py # Main TTS processor
├── [engine_name]_srt_processor.py # SRT processor
└── [engine_name]_vc_processor.py # Voice conversion (if applicable)
nodes/[engine_name]_special/ # Special features (if any)
└── [engine_name]_special_node.py # Special functionality nodes
```
---
## Implementation Steps
### Phase 1: Basic UI Engine Implementation
#### Step 1: Create Core Engine Implementation
**File:** `engines/[ENGINE_NAME]/[engine_name].py`
**Key Implementation Notes:**
- **Audio Format**: Always return `torch.Tensor` in shape `[1, samples]` or `[batch, samples]`
- **Sample Rate**: Must match engine's native sample rate, handle conversion in adapter if needed
- **Device Management**: Support "auto", "cuda", "cpu" device selection
- **Clear VRAM Integration**: CRITICAL - Implement `.to()` method and device checking for ComfyUI model management (see Step 1b below)
- **Error Handling**: Graceful fallbacks, informative error messages
#### Step 1b: Implement Clear VRAM Integration (CRITICAL)
**⚠️ REQUIRED FOR ALL NEW ENGINES**
ComfyUI's "Clear VRAM" button offloads models to CPU. Your engine MUST support this or it will cause device mismatch errors.
**Implementation Requirements:**
**1. Add `.to()` Method to Engine Class**
This method moves ALL model components between devices (CPU ↔ CUDA).
```python
def to(self, device):
"""
Move all model components to the specified device.
Critical for ComfyUI model management - ensures all components move together
when models are detached to CPU and later reloaded to CUDA.
"""
self.device = device
# Move the underlying model/engine if loaded
if self._model is not None:
# OPTION 1: Simple engines with single model
if hasattr(self._model, 'to'):
self._model = self._model.to(device)
# OPTION 2: Complex engines with multiple components
# Example: ChatterBox has model.s3gen, model.t3, model.voice_encoder
for component_name in ['s3gen', 't3', 'voice_encoder', 'tokenizer']:
if hasattr(self._model, component_name):
component = getattr(self._model, component_name)
if hasattr(component, 'to'):
setattr(self._model, component_name, component.to(device))
# OPTION 3: Deeply nested engines (like Index-TTS)
# Call .to() on engine itself for recursive moving, with manual fallback
if hasattr(self._model, 'to'):
self._model = self._model.to(device)
else:
# Manually move known nested components
for attr_name in ['semantic_model', 'gpt_model', 'vocoder']:
if hasattr(self._model, attr_name):
component = getattr(self._model, attr_name)
if hasattr(component, 'to'):
setattr(self._model, attr_name, component.to(device))
# Update device attribute on model if it exists
if hasattr(self._model, 'device'):
self._model.device = torch.device(device) if isinstance(device, str) else device
return self
```
**2. Add Device Checking Before Generation**
Check if model was offloaded to CPU and reload to CUDA before generation.
```python
def generate(self, text, reference_audio, **kwargs):
"""Main generation method"""
# CRITICAL: Check and reload model if offloaded to CPU
target_device = "cuda" if torch.cuda.is_available() else "cpu"
if self._model is not None:
# Check current device of model
# Strategy 1: Check first parameter device (most reliable)
if hasattr(self._model, 'parameters'):
try:
first_param = next(self._model.parameters())
current_device = str(first_param.device)
if current_device != target_device:
print(f"🔄 Reloading {self.__class__.__name__} from {current_device} to {target_device}")
# Try to find wrapper and call wrapper.model_load()
wrapper_found = False
try:
from utils.models.unified_model_interface import unified_model_interface
if hasattr(unified_model_interface, 'model_manager'):
for cache_key, wrapper in unified_model_interface.model_manager._model_cache.items():
model = wrapper.model if hasattr(wrapper, 'model') else None
if model is self:
wrapper.model_load(target_device)
print(f"✅ Reloaded via wrapper - ComfyUI tracking in sync")
wrapper_found = True
break
# Check through SimpleModelWrapper if present
elif hasattr(model, 'model') and model.model is self:
wrapper.model_load(target_device)
print(f"✅ Reloaded via wrapper (unwrapped)")
wrapper_found = True
break
if not wrapper_found:
# Fallback: direct .to() and check if already registered
self.to(target_device)
# Check ComfyUI's current_loaded_models for existing wrapper
try:
import comfy.model_management as model_management
already_registered = False
if hasattr(model_management, 'current_loaded_models'):
for wrapper in model_management.current_loaded_models:
if hasattr(wrapper, 'model_info') and wrapper.model_info.engine == "[engine_name]":
wrapper.model_load(target_device)
print(f"✅ Reloaded via existing ComfyUI wrapper")
already_registered = True
break
if not already_registered:
# Re-register with ComfyUI
from utils.models.comfyui_model_wrapper.base_wrapper import ComfyUIModelWrapper, ModelInfo, SimpleModelWrapper
model_size = ComfyUIModelWrapper.calculate_model_memory(self)
wrapped_model = SimpleModelWrapper(self)
model_info = ModelInfo(
model=wrapped_model,
model_type="tts",
engine="[engine_name]",
device=target_device,
memory_size=model_size,
load_device=target_device
)
new_wrapper = ComfyUIModelWrapper(wrapped_model, model_info)
model_management.current_loaded_models.append(new_wrapper)
print(f"✅ Re-registered with ComfyUI model management")
except Exception as e:
print(f"⚠️ Re-registration failed: {e}")
except Exception as e:
print(f"⚠️ Wrapper reload failed ({e}), using direct .to()")
self.to(target_device)
except StopIteration:
pass
# Strategy 2: For models with nested components
elif hasattr(self._model, 'semantic_model') and hasattr(self._model.semantic_model, 'parameters'):
try:
first_param = next(self._model.semantic_model.parameters())
current_device = str(first_param.device)
# ... same reload logic as above
except StopIteration:
pass
# Continue with normal generation
# ... your generation code here
```
**3. Important Device Checking Notes:**
- **Always check against INTENDED device** (`"cuda"` if available), NOT `self.device` which gets updated to "cpu" after offload
- **Search for existing wrapper first** before creating new ones to avoid duplicates
- **Use wrapper.model_load()** when possible to keep ComfyUI tracking in sync
- **Fallback to direct .to()** if wrapper not found, then re-register
- **Check both unified interface cache AND ComfyUI's current_loaded_models** for wrappers
**4. Key Implementation Notes:**
- **Always check against INTENDED device** (`"cuda"` if available), NOT `self.device`
- **Search for existing wrapper first** to avoid duplicates
- **Use wrapper.model_load()** when possible to keep ComfyUI tracking in sync
- **Check both unified interface cache AND current_loaded_models**
#### Step 2: Create Model Downloader
**File:** `engines/[ENGINE_NAME]/[engine_name]_downloader.py`
**Follow Unified Download Pattern:**
```python
from utils.downloads.unified_downloader import UnifiedDownloader
```
#### Step 3: Create Engine Adapter
**File:** `engines/adapters/[engine_name]_adapter.py`
#### Step 4: Create Engine Configuration Node
**File:** `nodes/engines/[engine_name]_engine_node.py`
### Phase 2: Unified Systems Integration
#### Step 5: Integrate with Unified Model Loading
**Use ComfyUI Model Management:**
```python
from utils.models.unified_model_interface import UnifiedModelInterface
from utils.models.comfyui_model_wrapper import ComfyUIModelWrapper
```
#### Step 6: Implement Caching System
**Cache Integration:**
```python
from utils.audio.cache import UnifiedCacheManager
from utils.audio.audio_hash import create_content_hash
```
#### Step 7: Character Switching Integration
**Use Unified Character System:**
```python
from utils.text.character_parser import CharacterParser
from utils.voice.discovery import get_character_mapping
```
#### Step 8: Language Switching Integration
**Use Unified Language System:**
```python
from utils.models.language_mapper import LanguageMapper
class [EngineClass]Processor:
def __init__(self):
self.language_mapper = LanguageMapper()
def generate_with_language(self, text, language, **params):
# Map language code
engine_language = self.language_mapper.map_language(
language_code=language,
engine_type="[engine_name]"
)
# Apply language-specific model loading if needed
if self.supports_multiple_languages:
self.load_language_model(engine_language)
# Generate with language parameter
return self.engine.generate(text, language=engine_language, **params)
```
#### Step 9: Pause Tag Integration
**Use Unified Pause System, search for the unified code**
#### Step 9.5: Implement Interrupt Handling (CRITICAL)
**⚠️ REQUIRED FOR ALL NEW ENGINES - Users Must Be Able to Cancel Operations**
ComfyUI provides an interrupt mechanism that allows users to stop long-running operations (like SRT generation with many segments). Your processors MUST check for interruption signals.
**Import and Check Pattern:**
```python
import comfy.model_management as model_management
# In your main processing loops:
def generate_srt_speech(self, srt_content: str, ...):
# ... setup code ...
for i, segment in enumerate(srt_segments):
# Check for interruption before processing each segment
if model_management.interrupt_processing:
raise InterruptedError(f"Engine SRT segment {i+1}/{len(srt_segments)} interrupted by user")
# Your generation code...
```
**Where to Add Interrupt Checks:**
1. **SRT Processors** - Add checks in main subtitle/segment loops:
- Before processing each subtitle/segment
- Before processing character segments within a subtitle
- Before timing assembly (after all audio generation)
2. **Text Processors** - Add checks in main generation loops:
- Before character setup/loading
- Before processing each character segment
- Before processing chunks (if chunking is enabled)
3. **Special Feature Processors** - Add checks in long-running operations:
- Before each major processing step
- At natural breakpoints (before/after audio generation)
**Example SRT Processor Structure:**
```python
class EngineSRTProcessor:
def generate_srt_speech(self, srt_content, ...):
# Pre-generation check
if model_management.interrupt_processing:
raise InterruptedError("Setup interrupted")
# Main segment loop
for i, segment in enumerate(srt_segments):
if model_management.interrupt_processing:
raise InterruptedError(f"Segment {i+1}/{len(srt_segments)} interrupted")
# Character switching support
for char_segment in char_segments:
if model_management.interrupt_processing:
raise InterruptedError(f"Character {char} interrupted")
# ... generate audio ...
# Pre-assembly check
if model_management.interrupt_processing:
raise InterruptedError("Assembly interrupted")
# Timing assembly...
```
**Real Examples:**
- See `nodes/higgs_audio/higgs_audio_srt_processor.py` - SRT interrupts
- See `nodes/vibevoice/vibevoice_processor.py` - TTS processor interrupts
- See `nodes/chatterbox_official_23lang/chatterbox_official_23lang_srt_processor.py` - Complex interrupts
#### Step 10: Create Main TTS Processor
**File:** `nodes/[engine_name]/[engine_name]_processor.py`
#### Step 11: Test TTS Text Implementation
BUT rememeber to register all nodes so they load and work on comfyui.
Also to test, requirements and dependencies need to be added.
**Ask suer to Test Checklist:**
- [ ] Basic text generation works
- [ ] Character switching works with `[CharacterName] text`
- [ ] Language switching works (if applicable)
- [ ] Pause tags work with `[pause:1.5s]`
- [ ] Caching works (same input = cached output)
- [ ] Model auto-download works
- [ ] VRAM management works (model unloads)
- [ ] Different parameter combinations work
- [ ] **Interrupt handling works** - User can stop SRT generation and it stops within ~1 segment
### Phase 4: SRT Implementation
#### Step 12: Analyze SRT Strategies
**Study Existing SRT Implementations:**
- **ChatterBox**: Sequential processing with language grouping
- **F5-TTS**: Language grouping with chunking
- **Higgs Audio**: Character-based processing
- VibeVoice?
**Choose Strategy:**
1. **Sequential**: Process each subtitle line individually
2. **Language Grouping**: Group by language, then process
3. **Character Grouping**: Group by character, then batch process
4. **Hybrid**: Combine multiple strategies
#### Step 13: Create SRT Processor
**File:** `nodes/[engine_name]/[engine_name]_srt_processor.py`
### Phase 5: Special Features Implementation
#### Step 14: Identify Special Features
**Common Special Features:**
- **Speech Editing** (F5-TTS): Edit specific words in audio
- **Voice Conversion** - speech2speech (ChatterBox): Convert voice characteristics
- **Multi-Speaker** (Some engines): Multiple speakers in one generation
- **Style Control**: Emotion, speaking rate, emphasis
#### Step 15: Implement Special Features
Create Dedicated Nodes
---
## Unified Systems Integration
### Character Voice System
**Files to Study:**
- `utils/voice/discovery.py` - Voice file discovery
- `utils/text/character_parser.py` - Character tag parsing
- `nodes/shared/character_voices_node.py` - Character voice management
### Language System
**Files to Study:**
- `utils/models/language_mapper.py` - Language code mapping
- Engine-specific language model files
### Pause Tag System
**Files to Study:**
- `utils/text/pause_processor.py` - Pause tag parsing and generation
**Integration Pattern:**
### Model Management
**Files to Study:**
- `utils.models/unified_model_interface.py` - Unified model loading
- `utils/models/comfyui_model_wrapper.py` - ComfyUI integration
---
## Documentation Updates
### README.md Updates
#### 1. Features Section
Add engine to the features list with its unique capabilities.
#### 2. What's New Section
Add changelog entry for the new engine.
#### 3. Model Download Section
Add download instructions and model requirements.
#### 4. Supported Engines Table
Update the engines comparison table.
### Example README Addition:
```markdown
## What's New in v4.X.X
### 🚀 New [Engine Name] TTS Engine
- High-quality text-to-speech with [unique feature]
- Support for [languages/voices/special capabilities]
- Integrated with unified interface (TTS Text, SRT, Voice Changer)
- Auto-download models with one click
## Features
### 🎤 [Engine Name] TTS Engine
- **[Unique Feature 1]**: Description of what makes this engine special
- **[Unique Feature 2]**: Another special capability
- **Multi-language support**: List of supported languages (if applicable)
- **Voice cloning**: Description of voice capabilities (if applicable)
```
---
## Implementation Phase Strategy
### Phase 1: Foundation (Implement First)
1. Core engine implementation
2. Basic text generation (With character switching,
- Language switching (if applicable)
- Pause tag support
- Caching system
- VRAM management)
3. Model loading and downloading
4. Engine configuration node
5. Integration with TTS Text node
**Stop here and test with user before proceeding**
### Phase 2: SRT Support
1. SRT processor implementation
2. Timing and assembly integration
3. Character switching in SRT
4. Performance optimization
### Phase 3: Special Features
1. Engine-specific unique features
2. Special nodes for unique capabilities
3. Advanced parameter controls
### Phase 4: Documentation and Polish
1. README updates
2. Example workflows
3. Performance testing
4. Error handling improvements
---
## Critical Integration Points
### Must Use These Unified Systems
#### ✅ Required Integrations
- **UnifiedModelInterface** - Model loading
- **ComfyUIModelWrapper** - VRAM management
- **UnifiedCacheManager** - Audio caching
- **CharacterParser** - Character tag parsing
- **PauseTagProcessor** - Pause tag handling
- **LanguageMapper** - Language code mapping
- **UnifiedDownloader** - Model downloads
- **model_management.interrupt_processing** - Interrupt signal checking (CRITICAL for cancellation)
#### ❌ Never Duplicate These
- Character parsing logic
- Pause tag parsing logic
- Language mapping logic
- Cache key generation
- Model management logic
- Audio format conversion utilities
---
+280
View File
@@ -0,0 +1,280 @@
# ⚙️ Per-Segment Parameter Switching Guide
## Overview
The TTS Audio Suite supports **per-segment parameter control** through inline tags. This allows you to override TTS generation parameters (seed, temperature, cfg, speed, etc.) on a per-segment basis, providing fine-grained control over individual audio segments without modifying node-level defaults.
Parameters are applied **only to the current segment** and automatically revert to node defaults for subsequent segments.
---
## ✨ Key Features
- **⚙️ Per-Segment Control** - Override parameters for individual text segments or SRT subtitle entries
- **🔀 Order-Independent** - Specify parameters in any order: `[Alice|seed:42|temp:0.5]` or `[seed:42|Alice|temp:0.5]`
- **📝 Alias Support** - Use shortcuts like `temp` (temperature), `cfg_weight` (cfg), `exag` (exaggeration)
- **🔤 Case-Insensitive** - `[SEED:42]`, `[Seed:42]`, `[seed:42]` all work identically
- **🚨 Smart Filtering** - Unsupported parameters are silently ignored with warnings (e.g., `[speed:2.0]` on ChatterBox)
- **📺 SRT Support** - Apply parameters to individual SRT subtitle lines
- **🔙 Backward Compatible** - Works alongside character switching: `[de:Alice|seed:42|temp:0.7]`
---
## 🚀 Quick Start
### Basic Parameter Syntax
**Format:** `[character|parameter:value]` or `[parameter:value|character]`
```
[Alice|seed:42] Hi there with a specific seed!
[Bob|temperature:0.5] Bob speaks more deterministically.
[Alice|seed:42|temperature:0.7] Alice with both seed and temperature.
[seed:42|Alice] Order doesn't matter!
```
### Real-World Examples
#### Consistent Character Voice
```
[Alice|seed:42] This is the first segment of Alice.
[Alice|seed:42] This is the second segment, sounding identical.
[Alice|seed:42] Third segment with same seed.
```
#### Varied Voice Characteristics
```
[Alice|temperature:0.3] Alice speaks carefully and precisely.
[Alice|temperature:0.8] Alice speaks more creatively and varied.
[Alice|temperature:0.3] Alice is precise again.
```
#### SRT Example with Parameters
```srt
1
00:00:01,000 --> 00:00:04,000
[Alice|seed:42] Hello! I'm Alice and this line has a specific seed.
2
00:00:04,500 --> 00:00:09,500
[Bob|seed:100] And I'm Bob with my own unique seed.
3
00:00:10,000 --> 00:00:14,000
[Alice|seed:42] Alice again with the same seed as segment 1.
```
---
## 📋 Supported Parameters
### Universal Parameters (All Engines)
| Parameter | Alias | Type | Range | Description |
|-----------|-------|------|-------|-------------|
| `seed` | — | int | 0-4294967295 | Random seed for reproducible generation |
| `temperature` | `temp` | float | 0.1-2.0 | Randomness/creativity (lower=more deterministic) |
### Engine-Specific Parameters
#### ChatterBox & ChatterBox Official 23-Lang
| Parameter | Alias | Type | Range | Description |
|-----------|-------|------|-------|-------------|
| `cfg` | `cfg_weight` | float | 0.0-1.0 | CFG weight for speaker guidance |
| `exaggeration` | `exag` | float | 0.0-2.0 | Voice emotion exaggeration |
#### F5-TTS
| Parameter | Alias | Type | Range | Description |
|-----------|-------|------|-------|-------------|
| `cfg` | — | float | 0.0-20.0 | Classifier-free guidance strength |
| `speed` | — | float | 0.5-2.0 | Speech speed multiplier |
#### Higgs Audio 2
| Parameter | Alias | Type | Range | Description |
|-----------|-------|------|-------|-------------|
| `cfg` | — | float | 0.0-20.0 | CFG strength |
| `top_p` | `topp` | float | 0.0-1.0 | Nucleus sampling probability |
| `top_k` | `topk` | int | 1-100 | Top-k sampling |
#### VibeVoice
| Parameter | Alias | Type | Range | Description |
|-----------|-------|------|-------|-------------|
| `cfg` | — | float | 0.0-20.0 | CFG strength |
| `top_p` | `topp` | float | 0.0-1.0 | Nucleus sampling probability |
| `inference_steps` | `steps` | int | 1-100 | Number of inference steps |
#### IndexTTS-2
| Parameter | Alias | Type | Range | Description |
|-----------|-------|------|-------|-------------|
| `cfg` | — | float | 0.0-20.0 | CFG strength |
| `top_p` | `topp` | float | 0.0-1.0 | Nucleus sampling probability |
| `top_k` | `topk` | int | 1-100 | Top-k sampling |
| `emotion_alpha` | — | float | 0.0-1.0 | Emotion control strength |
---
## 🎯 Advanced Usage
### Combining with Character Switching
Parameters work seamlessly with character and language tags:
```
[de:Alice|seed:42] German Alice with specific seed.
[fr:Bob|temperature:0.7] French Bob with higher temperature.
[seed:123|de:Alice] Order-independent syntax.
```
### Per-Segment Fine-Tuning in SRT
Create dynamic SRT content with per-segment control:
```srt
1
00:00:00,500 --> 00:00:03,000
[Narrator|temp:0.4] The important introduction needs precision.
2
00:00:03,100 --> 00:00:06,500
[Narrator|temp:0.4] Continuing with careful, consistent delivery.
3
00:00:06,600 --> 00:00:10,000
[Narrator|temp:0.7] Now for the creative narrative section!
4
00:00:10,100 --> 00:00:14,000
[Narrator|temp:0.4] Back to precise delivery for the conclusion.
```
### Alias Examples
Use shorter aliases for convenience:
```
[Alice|temp:0.5] # temperature
[Alice|cfg_weight:0.7] # Same as cfg:0.7
[Alice|exag:1.5] # exaggeration
[Alice|steps:50] # inference_steps (VibeVoice)
[Alice|topk:20] # top_k
[Alice|topp:0.95] # top_p
```
### Semantic Parameters for Control
#### Temperature Examples
```
[Alice|temperature:0.2] This is spoken very carefully, word for word.
[Alice|temperature:0.5] This is natural but somewhat consistent.
[Alice|temperature:0.9] This is creative with varied expression.
```
#### Seed Examples
```
# Same seed = identical audio output
[Alice|seed:42] First attempt.
[Alice|seed:42] Identical to first attempt.
# Different seeds = different variations
[Alice|seed:42] Variation 1.
[Alice|seed:100] Variation 2.
[Alice|seed:999] Variation 3.
```
---
## ⚠️ Important Notes
### Parameter Validation
- **Invalid values** are logged with warnings but don't cause errors
- **Unsupported parameters** for your engine are silently ignored
- **Out-of-range values** are clamped to valid ranges
- **Type mismatches** (e.g., `temperature:abc`) are logged and rejected
### Engine Compatibility
Different engines support different parameters. The system automatically:
- ✅ Applies supported parameters
- ⚠️ Warns about unsupported parameters
- ✅ Continues processing without errors
Example:
```
[Alice|speed:0.8|temperature:0.5] # ChatterBox: ignores speed, applies temperature
[Alice|speed:0.8|temperature:0.5] # F5-TTS: applies both
```
### Performance Considerations
- Parameter extraction happens during text parsing (minimal overhead)
- Parameters only affect the specific segment
- No impact on caching or model loading
---
## 🔧 Implementation Notes
### For Node Developers
If you're adding parameter support to a new engine:
1. Import the parameter system:
```python
from utils.text.segment_parameters import apply_segment_parameters
```
2. When processing segments with parameters:
```python
if segment.parameters:
# Apply segment parameters to override config
segment_config = apply_segment_parameters(base_config, segment.parameters, engine_type)
else:
segment_config = base_config
```
3. Generate audio with `segment_config` instead of `base_config`
### Parameter Storage
Parameters are stored in the `CharacterSegment.parameters` dictionary:
```python
segment.parameters = {
'seed': 42,
'temperature': 0.5,
'cfg': 0.7
}
```
---
## 📚 See Also
- [Character Switching Guide](CHARACTER_SWITCHING_GUIDE.md) - Multi-character and language switching
- [ChatterBox Special Tokens Guide](CHATTERBOX_V2_SPECIAL_TOKENS.md) - Emotion and sound tokens
- README.md - Complete feature overview
---
## 🐛 Troubleshooting
### Parameters Not Being Applied
1. **Check engine compatibility** - Parameter might not be supported by your engine
2. **Verify syntax** - Use `|` to separate parameters: `[Alice|seed:42|temp:0.5]`
3. **Check for typos** - Parameter names are case-insensitive but must match known parameters
4. **Look at console** - Warnings are logged when parameters are unsupported or invalid
### "Parameter not supported" Warning
This is normal! Different engines support different parameters. For example:
- ChatterBox doesn't support `speed` (F5-TTS-only parameter)
- F5-TTS doesn't support `exaggeration` (ChatterBox-only parameter)
The system silently ignores unsupported parameters and continues processing.
### Values Not Taking Effect
- **Check range** - Values outside the valid range are clamped to min/max
- **Verify engine** - Make sure you're using an engine that supports that parameter
- **Check node defaults** - Parameters only override node-level defaults, they don't create new capabilities
+201
View File
@@ -0,0 +1,201 @@
# Post v4.4.0 Development Review
*Analysis of 32 local commits since v4.4.0 release*
## Executive Summary
Since the v4.4.0 release featuring the Silent Speech Analyzer, development has focused on three major areas: **Higgs Audio 2 integration**, **architectural refinement**, and **smart processing optimization**. These 32 commits represent significant enhancements that maintain the project's momentum toward becoming the ultimate universal TTS platform.
## Major New Features
### 🎯 **Higgs Audio 2 Engine Integration** (MAJOR FEATURE)
**Commits**: `7a7c205`, `fa0a6d8`, `c6d6efb`, `d750190`, `31b8aed`, `2c8f955`, `460da24`, `d94df09`
**What's New**:
- **Complete Higgs Audio 2 TTS engine** with state-of-the-art voice cloning capabilities
- **Real-time voice cloning** from 30+ second reference audio samples
- **Multi-speaker conversation processing** with native character switching
- **8 premium voice presets** (belinda, en_woman, en_man, chadwick, vex, etc.)
**Technical Implementation**:
- **10,000+ lines of code** added for complete engine integration
- **Modular architecture** following established F5-TTS/ChatterBox patterns
- **Unified adapter integration** - works seamlessly with existing TTS Text/SRT nodes
- **Automatic model downloading** to organized TTS/HiggsAudio/ structure
- **Smart chunking system** for unlimited length text generation
**Voice Cloning Capabilities**:
- High-quality voice replication from short audio samples
- Multi-language support (English, Chinese dialects)
- Configurable generation parameters (temperature, top_p, top_k, max_new_tokens)
- Custom reference audio support beyond preset voices
### 🧠 **Smart Modular Chunk Combination System** (PERFORMANCE)
**Commits**: `bda23d2`, `19323f1`
**What's New**:
- **ChunkCombiner utility** with intelligent auto-selection algorithms
- **Smart analysis** of chunk split patterns (sentence boundaries, commas, forced splits)
- **Per-junction analysis** for optimal audio segment combination
- **Universal implementation** across all TTS engines (ChatterBox, F5-TTS, Higgs Audio)
**Technical Innovation**:
- **202+ lines** of sophisticated chunk analysis logic
- **ChunkTiming utility** providing standardized timing info integration
- **Auto-selection method** that analyzes text structure for optimal processing
- **Cache optimization** maintained through post-processing approach
**Performance Impact**:
- Improved audio quality through intelligent segment boundaries
- Reduced artifacts from poor chunk combinations
- Consistent processing across all engines
- Enhanced timing accuracy for SRT processing
### 🏗️ **Modular Processor Architecture Completion** (ARCHITECTURE)
**Commits**: `460da24`, `d94df09`, `c9f648f`
**What's New**:
- **HiggsAudioSRTProcessor** and **HiggsAudioTTSProcessor** - dedicated engine processors
- **Modular SRT overlap detection** across all TTS engines
- **Clean delegation pattern** - Unified nodes now 23 lines vs 267 lines of hardcoded logic
- **Architectural consistency** achieved across all engines
**Technical Benefits**:
- **267 lines of hardcoded logic** extracted into dedicated processors
- **Unified architecture** - all engines now follow identical delegation patterns
- **Easier maintenance** and debugging through modular separation
- **Consistent error handling** and processing across engines
## Quality of Life Improvements
### 📊 **Enhanced User Experience**
**Progress Visualization**:
- **tqdm progress bars** added to Higgs Audio generation (`83c5274`)
- Real-time feedback for long-running operations
- Better user awareness of processing status
**Improved Documentation**:
- **Detailed tooltips** for Higgs Audio Engine parameters (`240fbb7`)
- **Multi-speaker mode recommendations** for optimal usage (`1235faf`)
- Clear guidance on parameter usage and expected results
**Parameter Optimization**:
- **Fixed max_new_tokens minimum** from 128 to 1 (`40c1da4`)
- More flexible generation control for varied use cases
- Better parameter validation and user guidance
### 🔧 **Critical Bug Fixes**
**Cache System Fixes**:
- **Critical cache invalidation bug** fixed for Higgs Audio (`5ee6b10`, `ba8bace`)
- **Centralized cache system** improvements for consistency
- **Parameter flow fixes** for proper SRT node configuration (`083d865`)
**Engine Integration Fixes**:
- **F5-TTS bundled integration** improvements (`34db557`)
- **ChatterBox model detection** enhancements
- **Character switching** fixes for narrator voice priority (`9e11f12`)
**Initialization and Parameter Flow**:
- **Engine initialization bypass bug** fixed (`a676dad`, `ab90051`)
- **SRT stretch_to_fit mode** parameter format corrections (`1912250`)
- **Timing report generation** using modular calculation system (`762e472`)
## Technical Architecture Improvements
### Unified Processing Pipeline
**Before**: Each engine had custom SRT processing with duplicated logic
**After**: Clean delegation to dedicated engine processors
**Benefits**:
- Consistent behavior across all engines
- Easier debugging and maintenance
- Reduced code duplication
- Modular testing capabilities
### Enhanced Cache Management
**Centralized Cache System**:
- Universal cache invalidation across all engines
- Content-based hashing for consistency
- Engine-specific optimizations while maintaining unified interface
### Voice Management Evolution
**Higgs Audio Voice System**:
- **Removed voice_preset system** in favor of flexible reference text handling (`a112ee5`)
- **Improved reference text processing** for better voice cloning results
- **Character switching compatibility** with existing voice management
## Code Quality and Maintenance
### Repository Management
**Cleanup and Organization**:
- **ComfyUI reference folders** properly gitignored (`fe9dcd6`, `24982e5`)
- **Requirements.txt updates** for Higgs Audio dependencies (`00483cc`)
- **README consolidation** for better feature presentation (`4111d64`)
**Merge Management**:
- **External contributions** properly integrated (`6f3452d`, `894692c`)
- Clean merge handling without conflicts
### Development Process
**32 Commits** of focused development since v4.4.0:
- **1 Major Feature** (Higgs Audio 2 integration)
- **2 Performance Systems** (Smart chunking, modular processors)
- **15+ Bug fixes** and stability improvements
- **Multiple UX enhancements**
## Impact Analysis
### Feature Completeness
The addition of Higgs Audio 2 completes the "Big Three" TTS engine integration:
1. ✅ **ChatterBox** - Fast, reliable, multilingual
2. ✅ **F5-TTS** - High-quality, speech editing
3. ✅ **Higgs Audio 2** - Voice cloning, multi-speaker
### Performance Optimization
- **Smart chunking** reduces audio artifacts
- **Modular processors** improve maintainability
- **Enhanced caching** provides instant regeneration
- **Progress feedback** improves user experience
### Architecture Maturity
- **Unified delegation pattern** completed across all engines
- **Modular utilities** enable consistent behavior
- **Clean separation** of concerns for easier development
- **Future-proof design** for additional engine integration
## Upcoming Version Readiness
These 32 commits represent substantial improvements worthy of a **v4.5.0 release**:
**Major Features**: Higgs Audio 2 integration with voice cloning
**Performance**: Smart chunk combination system
**Architecture**: Modular processor completion
**Stability**: 15+ critical bug fixes
**UX**: Enhanced progress feedback and documentation
## Conclusion
The post-v4.4.0 development cycle demonstrates continued innovation and architectural refinement. The **Higgs Audio 2 integration** brings cutting-edge voice cloning capabilities, while the **smart chunking system** and **modular processor architecture** ensure robust, maintainable code.
These improvements maintain the project's position as the most comprehensive TTS platform available, with three world-class engines unified under a single, intuitive interface.
---
*Review Date*: August 21, 2025
*Commits Analyzed*: 32 local commits since v4.4.0
*Major Features Added*: 1 (Higgs Audio 2)
*Performance Systems*: 2 (Smart chunking, Modular processors)
*Bug Fixes*: 15+
@@ -0,0 +1,225 @@
# IndexTTS-2 Updates Log
This document tracks updates applied to our bundled IndexTTS-2 code from the upstream repository.
## Update Process
1. **Check for upstream changes:** `cd IgnoredForGitHubDocs/For_reference/index_tts && git pull`
2. **Review changes:** `git diff OLD_COMMIT..NEW_COMMIT -- indextts/infer_v2.py`
3. **Apply relevant changes** to `engines/index_tts/indextts/infer_v2.py`
4. **Test functionality** with our IndexTTS-2 nodes
5. **Document changes** in this log
---
## 2025-09-18: Major Update - Cache & Emotion Improvements
**Reference commit range:** `8336824..64cb31a` (September 11 → September 18, 2025)
**Upstream commits reviewed:** 5 commits affecting `infer_v2.py`
### 🔧 Changes Applied:
#### 1. **Emotion Vector Normalization Function** (commit: 8aa8064)
- **Added:** `normalize_emo_vec()` helper function
- **Purpose:** Prevents voice identity loss from overly strong emotion settings
- **Implementation:**
- Applies emotion bias factors to reduce problematic emotions
- Caps total emotion sum to 0.8 maximum
- Protects against extreme emotion vector values
- **User benefit:** Better emotion control without losing speaker characteristics
#### 2. **Audio Length Limiting** (commit: 0828dcb)
- **Added:** `_load_and_cut_audio()` helper function
- **Purpose:** Prevents memory/VRAM issues from overly long reference audio
- **Implementation:**
- Automatically truncates audio to 15 seconds maximum
- Supports different sample rates (16kHz, 22kHz)
- Provides verbose logging of truncation
- **User benefit:** More stable generation with long reference audio files
#### 3. **Persistent Cache Buildup Fix** (commit: 64cb31a)
- **Added:** Proper cache clearing with `torch.cuda.empty_cache()`
- **Purpose:** Solves memory accumulation issues during multiple generations
- **Implementation:**
- Clears old cache variables before loading new audio
- Calls `torch.cuda.empty_cache()` to free GPU memory
- Applied to both speaker and emotion cache invalidation
- **User benefit:** Better memory management during batch processing
#### 4. **BigVGAN CUDA Kernel Import Fix** (commits: ee23371, e409c4a)
- **Fixed:** Corrected import path for BigVGAN custom CUDA kernel
- **Changed:** `indextts.BigVGAN.alias_free_activation.cuda` → `indextts.s2mel.modules.bigvgan.alias_free_activation.cuda`
- **Purpose:** Ensures proper CUDA acceleration when available
- **User benefit:** Better performance with CUDA-enabled setups
### 📋 Implementation Status:
| Change | Applied to Bundled Code | Tested | Notes |
|--------|------------------------|--------|-------|
| `normalize_emo_vec()` | ✅ **Applied** | ⏳ Pending | Core emotion control improvement |
| `_load_and_cut_audio()` | ✅ **Applied** | ⏳ Pending | Memory stability fix |
| Cache clearing fixes | ✅ **Applied** | ⏳ Pending | Critical for batch processing |
| BigVGAN import fix | ✅ **Applied** | ⏳ Pending | Performance improvement |
### 🎯 User-Facing Improvements:
- **Better emotion control:** Emotion vectors now auto-normalize to prevent voice identity loss
- **Memory stability:** Long reference audio automatically truncated to prevent crashes
- **Batch processing:** Improved cache management for multiple generations
- **Performance:** Fixed CUDA kernel loading for faster inference
### 🧪 Testing Required:
- [ ] Emotion vector controls in radar chart widget
- [ ] Long reference audio handling (>15 seconds)
- [ ] Multiple sequential generations (cache clearing)
- [ ] CUDA performance with FP16 enabled
---
---
## 2025-11-06: Performance & Stability Improvements
**Reference commit range:** `64cb31a..1d5d079` (September 18 → November 6, 2025)
**Upstream commits reviewed:** 6 commits affecting `infer_v2.py`
### 🔧 New Changes Identified:
#### 1. **Empty Generator → IndexError Fix** (commit: 750d9d9)
- **Issue:** Non-streaming `infer()` would crash with IndexError on empty generator
- **Fix:** Wrapped generator list conversion in try/except, returns `None` on error
- **Changes to `infer_v2.py`:**
- Lines 354-364: Added exception handling
- Lines 680, 690: Changed `return` to `yield` for proper generator flow
- **Priority:** 🔴 **Critical** - Prevents runtime crashes
#### 2. **Streaming Implementation** (commit: b0c6ab8)
- **Added:** `stream_return` and `more_segment_before` parameters for low-latency streaming
- **Purpose:** Enable first-chunk audio output before full generation completes
- **Changes:**
- `stream_return` (bool): Return generator instead of final result
- `more_segment_before` (int): Apply extra segmentation for faster first output (0-80 recommended)
- Modified `indextts/utils/front.py` for tokenization changes
- **Priority:** 🟡 **Important** - Enables streaming use cases
#### 3. **UNK Token Warnings** (commit: 34be9bf)
- **Added:** Detection and warning for unknown tokens in input text
- **Behavior:**
- Detects tokens the BPE model cannot encode
- Prints warnings with token IDs and suggested fixes
- Helps users debug encoding issues
- **Changes to `infer_v2.py`:** Lines 450-459, added token validation logic
- **Priority:** 🟢 **Nice-to-have** - Improves user experience with error diagnosis
#### 4. **S2Mel Stage Optimization** (commit: 31e7e85)
- **Focus:** Optimized mel-spectrogram generation pipeline
- **Changes:**
- `indextts/s2mel/modules/commons.py`: 15 lines added/modified
- `indextts/s2mel/modules/diffusion_transformer.py`: 2 lines modified
- `indextts/s2mel/modules/flow_matching.py`: 15 lines added (new optimization code)
- `indextts/infer_v2.py`: 28 lines added/modified
- **Expected Impact:** Faster mel-spectrogram generation (stage 2 of 3)
- **Priority:** 🟡 **Important** - Performance improvement
#### 5. **GPT2 Inference Acceleration** (commit: c1ef414)
- **Focus:** Major performance optimization for GPT2 stage (stage 1)
- **Changes:**
- **New module:** `indextts/accel/` directory (609 lines added):
- `accel_engine.py`: Core acceleration logic (609 lines)
- `attention.py`: Optimized attention mechanism (154 lines)
- `gpt2_accel.py`: GPT2-specific optimizations (181 lines)
- `kv_manager.py`: Key-value cache management (209 lines)
- Modified `indextts/gpt/model_v2.py`: 63 lines changed (+47 new)
- Modified `indextts/infer_v2.py`: 17 lines changed
- **Expected Impact:** Significantly faster GPT2 inference (first stage bottleneck)
- **Priority:** 🔴 **Critical** - Major performance gain, but new code complexity
#### 6. **Merge Commit** (commit: 5d67f62 → 1d5d079)
- Merge of acceleration branch into main
- Contains all above changes integrated
### 📊 Summary
| Change | Files Modified | Lines Changed | Priority | Status |
|--------|---------------|---------------|----------|--------|
| Empty generator fix | 1 | +12/-9 | 🔴 Critical | ⏳ Pending |
| Streaming support | 2 | +72/-18 | 🟡 Important | ⏳ Pending |
| UNK token warnings | 1 | +7 | 🟢 Nice-to-have | ⏳ Pending |
| S2Mel optimization | 4 | +54/-6 | 🟡 Important | ⏳ Pending |
| GPT2 acceleration | 6 | +1228/-14 | 🔴 Critical | ⏳ Pending |
### 🚨 Integration Concerns
1. **New acceleration module complexity** - The `indextts/accel/` package adds significant complexity with KV-cache management and specialized attention mechanisms. Needs careful testing.
2. **Potential API changes** - Streaming parameters (`stream_return`, `more_segment_before`) may require adapter updates if our wrapper doesn't expose these.
3. **GPU memory implications** - KV-cache optimization might have different memory profiles; needs profiling on different GPU setups.
4. **File structure expansion** - New `accel/` subdirectory adds to maintenance burden.
### 🧪 Testing Required
- [ ] Empty generator fix - test non-streaming inference with various inputs
- [ ] Streaming latency - measure first-chunk output time with `stream_return=True`
- [ ] GPT2 acceleration - benchmark inference speed (expect 1.5-3x speedup)
- [ ] S2Mel optimization - verify audio quality unchanged
- [ ] UNK token handling - test with multilingual/special character inputs
- [ ] Memory profiling - GPU memory usage with new KV-cache manager
- [ ] ComfyUI integration - ensure all changes work through our node interface
### ✅ Applied Changes Summary (2025-11-06)
**All upstream commits successfully integrated:**
1. ✅ **Empty generator fix (750d9d9)** - Refactored `infer()` to use generator pattern:
- Created wrapper `infer()` method that delegates to `infer_generator()`
- Refactored monolithic method into `infer_generator()` with yield statements
- Changed final `return` statements to `yield` for generator compatibility
- Handles both streaming (`stream_return=True`) and non-streaming modes
2. ✅ **Streaming implementation (b0c6ab8)** - Added streaming parameters:
- Added `stream_return` parameter to `infer()` and `infer_generator()`
- Added `more_segment_before` parameter for streaming segmentation
- Integrated with generator-based inference for low-latency streaming
3. ✅ **UNK token warnings (34be9bf)** - Added token validation:
- Added token validation in `infer_generator()`
- Warns users about unknown tokens with suggestions
4. ✅ **S2Mel optimization (31e7e85)** - Applied all module updates:
- `commons.py`: Updated `fused_add_tanh_sigmoid_multiply()` with torch.split
- `diffusion_transformer.py`: Fixed `sequence_mask()` call with max_length parameter
- `flow_matching.py`: Added `enable_torch_compile()` method to CFM class
- `infer_v2.py`: Added `use_torch_compile` parameter and setup
5. ✅ **GPT2 acceleration (c1ef414)** - Applied complete acceleration module:
- Created `/accel/` directory with 5 files (init, accel_engine, attention, gpt2_accel, kv_manager)
- Modified `model_v2.py`: Added `use_accel` parameter to UnifiedVoice
- Modified `model_v2.py`: Added accel engine initialization in `post_init_gpt2_config()`
- Modified `model_v2.py`: Added accel-aware inference in `inference_speech()`
### 🔧 Custom Modifications Preserved
- QwenEmotion lazy loading with path fallbacks (lines 81-112 in infer_v2.py)
- W2V-BERT model loading from TTS folder with unified downloader (lines 145-166)
- All custom logging and error handling in infer_v2.py and throughout
- Custom model initialization sequence
- Custom emotion vector normalization and processing
---
## Next Update Check: 2025-11-20
**Monitoring:** Watch for commits to `indextts/infer_v2.py` in https://github.com/index-tts/index-tts
**Update command:**
```bash
cd /tmp/index_tts_upstream
git pull origin main
git log --oneline 1d5d079..HEAD -- indextts/infer_v2.py
```
**Priority changes to watch for:**
- Additional performance optimizations
- Bug fixes in new acceleration code
- Breaking API changes
- Model loading improvements
@@ -0,0 +1,103 @@
# [ENGINE_NAME] Upstream Update Log
**Engine:** [ENGINE_NAME]
**Official Repository:** [UPSTREAM_URL]
**Bundled Location:** `engines/[engine_folder]/`
**Last Checked:** YYYY-MM-DD
**Last Checked By:** [username]
---
## Check History
| Date | Commits Checked | New Features | Bug Fixes | Breaking Changes | Notes |
|------|-----------------|--------------|-----------|------------------|-------|
| YYYY-MM-DD | N/A | - | - | - | Initial setup |
---
## Latest Upstream Changes (Since Last Check)
### New Features 🆕
- Feature 1
- Feature 2
### Bug Fixes 🐛
- Fix 1
- Fix 2
### Breaking Changes ⚠️
- Change 1
- Change 2
### Performance Improvements ⚡
- Improvement 1
### Documentation Updates 📝
- Update 1
---
## Integration Status
| Item | Status | Priority | Notes |
|------|--------|----------|-------|
| Feature/Fix name | `pending`/`integrated`/`skipped` | 🔴/🟡/🟢 | Integration notes |
### Legend
- `pending` - Identified but not yet integrated
- `integrated` - Applied to bundled code
- `skipped` - Reviewed but decided not to integrate
- `wontfix` - Not applicable to our implementation
---
## Integration Notes
### Conflicts with Our Customizations
- [List any conflicts between upstream changes and our custom modifications]
### Testing Required
- [ ] Test case 1
- [ ] Test case 2
### Version Bump Recommendation
- Current bundled version: X.Y.Z
- Latest upstream version: X.Y.Z
- Recommended bump: `patch` / `minor` / `major` / `none`
---
## Known Customizations We Maintain
List any intentional differences from upstream:
- Customization 1
- Customization 2
---
## Next Check
**Scheduled for:** YYYY-MM-DD
**Check interval:** Weekly / Bi-weekly / Monthly
---
## Previous Check Summary
Link to previous check details if available.
---
## Quick Commands
```bash
# Clone/update upstream repository
git clone [UPSTREAM_URL] /tmp/[engine_name]_upstream
# Compare with our bundled version
diff -r /tmp/[engine_name]_upstream/[source_folder] engines/[bundled_folder]/
# Show recent commits from upstream
git log --oneline -20 origin/main
```
@@ -0,0 +1,196 @@
# Upstream TTS Engines Tracking Index
This document provides a centralized reference for monitoring updates to all bundled TTS engines. Each engine has its own update log file tracking changes, bug fixes, and new features from the official upstream repository.
**Last Updated:** 2025-11-06
---
## Quick Reference Table
| Engine | Bundled Source | Location | Official Repository | Update Log | Last Checked |
|--------|---------|----------|---------------------|------------|--------------|
| **IndexTTS-2** | ✅ Full | `engines/index_tts/indextts/` | [github.com/index-tts/index-tts](https://github.com/index-tts/index-tts) | [INDEXTTS2_UPDATE_LOG.md](./INDEXTTS2_UPDATE_LOG.md) | 2025-11-06 |
| **ChatterBox** | ✅ Full | `engines/chatterbox/` | [github.com/resemble-ai/chatterbox](https://github.com/resemble-ai/chatterbox) | [CHATTERBOX_UPDATE_LOG.md](./CHATTERBOX_UPDATE_LOG.md) | TBD |
| **ChatterBox Multilingual (23-Lang)** | ✅ Full | `engines/chatterbox_official_23lang/` | [github.com/resemble-ai/chatterbox](https://github.com/resemble-ai/chatterbox) | [CHATTERBOX_UPDATE_LOG.md](./CHATTERBOX_UPDATE_LOG.md) | TBD |
| **F5-TTS** | ✅ Full | `engines/f5_tts/` | [github.com/SWivid/F5-TTS](https://github.com/SWivid/F5-TTS) | [F5TTS_UPDATE_LOG.md](./F5TTS_UPDATE_LOG.md) | TBD |
| **Higgs Audio** | ✅ Full | `engines/higgs_audio/boson_multimodal/` | [github.com/boson-ai/higgs-audio](https://github.com/boson-ai/higgs-audio) | [HIGGS_AUDIO_UPDATE_LOG.md](./HIGGS_AUDIO_UPDATE_LOG.md) | TBD |
| **RVC** | ✅ Full | `engines/rvc/` | [github.com/RVC-Project/Retrieval-based-Voice-Conversion](https://github.com/RVC-Project/Retrieval-based-Voice-Conversion) | [RVC_UPDATE_LOG.md](./RVC_UPDATE_LOG.md) | TBD |
| **VibeVoice** | ⚠️ Wrapper Only | `engines/vibevoice_engine/` | [github.com/microsoft/VibeVoice](https://github.com/microsoft/VibeVoice) ⚠️ Disabled | [VIBEVOICE_UPDATE_LOG.md](./VIBEVOICE_UPDATE_LOG.md) | TBD |
---
## Engine Status Details
### ✅ Fully Bundled Engines (Source Code Included)
These engines have their complete source code bundled in the TTS Audio Suite repository:
1. **IndexTTS-2** - Full inference code and models bundled
2. **ChatterBox** - Full model and inference code bundled
3. **ChatterBox Multilingual** - Wrapper around same ChatterBox repo with language support
4. **F5-TTS** - Full source code with configs, models, and training scripts
5. **Higgs Audio** - Full boson_multimodal framework bundled
6. **RVC** - Complete voice conversion implementation
### ⚠️ Wrapper-Only Engines
These are external dependencies integrated via wrappers:
- **VibeVoice** - Wrapper around Microsoft's `vibevoice` PyPI package (⚠️ **Note**: Official repo is disabled by Microsoft as of Sept 2025)
---
## Update Checking Workflow
### Manual Checking (via slash command)
```bash
/check_tts_bundled_updates [engine_name] [--verbose]
```
Examples:
- `/check_tts_bundled_updates` - Check ALL engines
- `/check_tts_bundled_updates IndexTTS-2` - Check specific engine
- `/check_tts_bundled_updates ChatterBox --verbose` - Verbose output with detailed diffs
### What Gets Checked
For each engine, the process:
1. Clones/updates the official upstream repository
2. Compares commit history since last check
3. Identifies new features, bug fixes, breaking changes
4. Updates the engine's UPDATE_LOG.md file
5. Flags items requiring integration
### Priority Levels for Updates
- 🔴 **Critical**: Security fixes, breaking changes, major bug fixes
- 🟡 **Important**: New features, performance improvements, compatibility updates
- 🟢 **Nice-to-have**: Documentation, minor tweaks, non-critical improvements
---
## Individual Engine Logs
Each engine has its own update log tracking:
- **Date checked**: When upstream was last reviewed
- **New commits**: List of changes since last version
- **Actionable items**: What needs integration/testing
- **Integration status**: Whether changes have been applied
See individual UPDATE_LOG.md files in this directory for details.
---
## Integration Checklist
When integrating upstream updates:
- [ ] Read upstream changelog/commits
- [ ] Identify compatibility issues with our wrapper code
- [ ] Create backup of current bundled code
- [ ] Apply changes incrementally
- [ ] Run full test suite
- [ ] Document any conflicts or customizations we maintain
- [ ] Update version in nodes.py if integration warrants version bump
- [ ] Commit with reference to upstream changes
---
## Known Customizations We Maintain
### IndexTTS-2
- Audio normalization fix for Linux (v4.14.15) - **Not in upstream**
- Custom processor wrapper for unified interface
- Audio amplitude normalization to handle platform-specific torchaudio.load() behavior
### ChatterBox (both versions)
- Unified adapter pattern for consistent interface
- Integration with unified node architecture
### F5-TTS
- Unified adapter pattern for consistent interface
### Higgs Audio
- Unified adapter pattern for consistent interface
### RVC
- Complete custom implementation for TTS Audio Suite
### VibeVoice
- Wrapper around external vibevoice package
- ⚠️ **Status**: Microsoft disabled official repo in Sept 2025 - consider migration path
---
## Related Documentation
- [NEW_ENGINE_IMPLEMENTATION_GUIDE.md](../NEW_ENGINE_IMPLEMENTATION_GUIDE.md) - How to integrate new engines
- [INDEXTTS2_UPDATE_LOG.md](./INDEXTTS2_UPDATE_LOG.md) - IndexTTS-2 specific tracking
- [../DEPENDENCY_MANAGEMENT_GUIDE.md](../DEPENDENCY_MANAGEMENT_GUIDE.md) - Dependency compatibility
- [../BUMP_SCRIPT_INSTRUCTIONS.md](../BUMP_SCRIPT_INSTRUCTIONS.md) - Version management
---
## Source Code Location Details
### Where to Find the Bundled Engine Source Code
| Engine | Main Source Files | Config/Models | Processor/Adapter |
|--------|-------------------|---------------|-------------------|
| IndexTTS-2 | `engines/index_tts/indextts/` (infer.py, infer_v2.py, gpt/, s2mel/) | `engines/index_tts/indextts/` | `engines/processors/index_tts_processor.py` |
| ChatterBox | `engines/chatterbox/chatterbox/` (models, inference) | `engines/chatterbox/models/` | `engines/adapters/chatterbox_adapter.py` |
| ChatterBox Multilingual | `engines/chatterbox_official_23lang/` (tts.py, models/*, language_models.py) | `engines/chatterbox_official_23lang/models/` | `engines/processors/` |
| F5-TTS | `engines/f5_tts/` (src/, model/, infer/) | `engines/f5_tts/configs/` | `engines/f5tts/f5tts.py` (adapter) |
| Higgs Audio | `engines/higgs_audio/boson_multimodal/` (full framework) | `engines/higgs_audio/boson_multimodal/model/` | `engines/adapters/higgs_audio_adapter.py` |
| RVC | `engines/rvc/impl/` (inference, models) | `engines/rvc/impl/lib/` | `engines/rvc/rvc_engine.py` |
| VibeVoice | External package: `vibevoice` (PyPI) | Downloaded at runtime | `engines/vibevoice_engine/vibevoice_engine.py` |
### Key File Patterns to Check When Updating
**Inference/Generation Code:**
- IndexTTS-2: `engines/index_tts/indextts/infer_v2.py`
- ChatterBox: `engines/chatterbox/chatterbox/inference.py` or `tts.py`
- F5-TTS: `engines/f5_tts/src/f5_tts/infer/`
- Higgs Audio: `engines/higgs_audio/boson_multimodal/serve/serve_engine.py`
- RVC: `engines/rvc/impl/infer_pack/`
**Model Loading:**
- IndexTTS-2: `engines/index_tts/indextts/infer_v2.py` (model initialization)
- ChatterBox: `engines/chatterbox/chatterbox/models/`
- F5-TTS: `engines/f5_tts/src/f5_tts/model/`
- Higgs Audio: `engines/higgs_audio/boson_multimodal/model/higgs_audio.py`
- RVC: `engines/rvc/impl/infer_pack/modules.py`
**Audio Processing:**
- IndexTTS-2: `engines/index_tts/indextts/infer_v2.py` (audio normalization, vocoding)
- ChatterBox: Check audio post-processing in inference methods
- F5-TTS: `engines/f5_tts/` audio handling
- All: Look for clamp, normalize, scale operations
---
## Important Notes
- **ChatterBox and ChatterBox Multilingual** both use the same official repository: `github.com/resemble-ai/chatterbox`
- The difference is in implementation: one focuses on single-language, the other on 23-language support
- They share the same upstream to track for updates
- **VibeVoice** is NOT actively maintained - Microsoft disabled the official GitHub repository
- Consider evaluating alternatives or pinning to a specific version
- The wrapper depends on the external `vibevoice` PyPI package
- **F5-TTS** vs **f5tts** directories:
- `f5_tts/` = Full bundled source code (official implementation from SWivid)
- `f5tts/` = Wrapper/adapter layer only (NOT the official source code)
---
## Next Steps
1. ✅ Document all official upstream repositories
2. ✅ Identify which code is bundled vs wrapped
3. ✅ Implement `/check_tts_bundled_updates` slash command
4. ⏳ Create UPDATE_LOG.md for each engine (in progress)
5. ⏳ Establish regular checking schedule (weekly/monthly)
6. ⏳ Create integration PR template for upstream changes
+27 -12
View File
@@ -15,8 +15,10 @@ The **🤐 Noise or Vocal Removal** node is a powerful audio processing tool tha
### 🏆 BEST MODELS (2024-2025)
**Top Tier - State of the Art:**
- ★★★ **model_bs_roformer_ep_317_sdr_12.9755.ckpt** - BEST OVERALL (12.97 dB SDR, Transformer-based SOTA)
- ★★★ **MDX23C-8KFFT-InstVoc_HQ.ckpt** - Highest quality, minimal artifacts (Karafan architecture)
- ★★★ **MELBAND/denoise_mel_band_roformer_sdr_27.99.ckpt** - BEST DENOISING (27.99 dB SDR!)
- ★★★ **MELBAND/MelBandRoformer_fp32.safetensors** - Kijai's high-quality vocal separation
- ★★★ **model_bs_roformer_ep_317_sdr_12.9755.ckpt** - Reliable transformer-based (12.97 dB SDR)
- ★★☆ **MDX23C-8KFFT-InstVoc_HQ.ckpt** - Minimal artifacts (Karafan architecture)
- ★★☆ **UVR-MDX-NET-vocal_FT.onnx** - Professional vocal extraction (MDX architecture)
### 📂 VR MODELS (Convolutional Neural Networks)
@@ -37,6 +39,18 @@ The **🤐 Noise or Vocal Removal** node is a powerful audio processing tool tha
- **MDX23C-8KFFT-InstVoc_HQ.ckpt** - ⭐ HIGHEST QUALITY (8K FFT, minimal artifacts)
### 🎯 MELBAND ROFORMER MODELS (State-of-the-Art 2025)
**Kijai's Models (Vocal Separation):**
- **MELBAND/MelBandRoformer_fp16.safetensors** - Fast processing (456MB, FP16 precision)
- **MELBAND/MelBandRoformer_fp32.safetensors** - Maximum quality (913MB, FP32 precision)
**ZFTurbo's Models (Denoising Specialists):**
- **MELBAND/denoise_mel_band_roformer_sdr_27.99.ckpt** - ⭐ BEST DENOISING (27.99 dB SDR!)
- **MELBAND/denoise_mel_band_roformer_aggressive_sdr_27.97.ckpt** - Aggressive denoising
**Note:** MelBand models use advanced transformer architecture with rotary embeddings for superior separation quality
## Use Case Workflows
### 🎤 Karaoke Creation (Vocal Removal)
@@ -50,9 +64,10 @@ The **🤐 Noise or Vocal Removal** node is a powerful audio processing tool tha
3. **Use:** "extracted voice/noise/echo" output (isolated vocals)
### 🔧 Audio Denoising
1. **Primary:** `UVR-DeNoise.pth` (gentle: 5-8)
2. **Use:** "remaining" output (clean audio)
3. **Note:** "extracted" contains removed noise
1. **Best Quality:** `MELBAND/denoise_mel_band_roformer_sdr_27.99.ckpt` (no aggressiveness setting)
2. **Alternative:** `UVR-DeNoise.pth` (gentle: 5-8)
3. **Use:** "remaining" output (clean audio)
4. **Note:** "extracted" contains removed noise
### 💼 Professional Workflow
1. **Separate:** `model_bs_roformer_ep_317_sdr_12.9755.ckpt`
@@ -71,7 +86,8 @@ The **🤐 Noise or Vocal Removal** node is a powerful audio processing tool tha
- **VR (Vocal Remover):** Fast, magnitude-only processing, good for basic separation
- **MDX (Multi-Dimensional eXtraction):** Hybrid spectrogram/waveform, better quality preservation
- **Karafan:** Multi-stage ensemble, state-of-the-art quality (highest CPU usage)
- **RoFormer:** Transformer with rotary embeddings, current SOTA
- **RoFormer:** Transformer with rotary embeddings, excellent quality
- **MelBand RoFormer:** Advanced transformer with Mel-frequency bands, current SOTA (27+ dB SDR)
## Aggressiveness Settings (0-20)
@@ -122,12 +138,11 @@ The **🤐 Noise or Vocal Removal** node is a powerful audio processing tool tha
## Advanced Techniques
### 🚀 NEWER MODELS (Consider adding)
- **Mel-RoFormer** - Next-gen transformer architecture
- **SCNet-XL** - Large-scale separation network
- **VitLarge23** - Vision transformer adaptation
- **Demucs v4 (htdemucs_ft)** - Hybrid transformer fine-tuned
- **Kim Vocal models** - Highly regarded community favorites
### 🚀 MODEL NOTES
- **MelBand RoFormer** - ✅ NOW SUPPORTED! Best-in-class denoising (27+ dB SDR)
- **SCNet models** - ⚠️ Architecture issues causing audio artifacts
- **MDX23C models** - ⚠️ Tensor alignment errors in current implementation
- **Note:** Aggressiveness setting only affects VR architecture models, not MelBand/MDX/Karafan
### 💡 PRO TIPS
- **Use ensemble combinations** for best results
+25 -5
View File
@@ -283,21 +283,41 @@ class ChatterBoxEngineAdapter:
def combine_audio_chunks(self, audio_segments: List[torch.Tensor], **params) -> torch.Tensor:
"""
Combine ChatterBox audio segments.
Combine ChatterBox audio segments using modular combination utility.
Args:
audio_segments: List of audio tensors to combine
**params: Combination parameters
Returns:
Combined audio tensor
Combined audio tensor, or tuple with timing info if requested
"""
if len(audio_segments) == 1:
return audio_segments[0]
# ChatterBox uses simple concatenation
from utils.audio.processing import AudioProcessingUtils
return AudioProcessingUtils.concatenate_audio_segments(audio_segments, "simple")
# Use modular chunk combiner with ChatterBox settings
from utils.audio.chunk_combiner import ChunkCombiner
method = params.get("combination_method", "auto")
silence_ms = params.get("silence_ms", 100)
text_length = params.get("text_length", 0)
original_text = params.get("original_text", "")
text_chunks = params.get("text_chunks", None)
return_info = params.get("return_timing_info", False)
print(f"🔗 Combining {len(audio_segments)} ChatterBox chunks using '{method}' method")
return ChunkCombiner.combine_chunks(
audio_segments=audio_segments,
method=method,
silence_ms=silence_ms,
crossfade_duration=0.1,
sample_rate=44100, # ChatterBox sample rate
text_length=text_length,
original_text=original_text,
text_chunks=text_chunks,
return_info=return_info
)
def generate_audio(self, text: str, voice_preset: str, voice_settings: dict,
output_filename: str, language: str = "auto",
@@ -0,0 +1,407 @@
"""
ChatterBox Official 23-Lang Engine Adapter - Engine-specific adapter for ChatterBox Official 23-Lang TTS
Provides standardized interface for ChatterBox Official 23-Lang operations in multilingual engine
With native batch processing support for parallel text generation
"""
import torch
from typing import Dict, Any, Optional, List, Tuple
import concurrent.futures
import threading
# Use absolute import to avoid relative import issues in ComfyUI
import sys
import os
current_dir = os.path.dirname(__file__)
parent_dir = os.path.dirname(current_dir)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
from utils.models.language_mapper import get_model_for_language
class ChatterBoxOfficial23LangEngineAdapter:
"""Engine-specific adapter for ChatterBox TTS with batch processing support."""
def __init__(self, node_instance):
"""
Initialize ChatterBox adapter with batch processing capabilities.
Args:
node_instance: ChatterboxTTSNode or SRTTTSNode instance
"""
self.node = node_instance
self.engine_type = "chatterbox_official_23lang"
self.batch_size = 4 # Default batch size for parallel processing
self.enable_batch_processing = True # Enable batch processing by default
self._generation_lock = threading.Lock() # Thread safety for model access
def get_model_for_language(self, lang_code: str, default_model: str) -> str:
"""
Get ChatterBox model name for specified language.
Args:
lang_code: Language code (e.g., 'en', 'de', 'no')
default_model: Default model name (language)
Returns:
ChatterBox model name (language) for the specified language code
"""
return get_model_for_language(self.engine_type, lang_code, default_model)
def load_base_model(self, language: str, device: str):
"""
Load base ChatterBox model.
Args:
language: Language model to load (e.g., "English", "German")
device: Device to load model on
"""
self.node.load_tts_model(device, language)
def load_language_model(self, language: str, device: str):
"""
Load language-specific ChatterBox model.
Args:
language: Language model to load (e.g., "German", "Norwegian")
device: Device to load model on
"""
self.node.load_tts_model(device, language)
def generate_segment_audio(self, text: str, char_audio: str,
character: str = "narrator", **params) -> torch.Tensor:
"""
Generate ChatterBox audio for a text segment.
Args:
text: Text to generate audio for
char_audio: Reference audio file path
character: Character name for caching
**params: Additional ChatterBox parameters
Returns:
Generated audio tensor
"""
# Extract ChatterBox Official 23-Lang specific parameters
exaggeration = params.get("exaggeration", 1.0)
temperature = params.get("temperature", 0.8)
cfg_weight = params.get("cfg_weight", 1.0)
repetition_penalty = params.get("repetition_penalty", 2.0)
min_p = params.get("min_p", 0.05)
top_p = params.get("top_p", 1.0)
language_id = params.get("language_id", "en")
seed = params.get("seed", 0)
enable_cache = params.get("enable_audio_cache", True)
# Create cache function if caching is enabled
cache_fn = None
if enable_cache:
from utils.audio.cache import create_cache_function
# Get current language/model for cache key
current_language = params.get("current_language", params.get("model", "English"))
audio_component = params.get("stable_audio_component", "main_reference")
if character != "narrator":
audio_component = f"char_file_{character}"
# Get model source
model_source = params.get("model_source")
if not model_source and hasattr(self.node, 'model_manager'):
model_source = self.node.model_manager.get_model_source("tts")
cache_fn = create_cache_function(
engine_type="chatterbox_official_23lang",
character=character,
exaggeration=exaggeration,
temperature=temperature,
cfg_weight=cfg_weight,
seed=seed,
audio_component=audio_component,
model_source=model_source or "unknown",
device=params.get("device", "auto"),
language=current_language
)
# Handle caching externally for consistency with F5-TTS
if cache_fn:
# Check cache first
cached_audio = cache_fn(text)
if cached_audio is not None:
return cached_audio
# CRITICAL FIX: Handle caching based on pause tag presence
# If pause tags detected, let internal pause processor handle caching of text segments
# If no pause tags, use external caching as normal
enable_pause_tags = params.get("enable_pause_tags", True)
has_pause_tags = enable_pause_tags and "[pause:" in text
if has_pause_tags:
# Let internal pause processor handle caching of individual text segments
audio_result = self.node._generate_tts_with_pause_tags(
text=text,
audio_prompt=char_audio,
exaggeration=exaggeration,
temperature=temperature,
cfg_weight=cfg_weight,
language=params.get("current_language", params.get("model", "English")),
enable_pause_tags=True,
character=character,
seed=seed,
enable_cache=True, # Enable internal caching for pause tag processing
crash_protection_template=params.get("crash_protection_template", "hmm ,, {seg} hmm ,,"),
stable_audio_component=params.get("stable_audio_component", "main_reference")
)
# Don't use external cache for pause tag segments
else:
# No pause tags, use external caching as normal
audio_result = self.node._generate_tts_with_pause_tags(
text=text,
audio_prompt=char_audio,
exaggeration=exaggeration,
temperature=temperature,
cfg_weight=cfg_weight,
language=params.get("current_language", params.get("model", "English")),
enable_pause_tags=False,
character=character,
seed=seed,
enable_cache=False, # Disable internal caching since we handle it externally
crash_protection_template=params.get("crash_protection_template", "hmm ,, {seg} hmm ,,"),
stable_audio_component=params.get("stable_audio_component", "main_reference")
)
# Cache the result if external caching is enabled
if cache_fn:
cache_fn(text, audio_result)
return audio_result
def generate_batch_audio(self, text_segments: List[str], char_audio: str,
character: str = "narrator", batch_size: Optional[int] = None,
**params) -> List[torch.Tensor]:
"""
Generate ChatterBox audio for multiple text segments using true batch processing.
Args:
text_segments: List of text segments to generate audio for
char_audio: Reference audio file path
character: Character name for caching
batch_size: Optional batch size override (uses self.batch_size if None)
**params: Additional ChatterBox parameters
Returns:
List of generated audio tensors
"""
if not self.enable_batch_processing or len(text_segments) == 1:
# Fall back to sequential processing if batch processing is disabled or single segment
return [self.generate_segment_audio(text, char_audio, character, **params)
for text in text_segments]
batch_size = batch_size or self.batch_size
print(f"🚀 ChatterBox batch processing: {len(text_segments)} segments in batches of {batch_size}")
audio_results = []
# Process segments in batches using the engine's generate_batch method
for i in range(0, len(text_segments), batch_size):
batch_texts = text_segments[i:i + batch_size]
print(f"🔄 Processing batch {i//batch_size + 1}: {len(batch_texts)} segments")
try:
# Extract ChatterBox specific parameters
exaggeration = params.get("exaggeration", 1.0)
temperature = params.get("temperature", 0.8)
cfg_weight = params.get("cfg_weight", 1.0)
# Use the ChatterBox engine's generate_batch method for true batch processing
if hasattr(self.node, 'tts_model') and hasattr(self.node.tts_model, 'generate_batch'):
print(f"🔧 Using overlapping batch processing with max_workers={self.batch_size}")
batch_audio = self.node.tts_model.generate_batch(
texts=batch_texts,
audio_prompt_path=char_audio,
exaggeration=exaggeration,
cfg_weight=cfg_weight,
temperature=temperature,
batch_size=len(batch_texts),
max_workers=self.batch_size # Use the UI batch_size as max_workers
)
print(f"✅ Batch {i//batch_size + 1} completed: {len(batch_audio)} audio segments generated")
audio_results.extend(batch_audio)
else:
# Fallback to sequential generation if batch method not available
print(f"⚠️ Batch method not available, falling back to sequential processing")
for text in batch_texts:
audio = self.generate_segment_audio(text, char_audio, character, **params)
audio_results.append(audio)
except Exception as e:
print(f"⚠️ Batch processing failed for batch {i//batch_size + 1}: {e}")
print(f"🔄 Falling back to sequential processing for this batch")
# Fall back to sequential generation for failed batch
for text in batch_texts:
try:
audio = self.generate_segment_audio(text, char_audio, character, **params)
audio_results.append(audio)
except Exception as fallback_error:
print(f"❌ Sequential fallback also failed for '{text[:30]}...': {fallback_error}")
# Generate silence as last resort
from utils.audio.processing import AudioProcessingUtils
silence = AudioProcessingUtils.create_silence(1.0, 44100)
audio_results.append(silence)
print(f"🎉 Batch processing complete: {len(audio_results)} total segments generated")
return audio_results
def _generate_single_with_lock(self, text: str, char_audio: str,
character: str = "narrator", **params) -> torch.Tensor:
"""
Generate audio for a single segment with thread-safe model access.
Args:
text: Text to generate audio for
char_audio: Reference audio file path
character: Character name
**params: Additional parameters
Returns:
Generated audio tensor
"""
# Use lock to ensure thread-safe model access
with self._generation_lock:
return self.generate_segment_audio(text, char_audio, character, **params)
def set_batch_processing(self, enabled: bool, batch_size: Optional[int] = None):
"""
Configure batch processing settings.
Args:
enabled: Whether to enable batch processing
batch_size: Optional new batch size (1-32 supported, uses overlapping parallel processing)
"""
self.enable_batch_processing = enabled
if batch_size is not None:
self.batch_size = max(1, min(32, batch_size)) # Allow up to 32 parallel workers
status = "enabled" if enabled else "disabled"
print(f"🔧 ChatterBox batch processing {status} (batch_size={self.batch_size})")
def combine_audio_chunks(self, audio_segments: List[torch.Tensor], **params) -> torch.Tensor:
"""
Combine ChatterBox audio segments using modular combination utility.
Args:
audio_segments: List of audio tensors to combine
**params: Combination parameters
Returns:
Combined audio tensor, or tuple with timing info if requested
"""
if len(audio_segments) == 1:
return audio_segments[0]
# Use modular chunk combiner with ChatterBox settings
from utils.audio.chunk_combiner import ChunkCombiner
method = params.get("combination_method", "auto")
silence_ms = params.get("silence_ms", 100)
text_length = params.get("text_length", 0)
original_text = params.get("original_text", "")
text_chunks = params.get("text_chunks", None)
return_info = params.get("return_timing_info", False)
print(f"🔗 Combining {len(audio_segments)} ChatterBox chunks using '{method}' method")
return ChunkCombiner.combine_chunks(
audio_segments=audio_segments,
method=method,
silence_ms=silence_ms,
crossfade_duration=0.1,
sample_rate=44100, # ChatterBox sample rate
text_length=text_length,
original_text=original_text,
text_chunks=text_chunks,
return_info=return_info
)
def generate_audio(self, text: str, voice_preset: str, voice_settings: dict,
output_filename: str, language: str = "auto",
enable_batch_processing: bool = False, batch_size: int = 4) -> dict:
"""
Generate audio using ChatterBox adapter with batch processing support.
This method provides the interface expected by the unified TTS system.
"""
# For single text generation, use the existing segment generation
if not enable_batch_processing:
print(f"🎤 ChatterBox single generation: '{text[:50]}...'")
# Use the existing node functionality for single generation
audio_result = self.node.generate_speech(
text=text,
language=language,
device=self.node.device if hasattr(self.node, 'device') else 'auto',
exaggeration=voice_settings.get('exaggeration', 0.5),
temperature=voice_settings.get('temperature', 0.8),
cfg_weight=voice_settings.get('cfg_weight', 0.5),
seed=voice_settings.get('seed', 0),
reference_audio=voice_settings.get('reference_audio'),
audio_prompt_path=voice_settings.get('audio_prompt_path', ''),
batch_size=1 if not enable_batch_processing else batch_size
)
return {
'audio': audio_result[0], # Audio tensor
'info': audio_result[1] if len(audio_result) > 1 else 'Generated with ChatterBox'
}
else:
# For batch processing, split text into chunks and use batch generation
print(f"🚀 ChatterBox batch generation enabled: batch_size={batch_size}")
# Split text into reasonable chunks for batch processing
from utils.text.chunking import ImprovedChatterBoxChunker
chunker = ImprovedChatterBoxChunker()
# Use chunking to split text into segments
max_chars_per_chunk = voice_settings.get('max_chars_per_chunk', 400)
if len(text) > max_chars_per_chunk:
text_segments = chunker.split_into_chunks(text, max_chars_per_chunk)
print(f"📝 Text split into {len(text_segments)} chunks for batch processing")
else:
text_segments = [text]
# Generate audio for all segments using batch processing
audio_segments = self.generate_batch_audio(
text_segments=text_segments,
char_audio=voice_settings.get('audio_prompt_path', ''),
character=voice_preset or 'narrator',
batch_size=batch_size,
exaggeration=voice_settings.get('exaggeration', 0.5),
temperature=voice_settings.get('temperature', 0.8),
cfg_weight=voice_settings.get('cfg_weight', 0.5),
seed=voice_settings.get('seed', 0),
current_language=language,
device=self.node.device if hasattr(self.node, 'device') else 'auto'
)
# Combine audio segments
if len(audio_segments) == 1:
combined_audio = audio_segments[0]
else:
combined_audio = self.combine_audio_chunks(audio_segments)
return {
'audio': combined_audio,
'info': f'Generated {len(text_segments)} chunks using ChatterBox batch processing (batch_size={batch_size})'
}
def _get_audio_duration(self, audio_tensor: torch.Tensor) -> float:
"""Calculate audio duration in seconds."""
# ChatterBox uses 44.1kHz sample rate
if audio_tensor.dim() == 1:
num_samples = audio_tensor.shape[0]
elif audio_tensor.dim() == 2:
num_samples = audio_tensor.shape[1]
else:
num_samples = audio_tensor.numel()
return num_samples / 44100 # ChatterBox sample rate
@@ -103,6 +103,7 @@ class ChatterBoxStreamingAdapter(StreamingEngineAdapter):
if has_streaming_method:
# Build inputs dict like the old system
inputs = {
"character": segment.character, # CRITICAL: Character is needed for caching
"exaggeration": kwargs.get("exaggeration", 0.5),
"temperature": kwargs.get("temperature", 0.8),
"cfg_weight": kwargs.get("cfg_weight", 0.5),
@@ -113,6 +114,15 @@ class ChatterBoxStreamingAdapter(StreamingEngineAdapter):
"device": kwargs.get("device", "auto"),
"reference_audio": kwargs.get("reference_audio", None)
}
# Apply segment-level parameters if available
segment_params = segment.metadata.get('parameters', {}) if segment.metadata else {}
if segment_params:
# Import parameter application function
from utils.text.segment_parameters import apply_segment_parameters
segment_config = apply_segment_parameters(inputs, segment_params, "chatterbox_official_23lang")
inputs.update(segment_config)
print(f" 📊 Streaming: Applying segment parameters: {segment_params}")
# Call exactly like the old working system
audio = self.node._process_single_segment_for_streaming(
@@ -295,35 +305,25 @@ class ChatterBoxStreamingAdapter(StreamingEngineAdapter):
# Check if node has generation method
if hasattr(self.node, '_generate_tts_with_pause_tags'):
print(f"🏷️ ADAPTER: Calling _generate_tts_with_pause_tags for '{text[:30]}...'")
# Temporarily replace stateless wrapper with underlying model for pause tag processing
original_model = getattr(self.node, 'tts_model', None)
try:
# Load the correct model for this language and extract underlying model if needed
self.load_model_for_language(language, kwargs.get('device', 'auto'))
if hasattr(self.node, 'tts_model') and hasattr(self.node.tts_model, 'model'):
self.node.tts_model = self.node.tts_model.model
print(f"🔓 ADAPTER: Extracted underlying model for pause tag processing")
# Generate stable audio component for cache consistency
from utils.audio.audio_hash import generate_stable_audio_component
stable_audio_component = generate_stable_audio_component(
kwargs.get("reference_audio"), voice_path
)
# Use pause tag-aware generation
return self.node._generate_tts_with_pause_tags(
text, voice_path, exaggeration, temperature, cfg_weight,
language, True, character=character, seed=seed,
enable_cache=enable_cache,
crash_protection_template=crash_protection,
stable_audio_component=stable_audio_component
)
finally:
# Restore original model
if original_model is not None:
self.node.tts_model = original_model
# Load the correct model for this language
self.load_model_for_language(language, kwargs.get('device', 'auto'))
# Generate stable audio component for cache consistency
from utils.audio.audio_hash import generate_stable_audio_component
stable_audio_component = generate_stable_audio_component(
kwargs.get("reference_audio"), voice_path
)
# Use pause tag-aware generation
# Note: tts_model is now a wrapper with __getattr__ forwarding, no unwrapping needed
return self.node._generate_tts_with_pause_tags(
text, voice_path, exaggeration, temperature, cfg_weight,
language, True, character=character, seed=seed,
enable_cache=enable_cache,
crash_protection_template=crash_protection,
stable_audio_component=stable_audio_component
)
elif hasattr(self.node, 'tts_model') and self.node.tts_model:
# Direct model generation
return self.node.tts_model.generate(
@@ -352,12 +352,13 @@ class ChatterBoxStreamingAdapter(StreamingEngineAdapter):
Returns:
True if segment can be processed
"""
# Check language support
# Check language support - fallback to English for unsupported languages
if segment.language not in self.supported_languages:
# Check if it's a local model
if not segment.language.startswith('local:'):
print(f"❌ ChatterBox doesn't support language: {segment.language}")
return False
print(f"⚠️ ChatterBox: Language '{segment.language}' not supported, falling back to English model")
# Modify segment language to English for fallback
segment.language = 'en'
# Basic validation
if not segment.text.strip():
+7 -3
View File
@@ -152,14 +152,14 @@ class F5TTSEngineAdapter:
def combine_audio_chunks(self, audio_segments: List[torch.Tensor], **params) -> torch.Tensor:
"""
Combine F5-TTS audio segments.
Combine F5-TTS audio segments using modular combination utility.
Args:
audio_segments: List of audio tensors to combine
**params: Combination parameters
Returns:
Combined audio tensor
Combined audio tensor, or tuple with timing info if requested
"""
if len(audio_segments) == 1:
return audio_segments[0]
@@ -167,9 +167,13 @@ class F5TTSEngineAdapter:
method = params.get("combination_method", "auto")
silence_ms = params.get("silence_ms", 100)
text_length = params.get("text_length", 0)
original_text = params.get("original_text", "")
text_chunks = params.get("text_chunks", None)
return_info = params.get("return_timing_info", False)
return self.node.combine_f5tts_audio_chunks(
audio_segments, method, silence_ms, text_length
audio_segments, method, silence_ms, text_length,
original_text=original_text, text_chunks=text_chunks, return_info=return_info
)
def _get_audio_duration(self, audio_tensor: torch.Tensor) -> float:
+14 -29
View File
@@ -159,30 +159,18 @@ class F5TTSStreamingAdapter(StreamingEngineAdapter):
if self._current_model == model_name and self._current_language == language:
return True
# Use universal smart model loader
from utils.models.smart_loader import smart_model_loader
def f5tts_load_callback(device: str, model: str):
"""Callback for F5-TTS model loading"""
if hasattr(self.node, 'load_f5tts_model'):
# Use node's existing model loading (which now uses smart loader internally)
return self.node.load_f5tts_model(model, device)
else:
# Fallback: try to load directly
from engines.f5tts.f5tts import F5TTSEngine
engine = F5TTSEngine()
engine.load_model(model, device)
return engine
# Use smart loader to get or load the model
model_instance, was_cached = smart_model_loader.load_model_if_needed(
engine_type="f5tts",
# Use unified interface for model loading
from utils.models.unified_model_interface import load_tts_model
# Load model via unified interface
model_instance = load_tts_model(
engine_name="f5tts",
model_name=model_name,
current_model=getattr(self.node, 'f5tts_model', None),
device=device,
load_callback=f5tts_load_callback
language=language,
force_reload=False
)
# Update node's model reference and our tracking
if hasattr(self.node, 'f5tts_model'):
self.node.f5tts_model = model_instance
@@ -190,16 +178,13 @@ class F5TTSStreamingAdapter(StreamingEngineAdapter):
self.node.f5tts = model_instance
else:
self.node.f5tts = model_instance
# Cache the model for local reuse and update tracking
self._loaded_models[model_name] = model_instance
self._current_model = model_name
self._current_language = language
if was_cached:
print(f"♻️ F5TTS STREAMING: Reused {model_name} from smart loader")
else:
print(f"✅ F5TTS STREAMING: Loaded {model_name} via smart loader")
print(f"✅ F5TTS STREAMING: Loaded {model_name} via unified interface")
return True
@@ -312,8 +297,8 @@ class F5TTSStreamingAdapter(StreamingEngineAdapter):
reference_audio, reference_text = self.node._load_reference_audio(voice_path)
else:
# Simple loading fallback
import torchaudio
reference_audio, _ = torchaudio.load(voice_path)
from utils.audio.processing import AudioProcessingUtils
reference_audio, _ = AudioProcessingUtils.safe_load_audio(voice_path)
# Check for text file
text_path = voice_path.replace('.wav', '.txt').replace('.mp3', '.txt')
+442
View File
@@ -0,0 +1,442 @@
"""
Higgs Audio Engine Adapter - Engine-specific adapter for Higgs Audio 2
Provides standardized interface for Higgs Audio operations in unified engine
"""
import torch
from typing import Dict, Any, Optional, List, Tuple
# Use absolute import to avoid relative import issues in ComfyUI
import sys
import os
current_dir = os.path.dirname(__file__)
parent_dir = os.path.dirname(current_dir)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
from utils.models.language_mapper import get_model_for_language
from engines.higgs_audio.higgs_audio import HiggsAudioEngine
from engines.higgs_audio.higgs_audio_downloader import HIGGS_AUDIO_MODELS
class HiggsAudioEngineAdapter:
"""Engine-specific adapter for Higgs Audio 2."""
def __init__(self, node_instance):
"""
Initialize Higgs Audio adapter.
Args:
node_instance: HiggsAudioNode or HiggsAudioSRTNode instance
"""
self.node = node_instance
self.engine_type = "higgs_audio"
self.higgs_engine = HiggsAudioEngine()
def get_model_for_language(self, lang_code: str, default_model: str) -> str:
"""
Get Higgs Audio model name for specified language.
Args:
lang_code: Language code (e.g., 'en', 'zh')
default_model: Default model name
Returns:
Higgs Audio model name for the language
Note: Higgs Audio v2 supports multiple languages with the same model
"""
# Higgs Audio v2 3B model supports multiple languages
# Map all language codes to the default model for now
supported_languages = ['en', 'zh', 'zh-cn'] # Add more as confirmed
if lang_code.lower() in supported_languages:
return "higgs-audio-v2-3B"
else:
print(f"⚠️ Language '{lang_code}' not specifically tested with Higgs Audio, using default model")
return "higgs-audio-v2-3B"
def load_base_model(self, model_name: str, device: str, enable_cuda_graphs: bool = True):
"""
Load base Higgs Audio model.
Args:
model_name: Model name to load
device: Device to load model on
enable_cuda_graphs: Whether to enable CUDA Graph optimization
"""
# Check if the model is already loaded AND engine is initialized
# Also check for corruption flags that require recreation
current_model = getattr(self.node, 'current_model_name', None)
cuda_graph_corrupted = getattr(self.higgs_engine, '_cuda_graph_corrupted', False)
needs_recreation = getattr(self.higgs_engine, '_needs_recreation', False)
if (current_model == model_name and
self.higgs_engine.engine is not None and
not cuda_graph_corrupted and
not needs_recreation):
print(f"💾 Higgs Audio adapter: Model '{model_name}' already loaded - ensuring device and clearing other models")
# Even if model is loaded, ensure it's on the right device and clear other models
from utils.models.comfyui_model_wrapper.model_manager import tts_model_manager
# This will clear other TTS models (like Step Audio EditX) from VRAM
tts_model_manager.ensure_device("higgs_audio", device)
return
if cuda_graph_corrupted or needs_recreation:
print(f"🔥 Higgs Audio adapter: Engine corruption detected - forcing complete recreation")
# Clear the corruption flags
self.higgs_engine._cuda_graph_corrupted = False
self.higgs_engine._needs_recreation = False
# Force new engine creation by clearing current engine
self.higgs_engine.engine = None
# Determine model paths based on model name
if model_name in HIGGS_AUDIO_MODELS:
model_config = HIGGS_AUDIO_MODELS[model_name]
generation_model = model_config["generation_repo"]
tokenizer_model = model_config["tokenizer_repo"]
else:
# Default fallback
generation_model = "bosonai/higgs-audio-v2-generation-3B-base"
tokenizer_model = "bosonai/higgs-audio-v2-tokenizer"
# Initialize the Higgs Audio engine with CUDA Graph setting
self.higgs_engine.initialize_engine(
model_path=generation_model,
tokenizer_path=tokenizer_model,
device=device,
enable_cuda_graphs=enable_cuda_graphs
)
# Store current model for caching
self.node.current_model_name = model_name
print(f"✅ Higgs Audio adapter: Loaded model '{model_name}' on {device}")
def load_language_model(self, model_name: str, device: str):
"""
Load language-specific Higgs Audio model.
Args:
model_name: Language-specific model name
device: Device to load model on
Note: Higgs Audio v2 uses the same model for all languages
"""
# For Higgs Audio, language models are the same as base models
self.load_base_model(model_name, device)
def generate_segment_audio(self, text: str, char_audio: str, char_text: str,
character: str = "narrator", **params) -> torch.Tensor:
"""
Generate Higgs Audio audio for a text segment.
Args:
text: Text to generate audio for
char_audio: Reference audio file path or audio dict
char_text: Reference text
character: Character name for caching
**params: Additional Higgs Audio parameters
Returns:
Generated audio tensor
"""
# Ensure engine is initialized with current model/device
model = params.get("model", "higgs-audio-v2-3B")
device = params.get("device", "auto")
enable_cuda_graphs = params.get("enable_cuda_graphs", True) # Get CUDA Graph setting
self.load_base_model(model, device, enable_cuda_graphs)
# Extract Higgs Audio specific parameters
system_prompt = params.get("system_prompt", "Generate audio following instruction.")
temperature = params.get("temperature", 1.0)
top_p = params.get("top_p", 0.95)
top_k = params.get("top_k", 50)
max_new_tokens = params.get("max_new_tokens", 2048)
force_audio_gen = params.get("force_audio_gen", False)
ras_win_len = params.get("ras_win_len", 7) # Default from boson_multimodal
ras_max_num_repeat = params.get("ras_max_num_repeat", 2) # Default from boson_multimodal
enable_cuda_graphs = params.get("enable_cuda_graphs", True) # CUDA Graph toggle
seed = params.get("seed", -1)
enable_cache = params.get("enable_audio_cache", True)
model_name = params.get("model", "higgs-audio-v2-3B")
device = params.get("device", "auto")
# Native multi-speaker mode parameters
multi_speaker_mode = params.get("multi_speaker_mode", "Custom Character Switching")
second_narrator_audio = params.get("second_narrator_audio")
second_narrator_text = params.get("second_narrator_text", "")
# Chunking parameters
max_chars_per_chunk = params.get("max_chars_per_chunk", 400)
silence_between_chunks_ms = params.get("silence_between_chunks_ms", 100)
enable_chunking = len(text) > max_chars_per_chunk # Auto-enable chunking for long text
# Convert chars to tokens for engine (Higgs Audio uses ~3.5 chars per token)
max_tokens_per_chunk = int(max_chars_per_chunk / 3.5)
# Initialize engine if not already done
if not self.higgs_engine.engine:
print(f"🚀 Initializing Higgs Audio engine with model: {model_name}")
self.load_base_model(model_name, device)
# Prepare reference audio
reference_audio = None
if char_audio:
if isinstance(char_audio, str) and os.path.exists(char_audio):
# Load audio file
try:
from utils.audio.processing import AudioProcessingUtils
waveform, sample_rate = AudioProcessingUtils.safe_load_audio(char_audio)
if waveform.dim() == 2 and waveform.size(0) > 1:
# Convert to mono if stereo
waveform = torch.mean(waveform, dim=0, keepdim=True)
# Move to same device as the model to prevent device mismatch
from utils.device import resolve_torch_device
target_device = resolve_torch_device(device)
waveform = waveform.float().to(target_device)
reference_audio = {"waveform": waveform, "sample_rate": sample_rate}
print(f" 📁 Loaded reference audio on {target_device}")
except Exception as e:
print(f"⚠️ Failed to load reference audio {char_audio}: {e}")
elif isinstance(char_audio, dict):
# Already in ComfyUI format - ensure tensors are on correct device
reference_audio = {}
from utils.device import resolve_torch_device
target_device = resolve_torch_device(device)
for key, value in char_audio.items():
if torch.is_tensor(value):
reference_audio[key] = value.to(target_device)
print(f" 📦 Moved ComfyUI {key} tensor to {target_device}")
else:
reference_audio[key] = value
# Generate audio using Higgs Audio engine with stateless methods
try:
if multi_speaker_mode in ["Native Multi-Speaker (System Context)", "Native Multi-Speaker (Conversation)"]:
# Use stateless native multi-speaker mode with multiple reference audios
if hasattr(self.higgs_engine, 'generate_native_multispeaker_stateless'):
audio_result, generation_info = self.higgs_engine.generate_native_multispeaker_stateless(
text=text,
primary_reference_audio=reference_audio,
primary_reference_text=char_text or "",
secondary_reference_audio=second_narrator_audio,
secondary_reference_text=second_narrator_text,
use_system_context=(multi_speaker_mode == "Native Multi-Speaker (System Context)"),
system_prompt=system_prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
top_k=top_k,
force_audio_gen=force_audio_gen,
ras_win_len=ras_win_len,
ras_max_num_repeat=ras_max_num_repeat,
enable_cache=enable_cache,
character=character,
seed=seed
)
else:
# Fallback to standard method if wrapper not available
audio_result, generation_info = self.higgs_engine.generate_native_multispeaker(
text=text,
primary_reference_audio=reference_audio,
primary_reference_text=char_text or "",
secondary_reference_audio=second_narrator_audio,
secondary_reference_text=second_narrator_text,
use_system_context=(multi_speaker_mode == "Native Multi-Speaker (System Context)"),
system_prompt=system_prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
top_k=top_k,
force_audio_gen=force_audio_gen,
ras_win_len=ras_win_len,
ras_max_num_repeat=ras_max_num_repeat,
enable_cache=enable_cache,
character=character,
seed=seed
)
else:
# Use stateless custom character switching mode (existing behavior)
if hasattr(self.higgs_engine, 'generate_stateless'):
audio_result, generation_info = self.higgs_engine.generate_stateless(
text=text,
reference_audio=reference_audio,
reference_text=char_text or "",
system_prompt=system_prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
top_k=top_k,
force_audio_gen=force_audio_gen,
ras_win_len=ras_win_len,
ras_max_num_repeat=ras_max_num_repeat,
enable_chunking=enable_chunking,
max_tokens_per_chunk=max_tokens_per_chunk, # Now properly converted from chars to tokens
silence_between_chunks_ms=silence_between_chunks_ms,
enable_cache=enable_cache,
character=character,
seed=seed
)
else:
# Fallback to standard method if wrapper not available
audio_result, generation_info = self.higgs_engine.generate(
text=text,
reference_audio=reference_audio,
reference_text=char_text or "",
system_prompt=system_prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
top_k=top_k,
force_audio_gen=force_audio_gen,
ras_win_len=ras_win_len,
ras_max_num_repeat=ras_max_num_repeat,
enable_chunking=enable_chunking,
max_tokens_per_chunk=max_tokens_per_chunk, # Now properly converted from chars to tokens
silence_between_chunks_ms=silence_between_chunks_ms,
enable_cache=enable_cache,
character=character,
seed=seed
)
# Return the waveform tensor with correct dimensions for ComfyUI
waveform = audio_result["waveform"]
# Ensure correct dimensions: ComfyUI expects [channels, samples]
if waveform.dim() == 3:
# Handle [batch, channels, samples] -> [channels, samples]
if waveform.size(0) == 1: # batch size of 1
waveform = waveform.squeeze(0) # Remove batch dimension
else:
waveform = waveform[0] # Take first item from batch
elif waveform.dim() == 1:
waveform = waveform.unsqueeze(0) # Add channel dimension
return waveform
except Exception as e:
print(f"❌ Error generating Higgs Audio segment: {e}")
raise e
def get_available_models(self) -> List[str]:
"""
Get list of available Higgs Audio models.
Returns:
List of model names
"""
return self.higgs_engine.get_available_models()
def get_model_info(self, model_name: str) -> Optional[Dict[str, Any]]:
"""
Get information about a Higgs Audio model.
Args:
model_name: Name of the model
Returns:
Model information dict or None
"""
return self.higgs_engine.downloader.get_model_info(model_name)
def cleanup(self):
"""Clean up resources"""
if hasattr(self, 'higgs_engine'):
self.higgs_engine.cleanup()
# Clear current model reference
if hasattr(self.node, 'current_model_name'):
self.node.current_model_name = None
def supports_streaming(self) -> bool:
"""
Check if Higgs Audio supports streaming.
Returns:
False for now - streaming not implemented yet
"""
return False
def get_sample_rate(self) -> int:
"""
Get the sample rate used by Higgs Audio.
Returns:
Sample rate (24000 Hz for Higgs Audio v2)
"""
return 24000 # Higgs Audio v2 uses 24kHz sample rate
def supports_voice_cloning(self) -> bool:
"""
Check if Higgs Audio supports voice cloning.
Returns:
True - Higgs Audio v2 supports voice cloning
"""
return True
def validate_parameters(self, **params) -> Dict[str, Any]:
"""
Validate and normalize Higgs Audio parameters.
Args:
**params: Parameters to validate
Returns:
Validated parameters dict
"""
validated = {}
# Temperature validation
temperature = params.get("temperature", 1.0)
validated["temperature"] = max(0.0, min(2.0, float(temperature)))
# Top-p validation
top_p = params.get("top_p", 0.95)
validated["top_p"] = max(0.1, min(1.0, float(top_p)))
# Top-k validation
top_k = params.get("top_k", 50)
validated["top_k"] = max(-1, min(100, int(top_k)))
# Max tokens validation
max_tokens = params.get("max_new_tokens", 2048)
validated["max_new_tokens"] = max(1, min(4096, int(max_tokens)))
# Force audio gen validation
force_audio_gen = params.get("force_audio_gen", False)
validated["force_audio_gen"] = bool(force_audio_gen)
# RAS parameters validation
ras_win_len = params.get("ras_win_len", 7)
if ras_win_len is not None and ras_win_len > 0:
validated["ras_win_len"] = max(1, min(20, int(ras_win_len)))
else:
validated["ras_win_len"] = None # Disable RAS
ras_max_num_repeat = params.get("ras_max_num_repeat", 2)
validated["ras_max_num_repeat"] = max(1, min(5, int(ras_max_num_repeat)))
# Audio priority validation
audio_priority = params.get("audio_priority", "auto")
valid_priorities = ["auto", "preset_dropdown", "reference_input", "force_preset"]
if audio_priority not in valid_priorities:
print(f"⚠️ Invalid audio priority '{audio_priority}', using 'auto'")
validated["audio_priority"] = "auto"
else:
validated["audio_priority"] = audio_priority
# System prompt
validated["system_prompt"] = params.get("system_prompt", "Generate audio following instruction.")
# Seed validation
seed = params.get("seed", -1)
validated["seed"] = int(seed) if seed >= 0 else -1
# Cache settings
validated["enable_audio_cache"] = params.get("enable_audio_cache", True)
return validated
+586
View File
@@ -0,0 +1,586 @@
"""
IndexTTS-2 Engine Adapter
Provides standardized interface for IndexTTS-2 integration with TTS Audio Suite.
Handles parameter mapping, character switching, and emotion control.
"""
import os
import torch
from typing import Dict, Any, Optional, List, Union
from engines.index_tts.index_tts import IndexTTSEngine
from engines.index_tts.index_tts_downloader import index_tts_downloader
from utils.text.character_parser import character_parser
from utils.voice.discovery import get_character_mapping, get_available_characters
from utils.audio.cache import get_audio_cache
class IndexTTSAdapter:
"""
Adapter for IndexTTS-2 engine providing unified interface compatibility.
Handles:
- Parameter mapping between unified interface and IndexTTS-2
- Character switching with [character:emotion_ref] syntax
- Emotion control via audio references or vectors
- Caching integration
- Model management
"""
def __init__(self):
"""Initialize the IndexTTS-2 adapter."""
self.engine = None
self.audio_cache = get_audio_cache()
def initialize_engine(self,
model_path: Optional[str] = None,
device: str = "auto",
use_fp16: bool = True,
use_cuda_kernel: Optional[bool] = None,
use_deepspeed: bool = False,
use_torch_compile: bool = False,
use_accel: bool = False):
"""
Initialize IndexTTS-2 engine.
Args:
model_path: Path to model directory (auto-downloaded if None)
device: Target device
use_fp16: Use FP16 for inference
use_cuda_kernel: Use BigVGAN CUDA kernels
use_deepspeed: Use DeepSpeed optimization
use_torch_compile: Enable torch.compile optimization for S2Mel stage
use_accel: Enable GPT2 acceleration with FlashAttention
"""
# Auto-download model if not provided or if "auto-download" is specified
if model_path is None or model_path == "auto-download":
if not index_tts_downloader.is_model_available():
print("📥 IndexTTS-2 model not found, downloading...")
model_path = index_tts_downloader.download_model()
else:
model_path = index_tts_downloader.get_model_path()
# Initialize engine
self.engine = IndexTTSEngine(
model_dir=model_path,
device=device,
use_fp16=use_fp16,
use_cuda_kernel=use_cuda_kernel,
use_deepspeed=use_deepspeed,
use_torch_compile=use_torch_compile,
use_accel=use_accel
)
def generate(self,
text: str,
speaker_audio: Optional[str] = None,
emotion_audio: Optional[str] = None,
emotion_alpha: float = 1.0,
emotion_vector: Optional[List[float]] = None,
use_emotion_text: bool = False,
emotion_text: Optional[str] = None,
use_random: bool = False,
interval_silence: int = 200,
max_text_tokens_per_segment: int = 120,
# Generation parameters
temperature: float = 0.8,
top_p: float = 0.8,
top_k: int = 30,
length_penalty: float = 0.0,
num_beams: int = 3,
repetition_penalty: float = 10.0,
max_mel_tokens: int = 1500,
# Streaming parameters
stream_return: bool = False,
more_segment_before: int = 0,
**kwargs) -> torch.Tensor:
"""
Generate speech with IndexTTS-2.
Args:
text: Text to synthesize (supports [character:emotion] tags)
speaker_audio: Speaker reference audio file path
emotion_audio: Emotion reference audio file path
emotion_alpha: Emotion blend factor (0.0-1.0)
emotion_vector: Manual emotion vector [happy, angry, sad, afraid, disgusted, melancholic, surprised, calm]
use_emotion_text: Extract emotions from text
emotion_text: Custom emotion description text
use_random: Enable random sampling
interval_silence: Silence between segments (ms)
max_text_tokens_per_segment: Max tokens per segment
temperature: Sampling temperature
top_p: Nucleus sampling parameter
top_k: Top-k sampling parameter
length_penalty: Length penalty for beam search
num_beams: Number of beams for beam search
repetition_penalty: Repetition penalty
max_mel_tokens: Maximum mel tokens to generate
**kwargs: Additional parameters
Returns:
Generated audio tensor [1, samples] at 22050 Hz
"""
if self.engine is None:
self.initialize_engine()
# Check if text actually contains character tags before processing
has_character_tags = character_parser.CHARACTER_TAG_PATTERN.search(text) is not None
if has_character_tags:
# Parse character switching tags with emotion support
processed_segments = self._process_character_tags_with_emotions(text)
if len(processed_segments) > 1:
# Multi-segment character switching - process each segment separately
return self._generate_multi_character_segments(processed_segments, speaker_audio, emotion_audio, **kwargs)
elif processed_segments:
# Single character segment
first_segment = processed_segments[0]
processed_text = first_segment.get('text', '').strip()
character_name = first_segment.get('character')
emotion_ref = first_segment.get('emotion')
else:
processed_text = text
character_name = None
emotion_ref = None
else:
# No character tags - use text as is
processed_text = text
character_name = None
emotion_ref = None
# Determine final speaker and emotion audio
final_speaker_audio = speaker_audio
# Handle Character Voices emotion_audio format
if emotion_audio and isinstance(emotion_audio, dict):
if "audio_path" in emotion_audio:
# Character Voices format: {'audio': {...}, 'audio_path': 'path', ...}
final_emotion_audio = emotion_audio["audio_path"]
print(f"🎭 Using Character Voices emotion audio: {emotion_audio.get('character_name', 'unknown')} -> {final_emotion_audio}")
elif "waveform" in emotion_audio:
# Direct AUDIO format: {'waveform': tensor, 'sample_rate': rate}
final_emotion_audio = emotion_audio
else:
final_emotion_audio = emotion_audio
else:
final_emotion_audio = emotion_audio
# Only do character mapping if we actually have character tags
if has_character_tags:
# Collect all characters that might be needed
all_characters = []
if character_name:
all_characters.append(character_name)
if emotion_ref:
all_characters.append(emotion_ref)
# Get character mapping for IndexTTS
if all_characters:
character_mapping = get_character_mapping(all_characters, engine_type="index_tts")
if character_name and character_name in character_mapping:
# IndexTTS returns (audio_path, reference_text) tuples, we need audio_path
character_audio_path = character_mapping[character_name][0]
if character_audio_path is not None:
final_speaker_audio = character_audio_path
print(f"🎭 Using character voice: {character_name} -> {final_speaker_audio}")
else:
print(f"⚠️ Character '{character_name}' mapping returned None, using original speaker audio: {final_speaker_audio}")
elif character_name:
print(f"⚠️ Character '{character_name}' not found in character mapping, using original speaker audio: {final_speaker_audio}")
if emotion_ref and emotion_ref in character_mapping:
# IndexTTS returns (audio_path, reference_text) tuples, we need audio_path
final_emotion_audio = character_mapping[emotion_ref][0]
print(f"😊 Using emotion reference: {emotion_ref} -> {final_emotion_audio}")
# For caching, we need stable identifiers for audio references
# Convert file paths to content hashes if they're temp files
cache_speaker_audio = self._get_stable_audio_identifier(final_speaker_audio)
cache_emotion_audio = self._get_stable_audio_identifier(final_emotion_audio) if final_emotion_audio else final_emotion_audio
# Check cache before generation
cache_key = self._generate_cache_key(
text=processed_text,
speaker_audio=cache_speaker_audio,
emotion_audio=cache_emotion_audio,
emotion_alpha=emotion_alpha,
emotion_vector=emotion_vector,
use_emotion_text=use_emotion_text,
emotion_text=emotion_text,
use_random=use_random,
temperature=temperature,
top_p=top_p,
top_k=top_k,
length_penalty=length_penalty,
repetition_penalty=repetition_penalty,
max_mel_tokens=max_mel_tokens,
max_text_tokens_per_segment=max_text_tokens_per_segment,
interval_silence=interval_silence,
stream_return=stream_return,
more_segment_before=more_segment_before,
**kwargs # Include seed and other kwargs in cache key
)
# print(f"🐛 IndexTTS-2 cache key params: text='{processed_text[:30]}...', seed={kwargs.get('seed', 'missing')}, speaker='{final_speaker_audio}', emotion='{final_emotion_audio}'")
# print(f"🐛 IndexTTS-2 full cache key: {cache_key}")
# print(f"🐛 IndexTTS-2 cache stats: {self.audio_cache.get_cache_stats()}")
cached_audio = self.audio_cache.get_cached_audio(cache_key)
if cached_audio:
character_desc = character_name or 'narrator'
print(f"💾 Using cached IndexTTS-2 audio for '{character_desc}': '{processed_text[:30]}...'")
return cached_audio[0]
# else:
# print(f"❌ IndexTTS-2 cache MISS for key: {cache_key}")
# # Show first few existing keys for comparison
# # Access the global cache properly
# from utils.audio.cache import GLOBAL_AUDIO_CACHE
# existing_keys = list(GLOBAL_AUDIO_CACHE.keys())
# if existing_keys:
# print(f"🔍 Existing cache keys (first 3): {existing_keys[:3]}")
# else:
# print(f"🔍 Cache is empty")
# Handle seed for deterministic generation
seed = kwargs.get('seed', 0)
if seed != 0:
torch.manual_seed(seed)
import random
import numpy as np
random.seed(seed)
np.random.seed(seed)
# Filter out seed from kwargs (used for caching but not supported by IndexTTS engine)
engine_kwargs = {k: v for k, v in kwargs.items() if k != 'seed'}
# Add streaming parameters to engine kwargs
engine_kwargs['stream_return'] = stream_return
engine_kwargs['more_segment_before'] = more_segment_before
# Apply consistent emotion priority: emotion_audio takes precedence over other emotion controls
# This ensures consistent behavior whether using character tags or direct engine inputs
if final_emotion_audio:
# emotion_audio connected - disable other emotion controls
final_emotion_vector = None
final_use_emotion_text = False
final_emotion_text = None
else:
# No emotion_audio - use provided emotion controls
final_emotion_vector = emotion_vector
final_use_emotion_text = use_emotion_text
final_emotion_text = emotion_text
# Generate audio with OOM protection
try:
audio = self.engine.generate(
text=processed_text,
speaker_audio=final_speaker_audio,
emotion_audio=final_emotion_audio,
emotion_alpha=emotion_alpha,
emotion_vector=final_emotion_vector,
use_emotion_text=final_use_emotion_text,
emotion_text=final_emotion_text,
use_random=use_random,
interval_silence=interval_silence,
max_text_tokens_per_segment=max_text_tokens_per_segment,
temperature=temperature,
top_p=top_p,
top_k=top_k,
length_penalty=length_penalty,
num_beams=num_beams,
repetition_penalty=repetition_penalty,
max_mel_tokens=max_mel_tokens,
**engine_kwargs
)
except torch.OutOfMemoryError as e:
# Analyze audio after OOM to provide helpful feedback
audio_analysis = self._analyze_audio_after_oom(final_speaker_audio, final_emotion_audio, max_mel_tokens)
raise RuntimeError(
f"❌ IndexTTS-2 ran out of GPU memory during generation.\n{audio_analysis}"
) from e
# Handle streaming vs non-streaming returns
if stream_return:
# In streaming mode, audio is a generator - return it as-is without caching
return audio
else:
# Cache the result for non-streaming mode
duration = audio.shape[-1] / 22050.0 # IndexTTS-2 uses 22050 Hz
self.audio_cache.cache_audio(cache_key, audio, duration)
# print(f"💾 IndexTTS-2 CACHED audio with key: {cache_key}")
return audio
def _process_character_tags_with_emotions(self, text: str) -> List[Dict[str, Any]]:
"""
Process character switching tags with emotion support.
"""
from utils.text.character_parser.base_parser import CharacterParser
from utils.voice.discovery import get_available_characters, voice_discovery
# Create a temporary parser and set it up properly
temp_parser = CharacterParser()
# Get actual available characters and aliases
available_chars = get_available_characters()
character_aliases = voice_discovery.get_character_aliases()
# Build complete available set (like before but not hardcoded)
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())
temp_parser.set_available_characters(list(all_available))
# Set language defaults
char_lang_defaults = voice_discovery.get_character_language_defaults()
for char, lang in char_lang_defaults.items():
temp_parser.set_character_language_default(char, lang)
# Use emotion-aware parsing
segments = temp_parser.split_by_character_with_emotions(text)
processed_segments = []
for character, segment_text, language, emotion in segments:
segment_info = {
'character': character,
'text': segment_text,
'language': language,
'emotion': emotion
}
processed_segments.append(segment_info)
return processed_segments
def _generate_multi_character_segments(self, segments: List[Dict[str, Any]],
default_speaker_audio: Optional[str],
default_emotion_audio: Optional[str],
**kwargs) -> torch.Tensor:
"""
Generate audio for multiple character segments and combine them.
Args:
segments: List of segment dictionaries with character, text, language, emotion info
default_speaker_audio: Default speaker reference audio
default_emotion_audio: Default emotion reference audio
**kwargs: Additional generation parameters
Returns:
Combined audio tensor [1, samples] at 22050 Hz
"""
audio_segments = []
# Get character mapping for all unique characters
unique_characters = set()
for segment in segments:
if segment.get('character'):
unique_characters.add(segment['character'])
if segment.get('emotion'):
unique_characters.add(segment['emotion'])
# Get character mapping for IndexTTS
character_mapping = {}
if unique_characters:
character_mapping = get_character_mapping(list(unique_characters), engine_type="index_tts")
print(f"🎭 IndexTTS-2: Processing {len(segments)} character segment(s) - {', '.join([s.get('character', 'narrator') for s in segments])}")
for segment in segments:
character_name = segment.get('character', 'narrator')
segment_text = segment.get('text', '').strip()
emotion_ref = segment.get('emotion')
if not segment_text:
continue
# Determine speaker audio for this segment
speaker_audio = default_speaker_audio
if character_name and character_name in character_mapping:
character_audio_path = character_mapping[character_name][0]
if character_audio_path:
speaker_audio = character_audio_path
print(f"📖 Using character voice '{character_name}' | Ref: '{speaker_audio}'")
else:
print(f"⚠️ Character '{character_name}' has no audio reference, using default")
# Determine emotion audio for this segment
emotion_audio = default_emotion_audio
if emotion_ref and emotion_ref in character_mapping:
emotion_audio_path = character_mapping[emotion_ref][0]
if emotion_audio_path:
emotion_audio = emotion_audio_path
print(f"😊 Using emotion reference '{emotion_ref}' | Ref: '{emotion_audio}'")
# Generate cache key for this segment
segment_cache_key = self._generate_cache_key(
text=segment_text,
speaker_audio=speaker_audio,
emotion_audio=emotion_audio,
**kwargs
)
# Check cache first
cached_segment_audio = self.audio_cache.get_cached_audio(segment_cache_key)
if cached_segment_audio:
print(f"💾 Using cached IndexTTS-2 segment for '{character_name}': '{segment_text[:30]}...'")
segment_audio = cached_segment_audio[0]
else:
# Generate audio for this segment with OOM protection
try:
segment_audio = self.engine.generate(
text=segment_text,
speaker_audio=speaker_audio,
emotion_audio=emotion_audio,
**kwargs
)
except torch.OutOfMemoryError as e:
# Analyze audio after OOM in multi-character segments
audio_analysis = self._analyze_audio_after_oom(speaker_audio, emotion_audio, kwargs.get('max_mel_tokens', 1500))
raise RuntimeError(
f"❌ IndexTTS-2 OOM error in character segment '{character_name}'.\n{audio_analysis}"
) from e
# Cache the segment result
duration = segment_audio.shape[-1] / 22050.0 # IndexTTS-2 uses 22050 Hz
self.audio_cache.cache_audio(segment_cache_key, segment_audio, duration)
# print(f"💾 IndexTTS-2 CACHED segment for '{character_name}' with key: {segment_cache_key}")
audio_segments.append(segment_audio)
# Combine all audio segments
if audio_segments:
combined_audio = torch.cat(audio_segments, dim=-1)
return combined_audio
else:
# Return silence if no segments generated
return torch.zeros(1, 22050, dtype=torch.float32)
def _generate_cache_key(self, **params) -> str:
"""Generate cache key for IndexTTS-2."""
return self.audio_cache.generate_cache_key('index_tts', **params)
def _analyze_audio_after_oom(self, speaker_audio: str, emotion_audio: str, max_mel_tokens: int) -> str:
"""
Analyze audio files after OOM to provide helpful feedback.
Uses fast header-based duration check to avoid loading large files.
Args:
speaker_audio: Path to speaker audio file
emotion_audio: Path to emotion audio file
max_mel_tokens: Current max_mel_tokens setting
Returns:
Formatted analysis message with recommendations
"""
analysis_parts = []
# Analyze speaker audio
if speaker_audio and os.path.exists(speaker_audio):
try:
import soundfile as sf
info = sf.info(speaker_audio)
duration = info.duration
if duration > 60:
analysis_parts.append(f"🔍 Speaker audio: {duration:.1f}s (TOO LONG - major OOM risk)")
analysis_parts.append(" ✅ Solution: Use audio under 30 seconds")
elif duration > 30:
analysis_parts.append(f"🔍 Speaker audio: {duration:.1f}s (long - may cause OOM)")
analysis_parts.append(" ✅ Recommendation: Use shorter audio for stability")
else:
analysis_parts.append(f"🔍 Speaker audio: {duration:.1f}s (length OK)")
except Exception:
analysis_parts.append("🔍 Speaker audio: Could not analyze duration")
# Analyze emotion audio
if emotion_audio and os.path.exists(emotion_audio):
try:
import soundfile as sf
info = sf.info(emotion_audio)
duration = info.duration
if duration > 60:
analysis_parts.append(f"🔍 Emotion audio: {duration:.1f}s (TOO LONG - major OOM risk)")
elif duration > 30:
analysis_parts.append(f"🔍 Emotion audio: {duration:.1f}s (long - may cause OOM)")
else:
analysis_parts.append(f"🔍 Emotion audio: {duration:.1f}s (length OK)")
except Exception:
analysis_parts.append("🔍 Emotion audio: Could not analyze duration")
# Analyze max_mel_tokens setting
if max_mel_tokens > 1500:
analysis_parts.append(f"🔍 max_mel_tokens: {max_mel_tokens} (high - may cause OOM)")
analysis_parts.append(" ✅ Try reducing to 1000-1500")
else:
analysis_parts.append(f"🔍 max_mel_tokens: {max_mel_tokens} (setting OK)")
# Add general recommendations
analysis_parts.append("")
analysis_parts.append("💡 Quick fixes:")
analysis_parts.append(" • Use audio clips under 30 seconds")
analysis_parts.append(" • Reduce max_mel_tokens to 1000")
analysis_parts.append(" • Close other GPU applications")
return "\n".join(analysis_parts)
def _get_stable_audio_identifier(self, audio_path: str) -> str:
"""
Get stable identifier for audio file using centralized audio hashing.
"""
if not audio_path:
return audio_path
# Use our centralized audio hashing utility
from utils.audio.audio_hash import generate_stable_audio_component
return generate_stable_audio_component(audio_file_path=audio_path)
def get_supported_formats(self) -> List[str]:
"""Get supported audio formats."""
return ["wav", "mp3", "flac", "ogg"]
def get_sample_rate(self) -> int:
"""Get native sample rate."""
return 22050
def get_emotion_labels(self) -> List[str]:
"""Get supported emotion labels."""
return ["happy", "angry", "sad", "afraid", "disgusted", "melancholic", "surprised", "calm"]
def create_emotion_vector(self, **emotions) -> List[float]:
"""
Create emotion vector from keyword arguments.
Args:
**emotions: Emotion intensities (e.g., happy=0.8, angry=0.2)
Returns:
List of 8 emotion values
"""
if self.engine:
return self.engine.create_emotion_vector(**emotions)
else:
# Fallback implementation
labels = self.get_emotion_labels()
vector = [0.0] * len(labels)
for i, label in enumerate(labels):
if label in emotions:
vector[i] = max(0.0, min(1.2, float(emotions[label])))
return vector
def unload(self):
"""Unload the engine to free memory."""
if self.engine:
self.engine.unload()
self.engine = None
def __del__(self):
"""Cleanup on deletion."""
self.unload()
+6 -1
View File
@@ -160,7 +160,8 @@ class RVCEngineAdapter:
rms_mix_rate=rms_mix_rate,
resample_sr=resample_sr,
f0_autotune=f0_autotune,
crepe_hop_length=crepe_hop_length
crepe_hop_length=crepe_hop_length,
**kwargs # Pass through hubert_path and other engine parameters
)
if result is None:
@@ -321,4 +322,8 @@ class RVCEngineAdapter:
self._loaded_models.clear()
if hasattr(self, 'rvc_engine'):
self.rvc_engine.cleanup()
# CRITICAL: Clear minimal wrapper cache - this is where RVC models are actually stored
minimal_wrapper.clear_model_cache()
print("RVC adapter cleanup completed")
@@ -0,0 +1,439 @@
"""
Step Audio EditX Engine Adapter
Provides standardized interface for Step Audio EditX integration with TTS Audio Suite.
Handles parameter mapping, voice cloning, pause tag processing, and seed control.
"""
import os
import sys
import torch
import tempfile
import torchaudio
from typing import Dict, Any, Optional, List, Union
# Add project root to path
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.step_audio_editx.step_audio_editx import StepAudioEditXEngine
from utils.text.pause_processor import PauseTagProcessor
from utils.audio.cache import get_audio_cache
import folder_paths
class StepAudioEditXEngineAdapter:
"""
Adapter for Step Audio EditX engine providing unified interface compatibility.
Handles:
- Parameter mapping between unified interface and Step Audio EditX
- Voice cloning with prompt audio/text
- Pause tag processing and timing
- Seed control via global torch state
- Caching integration
- Model management
"""
def __init__(self, node_instance):
"""
Initialize the Step Audio EditX adapter.
Args:
node_instance: Parent node instance for context
"""
self.node = node_instance
self.engine = None
self.audio_cache = get_audio_cache()
# Job-level timing tracker (persists across blocks)
self.job_tracker = None
def start_job(self, total_blocks: int, block_texts: list):
"""
Initialize job tracker for time estimation across all blocks.
Args:
total_blocks: Number of blocks to process
block_texts: List of text lengths for each block (for weighted progress)
"""
import time
total_text = sum(block_texts)
self.job_tracker = {
'start_time': time.time(),
'total_blocks': total_blocks,
'blocks_completed': 0,
'block_texts': block_texts,
'total_text': total_text,
'text_completed': 0,
'current_block_text': 0
}
def set_current_block(self, block_idx: int):
"""Set the current block being processed."""
if self.job_tracker:
self.job_tracker['current_block_text'] = self.job_tracker['block_texts'][block_idx]
def complete_block(self):
"""Mark current block as completed."""
if self.job_tracker:
self.job_tracker['blocks_completed'] += 1
self.job_tracker['text_completed'] += self.job_tracker['current_block_text']
# Track total tokens from completed blocks
self.job_tracker['total_tokens'] = self.job_tracker.get('total_tokens', 0) + self.job_tracker.get('current_block_tokens', 0)
def end_job(self):
"""Clear job tracker after job completion."""
self.job_tracker = None
def load_base_model(self,
model_path: str,
device: str = "auto",
torch_dtype: str = "bfloat16",
quantization: Optional[str] = None):
"""
Load Step Audio EditX engine via unified interface (with caching).
Args:
model_path: Model identifier (local:ModelName or ModelName for auto-download)
device: Target device (auto/cuda/cpu)
torch_dtype: Model precision (bfloat16/float16/float32/auto)
quantization: Quantization mode (int4/int8 or None)
"""
from utils.models.unified_model_interface import unified_model_interface, ModelLoadConfig
from utils.device import resolve_torch_device
# Pass model_path directly - the downloader will handle "local:" resolution via factory
config = ModelLoadConfig(
engine_name="step_audio_editx",
model_type="tts",
model_name="Step-Audio-EditX",
model_path=model_path, # Downloader will resolve this if it's "local:xxx"
device=resolve_torch_device(device),
additional_params={
"torch_dtype": torch_dtype,
"quantization": quantization
}
)
self.engine = unified_model_interface.load_model(config)
def generate_with_pause_tags(self,
text: str,
voice_ref: Optional[Dict[str, Any]],
params: Dict[str, Any],
process_pauses: bool = True,
character_name: Optional[str] = None) -> torch.Tensor:
"""
Generate speech with pause tag processing (TTS Suite integration).
Args:
text: Input text (may contain <pause_X> tags)
voice_ref: Voice reference dict with 'prompt_audio_path' and 'prompt_text'
params: Generation parameters (including seed)
process_pauses: Whether to process pause tags
character_name: Character name for logging
Returns:
Generated audio tensor [1, samples] at 24000 Hz
"""
if self.engine is None:
raise RuntimeError("Engine not loaded. Call load_base_model() first.")
# Extract seed from params
seed = params.get('seed', 0)
# Set global torch seed for reproducibility (Step Audio EditX uses global torch state)
if seed > 0:
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
# Check if text has pause tags
if process_pauses and PauseTagProcessor.has_pause_tags(text):
# Process pause tags and generate segments
return self._generate_with_pauses(text, voice_ref, params, character_name)
else:
# Direct generation without pause processing
return self._generate_direct(text, voice_ref, params, character_name)
def _generate_direct(self,
text: str,
voice_ref: Optional[Dict[str, Any]],
params: Dict[str, Any],
character_name: Optional[str] = None) -> torch.Tensor:
"""
Direct generation without pause processing.
Args:
text: Input text (clean, no pause tags)
voice_ref: Voice reference dict
params: Generation parameters
character_name: Character name for logging
Returns:
Audio tensor [1, samples]
"""
# Get voice reference paths
prompt_audio_path, prompt_text = self._extract_voice_reference(voice_ref)
# Generate cache key and check cache
from utils.audio.audio_hash import generate_stable_audio_component
audio_component = generate_stable_audio_component(
audio_file_path=prompt_audio_path
) if prompt_audio_path else "default_voice"
cache_key = self.audio_cache.generate_cache_key(
'step_audio_editx',
text=text,
audio_component=audio_component,
prompt_text=prompt_text,
temperature=params.get('temperature', 0.7),
do_sample=params.get('do_sample', True),
max_new_tokens=params.get('max_new_tokens', 8192),
seed=params.get('seed', 0),
model_path=params.get('model_path', 'Step-Audio-EditX'),
device=params.get('device', 'auto'),
torch_dtype=params.get('torch_dtype', 'bfloat16'),
quantization=params.get('quantization', None),
character=character_name or 'narrator'
)
# Check cache
cached_audio = self.audio_cache.get_cached_audio(cache_key)
if cached_audio:
char_desc = character_name or 'narrator'
print(f"💾 Using cached Step Audio EditX audio for '{char_desc}': '{text[:30]}...'")
return cached_audio[0]
# Create ComfyUI progress bar for generation tracking with time prediction
max_new_tokens = params.get('max_new_tokens', 8192)
progress_bar = None
try:
import comfy.utils
import time
# Get job tracker from adapter (for total job time estimation)
job_tracker = self.job_tracker
class TimedProgressBar:
"""Progress bar wrapper with time tracking and prediction."""
def __init__(self, total, tracker):
self.total = total
self.current = 0
self.start_time = time.time()
self.wrapped = comfy.utils.ProgressBar(total)
self.last_print = 0
self.tracker = tracker
def update(self, delta=1):
self.current += delta
self.wrapped.update(delta)
def get_job_elapsed(self):
"""Get total job elapsed time in seconds."""
if self.tracker:
return time.time() - self.tracker['start_time']
return None
def get_job_remaining_str(self):
"""Calculate and return job remaining time string based on actual data."""
if not self.tracker or self.tracker['total_text'] <= 0:
return None
total_elapsed = time.time() - self.tracker['start_time']
if total_elapsed <= 0:
return None
text_completed = self.tracker['text_completed']
current_block_text = self.tracker['current_block_text']
total_text = self.tracker['total_text']
total_tokens_completed = self.tracker.get('total_tokens', 0)
total_tokens = total_tokens_completed + self.current
# Update current block tokens in tracker
self.tracker['current_block_tokens'] = self.current
if total_tokens <= 0:
return None
# Wait for more data before showing prediction
# Need at least 1 completed chunk OR 150+ tokens in first chunk
if total_tokens_completed == 0 and self.current < 150:
return None
# Estimate how much of current block is done based on tokens
# Use completed blocks to estimate tokens per char
if text_completed > 0 and total_tokens_completed > 0:
tokens_per_char = total_tokens_completed / text_completed
elif current_block_text > 0 and self.current > 0:
# First block - use conservative estimate based on current data
# Typical ratio is ~0.5-1.0 tokens per char
tokens_per_char = 0.7
else:
tokens_per_char = 0.7
# Estimate expected tokens for current block
expected_current_tokens = current_block_text * tokens_per_char if tokens_per_char > 0 else self.current * 2
# Current block progress (0 to 1)
current_block_progress = min(self.current / expected_current_tokens, 0.99) if expected_current_tokens > 0 else 0.5
# Text done = completed + portion of current block
effective_text_done = text_completed + (current_block_text * current_block_progress)
# Remaining text
remaining_text = total_text - effective_text_done
# Current speed (tokens per second)
tokens_per_sec = total_tokens / total_elapsed
# Remaining tokens estimate
remaining_tokens = remaining_text * tokens_per_char if tokens_per_char > 0 else remaining_text
# Remaining time
if tokens_per_sec > 0 and remaining_tokens > 0:
remaining_secs = remaining_tokens / tokens_per_sec
if remaining_secs < 60:
return f"~{remaining_secs:.0f}s left"
else:
return f"~{remaining_secs/60:.1f}m left"
# If no remaining text but still processing, show nothing
return None
progress_bar = TimedProgressBar(max_new_tokens, job_tracker)
except (ImportError, AttributeError):
pass # ComfyUI progress not available
# Generate using clone mode
result = self.engine.clone(
prompt_wav_path=prompt_audio_path,
prompt_text=prompt_text,
target_text=text,
temperature=params.get('temperature', 0.7),
do_sample=params.get('do_sample', True),
max_new_tokens=max_new_tokens,
progress_bar=progress_bar
)
# Handle both wrapped (tensor) and unwrapped (tuple) returns
if isinstance(result, tuple):
audio_tensor, _ = result # Unpack (audio, sample_rate) tuple
else:
audio_tensor = result
# Cache the result
duration = self.audio_cache._calculate_duration(audio_tensor, 'step_audio_editx')
self.audio_cache.cache_audio(cache_key, audio_tensor, duration)
return audio_tensor
def _generate_with_pauses(self,
text: str,
voice_ref: Optional[Dict[str, Any]],
params: Dict[str, Any],
character_name: Optional[str] = None) -> torch.Tensor:
"""
Generate with pause tag processing.
NOTE: This returns a COMBINED audio tensor. Edit tags are NOT extracted here
because the processor needs to track individual segments for post-processing.
The processor should NOT call this for text with edit tags - it should
extract edit tags BEFORE pause processing.
Args:
text: Text with pause tags ([pause:2], [pause:1.5s], [pause:500ms])
voice_ref: Voice reference dict
params: Generation parameters
character_name: Character name for logging
Returns:
Combined audio tensor [1, samples]
"""
# Parse pause tags - returns list of tuples: ('text', content) or ('pause', duration_seconds)
segments, clean_text = PauseTagProcessor.parse_pause_tags(text)
print(f"🎵 Processing {len(segments)} pause-delimited segments for '{character_name or 'unknown'}'")
audio_parts = []
sample_rate = 24000 # Step Audio EditX native sample rate
for segment_type, content in segments:
if segment_type == 'text':
# Generate audio for text segment
audio_tensor = self._generate_direct(content, voice_ref, params, character_name)
# Ensure correct shape [1, samples] (2D)
if audio_tensor.dim() == 1:
audio_tensor = audio_tensor.unsqueeze(0) # [samples] -> [1, samples]
elif audio_tensor.dim() == 3:
audio_tensor = audio_tensor.squeeze(0) # [1, 1, samples] -> [1, samples]
audio_parts.append(audio_tensor)
elif segment_type == 'pause':
# Create silence (content is duration in seconds)
silence = PauseTagProcessor.create_silence_segment(
content, sample_rate
)
audio_parts.append(silence)
# Concatenate all parts
if not audio_parts:
return torch.zeros(1, 0)
combined_audio = torch.cat(audio_parts, dim=-1)
return combined_audio
def _extract_voice_reference(self, voice_ref: Optional[Dict[str, Any]]) -> tuple:
"""
Extract voice reference audio path and text from voice_ref dict.
Args:
voice_ref: Voice reference dict (from voice discovery)
Returns:
Tuple of (prompt_audio_path, prompt_text)
"""
if voice_ref is None or not isinstance(voice_ref, dict):
raise ValueError(
"Step Audio EditX requires voice reference. "
"Please provide voice_ref with 'prompt_audio_path' and 'prompt_text'"
)
# Extract paths
prompt_audio_path = voice_ref.get('prompt_audio_path') or voice_ref.get('audio_path')
prompt_text = voice_ref.get('prompt_text') or voice_ref.get('reference_text', '')
if not prompt_audio_path:
raise ValueError(
"Voice reference missing 'prompt_audio_path'. "
"Ensure your voice file is in voices/ directory and properly detected."
)
if not prompt_text or not prompt_text.strip():
raise ValueError(
f"Voice reference missing 'prompt_text' for {prompt_audio_path}. "
"Step Audio EditX requires transcription of reference audio. "
"Add prompt_text to your voice.json file."
)
return prompt_audio_path, prompt_text
def cleanup(self):
"""
Clean up resources.
Note: For quantized models (int8/int4), we DON'T unload here because:
- Bitsandbytes models can't be moved to CPU and reloaded
- Auto-cleanup between generations would require full model reload
- Users should explicitly unload via the unload button when done
"""
# Don't auto-unload - let the model stay in VRAM for reuse
# Only explicit unload (via button) should trigger model deletion
pass
+833
View File
@@ -0,0 +1,833 @@
"""
VibeVoice Engine Adapter - Engine-specific adapter for VibeVoice
Provides standardized interface for VibeVoice operations in unified engine
"""
import torch
import re
from typing import Dict, Any, Optional, List, Tuple
import sys
import os
# Add parent directory for imports
current_dir = os.path.dirname(__file__)
parent_dir = os.path.dirname(current_dir)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
from utils.models.language_mapper import get_model_for_language
from utils.text.pause_processor import PauseTagProcessor
from utils.text.character_parser import CharacterParser
from engines.vibevoice_engine.vibevoice_downloader import VIBEVOICE_MODELS
from utils.models.manager import model_manager
class VibeVoiceEngineAdapter:
"""Engine-specific adapter for VibeVoice."""
def __init__(self, node_instance, engine_config=None):
"""
Initialize VibeVoice adapter.
Args:
node_instance: TTS node instance using this adapter
engine_config: Optional engine configuration dict
"""
self.node = node_instance
self.engine_type = "vibevoice"
self.model_manager = model_manager
self.character_parser = CharacterParser()
self.pause_processor = PauseTagProcessor()
# Track character to speaker mapping for native multi-speaker mode
self._character_speaker_map = {}
self._speaker_voices = []
# Store model parameters from config
if engine_config:
self._model_name = engine_config.get('model', 'vibevoice-1.5B')
self._device = engine_config.get('device', 'auto')
self._attention_mode = engine_config.get('attention_mode', 'auto')
self._quantize_4bit = engine_config.get('quantize_llm_4bit', False)
else:
self._model_name = 'vibevoice-1.5B'
self._device = 'auto'
self._attention_mode = 'auto'
self._quantize_4bit = False
@property
def vibevoice_engine(self):
"""Get VibeVoice engine, loading via unified interface if needed."""
from utils.models.unified_model_interface import load_tts_model
# Just call load - unified interface handles caching automatically
return load_tts_model(
engine_name="vibevoice",
model_name=self._model_name,
device=self._device,
attention_mode=self._attention_mode,
quantize_llm_4bit=self._quantize_4bit
)
@property
def current_model(self):
"""Get current model from engine."""
return self.vibevoice_engine.model if self.vibevoice_engine else None
@property
def current_processor(self):
"""Get current processor from engine."""
return self.vibevoice_engine.processor if self.vibevoice_engine else None
def get_model_for_language(self, lang_code: str, default_model: str) -> str:
"""
Get VibeVoice model name for specified language.
Args:
lang_code: Language code (e.g., 'en', 'zh')
default_model: Default model name
Returns:
VibeVoice model name for the language
Note: VibeVoice models support English and Chinese
"""
# VibeVoice models support both English and Chinese
supported_languages = ['en', 'zh', 'zh-cn', 'chinese', 'english']
if lang_code.lower() in supported_languages:
# Both models support EN/ZH, return the configured one
return default_model
else:
print(f"⚠️ VibeVoice: Language '{lang_code}' not officially supported (EN/ZH only)")
return default_model
def load_base_model(self, model_name: str, device: str, attention_mode: str = "auto", quantize_llm_4bit: bool = False):
"""
DEPRECATED: Model loading is now handled by unified TTS node.
This method is kept for backwards compatibility but does nothing.
The adapter now gets the engine dynamically from unified interface cache.
Args:
model_name: Model name (ignored, kept for compatibility)
device: Device (ignored, kept for compatibility)
attention_mode: Attention mode (ignored, kept for compatibility)
quantize_llm_4bit: Quantization (ignored, kept for compatibility)
"""
# Store parameters for reference (some code might check these)
self._cached_model_name = model_name
self._cached_device = device
self._cached_attention_mode = attention_mode
self._cached_quantize_4bit = quantize_llm_4bit
# Engine is now accessed via @property vibevoice_engine which gets it from unified interface
def _parse_language_tags(self, text: str) -> Tuple[str, Optional[str]]:
"""
Parse language tags like [de:Alice] from text.
Args:
text: Text with potential language tags
Returns:
Tuple of (processed_text, detected_language)
"""
# Pattern to match [language:character] tags
lang_pattern = r'\[([a-zA-Z\-]+):([^\]]+)\]'
detected_lang = None
processed_text = text
# Find and process language tags
matches = re.findall(lang_pattern, text)
if matches:
# Take the first language found
lang_code, character = matches[0]
detected_lang = lang_code.lower()
# Replace language tags with just character tags
for lang, char in matches:
processed_text = processed_text.replace(f'[{lang}:{char}]', f'[{char}]')
# Warn about language since VibeVoice doesn't have language control
if detected_lang not in ['en', 'zh', 'chinese', 'english']:
print(f"⚠️ VibeVoice: Language tag '{detected_lang}' found but model only supports EN/ZH")
return processed_text, detected_lang
def _convert_character_to_speaker_format(self, text: str) -> Tuple[str, Dict[str, int]]:
"""
Convert [Character] tags to Speaker N format for native multi-speaker.
Combines continuous character segments for better VibeVoice long-form generation.
Args:
text: Text with [Character] tags
Returns:
Tuple of (formatted_text, character_mapping)
"""
# Parse character segments
segments = self.character_parser.parse_text(text)
# Build character to speaker mapping and combine continuous segments
character_map = {}
character_blocks = {} # Group continuous text by character
for segment in segments:
char = segment.character
# Assign speaker index if not already mapped
if char not in character_map:
speaker_idx = len(character_map)
if speaker_idx >= 4:
print(f"⚠️ VibeVoice: More than 4 characters found, extra characters will use Speaker 3")
speaker_idx = 3
character_map[char] = speaker_idx
character_blocks[char] = []
# Accumulate text for this character (preserving paragraph structure)
character_blocks[char].append(segment.text.strip())
# Create Speaker blocks by combining all text for each character
speaker_lines = []
for char in character_map.keys():
speaker_idx = character_map[char]
# Combine all text blocks for this character with double newlines for paragraph breaks
combined_text = '\n\n'.join(character_blocks[char])
speaker_lines.append(f"Speaker {speaker_idx}: {combined_text}")
# Join with newlines for multi-speaker format
formatted_text = "\n".join(speaker_lines)
return formatted_text, character_map
def _combine_continuous_segments_for_vibevoice(self, segments: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
"""
Combine continuous same-character segments for better VibeVoice long-form generation.
This is VibeVoice-specific and doesn't affect other engines.
Args:
segments: Original segments from character parser
Returns:
Combined segments where continuous same-character text is merged
"""
if not segments:
return segments
combined_segments = []
current_character = None
current_text_blocks = []
for character, text in segments:
if character == current_character:
# Same character, accumulate text
current_text_blocks.append(text.strip())
else:
# Different character, finalize previous and start new
if current_character is not None:
# Combine accumulated text blocks with double newlines for paragraph separation
combined_text = '\n\n'.join(current_text_blocks)
combined_segments.append((current_character, combined_text))
# Start new character block
current_character = character
current_text_blocks = [text.strip()]
# Don't forget the last character block
if current_character is not None:
combined_text = '\n\n'.join(current_text_blocks)
combined_segments.append((current_character, combined_text))
return combined_segments
def generate_segment_audio(self, text: str, char_audio: str, char_text: str,
character: str = "narrator", **params) -> torch.Tensor:
"""
Generate VibeVoice audio for a text segment with caching support.
Follows the same pattern as other engines.
Args:
text: Text to generate audio for
char_audio: Reference audio file path or audio dict
char_text: Reference text
character: Character name for caching
**params: Additional VibeVoice parameters
Returns:
Generated audio tensor
"""
# Extract parameters
seed = params.get("seed", 42)
enable_cache = params.get("enable_audio_cache", True)
model = params.get("model", "vibevoice-1.5B")
device = params.get("device", "auto")
attention_mode = params.get("attention_mode", "auto")
quantize_llm_4bit = params.get("quantize_llm_4bit", False)
# Initialize engine if not already done
self.load_base_model(model, device, attention_mode, quantize_llm_4bit)
# Call engine with cache support
result = self.generate_segment(text, char_audio, {
**params,
'enable_cache': enable_cache,
'seed': seed
}, character=character)
# Extract tensor from result
if isinstance(result, dict) and "waveform" in result:
audio_tensor = result["waveform"]
# Ensure proper dimensions for ComfyUI
if audio_tensor.dim() == 3:
audio_tensor = audio_tensor.squeeze(0)
if audio_tensor.dim() == 1:
audio_tensor = audio_tensor.unsqueeze(0)
return audio_tensor
return result
def generate_segment(self, text: str, voice_ref: Optional[Dict], params: Dict, character: str = None) -> Dict:
"""
Generate audio for a text segment.
Args:
text: Text to generate
voice_ref: Voice reference audio
params: Generation parameters from engine config
character: Character name for cache isolation (required for proper caching)
Returns:
Audio dict with waveform and sample_rate
"""
# Extract parameters including new inference_steps
cfg_scale = params.get('cfg_scale', 1.3)
seed = params.get('seed', 42)
use_sampling = params.get('use_sampling', False)
temperature = params.get('temperature', 0.95)
top_p = params.get('top_p', 0.95)
inference_steps = params.get('inference_steps', 20) # New parameter
max_new_tokens = params.get('max_new_tokens')
# Check if model and processor are loaded
if self.current_model is None or self.current_processor is None:
raise RuntimeError("VibeVoice model not loaded. Call load_base_model() first.")
# Prepare voice samples
voice_samples = self.vibevoice_engine._prepare_voice_samples([voice_ref])
# For single segment, format as Speaker 1 (VibeVoice uses 1-based indexing)
formatted_text = f"Speaker 1: {text}"
# Extract cache parameters
enable_cache = params.get('enable_cache', True)
# Use provided character or fall back to params, then default
if character is None:
character = params.get('character', 'narrator')
# Generate stable audio component for cache consistency (like ChatterBox)
from utils.audio.audio_hash import generate_stable_audio_component
# Use provided stable component, or generate from voice reference (like ChatterBox)
audio_component = params.get("stable_audio_component")
if not audio_component:
# Generate stable component like ChatterBox does
if voice_ref and isinstance(voice_ref, dict):
if 'waveform' in voice_ref:
# Direct audio tensor format
audio_component = generate_stable_audio_component(voice_ref, None)
elif 'audio_path' in voice_ref:
# File path format
audio_component = generate_stable_audio_component(None, voice_ref['audio_path'])
else:
audio_component = "main_reference"
else:
audio_component = "main_reference"
# DEBUG: Print audio component to track cache invalidation
# print(f"🐛 VibeVoice DEBUG: character='{character}', audio_component='{audio_component[:50]}...'")
# Generate audio with inference_steps parameter
return self.vibevoice_engine.generate_speech(
text=formatted_text,
voice_samples=voice_samples,
cfg_scale=cfg_scale,
seed=seed,
use_sampling=use_sampling,
temperature=temperature,
top_p=top_p,
inference_steps=inference_steps, # New parameter
max_new_tokens=max_new_tokens,
enable_cache=enable_cache,
character=character,
stable_audio_component=audio_component,
multi_speaker_mode=params.get('multi_speaker_mode', 'Custom Character Switching')
)
def process_character_segments(self, segments: List[Tuple[str, str]],
voice_mapping: Dict[str, Any],
params: Dict) -> List[Dict]:
"""
Process multiple character segments.
Args:
segments: List of (character, text) tuples
voice_mapping: Dict mapping character names to voice references
params: Generation parameters
Returns:
List of audio dicts
"""
audio_segments = []
# VibeVoice-specific: Combine continuous same-character segments for better long-form generation
# This doesn't affect other engines since it's only applied here in VibeVoiceAdapter
combined_segments = self._combine_continuous_segments_for_vibevoice(segments)
print(f"🔄 VibeVoice: Combined {len(segments)} segments into {len(combined_segments)} continuous blocks")
for i, (char, text) in enumerate(combined_segments):
text_preview = text.replace('\n\n', '')[:100] + "..." if len(text) > 100 else text.replace('\n\n', '')
print(f" Block {i+1}: {char} -> {text_preview}")
# Check if we should use native multi-speaker mode
multi_speaker_mode = params.get('multi_speaker_mode', 'Custom Character Switching')
if multi_speaker_mode == "Native Multi-Speaker" and len(combined_segments) <= 4:
# Use native multi-speaker generation with combined segments
audio = self._generate_native_multispeaker(combined_segments, voice_mapping, params, None)
audio_segments.append(audio)
else:
# Custom Character Switching mode with VibeVoice-style grouped generation
# Group consecutive same-character segments and generate each group at once
audio_segments = self._generate_custom_character_switching_grouped(combined_segments, voice_mapping, params)
return audio_segments
def _generate_custom_character_switching_grouped(self, segments: List[Tuple[str, str]],
voice_mapping: Dict[str, Any],
params: Dict) -> List[Dict]:
"""
Generate audio for Custom Character Switching mode using VibeVoice-style grouped generation.
Groups consecutive same-character segments and generates each group at once like official VibeVoice.
Args:
segments: List of (character, text) tuples
voice_mapping: Dict mapping character names to voice references
params: Generation parameters
Returns:
List of audio dicts in order
"""
audio_segments = []
if not segments:
return audio_segments
# Group consecutive same-character segments for batch generation
character_groups = []
current_character = None
current_group = []
for character, text in segments:
if character == current_character:
# Same character, add to current group
current_group.append(text)
else:
# Different character, finalize previous group and start new
if current_character is not None and current_group:
character_groups.append((current_character, current_group))
current_character = character
current_group = [text]
# Don't forget the last group
if current_character is not None and current_group:
character_groups.append((current_character, current_group))
print(f"🎭 Custom Character Switching: Processing {len(character_groups)} character groups")
# Generate each character group using VibeVoice format
for group_idx, (character, text_list) in enumerate(character_groups):
print(f"🎤 Group {group_idx + 1}: Character '{character}' with {len(text_list)} segments")
# Format as Speaker 1 entries (VibeVoice style) and combine
formatted_lines = []
for text in text_list:
formatted_lines.append(f"Speaker 1: {text.strip()}")
# Combine all segments for this character into one script
combined_script = '\n'.join(formatted_lines)
print(f"🎭 CUSTOM CHARACTER GROUP - Formatted text for VibeVoice:")
print("="*60)
print(combined_script)
print("="*60)
# Handle pause tags across the entire combined script
if self.pause_processor.has_pause_tags(combined_script):
pause_segments, clean_text = self.pause_processor.parse_pause_tags(combined_script)
for seg_type, content in pause_segments:
if seg_type == 'text':
voice_ref = voice_mapping.get(character)
audio = self.generate_segment(content, voice_ref, params, character=character)
audio_segments.append(audio)
elif seg_type == 'pause':
# Create silence segment
silence = self.pause_processor.create_silence_segment(
content, 24000,
device=torch.device('cpu'),
dtype=torch.float32
)
audio_segments.append({
"waveform": silence.unsqueeze(0),
"sample_rate": 24000
})
else:
# Generate the entire character group at once
voice_ref = voice_mapping.get(character)
audio = self.generate_segment(combined_script, voice_ref, params, character=character)
audio_segments.append(audio)
return audio_segments
def _generate_native_multispeaker(self, segments: List[Tuple[str, str]],
voice_mapping: Dict[str, Any],
params: Dict,
global_char_to_speaker: Optional[Dict[str, int]] = None) -> Dict:
"""
Generate using VibeVoice's native multi-speaker mode.
Supports both [Character] tags and manual "Speaker N:" format.
Args:
segments: List of (character, text) tuples
voice_mapping: Dict mapping character names to voice references
params: Generation parameters
global_char_to_speaker: Global character-to-speaker mapping for consistent SRT processing
Returns:
Combined audio dict
"""
# Get speaker voice inputs from engine config for priority system
# For manual Speaker format, the main narrator voice comes from the TTS Text node
# We need to get it from a different source since voice_mapping may not have 'narrator' key
main_narrator_voice = voice_mapping.get('narrator') # Try narrator first
if main_narrator_voice is None:
# If no 'narrator' key, get it from any available voice (fallback for manual Speaker format)
available_voices = [v for v in voice_mapping.values() if v is not None]
main_narrator_voice = available_voices[0] if available_voices else None
# print(f"🐛 Debug: No 'narrator' key, using fallback voice: {'✅ found' if main_narrator_voice else '❌ none available'}")
speaker_inputs = {
1: main_narrator_voice, # Speaker 1 uses main narrator from TTS Text
2: params.get('speaker2_voice'),
3: params.get('speaker3_voice'),
4: params.get('speaker4_voice')
}
# print(f"🐛 Debug: speaker_inputs[1] (main narrator): {'✅ has voice' if speaker_inputs[1] else '❌ no voice'}")
# Build speaker mapping and format text
character_map = {}
speaker_voices = []
formatted_lines = []
print(f"🎭 Native multi-speaker: Processing {len(segments)} segments with characters: {[char for char, _ in segments]}")
print(f"🎤 Speaker inputs connected: {[f'Speaker {k}' for k, v in speaker_inputs.items() if v is not None]}")
for character, text in segments:
# Check if this is already a manual "Speaker N:" format
manual_speaker = self._detect_manual_speaker_format(character)
if manual_speaker is not None:
# Manual "Speaker N:" format - use speaker input directly
speaker_idx = manual_speaker - 1 # Convert to 0-based
if speaker_idx >= 4:
speaker_idx = 3
# Get the appropriate voice based on speaker number
voice = speaker_inputs.get(manual_speaker)
if manual_speaker == 1:
print(f"🎤 Manual format 'Speaker {manual_speaker}' -> using {'✅ main narrator (Tony)' if voice else '❌ no narrator, using default'}")
else:
print(f"🎤 Manual format 'Speaker {manual_speaker}' -> using {'✅ connected input' if voice else '❌ no input, using default'}")
# Ensure we have enough speaker_voices slots
while len(speaker_voices) <= speaker_idx:
speaker_voices.append(None)
speaker_voices[speaker_idx] = voice
formatted_lines.append(f"Speaker {manual_speaker}: {text.strip()}")
else:
# Character tag format - use global mapping if provided (for SRT consistency)
if character not in character_map:
# Special handling for numeric characters: [1] [2] [3] [4] -> map directly to Speaker N
if character.isdigit() and 1 <= int(character) <= 4:
speaker_idx = int(character) - 1 # Convert [1] to Speaker 1 (0-based index)
character_map[character] = speaker_idx
print(f"🔢 Numeric character '[{character}]' -> Speaker {int(character)} (direct mapping)")
elif global_char_to_speaker and character in global_char_to_speaker:
# Use global mapping for consistent SRT processing
speaker_idx = global_char_to_speaker[character] - 1 # Convert to 0-based
character_map[character] = speaker_idx
else:
# Fallback to sequential assignment
speaker_idx = len(character_map)
if speaker_idx >= 4:
print(f"⚠️ VibeVoice: Limiting to 4 speakers, '{character}' will use Speaker 4")
speaker_idx = 3 # Use 0-based internally, will convert to 1-based for format
else:
character_map[character] = speaker_idx
# Priority system: speaker inputs override character aliases
speaker_num = speaker_idx + 1
connected_voice = speaker_inputs.get(speaker_num)
character_voice = voice_mapping.get(character)
if connected_voice is not None and character_voice is not None:
print(f"⚠️ Priority: Speaker {speaker_num} input overrides ['{character}'] alias - using connected voice")
voice = connected_voice
elif connected_voice is not None:
print(f"🎤 Speaker {speaker_num}: Using connected voice input")
voice = connected_voice
else:
print(f"🎭 Character '{character}' -> Speaker {speaker_num}, using character voice")
voice = character_voice
# Ensure we have enough speaker_voices slots
while len(speaker_voices) <= speaker_idx:
speaker_voices.append(None)
speaker_voices[speaker_idx] = voice
speaker_idx = character_map.get(character, 3)
# Use 1-based Speaker format as per VibeVoice spec (Speaker 1:, Speaker 2:, etc.)
formatted_lines.append(f"Speaker {speaker_idx + 1}: {text.strip()}")
# Join with newlines for multi-speaker format
formatted_text = "\n".join(formatted_lines)
print(f"🎭 NATIVE MULTI-SPEAKER - Complete formatted text for VibeVoice:")
print("="*60)
print(formatted_text)
print("="*60)
print(f"🎤 Using {len(speaker_voices)} voice samples for generation")
# Validate and normalize voice references
normalized_voices = []
for i, voice in enumerate(speaker_voices):
if voice is None:
# print(f"🐛 Speaker {i+1} voice: None")
normalized_voices.append(None)
elif isinstance(voice, dict) and ('audio_path' in voice or 'waveform' in voice):
# Valid voice reference format
# if 'audio_path' in voice:
# print(f"🐛 Speaker {i+1} voice: {voice['audio_path']}")
# else:
# print(f"🐛 Speaker {i+1} voice: waveform tensor shape {voice['waveform'].shape}")
normalized_voices.append(voice)
elif isinstance(voice, str):
# Invalid format - just character name, try to convert to voice reference
print(f"❌ Speaker {i+1} received character name '{voice}' instead of voice reference")
print(f"💡 TIP: Connect Character Voices output (not character_name) to Speaker {i+1} input")
# Try to find matching voice file
try:
from utils.voice.discovery import get_character_mapping
char_mapping = get_character_mapping()
if voice in char_mapping:
voice_ref = char_mapping[voice]
print(f"🔄 Auto-converted '{voice}' to voice reference")
normalized_voices.append(voice_ref)
else:
print(f"⚠️ Unknown character '{voice}', using None")
normalized_voices.append(None)
except Exception as e:
print(f"⚠️ Could not convert character '{voice}': {e}")
normalized_voices.append(None)
else:
print(f"❌ Speaker {i+1} invalid voice format: {type(voice)} - {str(voice)[:100]}...")
normalized_voices.append(None)
# Use normalized voices
speaker_voices = normalized_voices
# Check if model and processor are loaded
if self.current_model is None or self.current_processor is None:
raise RuntimeError("VibeVoice model not loaded. Call load_base_model() first.")
# Prepare voice samples
voice_samples = self.vibevoice_engine._prepare_voice_samples(speaker_voices)
# Generate stable audio component for multi-speaker (like ChatterBox)
from utils.audio.audio_hash import generate_stable_audio_component
# For multi-speaker, use combined hash of all voices
combined_voice_hash = []
for voice in speaker_voices[:4]: # Max 4 speakers
if voice is not None and isinstance(voice, dict):
if 'waveform' in voice:
# Direct audio tensor format
combined_voice_hash.append(generate_stable_audio_component(voice, None))
elif 'audio_path' in voice:
# File path format
combined_voice_hash.append(generate_stable_audio_component(None, voice['audio_path']))
else:
combined_voice_hash.append("unknown_voice")
else:
combined_voice_hash.append("no_voice")
audio_component = f"multi_speaker_{'_'.join(combined_voice_hash)}"
# Generate with multi-speaker text and inference_steps
return self.vibevoice_engine.generate_speech(
text=formatted_text,
voice_samples=voice_samples,
cfg_scale=params.get('cfg_scale', 1.3),
seed=params.get('seed', 42),
use_sampling=params.get('use_sampling', False),
temperature=params.get('temperature', 0.95),
top_p=params.get('top_p', 0.95),
inference_steps=params.get('inference_steps', 20), # New parameter
max_new_tokens=params.get('max_new_tokens'),
enable_cache=params.get('enable_cache', True),
character="multi_speaker",
stable_audio_component=audio_component,
multi_speaker_mode=params.get('multi_speaker_mode', 'Native Multi-Speaker')
)
def handle_pause_tags(self, text: str) -> Tuple[str, Optional[List]]:
"""
Handle pause tags in text.
Args:
text: Text potentially containing pause tags
Returns:
Tuple of (processed_text, pause_segments)
"""
if self.pause_processor.has_pause_tags(text):
segments, clean_text = self.pause_processor.parse_pause_tags(text)
return clean_text, segments
return text, None
def generate_vibevoice_with_pause_tags(self, text: str, voice_ref: Optional[Dict], params: Dict,
enable_pause_tags: bool = True, character: str = "narrator"):
"""
Generate VibeVoice audio with pause tag support.
Args:
text: Input text potentially with pause tags
voice_ref: Voice reference dict
params: Generation parameters
enable_pause_tags: Whether to process pause tags
character: Character name for logging
Returns:
If pause tags present: List of segment dicts with audio, text, edit_tags
If no pause tags: Single audio tensor (backwards compatible)
"""
from utils.text.pause_processor import PauseTagProcessor
from utils.text.step_audio_editx_special_tags import get_edit_tags_for_segment
if not enable_pause_tags or not PauseTagProcessor.has_pause_tags(text):
# No pause tags, but still need to check for edit tags
clean_text, edit_tags = get_edit_tags_for_segment(text)
# If there are edit tags, return segment dict format for post-processing
if edit_tags:
result = self.generate_segment(clean_text, voice_ref, params, character)
waveform = result['waveform']
# Ensure proper tensor format
if waveform.dim() == 3:
waveform = waveform.squeeze(0)
if waveform.dim() == 1:
waveform = waveform.unsqueeze(0)
return [{
'waveform': waveform,
'sample_rate': 24000,
'character': character,
'text': clean_text,
'original_text': text,
'edit_tags': edit_tags
}]
else:
# No pause tags AND no edit tags - backwards compatible tensor return
result = self.generate_segment(text, voice_ref, params, character)
waveform = result['waveform']
# Ensure proper tensor format
if waveform.dim() == 3:
waveform = waveform.squeeze(0) # Remove batch dim
if waveform.dim() == 1:
waveform = waveform.unsqueeze(0) # Add channel dim
return waveform
print(f"🎵 VibeVoice: Processing pause tags in text")
# Process pause tags - returns list of ('text', content) or ('pause', duration)
pause_segments, _ = PauseTagProcessor.parse_pause_tags(text)
# Generate segment dicts for each pause-separated piece
segment_dicts = []
for seg_type, seg_content in pause_segments:
if seg_type == 'text':
# Extract edit tags from this text segment
clean_text, edit_tags = get_edit_tags_for_segment(seg_content)
# Generate TTS for this text piece
result = self.generate_segment(clean_text, voice_ref, params, character)
waveform = result['waveform']
# Ensure proper tensor format
if waveform.dim() == 3:
waveform = waveform.squeeze(0) # Remove batch dim
if waveform.dim() == 1:
waveform = waveform.unsqueeze(0) # Add channel dim
segment_dicts.append({
'waveform': waveform,
'sample_rate': 24000,
'character': character,
'text': clean_text,
'original_text': seg_content,
'edit_tags': edit_tags
})
else: # seg_type == 'pause'
# Create silence segment
duration_seconds = seg_content
silence = PauseTagProcessor.create_silence_segment(
duration_seconds, 24000, device=torch.device('cpu')
)
segment_dicts.append({
'waveform': silence,
'sample_rate': 24000,
'character': character,
'text': '',
'original_text': f'[pause:{duration_seconds}]',
'edit_tags': []
})
return segment_dicts
def _detect_manual_speaker_format(self, character: str) -> Optional[int]:
"""
Detect if character is already in manual "Speaker N:" format.
Args:
character: Character name to check
Returns:
Speaker number (1-4) if manual format, None otherwise
"""
import re
# Match "Speaker N" (case insensitive, with optional trailing colon/whitespace)
match = re.match(r'^speaker\s*(\d+)\s*:?\s*$', character.strip(), re.IGNORECASE)
if match:
speaker_num = int(match.group(1))
if 1 <= speaker_num <= 4:
return speaker_num
return None
def cleanup(self):
"""Clean up resources - models are now managed by ModelManager"""
self._character_speaker_map.clear()
self._speaker_voices.clear()
# Don't clean up vibevoice_engine - it just holds references to cached models
+16 -2
View File
@@ -58,7 +58,9 @@ except ImportError:
try:
from .language_models import (
get_chatterbox_models, get_model_config, get_model_files_for_language,
find_local_model_path, detect_model_format, get_available_languages
find_local_model_path, detect_model_format, get_available_languages,
is_model_incomplete, get_model_requirements, validate_model_completeness,
get_tokenizer_filename
)
LANGUAGE_MODELS_AVAILABLE = True
except ImportError:
@@ -76,6 +78,14 @@ except ImportError:
return ("pt", "ResembleAI/chatterbox")
def detect_model_format(model_path):
return "pt"
def is_model_incomplete(language):
return False
def get_model_requirements(language):
return []
def validate_model_completeness(model_path, language):
return True, []
def get_tokenizer_filename(language):
return "tokenizer.json"
# SRT subtitle support modules - import independently
try:
@@ -90,6 +100,8 @@ try:
'ChatterboxTTS', 'ChatterboxVC', 'ChatterBoxF5TTS',
'get_chatterbox_models', 'get_model_config', 'get_model_files_for_language',
'find_local_model_path', 'detect_model_format', 'get_available_languages',
'is_model_incomplete', 'get_model_requirements', 'validate_model_completeness',
'get_tokenizer_filename',
'SRTParser', 'SRTSubtitle', 'SRTParseError', 'validate_srt_timing_compatibility',
'AudioTimingUtils', 'PhaseVocoderTimeStretcher', 'TimedAudioAssembler',
'calculate_timing_adjustments', 'AudioTimingError'
@@ -100,5 +112,7 @@ except ImportError:
__all__ = [
'ChatterboxTTS', 'ChatterboxVC', 'ChatterBoxF5TTS',
'get_chatterbox_models', 'get_model_config', 'get_model_files_for_language',
'find_local_model_path', 'detect_model_format', 'get_available_languages'
'find_local_model_path', 'detect_model_format', 'get_available_languages',
'is_model_incomplete', 'get_model_requirements', 'validate_model_completeness',
'get_tokenizer_filename'
]
+53 -13
View File
@@ -476,10 +476,10 @@ class TimedAudioAssembler:
else:
raise AudioTimingError(f"Invalid stretcher_type: {stretcher_type}. Use 'ffmpeg' or 'phase_vocoder'")
def assemble_timed_audio(self, audio_segments: List[torch.Tensor],
def assemble_timed_audio(self, audio_segments: List[torch.Tensor],
target_timings: List[Tuple[float, float]],
total_duration: Optional[float] = None,
fade_duration: float = 0.01) -> torch.Tensor:
fade_duration: float = 0.01) -> Tuple[torch.Tensor, str]:
"""
Assemble audio segments with precise timing
@@ -581,8 +581,16 @@ class TimedAudioAssembler:
# Place segment in output buffer with crossfading for overlaps
self._place_segment_with_fade(output, audio_segment, start_sample, end_sample, fade_duration)
return output
return output, self.stretch_method_used or "none"
def get_stretch_method_used(self) -> str:
"""Get which stretching method was used in the last assembly"""
return self.stretch_method_used or "none"
def get_time_stretcher(self):
"""Get the time stretcher object used (for backward compatibility)"""
return self.time_stretcher
def _place_segment_with_fade(self, output: torch.Tensor, segment: torch.Tensor,
start_sample: int, end_sample: int, fade_duration: float):
"""
@@ -607,13 +615,28 @@ class TimedAudioAssembler:
fade_in = torch.linspace(0, 1, fade_samples, device=segment.device)
fade_out = torch.linspace(1, 0, fade_samples, device=segment.device)
# Fade in the beginning of new segment
if segment.dim() == 1:
segment[:fade_samples] *= fade_in
# Fade in the beginning of new segment and fade out existing content
if output.dim() == 1:
# 1D output (mono)
if segment.dim() == 1:
segment[:fade_samples] *= fade_in
else:
# Convert 2D segment to 1D for compatibility
segment = torch.mean(segment, dim=0)
segment[:fade_samples] *= fade_in
# Fade out existing content at the beginning
output[start_sample:start_sample + fade_samples] *= fade_out
else:
segment[:, :fade_samples] *= fade_in.unsqueeze(0)
# 2D output (stereo/multi-channel)
if segment.dim() == 1:
# Convert 1D segment to 2D to match output
segment = segment.unsqueeze(0).repeat(output.size(0), 1)
segment[:, :fade_samples] *= fade_in.unsqueeze(0)
else:
segment[:, :fade_samples] *= fade_in.unsqueeze(0)
# Fade out existing content at the beginning
output[:, start_sample:start_sample + fade_samples] *= fade_out.unsqueeze(0)
# Fade out the end of new segment if there's content after
@@ -622,16 +645,33 @@ class TimedAudioAssembler:
if output.dim() == 1:
if end_check_start < end_check_end and torch.any(output[end_check_start:end_check_end] != 0):
segment[-fade_samples:] *= fade_out
if segment.dim() == 1:
segment[-fade_samples:] *= fade_out
else:
segment[:, -fade_samples:] *= fade_out.unsqueeze(0)
else:
if end_check_start < end_check_end and torch.any(output[:, end_check_start:end_check_end] != 0):
segment[:, -fade_samples:] *= fade_out.unsqueeze(0)
if segment.dim() == 1:
# segment was already converted to 2D above, so this shouldn't happen
pass
else:
segment[:, -fade_samples:] *= fade_out.unsqueeze(0)
# Add segment to output
# Add segment to output (segment dimensions should now match output)
if output.dim() == 1:
output[start_sample:end_sample] += segment
if segment.dim() == 1:
output[start_sample:end_sample] += segment
else:
# This shouldn't happen after normalization above, but handle it
segment = torch.mean(segment, dim=0)
output[start_sample:end_sample] += segment
else:
output[:, start_sample:end_sample] += segment
if segment.dim() == 2:
output[:, start_sample:end_sample] += segment
else:
# This shouldn't happen after normalization above, but handle it
segment = segment.unsqueeze(0).repeat(output.size(0), 1)
output[:, start_sample:end_sample] += segment
def calculate_timing_adjustments(natural_durations: List[float],
+2 -3
View File
@@ -38,9 +38,8 @@ class GPUAwareBatchProcessor:
def _resolve_device(self, device: str) -> str:
"""Resolve device string to actual device."""
if device == "auto":
return "cuda" if torch.cuda.is_available() else "cpu"
return device
from utils.device import resolve_torch_device
return resolve_torch_device(device)
def _determine_optimal_workers(self) -> int:
"""
+170 -1
View File
@@ -19,11 +19,67 @@ CHATTERBOX_MODELS = {
"format": "safetensors",
"description": "German ChatterBox model with high quality"
},
"German (SebastianBodza)": {
"repo": "SebastianBodza/Kartoffelbox-v0.1",
"format": "mixed",
"description": "German model with emotion control features (<haha>, <wow> tags) - 600K samples"
},
"German (havok2)": {
"repo": "havok2/Kartoffelbox-v0.1_0.65h2",
"format": "safetensors",
"subdirectory": "finetuned_by_h2",
"description": "Hybrid German model (65% multi-speaker + 35% Kartoffelbox) - User-rated best quality"
},
"Norwegian": {
"repo": "akhbar/chatterbox-tts-norwegian",
"format": "safetensors",
"description": "Norwegian ChatterBox model (Bokmål and Nynorsk dialects) - 532M parameters"
},
"French": {
"repo": "Thomcles/Chatterbox-TTS-French",
"format": "safetensors",
"description": "French model trained on 1,400 hours of Emilia dataset - t3_cfg only, uses shared English components",
"incomplete": True
},
"Russian": {
"repo": "niobures/Chatterbox-TTS",
"format": "safetensors",
"subdirectory": "ru",
"description": "Russian model with training artifacts - Complete fine-tuned model"
},
"Armenian": {
"repo": "niobures/Chatterbox-TTS",
"format": "safetensors",
"subdirectory": "hy",
"description": "Armenian model with training artifacts - Complete fine-tuned model"
},
"Georgian": {
"repo": "niobures/Chatterbox-TTS",
"format": "safetensors",
"subdirectory": "ka",
"description": "Georgian model - Complete fine-tuned model with unique architecture"
},
"Japanese": {
"repo": "niobures/Chatterbox-TTS",
"format": "safetensors",
"subdirectory": "ja",
"description": "Japanese model - t3_cfg only, uses shared English components",
"incomplete": True
},
"Korean": {
"repo": "niobures/Chatterbox-TTS",
"format": "safetensors",
"subdirectory": "ko",
"description": "Korean model - t3_cfg only, uses shared English components",
"incomplete": True
},
"Italian": {
"repo": "niobures/Chatterbox-TTS",
"format": "pt",
"subdirectory": "it,en",
"description": "Bilingual Italian/English model - complete model in single file, extended vocabulary (1500 tokens)",
"special": "unified_model"
},
}
def get_chatterbox_models() -> List[str]:
@@ -80,7 +136,14 @@ def get_model_config(language: str) -> Optional[Dict]:
"description": f"Local ChatterBox model: {local_name}"
}
return CHATTERBOX_MODELS.get(language)
config = CHATTERBOX_MODELS.get(language)
if config and config.get("subdirectory"):
# Create a copy with the subdirectory information for download handling
config_copy = config.copy()
config_copy["download_path"] = f"{config['repo']}/{config['subdirectory']}"
return config_copy
return config
def get_model_files_for_language(language: str) -> Tuple[str, str]:
"""
@@ -143,6 +206,112 @@ def detect_model_format(model_path: str) -> str:
else:
return "pt"
def is_model_incomplete(language: str) -> bool:
"""Check if a model is marked as incomplete (missing components)"""
config = CHATTERBOX_MODELS.get(language)
return config.get("incomplete", False) if config else False
def is_unified_model(language: str) -> bool:
"""Check if a model uses unified/special architecture (like Italian)"""
config = CHATTERBOX_MODELS.get(language)
return config.get("special") == "unified_model" if config else False
def get_tokenizer_filename(language: str) -> str:
"""Get the correct tokenizer filename for a language model"""
if language == "Japanese":
return "tokenizer_jp.json"
elif language == "Korean":
return "tokenizer_en_ko.json"
else:
return "tokenizer.json"
def get_model_requirements(language: str) -> List[str]:
"""Get list of required files for a ChatterBox model"""
# Handle special Italian unified model
if language == "Italian":
return ["chatterbox_italian_final.pt", "config.json"]
# Handle special cases for incomplete models
if language == "French":
# French model only has t3_cfg.safetensors, no tokenizer in repo
base_requirements = ["t3_cfg.safetensors"]
else:
# Other models have tokenizers
tokenizer_file = get_tokenizer_filename(language)
base_requirements = ["t3_cfg.safetensors", tokenizer_file]
# Complete models need all components
if not is_model_incomplete(language):
base_requirements.extend(["s3gen.safetensors", "ve.safetensors", "conds.pt"])
return base_requirements
def validate_model_completeness(model_path: str, language: str) -> Tuple[bool, List[str]]:
"""
Validate if a model has all required components.
Returns (is_complete, missing_files)
"""
if not os.path.exists(model_path):
return False, ["Model directory not found"]
required_files = get_model_requirements(language)
missing_files = []
existing_files = os.listdir(model_path)
for required_file in required_files:
# Check for file with correct extension
file_found = False
for existing_file in existing_files:
if existing_file == required_file or existing_file.startswith(required_file.split('.')[0] + '.'):
file_found = True
break
if not file_found:
missing_files.append(required_file)
return len(missing_files) == 0, missing_files
def supports_voice_conversion(language: str) -> bool:
"""
Check if a language model supports voice conversion (has s3gen component).
Args:
language: Language model name
Returns:
True if VC is supported, False otherwise
"""
# Check if model is marked as incomplete
if is_model_incomplete(language):
return False
# Check if model requirements include s3gen (required for VC)
requirements = get_model_requirements(language)
has_s3gen = any(req.startswith("s3gen") for req in requirements)
# For local models, check if s3gen file actually exists
if language.startswith("local:"):
local_path = find_local_model_path(language)
if local_path and os.path.exists(local_path):
# Check if s3gen file exists in the directory
for ext in [".safetensors", ".pt"]:
if os.path.exists(os.path.join(local_path, f"s3gen{ext}")):
return True
return False
return has_s3gen
def get_vc_supported_languages() -> List[str]:
"""Get list of languages that support voice conversion"""
all_languages = get_chatterbox_models()
return [lang for lang in all_languages if supports_voice_conversion(lang)]
def get_vc_unsupported_languages() -> List[str]:
"""Get list of languages that do NOT support voice conversion"""
all_languages = get_chatterbox_models()
return [lang for lang in all_languages if not supports_voice_conversion(lang)]
def get_available_languages() -> List[str]:
"""Get list of available language names for display"""
models = get_chatterbox_models()
@@ -1,4 +1,16 @@
from typing import List, Tuple
import os
import sys
# 🔬 NUMBA WORKAROUND: Commented out - testing if still needed with numba 0.61.2+ and librosa 0.11.0+
# Python 3.13 compatibility: Disable numba JIT for librosa compatibility
# if sys.version_info >= (3, 13):
# os.environ['NUMBA_DISABLE_JIT'] = '1'
# try:
# import numba
# numba.config.DISABLE_JIT = True
# except ImportError:
# pass
import numpy as np
import librosa
@@ -119,10 +119,10 @@ class T3HuggingfaceBackend(LlamaPreTrainedModel, GenerationMixin):
past_key_values=pkv,
use_cache=use_cache,
output_attentions=False, # Always disable attention outputs
output_hidden_states=True,
return_dict=True,
)
hidden_states = tfmr_out.hidden_states[-1] # (B, seq, dim)
# Use last_hidden_state directly instead of hidden_states (transformers compatibility)
hidden_states = tfmr_out.last_hidden_state # (B, seq, dim)
logits = self.speech_head(hidden_states)
# assert inputs_embeds.size(0) == 1 # (disabled for CFG)
@@ -8,7 +8,7 @@ LLAMA_520M_CONFIG_DICT = dict(
intermediate_size=4096,
num_hidden_layers=30,
num_attention_heads=16,
attn_implementation="sdpa",
attn_implementation="eager", # Changed from sdpa to eager for compatibility with alignment analyzer
head_dim=64,
tie_word_embeddings=False,
hidden_act="silu",
+5 -3
View File
@@ -93,8 +93,11 @@ class T3(nn.Module):
Token cond data needs to be embedded, so that needs to be here instead of in `T3CondEnc`.
"""
if t3_cond.cond_prompt_speech_tokens is not None and t3_cond.cond_prompt_speech_emb is None:
t3_cond.cond_prompt_speech_emb = self.speech_emb(t3_cond.cond_prompt_speech_tokens) + \
self.speech_pos_emb(t3_cond.cond_prompt_speech_tokens)
# CRITICAL: Ensure tokens are on the correct device before embedding
# (fixes device mismatch when models are reused after CPU offloading)
cond_tokens = t3_cond.cond_prompt_speech_tokens.to(device=self.device)
t3_cond.cond_prompt_speech_emb = self.speech_emb(cond_tokens) + \
self.speech_pos_emb(cond_tokens)
return self.cond_enc(t3_cond) # (B, len_cond, dim)
def prepare_input_embeds(
@@ -391,7 +394,6 @@ class T3(nn.Module):
past_key_values=cache,
use_cache=True,
output_attentions=False,
output_hidden_states=True,
return_dict=True
)
@@ -2,13 +2,14 @@ from functools import lru_cache
from scipy import signal
import numpy as np
import librosa
# Use librosa fallback for Python 3.13 compatibility
from utils.audio.librosa_fallback import safe_mel_filters, safe_stft
@lru_cache()
def mel_basis(hp):
assert hp.fmax <= hp.sample_rate // 2
return librosa.filters.mel(
return safe_mel_filters(
sr=hp.sample_rate,
n_fft=hp.n_fft,
n_mels=hp.num_mels,
@@ -54,7 +55,7 @@ def melspectrogram(wav, hp, pad=True):
def _stft(y, hp, pad=True):
# NOTE: after 0.8, pad mode defaults to constant, setting this to reflect for
# historical consistency and streaming-version consistency
return librosa.stft(
return safe_stft(
y,
n_fft=hp.n_fft,
hop_length=hp.hop_size,
@@ -4,13 +4,14 @@ from typing import List, Union, Optional
import numpy as np
from numpy.lib.stride_tricks import as_strided
import librosa
import torch
import torch.nn.functional as F
from torch import nn, Tensor
from .config import VoiceEncConfig
from .melspec import melspectrogram
# Use librosa fallback for Python 3.13 compatibility
from utils.audio.librosa_fallback import safe_resample, safe_trim
def pack(arrays, seq_len: int=None, pad_value=0):
@@ -258,13 +259,15 @@ class VoiceEncoder(nn.Module):
:param trim_top_db: this argument was only added for the sake of compatibility with metavoice's implementation
"""
if sample_rate != self.hp.sample_rate:
# Use fallback for resampling for Python 3.13 compatibility
wavs = [
librosa.resample(wav, orig_sr=sample_rate, target_sr=self.hp.sample_rate, res_type="kaiser_fast")
safe_resample(wav, sample_rate, self.hp.sample_rate)
for wav in wavs
]
if trim_top_db:
wavs = [librosa.effects.trim(wav, top_db=trim_top_db)[0] for wav in wavs]
# Use fallback trimming for Python 3.13 compatibility
wavs = [safe_trim(wav, top_db=trim_top_db) for wav in wavs]
if "rate" not in kwargs:
kwargs["rate"] = 1.3 # Resemble's default value.
+7 -10
View File
@@ -13,23 +13,20 @@ Key benefits:
import torch
import torch.nn.functional as F
import librosa
import numpy as np
from dataclasses import dataclass
from pathlib import Path
import warnings
# Use librosa fallback for Python 3.13 compatibility
from utils.audio.librosa_fallback import safe_load, safe_resample
# Import ChatterBox components
from engines.chatterbox.models.s3tokenizer import S3_SR, drop_invalid_tokens
from engines.chatterbox.models.s3gen import S3GEN_SR
from engines.chatterbox.models.t3.modules.cond_enc import T3Cond
from engines.chatterbox.tts import Conditionals, punc_norm
# Suppress perth warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
import perth
class StatelessChatterBoxWrapper:
"""
@@ -164,11 +161,11 @@ class StatelessChatterBoxWrapper:
if not wav_fpath:
return self._get_default_conditions(exaggeration)
# Load reference wav
s3gen_ref_wav, _sr = librosa.load(wav_fpath, sr=S3GEN_SR)
# Load reference wav using fallback for Python 3.13 compatibility
s3gen_ref_wav, sample_rate = safe_load(wav_fpath, sr=S3GEN_SR, mono=True)
# Resample to 16k for S3 tokenizer
ref_16k_wav = librosa.resample(s3gen_ref_wav, orig_sr=S3GEN_SR, target_sr=S3_SR)
# Resample to 16k for S3 tokenizer using fallback
ref_16k_wav = safe_resample(s3gen_ref_wav, S3GEN_SR, S3_SR)
# Prepare S3Gen reference
s3gen_ref_wav = s3gen_ref_wav[:self.DEC_COND_LEN]
+28 -22
View File
@@ -30,42 +30,38 @@ class StreamingModelManager:
return models
def preload_models(self, language_codes: List[str], model_manager, device: str) -> None:
"""Pre-load all required models for the given languages using universal smart loader."""
"""Pre-load all required models for the given languages using unified interface."""
required_models = self.get_required_models(language_codes)
available_languages = get_available_languages()
print(f"🚀 STREAMING: Pre-loading {len(required_models)} models for {len(language_codes)} languages")
# Use universal smart model loader for consistency
from utils.models.smart_loader import smart_model_loader
# Use unified interface for consistency
from utils.models.unified_model_interface import load_tts_model
for model_name in required_models:
if model_name in self.preloaded_models:
print(f"♻️ {model_name} already loaded in streaming cache")
continue
if model_name not in available_languages:
print(f"⚠️ {model_name} model not available, using English fallback")
model_name = 'English'
# Use smart loader to get or load the model
# Use unified interface to load the model
try:
model_instance, was_cached = smart_model_loader.load_model_if_needed(
engine_type="chatterbox",
model_instance = load_tts_model(
engine_name="chatterbox",
model_name=model_name,
current_model=self.preloaded_models.get(model_name),
device=device,
load_callback=lambda d, m: model_manager.load_tts_model(d, m)
language=model_name,
force_reload=False
)
# Store reference in our streaming cache
self.preloaded_models[model_name] = model_instance
if was_cached:
print(f"♻️ STREAMING: Reused {model_name} from smart loader (ID: {id(model_instance)})")
else:
print(f"✅ STREAMING: Loaded {model_name} via smart loader (ID: {id(model_instance)})")
print(f"✅ STREAMING: Loaded {model_name} (ID: {id(model_instance)})")
except Exception as e:
print(f"❌ Failed to load {model_name}: {e}")
# Try fallback to English if not already English
@@ -74,11 +70,11 @@ class StreamingModelManager:
self.preloaded_models[model_name] = self.preloaded_models['English']
else:
print(f"❌ No fallback available for {model_name}")
# Debug: Show all current model IDs
if len(self.preloaded_models) > 1:
print(f"🔍 DEBUG: All stored model IDs: {[(k, id(v)) for k, v in self.preloaded_models.items()]}")
print(f"🚀 Model pre-loading complete! {len(self.preloaded_models)} models ready")
def get_model_for_language(self, language_code: str, fallback_model=None):
@@ -116,6 +112,16 @@ class StreamingModelManager:
return stateless_wrapper
return None
def load_vc_model(self, device: str = "auto", force_reload: bool = False, language: str = "English"):
"""
Voice Conversion not supported in StreamingModelManager.
This method exists to prevent signature errors when VC nodes try to call it.
"""
raise NotImplementedError(
"Voice Conversion is not supported with StreamingModelManager. "
"StreamingModelManager is for TTS only. VC requires the main ModelManager from utils.models.manager."
)
def cleanup(self):
"""Clean up pre-loaded models to free memory."""
print(f"🧹 Cleaning up {len(self.preloaded_models)} pre-loaded models")
+342 -140
View File
@@ -2,11 +2,13 @@ from dataclasses import dataclass
from pathlib import Path
import os
import librosa
import torch
import torch.nn.functional as F
from huggingface_hub import hf_hub_download
import warnings
# Use librosa fallback for Python 3.13 compatibility
from utils.audio.librosa_fallback import safe_load, safe_resample
# Import safetensors for multilanguage model support
from safetensors.torch import load_file
@@ -16,10 +18,15 @@ try:
except ImportError:
folder_paths = None
# Import perth with warnings disabled
with warnings.catch_warnings():
warnings.simplefilter("ignore")
import perth
# Import perth with warnings disabled and graceful fallback
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
import perth
PERTH_AVAILABLE = True
except ImportError:
perth = None
PERTH_AVAILABLE = False
from .models.t3 import T3
from .models.s3tokenizer import S3_SR, drop_invalid_tokens
@@ -30,12 +37,22 @@ from .models.t3.modules.cond_enc import T3Cond
# Import language model registry
try:
from .language_models import get_model_config, CHATTERBOX_MODELS
from .language_models import get_model_config, CHATTERBOX_MODELS, get_model_requirements, validate_model_completeness, get_tokenizer_filename, is_model_incomplete, is_unified_model
except ImportError:
# Fallback if language_models not available
CHATTERBOX_MODELS = {"English": {"repo": "ResembleAI/chatterbox", "format": "pt"}}
def get_model_config(language):
return CHATTERBOX_MODELS.get(language, CHATTERBOX_MODELS["English"])
def get_model_requirements(language):
return ["t3_cfg.safetensors", "s3gen.safetensors", "ve.safetensors", "conds.pt", "tokenizer.json"]
def validate_model_completeness(model_path, language):
return True, []
def get_tokenizer_filename(language):
return "tokenizer.json"
def is_model_incomplete(language):
return False
def is_unified_model(language):
return False
# Import modular processors
from .overlapping_processor import OverlappingBatchProcessor, BatchingStrategy
@@ -107,6 +124,9 @@ class Conditionals:
def to(self, device):
self.t3 = self.t3.to(device=device)
# CRITICAL: Also move s3gen model to device (fixes speaker_encoder device mismatch)
if hasattr(self, 's3gen') and self.s3gen is not None:
self.s3gen = self.s3gen.to(device=device)
for k, v in self.gen.items():
if torch.is_tensor(v):
self.gen[k] = v.to(device=device)
@@ -145,49 +165,156 @@ class ChatterboxTTS:
self.tokenizer = tokenizer
self.device = device
self.conds = conds
# Initialize watermarker silently (but disabled by default)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
self.watermarker = perth.PerthImplicitWatermarker()
self.enable_watermarking = False # Disabled by default for maximum compatibility
# Watermarking disabled by default for maximum compatibility
# Set to True to enable watermarking (requires perth library)
self.enable_watermarking = False
self.watermarker = None
self._watermarker_init_attempted = False
# Initialize modular processors
self.overlapping_processor = OverlappingBatchProcessor(self)
def to(self, device):
"""
Move all model components to the specified device.
Critical for ComfyUI model management - ensures all components move together
when models are detached to CPU and later reloaded to CUDA.
"""
self.device = device
# Move all PyTorch model components
if hasattr(self.t3, 'to'):
self.t3 = self.t3.to(device)
if hasattr(self.s3gen, 'to'):
self.s3gen = self.s3gen.to(device)
if hasattr(self.ve, 'to'):
self.ve = self.ve.to(device)
if hasattr(self.tokenizer, 'to'):
self.tokenizer = self.tokenizer.to(device)
if self.conds is not None and hasattr(self.conds, 'to'):
self.conds = self.conds.to(device)
return self
def _init_watermarker_if_needed(self):
"""Initialize watermarker on first use if enabled"""
if self.enable_watermarking and not self._watermarker_init_attempted:
self._watermarker_init_attempted = True
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
self.watermarker = perth.PerthImplicitWatermarker()
if self.watermarker is None:
raise ValueError("PerthImplicitWatermarker returned None")
except Exception as e:
print(f"❌ Failed to initialize watermarker: {e}")
self.watermarker = None
self.enable_watermarking = False
@classmethod
def from_local(cls, ckpt_dir, device) -> 'ChatterboxTTS':
print(f"📦 Loading local ChatterBox models from: {ckpt_dir}")
def from_local(cls, ckpt_dir, device, language=None) -> 'ChatterboxTTS':
ckpt_dir = Path(ckpt_dir)
# Resolve "auto" device to actual device
from utils.device import resolve_torch_device
device = resolve_torch_device(device)
# Handle Italian unified model (special architecture)
if language and is_unified_model(language):
return cls._load_unified_model(ckpt_dir, device, language)
# Auto-detect model format
def load_model_file(base_name: str):
# Determine if this is an incomplete model by checking language or directory structure
is_incomplete_model = False
if language:
is_incomplete_model = is_model_incomplete(language)
else:
# Check if critical files are missing (indicates incomplete model)
ve_exists = any((ckpt_dir / f"ve.{ext}").exists() for ext in ["safetensors", "pt"])
s3gen_exists = any((ckpt_dir / f"s3gen.{ext}").exists() for ext in ["safetensors", "pt"])
is_incomplete_model = not (ve_exists and s3gen_exists)
# Auto-detect model format with fallback support for incomplete models
def load_model_file(base_name: str, required: bool = True):
"""Load model file with auto-detection of format (.safetensors preferred over .pt)"""
safetensors_path = ckpt_dir / f"{base_name}.safetensors"
pt_path = ckpt_dir / f"{base_name}.pt"
if safetensors_path.exists():
print(f"📁 Loading {base_name} from safetensors format")
return load_file(safetensors_path, device=device)
elif pt_path.exists():
print(f"📁 Loading {base_name} from pt format")
return torch.load(pt_path, map_location=device)
elif not required:
return None
else:
raise FileNotFoundError(f"Neither {base_name}.safetensors nor {base_name}.pt found in {ckpt_dir}")
def load_from_english_fallback(base_name: str):
"""Load component from English model when missing in incomplete model"""
from utils.downloads.unified_downloader import UnifiedDownloader
from .language_models import find_local_model_path
# Try to find local English model first
english_path = find_local_model_path("English")
if english_path:
english_dir = Path(english_path)
safetensors_path = english_dir / f"{base_name}.safetensors"
pt_path = english_dir / f"{base_name}.pt"
if safetensors_path.exists():
print(f"📁 Loading {base_name} from local English model: {safetensors_path}")
return load_file(safetensors_path, device=device)
elif pt_path.exists():
print(f"📁 Loading {base_name} from local English model: {pt_path}")
return torch.load(pt_path, map_location=device)
# Download English model if not available locally
print(f"📦 Downloading English model components for incomplete language model...")
downloader = UnifiedDownloader()
english_dir = downloader.download_chatterbox_model("ResembleAI/chatterbox", "English")
if english_dir:
english_path = Path(english_dir)
safetensors_path = english_path / f"{base_name}.safetensors"
pt_path = english_path / f"{base_name}.pt"
if safetensors_path.exists():
print(f"📁 Loading {base_name} from downloaded English model: {safetensors_path}")
return load_file(safetensors_path, device=device)
elif pt_path.exists():
print(f"📁 Loading {base_name} from downloaded English model: {pt_path}")
return torch.load(pt_path, map_location=device)
raise FileNotFoundError(f"Could not load {base_name} from English fallback")
with warnings.catch_warnings():
warnings.simplefilter("ignore")
# Load VoiceEncoder
# Load VoiceEncoder (use English fallback for incomplete models)
ve = VoiceEncoder()
ve_state = load_model_file("ve")
ve_state = load_model_file("ve", required=not is_incomplete_model)
if ve_state is None and is_incomplete_model:
print(f"📎 Loading VoiceEncoder from English model (incomplete model fallback)")
ve_state = load_from_english_fallback("ve")
ve.load_state_dict(ve_state)
ve.to(device).eval()
# Load T3 config
# Load T3 config (always required)
t3_state = load_model_file("t3_cfg")
if "model" in t3_state.keys():
t3_state = t3_state["model"][0]
# Handle Japanese/Korean model state dict format (keys have "t3." prefix)
# Check if keys have "t3." prefix and need to be remapped
sample_keys = list(t3_state.keys())[:5] # Check first few keys
if sample_keys and all(key.startswith("t3.") for key in sample_keys):
print(f"🔧 Remapping state dict keys (removing 't3.' prefix) for incomplete model compatibility")
# Remove "t3." prefix from all keys
new_state_dict = {}
for key, value in t3_state.items():
new_key = key[3:] if key.startswith("t3.") else key # Remove "t3." prefix
new_state_dict[new_key] = value
t3_state = new_state_dict
# Create config with proper settings
from .models.t3.t3 import T3Config
config = T3Config()
@@ -201,23 +328,157 @@ class ChatterboxTTS:
t3.to(device).eval()
# Load S3Gen
# Load S3Gen (use English fallback for incomplete models)
s3gen = S3Gen()
s3gen_state = load_model_file("s3gen")
s3gen_state = load_model_file("s3gen", required=not is_incomplete_model)
if s3gen_state is None and is_incomplete_model:
print(f"📎 Loading S3Gen from English model (incomplete model fallback)")
s3gen_state = load_from_english_fallback("s3gen")
# Apply JaneDoe84's critical fix: strict=False to handle missing keys
s3gen.load_state_dict(s3gen_state, strict=False)
s3gen.to(device).eval()
tokenizer = EnTokenizer(
str(ckpt_dir / "tokenizer.json")
)
# Find the correct tokenizer file (prioritize language-specific tokenizer)
tokenizer_file = None
# First try the language-specific tokenizer based on language parameter
if language:
expected_tokenizer = get_tokenizer_filename(language)
expected_path = ckpt_dir / expected_tokenizer
if expected_path.exists():
tokenizer_file = str(expected_path)
print(f"🔤 Using language-specific tokenizer: {expected_tokenizer}")
# If language-specific not found, try available tokenizers
if not tokenizer_file:
for possible_tokenizer in ["tokenizer_jp.json", "tokenizer_en_ko.json", "tokenizer.json"]:
tokenizer_path = ckpt_dir / possible_tokenizer
if tokenizer_path.exists():
tokenizer_file = str(tokenizer_path)
print(f"🔤 Using available tokenizer: {possible_tokenizer}")
break
# For incomplete models, fall back to English tokenizer
if not tokenizer_file and is_incomplete_model:
print(f"📎 Loading tokenizer from English model (incomplete model fallback)")
# Try to find English model locally first
from .language_models import find_local_model_path
english_path = find_local_model_path("English")
if english_path:
english_tokenizer = Path(english_path) / "tokenizer.json"
if english_tokenizer.exists():
tokenizer_file = str(english_tokenizer)
print(f"🔤 Using English tokenizer: {english_tokenizer}")
else:
# Download English model if not available locally
from utils.downloads.unified_downloader import UnifiedDownloader
downloader = UnifiedDownloader()
english_dir = downloader.download_chatterbox_model("ResembleAI/chatterbox", "English")
if english_dir:
english_path = Path(english_dir)
english_tokenizer = english_path / "tokenizer.json"
if english_tokenizer.exists():
tokenizer_file = str(english_tokenizer)
print(f"🔤 Using English tokenizer: {english_tokenizer}")
if not tokenizer_file:
raise FileNotFoundError(f"No tokenizer file found in {ckpt_dir}")
tokenizer = EnTokenizer(tokenizer_file)
conds = None
if (builtin_voice := ckpt_dir / "conds.pt").exists():
conds = Conditionals.load(builtin_voice).to(device)
instance = cls(t3, s3gen, ve, tokenizer, device, conds=conds)
print("✅ Successfully loaded all local ChatterBox models")
print(f"📦 ChatterBox models loaded from: {ckpt_dir}")
return instance
@classmethod
def _load_unified_model(cls, ckpt_dir, device, language) -> 'ChatterboxTTS':
"""
Load unified model (like Italian) that contains all components in a single checkpoint file.
Based on the deployment script from niobures/Chatterbox-TTS/it,en/deploy_italian_tts.py
"""
ckpt_dir = Path(ckpt_dir)
print(f"🇮🇹 Loading unified {language} ChatterBox model from: {ckpt_dir}")
# Load the unified model file
unified_model_path = ckpt_dir / "chatterbox_italian_final.pt"
if not unified_model_path.exists():
raise FileNotFoundError(f"Unified model file not found: {unified_model_path}")
with warnings.catch_warnings():
warnings.simplefilter("ignore")
print(f"📦 Loading checkpoint: {unified_model_path.name}")
checkpoint = torch.load(unified_model_path, map_location=device)
# Extract model configuration
model_config = checkpoint.get('model_config', {})
vocab_size = model_config.get('vocab_size', 1500)
frozen_embeddings = model_config.get('frozen_embeddings', 704)
trainable_tokens = model_config.get('trainable_tokens', 796)
print(f"🎯 Vocab size: {vocab_size}")
print(f"🔒 Frozen tokens: {frozen_embeddings}")
print(f"🔥 Trainable tokens: {trainable_tokens}")
# Load base English ChatterBox components first
print(f"📦 Loading base English model components...")
# Initialize components with standard ChatterBox architecture
ve = VoiceEncoder()
ve.load_state_dict(checkpoint['ve_state_dict'], strict=False)
ve.to(device).eval()
# Load T3 config with Italian vocabulary size
from .models.t3.t3 import T3Config
config = T3Config()
# Override text token dictionary size for Italian extended vocab
config.text_tokens_dict_size = vocab_size # Use the 1500 tokens from checkpoint
t3 = T3(config)
t3.load_state_dict(checkpoint['t3_state_dict'], strict=False)
t3.tfmr.output_attentions = False
t3.to(device).eval()
# Load S3Gen
s3gen = S3Gen()
s3gen.load_state_dict(checkpoint['s3gen_state_dict'], strict=False)
s3gen.to(device).eval()
# Load tokenizer for Italian model (fallback to English tokenizer)
# The Italian model uses language prefixes like [it] for Italian text
from .models.tokenizers import EnTokenizer
from .language_models import find_local_model_path
# Try to use existing tokenizer if available in Italian model
tokenizer_path = ckpt_dir / "tokenizer.json"
if tokenizer_path.exists():
tokenizer = EnTokenizer(str(tokenizer_path))
print(f"🔤 Using Italian model tokenizer: tokenizer.json")
else:
# Fallback: use English model tokenizer (Italian extends English vocab)
english_model_path = find_local_model_path("English")
if english_model_path:
english_tokenizer_path = Path(english_model_path) / "tokenizer.json"
if english_tokenizer_path.exists():
tokenizer = EnTokenizer(str(english_tokenizer_path))
print(f"🔤 Using English tokenizer fallback: {english_tokenizer_path}")
else:
raise FileNotFoundError(f"No tokenizer found for Italian model and no English fallback available")
else:
raise FileNotFoundError(f"No tokenizer available for Italian model and no English model found for fallback")
# Check for conditional voices
conds = None
if (builtin_voice := ckpt_dir / "conds.pt").exists():
conds = Conditionals.load(builtin_voice).to(device)
instance = cls(t3, s3gen, ve, tokenizer, device, conds=conds)
print(f"✅ Italian TTS model loaded successfully!")
print(f"💡 Use '[it]' prefix for Italian text, no prefix for English")
print(f"📦 Model loaded from: {ckpt_dir}")
return instance
@classmethod
@@ -252,7 +513,7 @@ class ChatterboxTTS:
try:
return try_local_first(
search_paths=search_paths,
local_loader=lambda path: cls.from_local(path, device),
local_loader=lambda path: cls.from_local(path, device, language),
fallback_loader=lambda: cls.from_pretrained(device, language="English"),
fallback_name="English",
original_request=language
@@ -265,119 +526,58 @@ class ChatterboxTTS:
repo_id = model_config.get("repo", REPO_ID)
model_format = model_config.get("format", "pt")
print(f"📦 Loading ChatterBox model for {language} from {repo_id}")
# Silent loading unless errors occur
# Define file extensions based on format
if model_format == "safetensors":
file_extensions = ["ve.safetensors", "t3_cfg.safetensors", "s3gen.safetensors", "tokenizer.json", "conds.pt"]
elif model_format == "pt":
file_extensions = ["ve.pt", "t3_cfg.pt", "s3gen.pt", "tokenizer.json", "conds.pt"]
else:
# Auto format - try both, safetensors preferred
print("🔍 Auto-detecting model format...")
file_extensions = ["ve.safetensors", "t3_cfg.safetensors", "s3gen.safetensors", "tokenizer.json", "conds.pt"]
# Download files to local models directory
local_paths = []
for fpath in file_extensions:
# Define local path in TTS organization
local_model_path = os.path.join(folder_paths.models_dir, "TTS", "chatterbox", language, os.path.basename(fpath))
if os.path.exists(local_model_path):
print(f"📁 Using local Chatterbox model: {local_model_path}")
local_paths.append(local_model_path)
continue
# Check HuggingFace cache first
try:
hf_cached_file = hf_hub_download(repo_id=repo_id, filename=fpath, local_files_only=True)
print(f"📁 Using cached Chatterbox model: {hf_cached_file}")
local_paths.append(hf_cached_file)
continue
except Exception:
pass
# Download to local directory
print(f"📥 Downloading Chatterbox model to local directory: {fpath}")
os.makedirs(os.path.dirname(local_model_path), exist_ok=True)
try:
# Use unified downloader
from utils.downloads.unified_downloader import unified_downloader
files_to_download = [{
'remote': fpath,
'local': os.path.basename(fpath)
}]
downloaded_dir = unified_downloader.download_huggingface_model(
repo_id=repo_id,
model_name=language,
files=files_to_download,
engine_type="chatterbox"
)
if downloaded_dir:
local_model_path = os.path.join(downloaded_dir, os.path.basename(fpath))
local_paths.append(local_model_path)
print(f"✅ Downloaded Chatterbox model to: {local_model_path}")
else:
raise Exception("Unified download failed")
except Exception as e:
# If safetensors fails, try .pt format
if fpath.endswith('.safetensors'):
fallback_fpath = fpath.replace('.safetensors', '.pt')
fallback_local_path = os.path.join(folder_paths.models_dir, "TTS", "chatterbox", language, os.path.basename(fallback_fpath))
print(f"⚠️ {fpath} not found, trying {fallback_fpath}")
try:
# Use unified downloader for fallback
files_to_download = [{
'remote': fallback_fpath,
'local': os.path.basename(fallback_fpath)
}]
downloaded_dir = unified_downloader.download_huggingface_model(
repo_id=repo_id,
model_name=language,
files=files_to_download,
engine_type="chatterbox"
)
if downloaded_dir:
fallback_local_path = os.path.join(downloaded_dir, os.path.basename(fallback_fpath))
local_paths.append(fallback_local_path)
print(f"✅ Downloaded Chatterbox fallback model to: {fallback_local_path}")
else:
raise Exception("Unified fallback download failed")
except Exception as e2:
# Final fallback to HuggingFace cache
print(f"⚠️ Failed to download to local directory, using HF cache: {e2}")
try:
local_path = hf_hub_download(repo_id=repo_id, filename=fallback_fpath)
local_paths.append(local_path)
except Exception as e3:
print(f"❌ Failed to download both {fpath} and {fallback_fpath}: {e3}")
raise e3
else:
# Final fallback to HuggingFace cache
print(f"⚠️ Failed to download to local directory, using HF cache: {e}")
try:
local_path = hf_hub_download(repo_id=repo_id, filename=fpath)
local_paths.append(local_path)
except Exception as e2:
print(f"❌ Failed to download {fpath}: {e2}")
raise e2
# Check if local model exists first
model_dir = os.path.join(folder_paths.models_dir, "TTS", "chatterbox", language)
if os.path.exists(model_dir):
# Validate model completeness
is_complete, missing_files = validate_model_completeness(model_dir, language)
if is_complete:
print(f"📁 Using existing local model: {model_dir}")
return cls.from_local(model_dir, device, language)
else:
print(f"⚠️ Local model incomplete, missing: {missing_files}")
# Use the directory of the first downloaded file
model_dir = Path(local_paths[0]).parent
return cls.from_local(model_dir, device)
# Use new unified ChatterBox downloader
from utils.downloads.unified_downloader import unified_downloader
# Get files to download based on model completeness
files_to_download = get_model_requirements(language)
# Handle mixed format models (try safetensors first, fallback to pt)
if model_format == "mixed":
# Try safetensors first
files_to_download = [f.replace('.pt', '.safetensors') if f.endswith('.pt') else f for f in files_to_download]
elif model_format == "pt":
files_to_download = [f.replace('.safetensors', '.pt') if f.endswith('.safetensors') else f for f in files_to_download]
# Download model using new method with subdirectory support
subdirectory = model_config.get("subdirectory")
downloaded_dir = unified_downloader.download_chatterbox_model(
repo_id=repo_id,
model_name=language,
subdirectory=subdirectory,
files=files_to_download
)
if downloaded_dir:
print(f"✅ Downloaded ChatterBox model: {language}")
return cls.from_local(downloaded_dir, device, language)
else:
# Fallback to English if download fails
print(f"❌ Failed to download {language} model, falling back to English")
if language != "English":
return cls.from_pretrained(device, language="English")
else:
raise Exception("Failed to download English ChatterBox model")
def prepare_conditionals(self, wav_fpath, exaggeration=0.5):
## Load reference wav
s3gen_ref_wav, _sr = librosa.load(wav_fpath, sr=S3GEN_SR)
ref_16k_wav = librosa.resample(s3gen_ref_wav, orig_sr=S3GEN_SR, target_sr=S3_SR)
# Load reference wav using fallback for Python 3.13 compatibility
s3gen_ref_wav, sample_rate = safe_load(wav_fpath, sr=S3GEN_SR, mono=True)
# Resample to 16k for S3 tokenizer using fallback
ref_16k_wav = safe_resample(s3gen_ref_wav, S3GEN_SR, S3_SR)
s3gen_ref_wav = s3gen_ref_wav[:self.DEC_COND_LEN]
s3gen_ref_dict = self.s3gen.embed_ref(s3gen_ref_wav, S3GEN_SR, device=self.device)
@@ -453,10 +653,12 @@ class ChatterboxTTS:
)
wav = wav.squeeze(0).detach().cpu().numpy()
if self.enable_watermarking:
watermarked_wav = self.watermarker.apply_watermark(wav, sample_rate=self.sr)
return torch.from_numpy(watermarked_wav).unsqueeze(0)
else:
return torch.from_numpy(wav).unsqueeze(0)
self._init_watermarker_if_needed()
if self.watermarker is not None:
watermarked_wav = self.watermarker.apply_watermark(wav, sample_rate=self.sr)
return torch.from_numpy(watermarked_wav).unsqueeze(0)
return torch.from_numpy(wav).unsqueeze(0)
def generate_batch(
self,
+177 -31
View File
@@ -1,21 +1,31 @@
from pathlib import Path
import os
import librosa
import torch
import warnings
from huggingface_hub import hf_hub_download
# Use librosa fallback for Python 3.13 compatibility
from utils.audio.librosa_fallback import safe_load
# Import folder_paths for model directory detection
try:
import folder_paths
except ImportError:
folder_paths = None
# Import perth with warnings disabled
with warnings.catch_warnings():
warnings.simplefilter("ignore")
import perth
# Import perth with warnings disabled and graceful fallback
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
import perth
PERTH_AVAILABLE = True
except ImportError:
perth = None
PERTH_AVAILABLE = False
# Import safetensors for multilanguage model support
from safetensors.torch import load_file
from .models.s3tokenizer import S3_SR
from .models.s3gen import S3GEN_SR, S3Gen
@@ -37,11 +47,11 @@ class ChatterboxVC:
self.sr = S3GEN_SR
self.s3gen = s3gen
self.device = device
# Initialize watermarker silently (but disabled by default)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
self.watermarker = perth.PerthImplicitWatermarker()
self.enable_watermarking = False # Disabled by default for maximum compatibility
# Watermarking disabled by default for maximum compatibility
# Set to True to enable watermarking (requires perth library)
self.enable_watermarking = False
self.watermarker = None
self._watermarker_init_attempted = False
if ref_dict is None:
self.ref_dict = None
else:
@@ -50,11 +60,64 @@ class ChatterboxVC:
for k, v in ref_dict.items()
}
def to(self, device):
"""
Move all model components to the specified device.
Critical for ComfyUI model management - ensures all components move together
when models are detached to CPU and later reloaded to CUDA.
"""
self.device = device
# Move s3gen model
if hasattr(self.s3gen, 'to'):
self.s3gen = self.s3gen.to(device)
# Move reference dict tensors
if self.ref_dict is not None:
self.ref_dict = {
k: v.to(device) if torch.is_tensor(v) else v
for k, v in self.ref_dict.items()
}
return self
def _init_watermarker_if_needed(self):
"""Initialize watermarker on first use if enabled"""
if self.enable_watermarking and not self._watermarker_init_attempted:
self._watermarker_init_attempted = True
try:
if PERTH_AVAILABLE and hasattr(perth, 'PerthImplicitWatermarker'):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
self.watermarker = perth.PerthImplicitWatermarker()
if self.watermarker is None:
raise ValueError("PerthImplicitWatermarker returned None")
else:
raise AttributeError("PerthImplicitWatermarker not available in perth module")
except Exception as e:
print(f"❌ Failed to initialize watermarker: {e}")
self.watermarker = None
self.enable_watermarking = False
@classmethod
def from_local(cls, ckpt_dir, device) -> 'ChatterboxVC':
print(f"📦 Loading local ChatterBox VC models from: {ckpt_dir}")
ckpt_dir = Path(ckpt_dir)
# Auto-detect model format (same as TTS implementation)
def load_model_file(base_name: str):
"""Load model file with auto-detection of format (.safetensors preferred over .pt)"""
safetensors_path = ckpt_dir / f"{base_name}.safetensors"
pt_path = ckpt_dir / f"{base_name}.pt"
if safetensors_path.exists():
return load_file(safetensors_path, device=device)
elif pt_path.exists():
return torch.load(pt_path, map_location=device)
else:
raise FileNotFoundError(f"Neither {base_name}.safetensors nor {base_name}.pt found in {ckpt_dir}")
with warnings.catch_warnings():
warnings.simplefilter("ignore")
@@ -64,9 +127,10 @@ class ChatterboxVC:
ref_dict = states['gen']
s3gen = S3Gen()
s3gen.load_state_dict(
torch.load(ckpt_dir / "s3gen.pt")
)
# Load s3gen with auto-format detection
s3gen_state = load_model_file("s3gen")
# Apply JaneDoe84's critical fix: strict=False to handle missing keys (like Norwegian model)
s3gen.load_state_dict(s3gen_state, strict=False)
s3gen.to(device).eval()
instance = cls(s3gen, device, ref_dict=ref_dict)
@@ -85,6 +149,17 @@ class ChatterboxVC:
Returns:
ChatterboxVC model instance
"""
# Handle local models first - redirect to from_local
if language.startswith("local:"):
from .language_models import find_local_model_path
local_path = find_local_model_path(language)
if local_path and os.path.exists(local_path):
print(f"📁 Redirecting to local model at: {local_path}")
return cls.from_local(local_path, device)
else:
print(f"⚠️ Local model path not found for {language}, falling back to English")
language = "English"
# Import language model support
try:
from .language_models import get_model_config
@@ -109,11 +184,36 @@ class ChatterboxVC:
print(f"📦 Loading ChatterBox VC model for {language} from {repo_id}")
# Download VC models to local directory
# Sanitize language name for path construction (strip "local:" prefix)
clean_language = language[6:] if language.startswith("local:") else language
# Download VC models to local directory (use dynamic requirements like TTS)
try:
from .language_models import get_model_requirements, get_model_config
# Get VC-specific files (filter out TTS-only files)
all_files = get_model_requirements(language)
vc_files = [f for f in all_files if any(f.startswith(component) for component in ["s3gen", "conds"])]
# Separate required vs optional files (like TTS does)
required_files = [f for f in vc_files if f.startswith("s3gen")] # s3gen is required
optional_files = [f for f in vc_files if f.startswith("conds")] # conds is optional
# Get subdirectory info like TTS does
model_config = get_model_config(language)
subdirectory = model_config.get("subdirectory") if model_config else None
except ImportError:
# Fallback for older systems
required_files = ["s3gen.pt"]
optional_files = ["conds.pt"]
subdirectory = None
local_paths = []
for fpath in ["s3gen.pt", "conds.pt"]:
# Define local path in TTS organization
local_model_path = os.path.join(folder_paths.models_dir, "TTS", "chatterbox", language, fpath)
all_vc_files = required_files + optional_files
for fpath in all_vc_files:
# Define local path in TTS organization (use clean language name)
local_model_path = os.path.join(folder_paths.models_dir, "TTS", "chatterbox", clean_language, fpath)
if os.path.exists(local_model_path):
print(f"📁 Using local Chatterbox VC model: {local_model_path}")
@@ -126,7 +226,8 @@ class ChatterboxVC:
print(f"📁 Using cached Chatterbox VC model: {hf_cached_file}")
local_paths.append(hf_cached_file)
continue
except Exception:
except Exception as cache_error:
print(f"📋 Cache check for {fpath}: {str(cache_error)[:100]}... - will download")
pass
# Download to local directory
@@ -134,17 +235,22 @@ class ChatterboxVC:
os.makedirs(os.path.dirname(local_model_path), exist_ok=True)
try:
# Use unified downloader
# Use unified downloader with subdirectory support
from utils.downloads.unified_downloader import unified_downloader
# Build correct remote path with subdirectory
remote_path = fpath
if subdirectory:
remote_path = f"{subdirectory}/{fpath}"
files_to_download = [{
'remote': fpath,
'local': os.path.basename(fpath)
'remote': remote_path,
'local': os.path.basename(fpath) # Save directly to language folder
}]
downloaded_dir = unified_downloader.download_huggingface_model(
repo_id=repo_id,
model_name=language,
model_name=clean_language,
files=files_to_download,
engine_type="chatterbox"
)
@@ -154,20 +260,56 @@ class ChatterboxVC:
local_paths.append(local_model_path)
print(f"✅ Downloaded Chatterbox VC model to: {local_model_path}")
else:
raise Exception("Unified download failed")
# Check if this is an optional file before raising error
is_optional = fpath in optional_files
if is_optional:
print(f"ℹ️ Optional file {fpath} not available in {language} model - continuing without it")
continue
else:
raise Exception("Unified download failed")
except Exception as e:
# Fallback to HuggingFace cache
# Check for 401 authorization errors - these should be propagated
error_str = str(e)
if "401" in error_str or "Unauthorized" in error_str:
raise RuntimeError(f"401 Unauthorized: {language} model requires authentication - {e}")
# Fallback to HuggingFace cache for other errors
print(f"⚠️ Failed to download to local directory, using HF cache: {e}")
local_path = hf_hub_download(repo_id=repo_id, filename=fpath)
local_paths.append(local_path)
try:
local_path = hf_hub_download(repo_id=repo_id, filename=fpath)
local_paths.append(local_path)
except Exception as hf_error:
# Check HF cache error for 401 as well
hf_error_str = str(hf_error)
if "401" in hf_error_str or "Unauthorized" in hf_error_str:
raise RuntimeError(f"401 Unauthorized: {language} model requires authentication - {hf_error}")
else:
# Handle missing files based on whether they're required or optional
is_optional = fpath in optional_files
if is_optional and ("404" in hf_error_str or "Not Found" in hf_error_str):
print(f"ℹ️ Optional file {fpath} not available in {language} model - continuing without it")
continue # Skip optional files that are missing
else:
raise hf_error # Re-raise errors for required files
# Log final source summary
sources = []
for path in local_paths:
if "models/TTS/chatterbox" in str(path):
sources.append("local")
elif "cache" in str(path).lower():
sources.append("cache")
else:
sources.append("downloaded")
print(f"📦 Loading ChatterBox VC using: {', '.join(set(sources))} sources")
# Use the directory of the first downloaded file
model_dir = Path(local_paths[0]).parent
return cls.from_local(model_dir, device)
def set_target_voice(self, wav_fpath):
## Load reference wav
s3gen_ref_wav, _sr = librosa.load(wav_fpath, sr=S3GEN_SR)
# Load reference wav using fallback for Python 3.13 compatibility
s3gen_ref_wav, sample_rate = safe_load(wav_fpath, sr=S3GEN_SR, mono=True)
s3gen_ref_wav = s3gen_ref_wav[:self.DEC_COND_LEN]
self.ref_dict = self.s3gen.embed_ref(s3gen_ref_wav, S3GEN_SR, device=self.device)
@@ -183,8 +325,10 @@ class ChatterboxVC:
assert self.ref_dict is not None, "Please `prepare_conditionals` first or specify `target_voice_path`"
with torch.inference_mode():
audio_16, _ = librosa.load(audio, sr=S3_SR)
audio_16 = torch.from_numpy(audio_16).float().to(self.device)[None, ]
# Load audio using fallback for Python 3.13 compatibility
audio_16_wav, _ = safe_load(audio, sr=S3_SR, mono=True)
# Convert to the expected format
audio_16 = torch.from_numpy(audio_16_wav).float().to(self.device)[None, ]
s3_tokens, _ = self.s3gen.tokenizer(audio_16)
wav, _ = self.s3gen.inference(
@@ -192,7 +336,9 @@ class ChatterboxVC:
ref_dict=self.ref_dict,
)
wav = wav.squeeze(0).detach().cpu().numpy()
if self.enable_watermarking:
# Initialize watermarker lazily if needed
self._init_watermarker_if_needed()
if self.enable_watermarking and self.watermarker is not None:
watermarked_wav = self.watermarker.apply_watermark(wav, sample_rate=self.sr)
return torch.from_numpy(watermarked_wav).unsqueeze(0)
else:
@@ -0,0 +1,118 @@
# Set up global warning filters
import warnings
# Filter out specific warning messages
warnings.filterwarnings("ignore", message=".*LoRACompatibleLinear.*")
warnings.filterwarnings("ignore", message=".*PerthNet.*")
warnings.filterwarnings("ignore", message=".*requires authentication.*")
# Import main TTS/VC modules with error handling
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from .tts import ChatterboxOfficial23LangTTS
TTS_AVAILABLE = True
except ImportError:
TTS_AVAILABLE = False
# Create dummy class
class ChatterboxOfficial23LangTTS:
@classmethod
def from_pretrained(cls, device):
raise ImportError("ChatterboxOfficial23LangTTS not available - missing dependencies")
@classmethod
def from_local(cls, path, device):
raise ImportError("ChatterboxOfficial23LangTTS not available - missing dependencies")
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from .vc import ChatterboxOfficial23LangVC
VC_AVAILABLE = True
except ImportError:
VC_AVAILABLE = False
# Create dummy class
class ChatterboxOfficial23LangVC:
@classmethod
def from_pretrained(cls, device):
raise ImportError("ChatterboxVC not available - missing dependencies")
@classmethod
def from_local(cls, path, device):
raise ImportError("ChatterboxVC not available - missing dependencies")
# F5-TTS support modules - import independently
try:
from .f5tts import ChatterBoxF5TTS, F5TTS_AVAILABLE
F5TTS_SUPPORT_AVAILABLE = F5TTS_AVAILABLE
except ImportError:
F5TTS_SUPPORT_AVAILABLE = False
# Create dummy class
class ChatterBoxF5TTS:
@classmethod
def from_pretrained(cls, device, model_name):
raise ImportError("F5-TTS not available - missing dependencies")
@classmethod
def from_local(cls, path, device, model_name):
raise ImportError("F5-TTS not available - missing dependencies")
# Language models support
try:
from .language_models import (
get_chatterbox_models, get_model_config, get_model_files_for_language,
find_local_model_path, detect_model_format, get_available_languages,
is_model_incomplete, get_model_requirements, validate_model_completeness,
get_tokenizer_filename
)
LANGUAGE_MODELS_AVAILABLE = True
except ImportError:
LANGUAGE_MODELS_AVAILABLE = False
# Create dummy functions for compatibility
def get_available_languages():
return ["English"]
def find_local_model_path(language):
return None
def get_chatterbox_models():
return ["English"]
def get_model_config(language):
return None
def get_model_files_for_language(language):
return ("pt", "ResembleAI/chatterbox")
def detect_model_format(model_path):
return "pt"
def is_model_incomplete(language):
return False
def get_model_requirements(language):
return []
def validate_model_completeness(model_path, language):
return True, []
def get_tokenizer_filename(language):
return "tokenizer.json"
# SRT subtitle support modules - import independently
try:
from .srt_parser import SRTParser, SRTSubtitle, SRTParseError, validate_srt_timing_compatibility
from .audio_timing import (
AudioTimingUtils, PhaseVocoderTimeStretcher, TimedAudioAssembler,
calculate_timing_adjustments, AudioTimingError
)
SRT_AVAILABLE = True
__all__ = [
'ChatterboxOfficial23LangTTS', 'ChatterboxOfficial23LangVC',
'get_official_23lang_models', 'get_model_config', 'get_model_files_for_language',
'find_local_model_path', 'detect_model_format', 'get_available_languages',
'is_model_incomplete', 'get_model_requirements', 'validate_model_completeness',
'get_tokenizer_filename',
'SRTParser', 'SRTSubtitle', 'SRTParseError', 'validate_srt_timing_compatibility',
'AudioTimingUtils', 'PhaseVocoderTimeStretcher', 'TimedAudioAssembler',
'calculate_timing_adjustments', 'AudioTimingError'
]
except ImportError:
SRT_AVAILABLE = False
# SRT support not available - only export main modules and language functions
__all__ = [
'ChatterboxOfficial23LangTTS', 'ChatterboxOfficial23LangVC',
'get_official_23lang_models', 'get_model_config', 'get_model_files_for_language',
'find_local_model_path', 'detect_model_format', 'get_available_languages',
'is_model_incomplete', 'get_model_requirements', 'validate_model_completeness',
'get_tokenizer_filename'
]
@@ -0,0 +1,326 @@
"""
Adaptive Batch Processor for ChatterBox TTS
Dynamically adjusts worker count based on real-time performance monitoring.
Starts with user's batch_size (respecting their insight) and adapts from there.
Optional feature - can be disabled to respect user's exact worker count preference.
Modular design following CLAUDE.md guidelines - under 500 lines, focused responsibility.
"""
import torch
import time
from concurrent.futures import ThreadPoolExecutor, as_completed, Future
from typing import List, Tuple, Dict, Optional, Deque
from collections import deque
from dataclasses import dataclass
import threading
@dataclass
class PerformanceMetric:
"""Single performance measurement."""
timestamp: float
worker_count: int
completion_time: float
tokens_per_second: float
total_throughput: float # texts completed per second
class AdaptivePerformanceMonitor:
"""
Monitors real-time performance and suggests worker count adjustments.
"""
def __init__(self, initial_workers: int, adaptation_enabled: bool = True):
"""
Initialize performance monitor.
Args:
initial_workers: User's preferred starting worker count
adaptation_enabled: Whether to suggest adaptations
"""
self.initial_workers = initial_workers
self.current_optimal_workers = initial_workers
self.adaptation_enabled = adaptation_enabled
# Performance tracking
self.metrics: Deque[PerformanceMetric] = deque(maxlen=20) # Last 20 measurements
self.worker_performance: Dict[int, List[float]] = {} # worker_count -> [throughputs]
# Adaptation logic
self.evaluation_interval = 3 # Evaluate every N completions
self.completions_since_eval = 0
self.last_adaptation_time = time.time()
self.min_adaptation_interval = 10.0 # Wait 10s between adaptations
# Stability tracking
self.stable_performance_threshold = 0.1 # 10% variance
self.performance_declining_threshold = 0.15 # 15% decline triggers reduction
print(f"🔧 Adaptive Monitor: Starting with {initial_workers} workers (user preference)")
print(f"🎛️ Dynamic adaptation: {'ENABLED' if adaptation_enabled else 'DISABLED'}")
def record_completion(self, completion_time: float, tokens_per_second: float) -> Optional[int]:
"""
Record a text completion and return suggested worker count change.
Args:
completion_time: Time taken for this text (seconds)
tokens_per_second: Generation speed for this text
Returns:
Suggested new worker count, or None if no change recommended
"""
if not self.adaptation_enabled:
return None # Respect user's exact worker count
# Record this completion
current_time = time.time()
# Calculate current throughput (approximate) - FIXED: prevent division by zero
recent_completions = len([m for m in self.metrics if current_time - m.timestamp < 10])
if self.metrics:
time_window = max(0.1, current_time - self.metrics[0].timestamp) # Minimum 0.1s window
total_throughput = recent_completions / min(10, time_window)
else:
total_throughput = 0.0 # No data yet
metric = PerformanceMetric(
timestamp=current_time,
worker_count=self.current_optimal_workers,
completion_time=completion_time,
tokens_per_second=tokens_per_second,
total_throughput=total_throughput
)
self.metrics.append(metric)
# Track performance for this worker count
if self.current_optimal_workers not in self.worker_performance:
self.worker_performance[self.current_optimal_workers] = []
self.worker_performance[self.current_optimal_workers].append(total_throughput)
# Check if it's time to evaluate
self.completions_since_eval += 1
if self.completions_since_eval >= self.evaluation_interval:
return self._evaluate_adaptation()
return None
def _evaluate_adaptation(self) -> Optional[int]:
"""Evaluate whether to adapt worker count."""
self.completions_since_eval = 0
current_time = time.time()
# Don't adapt too frequently
if current_time - self.last_adaptation_time < self.min_adaptation_interval:
return None
if len(self.metrics) < 6: # Need some data
return None
# Get recent performance
recent_metrics = list(self.metrics)[-6:] # Last 6 completions
current_avg_throughput = sum(m.total_throughput for m in recent_metrics) / len(recent_metrics)
current_avg_speed = sum(m.tokens_per_second for m in recent_metrics) / len(recent_metrics)
# Compare with earlier performance for same worker count
worker_history = self.worker_performance.get(self.current_optimal_workers, [])
if len(worker_history) > 6:
earlier_avg = sum(worker_history[-12:-6]) / 6 # Earlier batch
current_avg = sum(worker_history[-6:]) / 6 # Current batch
performance_change = (current_avg - earlier_avg) / earlier_avg
if performance_change < -self.performance_declining_threshold:
# Performance declining significantly - reduce workers
new_workers = max(1, self.current_optimal_workers - 1)
print(f"🔽 ADAPTIVE: Performance declining ({performance_change:.1%}), reducing workers {self.current_optimal_workers} → {new_workers}")
return self._suggest_adaptation(new_workers)
elif performance_change > -0.05 and current_avg_speed > 3.0: # Stable + fast
# Performance stable and individual workers are fast - try more workers
# But don't go crazy - cap at 2x user's initial preference
max_workers = min(self.initial_workers * 2, 8)
if self.current_optimal_workers < max_workers:
new_workers = self.current_optimal_workers + 1
print(f"🔼 ADAPTIVE: Performance stable & fast ({current_avg_speed:.1f} it/s), trying more workers {self.current_optimal_workers} → {new_workers}")
return self._suggest_adaptation(new_workers)
return None
def _suggest_adaptation(self, new_workers: int) -> int:
"""Record adaptation suggestion."""
self.last_adaptation_time = time.time()
self.current_optimal_workers = new_workers
return new_workers
def get_performance_summary(self) -> Dict:
"""Get current performance summary."""
if not self.metrics:
return {"status": "No data yet"}
recent = list(self.metrics)[-5:] if len(self.metrics) >= 5 else list(self.metrics)
avg_throughput = sum(m.total_throughput for m in recent) / len(recent)
avg_speed = sum(m.tokens_per_second for m in recent) / len(recent)
return {
"current_workers": self.current_optimal_workers,
"initial_workers": self.initial_workers,
"avg_throughput": avg_throughput,
"avg_speed_per_worker": avg_speed,
"adaptation_enabled": self.adaptation_enabled,
"total_completions": len(self.metrics)
}
class AdaptiveBatchProcessor:
"""
Batch processor with optional dynamic worker adaptation.
Starts with user's batch_size (respecting their insight) and optionally
adapts based on real-time performance monitoring.
"""
def __init__(self, tts_model, enable_adaptation: bool = True):
"""
Initialize adaptive processor.
Args:
tts_model: ChatterboxTTS instance
enable_adaptation: Whether to enable dynamic adaptation
"""
self.tts_model = tts_model
self.enable_adaptation = enable_adaptation
self.current_executor: Optional[ThreadPoolExecutor] = None
self.monitor: Optional[AdaptivePerformanceMonitor] = None
self.adaptation_lock = threading.Lock()
def process_texts_adaptively(
self,
texts: List[str],
temperature: float,
cfg_weight: float,
exaggeration: float,
user_batch_size: int
) -> List[torch.Tensor]:
"""
Process texts with optional adaptive worker management.
Args:
texts: List of text strings
temperature: Sampling temperature
cfg_weight: CFG weight
exaggeration: Emotion exaggeration
user_batch_size: User's preferred batch size (starting point)
Returns:
List of audio tensors in original order
"""
# Initialize monitor with user's preference
self.monitor = AdaptivePerformanceMonitor(
initial_workers=user_batch_size,
adaptation_enabled=self.enable_adaptation
)
effective_workers = min(user_batch_size, len(texts))
if self.enable_adaptation:
print(f"🤖 ADAPTIVE PROCESSING: Starting with {effective_workers} workers (user preference), will adapt based on performance")
else:
print(f"🔒 FIXED PROCESSING: Using exactly {effective_workers} workers as requested")
return self._process_with_adaptive_workers(
texts, temperature, cfg_weight, exaggeration, effective_workers
)
def _process_with_adaptive_workers(
self,
texts: List[str],
temperature: float,
cfg_weight: float,
exaggeration: float,
initial_workers: int
) -> List[torch.Tensor]:
"""Process texts with adaptive worker management."""
results = [None] * len(texts)
completed_indices = set()
current_workers = initial_workers
def _process_single_text(text_index: int, text: str) -> Tuple[int, torch.Tensor, float, float]:
"""Process single text and return timing metrics."""
start_time = time.time()
try:
print(f" 🧵 Worker {text_index+1}: Processing: {text[:30]}...")
# Track generation performance
gen_start = time.time()
audio = self.tts_model.generate(
text=text,
audio_prompt_path=None,
exaggeration=exaggeration,
cfg_weight=cfg_weight,
temperature=temperature,
)
gen_time = time.time() - gen_start
# Estimate tokens/second (rough approximation)
estimated_tokens = len(text) * 2 # Rough estimate
tokens_per_second = estimated_tokens / gen_time if gen_time > 0 else 0
total_time = time.time() - start_time
print(f" ✅ Worker {text_index+1}: Completed in {total_time:.1f}s ({tokens_per_second:.1f} tok/s)")
return text_index, audio, total_time, tokens_per_second
except Exception as e:
total_time = time.time() - start_time
print(f" ❌ Worker {text_index+1}: Failed in {total_time:.1f}s - {e}")
return text_index, torch.zeros(1, 1000), total_time, 0.0
# Process all texts
with ThreadPoolExecutor(max_workers=current_workers) as executor:
# Submit all tasks
futures = {
executor.submit(_process_single_text, i, text): i
for i, text in enumerate(texts)
}
completed_count = 0
total_start_time = time.time()
for future in as_completed(futures):
text_index, audio, completion_time, tokens_per_second = future.result()
results[text_index] = audio
completed_indices.add(text_index)
completed_count += 1
# Record performance and get adaptation suggestion
if self.monitor:
suggested_workers = self.monitor.record_completion(completion_time, tokens_per_second)
# Note: For simplicity, we don't actually restart the executor mid-process
# Real adaptation would need more complex future management
# This logs what WOULD happen for user feedback
if suggested_workers and suggested_workers != current_workers:
print(f"📊 ADAPTATION SUGGESTION: Would adjust to {suggested_workers} workers (currently {current_workers})")
# In a full implementation, you'd restart executor with new worker count
# current_workers = suggested_workers
progress_percent = int(100 * completed_count / len(texts))
print(f" 📊 Progress: {completed_count}/{len(texts)} completed ({progress_percent}%)")
total_time = time.time() - total_start_time
avg_time = total_time / len(texts)
# Performance summary
if self.monitor:
summary = self.monitor.get_performance_summary()
print(f"✅ ADAPTIVE PROCESSING COMPLETED: {len(results)} texts in {total_time:.1f}s")
print(f"📈 Performance: {summary['avg_throughput']:.2f} texts/sec, {summary['avg_speed_per_worker']:.1f} tok/s per worker")
if self.enable_adaptation:
print(f"🎛️ Final recommendation: Use {summary['current_workers']} workers for this workload")
return results
@@ -0,0 +1,674 @@
"""
Audio Timing Utilities for ChatterBox TTS SRT Support
Provides time-stretching, silence padding, and sample-accurate timing conversion
"""
# Smart numba compatibility for audio timing
from utils.compatibility import setup_numba_compatibility
import sys
import os
setup_numba_compatibility(quick_startup=True, verbose=False)
import torch
import torchaudio
import numpy as np
from typing import Tuple, Optional, List, Union
import librosa
from scipy.signal import stft, istft
import warnings
import subprocess
import tempfile
import os
import shutil
import soundfile as sf
class AudioTimingError(Exception):
"""Exception raised when audio timing operations fail"""
pass
class AudioTimingUtils:
"""
Utilities for audio timing manipulation and synchronization
"""
@staticmethod
def seconds_to_samples(seconds: float, sample_rate: int) -> int:
"""
Convert time in seconds to sample count
Args:
seconds: Time in seconds
sample_rate: Audio sample rate
Returns:
Number of samples (integer)
"""
return int(seconds * sample_rate)
@staticmethod
def samples_to_seconds(samples: int, sample_rate: int) -> float:
"""
Convert sample count to time in seconds
Args:
samples: Number of samples
sample_rate: Audio sample rate
Returns:
Time in seconds
"""
return samples / sample_rate
@staticmethod
def get_audio_duration(audio: torch.Tensor, sample_rate: int) -> float:
"""
Get duration of audio tensor in seconds
Args:
audio: Audio tensor (1D or 2D)
sample_rate: Audio sample rate
Returns:
Duration in seconds
"""
if audio.dim() == 1:
return audio.size(0) / sample_rate
elif audio.dim() == 2:
return audio.size(-1) / sample_rate
else:
raise AudioTimingError(f"Unsupported audio tensor dimensions: {audio.dim()}")
@staticmethod
def create_silence(duration_seconds: float, sample_rate: int,
channels: int = 1, device: Optional[torch.device] = None) -> torch.Tensor:
"""
Create silence tensor of specified duration
Args:
duration_seconds: Duration of silence in seconds
sample_rate: Audio sample rate
channels: Number of audio channels
device: Target device for tensor
Returns:
Silence tensor of shape [channels, samples] or [samples] if channels=1
"""
if duration_seconds < 0:
raise AudioTimingError(f"Duration cannot be negative: {duration_seconds}")
num_samples = AudioTimingUtils.seconds_to_samples(duration_seconds, sample_rate)
if channels == 1:
silence = torch.zeros(num_samples, device=device)
else:
silence = torch.zeros(channels, num_samples, device=device)
return silence
@staticmethod
def pad_audio_to_duration(audio: torch.Tensor, target_duration: float,
sample_rate: int, pad_mode: str = "end") -> torch.Tensor:
"""
Pad audio to reach target duration
Args:
audio: Input audio tensor
target_duration: Target duration in seconds
sample_rate: Audio sample rate
pad_mode: Where to add padding ("start", "end", "both")
Returns:
Padded audio tensor
"""
current_duration = AudioTimingUtils.get_audio_duration(audio, sample_rate)
if current_duration >= target_duration:
return audio # No padding needed
padding_duration = target_duration - current_duration
padding_samples = AudioTimingUtils.seconds_to_samples(padding_duration, sample_rate)
if audio.dim() == 1:
padding = torch.zeros(padding_samples, device=audio.device, dtype=audio.dtype)
else:
padding = torch.zeros(audio.size(0), padding_samples, device=audio.device, dtype=audio.dtype)
if pad_mode == "start":
return torch.cat([padding, audio], dim=-1)
elif pad_mode == "end":
return torch.cat([audio, padding], dim=-1)
elif pad_mode == "both":
half_padding = padding_samples // 2
if audio.dim() == 1:
start_pad = torch.zeros(half_padding, device=audio.device, dtype=audio.dtype)
end_pad = torch.zeros(padding_samples - half_padding, device=audio.device, dtype=audio.dtype)
else:
start_pad = torch.zeros(audio.size(0), half_padding, device=audio.device, dtype=audio.dtype)
end_pad = torch.zeros(audio.size(0), padding_samples - half_padding, device=audio.device, dtype=audio.dtype)
return torch.cat([start_pad, audio, end_pad], dim=-1)
else:
raise AudioTimingError(f"Invalid pad_mode: {pad_mode}. Use 'start', 'end', or 'both'")
class PhaseVocoderTimeStretcher:
"""
Phase vocoder-based time stretching implementation
"""
def __init__(self, hop_length: int = 512, win_length: int = 2048):
"""
Initialize phase vocoder
Args:
hop_length: STFT hop length
win_length: STFT window length
"""
self.hop_length = hop_length
self.win_length = win_length
def time_stretch(self, audio: torch.Tensor, stretch_factor: float,
sample_rate: int) -> torch.Tensor:
"""
Time-stretch audio using phase vocoder
Args:
audio: Input audio tensor (1D or 2D)
stretch_factor: Time stretching factor (>1 = slower, <1 = faster)
sample_rate: Audio sample rate
Returns:
Time-stretched audio tensor
"""
if stretch_factor <= 0:
raise AudioTimingError(f"Stretch factor must be positive: {stretch_factor}")
if abs(stretch_factor - 1.0) < 1e-6:
return audio # No stretching needed
# Handle different tensor dimensions
original_shape = audio.shape
if audio.dim() == 1:
audio = audio.unsqueeze(0) # Add channel dimension
elif audio.dim() > 2:
raise AudioTimingError(f"Unsupported audio tensor dimensions: {audio.dim()}")
stretched_channels = []
for channel in range(audio.size(0)):
channel_audio = audio[channel].cpu().numpy()
try:
# Use librosa for phase vocoder time stretching
stretched = librosa.effects.time_stretch(
channel_audio,
rate=1.0/stretch_factor, # librosa uses rate = 1/stretch_factor
hop_length=self.hop_length
)
stretched_channels.append(torch.from_numpy(stretched))
except Exception as e:
# Fallback to simple resampling if phase vocoder fails
warnings.warn(f"Phase vocoder failed, using simple resampling: {e}")
stretched = self._simple_time_stretch(channel_audio, stretch_factor)
stretched_channels.append(torch.from_numpy(stretched))
# Combine channels
result = torch.stack(stretched_channels, dim=0).to(audio.device)
# Restore original shape if input was 1D
if len(original_shape) == 1:
result = result.squeeze(0)
return result
def _simple_time_stretch(self, audio: np.ndarray, stretch_factor: float) -> np.ndarray:
"""
Simple time stretching using interpolation (fallback method)
Args:
audio: Input audio array
stretch_factor: Time stretching factor
Returns:
Time-stretched audio array
"""
original_length = len(audio)
new_length = int(original_length * stretch_factor)
# Create new time indices
old_indices = np.linspace(0, original_length - 1, original_length)
new_indices = np.linspace(0, original_length - 1, new_length)
# Interpolate
stretched = np.interp(new_indices, old_indices, audio)
return stretched
class FFmpegTimeStretcher:
"""FFmpeg time stretching using atempo filter"""
def __init__(self):
"""Verify FFmpeg is available"""
try:
result = subprocess.run(['ffmpeg', '-version'], capture_output=True)
if result.returncode != 0:
raise AudioTimingError("FFmpeg check failed")
except Exception as e:
raise AudioTimingError(f"FFmpeg not found: {str(e)}")
def _build_filter_chain(self, stretch_factor: float) -> str:
"""Build safe FFmpeg filter chain for any stretch factor"""
if stretch_factor <= 0:
raise AudioTimingError("Invalid stretch factor")
speed = 1/stretch_factor
# Direct speed if within safe range
if 0.5 <= speed <= 2.0:
return f"aresample=async=0,atempo={speed:0.6f}"
# Chain filters for extreme speeds
filters = []
remaining = speed
while remaining < 0.5:
filters.append('atempo=0.5') # Max slowdown
remaining *= 2
if len(filters) > 4: # Safety limit
raise AudioTimingError("Too extreme slowdown")
while remaining > 2.0:
filters.append('atempo=2.0') # Max speedup
remaining /= 2
if len(filters) > 4:
raise AudioTimingError("Too extreme speedup")
# Final adjustment
if abs(remaining - 1.0) > 1e-6:
filters.append(f'atempo={remaining:0.6f}')
filter_str = f"aresample=async=0,{','.join(filters)}"
return filter_str
def time_stretch(self, audio: torch.Tensor, stretch_factor: float, sample_rate: int) -> torch.Tensor:
"""Time stretch audio using FFmpeg with safe filter chaining"""
# Full validation
if not isinstance(audio, torch.Tensor):
raise AudioTimingError("Input must be a tensor")
if not isinstance(stretch_factor, (int, float)) or stretch_factor <= 0:
raise AudioTimingError(f"Invalid stretch factor: {stretch_factor}")
if abs(stretch_factor - 1.0) < 1e-6:
return audio
try:
# Get filter chain
filter_str = self._build_filter_chain(stretch_factor)
# Shape handling
original_shape = audio.shape
if audio.dim() == 1:
audio = audio.unsqueeze(0)
elif audio.dim() > 2:
raise AudioTimingError("Only mono/stereo supported")
# Process channels
stretched = []
with tempfile.TemporaryDirectory() as temp_dir:
for i in range(audio.shape[0]):
# Setup paths
in_path = os.path.join(temp_dir, f'in_{i}.wav')
out_path = os.path.join(temp_dir, f'out_{i}.wav')
try:
# Save input
data = audio[i].cpu().numpy()
sf.write(in_path, data, sample_rate,
format='WAV', subtype='FLOAT')
# Process with FFmpeg
cmd = [
'ffmpeg',
'-nostdin', # No interaction
'-y', # Overwrite output
'-hide_banner', # Less noise
'-i', in_path, # Input
'-filter:a', filter_str, # Audio filter
'-c:a', 'pcm_f32le', # Output format
'-ar', str(sample_rate),
'-ac', '1', # Force mono
'-v', 'error', # Only errors (less verbose)
out_path
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise AudioTimingError(f"FFmpeg processing failed: {result.stderr}")
if not os.path.exists(out_path) or os.path.getsize(out_path) == 0:
raise AudioTimingError("No output produced")
# Load and verify
audio_data, sr = sf.read(out_path)
if sr != sample_rate:
raise AudioTimingError(f"Sample rate mismatch: {sr}")
if len(audio_data) == 0:
raise AudioTimingError("Empty output")
stretched.append(torch.from_numpy(audio_data))
except Exception as e:
raise AudioTimingError(f"Channel {i} failed: {str(e)}")
# Combine results
if not stretched:
raise AudioTimingError("No audio was processed successfully")
result = torch.stack(stretched, dim=0).to(audio.device)
return result.squeeze(0) if len(original_shape) == 1 else result
except Exception as e:
raise AudioTimingError(f"Time stretching failed: {str(e)}")
# Run FFmpeg with checks
def _process_audio_file(self, in_path: str, out_path: str, filter_str: str,
sample_rate: int) -> np.ndarray:
"""Process a single audio file through FFmpeg"""
cmd = [
'ffmpeg',
'-hide_banner', # Reduce noise
'-nostdin', # No interactive mode
'-y', # Overwrite output
'-i', in_path,
'-filter:a', filter_str,
'-acodec', 'pcm_f32le',
'-ar', str(sample_rate),
'-ac', '1',
'-v', 'info', # Informative output level
out_path
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
raise AudioTimingError(f"FFmpeg failed: {result.stderr}")
# Verify output
if not os.path.exists(out_path):
raise AudioTimingError("No output file produced")
out_size = os.path.getsize(out_path)
if out_size == 0:
raise AudioTimingError("Empty output file")
# Load and validate
audio_data, sr = sf.read(out_path)
if sr != sample_rate:
raise AudioTimingError(f"Sample rate mismatch: got {sr}, expected {sample_rate}")
if len(audio_data) == 0:
raise AudioTimingError("No audio data in output")
return audio_data
except subprocess.TimeoutExpired:
raise AudioTimingError("FFmpeg process timed out")
except Exception as e:
raise AudioTimingError(f"FFmpeg processing failed: {str(e)}")
if not stretched:
raise AudioTimingError("No channels were processed successfully")
try:
# Stack channels and restore shape
result = torch.stack(stretched, dim=0).to(audio.device)
print(f"Successfully processed all channels")
return result.squeeze(0) if len(original_shape) == 1 else result
except Exception as e:
raise AudioTimingError(f"Failed to combine channels: {str(e)}")
def _build_atempo_chain(self, stretch_factor: float) -> str:
"""Simple atempo command that stays within safe ranges"""
if stretch_factor <= 0:
raise AudioTimingError("Invalid stretch factor")
speed = 1/stretch_factor
if not (0.5 <= speed <= 2.0):
raise AudioTimingError("Speed factor out of supported range")
return f'atempo={speed:0.6f}'
class TimedAudioAssembler:
"""
Assembles audio segments with precise timing control
"""
def __init__(self, sample_rate: int, stretcher_type: str = "ffmpeg",
time_stretcher: Optional[Union[PhaseVocoderTimeStretcher, FFmpegTimeStretcher]] = None):
"""
Initialize audio assembler
Args:
sample_rate: Target sample rate for output
stretcher_type: Type of time stretcher to use ("ffmpeg" or "phase_vocoder")
time_stretcher: Custom time stretching utility (creates default if None)
"""
self.sample_rate = sample_rate
self.stretch_method_used = None # Track which stretching method was used
if time_stretcher is not None:
if not isinstance(time_stretcher, (PhaseVocoderTimeStretcher, FFmpegTimeStretcher)):
raise AudioTimingError("time_stretcher must be PhaseVocoderTimeStretcher or FFmpegTimeStretcher")
self.time_stretcher = time_stretcher
else:
if stretcher_type == "ffmpeg":
try:
self.time_stretcher = FFmpegTimeStretcher()
except AudioTimingError as e:
# Fall back to phase vocoder silently
self.time_stretcher = PhaseVocoderTimeStretcher()
elif stretcher_type == "phase_vocoder":
self.time_stretcher = PhaseVocoderTimeStretcher()
else:
raise AudioTimingError(f"Invalid stretcher_type: {stretcher_type}. Use 'ffmpeg' or 'phase_vocoder'")
def assemble_timed_audio(self, audio_segments: List[torch.Tensor],
target_timings: List[Tuple[float, float]],
total_duration: Optional[float] = None,
fade_duration: float = 0.01) -> torch.Tensor:
"""
Assemble audio segments with precise timing
Args:
audio_segments: List of audio tensors to assemble
target_timings: List of (start_time, end_time) tuples in seconds
total_duration: Total duration of output (auto-calculated if None)
fade_duration: Crossfade duration in seconds for overlaps
Returns:
Assembled audio tensor
"""
if len(audio_segments) != len(target_timings):
raise AudioTimingError(
f"Number of audio segments ({len(audio_segments)}) must match "
f"number of timings ({len(target_timings)})"
)
if not audio_segments:
raise AudioTimingError("No audio segments provided")
# Calculate total duration if not provided
if total_duration is None:
total_duration = max(end_time for _, end_time in target_timings)
# Create output buffer
total_samples = AudioTimingUtils.seconds_to_samples(total_duration, self.sample_rate)
# Determine output shape based on first segment
first_segment = audio_segments[0]
if first_segment.dim() == 1:
output = torch.zeros(total_samples, device=first_segment.device, dtype=first_segment.dtype)
else:
output = torch.zeros(first_segment.size(0), total_samples,
device=first_segment.device, dtype=first_segment.dtype)
# Process each segment
for i, (audio_segment, (start_time, end_time)) in enumerate(zip(audio_segments, target_timings)):
if start_time < 0 or end_time <= start_time:
raise AudioTimingError(f"Invalid timing for segment {i}: {start_time} -> {end_time}")
target_duration = end_time - start_time
current_duration = AudioTimingUtils.get_audio_duration(audio_segment, self.sample_rate)
# If input audio is empty but target duration is positive, create silence.
if current_duration == 0.0 and target_duration > 0.0:
num_output_channels = first_segment.size(0) if first_segment.dim() > 1 else 1
audio_segment = AudioTimingUtils.create_silence(
target_duration,
self.sample_rate,
channels=num_output_channels,
device=first_segment.device, # Use device/dtype consistent with output buffer
dtype=first_segment.dtype
)
current_duration = target_duration # Update duration, no stretching needed for this segment
self.stretch_method_used = "silence_created"
# Time-stretch if needed
if abs(current_duration - target_duration) > 0.01: # 10ms tolerance
if current_duration == 0.0: # Should only be true if target_duration is also ~0.0
stretch_factor = 1.0 # Avoid division by zero
else:
stretch_factor = target_duration / current_duration
# Determine and track stretching method
if isinstance(self.time_stretcher, FFmpegTimeStretcher):
self.stretch_method_used = "ffmpeg"
elif isinstance(self.time_stretcher, PhaseVocoderTimeStretcher):
self.stretch_method_used = "phase_vocoder"
audio_segment = self.time_stretcher.time_stretch(
audio_segment, stretch_factor, self.sample_rate
)
else:
# No stretching needed
self.stretch_method_used = "none"
# Calculate sample positions
start_sample = AudioTimingUtils.seconds_to_samples(start_time, self.sample_rate)
end_sample = AudioTimingUtils.seconds_to_samples(end_time, self.sample_rate)
# Ensure segment fits exactly
target_samples = end_sample - start_sample
current_samples = audio_segment.size(-1)
if current_samples != target_samples:
# Fine-tune length to match exactly
if current_samples > target_samples:
audio_segment = audio_segment[..., :target_samples]
else:
padding_needed = target_samples - current_samples
if audio_segment.dim() == 1:
padding = torch.zeros(padding_needed, device=audio_segment.device, dtype=audio_segment.dtype)
else:
padding = torch.zeros(audio_segment.size(0), padding_needed,
device=audio_segment.device, dtype=audio_segment.dtype)
audio_segment = torch.cat([audio_segment, padding], dim=-1)
# Place segment in output buffer with crossfading for overlaps
self._place_segment_with_fade(output, audio_segment, start_sample, end_sample, fade_duration)
return output
def _place_segment_with_fade(self, output: torch.Tensor, segment: torch.Tensor,
start_sample: int, end_sample: int, fade_duration: float):
"""
Place audio segment in output buffer with crossfading for overlaps
"""
segment_length = segment.size(-1)
fade_samples = min(
AudioTimingUtils.seconds_to_samples(fade_duration, self.sample_rate),
segment_length // 4 # Don't fade more than 25% of segment
)
# Check for overlap with existing content
if output.dim() == 1:
existing_content = output[start_sample:end_sample]
has_overlap = torch.any(existing_content != 0)
else:
existing_content = output[:, start_sample:end_sample]
has_overlap = torch.any(existing_content != 0)
if has_overlap and fade_samples > 0:
# Apply crossfade
fade_in = torch.linspace(0, 1, fade_samples, device=segment.device)
fade_out = torch.linspace(1, 0, fade_samples, device=segment.device)
# Fade in the beginning of new segment
if segment.dim() == 1:
segment[:fade_samples] *= fade_in
# Fade out existing content at the beginning
output[start_sample:start_sample + fade_samples] *= fade_out
else:
segment[:, :fade_samples] *= fade_in.unsqueeze(0)
output[:, start_sample:start_sample + fade_samples] *= fade_out.unsqueeze(0)
# Fade out the end of new segment if there's content after
end_check_start = min(end_sample, output.size(-1) - fade_samples)
end_check_end = min(end_sample + fade_samples, output.size(-1))
if output.dim() == 1:
if end_check_start < end_check_end and torch.any(output[end_check_start:end_check_end] != 0):
segment[-fade_samples:] *= fade_out
else:
if end_check_start < end_check_end and torch.any(output[:, end_check_start:end_check_end] != 0):
segment[:, -fade_samples:] *= fade_out.unsqueeze(0)
# Add segment to output
if output.dim() == 1:
output[start_sample:end_sample] += segment
else:
output[:, start_sample:end_sample] += segment
def calculate_timing_adjustments(natural_durations: List[float],
target_timings: List[Tuple[float, float]]) -> List[dict]:
"""
Calculate timing adjustments needed for each audio segment
Args:
natural_durations: Natural durations of TTS-generated segments
target_timings: Target (start_time, end_time) tuples from SRT
Returns:
List of adjustment dictionaries with timing information
"""
adjustments = []
for i, (natural_duration, (start_time, end_time)) in enumerate(zip(natural_durations, target_timings)):
target_duration = end_time - start_time
stretch_factor = target_duration / natural_duration if natural_duration > 0 else 1.0
adjustment = {
'segment_index': i,
'natural_duration': natural_duration,
'target_duration': target_duration,
'start_time': start_time,
'end_time': end_time,
'stretch_factor': stretch_factor,
'needs_stretching': abs(stretch_factor - 1.0) > 0.05, # 5% tolerance
'stretch_type': 'compress' if stretch_factor < 1.0 else 'expand' if stretch_factor > 1.0 else 'none'
}
adjustments.append(adjustment)
return adjustments
@@ -0,0 +1,187 @@
"""
Batch Processor Module
Handles actual batch processing of character groups.
Works with character_grouper.py to process multiple segments simultaneously.
"""
from typing import Dict, List, Any
import torch
from .character_grouper import CharacterGroup
class BatchProcessor:
"""
Processes character groups using batch inference.
This is where the actual batching magic happens - instead of:
1. Process segment 3 (crestfallen_original)
2. Process segment 5 (crestfallen_original)
We do:
1. Batch process segments [3, 5] for crestfallen_original simultaneously
"""
def __init__(self, tts_model):
self.tts_model = tts_model
def process_character_group_batch(
self,
character_group: CharacterGroup,
inputs: Dict[str, Any],
voice_refs: Dict[str, str],
chunker,
language: str
) -> Dict[int, torch.Tensor]:
"""
Process all segments for a character using TRUE batch processing.
Args:
character_group: Group of segments for same character
inputs: Node inputs (exaggeration, temperature, etc.)
voice_refs: Character voice reference mapping
chunker: Text chunking utility
language: Language code for logging
Returns:
Dict mapping original_idx -> generated_audio_tensor
"""
character = character_group.character
segments = character_group.segments
print(f"🚀 BATCH PROCESSING: {character} - {len(segments)} segments in {language}")
# Collect all texts for batching
batch_texts = []
segment_chunk_counts = [] # Track how many chunks each segment produces
for segment in segments:
# Apply chunking if needed
if inputs["enable_chunking"] and len(segment.segment_text) > inputs["max_chars_per_chunk"]:
chunks = chunker.split_into_chunks(segment.segment_text, inputs["max_chars_per_chunk"])
else:
chunks = [segment.segment_text]
# Process each chunk (apply crash protection, etc.)
processed_chunks = []
for chunk_text in chunks:
# processed_chunk = self._apply_crash_protection(chunk_text, inputs.get("crash_protection_template", "hmm ,, {seg} hmm ,,")) # DISABLED FOR TESTING
processed_chunk = chunk_text # Direct text without crash protection
processed_chunks.append(processed_chunk)
batch_texts.extend(processed_chunks)
segment_chunk_counts.append(len(processed_chunks))
# Get voice reference for this character
char_audio_prompt = voice_refs[character]
# THE ACTUAL BATCH PROCESSING - this is the key improvement
print(f"⚡ Batch generating {len(batch_texts)} chunks for {character}")
print(f"🔧 DEBUG: batch_size from inputs = {inputs.get('batch_size', 4)}")
try:
batch_audio = self.tts_model.generate_batch(
texts=batch_texts,
audio_prompt_path=char_audio_prompt,
exaggeration=inputs["exaggeration"],
cfg_weight=inputs["cfg_weight"],
temperature=inputs["temperature"],
batch_size=inputs.get("batch_size", 4),
max_workers=inputs.get("batch_size", 4), # Use batch_size as max_workers
enable_adaptive_batching=(inputs.get("batch_size", 1) > 1) # Auto-enable based on batch_size
)
print(f"✅ BATCH SUCCESS: Generated {len(batch_audio)} audio chunks")
except Exception as e:
print(f"❌ BATCH FAILED: {e}")
raise # Let calling code handle fallback
# Map batch results back to original segments
results = {}
batch_idx = 0
for i, segment in enumerate(segments):
chunk_count = segment_chunk_counts[i]
# Collect all chunks for this segment
segment_chunks = []
for _ in range(chunk_count):
segment_chunks.append(batch_audio[batch_idx])
batch_idx += 1
# Combine chunks if multiple
if len(segment_chunks) == 1:
segment_audio = segment_chunks[0]
else:
segment_audio = torch.cat(segment_chunks, dim=-1)
results[segment.original_idx] = segment_audio
return results
def process_character_group_sequential(
self,
character_group: CharacterGroup,
inputs: Dict[str, Any],
voice_refs: Dict[str, str],
required_language: str,
total_segments: int,
language: str,
stable_audio_component: str,
chunker,
generate_tts_method
) -> Dict[int, torch.Tensor]:
"""
Fallback: Process character group sequentially.
Used when batch processing fails or for single segments.
"""
character = character_group.character
segments = character_group.segments
print(f"→ SEQUENTIAL: {character} - {len(segments)} segments in {language}")
results = {}
char_audio_prompt = voice_refs[character]
for segment in segments:
segment_display_idx = segment.original_idx + 1 # 1-based for display
print(f"🎤 Generating segment {segment_display_idx}/{total_segments} for '{character}' (lang: {language})")
# Apply chunking
if inputs["enable_chunking"] and len(segment.segment_text) > inputs["max_chars_per_chunk"]:
chunks = chunker.split_into_chunks(segment.segment_text, inputs["max_chars_per_chunk"])
else:
chunks = [segment.segment_text]
# Process each chunk sequentially
segment_audio_chunks = []
for chunk_i, chunk_text in enumerate(chunks):
if len(chunks) > 1:
print(f" → Chunk {chunk_i+1}/{len(chunks)}")
chunk_audio = generate_tts_method(
chunk_text, char_audio_prompt, inputs["exaggeration"],
inputs["temperature"], inputs["cfg_weight"], required_language,
True, character=character, seed=inputs["seed"],
enable_cache=inputs.get("enable_audio_cache", True),
crash_protection_template=inputs.get("crash_protection_template", "hmm ,, {seg} hmm ,,"),
stable_audio_component=stable_audio_component
)
segment_audio_chunks.append(chunk_audio)
# Combine chunks for this segment
if len(segment_audio_chunks) == 1:
segment_audio = segment_audio_chunks[0]
else:
segment_audio = torch.cat(segment_audio_chunks, dim=-1)
results[segment.original_idx] = segment_audio
return results
def _apply_crash_protection(self, text: str, padding_template: str, min_length: int = 15) -> str:
"""Apply crash protection padding to short texts."""
if len(text.strip()) < min_length:
return padding_template.format(seg=text)
return text
@@ -0,0 +1,139 @@
"""
Character Grouper Module
Groups segments by character WITHIN each language group.
Integrates with existing language grouping system - does not replace it.
Fixes the issue where segments like:
- Segment 3: crestfallen_original
- Segment 5: crestfallen_original
Were processed separately instead of being batched together.
"""
from typing import List, Dict, Tuple
from dataclasses import dataclass
@dataclass
class CharacterSegment:
"""Represents a single segment for a specific character within a language."""
original_idx: int
character: str
segment_text: str
segment_lang: str
def __str__(self):
return f"Segment({self.original_idx+1}: {self.character})"
class CharacterGroup:
"""A group of segments for the same character within a language."""
def __init__(self, character: str):
self.character = character
self.segments: List[CharacterSegment] = []
def add_segment(self, segment: CharacterSegment):
"""Add a segment to this character group."""
self.segments.append(segment)
def can_batch(self) -> bool:
"""Check if this group has enough segments to warrant batch processing."""
return len(self.segments) > 1
def get_segment_data(self) -> List[Tuple[int, str, str, str]]:
"""Get segment data in the format expected by existing code: (original_idx, character, segment_text, segment_lang)"""
return [(seg.original_idx, seg.character, seg.segment_text, seg.segment_lang) for seg in self.segments]
def __len__(self):
return len(self.segments)
def __str__(self):
indices = [str(seg.original_idx + 1) for seg in self.segments] # 1-based for display
batch_indicator = "🚀" if self.can_batch() else "→"
return f"{batch_indicator} {self.character}({len(self.segments)}): #{','.join(indices)}"
class CharacterGrouper:
"""
Groups segments by character WITHIN a language group.
This works with the existing language grouping system:
EXISTING: language_groups[lang] = [(idx, char, text, lang), ...]
NEW: character_groups = group_by_character(lang_segments)
So we go from:
lang_segments = [(1,'narrator',...), (2,'david',...), (3,'narrator',...)]
To:
character_groups = {
'narrator': CharacterGroup([segment1, segment3]),
'david': CharacterGroup([segment2])
}
"""
@staticmethod
def group_by_character(lang_segments: List[Tuple[int, str, str, str]]) -> Dict[str, CharacterGroup]:
"""
Group segments from a language by character.
Args:
lang_segments: List of (original_idx, character, segment_text, segment_lang)
This comes from the existing language grouping system
Returns:
Dict mapping character_name -> CharacterGroup
"""
character_groups = {}
for original_idx, character, segment_text, segment_lang in lang_segments:
if character not in character_groups:
character_groups[character] = CharacterGroup(character)
segment = CharacterSegment(
original_idx=original_idx,
character=character,
segment_text=segment_text,
segment_lang=segment_lang
)
character_groups[character].add_segment(segment)
return character_groups
@staticmethod
def print_character_grouping_summary(language: str, character_groups: Dict[str, CharacterGroup]):
"""Print summary of character grouping for a language."""
total_segments = sum(len(group) for group in character_groups.values())
batchable_segments = sum(len(group) for group in character_groups.values() if group.can_batch())
group_summaries = [str(group) for group in character_groups.values()]
print(f"🎭 {language} CHARACTER GROUPS ({batchable_segments}/{total_segments} batchable):")
print(f" {' | '.join(group_summaries)}")
@staticmethod
def validate_character_grouping(original_lang_segments: List[Tuple[int, str, str, str]],
character_groups: Dict[str, CharacterGroup]):
"""Validate that character grouping preserves all segments."""
original_count = len(original_lang_segments)
grouped_count = sum(len(group) for group in character_groups.values())
if original_count != grouped_count:
raise RuntimeError(f"Character grouping lost segments: {original_count} -> {grouped_count}")
# Validate all original indices are preserved
original_indices = {idx for idx, _, _, _ in original_lang_segments}
grouped_indices = set()
for group in character_groups.values():
for seg in group.segments:
grouped_indices.add(seg.original_idx)
if original_indices != grouped_indices:
missing = original_indices - grouped_indices
extra = grouped_indices - original_indices
raise RuntimeError(f"Character grouping index mismatch. Missing: {missing}, Extra: {extra}")
print(f" ✅ Character grouping validated: {grouped_count} segments preserved")
@@ -0,0 +1,277 @@
"""
Continuous Worker Processor - True Non-Stop Parallelization for ChatterBox
This implements what the user actually wanted:
- Workers continuously process work without waiting for batches
- No idle workers when user sets high worker counts
- Maintains the beautiful logging structure from the original
- Falls back gracefully to sequential processing for 0-1 workers
"""
import torch
import time
import threading
from queue import Queue, Empty
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from concurrent.futures import ThreadPoolExecutor
import gc
@dataclass
class ContinuousWorkItem:
"""Single work item for continuous processing."""
text_index: int
text: str
temperature: float
cfg_weight: float
exaggeration: float
@dataclass
class ContinuousResult:
"""Result from continuous processing."""
text_index: int
audio: torch.Tensor
processing_time: float
worker_id: int
success: bool = True
error_msg: str = ""
class ContinuousWorkerProcessor:
"""
True continuous worker processor that maintains workers always busy.
Falls back to sequential processing gracefully for 0-1 workers.
Maintains beautiful logging structure.
"""
def __init__(self, tts_model):
self.tts_model = tts_model
def process_texts_continuously(
self,
texts: List[str],
temperature: float,
cfg_weight: float,
exaggeration: float,
max_workers: int
) -> List[torch.Tensor]:
"""
Process texts with continuous workers or fallback to sequential.
Args:
texts: List of texts to process
temperature: Sampling temperature
cfg_weight: CFG weight
exaggeration: Exaggeration factor
max_workers: Number of workers (0-1 = sequential, >1 = continuous)
Returns:
List of audio tensors
"""
if not texts:
return []
# SMART FALLBACK: Use sequential processing for 0-1 workers
if max_workers <= 1:
print(f"→ SEQUENTIAL FALLBACK: {len(texts)} texts (workers={max_workers})")
return self._process_sequential(texts, temperature, cfg_weight, exaggeration)
# Use continuous worker processing for >1 workers
print(f"🌊 CONTINUOUS PROCESSING: {len(texts)} texts with {max_workers} workers")
return self._process_with_continuous_workers(texts, temperature, cfg_weight, exaggeration, max_workers)
def _process_sequential(
self,
texts: List[str],
temperature: float,
cfg_weight: float,
exaggeration: float
) -> List[torch.Tensor]:
"""Sequential processing - clean and organized like the original."""
results = []
for i, text in enumerate(texts):
print(f" 🎤 Sequential {i+1}/{len(texts)}: {text[:40]}...")
try:
audio = self.tts_model.generate(
text=text,
audio_prompt_path=None, # Already prepared
temperature=temperature,
cfg_weight=cfg_weight,
exaggeration=exaggeration
)
results.append(audio)
print(f" ✅ Sequential {i+1}: Completed")
except Exception as e:
print(f" ❌ Sequential {i+1}: Failed - {e}")
results.append(torch.zeros(1, 1000))
print(f"✅ SEQUENTIAL COMPLETE: {len(texts)} texts processed")
return results
def _process_with_continuous_workers(
self,
texts: List[str],
temperature: float,
cfg_weight: float,
exaggeration: float,
max_workers: int
) -> List[torch.Tensor]:
"""
Continuous worker processing - workers never idle.
Maintains beautiful logging structure.
"""
# Build work queue
work_queue = Queue()
result_queue = Queue()
shutdown_event = threading.Event()
# Fill queue with all work
for i, text in enumerate(texts):
work_item = ContinuousWorkItem(
text_index=i,
text=text,
temperature=temperature,
cfg_weight=cfg_weight,
exaggeration=exaggeration
)
work_queue.put(work_item)
print(f"📥 Work queue filled: {len(texts)} items")
# Start workers
workers = []
worker_threads = []
for worker_id in range(max_workers):
worker = ContinuousWorker(
worker_id=worker_id + 1,
work_queue=work_queue,
result_queue=result_queue,
tts_model=self.tts_model,
shutdown_event=shutdown_event
)
thread = threading.Thread(target=worker.run)
thread.start()
workers.append(worker)
worker_threads.append(thread)
print(f"🚀 Started {max_workers} continuous workers")
# Collect results
results = [None] * len(texts)
completed_count = 0
start_time = time.time()
while completed_count < len(texts):
try:
result = result_queue.get(timeout=5.0)
results[result.text_index] = result.audio
completed_count += 1
# Beautiful progress logging (like original)
if completed_count % max(1, len(texts) // 10) == 0:
progress = int(100 * completed_count / len(texts))
elapsed = time.time() - start_time
rate = completed_count / elapsed if elapsed > 0 else 0
print(f"📊 Continuous progress: {completed_count}/{len(texts)} ({progress}%) - {rate:.1f}/sec")
except Empty:
print("⏳ Waiting for continuous workers...")
continue
# Shutdown workers
shutdown_event.set()
# Send shutdown signals
for _ in range(max_workers):
work_queue.put(None)
# Wait for threads
for thread in worker_threads:
thread.join(timeout=3.0)
total_time = time.time() - start_time
throughput = len(texts) / total_time if total_time > 0 else 0
print(f"✅ CONTINUOUS COMPLETE: {len(texts)} texts in {total_time:.1f}s ({throughput:.2f}/sec)")
return results
class ContinuousWorker:
"""Individual continuous worker that processes from shared queue."""
def __init__(self, worker_id: int, work_queue: Queue, result_queue: Queue,
tts_model, shutdown_event: threading.Event):
self.worker_id = worker_id
self.work_queue = work_queue
self.result_queue = result_queue
self.tts_model = tts_model
self.shutdown_event = shutdown_event
self.processed_count = 0
def run(self):
"""Main worker loop - continuously processes work."""
print(f" 🧵 Worker {self.worker_id}: Ready for continuous work")
while not self.shutdown_event.is_set():
try:
# Get next work item
work_item = self.work_queue.get(timeout=1.0)
if work_item is None: # Shutdown signal
break
print(f" 🧵 Worker {self.worker_id}: Processing: {work_item.text[:30]}...")
# Process work item
start_time = time.time()
try:
audio = self.tts_model.generate(
text=work_item.text,
audio_prompt_path=None, # Already prepared
temperature=work_item.temperature,
cfg_weight=work_item.cfg_weight,
exaggeration=work_item.exaggeration
)
processing_time = time.time() - start_time
self.processed_count += 1
result = ContinuousResult(
text_index=work_item.text_index,
audio=audio,
processing_time=processing_time,
worker_id=self.worker_id,
success=True
)
print(f" ✅ Worker {self.worker_id}: Completed in {processing_time:.1f}s")
except Exception as e:
processing_time = time.time() - start_time
result = ContinuousResult(
text_index=work_item.text_index,
audio=torch.zeros(1, 1000),
processing_time=processing_time,
worker_id=self.worker_id,
success=False,
error_msg=str(e)
)
print(f" ❌ Worker {self.worker_id}: Failed - {e}")
# Send result back
self.result_queue.put(result)
self.work_queue.task_done()
except Empty:
# Timeout - check shutdown and continue
continue
except Exception as e:
print(f" ❌ Worker {self.worker_id}: Unexpected error - {e}")
print(f" 🛑 Worker {self.worker_id}: Shutdown ({self.processed_count} processed)")
@@ -0,0 +1,240 @@
"""
GPU-Aware Batch Processor for ChatterBox TTS
Intelligently limits parallel processing based on GPU capabilities to avoid resource contention.
Uses empirical testing to find optimal worker count for maximum throughput.
Modular design following CLAUDE.md guidelines - under 500 lines, focused responsibility.
"""
import torch
import psutil
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Tuple, Optional
import time
class GPUAwareBatchProcessor:
"""
Intelligent batch processor that optimizes worker count based on GPU capabilities.
Instead of using all available workers (which causes resource contention),
this processor finds the optimal balance between parallelism and GPU efficiency.
"""
def __init__(self, tts_model, device: str = "auto"):
"""
Initialize with GPU-aware configuration.
Args:
tts_model: ChatterboxTTS instance
device: Target device ("cuda", "cpu", "auto")
"""
self.tts_model = tts_model
self.device = self._resolve_device(device)
self.optimal_workers = self._determine_optimal_workers()
print(f"🔧 GPU-Aware Processor initialized: {self.optimal_workers} optimal workers for {self.device}")
def _resolve_device(self, device: str) -> str:
"""Resolve device string to actual device."""
from utils.device import resolve_torch_device
return resolve_torch_device(device)
def _determine_optimal_workers(self) -> int:
"""
Determine optimal worker count based on hardware capabilities.
Returns:
Optimal number of parallel workers
"""
if self.device == "cpu":
# CPU-only: use more workers since no GPU contention
return min(psutil.cpu_count() or 4, 8)
# GPU-based optimization
if torch.cuda.is_available():
gpu_memory_gb = torch.cuda.get_device_properties(0).total_memory / (1024**3)
# Empirical optimization based on ChatterBox GPU requirements
if gpu_memory_gb >= 24: # RTX 4090, A6000, etc.
return 4 # Sweet spot for high-end cards
elif gpu_memory_gb >= 16: # RTX 4080, etc.
return 3
elif gpu_memory_gb >= 12: # RTX 4070 Ti, etc.
return 2
elif gpu_memory_gb >= 8: # RTX 4060 Ti, etc.
return 2
else: # < 8GB VRAM
return 1 # Sequential processing safer
return 2 # Conservative fallback
def process_texts_optimized(
self,
texts: List[str],
temperature: float,
cfg_weight: float,
exaggeration: float,
requested_workers: int
) -> List[torch.Tensor]:
"""
Process texts with GPU-optimized worker count.
Args:
texts: List of text strings
temperature: Sampling temperature
cfg_weight: CFG weight
exaggeration: Emotion exaggeration
requested_workers: User-requested worker count (will be clamped to optimal)
Returns:
List of audio tensors in original order
"""
# Use optimal workers regardless of user request for best performance
effective_workers = min(self.optimal_workers, len(texts))
if effective_workers != requested_workers:
print(f"🔧 GPU Optimization: Using {effective_workers} workers instead of {requested_workers} for optimal GPU utilization")
return self._process_with_workers(texts, temperature, cfg_weight, exaggeration, effective_workers)
def _process_with_workers(
self,
texts: List[str],
temperature: float,
cfg_weight: float,
exaggeration: float,
max_workers: int
) -> List[torch.Tensor]:
"""Process texts with specified worker count."""
def _process_single_text(text_index: int, text: str) -> Tuple[int, torch.Tensor]:
"""Process single text with GPU-aware error handling."""
try:
print(f" 🧵 GPU-Worker {text_index+1}: Processing: {text[:30]}...")
# Clear GPU cache before processing to avoid OOM
if self.device.startswith("cuda"):
torch.cuda.empty_cache()
start_time = time.time()
audio = self.tts_model.generate(
text=text,
audio_prompt_path=None,
exaggeration=exaggeration,
cfg_weight=cfg_weight,
temperature=temperature,
)
duration = time.time() - start_time
print(f" ✅ GPU-Worker {text_index+1}: Completed in {duration:.1f}s")
return text_index, audio
except Exception as e:
print(f" ❌ GPU-Worker {text_index+1}: Failed - {e}")
return text_index, torch.zeros(1, 1000)
print(f"🚀 GPU-OPTIMIZED PROCESSING: {len(texts)} texts with {max_workers} optimal GPU workers")
# Process with optimal worker count
results = [None] * len(texts)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [
executor.submit(_process_single_text, i, text)
for i, text in enumerate(texts)
]
completed_count = 0
total_duration = time.time()
for future in as_completed(futures):
text_index, audio = future.result()
results[text_index] = audio
completed_count += 1
progress_percent = int(100 * completed_count / len(texts))
print(f" 📊 Progress: {completed_count}/{len(texts)} completed ({progress_percent}%)")
total_time = time.time() - total_duration
avg_time = total_time / len(texts)
print(f"✅ GPU-OPTIMIZED PROCESSING COMPLETED: {len(results)} texts in {total_time:.1f}s (avg: {avg_time:.1f}s/text)")
return results
def benchmark_worker_counts(self, sample_texts: List[str], max_test_workers: int = 8) -> dict:
"""
Benchmark different worker counts to find empirically optimal settings.
Args:
sample_texts: Representative text samples for benchmarking
max_test_workers: Maximum workers to test
Returns:
Dict with benchmark results
"""
if len(sample_texts) < 2:
print("⚠️ Need at least 2 sample texts for benchmarking")
return {}
print(f"🔬 Benchmarking worker counts 1-{max_test_workers} with {len(sample_texts)} sample texts...")
results = {}
for workers in range(1, min(max_test_workers + 1, len(sample_texts) + 1)):
print(f"🧪 Testing {workers} workers...")
start_time = time.time()
_ = self._process_with_workers(
sample_texts[:workers], 0.8, 0.5, 0.5, workers
)
duration = time.time() - start_time
throughput = workers / duration # texts per second
results[workers] = {
'duration': duration,
'throughput': throughput,
'texts_processed': workers
}
print(f" 📈 {workers} workers: {duration:.1f}s total, {throughput:.2f} texts/sec")
# Find optimal
best_workers = max(results.keys(), key=lambda w: results[w]['throughput'])
best_throughput = results[best_workers]['throughput']
print(f"🏆 OPTIMAL: {best_workers} workers with {best_throughput:.2f} texts/sec throughput")
return results
class ProcessingStrategy:
"""Strategy selector for different processing approaches."""
@staticmethod
def choose_strategy(num_texts: int, requested_workers: int, device: str) -> str:
"""Choose optimal processing strategy."""
if device == "cpu":
return "cpu_parallel"
elif num_texts == 1:
return "single"
elif torch.cuda.is_available():
gpu_memory_gb = torch.cuda.get_device_properties(0).total_memory / (1024**3)
if gpu_memory_gb < 8:
return "sequential" # Low VRAM - avoid parallelism
else:
return "gpu_optimized"
else:
return "sequential"
@staticmethod
def get_strategy_explanation(strategy: str) -> str:
"""Get explanation of chosen strategy."""
explanations = {
"single": "Single text - direct processing",
"sequential": "Sequential processing - low GPU memory",
"cpu_parallel": "CPU parallel processing - no GPU constraints",
"gpu_optimized": "GPU-optimized parallel processing - balanced for throughput"
}
return explanations.get(strategy, "Unknown strategy")
@@ -0,0 +1,282 @@
"""
ChatterBox Official 23-Lang Language Model Registry
Manages the official ResembleAI multilingual ChatterBox model supporting 23 languages
"""
import os
import folder_paths
from typing import Dict, List, Tuple, Optional
# Official 23-language ChatterBox model configuration
# Based on ResembleAI's official multilingual implementation
OFFICIAL_23LANG_MODELS = {
# The single unified multilingual model supporting all 23 languages
"ChatterBox Official 23-Lang": {
"repo": "ResembleAI/chatterbox",
"format": "mixed", # Uses both .pt and .safetensors
"description": "Official ResembleAI multilingual ChatterBox model supporting 23 languages",
"languages": [
"ar", # Arabic
"da", # Danish
"de", # German
"el", # Greek
"en", # English
"es", # Spanish
"fi", # Finnish
"fr", # French
"he", # Hebrew
"hi", # Hindi
"it", # Italian
"ja", # Japanese
"ko", # Korean
"ms", # Malay
"nl", # Dutch
"no", # Norwegian
"pl", # Polish
"pt", # Portuguese
"ru", # Russian
"sv", # Swedish
"sw", # Swahili
"tr", # Turkish
"zh", # Chinese
],
"required_files": {
"v1": [
"t3_23lang.safetensors", # Multilingual T3 model v1
"s3gen.pt", # S3Gen model (same as English)
"ve.pt", # Voice encoder (same as English)
"mtl_tokenizer.json", # Multilingual tokenizer
"conds.pt" # Conditioning (optional)
],
"v2": [
"t3_mtl23ls_v2.safetensors", # Multilingual T3 model v2 with enhanced tokenization
"s3gen.pt", # S3Gen model (same as English)
"ve.pt", # Voice encoder (same as English)
"grapheme_mtl_merged_expanded_v1.json", # Enhanced grapheme/phoneme mappings with special tokens
"mtl_tokenizer.json", # Multilingual tokenizer (may be updated for v2)
"conds.pt" # Conditioning (optional)
]
},
"multilingual": True
}
}
# Language code to display name mapping
SUPPORTED_LANGUAGES = {
"ar": "Arabic",
"da": "Danish",
"de": "German",
"el": "Greek",
"en": "English",
"es": "Spanish",
"fi": "Finnish",
"fr": "French",
"he": "Hebrew",
"hi": "Hindi",
"it": "Italian",
"ja": "Japanese",
"ko": "Korean",
"ms": "Malay",
"nl": "Dutch",
"no": "Norwegian",
"pl": "Polish",
"pt": "Portuguese",
"ru": "Russian",
"sv": "Swedish",
"sw": "Swahili",
"tr": "Turkish",
"zh": "Chinese",
}
def get_official_23lang_models() -> List[str]:
"""
Get list of available official 23-lang models.
This implementation only supports the unified multilingual model.
"""
models = list(OFFICIAL_23LANG_MODELS.keys())
# Check for local models in ComfyUI models directory
try:
# Check both new TTS organization and legacy path
tts_models_dir = os.path.join(folder_paths.models_dir, "TTS", "chatterbox_official_23lang")
legacy_models_dir = os.path.join(folder_paths.models_dir, "chatterbox_official_23lang")
# Try TTS path first, then legacy
for models_dir in [tts_models_dir, legacy_models_dir]:
if not os.path.exists(models_dir):
continue
for item in os.listdir(models_dir):
item_path = os.path.join(models_dir, item)
if os.path.isdir(item_path):
# Check if it contains official 23-lang model files
required_files = ["t3_23lang.safetensors", "s3gen.pt", "ve.pt", "mtl_tokenizer.json"]
has_model = True
for required_file in required_files:
file_path = os.path.join(item_path, required_file)
if not os.path.exists(file_path):
has_model = False
break
if has_model:
local_model = f"local:{item}"
if local_model not in models:
models.append(local_model)
except Exception:
pass # Ignore errors in model discovery
return models
def get_supported_languages() -> List[str]:
"""Get list of supported language codes."""
return list(SUPPORTED_LANGUAGES.keys())
def get_supported_language_names() -> List[str]:
"""Get list of supported language display names."""
return list(SUPPORTED_LANGUAGES.values())
def get_language_name(language_code: str) -> str:
"""Get display name for a language code."""
return SUPPORTED_LANGUAGES.get(language_code, language_code)
def get_model_config(model_name: str) -> Optional[Dict]:
"""Get configuration for a specific model"""
if model_name.startswith("local:"):
# Local model
local_name = model_name[6:] # Remove "local:" prefix
return {
"repo": None,
"format": "mixed", # Auto-detect format
"local_path": os.path.join(folder_paths.models_dir, "TTS", "chatterbox_official_23lang", local_name),
"description": f"Local ChatterBox Official 23-Lang model: {local_name}",
"multilingual": True,
"languages": list(SUPPORTED_LANGUAGES.keys())
}
return OFFICIAL_23LANG_MODELS.get(model_name)
def get_model_files_for_model(model_name: str) -> Tuple[str, str]:
"""
Get the expected file format and repo for a model.
Returns (format, repo_id) tuple.
"""
config = get_model_config(model_name)
if not config:
# Default to the official model
config = OFFICIAL_23LANG_MODELS["ChatterBox Official 23-Lang"]
return config.get("format", "mixed"), config.get("repo")
def find_local_model_path(model_name: str) -> Optional[str]:
"""Find local model path for a given model"""
if model_name.startswith("local:"):
local_name = model_name[6:]
# Try TTS path first, then legacy
for base_dir in ["TTS", ""]:
if base_dir:
model_path = os.path.join(folder_paths.models_dir, base_dir, "chatterbox_official_23lang", local_name)
else:
model_path = os.path.join(folder_paths.models_dir, "chatterbox_official_23lang", local_name)
if os.path.exists(model_path):
return model_path
else:
# Check if we have a local version of a predefined model
# Try TTS path first, then legacy
for base_dir in ["TTS", ""]:
if base_dir:
model_path = os.path.join(folder_paths.models_dir, base_dir, "chatterbox_official_23lang", model_name)
else:
model_path = os.path.join(folder_paths.models_dir, "chatterbox_official_23lang", model_name)
if os.path.exists(model_path):
return model_path
return None
def detect_model_format(model_path: str) -> str:
"""
Auto-detect the format of models in a directory.
Returns 'safetensors', 'pt', or 'mixed'
"""
if not os.path.exists(model_path):
return "mixed" # Default format for official model
has_safetensors = False
has_pt = False
for file in os.listdir(model_path):
if file.endswith(".safetensors"):
has_safetensors = True
elif file.endswith(".pt"):
has_pt = True
if has_safetensors and has_pt:
return "mixed"
elif has_safetensors:
return "safetensors"
else:
return "pt"
def get_model_requirements(model_name: str, model_version: str = "v1") -> List[str]:
"""Get list of required files for the official 23-lang model based on version"""
config = get_model_config(model_name)
if config and "required_files" in config:
required_files = config["required_files"]
# Handle version-specific requirements
if isinstance(required_files, dict):
return required_files.get(model_version, required_files.get("v1", []))
else:
# Legacy format - return as-is
return required_files
# Default requirements for official model v1
return [
"t3_23lang.safetensors",
"s3gen.pt",
"ve.pt",
"mtl_tokenizer.json",
"conds.pt" # Optional but expected
]
def validate_model_completeness(model_path: str, model_name: str, model_version: str = "v1") -> Tuple[bool, List[str]]:
"""
Validate if a model has all required components.
Returns (is_complete, missing_files)
"""
if not os.path.exists(model_path):
return False, ["Model directory not found"]
required_files = get_model_requirements(model_name, model_version)
missing_files = []
optional_files = ["conds.pt", "grapheme_mtl_merged_expanded_v1.json"] # These are optional
existing_files = os.listdir(model_path)
for required_file in required_files:
# Check for exact file name
if required_file not in existing_files:
# Only consider it missing if it's not optional
if required_file not in optional_files:
missing_files.append(required_file)
return len(missing_files) == 0, missing_files
def supports_voice_conversion(model_name: str) -> bool:
"""
Check if a model supports voice conversion.
Official 23-lang model supports VC via S3Gen.
"""
return True # Official model supports VC
def get_available_models() -> List[str]:
"""Get list of available model names for display"""
return get_official_23lang_models()
# Legacy compatibility functions (renamed from original chatterbox functions)
def get_chatterbox_models():
"""Legacy compatibility - redirect to official models"""
return get_official_23lang_models()
def get_available_languages():
"""Legacy compatibility - redirect to supported languages"""
return get_supported_language_names()
@@ -0,0 +1,2 @@
from .s3gen import S3Token2Wav as S3Gen
from .const import S3GEN_SR
@@ -0,0 +1,10 @@
from ..utils import AttrDict
CFM_PARAMS = AttrDict({
"sigma_min": 1e-06,
"solver": "euler",
"t_scheduler": "cosine",
"training_cfg_rate": 0.2,
"inference_cfg_rate": 0.7,
"reg_loss_type": "l1"
})
@@ -0,0 +1 @@
S3GEN_SR = 24000
@@ -0,0 +1,317 @@
# Copyright (c) 2024 Alibaba Inc (authors: Xiang Lyu, Zhihao Du)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import pack, rearrange, repeat
from .utils.mask import add_optional_chunk_mask
from .matcha.decoder import SinusoidalPosEmb, Block1D, ResnetBlock1D, Downsample1D, \
TimestepEmbedding, Upsample1D
from .matcha.transformer import BasicTransformerBlock
def mask_to_bias(mask: torch.Tensor, dtype: torch.dtype) -> torch.Tensor:
assert mask.dtype == torch.bool
assert dtype in [torch.float32, torch.bfloat16, torch.float16]
mask = mask.to(dtype)
# attention mask bias
# NOTE(Mddct): torch.finfo jit issues
# chunk_masks = (1.0 - chunk_masks) * torch.finfo(dtype).min
mask = (1.0 - mask) * -1.0e+10
return mask
class Transpose(torch.nn.Module):
def __init__(self, dim0: int, dim1: int):
super().__init__()
self.dim0 = dim0
self.dim1 = dim1
def forward(self, x: torch.Tensor):
x = torch.transpose(x, self.dim0, self.dim1)
return x
class CausalBlock1D(Block1D):
def __init__(self, dim: int, dim_out: int):
super(CausalBlock1D, self).__init__(dim, dim_out)
self.block = torch.nn.Sequential(
CausalConv1d(dim, dim_out, 3),
Transpose(1, 2),
nn.LayerNorm(dim_out),
Transpose(1, 2),
nn.Mish(),
)
def forward(self, x: torch.Tensor, mask: torch.Tensor):
output = self.block(x * mask)
return output * mask
class CausalResnetBlock1D(ResnetBlock1D):
def __init__(self, dim: int, dim_out: int, time_emb_dim: int, groups: int = 8):
super(CausalResnetBlock1D, self).__init__(dim, dim_out, time_emb_dim, groups)
self.block1 = CausalBlock1D(dim, dim_out)
self.block2 = CausalBlock1D(dim_out, dim_out)
class CausalConv1d(torch.nn.Conv1d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int,
stride: int = 1,
dilation: int = 1,
groups: int = 1,
bias: bool = True,
padding_mode: str = 'zeros',
device=None,
dtype=None
) -> None:
super(CausalConv1d, self).__init__(in_channels, out_channels,
kernel_size, stride,
padding=0, dilation=dilation,
groups=groups, bias=bias,
padding_mode=padding_mode,
device=device, dtype=dtype)
assert stride == 1
self.causal_padding = (kernel_size - 1, 0)
def forward(self, x: torch.Tensor):
x = F.pad(x, self.causal_padding)
x = super(CausalConv1d, self).forward(x)
return x
class ConditionalDecoder(nn.Module):
def __init__(
self,
in_channels=320,
out_channels=80,
causal=True,
channels=[256],
dropout=0.0,
attention_head_dim=64,
n_blocks=4,
num_mid_blocks=12,
num_heads=8,
act_fn="gelu",
):
"""
This decoder requires an input with the same shape of the target. So, if your text content
is shorter or longer than the outputs, please re-sampling it before feeding to the decoder.
"""
super().__init__()
channels = tuple(channels)
self.in_channels = in_channels
self.out_channels = out_channels
self.causal = causal
self.time_embeddings = SinusoidalPosEmb(in_channels)
time_embed_dim = channels[0] * 4
self.time_mlp = TimestepEmbedding(
in_channels=in_channels,
time_embed_dim=time_embed_dim,
act_fn="silu",
)
self.down_blocks = nn.ModuleList([])
self.mid_blocks = nn.ModuleList([])
self.up_blocks = nn.ModuleList([])
# NOTE jrm: `static_chunk_size` is missing?
self.static_chunk_size = 0
output_channel = in_channels
for i in range(len(channels)): # pylint: disable=consider-using-enumerate
input_channel = output_channel
output_channel = channels[i]
is_last = i == len(channels) - 1
resnet = CausalResnetBlock1D(dim=input_channel, dim_out=output_channel, time_emb_dim=time_embed_dim) if self.causal else \
ResnetBlock1D(dim=input_channel, dim_out=output_channel, time_emb_dim=time_embed_dim)
transformer_blocks = nn.ModuleList(
[
BasicTransformerBlock(
dim=output_channel,
num_attention_heads=num_heads,
attention_head_dim=attention_head_dim,
dropout=dropout,
activation_fn=act_fn,
)
for _ in range(n_blocks)
]
)
downsample = (
Downsample1D(output_channel) if not is_last else
CausalConv1d(output_channel, output_channel, 3) if self.causal else nn.Conv1d(output_channel, output_channel, 3, padding=1)
)
self.down_blocks.append(nn.ModuleList([resnet, transformer_blocks, downsample]))
for _ in range(num_mid_blocks):
input_channel = channels[-1]
out_channels = channels[-1]
resnet = CausalResnetBlock1D(dim=input_channel, dim_out=output_channel, time_emb_dim=time_embed_dim) if self.causal else \
ResnetBlock1D(dim=input_channel, dim_out=output_channel, time_emb_dim=time_embed_dim)
transformer_blocks = nn.ModuleList(
[
BasicTransformerBlock(
dim=output_channel,
num_attention_heads=num_heads,
attention_head_dim=attention_head_dim,
dropout=dropout,
activation_fn=act_fn,
)
for _ in range(n_blocks)
]
)
self.mid_blocks.append(nn.ModuleList([resnet, transformer_blocks]))
channels = channels[::-1] + (channels[0],)
for i in range(len(channels) - 1):
input_channel = channels[i] * 2
output_channel = channels[i + 1]
is_last = i == len(channels) - 2
resnet = CausalResnetBlock1D(
dim=input_channel,
dim_out=output_channel,
time_emb_dim=time_embed_dim,
) if self.causal else ResnetBlock1D(
dim=input_channel,
dim_out=output_channel,
time_emb_dim=time_embed_dim,
)
transformer_blocks = nn.ModuleList(
[
BasicTransformerBlock(
dim=output_channel,
num_attention_heads=num_heads,
attention_head_dim=attention_head_dim,
dropout=dropout,
activation_fn=act_fn,
)
for _ in range(n_blocks)
]
)
upsample = (
Upsample1D(output_channel, use_conv_transpose=True)
if not is_last
else CausalConv1d(output_channel, output_channel, 3) if self.causal else nn.Conv1d(output_channel, output_channel, 3, padding=1)
)
self.up_blocks.append(nn.ModuleList([resnet, transformer_blocks, upsample]))
self.final_block = CausalBlock1D(channels[-1], channels[-1]) if self.causal else Block1D(channels[-1], channels[-1])
self.final_proj = nn.Conv1d(channels[-1], self.out_channels, 1)
self.initialize_weights()
def initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv1d):
nn.init.kaiming_normal_(m.weight, nonlinearity="relu")
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.GroupNorm):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight, nonlinearity="relu")
if m.bias is not None:
nn.init.constant_(m.bias, 0)
def forward(self, x, mask, mu, t, spks=None, cond=None):
"""Forward pass of the UNet1DConditional model.
Args:
x (torch.Tensor): shape (batch_size, in_channels, time)
mask (_type_): shape (batch_size, 1, time)
t (_type_): shape (batch_size)
spks (_type_, optional): shape: (batch_size, condition_channels). Defaults to None.
cond (_type_, optional): placeholder for future use. Defaults to None.
Raises:
ValueError: _description_
ValueError: _description_
Returns:
_type_: _description_
"""
t = self.time_embeddings(t).to(t.dtype)
t = self.time_mlp(t)
x = pack([x, mu], "b * t")[0]
if spks is not None:
spks = repeat(spks, "b c -> b c t", t=x.shape[-1])
x = pack([x, spks], "b * t")[0]
if cond is not None:
x = pack([x, cond], "b * t")[0]
hiddens = []
masks = [mask]
for resnet, transformer_blocks, downsample in self.down_blocks:
mask_down = masks[-1]
x = resnet(x, mask_down, t)
x = rearrange(x, "b c t -> b t c").contiguous()
# attn_mask = torch.matmul(mask_down.transpose(1, 2).contiguous(), mask_down)
attn_mask = add_optional_chunk_mask(x, mask_down.bool(), False, False, 0, self.static_chunk_size, -1)
attn_mask = mask_to_bias(attn_mask == 1, x.dtype)
for transformer_block in transformer_blocks:
x = transformer_block(
hidden_states=x,
attention_mask=attn_mask,
timestep=t,
)
x = rearrange(x, "b t c -> b c t").contiguous()
hiddens.append(x) # Save hidden states for skip connections
x = downsample(x * mask_down)
masks.append(mask_down[:, :, ::2])
masks = masks[:-1]
mask_mid = masks[-1]
for resnet, transformer_blocks in self.mid_blocks:
x = resnet(x, mask_mid, t)
x = rearrange(x, "b c t -> b t c").contiguous()
# attn_mask = torch.matmul(mask_mid.transpose(1, 2).contiguous(), mask_mid)
attn_mask = add_optional_chunk_mask(x, mask_mid.bool(), False, False, 0, self.static_chunk_size, -1)
attn_mask = mask_to_bias(attn_mask == 1, x.dtype)
for transformer_block in transformer_blocks:
x = transformer_block(
hidden_states=x,
attention_mask=attn_mask,
timestep=t,
)
x = rearrange(x, "b t c -> b c t").contiguous()
for resnet, transformer_blocks, upsample in self.up_blocks:
mask_up = masks.pop()
skip = hiddens.pop()
x = pack([x[:, :, :skip.shape[-1]], skip], "b * t")[0]
x = resnet(x, mask_up, t)
x = rearrange(x, "b c t -> b t c").contiguous()
# attn_mask = torch.matmul(mask_up.transpose(1, 2).contiguous(), mask_up)
attn_mask = add_optional_chunk_mask(x, mask_up.bool(), False, False, 0, self.static_chunk_size, -1)
attn_mask = mask_to_bias(attn_mask == 1, x.dtype)
for transformer_block in transformer_blocks:
x = transformer_block(
hidden_states=x,
attention_mask=attn_mask,
timestep=t,
)
x = rearrange(x, "b t c -> b c t").contiguous()
x = upsample(x * mask_up)
x = self.final_block(x, mask_up)
output = self.final_proj(x * mask_up)
return output * mask
@@ -0,0 +1,55 @@
# Copyright (c) 2024 Alibaba Inc (authors: Xiang Lyu, Kai Hu)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import torch
import torch.nn as nn
from torch.nn.utils.parametrizations import weight_norm
class ConvRNNF0Predictor(nn.Module):
def __init__(self,
num_class: int = 1,
in_channels: int = 80,
cond_channels: int = 512
):
super().__init__()
self.num_class = num_class
self.condnet = nn.Sequential(
weight_norm(
nn.Conv1d(in_channels, cond_channels, kernel_size=3, padding=1)
),
nn.ELU(),
weight_norm(
nn.Conv1d(cond_channels, cond_channels, kernel_size=3, padding=1)
),
nn.ELU(),
weight_norm(
nn.Conv1d(cond_channels, cond_channels, kernel_size=3, padding=1)
),
nn.ELU(),
weight_norm(
nn.Conv1d(cond_channels, cond_channels, kernel_size=3, padding=1)
),
nn.ELU(),
weight_norm(
nn.Conv1d(cond_channels, cond_channels, kernel_size=3, padding=1)
),
nn.ELU(),
)
self.classifier = nn.Linear(in_features=cond_channels, out_features=self.num_class)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.condnet(x)
x = x.transpose(1, 2)
return torch.abs(self.classifier(x).squeeze(-1))
@@ -0,0 +1,290 @@
# Copyright (c) 2024 Alibaba Inc (authors: Xiang Lyu, Zhihao Du)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import random
from typing import Dict, Optional
logger = logging.getLogger(__name__)
import torch
import torch.nn as nn
from torch.nn import functional as F
from .utils.mask import make_pad_mask
from .configs import CFM_PARAMS
class MaskedDiffWithXvec(torch.nn.Module):
def __init__(
self,
input_size: int = 512,
output_size: int = 80,
spk_embed_dim: int = 192,
output_type: str = "mel",
vocab_size: int = 4096,
input_frame_rate: int = 50,
only_mask_loss: bool = True,
encoder: torch.nn.Module = None,
length_regulator: torch.nn.Module = None,
decoder: torch.nn.Module = None,
decoder_conf: Dict = {
'in_channels': 240,
'out_channel': 80,
'spk_emb_dim': 80,
'n_spks': 1,
'cfm_params': CFM_PARAMS,
'decoder_params': {
'channels': [256, 256],
'dropout': 0.0,
'attention_head_dim': 64,
'n_blocks': 4,
'num_mid_blocks': 12,
'num_heads': 8,
'act_fn': 'gelu',
}
},
mel_feat_conf: Dict = {
'n_fft': 1024,
'num_mels': 80,
'sampling_rate': 22050,
'hop_size': 256,
'win_size': 1024,
'fmin': 0,
'fmax': 8000
}
):
super().__init__()
self.input_size = input_size
self.output_size = output_size
self.decoder_conf = decoder_conf
self.mel_feat_conf = mel_feat_conf
self.vocab_size = vocab_size
self.output_type = output_type
self.input_frame_rate = input_frame_rate
logging.info(f"input frame rate={self.input_frame_rate}")
self.input_embedding = nn.Embedding(vocab_size, input_size)
self.spk_embed_affine_layer = torch.nn.Linear(spk_embed_dim, output_size)
self.encoder = encoder
self.encoder_proj = torch.nn.Linear(self.encoder.output_size(), output_size)
self.decoder = decoder
self.length_regulator = length_regulator
self.only_mask_loss = only_mask_loss
def forward(
self,
batch: dict,
device: torch.device,
) -> Dict[str, Optional[torch.Tensor]]:
token = batch['speech_token'].to(device)
token_len = batch['speech_token_len'].to(device)
feat = batch['speech_feat'].to(device)
feat_len = batch['speech_feat_len'].to(device)
embedding = batch['embedding'].to(device)
# xvec projection
embedding = F.normalize(embedding, dim=1)
embedding = self.spk_embed_affine_layer(embedding)
# concat text and prompt_text
mask = (~make_pad_mask(token_len)).float().unsqueeze(-1).to(device)
token = self.input_embedding(torch.clamp(token, min=0, max=self.input_embedding.num_embeddings-1)) * mask
# text encode
h, h_lengths = self.encoder(token, token_len)
h = self.encoder_proj(h)
h, h_lengths = self.length_regulator(h, feat_len)
# get conditions
conds = torch.zeros(feat.shape, device=token.device)
for i, j in enumerate(feat_len):
if random.random() < 0.5:
continue
index = random.randint(0, int(0.3 * j))
conds[i, :index] = feat[i, :index]
conds = conds.transpose(1, 2)
mask = (~make_pad_mask(feat_len)).to(h)
feat = F.interpolate(feat.unsqueeze(dim=1), size=h.shape[1:], mode="nearest").squeeze(dim=1)
loss, _ = self.decoder.compute_loss(
feat.transpose(1, 2).contiguous(),
mask.unsqueeze(1),
h.transpose(1, 2).contiguous(),
embedding,
cond=conds
)
return {'loss': loss}
@torch.inference_mode()
def inference(self,
token,
token_len,
prompt_token,
prompt_token_len,
prompt_feat,
prompt_feat_len,
embedding,
flow_cache):
if self.fp16 is True:
prompt_feat = prompt_feat.half()
embedding = embedding.half()
assert token.shape[0] == 1
# xvec projection
embedding = F.normalize(embedding, dim=1)
embedding = self.spk_embed_affine_layer(embedding)
# concat text and prompt_text
token_len1, token_len2 = prompt_token.shape[1], token.shape[1]
token, token_len = torch.concat([prompt_token, token], dim=1), prompt_token_len + token_len
mask = (~make_pad_mask(token_len)).unsqueeze(-1).to(embedding)
# Check for out-of-bounds token IDs
vocab_size = self.input_embedding.num_embeddings
if token.max() >= vocab_size or token.min() < 0:
logging.warning(f"S3Gen: Token IDs out of bounds: min={token.min().item()}, max={token.max().item()}, vocab_size={vocab_size}")
token = self.input_embedding(torch.clamp(token, min=0, max=vocab_size-1)) * mask
# text encode
h, h_lengths = self.encoder(token, token_len)
h = self.encoder_proj(h)
mel_len1, mel_len2 = prompt_feat.shape[1], int(token_len2 / self.input_frame_rate * 22050 / 256)
h, h_lengths = self.length_regulator.inference(h[:, :token_len1], h[:, token_len1:], mel_len1, mel_len2, self.input_frame_rate)
# get conditions
conds = torch.zeros([1, mel_len1 + mel_len2, self.output_size], device=token.device).to(h.dtype)
conds[:, :mel_len1] = prompt_feat
conds = conds.transpose(1, 2)
mask = (~make_pad_mask(torch.tensor([mel_len1 + mel_len2]))).to(h)
feat, flow_cache = self.decoder(
mu=h.transpose(1, 2).contiguous(),
mask=mask.unsqueeze(1),
spks=embedding,
cond=conds,
n_timesteps=10,
prompt_len=mel_len1,
flow_cache=flow_cache
)
feat = feat[:, :, mel_len1:]
assert feat.shape[2] == mel_len2
return feat.float(), flow_cache
class CausalMaskedDiffWithXvec(torch.nn.Module):
def __init__(
self,
input_size: int = 512,
output_size: int = 80,
spk_embed_dim: int = 192,
output_type: str = "mel",
vocab_size: int = 6561,
input_frame_rate: int = 25,
only_mask_loss: bool = True,
token_mel_ratio: int = 2,
pre_lookahead_len: int = 3,
encoder: torch.nn.Module = None,
decoder: torch.nn.Module = None,
decoder_conf: Dict = {
'in_channels': 240,
'out_channel': 80,
'spk_emb_dim': 80,
'n_spks': 1,
'cfm_params': CFM_PARAMS,
'decoder_params': {
'channels': [256, 256],
'dropout': 0.0,
'attention_head_dim': 64,
'n_blocks': 4,
'num_mid_blocks': 12,
'num_heads': 8,
'act_fn': 'gelu',
}
},
mel_feat_conf: Dict = {
'n_fft': 1024,
'num_mels': 80,
'sampling_rate': 22050,
'hop_size': 256,
'win_size': 1024,
'fmin': 0,
'fmax': 8000
}
):
super().__init__()
self.input_size = input_size
self.output_size = output_size
self.decoder_conf = decoder_conf
self.mel_feat_conf = mel_feat_conf
self.vocab_size = vocab_size
self.output_type = output_type
self.input_frame_rate = input_frame_rate
logging.info(f"input frame rate={self.input_frame_rate}")
self.input_embedding = nn.Embedding(vocab_size, input_size)
self.spk_embed_affine_layer = torch.nn.Linear(spk_embed_dim, output_size)
self.encoder = encoder
self.encoder_proj = torch.nn.Linear(self.encoder.output_size(), output_size)
self.decoder = decoder
self.only_mask_loss = only_mask_loss
self.token_mel_ratio = token_mel_ratio
self.pre_lookahead_len = pre_lookahead_len
# FIXME: this was missing - just putting it in as false
self.fp16 = False
@torch.inference_mode()
def inference(self,
token,
token_len,
prompt_token,
prompt_token_len,
prompt_feat,
prompt_feat_len,
embedding,
finalize):
if self.fp16 is True:
prompt_feat = prompt_feat.half()
embedding = embedding.half()
assert token.shape[0] == 1
# xvec projection
embedding = F.normalize(embedding, dim=1)
embedding = self.spk_embed_affine_layer(embedding)
# concat text and prompt_text
token, token_len = torch.concat([prompt_token, token], dim=1), prompt_token_len + token_len
mask = (~make_pad_mask(token_len)).unsqueeze(-1).to(embedding)
token = self.input_embedding(torch.clamp(token, min=0, max=self.input_embedding.num_embeddings-1)) * mask
# text encode
h, h_lengths = self.encoder(token, token_len)
if finalize is False:
h = h[:, :-self.pre_lookahead_len * self.token_mel_ratio]
mel_len1, mel_len2 = prompt_feat.shape[1], h.shape[1] - prompt_feat.shape[1]
h = self.encoder_proj(h)
# get conditions
conds = torch.zeros([1, mel_len1 + mel_len2, self.output_size], device=token.device).to(h.dtype)
conds[:, :mel_len1] = prompt_feat
conds = conds.transpose(1, 2)
mask = (~make_pad_mask(torch.tensor([mel_len1 + mel_len2]))).to(h)
feat, _ = self.decoder(
mu=h.transpose(1, 2).contiguous(),
mask=mask.unsqueeze(1),
spks=embedding,
cond=conds,
n_timesteps=10
)
feat = feat[:, :, mel_len1:]
assert feat.shape[2] == mel_len2
return feat.float(), None # NOTE jrm: why are they returning None here?
@@ -0,0 +1,218 @@
# Copyright (c) 2024 Alibaba Inc (authors: Xiang Lyu, Zhihao Du)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import threading
import torch
import torch.nn.functional as F
from .matcha.flow_matching import BASECFM
from .configs import CFM_PARAMS
class ConditionalCFM(BASECFM):
def __init__(self, in_channels, cfm_params, n_spks=1, spk_emb_dim=64, estimator: torch.nn.Module = None):
super().__init__(
n_feats=in_channels,
cfm_params=cfm_params,
n_spks=n_spks,
spk_emb_dim=spk_emb_dim,
)
self.t_scheduler = cfm_params.t_scheduler
self.training_cfg_rate = cfm_params.training_cfg_rate
self.inference_cfg_rate = cfm_params.inference_cfg_rate
in_channels = in_channels + (spk_emb_dim if n_spks > 0 else 0)
# Just change the architecture of the estimator here
self.estimator = estimator
self.lock = threading.Lock()
@torch.inference_mode()
def forward(self, mu, mask, n_timesteps, temperature=1.0, spks=None, cond=None, prompt_len=0, flow_cache=torch.zeros(1, 80, 0, 2)):
"""Forward diffusion
Args:
mu (torch.Tensor): output of encoder
shape: (batch_size, n_feats, mel_timesteps)
mask (torch.Tensor): output_mask
shape: (batch_size, 1, mel_timesteps)
n_timesteps (int): number of diffusion steps
temperature (float, optional): temperature for scaling noise. Defaults to 1.0.
spks (torch.Tensor, optional): speaker ids. Defaults to None.
shape: (batch_size, spk_emb_dim)
cond: Not used but kept for future purposes
Returns:
sample: generated mel-spectrogram
shape: (batch_size, n_feats, mel_timesteps)
"""
z = torch.randn_like(mu).to(mu.device).to(mu.dtype) * temperature
cache_size = flow_cache.shape[2]
# fix prompt and overlap part mu and z
if cache_size != 0:
z[:, :, :cache_size] = flow_cache[:, :, :, 0]
mu[:, :, :cache_size] = flow_cache[:, :, :, 1]
z_cache = torch.concat([z[:, :, :prompt_len], z[:, :, -34:]], dim=2)
mu_cache = torch.concat([mu[:, :, :prompt_len], mu[:, :, -34:]], dim=2)
flow_cache = torch.stack([z_cache, mu_cache], dim=-1)
t_span = torch.linspace(0, 1, n_timesteps + 1, device=mu.device, dtype=mu.dtype)
if self.t_scheduler == 'cosine':
t_span = 1 - torch.cos(t_span * 0.5 * torch.pi)
return self.solve_euler(z, t_span=t_span, mu=mu, mask=mask, spks=spks, cond=cond), flow_cache
def solve_euler(self, x, t_span, mu, mask, spks, cond):
"""
Fixed euler solver for ODEs.
Args:
x (torch.Tensor): random noise
t_span (torch.Tensor): n_timesteps interpolated
shape: (n_timesteps + 1,)
mu (torch.Tensor): output of encoder
shape: (batch_size, n_feats, mel_timesteps)
mask (torch.Tensor): output_mask
shape: (batch_size, 1, mel_timesteps)
spks (torch.Tensor, optional): speaker ids. Defaults to None.
shape: (batch_size, spk_emb_dim)
cond: Not used but kept for future purposes
"""
t, _, dt = t_span[0], t_span[-1], t_span[1] - t_span[0]
t = t.unsqueeze(dim=0)
# I am storing this because I can later plot it by putting a debugger here and saving it to a file
# Or in future might add like a return_all_steps flag
sol = []
# Do not use concat, it may cause memory format changed and trt infer with wrong results!
x_in = torch.zeros([2, 80, x.size(2)], device=x.device, dtype=x.dtype)
mask_in = torch.zeros([2, 1, x.size(2)], device=x.device, dtype=x.dtype)
mu_in = torch.zeros([2, 80, x.size(2)], device=x.device, dtype=x.dtype)
t_in = torch.zeros([2], device=x.device, dtype=x.dtype)
spks_in = torch.zeros([2, 80], device=x.device, dtype=x.dtype)
cond_in = torch.zeros([2, 80, x.size(2)], device=x.device, dtype=x.dtype)
for step in range(1, len(t_span)):
# Classifier-Free Guidance inference introduced in VoiceBox
x_in[:] = x
mask_in[:] = mask
mu_in[0] = mu
t_in[:] = t.unsqueeze(0)
spks_in[0] = spks
cond_in[0] = cond
dphi_dt = self.forward_estimator(
x_in, mask_in,
mu_in, t_in,
spks_in,
cond_in
)
dphi_dt, cfg_dphi_dt = torch.split(dphi_dt, [x.size(0), x.size(0)], dim=0)
dphi_dt = ((1.0 + self.inference_cfg_rate) * dphi_dt - self.inference_cfg_rate * cfg_dphi_dt)
x = x + dt * dphi_dt
t = t + dt
sol.append(x)
if step < len(t_span) - 1:
dt = t_span[step + 1] - t
return sol[-1].float()
def forward_estimator(self, x, mask, mu, t, spks, cond):
if isinstance(self.estimator, torch.nn.Module):
return self.estimator.forward(x, mask, mu, t, spks, cond)
else:
with self.lock:
self.estimator.set_input_shape('x', (2, 80, x.size(2)))
self.estimator.set_input_shape('mask', (2, 1, x.size(2)))
self.estimator.set_input_shape('mu', (2, 80, x.size(2)))
self.estimator.set_input_shape('t', (2,))
self.estimator.set_input_shape('spks', (2, 80))
self.estimator.set_input_shape('cond', (2, 80, x.size(2)))
# run trt engine
self.estimator.execute_v2([x.contiguous().data_ptr(),
mask.contiguous().data_ptr(),
mu.contiguous().data_ptr(),
t.contiguous().data_ptr(),
spks.contiguous().data_ptr(),
cond.contiguous().data_ptr(),
x.data_ptr()])
return x
def compute_loss(self, x1, mask, mu, spks=None, cond=None):
"""Computes diffusion loss
Args:
x1 (torch.Tensor): Target
shape: (batch_size, n_feats, mel_timesteps)
mask (torch.Tensor): target mask
shape: (batch_size, 1, mel_timesteps)
mu (torch.Tensor): output of encoder
shape: (batch_size, n_feats, mel_timesteps)
spks (torch.Tensor, optional): speaker embedding. Defaults to None.
shape: (batch_size, spk_emb_dim)
Returns:
loss: conditional flow matching loss
y: conditional flow
shape: (batch_size, n_feats, mel_timesteps)
"""
b, _, t = mu.shape
# random timestep
t = torch.rand([b, 1, 1], device=mu.device, dtype=mu.dtype)
if self.t_scheduler == 'cosine':
t = 1 - torch.cos(t * 0.5 * torch.pi)
# sample noise p(x_0)
z = torch.randn_like(x1)
y = (1 - (1 - self.sigma_min) * t) * z + t * x1
u = x1 - (1 - self.sigma_min) * z
# during training, we randomly drop condition to trade off mode coverage and sample fidelity
if self.training_cfg_rate > 0:
cfg_mask = torch.rand(b, device=x1.device) > self.training_cfg_rate
mu = mu * cfg_mask.view(-1, 1, 1)
spks = spks * cfg_mask.view(-1, 1)
cond = cond * cfg_mask.view(-1, 1, 1)
pred = self.estimator(y, mask, mu, t.squeeze(), spks, cond)
loss = F.mse_loss(pred * mask, u * mask, reduction="sum") / (torch.sum(mask) * u.shape[1])
return loss, y
class CausalConditionalCFM(ConditionalCFM):
def __init__(self, in_channels=240, cfm_params=CFM_PARAMS, n_spks=1, spk_emb_dim=80, estimator=None):
super().__init__(in_channels, cfm_params, n_spks, spk_emb_dim, estimator)
self.rand_noise = torch.randn([1, 80, 50 * 300])
@torch.inference_mode()
def forward(self, mu, mask, n_timesteps, temperature=1.0, spks=None, cond=None):
"""Forward diffusion
Args:
mu (torch.Tensor): output of encoder
shape: (batch_size, n_feats, mel_timesteps)
mask (torch.Tensor): output_mask
shape: (batch_size, 1, mel_timesteps)
n_timesteps (int): number of diffusion steps
temperature (float, optional): temperature for scaling noise. Defaults to 1.0.
spks (torch.Tensor, optional): speaker ids. Defaults to None.
shape: (batch_size, spk_emb_dim)
cond: Not used but kept for future purposes
Returns:
sample: generated mel-spectrogram
shape: (batch_size, n_feats, mel_timesteps)
"""
z = self.rand_noise[:, :, :mu.size(2)].to(mu.device).to(mu.dtype) * temperature
# fix prompt and overlap part mu and z
t_span = torch.linspace(0, 1, n_timesteps + 1, device=mu.device, dtype=mu.dtype)
if self.t_scheduler == 'cosine':
t_span = 1 - torch.cos(t_span * 0.5 * torch.pi)
return self.solve_euler(z, t_span=t_span, mu=mu, mask=mask, spks=spks, cond=cond), None
@@ -0,0 +1,474 @@
# jrm: adapted from CosyVoice/cosyvoice/hifigan/generator.py
# most modules should be reusable, but I found their SineGen changed a git.
# Copyright (c) 2024 Alibaba Inc (authors: Xiang Lyu, Kai Hu)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""HIFI-GAN"""
from typing import Dict, Optional, List
import numpy as np
from scipy.signal import get_window
import torch
import torch.nn.functional as F
from torch.nn import Conv1d
from torch.nn import ConvTranspose1d
from torch.nn.utils import remove_weight_norm
from torch.nn.utils.parametrizations import weight_norm
from torch.distributions.uniform import Uniform
from torch import nn, sin, pow
from torch.nn import Parameter
class Snake(nn.Module):
'''
Implementation of a sine-based periodic activation function
Shape:
- Input: (B, C, T)
- Output: (B, C, T), same shape as the input
Parameters:
- alpha - trainable parameter
References:
- This activation function is from this paper by Liu Ziyin, Tilman Hartwig, Masahito Ueda:
https://arxiv.org/abs/2006.08195
Examples:
>>> a1 = snake(256)
>>> x = torch.randn(256)
>>> x = a1(x)
'''
def __init__(self, in_features, alpha=1.0, alpha_trainable=True, alpha_logscale=False):
'''
Initialization.
INPUT:
- in_features: shape of the input
- alpha: trainable parameter
alpha is initialized to 1 by default, higher values = higher-frequency.
alpha will be trained along with the rest of your model.
'''
super(Snake, self).__init__()
self.in_features = in_features
# initialize alpha
self.alpha_logscale = alpha_logscale
if self.alpha_logscale: # log scale alphas initialized to zeros
self.alpha = Parameter(torch.zeros(in_features) * alpha)
else: # linear scale alphas initialized to ones
self.alpha = Parameter(torch.ones(in_features) * alpha)
self.alpha.requires_grad = alpha_trainable
self.no_div_by_zero = 0.000000001
def forward(self, x):
'''
Forward pass of the function.
Applies the function to the input elementwise.
Snake ∶= x + 1/a * sin^2 (xa)
'''
alpha = self.alpha.unsqueeze(0).unsqueeze(-1) # line up with x to [B, C, T]
if self.alpha_logscale:
alpha = torch.exp(alpha)
x = x + (1.0 / (alpha + self.no_div_by_zero)) * pow(sin(x * alpha), 2)
return x
def get_padding(kernel_size, dilation=1):
return int((kernel_size * dilation - dilation) / 2)
def init_weights(m, mean=0.0, std=0.01):
classname = m.__class__.__name__
if classname.find("Conv") != -1:
m.weight.data.normal_(mean, std)
"""hifigan based generator implementation.
This code is modified from https://github.com/jik876/hifi-gan
,https://github.com/kan-bayashi/ParallelWaveGAN and
https://github.com/NVIDIA/BigVGAN
"""
class ResBlock(torch.nn.Module):
"""Residual block module in HiFiGAN/BigVGAN."""
def __init__(
self,
channels: int = 512,
kernel_size: int = 3,
dilations: List[int] = [1, 3, 5],
):
super(ResBlock, self).__init__()
self.convs1 = nn.ModuleList()
self.convs2 = nn.ModuleList()
for dilation in dilations:
self.convs1.append(
weight_norm(
Conv1d(
channels,
channels,
kernel_size,
1,
dilation=dilation,
padding=get_padding(kernel_size, dilation)
)
)
)
self.convs2.append(
weight_norm(
Conv1d(
channels,
channels,
kernel_size,
1,
dilation=1,
padding=get_padding(kernel_size, 1)
)
)
)
self.convs1.apply(init_weights)
self.convs2.apply(init_weights)
self.activations1 = nn.ModuleList([
Snake(channels, alpha_logscale=False)
for _ in range(len(self.convs1))
])
self.activations2 = nn.ModuleList([
Snake(channels, alpha_logscale=False)
for _ in range(len(self.convs2))
])
def forward(self, x: torch.Tensor) -> torch.Tensor:
for idx in range(len(self.convs1)):
xt = self.activations1[idx](x)
xt = self.convs1[idx](xt)
xt = self.activations2[idx](xt)
xt = self.convs2[idx](xt)
x = xt + x
return x
def remove_weight_norm(self):
for idx in range(len(self.convs1)):
remove_weight_norm(self.convs1[idx])
remove_weight_norm(self.convs2[idx])
class SineGen(torch.nn.Module):
""" Definition of sine generator
SineGen(samp_rate, harmonic_num = 0,
sine_amp = 0.1, noise_std = 0.003,
voiced_threshold = 0,
flag_for_pulse=False)
samp_rate: sampling rate in Hz
harmonic_num: number of harmonic overtones (default 0)
sine_amp: amplitude of sine-wavefrom (default 0.1)
noise_std: std of Gaussian noise (default 0.003)
voiced_thoreshold: F0 threshold for U/V classification (default 0)
flag_for_pulse: this SinGen is used inside PulseGen (default False)
Note: when flag_for_pulse is True, the first time step of a voiced
segment is always sin(np.pi) or cos(0)
"""
def __init__(self, samp_rate, harmonic_num=0,
sine_amp=0.1, noise_std=0.003,
voiced_threshold=0):
super(SineGen, self).__init__()
self.sine_amp = sine_amp
self.noise_std = noise_std
self.harmonic_num = harmonic_num
self.sampling_rate = samp_rate
self.voiced_threshold = voiced_threshold
def _f02uv(self, f0):
# generate uv signal
uv = (f0 > self.voiced_threshold).type(torch.float32)
return uv
@torch.no_grad()
def forward(self, f0):
"""
:param f0: [B, 1, sample_len], Hz
:return: [B, 1, sample_len]
"""
F_mat = torch.zeros((f0.size(0), self.harmonic_num + 1, f0.size(-1))).to(f0.device)
for i in range(self.harmonic_num + 1):
F_mat[:, i: i + 1, :] = f0 * (i + 1) / self.sampling_rate
theta_mat = 2 * np.pi * (torch.cumsum(F_mat, dim=-1) % 1)
u_dist = Uniform(low=-np.pi, high=np.pi)
phase_vec = u_dist.sample(sample_shape=(f0.size(0), self.harmonic_num + 1, 1)).to(F_mat.device)
phase_vec[:, 0, :] = 0
# generate sine waveforms
sine_waves = self.sine_amp * torch.sin(theta_mat + phase_vec)
# generate uv signal
uv = self._f02uv(f0)
# noise: for unvoiced should be similar to sine_amp
# std = self.sine_amp/3 -> max value ~ self.sine_amp
# . for voiced regions is self.noise_std
noise_amp = uv * self.noise_std + (1 - uv) * self.sine_amp / 3
noise = noise_amp * torch.randn_like(sine_waves)
# first: set the unvoiced part to 0 by uv
# then: additive noise
sine_waves = sine_waves * uv + noise
return sine_waves, uv, noise
class SourceModuleHnNSF(torch.nn.Module):
""" SourceModule for hn-nsf
SourceModule(sampling_rate, harmonic_num=0, sine_amp=0.1,
add_noise_std=0.003, voiced_threshod=0)
sampling_rate: sampling_rate in Hz
harmonic_num: number of harmonic above F0 (default: 0)
sine_amp: amplitude of sine source signal (default: 0.1)
add_noise_std: std of additive Gaussian noise (default: 0.003)
note that amplitude of noise in unvoiced is decided
by sine_amp
voiced_threshold: threhold to set U/V given F0 (default: 0)
Sine_source, noise_source = SourceModuleHnNSF(F0_sampled)
F0_sampled (batchsize, length, 1)
Sine_source (batchsize, length, 1)
noise_source (batchsize, length 1)
uv (batchsize, length, 1)
"""
def __init__(self, sampling_rate, upsample_scale, harmonic_num=0, sine_amp=0.1,
add_noise_std=0.003, voiced_threshod=0):
super(SourceModuleHnNSF, self).__init__()
self.sine_amp = sine_amp
self.noise_std = add_noise_std
# to produce sine waveforms
self.l_sin_gen = SineGen(sampling_rate, harmonic_num,
sine_amp, add_noise_std, voiced_threshod)
# to merge source harmonics into a single excitation
self.l_linear = torch.nn.Linear(harmonic_num + 1, 1)
self.l_tanh = torch.nn.Tanh()
def forward(self, x):
"""
Sine_source, noise_source = SourceModuleHnNSF(F0_sampled)
F0_sampled (batchsize, length, 1)
Sine_source (batchsize, length, 1)
noise_source (batchsize, length 1)
"""
# source for harmonic branch
with torch.no_grad():
sine_wavs, uv, _ = self.l_sin_gen(x.transpose(1, 2))
sine_wavs = sine_wavs.transpose(1, 2)
uv = uv.transpose(1, 2)
sine_merge = self.l_tanh(self.l_linear(sine_wavs))
# source for noise branch, in the same shape as uv
noise = torch.randn_like(uv) * self.sine_amp / 3
return sine_merge, noise, uv
class HiFTGenerator(nn.Module):
"""
HiFTNet Generator: Neural Source Filter + ISTFTNet
https://arxiv.org/abs/2309.09493
"""
def __init__(
self,
in_channels: int = 80,
base_channels: int = 512,
nb_harmonics: int = 8,
sampling_rate: int = 22050,
nsf_alpha: float = 0.1,
nsf_sigma: float = 0.003,
nsf_voiced_threshold: float = 10,
upsample_rates: List[int] = [8, 8],
upsample_kernel_sizes: List[int] = [16, 16],
istft_params: Dict[str, int] = {"n_fft": 16, "hop_len": 4},
resblock_kernel_sizes: List[int] = [3, 7, 11],
resblock_dilation_sizes: List[List[int]] = [[1, 3, 5], [1, 3, 5], [1, 3, 5]],
source_resblock_kernel_sizes: List[int] = [7, 11],
source_resblock_dilation_sizes: List[List[int]] = [[1, 3, 5], [1, 3, 5]],
lrelu_slope: float = 0.1,
audio_limit: float = 0.99,
f0_predictor: torch.nn.Module = None,
):
super(HiFTGenerator, self).__init__()
self.out_channels = 1
self.nb_harmonics = nb_harmonics
self.sampling_rate = sampling_rate
self.istft_params = istft_params
self.lrelu_slope = lrelu_slope
self.audio_limit = audio_limit
self.num_kernels = len(resblock_kernel_sizes)
self.num_upsamples = len(upsample_rates)
self.m_source = SourceModuleHnNSF(
sampling_rate=sampling_rate,
upsample_scale=np.prod(upsample_rates) * istft_params["hop_len"],
harmonic_num=nb_harmonics,
sine_amp=nsf_alpha,
add_noise_std=nsf_sigma,
voiced_threshod=nsf_voiced_threshold)
self.f0_upsamp = torch.nn.Upsample(scale_factor=np.prod(upsample_rates) * istft_params["hop_len"])
self.conv_pre = weight_norm(
Conv1d(in_channels, base_channels, 7, 1, padding=3)
)
# Up
self.ups = nn.ModuleList()
for i, (u, k) in enumerate(zip(upsample_rates, upsample_kernel_sizes)):
self.ups.append(
weight_norm(
ConvTranspose1d(
base_channels // (2**i),
base_channels // (2**(i + 1)),
k,
u,
padding=(k - u) // 2,
)
)
)
# Down
self.source_downs = nn.ModuleList()
self.source_resblocks = nn.ModuleList()
downsample_rates = [1] + upsample_rates[::-1][:-1]
downsample_cum_rates = np.cumprod(downsample_rates)
for i, (u, k, d) in enumerate(zip(downsample_cum_rates[::-1], source_resblock_kernel_sizes, source_resblock_dilation_sizes)):
if u == 1:
self.source_downs.append(
Conv1d(istft_params["n_fft"] + 2, base_channels // (2 ** (i + 1)), 1, 1)
)
else:
self.source_downs.append(
Conv1d(istft_params["n_fft"] + 2, base_channels // (2 ** (i + 1)), u * 2, u, padding=(u // 2))
)
self.source_resblocks.append(
ResBlock(base_channels // (2 ** (i + 1)), k, d)
)
self.resblocks = nn.ModuleList()
for i in range(len(self.ups)):
ch = base_channels // (2**(i + 1))
for _, (k, d) in enumerate(zip(resblock_kernel_sizes, resblock_dilation_sizes)):
self.resblocks.append(ResBlock(ch, k, d))
self.conv_post = weight_norm(Conv1d(ch, istft_params["n_fft"] + 2, 7, 1, padding=3))
self.ups.apply(init_weights)
self.conv_post.apply(init_weights)
self.reflection_pad = nn.ReflectionPad1d((1, 0))
self.stft_window = torch.from_numpy(get_window("hann", istft_params["n_fft"], fftbins=True).astype(np.float32))
self.f0_predictor = f0_predictor
def remove_weight_norm(self):
print('Removing weight norm...')
for l in self.ups:
remove_weight_norm(l)
for l in self.resblocks:
l.remove_weight_norm()
remove_weight_norm(self.conv_pre)
remove_weight_norm(self.conv_post)
self.m_source.remove_weight_norm()
for l in self.source_downs:
remove_weight_norm(l)
for l in self.source_resblocks:
l.remove_weight_norm()
def _stft(self, x):
spec = torch.stft(
x,
self.istft_params["n_fft"], self.istft_params["hop_len"], self.istft_params["n_fft"], window=self.stft_window.to(x.device),
return_complex=True)
spec = torch.view_as_real(spec) # [B, F, TT, 2]
return spec[..., 0], spec[..., 1]
def _istft(self, magnitude, phase):
magnitude = torch.clip(magnitude, max=1e2)
real = magnitude * torch.cos(phase)
img = magnitude * torch.sin(phase)
inverse_transform = torch.istft(torch.complex(real, img), self.istft_params["n_fft"], self.istft_params["hop_len"],
self.istft_params["n_fft"], window=self.stft_window.to(magnitude.device))
return inverse_transform
def decode(self, x: torch.Tensor, s: torch.Tensor = torch.zeros(1, 1, 0)) -> torch.Tensor:
s_stft_real, s_stft_imag = self._stft(s.squeeze(1))
s_stft = torch.cat([s_stft_real, s_stft_imag], dim=1)
x = self.conv_pre(x)
for i in range(self.num_upsamples):
x = F.leaky_relu(x, self.lrelu_slope)
x = self.ups[i](x)
if i == self.num_upsamples - 1:
x = self.reflection_pad(x)
# fusion
si = self.source_downs[i](s_stft)
si = self.source_resblocks[i](si)
x = x + si
xs = None
for j in range(self.num_kernels):
if xs is None:
xs = self.resblocks[i * self.num_kernels + j](x)
else:
xs += self.resblocks[i * self.num_kernels + j](x)
x = xs / self.num_kernels
x = F.leaky_relu(x)
x = self.conv_post(x)
magnitude = torch.exp(x[:, :self.istft_params["n_fft"] // 2 + 1, :])
phase = torch.sin(x[:, self.istft_params["n_fft"] // 2 + 1:, :]) # actually, sin is redundancy
x = self._istft(magnitude, phase)
x = torch.clamp(x, -self.audio_limit, self.audio_limit)
return x
def forward(
self,
batch: dict,
device: torch.device,
) -> Dict[str, Optional[torch.Tensor]]:
speech_feat = batch['speech_feat'].transpose(1, 2).to(device)
# mel->f0
f0 = self.f0_predictor(speech_feat)
# f0->source
s = self.f0_upsamp(f0[:, None]).transpose(1, 2) # bs,n,t
s, _, _ = self.m_source(s)
s = s.transpose(1, 2)
# mel+source->speech
generated_speech = self.decode(x=speech_feat, s=s)
return generated_speech, f0
@torch.inference_mode()
def inference(self, speech_feat: torch.Tensor, cache_source: torch.Tensor = torch.zeros(1, 1, 0)) -> torch.Tensor:
# mel->f0
f0 = self.f0_predictor(speech_feat)
# f0->source
s = self.f0_upsamp(f0[:, None]).transpose(1, 2) # bs,n,t
s, _, _ = self.m_source(s)
s = s.transpose(1, 2)
# use cache_source to avoid glitch
if cache_source.shape[2] != 0:
s[:, :, :cache_source.shape[2]] = cache_source
generated_speech = self.decode(x=speech_feat, s=s)
return generated_speech, s
@@ -0,0 +1,443 @@
import math
from typing import Optional
import torch
import torch.nn as nn
import torch.nn.functional as F
from conformer import ConformerBlock
from diffusers.models.activations import get_activation
from einops import pack, rearrange, repeat
from .transformer import BasicTransformerBlock
class SinusoidalPosEmb(torch.nn.Module):
def __init__(self, dim):
super().__init__()
self.dim = dim
assert self.dim % 2 == 0, "SinusoidalPosEmb requires dim to be even"
def forward(self, x, scale=1000):
if x.ndim < 1:
x = x.unsqueeze(0)
device = x.device
half_dim = self.dim // 2
emb = math.log(10000) / (half_dim - 1)
emb = torch.exp(torch.arange(half_dim, device=device).float() * -emb)
emb = scale * x.unsqueeze(1) * emb.unsqueeze(0)
emb = torch.cat((emb.sin(), emb.cos()), dim=-1)
return emb
class Block1D(torch.nn.Module):
def __init__(self, dim, dim_out, groups=8):
super().__init__()
self.block = torch.nn.Sequential(
torch.nn.Conv1d(dim, dim_out, 3, padding=1),
torch.nn.GroupNorm(groups, dim_out),
nn.Mish(),
)
def forward(self, x, mask):
output = self.block(x * mask)
return output * mask
class ResnetBlock1D(torch.nn.Module):
def __init__(self, dim, dim_out, time_emb_dim, groups=8):
super().__init__()
self.mlp = torch.nn.Sequential(nn.Mish(), torch.nn.Linear(time_emb_dim, dim_out))
self.block1 = Block1D(dim, dim_out, groups=groups)
self.block2 = Block1D(dim_out, dim_out, groups=groups)
self.res_conv = torch.nn.Conv1d(dim, dim_out, 1)
def forward(self, x, mask, time_emb):
h = self.block1(x, mask)
h += self.mlp(time_emb).unsqueeze(-1)
h = self.block2(h, mask)
output = h + self.res_conv(x * mask)
return output
class Downsample1D(nn.Module):
def __init__(self, dim):
super().__init__()
self.conv = torch.nn.Conv1d(dim, dim, 3, 2, 1)
def forward(self, x):
return self.conv(x)
class TimestepEmbedding(nn.Module):
def __init__(
self,
in_channels: int,
time_embed_dim: int,
act_fn: str = "silu",
out_dim: int = None,
post_act_fn: Optional[str] = None,
cond_proj_dim=None,
):
super().__init__()
self.linear_1 = nn.Linear(in_channels, time_embed_dim)
if cond_proj_dim is not None:
self.cond_proj = nn.Linear(cond_proj_dim, in_channels, bias=False)
else:
self.cond_proj = None
self.act = get_activation(act_fn)
if out_dim is not None:
time_embed_dim_out = out_dim
else:
time_embed_dim_out = time_embed_dim
self.linear_2 = nn.Linear(time_embed_dim, time_embed_dim_out)
if post_act_fn is None:
self.post_act = None
else:
self.post_act = get_activation(post_act_fn)
def forward(self, sample, condition=None):
if condition is not None:
sample = sample + self.cond_proj(condition)
sample = self.linear_1(sample)
if self.act is not None:
sample = self.act(sample)
sample = self.linear_2(sample)
if self.post_act is not None:
sample = self.post_act(sample)
return sample
class Upsample1D(nn.Module):
"""A 1D upsampling layer with an optional convolution.
Parameters:
channels (`int`):
number of channels in the inputs and outputs.
use_conv (`bool`, default `False`):
option to use a convolution.
use_conv_transpose (`bool`, default `False`):
option to use a convolution transpose.
out_channels (`int`, optional):
number of output channels. Defaults to `channels`.
"""
def __init__(self, channels, use_conv=False, use_conv_transpose=True, out_channels=None, name="conv"):
super().__init__()
self.channels = channels
self.out_channels = out_channels or channels
self.use_conv = use_conv
self.use_conv_transpose = use_conv_transpose
self.name = name
self.conv = None
if use_conv_transpose:
self.conv = nn.ConvTranspose1d(channels, self.out_channels, 4, 2, 1)
elif use_conv:
self.conv = nn.Conv1d(self.channels, self.out_channels, 3, padding=1)
def forward(self, inputs):
assert inputs.shape[1] == self.channels
if self.use_conv_transpose:
return self.conv(inputs)
outputs = F.interpolate(inputs, scale_factor=2.0, mode="nearest")
if self.use_conv:
outputs = self.conv(outputs)
return outputs
class ConformerWrapper(ConformerBlock):
def __init__( # pylint: disable=useless-super-delegation
self,
*,
dim,
dim_head=64,
heads=8,
ff_mult=4,
conv_expansion_factor=2,
conv_kernel_size=31,
attn_dropout=0,
ff_dropout=0,
conv_dropout=0,
conv_causal=False,
):
super().__init__(
dim=dim,
dim_head=dim_head,
heads=heads,
ff_mult=ff_mult,
conv_expansion_factor=conv_expansion_factor,
conv_kernel_size=conv_kernel_size,
attn_dropout=attn_dropout,
ff_dropout=ff_dropout,
conv_dropout=conv_dropout,
conv_causal=conv_causal,
)
def forward(
self,
hidden_states,
attention_mask,
encoder_hidden_states=None,
encoder_attention_mask=None,
timestep=None,
):
return super().forward(x=hidden_states, mask=attention_mask.bool())
class Decoder(nn.Module):
def __init__(
self,
in_channels,
out_channels,
channels=(256, 256),
dropout=0.05,
attention_head_dim=64,
n_blocks=1,
num_mid_blocks=2,
num_heads=4,
act_fn="snake",
down_block_type="transformer",
mid_block_type="transformer",
up_block_type="transformer",
):
super().__init__()
channels = tuple(channels)
self.in_channels = in_channels
self.out_channels = out_channels
self.time_embeddings = SinusoidalPosEmb(in_channels)
time_embed_dim = channels[0] * 4
self.time_mlp = TimestepEmbedding(
in_channels=in_channels,
time_embed_dim=time_embed_dim,
act_fn="silu",
)
self.down_blocks = nn.ModuleList([])
self.mid_blocks = nn.ModuleList([])
self.up_blocks = nn.ModuleList([])
output_channel = in_channels
for i in range(len(channels)): # pylint: disable=consider-using-enumerate
input_channel = output_channel
output_channel = channels[i]
is_last = i == len(channels) - 1
resnet = ResnetBlock1D(dim=input_channel, dim_out=output_channel, time_emb_dim=time_embed_dim)
transformer_blocks = nn.ModuleList(
[
self.get_block(
down_block_type,
output_channel,
attention_head_dim,
num_heads,
dropout,
act_fn,
)
for _ in range(n_blocks)
]
)
downsample = (
Downsample1D(output_channel) if not is_last else nn.Conv1d(output_channel, output_channel, 3, padding=1)
)
self.down_blocks.append(nn.ModuleList([resnet, transformer_blocks, downsample]))
for i in range(num_mid_blocks):
input_channel = channels[-1]
out_channels = channels[-1]
resnet = ResnetBlock1D(dim=input_channel, dim_out=output_channel, time_emb_dim=time_embed_dim)
transformer_blocks = nn.ModuleList(
[
self.get_block(
mid_block_type,
output_channel,
attention_head_dim,
num_heads,
dropout,
act_fn,
)
for _ in range(n_blocks)
]
)
self.mid_blocks.append(nn.ModuleList([resnet, transformer_blocks]))
channels = channels[::-1] + (channels[0],)
for i in range(len(channels) - 1):
input_channel = channels[i]
output_channel = channels[i + 1]
is_last = i == len(channels) - 2
resnet = ResnetBlock1D(
dim=2 * input_channel,
dim_out=output_channel,
time_emb_dim=time_embed_dim,
)
transformer_blocks = nn.ModuleList(
[
self.get_block(
up_block_type,
output_channel,
attention_head_dim,
num_heads,
dropout,
act_fn,
)
for _ in range(n_blocks)
]
)
upsample = (
Upsample1D(output_channel, use_conv_transpose=True)
if not is_last
else nn.Conv1d(output_channel, output_channel, 3, padding=1)
)
self.up_blocks.append(nn.ModuleList([resnet, transformer_blocks, upsample]))
self.final_block = Block1D(channels[-1], channels[-1])
self.final_proj = nn.Conv1d(channels[-1], self.out_channels, 1)
self.initialize_weights()
# nn.init.normal_(self.final_proj.weight)
@staticmethod
def get_block(block_type, dim, attention_head_dim, num_heads, dropout, act_fn):
if block_type == "conformer":
block = ConformerWrapper(
dim=dim,
dim_head=attention_head_dim,
heads=num_heads,
ff_mult=1,
conv_expansion_factor=2,
ff_dropout=dropout,
attn_dropout=dropout,
conv_dropout=dropout,
conv_kernel_size=31,
)
elif block_type == "transformer":
block = BasicTransformerBlock(
dim=dim,
num_attention_heads=num_heads,
attention_head_dim=attention_head_dim,
dropout=dropout,
activation_fn=act_fn,
)
else:
raise ValueError(f"Unknown block type {block_type}")
return block
def initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv1d):
nn.init.kaiming_normal_(m.weight, nonlinearity="relu")
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.GroupNorm):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight, nonlinearity="relu")
if m.bias is not None:
nn.init.constant_(m.bias, 0)
def forward(self, x, mask, mu, t, spks=None, cond=None):
"""Forward pass of the UNet1DConditional model.
Args:
x (torch.Tensor): shape (batch_size, in_channels, time)
mask (_type_): shape (batch_size, 1, time)
t (_type_): shape (batch_size)
spks (_type_, optional): shape: (batch_size, condition_channels). Defaults to None.
cond (_type_, optional): placeholder for future use. Defaults to None.
Raises:
ValueError: _description_
ValueError: _description_
Returns:
_type_: _description_
"""
t = self.time_embeddings(t)
t = self.time_mlp(t)
x = pack([x, mu], "b * t")[0]
if spks is not None:
spks = repeat(spks, "b c -> b c t", t=x.shape[-1])
x = pack([x, spks], "b * t")[0]
hiddens = []
masks = [mask]
for resnet, transformer_blocks, downsample in self.down_blocks:
mask_down = masks[-1]
x = resnet(x, mask_down, t)
x = rearrange(x, "b c t -> b t c")
mask_down = rearrange(mask_down, "b 1 t -> b t")
for transformer_block in transformer_blocks:
x = transformer_block(
hidden_states=x,
attention_mask=mask_down,
timestep=t,
)
x = rearrange(x, "b t c -> b c t")
mask_down = rearrange(mask_down, "b t -> b 1 t")
hiddens.append(x) # Save hidden states for skip connections
x = downsample(x * mask_down)
masks.append(mask_down[:, :, ::2])
masks = masks[:-1]
mask_mid = masks[-1]
for resnet, transformer_blocks in self.mid_blocks:
x = resnet(x, mask_mid, t)
x = rearrange(x, "b c t -> b t c")
mask_mid = rearrange(mask_mid, "b 1 t -> b t")
for transformer_block in transformer_blocks:
x = transformer_block(
hidden_states=x,
attention_mask=mask_mid,
timestep=t,
)
x = rearrange(x, "b t c -> b c t")
mask_mid = rearrange(mask_mid, "b t -> b 1 t")
for resnet, transformer_blocks, upsample in self.up_blocks:
mask_up = masks.pop()
x = resnet(pack([x, hiddens.pop()], "b * t")[0], mask_up, t)
x = rearrange(x, "b c t -> b t c")
mask_up = rearrange(mask_up, "b 1 t -> b t")
for transformer_block in transformer_blocks:
x = transformer_block(
hidden_states=x,
attention_mask=mask_up,
timestep=t,
)
x = rearrange(x, "b t c -> b c t")
mask_up = rearrange(mask_up, "b t -> b 1 t")
x = upsample(x * mask_up)
x = self.final_block(x, mask_up)
output = self.final_proj(x * mask_up)
return output * mask
@@ -0,0 +1,129 @@
from abc import ABC
import torch
import torch.nn.functional as F
from .decoder import Decoder
class BASECFM(torch.nn.Module, ABC):
def __init__(
self,
n_feats,
cfm_params,
n_spks=1,
spk_emb_dim=128,
):
super().__init__()
self.n_feats = n_feats
self.n_spks = n_spks
self.spk_emb_dim = spk_emb_dim
self.solver = cfm_params.solver
if hasattr(cfm_params, "sigma_min"):
self.sigma_min = cfm_params.sigma_min
else:
self.sigma_min = 1e-4
self.estimator = None
@torch.inference_mode()
def forward(self, mu, mask, n_timesteps, temperature=1.0, spks=None, cond=None):
"""Forward diffusion
Args:
mu (torch.Tensor): output of encoder
shape: (batch_size, n_feats, mel_timesteps)
mask (torch.Tensor): output_mask
shape: (batch_size, 1, mel_timesteps)
n_timesteps (int): number of diffusion steps
temperature (float, optional): temperature for scaling noise. Defaults to 1.0.
spks (torch.Tensor, optional): speaker ids. Defaults to None.
shape: (batch_size, spk_emb_dim)
cond: Not used but kept for future purposes
Returns:
sample: generated mel-spectrogram
shape: (batch_size, n_feats, mel_timesteps)
"""
z = torch.randn_like(mu) * temperature
t_span = torch.linspace(0, 1, n_timesteps + 1, device=mu.device)
return self.solve_euler(z, t_span=t_span, mu=mu, mask=mask, spks=spks, cond=cond)
def solve_euler(self, x, t_span, mu, mask, spks, cond):
"""
Fixed euler solver for ODEs.
Args:
x (torch.Tensor): random noise
t_span (torch.Tensor): n_timesteps interpolated
shape: (n_timesteps + 1,)
mu (torch.Tensor): output of encoder
shape: (batch_size, n_feats, mel_timesteps)
mask (torch.Tensor): output_mask
shape: (batch_size, 1, mel_timesteps)
spks (torch.Tensor, optional): speaker ids. Defaults to None.
shape: (batch_size, spk_emb_dim)
cond: Not used but kept for future purposes
"""
t, _, dt = t_span[0], t_span[-1], t_span[1] - t_span[0]
# I am storing this because I can later plot it by putting a debugger here and saving it to a file
# Or in future might add like a return_all_steps flag
sol = []
for step in range(1, len(t_span)):
dphi_dt = self.estimator(x, mask, mu, t, spks, cond)
x = x + dt * dphi_dt
t = t + dt
sol.append(x)
if step < len(t_span) - 1:
dt = t_span[step + 1] - t
return sol[-1]
def compute_loss(self, x1, mask, mu, spks=None, cond=None):
"""Computes diffusion loss
Args:
x1 (torch.Tensor): Target
shape: (batch_size, n_feats, mel_timesteps)
mask (torch.Tensor): target mask
shape: (batch_size, 1, mel_timesteps)
mu (torch.Tensor): output of encoder
shape: (batch_size, n_feats, mel_timesteps)
spks (torch.Tensor, optional): speaker embedding. Defaults to None.
shape: (batch_size, spk_emb_dim)
Returns:
loss: conditional flow matching loss
y: conditional flow
shape: (batch_size, n_feats, mel_timesteps)
"""
b, _, t = mu.shape
# random timestep
t = torch.rand([b, 1, 1], device=mu.device, dtype=mu.dtype)
# sample noise p(x_0)
z = torch.randn_like(x1)
y = (1 - (1 - self.sigma_min) * t) * z + t * x1
u = x1 - (1 - self.sigma_min) * z
loss = F.mse_loss(self.estimator(y, mask, mu, t.squeeze(), spks), u, reduction="sum") / (
torch.sum(mask) * u.shape[1]
)
return loss, y
class CFM(BASECFM):
def __init__(self, in_channels, out_channel, cfm_params, decoder_params, n_spks=1, spk_emb_dim=64):
super().__init__(
n_feats=in_channels,
cfm_params=cfm_params,
n_spks=n_spks,
spk_emb_dim=spk_emb_dim,
)
in_channels = in_channels + (spk_emb_dim if n_spks > 1 else 0)
# Just change the architecture of the estimator here
self.estimator = Decoder(in_channels=in_channels, out_channels=out_channel, **decoder_params)
@@ -0,0 +1,413 @@
""" from https://github.com/jaywalnut310/glow-tts """
import math
import torch
import torch.nn as nn
from einops import rearrange
def sequence_mask(length, max_length=None):
if max_length is None:
max_length = length.max()
x = torch.arange(max_length, dtype=length.dtype, device=length.device)
return x.unsqueeze(0) < length.unsqueeze(1)
class LayerNorm(nn.Module):
def __init__(self, channels, eps=1e-4):
super().__init__()
self.channels = channels
self.eps = eps
self.gamma = torch.nn.Parameter(torch.ones(channels))
self.beta = torch.nn.Parameter(torch.zeros(channels))
def forward(self, x):
n_dims = len(x.shape)
mean = torch.mean(x, 1, keepdim=True)
variance = torch.mean((x - mean) ** 2, 1, keepdim=True)
x = (x - mean) * torch.rsqrt(variance + self.eps)
shape = [1, -1] + [1] * (n_dims - 2)
x = x * self.gamma.view(*shape) + self.beta.view(*shape)
return x
class ConvReluNorm(nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels, kernel_size, n_layers, p_dropout):
super().__init__()
self.in_channels = in_channels
self.hidden_channels = hidden_channels
self.out_channels = out_channels
self.kernel_size = kernel_size
self.n_layers = n_layers
self.p_dropout = p_dropout
self.conv_layers = torch.nn.ModuleList()
self.norm_layers = torch.nn.ModuleList()
self.conv_layers.append(torch.nn.Conv1d(in_channels, hidden_channels, kernel_size, padding=kernel_size // 2))
self.norm_layers.append(LayerNorm(hidden_channels))
self.relu_drop = torch.nn.Sequential(torch.nn.ReLU(), torch.nn.Dropout(p_dropout))
for _ in range(n_layers - 1):
self.conv_layers.append(
torch.nn.Conv1d(hidden_channels, hidden_channels, kernel_size, padding=kernel_size // 2)
)
self.norm_layers.append(LayerNorm(hidden_channels))
self.proj = torch.nn.Conv1d(hidden_channels, out_channels, 1)
self.proj.weight.data.zero_()
self.proj.bias.data.zero_()
def forward(self, x, x_mask):
x_org = x
for i in range(self.n_layers):
x = self.conv_layers[i](x * x_mask)
x = self.norm_layers[i](x)
x = self.relu_drop(x)
x = x_org + self.proj(x)
return x * x_mask
class DurationPredictor(nn.Module):
def __init__(self, in_channels, filter_channels, kernel_size, p_dropout):
super().__init__()
self.in_channels = in_channels
self.filter_channels = filter_channels
self.p_dropout = p_dropout
self.drop = torch.nn.Dropout(p_dropout)
self.conv_1 = torch.nn.Conv1d(in_channels, filter_channels, kernel_size, padding=kernel_size // 2)
self.norm_1 = LayerNorm(filter_channels)
self.conv_2 = torch.nn.Conv1d(filter_channels, filter_channels, kernel_size, padding=kernel_size // 2)
self.norm_2 = LayerNorm(filter_channels)
self.proj = torch.nn.Conv1d(filter_channels, 1, 1)
def forward(self, x, x_mask):
x = self.conv_1(x * x_mask)
x = torch.relu(x)
x = self.norm_1(x)
x = self.drop(x)
x = self.conv_2(x * x_mask)
x = torch.relu(x)
x = self.norm_2(x)
x = self.drop(x)
x = self.proj(x * x_mask)
return x * x_mask
class RotaryPositionalEmbeddings(nn.Module):
"""
## RoPE module
Rotary encoding transforms pairs of features by rotating in the 2D plane.
That is, it organizes the $d$ features as $\frac{d}{2}$ pairs.
Each pair can be considered a coordinate in a 2D plane, and the encoding will rotate it
by an angle depending on the position of the token.
"""
def __init__(self, d: int, base: int = 10_000):
r"""
* `d` is the number of features $d$
* `base` is the constant used for calculating $\Theta$
"""
super().__init__()
self.base = base
self.d = int(d)
self.cos_cached = None
self.sin_cached = None
def _build_cache(self, x: torch.Tensor):
r"""
Cache $\cos$ and $\sin$ values
"""
# Return if cache is already built
if self.cos_cached is not None and x.shape[0] <= self.cos_cached.shape[0]:
return
# Get sequence length
seq_len = x.shape[0]
# $\Theta = {\theta_i = 10000^{-\frac{2(i-1)}{d}}, i \in [1, 2, ..., \frac{d}{2}]}$
theta = 1.0 / (self.base ** (torch.arange(0, self.d, 2).float() / self.d)).to(x.device)
# Create position indexes `[0, 1, ..., seq_len - 1]`
seq_idx = torch.arange(seq_len, device=x.device).float().to(x.device)
# Calculate the product of position index and $\theta_i$
idx_theta = torch.einsum("n,d->nd", seq_idx, theta)
# Concatenate so that for row $m$ we have
# $[m \theta_0, m \theta_1, ..., m \theta_{\frac{d}{2}}, m \theta_0, m \theta_1, ..., m \theta_{\frac{d}{2}}]$
idx_theta2 = torch.cat([idx_theta, idx_theta], dim=1)
# Cache them
self.cos_cached = idx_theta2.cos()[:, None, None, :]
self.sin_cached = idx_theta2.sin()[:, None, None, :]
def _neg_half(self, x: torch.Tensor):
# $\frac{d}{2}$
d_2 = self.d // 2
# Calculate $[-x^{(\frac{d}{2} + 1)}, -x^{(\frac{d}{2} + 2)}, ..., -x^{(d)}, x^{(1)}, x^{(2)}, ..., x^{(\frac{d}{2})}]$
return torch.cat([-x[:, :, :, d_2:], x[:, :, :, :d_2]], dim=-1)
def forward(self, x: torch.Tensor):
"""
* `x` is the Tensor at the head of a key or a query with shape `[seq_len, batch_size, n_heads, d]`
"""
# Cache $\cos$ and $\sin$ values
x = rearrange(x, "b h t d -> t b h d")
self._build_cache(x)
# Split the features, we can choose to apply rotary embeddings only to a partial set of features.
x_rope, x_pass = x[..., : self.d], x[..., self.d :]
# Calculate
# $[-x^{(\frac{d}{2} + 1)}, -x^{(\frac{d}{2} + 2)}, ..., -x^{(d)}, x^{(1)}, x^{(2)}, ..., x^{(\frac{d}{2})}]$
neg_half_x = self._neg_half(x_rope)
x_rope = (x_rope * self.cos_cached[: x.shape[0]]) + (neg_half_x * self.sin_cached[: x.shape[0]])
return rearrange(torch.cat((x_rope, x_pass), dim=-1), "t b h d -> b h t d")
class MultiHeadAttention(nn.Module):
def __init__(
self,
channels,
out_channels,
n_heads,
heads_share=True,
p_dropout=0.0,
proximal_bias=False,
proximal_init=False,
):
super().__init__()
assert channels % n_heads == 0
self.channels = channels
self.out_channels = out_channels
self.n_heads = n_heads
self.heads_share = heads_share
self.proximal_bias = proximal_bias
self.p_dropout = p_dropout
self.attn = None
self.k_channels = channels // n_heads
self.conv_q = torch.nn.Conv1d(channels, channels, 1)
self.conv_k = torch.nn.Conv1d(channels, channels, 1)
self.conv_v = torch.nn.Conv1d(channels, channels, 1)
# from https://nn.labml.ai/transformers/rope/index.html
self.query_rotary_pe = RotaryPositionalEmbeddings(self.k_channels * 0.5)
self.key_rotary_pe = RotaryPositionalEmbeddings(self.k_channels * 0.5)
self.conv_o = torch.nn.Conv1d(channels, out_channels, 1)
self.drop = torch.nn.Dropout(p_dropout)
torch.nn.init.xavier_uniform_(self.conv_q.weight)
torch.nn.init.xavier_uniform_(self.conv_k.weight)
if proximal_init:
self.conv_k.weight.data.copy_(self.conv_q.weight.data)
self.conv_k.bias.data.copy_(self.conv_q.bias.data)
torch.nn.init.xavier_uniform_(self.conv_v.weight)
def forward(self, x, c, attn_mask=None):
q = self.conv_q(x)
k = self.conv_k(c)
v = self.conv_v(c)
x, self.attn = self.attention(q, k, v, mask=attn_mask)
x = self.conv_o(x)
return x
def attention(self, query, key, value, mask=None):
b, d, t_s, t_t = (*key.size(), query.size(2))
query = rearrange(query, "b (h c) t-> b h t c", h=self.n_heads)
key = rearrange(key, "b (h c) t-> b h t c", h=self.n_heads)
value = rearrange(value, "b (h c) t-> b h t c", h=self.n_heads)
query = self.query_rotary_pe(query)
key = self.key_rotary_pe(key)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(self.k_channels)
if self.proximal_bias:
assert t_s == t_t, "Proximal bias is only available for self-attention."
scores = scores + self._attention_bias_proximal(t_s).to(device=scores.device, dtype=scores.dtype)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e4)
p_attn = torch.nn.functional.softmax(scores, dim=-1)
p_attn = self.drop(p_attn)
output = torch.matmul(p_attn, value)
output = output.transpose(2, 3).contiguous().view(b, d, t_t)
return output, p_attn
@staticmethod
def _attention_bias_proximal(length):
r = torch.arange(length, dtype=torch.float32)
diff = torch.unsqueeze(r, 0) - torch.unsqueeze(r, 1)
return torch.unsqueeze(torch.unsqueeze(-torch.log1p(torch.abs(diff)), 0), 0)
class FFN(nn.Module):
def __init__(self, in_channels, out_channels, filter_channels, kernel_size, p_dropout=0.0):
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.filter_channels = filter_channels
self.kernel_size = kernel_size
self.p_dropout = p_dropout
self.conv_1 = torch.nn.Conv1d(in_channels, filter_channels, kernel_size, padding=kernel_size // 2)
self.conv_2 = torch.nn.Conv1d(filter_channels, out_channels, kernel_size, padding=kernel_size // 2)
self.drop = torch.nn.Dropout(p_dropout)
def forward(self, x, x_mask):
x = self.conv_1(x * x_mask)
x = torch.relu(x)
x = self.drop(x)
x = self.conv_2(x * x_mask)
return x * x_mask
class Encoder(nn.Module):
def __init__(
self,
hidden_channels,
filter_channels,
n_heads,
n_layers,
kernel_size=1,
p_dropout=0.0,
**kwargs,
):
super().__init__()
self.hidden_channels = hidden_channels
self.filter_channels = filter_channels
self.n_heads = n_heads
self.n_layers = n_layers
self.kernel_size = kernel_size
self.p_dropout = p_dropout
self.drop = torch.nn.Dropout(p_dropout)
self.attn_layers = torch.nn.ModuleList()
self.norm_layers_1 = torch.nn.ModuleList()
self.ffn_layers = torch.nn.ModuleList()
self.norm_layers_2 = torch.nn.ModuleList()
for _ in range(self.n_layers):
self.attn_layers.append(MultiHeadAttention(hidden_channels, hidden_channels, n_heads, p_dropout=p_dropout))
self.norm_layers_1.append(LayerNorm(hidden_channels))
self.ffn_layers.append(
FFN(
hidden_channels,
hidden_channels,
filter_channels,
kernel_size,
p_dropout=p_dropout,
)
)
self.norm_layers_2.append(LayerNorm(hidden_channels))
def forward(self, x, x_mask):
attn_mask = x_mask.unsqueeze(2) * x_mask.unsqueeze(-1)
for i in range(self.n_layers):
x = x * x_mask
y = self.attn_layers[i](x, x, attn_mask)
y = self.drop(y)
x = self.norm_layers_1[i](x + y)
y = self.ffn_layers[i](x, x_mask)
y = self.drop(y)
x = self.norm_layers_2[i](x + y)
x = x * x_mask
return x
class TextEncoder(nn.Module):
def __init__(
self,
encoder_type,
encoder_params,
duration_predictor_params,
n_vocab,
n_spks=1,
spk_emb_dim=128,
):
super().__init__()
self.encoder_type = encoder_type
self.n_vocab = n_vocab
self.n_feats = encoder_params.n_feats
self.n_channels = encoder_params.n_channels
self.spk_emb_dim = spk_emb_dim
self.n_spks = n_spks
self.emb = torch.nn.Embedding(n_vocab, self.n_channels)
torch.nn.init.normal_(self.emb.weight, 0.0, self.n_channels**-0.5)
if encoder_params.prenet:
self.prenet = ConvReluNorm(
self.n_channels,
self.n_channels,
self.n_channels,
kernel_size=5,
n_layers=3,
p_dropout=0.5,
)
else:
self.prenet = lambda x, x_mask: x
self.encoder = Encoder(
encoder_params.n_channels + (spk_emb_dim if n_spks > 1 else 0),
encoder_params.filter_channels,
encoder_params.n_heads,
encoder_params.n_layers,
encoder_params.kernel_size,
encoder_params.p_dropout,
)
self.proj_m = torch.nn.Conv1d(self.n_channels + (spk_emb_dim if n_spks > 1 else 0), self.n_feats, 1)
self.proj_w = DurationPredictor(
self.n_channels + (spk_emb_dim if n_spks > 1 else 0),
duration_predictor_params.filter_channels_dp,
duration_predictor_params.kernel_size,
duration_predictor_params.p_dropout,
)
def forward(self, x, x_lengths, spks=None):
"""Run forward pass to the transformer based encoder and duration predictor
Args:
x (torch.Tensor): text input
shape: (batch_size, max_text_length)
x_lengths (torch.Tensor): text input lengths
shape: (batch_size,)
spks (torch.Tensor, optional): speaker ids. Defaults to None.
shape: (batch_size,)
Returns:
mu (torch.Tensor): average output of the encoder
shape: (batch_size, n_feats, max_text_length)
logw (torch.Tensor): log duration predicted by the duration predictor
shape: (batch_size, 1, max_text_length)
x_mask (torch.Tensor): mask for the text input
shape: (batch_size, 1, max_text_length)
"""
x = self.emb(x) * math.sqrt(self.n_channels)
x = torch.transpose(x, 1, -1)
x_mask = torch.unsqueeze(sequence_mask(x_lengths, x.size(2)), 1).to(x.dtype)
x = self.prenet(x, x_mask)
if self.n_spks > 1:
x = torch.cat([x, spks.unsqueeze(-1).repeat(1, 1, x.shape[-1])], dim=1)
x = self.encoder(x, x_mask)
mu = self.proj_m(x) * x_mask
x_dp = torch.detach(x)
logw = self.proj_w(x_dp, x_mask)
return mu, logw, x_mask
@@ -0,0 +1,316 @@
from typing import Any, Dict, Optional
import torch
import torch.nn as nn
from diffusers.models.attention import (
GEGLU,
GELU,
AdaLayerNorm,
AdaLayerNormZero,
ApproximateGELU,
)
from diffusers.models.attention_processor import Attention
from diffusers.models.lora import LoRACompatibleLinear
from diffusers.utils.torch_utils import maybe_allow_in_graph
class SnakeBeta(nn.Module):
"""
A modified Snake function which uses separate parameters for the magnitude of the periodic components
Shape:
- Input: (B, C, T)
- Output: (B, C, T), same shape as the input
Parameters:
- alpha - trainable parameter that controls frequency
- beta - trainable parameter that controls magnitude
References:
- This activation function is a modified version based on this paper by Liu Ziyin, Tilman Hartwig, Masahito Ueda:
https://arxiv.org/abs/2006.08195
Examples:
>>> a1 = snakebeta(256)
>>> x = torch.randn(256)
>>> x = a1(x)
"""
def __init__(self, in_features, out_features, alpha=1.0, alpha_trainable=True, alpha_logscale=True):
"""
Initialization.
INPUT:
- in_features: shape of the input
- alpha - trainable parameter that controls frequency
- beta - trainable parameter that controls magnitude
alpha is initialized to 1 by default, higher values = higher-frequency.
beta is initialized to 1 by default, higher values = higher-magnitude.
alpha will be trained along with the rest of your model.
"""
super().__init__()
self.in_features = out_features if isinstance(out_features, list) else [out_features]
self.proj = LoRACompatibleLinear(in_features, out_features)
# initialize alpha
self.alpha_logscale = alpha_logscale
if self.alpha_logscale: # log scale alphas initialized to zeros
self.alpha = nn.Parameter(torch.zeros(self.in_features) * alpha)
self.beta = nn.Parameter(torch.zeros(self.in_features) * alpha)
else: # linear scale alphas initialized to ones
self.alpha = nn.Parameter(torch.ones(self.in_features) * alpha)
self.beta = nn.Parameter(torch.ones(self.in_features) * alpha)
self.alpha.requires_grad = alpha_trainable
self.beta.requires_grad = alpha_trainable
self.no_div_by_zero = 0.000000001
def forward(self, x):
"""
Forward pass of the function.
Applies the function to the input elementwise.
SnakeBeta ∶= x + 1/b * sin^2 (xa)
"""
x = self.proj(x)
if self.alpha_logscale:
alpha = torch.exp(self.alpha)
beta = torch.exp(self.beta)
else:
alpha = self.alpha
beta = self.beta
x = x + (1.0 / (beta + self.no_div_by_zero)) * torch.pow(torch.sin(x * alpha), 2)
return x
class FeedForward(nn.Module):
r"""
A feed-forward layer.
Parameters:
dim (`int`): The number of channels in the input.
dim_out (`int`, *optional*): The number of channels in the output. If not given, defaults to `dim`.
mult (`int`, *optional*, defaults to 4): The multiplier to use for the hidden dimension.
dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use.
activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward.
final_dropout (`bool` *optional*, defaults to False): Apply a final dropout.
"""
def __init__(
self,
dim: int,
dim_out: Optional[int] = None,
mult: int = 4,
dropout: float = 0.0,
activation_fn: str = "geglu",
final_dropout: bool = False,
):
super().__init__()
inner_dim = int(dim * mult)
dim_out = dim_out if dim_out is not None else dim
if activation_fn == "gelu":
act_fn = GELU(dim, inner_dim)
if activation_fn == "gelu-approximate":
act_fn = GELU(dim, inner_dim, approximate="tanh")
elif activation_fn == "geglu":
act_fn = GEGLU(dim, inner_dim)
elif activation_fn == "geglu-approximate":
act_fn = ApproximateGELU(dim, inner_dim)
elif activation_fn == "snakebeta":
act_fn = SnakeBeta(dim, inner_dim)
self.net = nn.ModuleList([])
# project in
self.net.append(act_fn)
# project dropout
self.net.append(nn.Dropout(dropout))
# project out
self.net.append(LoRACompatibleLinear(inner_dim, dim_out))
# FF as used in Vision Transformer, MLP-Mixer, etc. have a final dropout
if final_dropout:
self.net.append(nn.Dropout(dropout))
def forward(self, hidden_states):
for module in self.net:
hidden_states = module(hidden_states)
return hidden_states
@maybe_allow_in_graph
class BasicTransformerBlock(nn.Module):
r"""
A basic Transformer block.
Parameters:
dim (`int`): The number of channels in the input and output.
num_attention_heads (`int`): The number of heads to use for multi-head attention.
attention_head_dim (`int`): The number of channels in each head.
dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use.
cross_attention_dim (`int`, *optional*): The size of the encoder_hidden_states vector for cross attention.
only_cross_attention (`bool`, *optional*):
Whether to use only cross-attention layers. In this case two cross attention layers are used.
double_self_attention (`bool`, *optional*):
Whether to use two self-attention layers. In this case no cross attention layers are used.
activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward.
num_embeds_ada_norm (:
obj: `int`, *optional*): The number of diffusion steps used during training. See `Transformer2DModel`.
attention_bias (:
obj: `bool`, *optional*, defaults to `False`): Configure if the attentions should contain a bias parameter.
"""
def __init__(
self,
dim: int,
num_attention_heads: int,
attention_head_dim: int,
dropout=0.0,
cross_attention_dim: Optional[int] = None,
activation_fn: str = "geglu",
num_embeds_ada_norm: Optional[int] = None,
attention_bias: bool = False,
only_cross_attention: bool = False,
double_self_attention: bool = False,
upcast_attention: bool = False,
norm_elementwise_affine: bool = True,
norm_type: str = "layer_norm",
final_dropout: bool = False,
):
super().__init__()
self.only_cross_attention = only_cross_attention
self.use_ada_layer_norm_zero = (num_embeds_ada_norm is not None) and norm_type == "ada_norm_zero"
self.use_ada_layer_norm = (num_embeds_ada_norm is not None) and norm_type == "ada_norm"
if norm_type in ("ada_norm", "ada_norm_zero") and num_embeds_ada_norm is None:
raise ValueError(
f"`norm_type` is set to {norm_type}, but `num_embeds_ada_norm` is not defined. Please make sure to"
f" define `num_embeds_ada_norm` if setting `norm_type` to {norm_type}."
)
# Define 3 blocks. Each block has its own normalization layer.
# 1. Self-Attn
if self.use_ada_layer_norm:
self.norm1 = AdaLayerNorm(dim, num_embeds_ada_norm)
elif self.use_ada_layer_norm_zero:
self.norm1 = AdaLayerNormZero(dim, num_embeds_ada_norm)
else:
self.norm1 = nn.LayerNorm(dim, elementwise_affine=norm_elementwise_affine)
self.attn1 = Attention(
query_dim=dim,
heads=num_attention_heads,
dim_head=attention_head_dim,
dropout=dropout,
bias=attention_bias,
cross_attention_dim=cross_attention_dim if only_cross_attention else None,
upcast_attention=upcast_attention,
)
# 2. Cross-Attn
if cross_attention_dim is not None or double_self_attention:
# We currently only use AdaLayerNormZero for self attention where there will only be one attention block.
# I.e. the number of returned modulation chunks from AdaLayerZero would not make sense if returned during
# the second cross attention block.
self.norm2 = (
AdaLayerNorm(dim, num_embeds_ada_norm)
if self.use_ada_layer_norm
else nn.LayerNorm(dim, elementwise_affine=norm_elementwise_affine)
)
self.attn2 = Attention(
query_dim=dim,
cross_attention_dim=cross_attention_dim if not double_self_attention else None,
heads=num_attention_heads,
dim_head=attention_head_dim,
dropout=dropout,
bias=attention_bias,
upcast_attention=upcast_attention,
# scale_qk=False, # uncomment this to not to use flash attention
) # is self-attn if encoder_hidden_states is none
else:
self.norm2 = None
self.attn2 = None
# 3. Feed-forward
self.norm3 = nn.LayerNorm(dim, elementwise_affine=norm_elementwise_affine)
self.ff = FeedForward(dim, dropout=dropout, activation_fn=activation_fn, final_dropout=final_dropout)
# let chunk size default to None
self._chunk_size = None
self._chunk_dim = 0
def set_chunk_feed_forward(self, chunk_size: Optional[int], dim: int):
# Sets chunk feed-forward
self._chunk_size = chunk_size
self._chunk_dim = dim
def forward(
self,
hidden_states: torch.FloatTensor,
attention_mask: Optional[torch.FloatTensor] = None,
encoder_hidden_states: Optional[torch.FloatTensor] = None,
encoder_attention_mask: Optional[torch.FloatTensor] = None,
timestep: Optional[torch.LongTensor] = None,
cross_attention_kwargs: Dict[str, Any] = None,
class_labels: Optional[torch.LongTensor] = None,
):
# Notice that normalization is always applied before the real computation in the following blocks.
# 1. Self-Attention
if self.use_ada_layer_norm:
norm_hidden_states = self.norm1(hidden_states, timestep)
elif self.use_ada_layer_norm_zero:
norm_hidden_states, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.norm1(
hidden_states, timestep, class_labels, hidden_dtype=hidden_states.dtype
)
else:
norm_hidden_states = self.norm1(hidden_states)
cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {}
attn_output = self.attn1(
norm_hidden_states,
encoder_hidden_states=encoder_hidden_states if self.only_cross_attention else None,
attention_mask=encoder_attention_mask if self.only_cross_attention else attention_mask,
**cross_attention_kwargs,
)
if self.use_ada_layer_norm_zero:
attn_output = gate_msa.unsqueeze(1) * attn_output
hidden_states = attn_output + hidden_states
# 2. Cross-Attention
if self.attn2 is not None:
norm_hidden_states = (
self.norm2(hidden_states, timestep) if self.use_ada_layer_norm else self.norm2(hidden_states)
)
attn_output = self.attn2(
norm_hidden_states,
encoder_hidden_states=encoder_hidden_states,
attention_mask=encoder_attention_mask,
**cross_attention_kwargs,
)
hidden_states = attn_output + hidden_states
# 3. Feed-forward
norm_hidden_states = self.norm3(hidden_states)
if self.use_ada_layer_norm_zero:
norm_hidden_states = norm_hidden_states * (1 + scale_mlp[:, None]) + shift_mlp[:, None]
if self._chunk_size is not None:
# "feed_forward_chunk_size" can be used to save memory
if norm_hidden_states.shape[self._chunk_dim] % self._chunk_size != 0:
raise ValueError(
f"`hidden_states` dimension to be chunked: {norm_hidden_states.shape[self._chunk_dim]} has to be divisible by chunk size: {self._chunk_size}. Make sure to set an appropriate `chunk_size` when calling `unet.enable_forward_chunking`."
)
num_chunks = norm_hidden_states.shape[self._chunk_dim] // self._chunk_size
ff_output = torch.cat(
[self.ff(hid_slice) for hid_slice in norm_hidden_states.chunk(num_chunks, dim=self._chunk_dim)],
dim=self._chunk_dim,
)
else:
ff_output = self.ff(norm_hidden_states)
if self.use_ada_layer_norm_zero:
ff_output = gate_mlp.unsqueeze(1) * ff_output
hidden_states = ff_output + hidden_states
return hidden_states
@@ -0,0 +1,301 @@
# Modified from CosyVoice https://github.com/FunAudioLLM/CosyVoice
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import numpy as np
import torch
import torchaudio as ta
from functools import lru_cache
from typing import Optional
from ..s3tokenizer import S3_SR, SPEECH_VOCAB_SIZE, S3Tokenizer
from .const import S3GEN_SR
from .flow import CausalMaskedDiffWithXvec
from .xvector import CAMPPlus
from .utils.mel import mel_spectrogram
from .f0_predictor import ConvRNNF0Predictor
from .hifigan import HiFTGenerator
from .transformer.upsample_encoder import UpsampleConformerEncoder
from .flow_matching import CausalConditionalCFM
from .decoder import ConditionalDecoder
from .configs import CFM_PARAMS
def drop_invalid_tokens(x):
assert len(x.shape) <= 2 and x.shape[0] == 1, "only batch size of one allowed for now"
return x[x < SPEECH_VOCAB_SIZE]
# TODO: global resampler cache
@lru_cache(100)
def get_resampler(src_sr, dst_sr, device):
return ta.transforms.Resample(src_sr, dst_sr).to(device)
class S3Token2Mel(torch.nn.Module):
"""
CosyVoice2's CFM decoder maps S3 speech tokens to mel-spectrograms.
TODO: make these modules configurable?
"""
def __init__(self):
super().__init__()
self.tokenizer = S3Tokenizer("speech_tokenizer_v2_25hz")
self.mel_extractor = mel_spectrogram # TODO: make it a torch module?
self.speaker_encoder = CAMPPlus() # use default args
encoder = UpsampleConformerEncoder(
output_size=512,
attention_heads=8,
linear_units=2048,
num_blocks=6,
dropout_rate=0.1,
positional_dropout_rate=0.1,
attention_dropout_rate=0.1,
normalize_before=True,
input_layer='linear',
pos_enc_layer_type='rel_pos_espnet',
selfattention_layer_type='rel_selfattn',
input_size=512,
use_cnn_module=False,
macaron_style=False,
)
estimator = ConditionalDecoder(
in_channels=320,
out_channels=80,
causal=True,
channels=[256],
dropout=0.0,
attention_head_dim=64,
n_blocks=4,
num_mid_blocks=12,
num_heads=8,
act_fn='gelu',
)
cfm_params = CFM_PARAMS
decoder = CausalConditionalCFM(
spk_emb_dim=80,
cfm_params=cfm_params,
estimator=estimator,
)
self.flow = CausalMaskedDiffWithXvec(
encoder=encoder,
decoder=decoder
)
self.resamplers = {}
@property
def device(self):
params = self.tokenizer.parameters()
return next(params).device
def embed_ref(
self,
ref_wav: torch.Tensor,
ref_sr: int,
device="auto",
ref_fade_out=True,
):
device = self.device if device == "auto" else device
if isinstance(ref_wav, np.ndarray):
ref_wav = torch.from_numpy(ref_wav).float()
if ref_wav.device != device:
ref_wav = ref_wav.to(device)
if len(ref_wav.shape) == 1:
ref_wav = ref_wav.unsqueeze(0) # (B, L)
if ref_wav.size(1) > 10 * ref_sr:
print("WARNING: cosydec received ref longer than 10s")
ref_wav_24 = ref_wav
if ref_sr != S3GEN_SR:
ref_wav_24 = get_resampler(ref_sr, S3GEN_SR, device)(ref_wav)
ref_mels_24 = self.mel_extractor(ref_wav_24).transpose(1, 2).to(device)
ref_mels_24_len = None
# Resample to 16kHz
ref_wav_16 = get_resampler(ref_sr, S3_SR, device)(ref_wav).to(device)
# Speaker embedding
# Ensure speaker_encoder is on the correct device
if next(self.speaker_encoder.parameters()).device != device:
self.speaker_encoder = self.speaker_encoder.to(device)
ref_x_vector = self.speaker_encoder.inference(ref_wav_16)
# Tokenize 16khz reference
ref_speech_tokens, ref_speech_token_lens = self.tokenizer(ref_wav_16)
# Make sure mel_len = 2 * stoken_len (happens when the input is not padded to multiple of 40ms)
if ref_mels_24.shape[1] != 2 * ref_speech_tokens.shape[1]:
logging.warning(
"Reference mel length is not equal to 2 * reference token length.\n"
)
ref_speech_tokens = ref_speech_tokens[:, :ref_mels_24.shape[1] // 2]
ref_speech_token_lens[0] = ref_speech_tokens.shape[1]
return dict(
prompt_token=ref_speech_tokens.to(device),
prompt_token_len=ref_speech_token_lens,
prompt_feat=ref_mels_24,
prompt_feat_len=ref_mels_24_len,
embedding=ref_x_vector,
)
def forward(
self,
speech_tokens: torch.LongTensor,
# locally-computed ref embedding (mutex with ref_dict)
ref_wav: Optional[torch.Tensor],
ref_sr: Optional[int],
# pre-computed ref embedding (prod API)
ref_dict: Optional[dict] = None,
finalize: bool = False,
):
"""
Generate waveforms from S3 speech tokens and a reference waveform, which the speaker timbre is inferred from.
NOTE:
- The speaker encoder accepts 16 kHz waveform.
- S3TokenizerV2 accepts 16 kHz waveform.
- The mel-spectrogram for the reference assumes 24 kHz input signal.
- This function is designed for batch_size=1 only.
Args
----
- `speech_tokens`: S3 speech tokens [B=1, T]
- `ref_wav`: reference waveform (`torch.Tensor` with shape=[B=1, T])
- `ref_sr`: reference sample rate
- `finalize`: whether streaming is finished or not. Note that if False, the last 3 tokens will be ignored.
"""
assert (ref_wav is None) ^ (ref_dict is None), f"Must provide exactly one of ref_wav or ref_dict (got {ref_wav} and {ref_dict})"
if ref_dict is None:
ref_dict = self.embed_ref(ref_wav, ref_sr)
else:
# type/device casting (all values will be numpy if it's from a prod API call)
for rk in list(ref_dict):
if isinstance(ref_dict[rk], np.ndarray):
ref_dict[rk] = torch.from_numpy(ref_dict[rk])
if torch.is_tensor(ref_dict[rk]):
ref_dict[rk] = ref_dict[rk].to(self.device)
if len(speech_tokens.shape) == 1:
speech_tokens = speech_tokens.unsqueeze(0)
# assert speech_tokens.shape[0] == 1, "only batch size of one allowed for now"
speech_token_lens = torch.LongTensor([speech_tokens.size(1)]).to(self.device)
output_mels, _ = self.flow.inference(
token=speech_tokens,
token_len=speech_token_lens,
finalize=finalize,
**ref_dict,
)
return output_mels
class S3Token2Wav(S3Token2Mel):
"""
The decoder of CosyVoice2 is a concat of token-to-mel (CFM) and a mel-to-waveform (HiFiGAN) modules.
TODO: make these modules configurable?
"""
def __init__(self):
super().__init__()
f0_predictor = ConvRNNF0Predictor()
self.mel2wav = HiFTGenerator(
sampling_rate=S3GEN_SR,
upsample_rates=[8, 5, 3],
upsample_kernel_sizes=[16, 11, 7],
source_resblock_kernel_sizes=[7, 7, 11],
source_resblock_dilation_sizes=[[1, 3, 5], [1, 3, 5], [1, 3, 5]],
f0_predictor=f0_predictor,
)
# silence out a few ms and fade audio in to reduce artifacts
n_trim = S3GEN_SR // 50 # 20ms = half of a frame
trim_fade = torch.zeros(2 * n_trim)
trim_fade[n_trim:] = (torch.cos(torch.linspace(torch.pi, 0, n_trim)) + 1) / 2
self.register_buffer("trim_fade", trim_fade, persistent=False) # (buffers get automatic device casting)
def forward(
self,
speech_tokens,
# locally-computed ref embedding (mutex with ref_dict)
ref_wav: Optional[torch.Tensor],
ref_sr: Optional[int],
# pre-computed ref embedding (prod API)
ref_dict: Optional[dict] = None,
finalize: bool = False
):
output_mels = super().forward(speech_tokens, ref_wav=ref_wav, ref_sr=ref_sr, ref_dict=ref_dict, finalize=finalize)
# TODO jrm: ignoring the speed control (mel interpolation) and the HiFTGAN caching mechanisms for now.
hift_cache_source = torch.zeros(1, 1, 0).to(self.device)
output_wavs, *_ = self.mel2wav.inference(speech_feat=output_mels, cache_source=hift_cache_source)
if not self.training:
# NOTE: ad-hoc method to reduce "spillover" from the reference clip.
output_wavs[:, :len(self.trim_fade)] *= self.trim_fade
return output_wavs
@torch.inference_mode()
def flow_inference(
self,
speech_tokens,
# locally-computed ref embedding (mutex with ref_dict)
ref_wav: Optional[torch.Tensor] = None,
ref_sr: Optional[int] = None,
# pre-computed ref embedding (prod API)
ref_dict: Optional[dict] = None,
finalize: bool = False,
):
return super().forward(speech_tokens, ref_wav=ref_wav, ref_sr=ref_sr, ref_dict=ref_dict, finalize=finalize)
@torch.inference_mode()
def hift_inference(self, speech_feat, cache_source: torch.Tensor = None):
if cache_source is None:
cache_source = torch.zeros(1, 1, 0).to(self.device)
return self.mel2wav.inference(speech_feat=speech_feat, cache_source=cache_source)
@torch.inference_mode()
def inference(
self,
speech_tokens,
# locally-computed ref embedding (mutex with ref_dict)
ref_wav: Optional[torch.Tensor] = None,
ref_sr: Optional[int] = None,
# pre-computed ref embedding (prod API)
ref_dict: Optional[dict] = None,
cache_source: torch.Tensor = None, # NOTE: this arg is for streaming, it can probably be removed here
finalize: bool = True,
):
output_mels = self.flow_inference(speech_tokens, ref_wav=ref_wav, ref_sr=ref_sr, ref_dict=ref_dict, finalize=finalize)
output_wavs, output_sources = self.hift_inference(output_mels, cache_source)
# NOTE: ad-hoc method to reduce "spillover" from the reference clip.
output_wavs[:, :len(self.trim_fade)] *= self.trim_fade
return output_wavs, output_sources
@@ -0,0 +1,84 @@
# Copyright (c) 2020 Johns Hopkins University (Shinji Watanabe)
# 2020 Northwestern Polytechnical University (Pengcheng Guo)
# 2020 Mobvoi Inc (Binbin Zhang)
# 2024 Alibaba Inc (Xiang Lyu)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Swish() activation function for Conformer."""
import torch
from torch import nn, sin, pow
from torch.nn import Parameter
class Swish(torch.nn.Module):
"""Construct an Swish object."""
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Return Swish activation function."""
return x * torch.sigmoid(x)
# Implementation adapted from https://github.com/EdwardDixon/snake under the MIT license.
# LICENSE is in incl_licenses directory.
class Snake(nn.Module):
'''
Implementation of a sine-based periodic activation function
Shape:
- Input: (B, C, T)
- Output: (B, C, T), same shape as the input
Parameters:
- alpha - trainable parameter
References:
- This activation function is from this paper by Liu Ziyin, Tilman Hartwig, Masahito Ueda:
https://arxiv.org/abs/2006.08195
Examples:
>>> a1 = snake(256)
>>> x = torch.randn(256)
>>> x = a1(x)
'''
def __init__(self, in_features, alpha=1.0, alpha_trainable=True, alpha_logscale=False):
'''
Initialization.
INPUT:
- in_features: shape of the input
- alpha: trainable parameter
alpha is initialized to 1 by default, higher values = higher-frequency.
alpha will be trained along with the rest of your model.
'''
super(Snake, self).__init__()
self.in_features = in_features
# initialize alpha
self.alpha_logscale = alpha_logscale
if self.alpha_logscale: # log scale alphas initialized to zeros
self.alpha = Parameter(torch.zeros(in_features) * alpha)
else: # linear scale alphas initialized to ones
self.alpha = Parameter(torch.ones(in_features) * alpha)
self.alpha.requires_grad = alpha_trainable
self.no_div_by_zero = 0.000000001
def forward(self, x):
'''
Forward pass of the function.
Applies the function to the input elementwise.
Snake ∶= x + 1/a * sin^2 (xa)
'''
alpha = self.alpha.unsqueeze(0).unsqueeze(-1) # line up with x to [B, C, T]
if self.alpha_logscale:
alpha = torch.exp(alpha)
x = x + (1.0 / (alpha + self.no_div_by_zero)) * pow(sin(x * alpha), 2)
return x
@@ -0,0 +1,330 @@
# Copyright (c) 2019 Shigeki Karita
# 2020 Mobvoi Inc (Binbin Zhang)
# 2022 Xingchen Song (sxc19@mails.tsinghua.edu.cn)
# 2024 Alibaba Inc (Xiang Lyu)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Multi-Head Attention layer definition."""
import math
from typing import Tuple
import torch
from torch import nn
class MultiHeadedAttention(nn.Module):
"""Multi-Head Attention layer.
Args:
n_head (int): The number of heads.
n_feat (int): The number of features.
dropout_rate (float): Dropout rate.
"""
def __init__(self,
n_head: int,
n_feat: int,
dropout_rate: float,
key_bias: bool = True):
"""Construct an MultiHeadedAttention object."""
super().__init__()
assert n_feat % n_head == 0
# We assume d_v always equals d_k
self.d_k = n_feat // n_head
self.h = n_head
self.linear_q = nn.Linear(n_feat, n_feat)
self.linear_k = nn.Linear(n_feat, n_feat, bias=key_bias)
self.linear_v = nn.Linear(n_feat, n_feat)
self.linear_out = nn.Linear(n_feat, n_feat)
self.dropout = nn.Dropout(p=dropout_rate)
def forward_qkv(
self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Transform query, key and value.
Args:
query (torch.Tensor): Query tensor (#batch, time1, size).
key (torch.Tensor): Key tensor (#batch, time2, size).
value (torch.Tensor): Value tensor (#batch, time2, size).
Returns:
torch.Tensor: Transformed query tensor, size
(#batch, n_head, time1, d_k).
torch.Tensor: Transformed key tensor, size
(#batch, n_head, time2, d_k).
torch.Tensor: Transformed value tensor, size
(#batch, n_head, time2, d_k).
"""
n_batch = query.size(0)
q = self.linear_q(query).view(n_batch, -1, self.h, self.d_k)
k = self.linear_k(key).view(n_batch, -1, self.h, self.d_k)
v = self.linear_v(value).view(n_batch, -1, self.h, self.d_k)
q = q.transpose(1, 2) # (batch, head, time1, d_k)
k = k.transpose(1, 2) # (batch, head, time2, d_k)
v = v.transpose(1, 2) # (batch, head, time2, d_k)
return q, k, v
def forward_attention(
self,
value: torch.Tensor,
scores: torch.Tensor,
mask: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool)
) -> torch.Tensor:
"""Compute attention context vector.
Args:
value (torch.Tensor): Transformed value, size
(#batch, n_head, time2, d_k).
scores (torch.Tensor): Attention score, size
(#batch, n_head, time1, time2).
mask (torch.Tensor): Mask, size (#batch, 1, time2) or
(#batch, time1, time2), (0, 0, 0) means fake mask.
Returns:
torch.Tensor: Transformed value (#batch, time1, d_model)
weighted by the attention score (#batch, time1, time2).
"""
n_batch = value.size(0)
# NOTE(xcsong): When will `if mask.size(2) > 0` be True?
# 1. onnx(16/4) [WHY? Because we feed real cache & real mask for the
# 1st chunk to ease the onnx export.]
# 2. pytorch training
if mask.size(2) > 0: # time2 > 0
mask = mask.unsqueeze(1).eq(0) # (batch, 1, *, time2)
# For last chunk, time2 might be larger than scores.size(-1)
mask = mask[:, :, :, :scores.size(-1)] # (batch, 1, *, time2)
scores = scores.masked_fill(mask, -float('inf'))
attn = torch.softmax(scores, dim=-1).masked_fill(
mask, 0.0) # (batch, head, time1, time2)
# NOTE(xcsong): When will `if mask.size(2) > 0` be False?
# 1. onnx(16/-1, -1/-1, 16/0)
# 2. jit (16/-1, -1/-1, 16/0, 16/4)
else:
attn = torch.softmax(scores, dim=-1) # (batch, head, time1, time2)
p_attn = self.dropout(attn)
x = torch.matmul(p_attn, value) # (batch, head, time1, d_k)
x = (x.transpose(1, 2).contiguous().view(n_batch, -1,
self.h * self.d_k)
) # (batch, time1, d_model)
return self.linear_out(x) # (batch, time1, d_model)
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
mask: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool),
pos_emb: torch.Tensor = torch.empty(0),
cache: torch.Tensor = torch.zeros((0, 0, 0, 0))
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Compute scaled dot product attention.
Args:
query (torch.Tensor): Query tensor (#batch, time1, size).
key (torch.Tensor): Key tensor (#batch, time2, size).
value (torch.Tensor): Value tensor (#batch, time2, size).
mask (torch.Tensor): Mask tensor (#batch, 1, time2) or
(#batch, time1, time2).
1.When applying cross attention between decoder and encoder,
the batch padding mask for input is in (#batch, 1, T) shape.
2.When applying self attention of encoder,
the mask is in (#batch, T, T) shape.
3.When applying self attention of decoder,
the mask is in (#batch, L, L) shape.
4.If the different position in decoder see different block
of the encoder, such as Mocha, the passed in mask could be
in (#batch, L, T) shape. But there is no such case in current
CosyVoice.
cache (torch.Tensor): Cache tensor (1, head, cache_t, d_k * 2),
where `cache_t == chunk_size * num_decoding_left_chunks`
and `head * d_k == size`
Returns:
torch.Tensor: Output tensor (#batch, time1, d_model).
torch.Tensor: Cache tensor (1, head, cache_t + time1, d_k * 2)
where `cache_t == chunk_size * num_decoding_left_chunks`
and `head * d_k == size`
"""
q, k, v = self.forward_qkv(query, key, value)
# NOTE(xcsong):
# when export onnx model, for 1st chunk, we feed
# cache(1, head, 0, d_k * 2) (16/-1, -1/-1, 16/0 mode)
# or cache(1, head, real_cache_t, d_k * 2) (16/4 mode).
# In all modes, `if cache.size(0) > 0` will alwayse be `True`
# and we will always do splitting and
# concatnation(this will simplify onnx export). Note that
# it's OK to concat & split zero-shaped tensors(see code below).
# when export jit model, for 1st chunk, we always feed
# cache(0, 0, 0, 0) since jit supports dynamic if-branch.
# >>> a = torch.ones((1, 2, 0, 4))
# >>> b = torch.ones((1, 2, 3, 4))
# >>> c = torch.cat((a, b), dim=2)
# >>> torch.equal(b, c) # True
# >>> d = torch.split(a, 2, dim=-1)
# >>> torch.equal(d[0], d[1]) # True
if cache.size(0) > 0:
key_cache, value_cache = torch.split(cache,
cache.size(-1) // 2,
dim=-1)
k = torch.cat([key_cache, k], dim=2)
v = torch.cat([value_cache, v], dim=2)
# NOTE(xcsong): We do cache slicing in encoder.forward_chunk, since it's
# non-trivial to calculate `next_cache_start` here.
new_cache = torch.cat((k, v), dim=-1)
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k)
return self.forward_attention(v, scores, mask), new_cache
class RelPositionMultiHeadedAttention(MultiHeadedAttention):
"""Multi-Head Attention layer with relative position encoding.
Paper: https://arxiv.org/abs/1901.02860
Args:
n_head (int): The number of heads.
n_feat (int): The number of features.
dropout_rate (float): Dropout rate.
"""
def __init__(self,
n_head: int,
n_feat: int,
dropout_rate: float,
key_bias: bool = True):
"""Construct an RelPositionMultiHeadedAttention object."""
super().__init__(n_head, n_feat, dropout_rate, key_bias)
# linear transformation for positional encoding
self.linear_pos = nn.Linear(n_feat, n_feat, bias=False)
# these two learnable bias are used in matrix c and matrix d
# as described in https://arxiv.org/abs/1901.02860 Section 3.3
self.pos_bias_u = nn.Parameter(torch.Tensor(self.h, self.d_k))
self.pos_bias_v = nn.Parameter(torch.Tensor(self.h, self.d_k))
torch.nn.init.xavier_uniform_(self.pos_bias_u)
torch.nn.init.xavier_uniform_(self.pos_bias_v)
def rel_shift(self, x: torch.Tensor) -> torch.Tensor:
"""Compute relative positional encoding.
Args:
x (torch.Tensor): Input tensor (batch, head, time1, 2*time1-1).
time1 means the length of query vector.
Returns:
torch.Tensor: Output tensor.
"""
zero_pad = torch.zeros((x.size()[0], x.size()[1], x.size()[2], 1),
device=x.device,
dtype=x.dtype)
x_padded = torch.cat([zero_pad, x], dim=-1)
x_padded = x_padded.view(x.size()[0],
x.size()[1],
x.size(3) + 1, x.size(2))
x = x_padded[:, :, 1:].view_as(x)[
:, :, :, : x.size(-1) // 2 + 1
] # only keep the positions from 0 to time2
return x
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
mask: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool),
pos_emb: torch.Tensor = torch.empty(0),
cache: torch.Tensor = torch.zeros((0, 0, 0, 0))
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Compute 'Scaled Dot Product Attention' with rel. positional encoding.
Args:
query (torch.Tensor): Query tensor (#batch, time1, size).
key (torch.Tensor): Key tensor (#batch, time2, size).
value (torch.Tensor): Value tensor (#batch, time2, size).
mask (torch.Tensor): Mask tensor (#batch, 1, time2) or
(#batch, time1, time2), (0, 0, 0) means fake mask.
pos_emb (torch.Tensor): Positional embedding tensor
(#batch, time2, size).
cache (torch.Tensor): Cache tensor (1, head, cache_t, d_k * 2),
where `cache_t == chunk_size * num_decoding_left_chunks`
and `head * d_k == size`
Returns:
torch.Tensor: Output tensor (#batch, time1, d_model).
torch.Tensor: Cache tensor (1, head, cache_t + time1, d_k * 2)
where `cache_t == chunk_size * num_decoding_left_chunks`
and `head * d_k == size`
"""
q, k, v = self.forward_qkv(query, key, value)
q = q.transpose(1, 2) # (batch, time1, head, d_k)
# NOTE(xcsong):
# when export onnx model, for 1st chunk, we feed
# cache(1, head, 0, d_k * 2) (16/-1, -1/-1, 16/0 mode)
# or cache(1, head, real_cache_t, d_k * 2) (16/4 mode).
# In all modes, `if cache.size(0) > 0` will alwayse be `True`
# and we will always do splitting and
# concatnation(this will simplify onnx export). Note that
# it's OK to concat & split zero-shaped tensors(see code below).
# when export jit model, for 1st chunk, we always feed
# cache(0, 0, 0, 0) since jit supports dynamic if-branch.
# >>> a = torch.ones((1, 2, 0, 4))
# >>> b = torch.ones((1, 2, 3, 4))
# >>> c = torch.cat((a, b), dim=2)
# >>> torch.equal(b, c) # True
# >>> d = torch.split(a, 2, dim=-1)
# >>> torch.equal(d[0], d[1]) # True
if cache.size(0) > 0:
key_cache, value_cache = torch.split(cache,
cache.size(-1) // 2,
dim=-1)
k = torch.cat([key_cache, k], dim=2)
v = torch.cat([value_cache, v], dim=2)
# NOTE(xcsong): We do cache slicing in encoder.forward_chunk, since it's
# non-trivial to calculate `next_cache_start` here.
new_cache = torch.cat((k, v), dim=-1)
n_batch_pos = pos_emb.size(0)
p = self.linear_pos(pos_emb).view(n_batch_pos, -1, self.h, self.d_k)
p = p.transpose(1, 2) # (batch, head, time1, d_k)
# (batch, head, time1, d_k)
q_with_bias_u = (q + self.pos_bias_u.to(q.device)).transpose(1, 2)
# (batch, head, time1, d_k)
q_with_bias_v = (q + self.pos_bias_v.to(q.device)).transpose(1, 2)
# compute attention score
# first compute matrix a and matrix c
# as described in https://arxiv.org/abs/1901.02860 Section 3.3
# (batch, head, time1, time2)
matrix_ac = torch.matmul(q_with_bias_u, k.transpose(-2, -1))
# compute matrix b and matrix d
# (batch, head, time1, time2)
matrix_bd = torch.matmul(q_with_bias_v, p.transpose(-2, -1))
# NOTE(Xiang Lyu): Keep rel_shift since espnet rel_pos_emb is used
if matrix_ac.shape != matrix_bd.shape:
matrix_bd = self.rel_shift(matrix_bd)
scores = (matrix_ac + matrix_bd) / math.sqrt(
self.d_k) # (batch, head, time1, time2)
return self.forward_attention(v, scores, mask), new_cache
@@ -0,0 +1,145 @@
# Copyright (c) 2020 Mobvoi Inc. (authors: Binbin Zhang, Di Wu)
# 2024 Alibaba Inc (Xiang Lyu)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Modified from ESPnet(https://github.com/espnet/espnet)
"""ConvolutionModule definition."""
from typing import Tuple
import torch
from torch import nn
class ConvolutionModule(nn.Module):
"""ConvolutionModule in Conformer model."""
def __init__(self,
channels: int,
kernel_size: int = 15,
activation: nn.Module = nn.ReLU(),
norm: str = "batch_norm",
causal: bool = False,
bias: bool = True):
"""Construct an ConvolutionModule object.
Args:
channels (int): The number of channels of conv layers.
kernel_size (int): Kernel size of conv layers.
causal (int): Whether use causal convolution or not
"""
super().__init__()
self.pointwise_conv1 = nn.Conv1d(
channels,
2 * channels,
kernel_size=1,
stride=1,
padding=0,
bias=bias,
)
# self.lorder is used to distinguish if it's a causal convolution,
# if self.lorder > 0: it's a causal convolution, the input will be
# padded with self.lorder frames on the left in forward.
# else: it's a symmetrical convolution
if causal:
padding = 0
self.lorder = kernel_size - 1
else:
# kernel_size should be an odd number for none causal convolution
assert (kernel_size - 1) % 2 == 0
padding = (kernel_size - 1) // 2
self.lorder = 0
self.depthwise_conv = nn.Conv1d(
channels,
channels,
kernel_size,
stride=1,
padding=padding,
groups=channels,
bias=bias,
)
assert norm in ['batch_norm', 'layer_norm']
if norm == "batch_norm":
self.use_layer_norm = False
self.norm = nn.BatchNorm1d(channels)
else:
self.use_layer_norm = True
self.norm = nn.LayerNorm(channels)
self.pointwise_conv2 = nn.Conv1d(
channels,
channels,
kernel_size=1,
stride=1,
padding=0,
bias=bias,
)
self.activation = activation
def forward(
self,
x: torch.Tensor,
mask_pad: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool),
cache: torch.Tensor = torch.zeros((0, 0, 0)),
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Compute convolution module.
Args:
x (torch.Tensor): Input tensor (#batch, time, channels).
mask_pad (torch.Tensor): used for batch padding (#batch, 1, time),
(0, 0, 0) means fake mask.
cache (torch.Tensor): left context cache, it is only
used in causal convolution (#batch, channels, cache_t),
(0, 0, 0) meas fake cache.
Returns:
torch.Tensor: Output tensor (#batch, time, channels).
"""
# exchange the temporal dimension and the feature dimension
x = x.transpose(1, 2) # (#batch, channels, time)
# mask batch padding
if mask_pad.size(2) > 0: # time > 0
x.masked_fill_(~mask_pad, 0.0)
if self.lorder > 0:
if cache.size(2) == 0: # cache_t == 0
x = nn.functional.pad(x, (self.lorder, 0), 'constant', 0.0)
else:
assert cache.size(0) == x.size(0) # equal batch
assert cache.size(1) == x.size(1) # equal channel
x = torch.cat((cache, x), dim=2)
assert (x.size(2) > self.lorder)
new_cache = x[:, :, -self.lorder:]
else:
# It's better we just return None if no cache is required,
# However, for JIT export, here we just fake one tensor instead of
# None.
new_cache = torch.zeros((0, 0, 0), dtype=x.dtype, device=x.device)
# GLU mechanism
x = self.pointwise_conv1(x) # (batch, 2*channel, dim)
x = nn.functional.glu(x, dim=1) # (batch, channel, dim)
# 1D Depthwise Conv
x = self.depthwise_conv(x)
if self.use_layer_norm:
x = x.transpose(1, 2)
x = self.activation(self.norm(x))
if self.use_layer_norm:
x = x.transpose(1, 2)
x = self.pointwise_conv2(x)
# mask batch padding
if mask_pad.size(2) > 0: # time > 0
x.masked_fill_(~mask_pad, 0.0)
return x.transpose(1, 2), new_cache
@@ -0,0 +1,294 @@
# Copyright (c) 2020 Mobvoi Inc. (authors: Binbin Zhang, Di Wu)
# 2024 Alibaba Inc (Xiang Lyu)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Modified from ESPnet(https://github.com/espnet/espnet)
"""Positonal Encoding Module."""
import math
from typing import Tuple, Union
import torch
import torch.nn.functional as F
import numpy as np
class PositionalEncoding(torch.nn.Module):
"""Positional encoding.
:param int d_model: embedding dim
:param float dropout_rate: dropout rate
:param int max_len: maximum input length
PE(pos, 2i) = sin(pos/(10000^(2i/dmodel)))
PE(pos, 2i+1) = cos(pos/(10000^(2i/dmodel)))
"""
def __init__(self,
d_model: int,
dropout_rate: float,
max_len: int = 5000,
reverse: bool = False):
"""Construct an PositionalEncoding object."""
super().__init__()
self.d_model = d_model
self.xscale = math.sqrt(self.d_model)
self.dropout = torch.nn.Dropout(p=dropout_rate)
self.max_len = max_len
self.pe = torch.zeros(self.max_len, self.d_model)
position = torch.arange(0, self.max_len,
dtype=torch.float32).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, self.d_model, 2, dtype=torch.float32) *
-(math.log(10000.0) / self.d_model))
self.pe[:, 0::2] = torch.sin(position * div_term)
self.pe[:, 1::2] = torch.cos(position * div_term)
self.pe = self.pe.unsqueeze(0)
def forward(self,
x: torch.Tensor,
offset: Union[int, torch.Tensor] = 0) \
-> Tuple[torch.Tensor, torch.Tensor]:
"""Add positional encoding.
Args:
x (torch.Tensor): Input. Its shape is (batch, time, ...)
offset (int, torch.tensor): position offset
Returns:
torch.Tensor: Encoded tensor. Its shape is (batch, time, ...)
torch.Tensor: for compatibility to RelPositionalEncoding
"""
self.pe = self.pe.to(x.device)
pos_emb = self.position_encoding(offset, x.size(1), False)
x = x * self.xscale + pos_emb
return self.dropout(x), self.dropout(pos_emb)
def position_encoding(self,
offset: Union[int, torch.Tensor],
size: int,
apply_dropout: bool = True) -> torch.Tensor:
""" For getting encoding in a streaming fashion
Attention!!!!!
we apply dropout only once at the whole utterance level in a none
streaming way, but will call this function several times with
increasing input size in a streaming scenario, so the dropout will
be applied several times.
Args:
offset (int or torch.tensor): start offset
size (int): required size of position encoding
Returns:
torch.Tensor: Corresponding encoding
"""
# How to subscript a Union type:
# https://github.com/pytorch/pytorch/issues/69434
if isinstance(offset, int):
assert offset + size <= self.max_len
pos_emb = self.pe[:, offset:offset + size]
elif isinstance(offset, torch.Tensor) and offset.dim() == 0: # scalar
assert offset + size <= self.max_len
pos_emb = self.pe[:, offset:offset + size]
else: # for batched streaming decoding on GPU
assert torch.max(offset) + size <= self.max_len
index = offset.unsqueeze(1) + \
torch.arange(0, size).to(offset.device) # B X T
flag = index > 0
# remove negative offset
index = index * flag
pos_emb = F.embedding(index, self.pe[0]) # B X T X d_model
if apply_dropout:
pos_emb = self.dropout(pos_emb)
return pos_emb
class RelPositionalEncoding(PositionalEncoding):
"""Relative positional encoding module.
See : Appendix B in https://arxiv.org/abs/1901.02860
Args:
d_model (int): Embedding dimension.
dropout_rate (float): Dropout rate.
max_len (int): Maximum input length.
"""
def __init__(self, d_model: int, dropout_rate: float, max_len: int = 5000):
"""Initialize class."""
super().__init__(d_model, dropout_rate, max_len, reverse=True)
def forward(self,
x: torch.Tensor,
offset: Union[int, torch.Tensor] = 0) \
-> Tuple[torch.Tensor, torch.Tensor]:
"""Compute positional encoding.
Args:
x (torch.Tensor): Input tensor (batch, time, `*`).
Returns:
torch.Tensor: Encoded tensor (batch, time, `*`).
torch.Tensor: Positional embedding tensor (1, time, `*`).
"""
self.pe = self.pe.to(x.device)
x = x * self.xscale
pos_emb = self.position_encoding(offset, x.size(1), False)
return self.dropout(x), self.dropout(pos_emb)
class WhisperPositionalEncoding(PositionalEncoding):
""" Sinusoids position encoding used in openai-whisper.encoder
"""
def __init__(self, d_model: int, dropout_rate: float, max_len: int = 1500):
super().__init__(d_model, dropout_rate, max_len)
self.xscale = 1.0
log_timescale_increment = np.log(10000) / (d_model // 2 - 1)
inv_timescales = torch.exp(-log_timescale_increment *
torch.arange(d_model // 2))
scaled_time = torch.arange(max_len)[:, np.newaxis] * \
inv_timescales[np.newaxis, :]
pe = torch.cat([torch.sin(scaled_time), torch.cos(scaled_time)], dim=1)
delattr(self, "pe")
self.register_buffer("pe", pe.unsqueeze(0))
class LearnablePositionalEncoding(PositionalEncoding):
""" Learnable position encoding used in openai-whisper.decoder
"""
def __init__(self, d_model: int, dropout_rate: float, max_len: int = 448):
super().__init__(d_model, dropout_rate, max_len)
# NOTE(xcsong): overwrite self.pe & self.xscale
self.pe = torch.nn.Parameter(torch.empty(1, max_len, d_model))
self.xscale = 1.0
class NoPositionalEncoding(torch.nn.Module):
""" No position encoding
"""
def __init__(self, d_model: int, dropout_rate: float):
super().__init__()
self.d_model = d_model
self.dropout = torch.nn.Dropout(p=dropout_rate)
def forward(self,
x: torch.Tensor,
offset: Union[int, torch.Tensor] = 0) \
-> Tuple[torch.Tensor, torch.Tensor]:
""" Just return zero vector for interface compatibility
"""
pos_emb = torch.zeros(1, x.size(1), self.d_model).to(x.device)
return self.dropout(x), pos_emb
def position_encoding(self, offset: Union[int, torch.Tensor],
size: int) -> torch.Tensor:
return torch.zeros(1, size, self.d_model)
class EspnetRelPositionalEncoding(torch.nn.Module):
"""Relative positional encoding module (new implementation).
Details can be found in https://github.com/espnet/espnet/pull/2816.
See : Appendix B in https://arxiv.org/abs/1901.02860
Args:
d_model (int): Embedding dimension.
dropout_rate (float): Dropout rate.
max_len (int): Maximum input length.
"""
def __init__(self, d_model: int, dropout_rate: float, max_len: int = 5000):
"""Construct an PositionalEncoding object."""
super(EspnetRelPositionalEncoding, self).__init__()
self.d_model = d_model
self.xscale = math.sqrt(self.d_model)
self.dropout = torch.nn.Dropout(p=dropout_rate)
self.pe = None
self.extend_pe(torch.tensor(0.0).expand(1, max_len))
def extend_pe(self, x: torch.Tensor):
"""Reset the positional encodings."""
if self.pe is not None:
# self.pe contains both positive and negative parts
# the length of self.pe is 2 * input_len - 1
if self.pe.size(1) >= x.size(1) * 2 - 1:
if self.pe.dtype != x.dtype or self.pe.device != x.device:
self.pe = self.pe.to(dtype=x.dtype, device=x.device)
return
# Suppose `i` means to the position of query vecotr and `j` means the
# position of key vector. We use position relative positions when keys
# are to the left (i>j) and negative relative positions otherwise (i<j).
pe_positive = torch.zeros(x.size(1), self.d_model)
pe_negative = torch.zeros(x.size(1), self.d_model)
position = torch.arange(0, x.size(1), dtype=torch.float32).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, self.d_model, 2, dtype=torch.float32)
* -(math.log(10000.0) / self.d_model)
)
pe_positive[:, 0::2] = torch.sin(position * div_term)
pe_positive[:, 1::2] = torch.cos(position * div_term)
pe_negative[:, 0::2] = torch.sin(-1 * position * div_term)
pe_negative[:, 1::2] = torch.cos(-1 * position * div_term)
# Reserve the order of positive indices and concat both positive and
# negative indices. This is used to support the shifting trick
# as in https://arxiv.org/abs/1901.02860
pe_positive = torch.flip(pe_positive, [0]).unsqueeze(0)
pe_negative = pe_negative[1:].unsqueeze(0)
pe = torch.cat([pe_positive, pe_negative], dim=1)
self.pe = pe.to(device=x.device, dtype=x.dtype)
def forward(self, x: torch.Tensor, offset: Union[int, torch.Tensor] = 0) \
-> Tuple[torch.Tensor, torch.Tensor]:
"""Add positional encoding.
Args:
x (torch.Tensor): Input tensor (batch, time, `*`).
Returns:
torch.Tensor: Encoded tensor (batch, time, `*`).
"""
self.extend_pe(x)
x = x * self.xscale
pos_emb = self.position_encoding(size=x.size(1), offset=offset)
return self.dropout(x), self.dropout(pos_emb)
def position_encoding(self,
offset: Union[int, torch.Tensor],
size: int) -> torch.Tensor:
""" For getting encoding in a streaming fashion
Attention!!!!!
we apply dropout only once at the whole utterance level in a none
streaming way, but will call this function several times with
increasing input size in a streaming scenario, so the dropout will
be applied several times.
Args:
offset (int or torch.tensor): start offset
size (int): required size of position encoding
Returns:
torch.Tensor: Corresponding encoding
"""
pos_emb = self.pe[
:,
self.pe.size(1) // 2 - size + 1: self.pe.size(1) // 2 + size,
]
return pos_emb
@@ -0,0 +1,236 @@
# Copyright (c) 2021 Mobvoi Inc (Binbin Zhang, Di Wu)
# 2022 Xingchen Song (sxc19@mails.tsinghua.edu.cn)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Modified from ESPnet(https://github.com/espnet/espnet)
"""Encoder self-attention layer definition."""
from typing import Optional, Tuple
import torch
from torch import nn
class TransformerEncoderLayer(nn.Module):
"""Encoder layer module.
Args:
size (int): Input dimension.
self_attn (torch.nn.Module): Self-attention module instance.
`MultiHeadedAttention` or `RelPositionMultiHeadedAttention`
instance can be used as the argument.
feed_forward (torch.nn.Module): Feed-forward module instance.
`PositionwiseFeedForward`, instance can be used as the argument.
dropout_rate (float): Dropout rate.
normalize_before (bool):
True: use layer_norm before each sub-block.
False: to use layer_norm after each sub-block.
"""
def __init__(
self,
size: int,
self_attn: torch.nn.Module,
feed_forward: torch.nn.Module,
dropout_rate: float,
normalize_before: bool = True,
):
"""Construct an EncoderLayer object."""
super().__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
self.norm1 = nn.LayerNorm(size, eps=1e-12)
self.norm2 = nn.LayerNorm(size, eps=1e-12)
self.dropout = nn.Dropout(dropout_rate)
self.size = size
self.normalize_before = normalize_before
def forward(
self,
x: torch.Tensor,
mask: torch.Tensor,
pos_emb: torch.Tensor,
mask_pad: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool),
att_cache: torch.Tensor = torch.zeros((0, 0, 0, 0)),
cnn_cache: torch.Tensor = torch.zeros((0, 0, 0, 0)),
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Compute encoded features.
Args:
x (torch.Tensor): (#batch, time, size)
mask (torch.Tensor): Mask tensor for the input (#batch, time,time),
(0, 0, 0) means fake mask.
pos_emb (torch.Tensor): just for interface compatibility
to ConformerEncoderLayer
mask_pad (torch.Tensor): does not used in transformer layer,
just for unified api with conformer.
att_cache (torch.Tensor): Cache tensor of the KEY & VALUE
(#batch=1, head, cache_t1, d_k * 2), head * d_k == size.
cnn_cache (torch.Tensor): Convolution cache in conformer layer
(#batch=1, size, cache_t2), not used here, it's for interface
compatibility to ConformerEncoderLayer.
Returns:
torch.Tensor: Output tensor (#batch, time, size).
torch.Tensor: Mask tensor (#batch, time, time).
torch.Tensor: att_cache tensor,
(#batch=1, head, cache_t1 + time, d_k * 2).
torch.Tensor: cnn_cahce tensor (#batch=1, size, cache_t2).
"""
residual = x
if self.normalize_before:
x = self.norm1(x)
x_att, new_att_cache = self.self_attn(x, x, x, mask, pos_emb=pos_emb, cache=att_cache)
x = residual + self.dropout(x_att)
if not self.normalize_before:
x = self.norm1(x)
residual = x
if self.normalize_before:
x = self.norm2(x)
x = residual + self.dropout(self.feed_forward(x))
if not self.normalize_before:
x = self.norm2(x)
fake_cnn_cache = torch.zeros((0, 0, 0), dtype=x.dtype, device=x.device)
return x, mask, new_att_cache, fake_cnn_cache
class ConformerEncoderLayer(nn.Module):
"""Encoder layer module.
Args:
size (int): Input dimension.
self_attn (torch.nn.Module): Self-attention module instance.
`MultiHeadedAttention` or `RelPositionMultiHeadedAttention`
instance can be used as the argument.
feed_forward (torch.nn.Module): Feed-forward module instance.
`PositionwiseFeedForward` instance can be used as the argument.
feed_forward_macaron (torch.nn.Module): Additional feed-forward module
instance.
`PositionwiseFeedForward` instance can be used as the argument.
conv_module (torch.nn.Module): Convolution module instance.
`ConvlutionModule` instance can be used as the argument.
dropout_rate (float): Dropout rate.
normalize_before (bool):
True: use layer_norm before each sub-block.
False: use layer_norm after each sub-block.
"""
def __init__(
self,
size: int,
self_attn: torch.nn.Module,
feed_forward: Optional[nn.Module] = None,
feed_forward_macaron: Optional[nn.Module] = None,
conv_module: Optional[nn.Module] = None,
dropout_rate: float = 0.1,
normalize_before: bool = True,
):
"""Construct an EncoderLayer object."""
super().__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
self.feed_forward_macaron = feed_forward_macaron
self.conv_module = conv_module
self.norm_ff = nn.LayerNorm(size, eps=1e-12) # for the FNN module
self.norm_mha = nn.LayerNorm(size, eps=1e-12) # for the MHA module
if feed_forward_macaron is not None:
self.norm_ff_macaron = nn.LayerNorm(size, eps=1e-12)
self.ff_scale = 0.5
else:
self.ff_scale = 1.0
if self.conv_module is not None:
self.norm_conv = nn.LayerNorm(size, eps=1e-12) # for the CNN module
self.norm_final = nn.LayerNorm(
size, eps=1e-12) # for the final output of the block
self.dropout = nn.Dropout(dropout_rate)
self.size = size
self.normalize_before = normalize_before
def forward(
self,
x: torch.Tensor,
mask: torch.Tensor,
pos_emb: torch.Tensor,
mask_pad: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool),
att_cache: torch.Tensor = torch.zeros((0, 0, 0, 0)),
cnn_cache: torch.Tensor = torch.zeros((0, 0, 0, 0)),
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Compute encoded features.
Args:
x (torch.Tensor): (#batch, time, size)
mask (torch.Tensor): Mask tensor for the input (#batch, time,time),
(0, 0, 0) means fake mask.
pos_emb (torch.Tensor): positional encoding, must not be None
for ConformerEncoderLayer.
mask_pad (torch.Tensor): batch padding mask used for conv module.
(#batch, 1,time), (0, 0, 0) means fake mask.
att_cache (torch.Tensor): Cache tensor of the KEY & VALUE
(#batch=1, head, cache_t1, d_k * 2), head * d_k == size.
cnn_cache (torch.Tensor): Convolution cache in conformer layer
(#batch=1, size, cache_t2)
Returns:
torch.Tensor: Output tensor (#batch, time, size).
torch.Tensor: Mask tensor (#batch, time, time).
torch.Tensor: att_cache tensor,
(#batch=1, head, cache_t1 + time, d_k * 2).
torch.Tensor: cnn_cahce tensor (#batch, size, cache_t2).
"""
# whether to use macaron style
if self.feed_forward_macaron is not None:
residual = x
if self.normalize_before:
x = self.norm_ff_macaron(x)
x = residual + self.ff_scale * self.dropout(
self.feed_forward_macaron(x))
if not self.normalize_before:
x = self.norm_ff_macaron(x)
# multi-headed self-attention module
residual = x
if self.normalize_before:
x = self.norm_mha(x)
x_att, new_att_cache = self.self_attn(x, x, x, mask, pos_emb,
att_cache)
x = residual + self.dropout(x_att)
if not self.normalize_before:
x = self.norm_mha(x)
# convolution module
# Fake new cnn cache here, and then change it in conv_module
new_cnn_cache = torch.zeros((0, 0, 0), dtype=x.dtype, device=x.device)
if self.conv_module is not None:
residual = x
if self.normalize_before:
x = self.norm_conv(x)
x, new_cnn_cache = self.conv_module(x, mask_pad, cnn_cache)
x = residual + self.dropout(x)
if not self.normalize_before:
x = self.norm_conv(x)
# feed forward module
residual = x
if self.normalize_before:
x = self.norm_ff(x)
x = residual + self.ff_scale * self.dropout(self.feed_forward(x))
if not self.normalize_before:
x = self.norm_ff(x)
if self.conv_module is not None:
x = self.norm_final(x)
return x, mask, new_att_cache, new_cnn_cache
@@ -0,0 +1,115 @@
# Copyright (c) 2019 Shigeki Karita
# 2020 Mobvoi Inc (Binbin Zhang)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Positionwise feed forward layer definition."""
import torch
class PositionwiseFeedForward(torch.nn.Module):
"""Positionwise feed forward layer.
FeedForward are appied on each position of the sequence.
The output dim is same with the input dim.
Args:
idim (int): Input dimenstion.
hidden_units (int): The number of hidden units.
dropout_rate (float): Dropout rate.
activation (torch.nn.Module): Activation function
"""
def __init__(
self,
idim: int,
hidden_units: int,
dropout_rate: float,
activation: torch.nn.Module = torch.nn.ReLU(),
):
"""Construct a PositionwiseFeedForward object."""
super(PositionwiseFeedForward, self).__init__()
self.w_1 = torch.nn.Linear(idim, hidden_units)
self.activation = activation
self.dropout = torch.nn.Dropout(dropout_rate)
self.w_2 = torch.nn.Linear(hidden_units, idim)
def forward(self, xs: torch.Tensor) -> torch.Tensor:
"""Forward function.
Args:
xs: input tensor (B, L, D)
Returns:
output tensor, (B, L, D)
"""
return self.w_2(self.dropout(self.activation(self.w_1(xs))))
class MoEFFNLayer(torch.nn.Module):
"""
Mixture of expert with Positionwise feed forward layer
See also figure 1 in https://arxiv.org/pdf/2305.15663.pdf
The output dim is same with the input dim.
Modified from https://github.com/Lightning-AI/lit-gpt/pull/823
https://github.com/mistralai/mistral-src/blob/b46d6/moe_one_file_ref.py#L203-L219
Args:
n_expert: number of expert.
n_expert_per_token: The actual number of experts used for each frame
idim (int): Input dimenstion.
hidden_units (int): The number of hidden units.
dropout_rate (float): Dropout rate.
activation (torch.nn.Module): Activation function
"""
def __init__(
self,
n_expert: int,
n_expert_per_token: int,
idim: int,
hidden_units: int,
dropout_rate: float,
activation: torch.nn.Module = torch.nn.ReLU(),
):
super(MoEFFNLayer, self).__init__()
self.gate = torch.nn.Linear(idim, n_expert, bias=False)
self.experts = torch.nn.ModuleList(
PositionwiseFeedForward(idim, hidden_units, dropout_rate,
activation) for _ in range(n_expert))
self.n_expert_per_token = n_expert_per_token
def forward(self, xs: torch.Tensor) -> torch.Tensor:
"""Foward function.
Args:
xs: input tensor (B, L, D)
Returns:
output tensor, (B, L, D)
"""
B, L, D = xs.size(
) # batch size, sequence length, embedding dimension (idim)
xs = xs.view(-1, D) # (B*L, D)
router = self.gate(xs) # (B*L, n_expert)
logits, indices = torch.topk(
router, self.n_expert_per_token
) # probs:(B*L, n_expert), indices: (B*L, n_expert)
weights = torch.nn.functional.softmax(
logits, dim=1,
dtype=torch.float).to(dtype=xs.dtype) # (B*L, n_expert_per_token)
output = torch.zeros_like(xs) # (B*L, D)
for i, expert in enumerate(self.experts):
mask = indices == i
batch_idx, ith_expert = torch.where(mask)
output[batch_idx] += weights[batch_idx, ith_expert, None] * expert(
xs[batch_idx])
return output.view(B, L, D)
@@ -0,0 +1,383 @@
# Copyright (c) 2021 Mobvoi Inc (Binbin Zhang, Di Wu)
# 2024 Alibaba Inc (Xiang Lyu)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Modified from ESPnet(https://github.com/espnet/espnet)
"""Subsampling layer definition."""
from typing import Tuple, Union
import torch
class BaseSubsampling(torch.nn.Module):
def __init__(self):
super().__init__()
self.right_context = 0
self.subsampling_rate = 1
def position_encoding(self, offset: Union[int, torch.Tensor],
size: int) -> torch.Tensor:
return self.pos_enc.position_encoding(offset, size)
class EmbedinigNoSubsampling(BaseSubsampling):
"""Embedding input without subsampling
"""
def __init__(self, idim: int, odim: int, dropout_rate: float,
pos_enc_class: torch.nn.Module):
super().__init__()
self.embed = torch.nn.Embedding(idim, odim)
self.pos_enc = pos_enc_class
def forward(
self,
x: torch.Tensor,
x_mask: torch.Tensor,
offset: Union[int, torch.Tensor] = 0
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Input x.
Args:
x (torch.Tensor): Input tensor (#batch, time, idim).
x_mask (torch.Tensor): Input mask (#batch, 1, time).
Returns:
torch.Tensor: linear input tensor (#batch, time', odim),
where time' = time .
torch.Tensor: linear input mask (#batch, 1, time'),
where time' = time .
"""
x = self.embed(x)
x, pos_emb = self.pos_enc(x, offset)
return x, pos_emb, x_mask
class LinearNoSubsampling(BaseSubsampling):
"""Linear transform the input without subsampling
Args:
idim (int): Input dimension.
odim (int): Output dimension.
dropout_rate (float): Dropout rate.
"""
def __init__(self, idim: int, odim: int, dropout_rate: float,
pos_enc_class: torch.nn.Module):
"""Construct an linear object."""
super().__init__()
self.out = torch.nn.Sequential(
torch.nn.Linear(idim, odim),
torch.nn.LayerNorm(odim, eps=1e-5),
torch.nn.Dropout(dropout_rate),
)
self.pos_enc = pos_enc_class
self.right_context = 0
self.subsampling_rate = 1
def forward(
self,
x: torch.Tensor,
x_mask: torch.Tensor,
offset: Union[int, torch.Tensor] = 0
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Input x.
Args:
x (torch.Tensor): Input tensor (#batch, time, idim).
x_mask (torch.Tensor): Input mask (#batch, 1, time).
Returns:
torch.Tensor: linear input tensor (#batch, time', odim),
where time' = time .
torch.Tensor: linear input mask (#batch, 1, time'),
where time' = time .
"""
x = self.out(x)
x, pos_emb = self.pos_enc(x, offset)
return x, pos_emb, x_mask
class Conv1dSubsampling2(BaseSubsampling):
"""Convolutional 1D subsampling (to 1/2 length).
It is designed for Whisper, ref:
https://github.com/openai/whisper/blob/main/whisper/model.py
Args:
idim (int): Input dimension.
odim (int): Output dimension.
dropout_rate (float): Dropout rate.
"""
def __init__(self, idim: int, odim: int, dropout_rate: float,
pos_enc_class: torch.nn.Module):
"""Construct an Conv1dSubsampling2 object."""
super().__init__()
self.conv = torch.nn.Sequential(
torch.nn.Conv1d(idim, odim, kernel_size=3, padding=1),
torch.nn.GELU(),
torch.nn.Conv1d(odim, odim, kernel_size=3, stride=2, padding=1),
torch.nn.GELU(),
)
self.pos_enc = pos_enc_class
# The right context for every conv layer is computed by:
# (kernel_size - 1) * frame_rate_of_this_layer
self.subsampling_rate = 2
# 4 = (3 - 1) * 1 + (3 - 1) * 1
self.right_context = 4
def forward(
self,
x: torch.Tensor,
x_mask: torch.Tensor,
offset: Union[int, torch.Tensor] = 0
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Subsample x.
Args:
x (torch.Tensor): Input tensor (#batch, time, idim).
x_mask (torch.Tensor): Input mask (#batch, 1, time).
Returns:
torch.Tensor: Subsampled tensor (#batch, time', odim),
where time' = time // 2.
torch.Tensor: Subsampled mask (#batch, 1, time'),
where time' = time // 2.
torch.Tensor: positional encoding
"""
time = x.size(1)
x = x.transpose(1, 2) # (b, f, t)
x = self.conv(x)
x = x.transpose(1, 2) # (b, t, f)
x, pos_emb = self.pos_enc(x, offset)
return x, pos_emb, x_mask[:, :, (time + 1) % 2::2]
class Conv2dSubsampling4(BaseSubsampling):
"""Convolutional 2D subsampling (to 1/4 length).
Args:
idim (int): Input dimension.
odim (int): Output dimension.
dropout_rate (float): Dropout rate.
"""
def __init__(self, idim: int, odim: int, dropout_rate: float,
pos_enc_class: torch.nn.Module):
"""Construct an Conv2dSubsampling4 object."""
super().__init__()
self.conv = torch.nn.Sequential(
torch.nn.Conv2d(1, odim, 3, 2),
torch.nn.ReLU(),
torch.nn.Conv2d(odim, odim, 3, 2),
torch.nn.ReLU(),
)
self.out = torch.nn.Sequential(
torch.nn.Linear(odim * (((idim - 1) // 2 - 1) // 2), odim))
self.pos_enc = pos_enc_class
# The right context for every conv layer is computed by:
# (kernel_size - 1) * frame_rate_of_this_layer
self.subsampling_rate = 4
# 6 = (3 - 1) * 1 + (3 - 1) * 2
self.right_context = 6
def forward(
self,
x: torch.Tensor,
x_mask: torch.Tensor,
offset: Union[int, torch.Tensor] = 0
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Subsample x.
Args:
x (torch.Tensor): Input tensor (#batch, time, idim).
x_mask (torch.Tensor): Input mask (#batch, 1, time).
Returns:
torch.Tensor: Subsampled tensor (#batch, time', odim),
where time' = time // 4.
torch.Tensor: Subsampled mask (#batch, 1, time'),
where time' = time // 4.
torch.Tensor: positional encoding
"""
x = x.unsqueeze(1) # (b, c=1, t, f)
x = self.conv(x)
b, c, t, f = x.size()
x = self.out(x.transpose(1, 2).contiguous().view(b, t, c * f))
x, pos_emb = self.pos_enc(x, offset)
return x, pos_emb, x_mask[:, :, 2::2][:, :, 2::2]
class Conv2dSubsampling6(BaseSubsampling):
"""Convolutional 2D subsampling (to 1/6 length).
Args:
idim (int): Input dimension.
odim (int): Output dimension.
dropout_rate (float): Dropout rate.
pos_enc (torch.nn.Module): Custom position encoding layer.
"""
def __init__(self, idim: int, odim: int, dropout_rate: float,
pos_enc_class: torch.nn.Module):
"""Construct an Conv2dSubsampling6 object."""
super().__init__()
self.conv = torch.nn.Sequential(
torch.nn.Conv2d(1, odim, 3, 2),
torch.nn.ReLU(),
torch.nn.Conv2d(odim, odim, 5, 3),
torch.nn.ReLU(),
)
self.linear = torch.nn.Linear(odim * (((idim - 1) // 2 - 2) // 3),
odim)
self.pos_enc = pos_enc_class
# 10 = (3 - 1) * 1 + (5 - 1) * 2
self.subsampling_rate = 6
self.right_context = 10
def forward(
self,
x: torch.Tensor,
x_mask: torch.Tensor,
offset: Union[int, torch.Tensor] = 0
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Subsample x.
Args:
x (torch.Tensor): Input tensor (#batch, time, idim).
x_mask (torch.Tensor): Input mask (#batch, 1, time).
Returns:
torch.Tensor: Subsampled tensor (#batch, time', odim),
where time' = time // 6.
torch.Tensor: Subsampled mask (#batch, 1, time'),
where time' = time // 6.
torch.Tensor: positional encoding
"""
x = x.unsqueeze(1) # (b, c, t, f)
x = self.conv(x)
b, c, t, f = x.size()
x = self.linear(x.transpose(1, 2).contiguous().view(b, t, c * f))
x, pos_emb = self.pos_enc(x, offset)
return x, pos_emb, x_mask[:, :, 2::2][:, :, 4::3]
class Conv2dSubsampling8(BaseSubsampling):
"""Convolutional 2D subsampling (to 1/8 length).
Args:
idim (int): Input dimension.
odim (int): Output dimension.
dropout_rate (float): Dropout rate.
"""
def __init__(self, idim: int, odim: int, dropout_rate: float,
pos_enc_class: torch.nn.Module):
"""Construct an Conv2dSubsampling8 object."""
super().__init__()
self.conv = torch.nn.Sequential(
torch.nn.Conv2d(1, odim, 3, 2),
torch.nn.ReLU(),
torch.nn.Conv2d(odim, odim, 3, 2),
torch.nn.ReLU(),
torch.nn.Conv2d(odim, odim, 3, 2),
torch.nn.ReLU(),
)
self.linear = torch.nn.Linear(
odim * ((((idim - 1) // 2 - 1) // 2 - 1) // 2), odim)
self.pos_enc = pos_enc_class
self.subsampling_rate = 8
# 14 = (3 - 1) * 1 + (3 - 1) * 2 + (3 - 1) * 4
self.right_context = 14
def forward(
self,
x: torch.Tensor,
x_mask: torch.Tensor,
offset: Union[int, torch.Tensor] = 0
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Subsample x.
Args:
x (torch.Tensor): Input tensor (#batch, time, idim).
x_mask (torch.Tensor): Input mask (#batch, 1, time).
Returns:
torch.Tensor: Subsampled tensor (#batch, time', odim),
where time' = time // 8.
torch.Tensor: Subsampled mask (#batch, 1, time'),
where time' = time // 8.
torch.Tensor: positional encoding
"""
x = x.unsqueeze(1) # (b, c, t, f)
x = self.conv(x)
b, c, t, f = x.size()
x = self.linear(x.transpose(1, 2).contiguous().view(b, t, c * f))
x, pos_emb = self.pos_enc(x, offset)
return x, pos_emb, x_mask[:, :, 2::2][:, :, 2::2][:, :, 2::2]
class LegacyLinearNoSubsampling(BaseSubsampling):
"""Linear transform the input without subsampling
Args:
idim (int): Input dimension.
odim (int): Output dimension.
dropout_rate (float): Dropout rate.
"""
def __init__(self, idim: int, odim: int, dropout_rate: float,
pos_enc_class: torch.nn.Module):
"""Construct an linear object."""
super().__init__()
self.out = torch.nn.Sequential(
torch.nn.Linear(idim, odim),
torch.nn.LayerNorm(odim, eps=1e-5),
torch.nn.Dropout(dropout_rate),
torch.nn.ReLU(),
)
self.pos_enc = pos_enc_class
self.right_context = 0
self.subsampling_rate = 1
def forward(
self,
x: torch.Tensor,
x_mask: torch.Tensor,
offset: Union[int, torch.Tensor] = 0
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""Input x.
Args:
x (torch.Tensor): Input tensor (#batch, time, idim).
x_mask (torch.Tensor): Input mask (#batch, 1, time).
Returns:
torch.Tensor: linear input tensor (#batch, time', odim),
where time' = time .
torch.Tensor: linear input mask (#batch, 1, time'),
where time' = time .
"""
x = self.out(x)
x, pos_emb = self.pos_enc(x, offset)
return x, pos_emb, x_mask
@@ -0,0 +1,318 @@
# Copyright (c) 2021 Mobvoi Inc (Binbin Zhang, Di Wu)
# 2022 Xingchen Song (sxc19@mails.tsinghua.edu.cn)
# 2024 Alibaba Inc (Xiang Lyu)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Modified from ESPnet(https://github.com/espnet/espnet)
"""Encoder definition."""
from typing import Tuple
import torch
from torch import nn
from torch.nn import functional as F
from .convolution import ConvolutionModule
from .encoder_layer import ConformerEncoderLayer
from .positionwise_feed_forward import PositionwiseFeedForward
from ..utils.class_utils import (
COSYVOICE_EMB_CLASSES,
COSYVOICE_SUBSAMPLE_CLASSES,
COSYVOICE_ATTENTION_CLASSES,
COSYVOICE_ACTIVATION_CLASSES,
)
from ..utils.mask import make_pad_mask
from ..utils.mask import add_optional_chunk_mask
class Upsample1D(nn.Module):
"""A 1D upsampling layer with an optional convolution.
Parameters:
channels (`int`):
number of channels in the inputs and outputs.
use_conv (`bool`, default `False`):
option to use a convolution.
use_conv_transpose (`bool`, default `False`):
option to use a convolution transpose.
out_channels (`int`, optional):
number of output channels. Defaults to `channels`.
"""
def __init__(self, channels: int, out_channels: int, stride: int = 2):
super().__init__()
self.channels = channels
self.out_channels = out_channels
self.stride = stride
# In this mode, first repeat interpolate, than conv with stride=1
self.conv = nn.Conv1d(self.channels, self.out_channels, stride * 2 + 1, stride=1, padding=0)
def forward(self, inputs: torch.Tensor, input_lengths: torch.Tensor):
outputs = F.interpolate(inputs, scale_factor=float(self.stride), mode="nearest")
outputs = F.pad(outputs, (self.stride * 2, 0), value=0.0)
outputs = self.conv(outputs)
return outputs, input_lengths * self.stride
class PreLookaheadLayer(nn.Module):
def __init__(self, channels: int, pre_lookahead_len: int = 1):
super().__init__()
self.channels = channels
self.pre_lookahead_len = pre_lookahead_len
self.conv1 = nn.Conv1d(
channels, channels,
kernel_size=pre_lookahead_len + 1,
stride=1, padding=0,
)
self.conv2 = nn.Conv1d(
channels, channels,
kernel_size=3, stride=1, padding=0,
)
def forward(self, inputs: torch.Tensor) -> torch.Tensor:
"""
inputs: (batch_size, seq_len, channels)
"""
outputs = inputs.transpose(1, 2).contiguous()
# look ahead
outputs = F.pad(outputs, (0, self.pre_lookahead_len), mode='constant', value=0.0)
outputs = F.leaky_relu(self.conv1(outputs))
# outputs
outputs = F.pad(outputs, (2, 0), mode='constant', value=0.0)
outputs = self.conv2(outputs)
outputs = outputs.transpose(1, 2).contiguous()
# residual connection
outputs = outputs + inputs
return outputs
class UpsampleConformerEncoder(torch.nn.Module):
def __init__(
self,
input_size: int = 512,
output_size: int = 512,
attention_heads: int = 8,
linear_units: int = 2048,
num_blocks: int = 6,
dropout_rate: float = 0.1,
positional_dropout_rate: float = 0.1,
attention_dropout_rate: float = 0.1,
input_layer: str = "linear",
pos_enc_layer_type: str = "rel_pos_espnet",
normalize_before: bool = True,
static_chunk_size: int = 0,
use_dynamic_chunk: bool = False,
global_cmvn: torch.nn.Module = None,
use_dynamic_left_chunk: bool = False,
positionwise_conv_kernel_size: int = 1,
macaron_style: bool = False,
selfattention_layer_type: str = "rel_selfattn",
activation_type: str = "swish",
use_cnn_module: bool = False,
cnn_module_kernel: int = 15,
causal: bool = False,
cnn_module_norm: str = "batch_norm",
key_bias: bool = True,
gradient_checkpointing: bool = False,
):
"""
Args:
input_size (int): input dim
output_size (int): dimension of attention
attention_heads (int): the number of heads of multi head attention
linear_units (int): the hidden units number of position-wise feed
forward
num_blocks (int): the number of decoder blocks
dropout_rate (float): dropout rate
attention_dropout_rate (float): dropout rate in attention
positional_dropout_rate (float): dropout rate after adding
positional encoding
input_layer (str): input layer type.
optional [linear, conv2d, conv2d6, conv2d8]
pos_enc_layer_type (str): Encoder positional encoding layer type.
opitonal [abs_pos, scaled_abs_pos, rel_pos, no_pos]
normalize_before (bool):
True: use layer_norm before each sub-block of a layer.
False: use layer_norm after each sub-block of a layer.
static_chunk_size (int): chunk size for static chunk training and
decoding
use_dynamic_chunk (bool): whether use dynamic chunk size for
training or not, You can only use fixed chunk(chunk_size > 0)
or dyanmic chunk size(use_dynamic_chunk = True)
global_cmvn (Optional[torch.nn.Module]): Optional GlobalCMVN module
use_dynamic_left_chunk (bool): whether use dynamic left chunk in
dynamic chunk training
key_bias: whether use bias in attention.linear_k, False for whisper models.
gradient_checkpointing: rerunning a forward-pass segment for each
checkpointed segment during backward.
"""
super().__init__()
self._output_size = output_size
self.global_cmvn = global_cmvn
self.embed = COSYVOICE_SUBSAMPLE_CLASSES[input_layer](
input_size,
output_size,
dropout_rate,
COSYVOICE_EMB_CLASSES[pos_enc_layer_type](output_size,
positional_dropout_rate),
)
self.normalize_before = normalize_before
self.after_norm = torch.nn.LayerNorm(output_size, eps=1e-5)
self.static_chunk_size = static_chunk_size
self.use_dynamic_chunk = use_dynamic_chunk
self.use_dynamic_left_chunk = use_dynamic_left_chunk
self.gradient_checkpointing = gradient_checkpointing
activation = COSYVOICE_ACTIVATION_CLASSES[activation_type]()
# self-attention module definition
encoder_selfattn_layer_args = (
attention_heads,
output_size,
attention_dropout_rate,
key_bias,
)
# feed-forward module definition
positionwise_layer_args = (
output_size,
linear_units,
dropout_rate,
activation,
)
# convolution module definition
convolution_layer_args = (output_size, cnn_module_kernel, activation,
cnn_module_norm, causal)
self.pre_lookahead_layer = PreLookaheadLayer(channels=512, pre_lookahead_len=3)
self.encoders = torch.nn.ModuleList([
ConformerEncoderLayer(
output_size,
COSYVOICE_ATTENTION_CLASSES[selfattention_layer_type](
*encoder_selfattn_layer_args),
PositionwiseFeedForward(*positionwise_layer_args),
PositionwiseFeedForward(
*positionwise_layer_args) if macaron_style else None,
ConvolutionModule(
*convolution_layer_args) if use_cnn_module else None,
dropout_rate,
normalize_before,
) for _ in range(num_blocks)
])
self.up_layer = Upsample1D(channels=512, out_channels=512, stride=2)
self.up_embed = COSYVOICE_SUBSAMPLE_CLASSES[input_layer](
input_size,
output_size,
dropout_rate,
COSYVOICE_EMB_CLASSES[pos_enc_layer_type](output_size,
positional_dropout_rate),
)
self.up_encoders = torch.nn.ModuleList([
ConformerEncoderLayer(
output_size,
COSYVOICE_ATTENTION_CLASSES[selfattention_layer_type](
*encoder_selfattn_layer_args),
PositionwiseFeedForward(*positionwise_layer_args),
PositionwiseFeedForward(
*positionwise_layer_args) if macaron_style else None,
ConvolutionModule(
*convolution_layer_args) if use_cnn_module else None,
dropout_rate,
normalize_before,
) for _ in range(4)
])
def output_size(self) -> int:
return self._output_size
def forward(
self,
xs: torch.Tensor,
xs_lens: torch.Tensor,
decoding_chunk_size: int = 0,
num_decoding_left_chunks: int = -1,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Embed positions in tensor.
Args:
xs: padded input tensor (B, T, D)
xs_lens: input length (B)
decoding_chunk_size: decoding chunk size for dynamic chunk
0: default for training, use random dynamic chunk.
<0: for decoding, use full chunk.
>0: for decoding, use fixed chunk size as set.
num_decoding_left_chunks: number of left chunks, this is for decoding,
the chunk size is decoding_chunk_size.
>=0: use num_decoding_left_chunks
<0: use all left chunks
Returns:
encoder output tensor xs, and subsampled masks
xs: padded output tensor (B, T' ~= T/subsample_rate, D)
masks: torch.Tensor batch padding mask after subsample
(B, 1, T' ~= T/subsample_rate)
NOTE(xcsong):
We pass the `__call__` method of the modules instead of `forward` to the
checkpointing API because `__call__` attaches all the hooks of the module.
https://discuss.pytorch.org/t/any-different-between-model-input-and-model-forward-input/3690/2
"""
T = xs.size(1)
masks = ~make_pad_mask(xs_lens, T).unsqueeze(1) # (B, 1, T)
if self.global_cmvn is not None:
xs = self.global_cmvn(xs)
xs, pos_emb, masks = self.embed(xs, masks)
mask_pad = masks # (B, 1, T/subsample_rate)
chunk_masks = add_optional_chunk_mask(xs, masks,
self.use_dynamic_chunk,
self.use_dynamic_left_chunk,
decoding_chunk_size,
self.static_chunk_size,
num_decoding_left_chunks)
# lookahead + conformer encoder
xs = self.pre_lookahead_layer(xs)
xs = self.forward_layers(xs, chunk_masks, pos_emb, mask_pad)
# upsample + conformer encoder
xs = xs.transpose(1, 2).contiguous()
xs, xs_lens = self.up_layer(xs, xs_lens)
xs = xs.transpose(1, 2).contiguous()
T = xs.size(1)
masks = ~make_pad_mask(xs_lens, T).unsqueeze(1) # (B, 1, T)
xs, pos_emb, masks = self.up_embed(xs, masks)
mask_pad = masks # (B, 1, T/subsample_rate)
chunk_masks = add_optional_chunk_mask(xs, masks,
self.use_dynamic_chunk,
self.use_dynamic_left_chunk,
decoding_chunk_size,
self.static_chunk_size * self.up_layer.stride,
num_decoding_left_chunks)
xs = self.forward_up_layers(xs, chunk_masks, pos_emb, mask_pad)
if self.normalize_before:
xs = self.after_norm(xs)
# Here we assume the mask is not changed in encoder layers, so just
# return the masks before encoder layers, and the masks will be used
# for cross attention with decoder later
return xs, masks
def forward_layers(self, xs: torch.Tensor, chunk_masks: torch.Tensor,
pos_emb: torch.Tensor,
mask_pad: torch.Tensor) -> torch.Tensor:
for layer in self.encoders:
xs, chunk_masks, _, _ = layer(xs, chunk_masks, pos_emb, mask_pad)
return xs
def forward_up_layers(self, xs: torch.Tensor, chunk_masks: torch.Tensor,
pos_emb: torch.Tensor,
mask_pad: torch.Tensor) -> torch.Tensor:
for layer in self.up_encoders:
xs, chunk_masks, _, _ = layer(xs, chunk_masks, pos_emb, mask_pad)
return xs
@@ -0,0 +1,71 @@
# Copyright [2023-11-28] <sxc19@mails.tsinghua.edu.cn, Xingchen Song>
# 2024 Alibaba Inc (authors: Xiang Lyu)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import torch
from ..transformer.activation import Swish
from ..transformer.subsampling import (
LinearNoSubsampling,
EmbedinigNoSubsampling,
Conv1dSubsampling2,
Conv2dSubsampling4,
Conv2dSubsampling6,
Conv2dSubsampling8,
)
from ..transformer.embedding import (
PositionalEncoding,
RelPositionalEncoding,
WhisperPositionalEncoding,
LearnablePositionalEncoding,
NoPositionalEncoding)
from ..transformer.attention import (MultiHeadedAttention,
RelPositionMultiHeadedAttention)
from ..transformer.embedding import EspnetRelPositionalEncoding
from ..transformer.subsampling import LegacyLinearNoSubsampling
COSYVOICE_ACTIVATION_CLASSES = {
"hardtanh": torch.nn.Hardtanh,
"tanh": torch.nn.Tanh,
"relu": torch.nn.ReLU,
"selu": torch.nn.SELU,
"swish": getattr(torch.nn, "SiLU", Swish),
"gelu": torch.nn.GELU,
}
COSYVOICE_SUBSAMPLE_CLASSES = {
"linear": LinearNoSubsampling,
"linear_legacy": LegacyLinearNoSubsampling,
"embed": EmbedinigNoSubsampling,
"conv1d2": Conv1dSubsampling2,
"conv2d": Conv2dSubsampling4,
"conv2d6": Conv2dSubsampling6,
"conv2d8": Conv2dSubsampling8,
'paraformer_dummy': torch.nn.Identity
}
COSYVOICE_EMB_CLASSES = {
"embed": PositionalEncoding,
"abs_pos": PositionalEncoding,
"rel_pos": RelPositionalEncoding,
"rel_pos_espnet": EspnetRelPositionalEncoding,
"no_pos": NoPositionalEncoding,
"abs_pos_whisper": WhisperPositionalEncoding,
"embed_learnable_pe": LearnablePositionalEncoding,
}
COSYVOICE_ATTENTION_CLASSES = {
"selfattn": MultiHeadedAttention,
"rel_selfattn": RelPositionMultiHeadedAttention,
}
@@ -0,0 +1,193 @@
# Copyright (c) 2019 Shigeki Karita
# 2020 Mobvoi Inc (Binbin Zhang)
# 2024 Alibaba Inc (authors: Xiang Lyu)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import torch
'''
def subsequent_mask(
size: int,
device: torch.device = torch.device("cpu"),
) -> torch.Tensor:
"""Create mask for subsequent steps (size, size).
This mask is used only in decoder which works in an auto-regressive mode.
This means the current step could only do attention with its left steps.
In encoder, fully attention is used when streaming is not necessary and
the sequence is not long. In this case, no attention mask is needed.
When streaming is need, chunk-based attention is used in encoder. See
subsequent_chunk_mask for the chunk-based attention mask.
Args:
size (int): size of mask
str device (str): "cpu" or "cuda" or torch.Tensor.device
dtype (torch.device): result dtype
Returns:
torch.Tensor: mask
Examples:
>>> subsequent_mask(3)
[[1, 0, 0],
[1, 1, 0],
[1, 1, 1]]
"""
ret = torch.ones(size, size, device=device, dtype=torch.bool)
return torch.tril(ret)
'''
def subsequent_chunk_mask(
size: int,
chunk_size: int,
num_left_chunks: int = -1,
device: torch.device = torch.device("cpu"),
) -> torch.Tensor:
"""Create mask for subsequent steps (size, size) with chunk size,
this is for streaming encoder
Args:
size (int): size of mask
chunk_size (int): size of chunk
num_left_chunks (int): number of left chunks
<0: use full chunk
>=0: use num_left_chunks
device (torch.device): "cpu" or "cuda" or torch.Tensor.device
Returns:
torch.Tensor: mask
Examples:
>>> subsequent_chunk_mask(4, 2)
[[1, 1, 0, 0],
[1, 1, 0, 0],
[1, 1, 1, 1],
[1, 1, 1, 1]]
"""
# NOTE this modified implementation meets onnx export requirements, but it doesn't support num_left_chunks
# actually this is not needed after we have inference cache implemented, will remove it later
pos_idx = torch.arange(size, device=device)
block_value = (torch.div(pos_idx, chunk_size, rounding_mode='trunc') + 1) * chunk_size
ret = pos_idx.unsqueeze(0) < block_value.unsqueeze(1)
return ret
def add_optional_chunk_mask(xs: torch.Tensor,
masks: torch.Tensor,
use_dynamic_chunk: bool,
use_dynamic_left_chunk: bool,
decoding_chunk_size: int,
static_chunk_size: int,
num_decoding_left_chunks: int,
enable_full_context: bool = True):
""" Apply optional mask for encoder.
Args:
xs (torch.Tensor): padded input, (B, L, D), L for max length
mask (torch.Tensor): mask for xs, (B, 1, L)
use_dynamic_chunk (bool): whether to use dynamic chunk or not
use_dynamic_left_chunk (bool): whether to use dynamic left chunk for
training.
decoding_chunk_size (int): decoding chunk size for dynamic chunk, it's
0: default for training, use random dynamic chunk.
<0: for decoding, use full chunk.
>0: for decoding, use fixed chunk size as set.
static_chunk_size (int): chunk size for static chunk training/decoding
if it's greater than 0, if use_dynamic_chunk is true,
this parameter will be ignored
num_decoding_left_chunks: number of left chunks, this is for decoding,
the chunk size is decoding_chunk_size.
>=0: use num_decoding_left_chunks
<0: use all left chunks
enable_full_context (bool):
True: chunk size is either [1, 25] or full context(max_len)
False: chunk size ~ U[1, 25]
Returns:
torch.Tensor: chunk mask of the input xs.
"""
# Whether to use chunk mask or not
if use_dynamic_chunk:
max_len = xs.size(1)
if decoding_chunk_size < 0:
chunk_size = max_len
num_left_chunks = -1
elif decoding_chunk_size > 0:
chunk_size = decoding_chunk_size
num_left_chunks = num_decoding_left_chunks
else:
# chunk size is either [1, 25] or full context(max_len).
# Since we use 4 times subsampling and allow up to 1s(100 frames)
# delay, the maximum frame is 100 / 4 = 25.
chunk_size = torch.randint(1, max_len, (1, )).item()
num_left_chunks = -1
if chunk_size > max_len // 2 and enable_full_context:
chunk_size = max_len
else:
chunk_size = chunk_size % 25 + 1
if use_dynamic_left_chunk:
max_left_chunks = (max_len - 1) // chunk_size
num_left_chunks = torch.randint(0, max_left_chunks,
(1, )).item()
chunk_masks = subsequent_chunk_mask(xs.size(1), chunk_size,
num_left_chunks,
xs.device) # (L, L)
chunk_masks = chunk_masks.unsqueeze(0) # (1, L, L)
chunk_masks = masks & chunk_masks # (B, L, L)
elif static_chunk_size > 0:
num_left_chunks = num_decoding_left_chunks
chunk_masks = subsequent_chunk_mask(xs.size(1), static_chunk_size,
num_left_chunks,
xs.device) # (L, L)
chunk_masks = chunk_masks.unsqueeze(0) # (1, L, L)
chunk_masks = masks & chunk_masks # (B, L, L)
else:
chunk_masks = masks
assert chunk_masks.dtype == torch.bool
if (chunk_masks.sum(dim=-1) == 0).sum().item() != 0:
logging.warning('get chunk_masks all false at some timestep, force set to true, make sure they are masked in futuer computation!')
chunk_masks[chunk_masks.sum(dim=-1)==0] = True
return chunk_masks
def make_pad_mask(lengths: torch.Tensor, max_len: int = 0) -> torch.Tensor:
"""Make mask tensor containing indices of padded part.
See description of make_non_pad_mask.
Args:
lengths (torch.Tensor): Batch of lengths (B,).
Returns:
torch.Tensor: Mask tensor containing indices of padded part.
Examples:
>>> lengths = [5, 3, 2]
>>> make_pad_mask(lengths)
masks = [[0, 0, 0, 0 ,0],
[0, 0, 0, 1, 1],
[0, 0, 1, 1, 1]]
"""
batch_size = lengths.size(0)
max_len = max_len if max_len > 0 else lengths.max().item()
seq_range = torch.arange(0,
max_len,
dtype=torch.int64,
device=lengths.device)
seq_range_expand = seq_range.unsqueeze(0).expand(batch_size, max_len)
seq_length_expand = lengths.unsqueeze(-1)
mask = seq_range_expand >= seq_length_expand
return mask
@@ -0,0 +1,85 @@
"""mel-spectrogram extraction in Matcha-TTS"""
import logging
from librosa.filters import mel as librosa_mel_fn
import torch
import numpy as np
logger = logging.getLogger(__name__)
# NOTE: they decalred these global vars
mel_basis = {}
hann_window = {}
def dynamic_range_compression_torch(x, C=1, clip_val=1e-5):
return torch.log(torch.clamp(x, min=clip_val) * C)
def spectral_normalize_torch(magnitudes):
output = dynamic_range_compression_torch(magnitudes)
return output
"""
feat_extractor: !name:matcha.utils.audio.mel_spectrogram
n_fft: 1920
num_mels: 80
sampling_rate: 24000
hop_size: 480
win_size: 1920
fmin: 0
fmax: 8000
center: False
"""
def mel_spectrogram(y, n_fft=1920, num_mels=80, sampling_rate=24000, hop_size=480, win_size=1920,
fmin=0, fmax=8000, center=False):
"""Copied from https://github.com/shivammehta25/Matcha-TTS/blob/main/matcha/utils/audio.py
Set default values according to Cosyvoice's config.
"""
if isinstance(y, np.ndarray):
y = torch.tensor(y).float()
if len(y.shape) == 1:
y = y[None, ]
# Debug: Check for audio clipping (values outside [-1.0, 1.0] range)
min_val = torch.min(y)
max_val = torch.max(y)
if min_val < -1.0 or max_val > 1.0:
logger.warning(f"Audio values outside normalized range: min={min_val.item():.4f}, max={max_val.item():.4f}")
global mel_basis, hann_window # pylint: disable=global-statement,global-variable-not-assigned
if f"{str(fmax)}_{str(y.device)}" not in mel_basis:
mel = librosa_mel_fn(sr=sampling_rate, n_fft=n_fft, n_mels=num_mels, fmin=fmin, fmax=fmax)
mel_basis[str(fmax) + "_" + str(y.device)] = torch.from_numpy(mel).float().to(y.device)
hann_window[str(y.device)] = torch.hann_window(win_size).to(y.device)
y = torch.nn.functional.pad(
y.unsqueeze(1), (int((n_fft - hop_size) / 2), int((n_fft - hop_size) / 2)), mode="reflect"
)
y = y.squeeze(1)
spec = torch.view_as_real(
torch.stft(
y,
n_fft,
hop_length=hop_size,
win_length=win_size,
window=hann_window[str(y.device)],
center=center,
pad_mode="reflect",
normalized=False,
onesided=True,
return_complex=True,
)
)
spec = torch.sqrt(spec.pow(2).sum(-1) + (1e-9))
spec = torch.matmul(mel_basis[str(fmax) + "_" + str(y.device)], spec)
spec = spectral_normalize_torch(spec)
return spec
@@ -0,0 +1,428 @@
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
# MIT License (https://opensource.org/licenses/MIT)
# Modified from 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker)
from collections import OrderedDict
import torch
import torch.nn.functional as F
import torch.utils.checkpoint as cp
import torchaudio.compliance.kaldi as Kaldi
def pad_list(xs, pad_value):
"""Perform padding for the list of tensors.
Args:
xs (List): List of Tensors [(T_1, `*`), (T_2, `*`), ..., (T_B, `*`)].
pad_value (float): Value for padding.
Returns:
Tensor: Padded tensor (B, Tmax, `*`).
Examples:
>>> x = [torch.ones(4), torch.ones(2), torch.ones(1)]
>>> x
[tensor([1., 1., 1., 1.]), tensor([1., 1.]), tensor([1.])]
>>> pad_list(x, 0)
tensor([[1., 1., 1., 1.],
[1., 1., 0., 0.],
[1., 0., 0., 0.]])
"""
n_batch = len(xs)
max_len = max(x.size(0) for x in xs)
pad = xs[0].new(n_batch, max_len, *xs[0].size()[1:]).fill_(pad_value)
for i in range(n_batch):
pad[i, : xs[i].size(0)] = xs[i]
return pad
def extract_feature(audio):
features = []
feature_times = []
feature_lengths = []
for au in audio:
feature = Kaldi.fbank(au.unsqueeze(0), num_mel_bins=80)
feature = feature - feature.mean(dim=0, keepdim=True)
features.append(feature)
feature_times.append(au.shape[0])
feature_lengths.append(feature.shape[0])
# padding for batch inference
features_padded = pad_list(features, pad_value=0)
# features = torch.cat(features)
return features_padded, feature_lengths, feature_times
class BasicResBlock(torch.nn.Module):
expansion = 1
def __init__(self, in_planes, planes, stride=1):
super(BasicResBlock, self).__init__()
self.conv1 = torch.nn.Conv2d(
in_planes, planes, kernel_size=3, stride=(stride, 1), padding=1, bias=False
)
self.bn1 = torch.nn.BatchNorm2d(planes)
self.conv2 = torch.nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = torch.nn.BatchNorm2d(planes)
self.shortcut = torch.nn.Sequential()
if stride != 1 or in_planes != self.expansion * planes:
self.shortcut = torch.nn.Sequential(
torch.nn.Conv2d(
in_planes,
self.expansion * planes,
kernel_size=1,
stride=(stride, 1),
bias=False,
),
torch.nn.BatchNorm2d(self.expansion * planes),
)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(x)
out = F.relu(out)
return out
class FCM(torch.nn.Module):
def __init__(self, block=BasicResBlock, num_blocks=[2, 2], m_channels=32, feat_dim=80):
super(FCM, self).__init__()
self.in_planes = m_channels
self.conv1 = torch.nn.Conv2d(1, m_channels, kernel_size=3, stride=1, padding=1, bias=False)
self.bn1 = torch.nn.BatchNorm2d(m_channels)
self.layer1 = self._make_layer(block, m_channels, num_blocks[0], stride=2)
self.layer2 = self._make_layer(block, m_channels, num_blocks[0], stride=2)
self.conv2 = torch.nn.Conv2d(
m_channels, m_channels, kernel_size=3, stride=(2, 1), padding=1, bias=False
)
self.bn2 = torch.nn.BatchNorm2d(m_channels)
self.out_channels = m_channels * (feat_dim // 8)
def _make_layer(self, block, planes, num_blocks, stride):
strides = [stride] + [1] * (num_blocks - 1)
layers = []
for stride in strides:
layers.append(block(self.in_planes, planes, stride))
self.in_planes = planes * block.expansion
return torch.nn.Sequential(*layers)
def forward(self, x):
x = x.unsqueeze(1)
out = F.relu(self.bn1(self.conv1(x)))
out = self.layer1(out)
out = self.layer2(out)
out = F.relu(self.bn2(self.conv2(out)))
shape = out.shape
out = out.reshape(shape[0], shape[1] * shape[2], shape[3])
return out
def get_nonlinear(config_str, channels):
nonlinear = torch.nn.Sequential()
for name in config_str.split("-"):
if name == "relu":
nonlinear.add_module("relu", torch.nn.ReLU(inplace=True))
elif name == "prelu":
nonlinear.add_module("prelu", torch.nn.PReLU(channels))
elif name == "batchnorm":
nonlinear.add_module("batchnorm", torch.nn.BatchNorm1d(channels))
elif name == "batchnorm_":
nonlinear.add_module("batchnorm", torch.nn.BatchNorm1d(channels, affine=False))
else:
raise ValueError("Unexpected module ({}).".format(name))
return nonlinear
def statistics_pooling(x, dim=-1, keepdim=False, unbiased=True, eps=1e-2):
mean = x.mean(dim=dim)
std = x.std(dim=dim, unbiased=unbiased)
stats = torch.cat([mean, std], dim=-1)
if keepdim:
stats = stats.unsqueeze(dim=dim)
return stats
class StatsPool(torch.nn.Module):
def forward(self, x):
return statistics_pooling(x)
class TDNNLayer(torch.nn.Module):
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
bias=False,
config_str="batchnorm-relu",
):
super(TDNNLayer, self).__init__()
if padding < 0:
assert (
kernel_size % 2 == 1
), "Expect equal paddings, but got even kernel size ({})".format(kernel_size)
padding = (kernel_size - 1) // 2 * dilation
self.linear = torch.nn.Conv1d(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
bias=bias,
)
self.nonlinear = get_nonlinear(config_str, out_channels)
def forward(self, x):
x = self.linear(x)
x = self.nonlinear(x)
return x
class CAMLayer(torch.nn.Module):
def __init__(
self, bn_channels, out_channels, kernel_size, stride, padding, dilation, bias, reduction=2
):
super(CAMLayer, self).__init__()
self.linear_local = torch.nn.Conv1d(
bn_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
bias=bias,
)
self.linear1 = torch.nn.Conv1d(bn_channels, bn_channels // reduction, 1)
self.relu = torch.nn.ReLU(inplace=True)
self.linear2 = torch.nn.Conv1d(bn_channels // reduction, out_channels, 1)
self.sigmoid = torch.nn.Sigmoid()
def forward(self, x):
y = self.linear_local(x)
context = x.mean(-1, keepdim=True) + self.seg_pooling(x)
context = self.relu(self.linear1(context))
m = self.sigmoid(self.linear2(context))
return y * m
def seg_pooling(self, x, seg_len=100, stype="avg"):
if stype == "avg":
seg = F.avg_pool1d(x, kernel_size=seg_len, stride=seg_len, ceil_mode=True)
elif stype == "max":
seg = F.max_pool1d(x, kernel_size=seg_len, stride=seg_len, ceil_mode=True)
else:
raise ValueError("Wrong segment pooling type.")
shape = seg.shape
seg = seg.unsqueeze(-1).expand(*shape, seg_len).reshape(*shape[:-1], -1)
seg = seg[..., : x.shape[-1]]
return seg
class CAMDenseTDNNLayer(torch.nn.Module):
def __init__(
self,
in_channels,
out_channels,
bn_channels,
kernel_size,
stride=1,
dilation=1,
bias=False,
config_str="batchnorm-relu",
memory_efficient=False,
):
super(CAMDenseTDNNLayer, self).__init__()
assert kernel_size % 2 == 1, "Expect equal paddings, but got even kernel size ({})".format(
kernel_size
)
padding = (kernel_size - 1) // 2 * dilation
self.memory_efficient = memory_efficient
self.nonlinear1 = get_nonlinear(config_str, in_channels)
self.linear1 = torch.nn.Conv1d(in_channels, bn_channels, 1, bias=False)
self.nonlinear2 = get_nonlinear(config_str, bn_channels)
self.cam_layer = CAMLayer(
bn_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
bias=bias,
)
def bn_function(self, x):
return self.linear1(self.nonlinear1(x))
def forward(self, x):
if self.training and self.memory_efficient:
x = cp.checkpoint(self.bn_function, x)
else:
x = self.bn_function(x)
x = self.cam_layer(self.nonlinear2(x))
return x
class CAMDenseTDNNBlock(torch.nn.ModuleList):
def __init__(
self,
num_layers,
in_channels,
out_channels,
bn_channels,
kernel_size,
stride=1,
dilation=1,
bias=False,
config_str="batchnorm-relu",
memory_efficient=False,
):
super(CAMDenseTDNNBlock, self).__init__()
for i in range(num_layers):
layer = CAMDenseTDNNLayer(
in_channels=in_channels + i * out_channels,
out_channels=out_channels,
bn_channels=bn_channels,
kernel_size=kernel_size,
stride=stride,
dilation=dilation,
bias=bias,
config_str=config_str,
memory_efficient=memory_efficient,
)
self.add_module("tdnnd%d" % (i + 1), layer)
def forward(self, x):
for layer in self:
x = torch.cat([x, layer(x)], dim=1)
return x
class TransitLayer(torch.nn.Module):
def __init__(self, in_channels, out_channels, bias=True, config_str="batchnorm-relu"):
super(TransitLayer, self).__init__()
self.nonlinear = get_nonlinear(config_str, in_channels)
self.linear = torch.nn.Conv1d(in_channels, out_channels, 1, bias=bias)
def forward(self, x):
x = self.nonlinear(x)
x = self.linear(x)
return x
class DenseLayer(torch.nn.Module):
def __init__(self, in_channels, out_channels, bias=False, config_str="batchnorm-relu"):
super(DenseLayer, self).__init__()
self.linear = torch.nn.Conv1d(in_channels, out_channels, 1, bias=bias)
self.nonlinear = get_nonlinear(config_str, out_channels)
def forward(self, x):
if len(x.shape) == 2:
x = self.linear(x.unsqueeze(dim=-1)).squeeze(dim=-1)
else:
x = self.linear(x)
x = self.nonlinear(x)
return x
# @tables.register("model_classes", "CAMPPlus")
class CAMPPlus(torch.nn.Module):
def __init__(
self,
feat_dim=80,
embedding_size=192,
growth_rate=32,
bn_size=4,
init_channels=128,
config_str="batchnorm-relu",
memory_efficient=True,
output_level="segment",
**kwargs,
):
super().__init__()
self.head = FCM(feat_dim=feat_dim)
channels = self.head.out_channels
self.output_level = output_level
self.xvector = torch.nn.Sequential(
OrderedDict(
[
(
"tdnn",
TDNNLayer(
channels,
init_channels,
5,
stride=2,
dilation=1,
padding=-1,
config_str=config_str,
),
),
]
)
)
channels = init_channels
for i, (num_layers, kernel_size, dilation) in enumerate(
zip((12, 24, 16), (3, 3, 3), (1, 2, 2))
):
block = CAMDenseTDNNBlock(
num_layers=num_layers,
in_channels=channels,
out_channels=growth_rate,
bn_channels=bn_size * growth_rate,
kernel_size=kernel_size,
dilation=dilation,
config_str=config_str,
memory_efficient=memory_efficient,
)
self.xvector.add_module("block%d" % (i + 1), block)
channels = channels + num_layers * growth_rate
self.xvector.add_module(
"transit%d" % (i + 1),
TransitLayer(channels, channels // 2, bias=False, config_str=config_str),
)
channels //= 2
self.xvector.add_module("out_nonlinear", get_nonlinear(config_str, channels))
if self.output_level == "segment":
self.xvector.add_module("stats", StatsPool())
self.xvector.add_module(
"dense", DenseLayer(channels * 2, embedding_size, config_str="batchnorm_")
)
else:
assert (
self.output_level == "frame"
), "`output_level` should be set to 'segment' or 'frame'. "
for m in self.modules():
if isinstance(m, (torch.nn.Conv1d, torch.nn.Linear)):
torch.nn.init.kaiming_normal_(m.weight.data)
if m.bias is not None:
torch.nn.init.zeros_(m.bias)
def forward(self, x):
x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T)
x = self.head(x)
x = self.xvector(x)
if self.output_level == "frame":
x = x.transpose(1, 2)
return x
def inference(self, audio_list):
speech, speech_lengths, speech_times = extract_feature(audio_list)
results = self.forward(speech.to(torch.float32))
return results
@@ -0,0 +1,30 @@
from .s3tokenizer import (
S3_SR,
S3_HOP,
S3_TOKEN_HOP,
S3_TOKEN_RATE,
SPEECH_VOCAB_SIZE,
S3Tokenizer,
)
SOS = SPEECH_VOCAB_SIZE
EOS = SPEECH_VOCAB_SIZE + 1
def drop_invalid_tokens(x):
"""Drop SoS and EoS"""
assert len(x.shape) == 1 or (len(x.shape) == 2 and x.shape[0] == 1), "only batch size of one allowed for now"
if SOS in x:
s = (x == SOS).nonzero(as_tuple=True)[0].squeeze(0) + 1
else:
s = 0
if EOS in x:
e = (x == EOS).nonzero(as_tuple=True)[0].squeeze(0)
else:
e = None
x = x[s: e]
return x

Some files were not shown because too many files have changed in this diff Show More