Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c19fbbd75 | ||
|
|
1befb1b09a | ||
|
|
a96e75c718 | ||
|
|
37f36ac821 | ||
|
|
57e7329377 | ||
|
|
c5c4262d4a | ||
|
|
4755f4cb4c | ||
|
|
4530f7755b | ||
|
|
bab68fd256 | ||
|
|
4b844b327d | ||
|
|
7bf270cae9 | ||
|
|
ada51d7937 | ||
|
|
fa203f52ff | ||
|
|
6bfefd0df5 | ||
|
|
f010a0ac79 | ||
|
|
c7e6c4b2bf | ||
|
|
7ea1f743fd | ||
|
|
3833092253 | ||
|
|
a3ace77eb5 | ||
|
|
a2e11108e2 | ||
|
|
76b0818d7c | ||
|
|
3f14436b17 | ||
|
|
e840c6df4d | ||
|
|
b13ec81872 | ||
|
|
b3f8255a33 | ||
|
|
4f0782e75d | ||
|
|
28a738662e | ||
|
|
ddadcdfec5 | ||
|
|
6c87d67ed7 | ||
|
|
5af9368406 | ||
|
|
3a00e7d104 | ||
|
|
077d7037d6 | ||
|
|
71ca26da0e | ||
|
|
845ffc3c05 | ||
|
|
a8f49bea1e | ||
|
|
94df261046 | ||
|
|
fa74973ad5 | ||
|
|
88feb3a99a | ||
|
|
2b3351a047 | ||
|
|
71bfb2b04e | ||
|
|
a8e2de16e7 | ||
|
|
65ddbca887 | ||
|
|
574684a781 | ||
|
|
c4a3a9ace6 | ||
|
|
842100d05f | ||
|
|
4bec0df81c | ||
|
|
d195a6a51e | ||
|
|
bf624cac57 | ||
|
|
26cb0790c1 | ||
|
|
29e2fbf604 | ||
|
|
dfc86e868d | ||
|
|
ff0f1947bb | ||
|
|
c85f93b5c7 | ||
|
|
7d9438ced1 | ||
|
|
4926a24483 | ||
|
|
14c95fd981 | ||
|
|
6fdaa0a188 | ||
|
|
33629f30a5 | ||
|
|
164c7f65a7 | ||
|
|
33643e4f7f | ||
|
|
944a47b5c9 | ||
|
|
854c7769fb | ||
|
|
4e6da21b65 | ||
|
|
524c2d7bd4 | ||
|
|
41b1bda1e5 | ||
|
|
79eb96d0de | ||
|
|
edf5f011a8 | ||
|
|
cb7452015d | ||
|
|
b2c4d19641 | ||
|
|
c2bb619206 | ||
|
|
6fff668ec6 | ||
|
|
bd268dca97 | ||
|
|
65f66f6809 | ||
|
|
8e17e130b0 | ||
|
|
61c25f19b4 | ||
|
|
bcbc52376b | ||
|
|
a58c68d575 | ||
|
|
dbfe1872bc | ||
|
|
7a6d6ca77d | ||
|
|
828e27b45c | ||
|
|
1813a439cf | ||
|
|
94b6f17d89 | ||
|
|
cd8bc9801d | ||
|
|
273677b8a9 | ||
|
|
9ea7dc7243 | ||
|
|
f32cbb98f9 | ||
|
|
d6ffa42cc7 | ||
|
|
e85de6bda6 | ||
|
|
ecec27aed4 | ||
|
|
4265a5e8c1 | ||
|
|
465fc8dcfa | ||
|
|
6f0fdb0cac | ||
|
|
ca646f2f57 | ||
|
|
06431d3b08 | ||
|
|
15e2c4e665 | ||
|
|
d5718af782 | ||
|
|
a5bab4e767 | ||
|
|
106cd99a4b | ||
|
|
9352536594 | ||
|
|
dcd2252d53 | ||
|
|
b85861bc15 | ||
|
|
fdf5146f54 | ||
|
|
1a4e24a333 | ||
|
|
142a295189 | ||
|
|
cf33d03d43 | ||
|
|
ab83955cf4 | ||
|
|
3dc455e565 | ||
|
|
a2cac49587 | ||
|
|
113c25efe7 | ||
|
|
6f37b63f03 | ||
|
|
229de4d0ef | ||
|
|
16fc4a1055 | ||
|
|
84a10d3ee4 | ||
|
|
053b0510c3 | ||
|
|
a5a9efb31e | ||
|
|
a48d50fcf5 | ||
|
|
0e13a52e24 | ||
|
|
c4ef2acf03 | ||
|
|
a48a6267b8 | ||
|
|
81f8b515b9 | ||
|
|
b7a3f33d40 | ||
|
|
bb5f689627 | ||
|
|
d856e4a04c | ||
|
|
5925f4df27 | ||
|
|
55fd404618 | ||
|
|
b616f1cf3f | ||
|
|
f6363c3d86 | ||
|
|
cce58d3fed | ||
|
|
84df1c1163 | ||
|
|
fb0bd1317f | ||
|
|
2709718db7 | ||
|
|
83be911c36 | ||
|
|
7d053c1cdc | ||
|
|
ce5404d13e |
+13
@@ -212,6 +212,13 @@ docs/Dev reports/CLAUDE_VERSION_MANAGEMENT_GUIDE.md
|
||||
.claude-code.json
|
||||
.claude/
|
||||
|
||||
#codex
|
||||
*AGENTS.md
|
||||
AGENTS.md
|
||||
|
||||
# Antigravity / agent configuration
|
||||
.agent/
|
||||
|
||||
IgnoredForGitHubDocs/
|
||||
|
||||
# Windows Zone.Identifier files
|
||||
@@ -225,3 +232,9 @@ IgnoredForGitHubDocs/
|
||||
.roo/
|
||||
.vscode/
|
||||
ignore/
|
||||
|
||||
# Test output files
|
||||
tests/output/
|
||||
|
||||
# TTS Audio Suite settings cache (user preferences)
|
||||
.inline_tag_settings_cache.json
|
||||
|
||||
+569
@@ -5,6 +5,575 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [4.20.1] - 2026-01-31
|
||||
|
||||
### Added
|
||||
|
||||
- Add .agent/ directory to .gitignore for personal development tools
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve cache validation to check both voice description and reference text before loading from disk
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Qwen3-TTS Voice Designer caching issues and add voice refinement support
|
||||
- Fix Voice Designer loading stale audio when reference text changes but description stays same
|
||||
- Add overwrite_character toggle to enable voice refinement workflow (default: False for backward compatibility)
|
||||
## [4.20.0] - 2026-01-31
|
||||
|
||||
### Added
|
||||
|
||||
- Add Qwen3-ASR transcription with unified SRT output
|
||||
- New ASR Transcribe node with text, SRT, and word timestamps
|
||||
- Built-in subtitle readability presets and punctuation-aware SRT formatting
|
||||
- Automatic Qwen3-ASR downloads with forced aligner support
|
||||
- Faster experimentation via ASR result caching
|
||||
## [4.19.12] - 2026-01-30
|
||||
|
||||
### Added
|
||||
|
||||
- Add Qwen3-TTS torch.compile optimizations for 1.7x speedup
|
||||
- Add torch.compile support with ~1.7x faster generation (5 it/s → 8.5 it/s on RTX 4090)
|
||||
- Add optimization mode selection: default (Windows), reduce-overhead (Linux), max-autotune (Linux)
|
||||
- Add optional manual CUDA graph capture (minimal gain over torch.compile alone)
|
||||
- Add comprehensive optimization guide with tested benchmark results and requirements
|
||||
- Note: Optimizations disabled by default, Windows users should use compile_mode='default'
|
||||
- Based on streaming implementation by dffdeeq: https://github.com/dffdeeq/Qwen3-TTS-streaming
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve compatibility with PyTorch 2.10+ and triton-windows 3.6+ required for optimizations
|
||||
- Fix encoding errors in startup messages
|
||||
## [4.19.11] - 2026-01-30
|
||||
|
||||
### Added
|
||||
|
||||
- Properly respect user's attention mechanism choice (sdpa/eager/flash_attention_2)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Qwen3-TTS Voice Designer attention selection on AMD GPUs
|
||||
- Fix flash_attn import error when user selects sdpa attention
|
||||
- Fix config parameter extraction in Voice Designer node
|
||||
## [4.19.10] - 2026-01-29
|
||||
|
||||
### Added
|
||||
|
||||
- Increase Qwen3-TTS max_tokens range to 64-8192 (previously limited to 512-4096)
|
||||
- Add clear warning when x_vector_only mode is enabled with reference text available
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Qwen3-TTS mode selection and Character Voices reference text handling
|
||||
- Fix Character Voices always using folder reference text when character is selected from dropdown (manual field now only for direct audio)
|
||||
- Fix Qwen3-TTS x_vector_only mode properly respecting engine widget setting when reference text is available
|
||||
- Fix Qwen3-TTS progress bar showing accurate progress (0-100%) instead of tiny percentages
|
||||
## [4.19.9] - 2026-01-29
|
||||
|
||||
### Added
|
||||
|
||||
- Audio inputs without transcript now properly use x_vector_only mode (lower quality but functional)
|
||||
|
||||
### Changed
|
||||
|
||||
- Update warning messages to clarify engine-specific behavior with direct audio input
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Qwen3-TTS not using audio from opt_narrator input
|
||||
- Fix Qwen3-TTS falling back to default narrator when audio-only is connected to opt_narrator
|
||||
## [4.19.8] - 2026-01-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve model lifecycle management for VoiceDesign↔Base switching
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Qwen3-TTS crashes when switching between Voice Designer and TTS Text
|
||||
- Fix device mismatch errors (CPU/CUDA tensor conflicts)
|
||||
- Fix language dropdown selection being overridden by English
|
||||
- Better handling of cached processors with correct engine references
|
||||
## [4.19.7] - 2026-01-28
|
||||
|
||||
### Added
|
||||
|
||||
- Narrator segments now correctly use the selected language
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Qwen3-TTS language selection being ignored
|
||||
- Fix language dropdown selection not applying to generation
|
||||
- Resolves issue where Spanish/other languages switched to English
|
||||
## [4.19.6] - 2026-01-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve handling of different voice reference formats (file paths, tensors, audio dicts)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Qwen3-TTS voice cloning error with tensor references
|
||||
- Fix 'Boolean value of Tensor with more than one value is ambiguous' error when using voice references
|
||||
## [4.19.5] - 2026-01-28
|
||||
|
||||
### Added
|
||||
|
||||
- Add CosyVoice model variant setting (RL/standard) for <restore> tags in settings menu
|
||||
- Prevent unnecessary model reload when TTS and VC use the same variant
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve device management to properly reload CosyVoice components after unload
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix CosyVoice voice restoration device errors and add variant setting
|
||||
- Fix "Expected all tensors to be on the same device" errors during CosyVoice <restore> tag processing
|
||||
## [4.19.4] - 2026-01-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve audio tensor handling across all engines
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix F5-TTS restore tag voice conversion failures
|
||||
- Fix voice conversion crashes when using restore tags with F5-TTS
|
||||
- Better compatibility between different TTS engines and voice conversion
|
||||
## [4.19.3] - 2026-01-28
|
||||
|
||||
### Added
|
||||
|
||||
- CosyVoice VC now reuses existing TTS engine instance when available
|
||||
- Prevents model thrashing when using CosyVoice for both TTS and voice restoration
|
||||
- Significantly faster VC processing when TTS model already loaded
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix CosyVoice voice conversion causing model reload errors
|
||||
- Eliminates device mismatch errors (tensors on different devices)
|
||||
## [4.19.2] - 2026-01-28
|
||||
|
||||
### Added
|
||||
|
||||
- Add user-selectable voice conversion engine for <restore> tags
|
||||
- New ComfyUI settings menu: Voice Restoration (<restore> Tags)
|
||||
- Choose between ChatterBox 23-Lang (default), ChatterBox, or CosyVoice for voice restoration
|
||||
- Settings persist across ComfyUI restarts automatically
|
||||
- CosyVoice option enables native CosyVoice3 VC for seamless workflows
|
||||
- All engines support the same <restore> tag syntax with iteration control
|
||||
## [4.19.1] - 2026-01-28
|
||||
|
||||
### Added
|
||||
|
||||
- Add CosyVoice3 voice restoration support via <restore> tags
|
||||
- CosyVoice now supports <restore> tags for voice restoration using ChatterBox VC
|
||||
- Native CosyVoice paralinguistic tags (<breath>, <laughter>, <cough>, etc.) remain fully functional
|
||||
- Step Audio EditX emotion/style/speed tags are blocked to avoid conflicts with CosyVoice's native system
|
||||
- Voice restoration applies after TTS generation as post-processing step
|
||||
## [4.19.0] - 2026-01-28
|
||||
|
||||
### Added
|
||||
|
||||
- Add Qwen3-TTS engine with voice design capabilities
|
||||
- New Qwen3-TTS engine supporting 0.6B and 1.7B models
|
||||
- Three model types: CustomVoice (9 preset speakers), VoiceDesign (create voices from text descriptions), Base (zero-shot voice cloning)
|
||||
- Support for 10 languages (Chinese, English, Japanese, Korean, German, French, Russian, Portuguese, Spanish, Italian)
|
||||
- Voice Designer node creates custom voices from natural language descriptions
|
||||
- Full SRT subtitle support with character switching and all timing modes
|
||||
- Sage attention support for improved VRAM efficiency
|
||||
- Step Audio EditX inline edit tags for post-processing (emotions, styles, paralinguistic effects)
|
||||
- Auto-download system for all 6 model variants
|
||||
|
||||
### Changed
|
||||
|
||||
- Smart caching prevents duplicate voice generation
|
||||
## [4.18.0] - 2026-01-26
|
||||
|
||||
### Added
|
||||
|
||||
- Add VibeVoice Hindi community finetunes
|
||||
- Add vibevoice-hindi-1.5B support - Hindi-optimized 2.7B model
|
||||
- Add vibevoice-hindi-7B support - Hindi-optimized 9B model
|
||||
- Both models support Hindi and English languages
|
||||
- Auto-download from tarun7r HuggingFace repositories
|
||||
- Show download progress bars during model downloads
|
||||
## [4.17.0] - 2026-01-25
|
||||
|
||||
### Added
|
||||
|
||||
- Add ChatterBox Official 23-Lang Vietnamese community finetune
|
||||
- Add Vietnamese (Viterbox) support - community finetune with 24 languages total
|
||||
- Vietnamese model auto-downloads from dolly-vn/viterbox on first use
|
||||
- Select Vietnamese (Viterbox) from model version dropdown
|
||||
- Support for custom community finetunes with different vocab sizes
|
||||
|
||||
### Changed
|
||||
|
||||
- Expanded Vietnamese tokenization for better quality
|
||||
## [4.16.14] - 2026-01-24
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for Apple Silicon Homebrew path (/opt/homebrew/lib)
|
||||
- Add support for Intel Mac Homebrew path (/usr/local/lib)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix macOS install script failing on Apple Silicon with Homebrew
|
||||
- Fix dependency check not detecting Homebrew-installed libsamplerate and portaudio
|
||||
- Address issue #228
|
||||
## [4.16.13] - 2026-01-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Step Audio EditX initialization on embedded Python 3.12+
|
||||
- Add setuptools dependency for Python 3.12+ distutils compatibility
|
||||
- Fix Step Audio EditX failing to load on embedded Python installations
|
||||
- Resolve model registration failures in FunASR bundled code
|
||||
- Address 'NoneType' object is not callable error during engine initialization
|
||||
## [4.16.12] - 2026-01-20
|
||||
|
||||
### Added
|
||||
|
||||
- Step Audio EditX engine now initializes correctly
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Step Audio EditX import errors preventing engine initialization
|
||||
- Fix ModuleNotFoundError: No module named 'config.prompts'
|
||||
- Resolve module name collision with other config.py files in project
|
||||
## [4.16.11] - 2026-01-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix F5-TTS vocab.txt loading errors
|
||||
- Fix file not found error when loading E2-TTS models from HuggingFace
|
||||
- Fix fallback model loading path resolution
|
||||
- Resolve doubled 'infer/infer/' path issue (issue #222)
|
||||
## [4.16.10] - 2026-01-10
|
||||
|
||||
### Added
|
||||
|
||||
- Add automatic precision detection based on GPU capability (bf16 for RTX 30xx+, fp16 for older GPUs)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Step Audio EditX model loading and GPU compatibility
|
||||
- Fix dtype parameter error preventing model initialization
|
||||
- Improve compatibility with older GPUs (SM 7.5 and below)
|
||||
## [4.16.9] - 2026-01-07
|
||||
|
||||
### Added
|
||||
|
||||
- Add clear tooltips explaining TensorRT (<1% speedup) vs vLLM (~2.5x speedup)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve debug logging for optimization loading
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix CosyVoice TensorRT and vLLM loading issues
|
||||
- Fix TensorRT engine configuration (was missing required inputs)
|
||||
- Fix optimization settings not being preserved during lazy loading
|
||||
## [4.16.8] - 2026-01-05
|
||||
|
||||
### Added
|
||||
|
||||
- Add IndexTTS-2 QwenEmotion debug logging
|
||||
- Add debug output to help diagnose model loading issues
|
||||
- Shows exact path being searched for emotion model
|
||||
- Helps identify path mismatches on affected systems
|
||||
## [4.16.7] - 2026-01-03
|
||||
|
||||
### Added
|
||||
|
||||
- Voices configured in extra_model_paths.yaml now properly discovered
|
||||
- Voice files from shared model directories now appear in dropdowns
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix voices folder not discovered from extra_model_paths.yaml
|
||||
- Fixes issue where only local ComfyUI voices folder was scanned
|
||||
## [4.16.6] - 2026-01-03
|
||||
|
||||
### Added
|
||||
|
||||
- Characters without .txt files now properly clone voice from audio only
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix CosyVoice3 character switching without text references
|
||||
- Fix characters with audio but no .txt file using wrong voice
|
||||
- Automatically use cross_lingual mode when text reference is missing
|
||||
## [4.16.5] - 2026-01-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix CosyVoice3 PyYAML 6.0+ compatibility (enhanced fix)
|
||||
- Apply PyYAML compatibility patch across all CosyVoice entry points
|
||||
- Fix 'Loader' object has no attribute max_depth error more reliably
|
||||
- Ensure patch is applied before any yaml imports in the loading chain
|
||||
## [4.16.4] - 2026-01-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix CosyVoice3 model loading with PyYAML 6.0+ (tentative)
|
||||
- Fix 'Loader' object has no attribute max_depth error in CosyVoice3
|
||||
- Improve compatibility with newer PyYAML versions (6.0+)
|
||||
- Resolve CosyVoice3 initialization failures
|
||||
## [4.16.3] - 2026-01-02
|
||||
|
||||
### Added
|
||||
|
||||
- Address issues with embed_tokens and affine layers staying on CPU
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix CosyVoice3 device mismatch errors (tentative)
|
||||
- Fix 'Expected all tensors to be on the same device' error in CosyVoice3
|
||||
- Improve compatibility with PyTorch 2.9.1+cu130
|
||||
## [4.16.2] - 2026-01-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve detection accuracy by checking actual module location instead of path keywords
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix IndexTTS-2 false positive detection in conda environments
|
||||
- Fix bundled IndexTTS-2 incorrectly flagged as external installation
|
||||
- Resolve compatibility issue when ComfyUI installed in directories with 'conda' in path
|
||||
## [4.16.1] - 2025-12-29
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix IndexTTS-2 QwenEmotion text emotion model failing to load
|
||||
- Fix error preventing QwenEmotion model from loading on some systems
|
||||
- Addresses compatibility issue with certain Python/modelscope versions
|
||||
- Note: Tentative fix - user testing required to confirm resolution
|
||||
## [4.16.0] - 2025-12-29
|
||||
|
||||
### Added
|
||||
|
||||
- Add CosyVoice3 TTS engine with multilingual support and voice conversion
|
||||
- New CosyVoice3 engine with 4 core languages (Chinese, English, Japanese, Korean) plus 5 additional languages
|
||||
- Voice cloning with zero-shot, instruction, and cross-lingual modes
|
||||
- Voice Changer (VC) with iterative refinement for quality improvement
|
||||
- Model variants: standard and RL-enhanced (improved quality, set as default)
|
||||
- Paralinguistic tag support for natural speech effects (laughter, breath, cough, etc.)
|
||||
- Character voice switching with [CharacterName] syntax
|
||||
- Language switching with [lang:code] syntax
|
||||
- SRT subtitle timing support for synchronized audio generation
|
||||
- Per-segment parameter control ([seed:42], [speed:1.5])
|
||||
- Live generation progress with token-by-token updates and ETA
|
||||
- Credit: Initial CosyVoice3 implementation by @tazztone (https://github.com/tazztone/TTS-Audio-Suite)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix ChatterBox and RVC model discovery to work with custom model paths
|
||||
- Fix RVC audio chunking errors with short segments
|
||||
## [4.15.17] - 2025-12-28
|
||||
|
||||
### Added
|
||||
|
||||
- Parameters are now properly applied to generation
|
||||
- Add warning when using inline edit tags with multiple speakers in Native Multi-Speaker mode (edit tags only work with single narrator or Custom Character Switching mode)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix VibeVoice inline parameters not applying correctly
|
||||
- Fix inline parameters like [seed:2|cfg:7] hitting cache incorrectly
|
||||
## [4.15.16] - 2025-12-28
|
||||
|
||||
### Added
|
||||
|
||||
- Russian model now downloads correctly from HuggingFace
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix ChatterBox Russian model download failing with 404 error
|
||||
- Resolves issue #210 crash on Russian text generation
|
||||
## [4.15.15] - 2025-12-28
|
||||
|
||||
### Added
|
||||
|
||||
- IndexTTS-2 SRT generation now works correctly with smart_natural
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix IndexTTS-2 SRT crash in smart_natural timing mode
|
||||
- timing mode - fixes crash that occurred during audio assembly
|
||||
## [4.15.14] - 2025-12-28
|
||||
|
||||
### Added
|
||||
|
||||
- that would cause installation errors
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix pyproject.toml configuration file syntax
|
||||
- ComfyUI Registry compatibility fix - removed invalid dependencies field
|
||||
## [4.15.13] - 2025-12-20
|
||||
|
||||
### Added
|
||||
|
||||
- Add automatic fallback to bundled vocab if download fails
|
||||
- Help users diagnose download issues more easily
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve F5-TTS model download error detection
|
||||
- Fix confusing errors when F5-TTS model downloads are incomplete
|
||||
- Provide clearer error messages indicating which files are missing
|
||||
## [4.15.12] - 2025-12-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve Audio Editor standalone mode reliability
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Step Audio EditX Audio Editor node errors
|
||||
- Fix Audio Editor failing with 'object has no attribute edit_single' error
|
||||
- Fix Windows compatibility issue with audio tokenization (torchcodec BytesIO)
|
||||
- Fix model not found error when using extra_model_paths.yaml configuration
|
||||
## [4.15.11] - 2025-12-18
|
||||
|
||||
### Added
|
||||
|
||||
- Removes GPU memory copy and node assignment warnings
|
||||
- These warnings are expected and don't affect functionality
|
||||
|
||||
### Changed
|
||||
|
||||
- Suppress harmless ONNX Runtime performance warnings in Step Audio EditX
|
||||
- Cleaner console output during Step Audio EditX generation
|
||||
## [4.15.10] - 2025-12-18
|
||||
|
||||
### Added
|
||||
|
||||
- Character voices discovery now logs only once per workflow
|
||||
- Eliminates duplicate 'Found X characters' messages in console
|
||||
|
||||
### Changed
|
||||
|
||||
- Character changes on refresh still show update notifications
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix character discovery log spam when using multiple character nodes
|
||||
## [4.15.9] - 2025-12-18
|
||||
|
||||
### Added
|
||||
|
||||
- Silent/gibberish audio generation
|
||||
- Generation ignoring max_new_tokens parameter
|
||||
- Model behaving unpredictably across different transformers versions
|
||||
- Implementation:
|
||||
- Detects weight tying via data pointer comparison
|
||||
- Detailed logging for future removal if stepfun-ai fixes their model
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Step Audio EditX silence and max_new_tokens generation bug
|
||||
- Properly resolves transformers 4.54+ weight tying issue that caused:
|
||||
- Restores correct lm_head weights from model checkpoint
|
||||
- Comprehensive error handling with fallback behavior
|
||||
## [4.15.8] - 2025-12-17
|
||||
|
||||
### Added
|
||||
|
||||
- Detect conflicting utils.py files from other custom nodes
|
||||
- Add warning message showing which node is causing conflict
|
||||
|
||||
### Fixed
|
||||
|
||||
- Address issue #191: utils namespace conflict from other custom nodes
|
||||
- This is a tentative fix for users experiencing 'utils' is not a package errors.
|
||||
- Should resolve all engine loading failures
|
||||
- Note: This fix has been tested with simulated conflicts but not yet confirmed on affected user systems.
|
||||
|
||||
### Removed
|
||||
|
||||
- Automatically remove conflicts before TTS Suite loads
|
||||
## [4.15.7] - 2025-12-17
|
||||
|
||||
### Added
|
||||
|
||||
- Add GPU acceleration support for Step Audio EditX ONNX operations
|
||||
- New features:
|
||||
- Automatic ONNX GPU acceleration for CosyVoice vocoder and FunASR processing
|
||||
- Verified working on Windows with Python 3.10+
|
||||
- Improvements:
|
||||
- Clarified Step Audio EditX model directory structure in documentation
|
||||
|
||||
### Fixed
|
||||
|
||||
- Smart CUDA detection with automatic CPU fallback for compatibility
|
||||
- Better error handling for missing GPU libraries
|
||||
## [4.15.6] - 2025-12-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Step Audio EditX silence/humming output issue
|
||||
- Resolves transformers 4.54+ compatibility by ensuring lm_head and embed_tokens weights are not incorrectly tied during model loading. Users will now get proper audio generation from Step Audio EditX without needing to re-download models or update config.json manually.
|
||||
## [4.15.5] - 2025-12-16
|
||||
|
||||
### Added
|
||||
|
||||
- Prevent AttributeError when Step Audio EditX model selection is None or invalid. Now gracefully defaults to auto-download instead of crashing.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Step Audio EditX model loading crash on Windows
|
||||
## [4.15.4] - 2025-12-16
|
||||
|
||||
### Added
|
||||
|
||||
- Add comprehensive validation for CosyVoice model files
|
||||
- Clear instructions for fixing corrupted model downloads
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve Step Audio EditX model validation and error handling
|
||||
- Better error messages when model files are missing or incomplete
|
||||
- Improved compatibility with different hyperpyyaml versions
|
||||
## [4.15.3] - 2025-12-12
|
||||
|
||||
### Added
|
||||
|
||||
- Add Character Voices node refresh support
|
||||
- Character Voices dropdown now refreshes with R key (no restart needed)
|
||||
- Character switching tags refresh automatically when pressing R
|
||||
- New voice files appear immediately after refresh
|
||||
- Console shows character count updates when changes detected
|
||||
## [4.15.2] - 2025-12-12
|
||||
|
||||
### Added
|
||||
|
||||
- Extension now immune to Python import path conflicts
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix extension failing to load when other custom nodes have utils.py files
|
||||
- Fix 'utils' is not a package error affecting all engines
|
||||
- Improve compatibility with other ComfyUI extensions
|
||||
## [4.15.1] - 2025-12-12
|
||||
|
||||
### Added
|
||||
|
||||
- Add IndexTTS-2 Low VRAM mode for 8GB VRAM GPUs
|
||||
- Add Low VRAM option to IndexTTS-2 Engine node
|
||||
- Keep models on CPU and move to GPU only when needed during generation
|
||||
- Prevents out-of-memory errors on 8GB VRAM cards
|
||||
- Successfully tested with <8GB VRAM usage throughout generation
|
||||
- Allows longer reference audio (tested up to ~2 minutes)
|
||||
- Trade-off: Slightly slower generation due to model swapping
|
||||
- Credit: Feature contributed by @CrispyToken in PR #186
|
||||
## [4.15.0] - 2025-12-12
|
||||
|
||||
### Added
|
||||
|
||||
+56
-7
@@ -7,9 +7,9 @@
|
||||
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 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
|
||||
- **Universal Model Loading System**: Standardized model loading interface across all engines (ChatterBox, F5-TTS, Higgs Audio, VibeVoice, Step Audio EditX, Qwen3-TTS, RVC, Audio Separation) with fallback support
|
||||
- **Engine Processors**: Internal processing engines for each TTS system (ChatterBox, F5-TTS, Higgs Audio, VibeVoice, Step Audio EditX, Qwen3-TTS) handling engine-specific orchestration
|
||||
- **Engine Adapters**: Modular adapters for ChatterBox, F5-TTS, Higgs Audio 2, VibeVoice, Step Audio EditX, Qwen3-TTS, 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)
|
||||
@@ -111,6 +111,21 @@ This extension features a **unified modular architecture** supporting multiple T
|
||||
- **model_loader.py** - Model loading and management utilities
|
||||
- **quantization/** - AWQ quantization support for VRAM reduction
|
||||
|
||||
**engines/cosyvoice/** - CosyVoice3 TTS engine with multilingual voice cloning
|
||||
- **cosyvoice.py** - Core CosyVoice3 engine wrapper with zero-shot, instruct, and cross-lingual modes
|
||||
- **cosyvoice_downloader.py** - Auto-download system for Fun-CosyVoice3-0.5B model
|
||||
- **impl/** - Bundled CosyVoice library from FunAudioLLM/CosyVoice repository
|
||||
|
||||
**engines/processors/cosyvoice_processor.py** - CosyVoice3 TTS processor with pause tags, character switching, and chunking support
|
||||
|
||||
**engines/qwen3_tts/** - Qwen3-TTS engine with CustomVoice (presets), VoiceDesign (text-to-voice), and Base (voice cloning) models
|
||||
- **qwen3_tts.py** - Core wrapper with VRAM management and 3 model type support (0.6B/1.7B sizes)
|
||||
- **qwen3_tts_downloader.py** - Auto-download system for 6 model variants and tokenizer
|
||||
- **impl/** - Bundled official Qwen3-TTS implementation
|
||||
|
||||
**engines/qwen3_asr/** - Qwen3-ASR engine implementation (bundled)
|
||||
- **impl/** - Bundled official Qwen3-ASR implementation (qwen_asr package)
|
||||
|
||||
**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
|
||||
@@ -140,7 +155,13 @@ This extension features a **unified modular architecture** supporting multiple T
|
||||
|
||||
**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/cosyvoice_adapter.py** - CosyVoice3 engine adapter with zero-shot, instruct, and cross-lingual mode support and character switching
|
||||
|
||||
**engines/adapters/qwen3_tts_adapter.py** - Qwen3-TTS adapter with intelligent model selection (CustomVoice/VoiceDesign/Base), character-to-speaker mapping, and audio hash integration
|
||||
|
||||
**engines/adapters/asr_qwen3_adapter.py** - Qwen3-ASR adapter providing unified ASR interface with forced aligner support
|
||||
|
||||
**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
|
||||
|
||||
@@ -164,13 +185,19 @@ This extension features a **unified modular architecture** supporting multiple T
|
||||
|
||||
**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
|
||||
|
||||
**nodes/engines/cosyvoice_engine_node.py** - ⚙️ CosyVoice3 Engine configuration node with mode selection (zero_shot, instruct, cross_lingual), speed control, and FP16 support
|
||||
|
||||
**nodes/engines/qwen3_tts_engine_node.py** - ⚙️ Qwen3-TTS Engine configuration node with unified voice_preset dropdown (9 speakers + zero-shot), model size selection (0.6B/1.7B), language support (10 languages), and conditional instruction field
|
||||
|
||||
### 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 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
|
||||
**nodes/unified/voice_changer_node.py** - Universal voice conversion node with multilingual model support and flexible audio inputs
|
||||
|
||||
**nodes/unified/asr_transcribe_node.py** - Universal ASR transcription node working with any engine that advertises ASR capability
|
||||
|
||||
### Shared Components
|
||||
|
||||
@@ -210,6 +237,14 @@ This extension features a **unified modular architecture** supporting multiple T
|
||||
- **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
|
||||
|
||||
**nodes/cosyvoice/** - CosyVoice3 internal processors
|
||||
- **cosyvoice_srt_processor.py** - CosyVoice3 SRT processor with character switching, timing modes, and interrupt handling
|
||||
- **cosyvoice_vc_processor.py** - CosyVoice3 voice conversion processor with zero-shot, instruct, and cross-lingual voice cloning modes
|
||||
|
||||
**nodes/qwen3_tts/** - Qwen3-TTS internal processors
|
||||
- **qwen3_tts_processor.py** - Qwen3-TTS TTS processor with character switching (CustomVoice presets + Base cloning + VoiceDesign), pause tags, language switching, and MD5-based character-to-speaker mapping
|
||||
- **qwen3_tts_voice_designer_node.py** - 🎨 Qwen3-TTS Voice Designer node creating voices from text descriptions with smart disk caching and unified NARRATOR_VOICE output
|
||||
|
||||
### 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
|
||||
@@ -302,6 +337,8 @@ This extension features a **unified modular architecture** supporting multiple T
|
||||
|
||||
**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
|
||||
|
||||
**utils/text/cosyvoice_special_tags.py** - CosyVoice3 special tag system for parsing and handling CosyVoice-specific inline tags and control tokens for emotion, style, and paralinguistic effects
|
||||
|
||||
### Voice Management
|
||||
|
||||
**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
|
||||
@@ -346,7 +383,15 @@ This extension features a **unified modular architecture** supporting multiple T
|
||||
|
||||
**utils/streaming/streaming_coordinator.py** - Universal coordinator replacing all format-specific routers
|
||||
|
||||
**utils/streaming/work_queue_processor.py** - Engine-agnostic parallel processing system
|
||||
**utils/streaming/work_queue_processor.py** - Engine-agnostic parallel processing system
|
||||
|
||||
## ASR System
|
||||
|
||||
**utils/asr/types.py** - ASR data types (requests, results, segments, word timestamps)
|
||||
|
||||
**utils/asr/adapter_registry.py** - ASR adapter registry mapping engine_type to adapter classes
|
||||
|
||||
**utils/asr/pipeline.py** - Unified ASR pipeline orchestration and output formatting
|
||||
|
||||
## Web Interface Components
|
||||
|
||||
@@ -388,6 +433,10 @@ This extension features a **unified modular architecture** supporting multiple T
|
||||
|
||||
**web/step_audio_editx_showcontrol.js** - Step Audio EditX SRT subtitle display and timing controls
|
||||
|
||||
**web/qwen3_tts_widgets.js** - Qwen3-TTS widget locking system for conditional instruction field based on voice_preset and model_size
|
||||
|
||||
**web/asr_srt_preset_widgets.js** - ASR SRT preset widget locking and auto-apply for readability presets
|
||||
|
||||
**web/audio_analyzer.css** - Styling for audio analyzer interface components
|
||||
|
||||
## Development Tools
|
||||
@@ -440,4 +489,4 @@ The TTS Audio Suite follows a **unified modular architecture** where:
|
||||
7. **Utilities** (`utils/`) provide shared functionality across all components
|
||||
8. **Web Interface** (`web/`) enables interactive features like audio analysis
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
[![Dynamic TOML Badge][version-shield]][version-url]
|
||||
[](https://ko-fi.com/diogogo)
|
||||
|
||||
# TTS Audio Suite v4.15.0
|
||||
# TTS Audio Suite v4.20.1
|
||||
|
||||
[](https://ko-fi.com/diogogo)
|
||||
|
||||
@@ -19,6 +19,27 @@
|
||||
|
||||
A comprehensive ComfyUI extension providing unified Text-to-Speech, Voice Conversion, and Audio Editing capabilities through multiple engines including ChatterboxTTS, F5-TTS, Higgs Audio 2, Step Audio EditX, and RVC (Real-time Voice Conversion), with modular architecture designed for extensibility and future engine integrations.
|
||||
|
||||
<!-- ENGINE_COMPARISON_START -->
|
||||
|
||||
## Quick Engine Comparison
|
||||
|
||||
| Engine | Languages | Size | Key Features |
|
||||
|--------|-----------|------|--------------|
|
||||
| **F5-TTS** | 🇺🇸🇩🇪🇪🇸🇫🇷🇮🇹🇯🇵 +4 | ~1.2GB each | Targeted Word/Speech Editing, Speed control |
|
||||
| **ChatterBox** | 🇺🇸🇩🇪🇫🇷🇮🇹🇯🇵🇰🇷 +4 | ~4.3GB | Expressiveness slider |
|
||||
| **ChatterBox 23L** | 🌐 24 languages | ~4.3GB | 24 languages in single model, emotion tokens (v2 - doesn't work) |
|
||||
| **VibeVoice** | 🇺🇸🇨🇳🇮🇳 | 5.4GB / 18GB | 90-min long-form, Native 4-speaker |
|
||||
| **Higgs Audio 2** | 🇺🇸🇨🇳🇩🇪🇪🇸🇰🇷 | ~9GB | 3 multi-speaker, CUDA graphs (55+ tokens/sec) |
|
||||
| **IndexTTS-2** | 🇺🇸🇨🇳🇯🇵 | ? | Emotion Control: 8 vectors, Text as reference |
|
||||
| **CosyVoice3** | 🇺🇸🇨🇳🇯🇵🇰🇷 | ~5.4GB | Paralinguistic tags |
|
||||
| **Qwen3-TTS** | 🇺🇸🇨🇳🇩🇪🇪🇸🇫🇷🇮🇹 +4 | ~3-6GB | Voice design |
|
||||
| **Step Audio EditX** | 🇺🇸🇨🇳🇯🇵🇰🇷 | ~7GB | Second Pass Speech Editing Node: 14 emotions, 32 speaking styles |
|
||||
| **RVC** | 🌐 Any | 100-300MB | Real-time VC, Pitch shift (±14) |
|
||||
|
||||
📊 **[Full comparison tables →](docs/ENGINE_COMPARISON.md)** | **[Language matrix →](docs/LANGUAGE_SUPPORT.md)** | **[Feature matrix →](docs/FEATURE_COMPARISON.md)**
|
||||
|
||||
<!-- ENGINE_COMPARISON_END -->
|
||||
|
||||
## 🚀 Project Evolution Timeline
|
||||
|
||||
```
|
||||
@@ -55,12 +76,14 @@ IndexTTS-2 Chatterbox VibeVoice
|
||||
Emotion Multilingual New TTS Engine
|
||||
Control Official (23-lang) 90min Generation
|
||||
│
|
||||
▼
|
||||
v4.12 ──────────►
|
||||
Oct 25
|
||||
│
|
||||
Per-Seg Parameter
|
||||
Switching [seed:24]
|
||||
│ 🎨 Inline Editor Tags Era
|
||||
▼ |
|
||||
v4.12 ──────────────► v4.15 ────────────► v4.16 ──────────► v4.19
|
||||
Oct 25 Dez 25 Dez 25 Jan 26
|
||||
│ │ │ │
|
||||
Per-Seg Parameter Step Audio EditX CosyVoice3 Qwen3-TTS
|
||||
Switching [seed:24] Inline Edit tags TTS + VC TTS
|
||||
<laughter:2> VoiceDesign
|
||||
```
|
||||
|
||||
<details>
|
||||
@@ -84,6 +107,8 @@ Switching [seed:24]
|
||||
- [🎵 VibeVoice Long-Form Generation](#-vibevoice-long-form-generation)
|
||||
- [🌈 IndexTTS-2 With Emotion Control](#-indextts-2-with-emotion-control)
|
||||
- [🎨 Step Audio EditX - LLM Audio Editing](#-step-audio-editx---llm-audio-editing)
|
||||
- [🗣️ CosyVoice3 Multilingual Voice Cloning](#️-cosyvoice3-multilingual-voice-cloning)
|
||||
- [🎤 Qwen3-TTS - 3 Model Types with Text-to-Voice Design](#-qwen3-tts---3-model-types-with-text-to-voice-design)
|
||||
- [📝 Phoneme Text Normalizer](#-phoneme-text-normalizer)
|
||||
- [🏷️ Multiline TTS Tag Editor & Per-Segment Parameter Switching](#️-multiline-tts-tag-editor--per-segment-parameter-switching)
|
||||
- [🚀 Quick Start](#-quick-start)
|
||||
@@ -147,7 +172,7 @@ Switching [seed:24]
|
||||
|
||||
## Features
|
||||
|
||||
- 🎤 **Multi-Engine TTS** - ChatterBox TTS, **Chatterbox Multilingual TTS**, F5-TTS, Higgs Audio 2, VibeVoice, and **IndexTTS-2** with voice cloning, reference audio synthesis, and production-grade quality
|
||||
- 🎤 **Multi-Engine TTS** - ChatterBox TTS, **Chatterbox Multilingual TTS**, F5-TTS, Higgs Audio 2, VibeVoice, **IndexTTS-2**, **CosyVoice3**, and **Qwen3-TTS** with voice cloning, reference audio synthesis, and production-grade quality
|
||||
- 🎨 **Audio Post-Processing** - **Step Audio EditX** LLM-based audio editing with paralinguistic effects (laughter, breathing, sigh), emotion control (14 emotions), speaking styles (32 styles), speed adjustment, and voice restoration → **[📖 Inline Edit Tags Guide](docs/INLINE_EDIT_TAGS_USER_GUIDE.md)**
|
||||
- 🔄 **Voice Conversion** - ChatterBox VC with iterative refinement + RVC real-time conversion using .pth character models
|
||||
- 🎙️ **Voice Capture & Recording** - Smart silence detection and voice input recording
|
||||
@@ -162,10 +187,10 @@ Switching [seed:24]
|
||||
- 🌊 **Audio Wave Analyzer** - Interactive waveform visualization and precise timing extraction for F5-TTS workflows → **[📖 Complete Guide](docs/🌊_Audio_Wave_Analyzer-Complete_User_Guide.md)**
|
||||
- 🗣️ **Silent Speech Analyzer** - Video analysis with experimental viseme detection, mouth movement tracking, and base SRT timing generation from silent video using MediaPipe
|
||||
- ⚙️ **Parallel Processing** - Configurable worker-based processing via `batch_size` parameter (Note: sequential processing with `batch_size=0` remains optimal for performance)
|
||||
- ⚡ **Performance Optimizations** - Qwen3-TTS supports torch.compile for ~1.7x speedup (requires PyTorch 2.10+ and triton-windows 3.6+) → **[📖 Optimization Guide](docs/qwen3_tts_optimizations.md)**
|
||||
|
||||
<div align="right"><a href="#-table-of-contents">Back to top</a></div>
|
||||
|
||||
<details>
|
||||
<summary><h2>🆕 What's New in my Project? (click to expand)</h2></summary>
|
||||
|
||||
<details>
|
||||
@@ -292,7 +317,7 @@ For comprehensive technical information, refer to the [SRT_IMPLEMENTATION.md](do
|
||||
|
||||
**Technical Features:**
|
||||
|
||||
- **Dual Model Support**: Microsoft vibevoice-1.5B (2.7B params, faster) and community vibevoice-7B (9.3B params, higher quality)
|
||||
- **Model Support**: Microsoft vibevoice-1.5B/7B (official models for English/Chinese) + vibevoice-hindi-1.5B/7B (Hindi community finetunes supporting Hindi/English)
|
||||
- **Intelligent Caching**: Advanced caching system with mode-aware invalidation for instant regeneration
|
||||
- **Memory Optimization**: Configurable chunking system balances quality with memory usage
|
||||
- **Unified Architecture**: Seamless integration with existing TTS Text and TTS SRT nodes
|
||||
@@ -494,10 +519,12 @@ The **Chatterbox Multilingual TTS** (referred to internally as "ChatterBox Offic
|
||||
* **MIT Licensed**: Fully open-source with commercial usage rights
|
||||
* **Perth Watermarking**: Built-in responsible AI usage (disabled by default for compatibility)
|
||||
|
||||
**🌍 Supported Languages (23 total):**
|
||||
**🌍 Supported Languages (23 total + Vietnamese community finetune):**
|
||||
|
||||
Arabic (ar), Danish (da), German (de), Greek (el), English (en), Spanish (es), Finnish (fi), French (fr), Hebrew (he), Hindi (hi), Italian (it), Japanese (ja), Korean (ko), Malay (ms), Dutch (nl), Norwegian (no), Polish (pl), Portuguese (pt), Russian (ru), Swedish (sv), Swahili (sw), Turkish (tr), Chinese (zh)
|
||||
|
||||
**🇻🇳 Vietnamese (Viterbox)**: Community finetune by Dolly AI 23 with expanded Vietnamese tokenization (dolly-vn/viterbox) - select from model version dropdown
|
||||
|
||||
**🔧 Fully Integrated Features:**
|
||||
|
||||
* ✅ **Character Switching**: Full `[CharacterName]` support with per-character voice references
|
||||
@@ -665,6 +692,105 @@ Welcome to our show! [Alice:happy_sarah] I'm so excited to be here!
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>🗣️ CosyVoice3 Multilingual Voice Cloning</h3></summary>
|
||||
|
||||
**NEW in v4.16**: Alibaba's fast multilingual voice cloning with native paralinguistic tags, instruct mode, and zero-shot voice conversion!
|
||||
|
||||
* **⚡ Ultra-Fast Generation**: ~0.05 RTF (20x faster than real-time) - 10s audio in ~0.5s
|
||||
* **🎵 Native Paralinguistic Tags**: Built-in `<breath>`, `<laughter>`, `<cough>`, `<sigh>`, `<laughing>text</laughing>` - processed during generation → **[📖 Tags Guide](docs/COSYVOICE3_TAGS_GUIDE.md)**
|
||||
* **🎭 Instruct Mode**: Natural language control - "Speak with a joyful tone" or "Use Cantonese dialect with excitement"
|
||||
* **🔄 Zero-Shot Voice Conversion**: Built-in VC with iterative refinement (10 passes) and chunking via Voice Changer node
|
||||
* **🌍 4 Core Languages**: English, Chinese, Japanese, Korean with native language tag support
|
||||
* **🎤 Zero-Shot Voice Cloning**: Clone any voice from 3-30s reference audio
|
||||
* **~5.4GB Model**: Efficient 0.5B parameter model with production quality
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- **Native Tag Support**: 13 paralinguistic effects - `<breath>`, `<laughter>`, `<cough>`, `<sigh>`, `<gasp>`, `<laughing>text</laughing>`, `<strong>text</strong>`
|
||||
- **Instruct Mode**: Control emotions, dialects, styles via text instructions - no tag syntax needed
|
||||
- **Fast Voice Conversion**: CosyVoice VC with 10-pass iterative refinement and smart/fixed chunking
|
||||
- **Character Switching**: Full `[CharacterName]` support with per-character voice references
|
||||
- **Language Switching**: `[en:]`, `[zh:]`, `[ja:]`, `[ko:]` bracket syntax or native `<|en|>` tags
|
||||
- **Pause Tags**: Complete `[pause:Ns]` support for natural speech timing
|
||||
- **SRT Processing**: Advanced subtitle timing with all timing modes
|
||||
- **Speed Control**: 0.5x to 2.0x speech speed adjustment
|
||||
|
||||
**Example Usage:**
|
||||
|
||||
```
|
||||
[Alice] Hello everyone! This is zero-shot voice cloning.
|
||||
[Bob] 你好!我说普通话。[pause:1s] 还可以说方言。
|
||||
```
|
||||
|
||||
**Instruct Mode Examples:**
|
||||
|
||||
```
|
||||
# Speak with Cantonese dialect
|
||||
Instruct: 请用广东话表达。
|
||||
|
||||
# Speak with excitement
|
||||
Instruct: 用兴奋的语气说话。
|
||||
```
|
||||
|
||||
**Perfect for:**
|
||||
|
||||
- Multilingual content with Chinese dialect support
|
||||
- Voice cloning with fine emotional control via instructions
|
||||
- Multi-character conversations across languages
|
||||
- Professional localization with native accent preservation
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>🎤 Qwen3-TTS - 3 Model Types with Text-to-Voice Design</h3></summary>
|
||||
|
||||
**NEW in v4.19**: Alibaba's Qwen3-TTS with 3 distinct model types - CustomVoice presets, unique text-to-voice design, and zero-shot voice cloning!
|
||||
|
||||
**Model Types:**
|
||||
|
||||
* **🎭 CustomVoice Model**: 9 preset multilingual speakers (Vivian, Serena, Uncle_Fu, Dylan, Eric, Ryan, Aiden, Ono_Anna, Sohee)
|
||||
- Optional instruction field for style control ("Speak cheerfully", "Sound professional")
|
||||
- Character switching auto-maps to different preset speakers
|
||||
|
||||
* **✍️ VoiceDesign Model**: **UNIQUE** - Create voices from text descriptions
|
||||
- Input: "A cheerful young woman with a bright, energetic tone"
|
||||
- Output: Instant voice generation matching the description
|
||||
- Smart disk caching for reuse across sessions
|
||||
|
||||
* **🎤 Base Model**: Zero-shot voice cloning from 3-30s reference audio
|
||||
- In-Context Learning (ICL) mode for best quality
|
||||
- X-Vector mode for faster generation
|
||||
|
||||
**Technical Specs:**
|
||||
|
||||
* **🌍 10 Languages**: Chinese, English, Japanese, Korean, German, French, Russian, Portuguese, Spanish, Italian
|
||||
* **📏 2 Model Sizes**: 0.6B and 1.7B parameter variants
|
||||
* **⚡ Attention Options**: eager, flash_attention_2, sdpa, sage_attn for performance tuning
|
||||
* **🔧 Generation Control**: Temperature, top_k, top_p, repetition_penalty parameters
|
||||
* **⚡ torch.compile Optimizations**: Optional 1.7x speedup (PyTorch 2.10+ required) → [📖 Setup Guide](docs/qwen3_tts_optimizations.md)
|
||||
|
||||
**Unified Features Support:**
|
||||
- Works with all project features: character switching, language switching, pause tags, SRT timing, Step Audio EditX post-processing
|
||||
|
||||
**Voice Designer Node:**
|
||||
|
||||
Unique text-to-voice generation node that creates voices from descriptions and outputs unified NARRATOR_VOICE format for use with any TTS node.
|
||||
|
||||
```
|
||||
Description: "A deep, authoritative male voice with clear articulation"
|
||||
→ Voice generated and cached → Use in TTS Text/SRT nodes
|
||||
```
|
||||
|
||||
**Perfect for:**
|
||||
|
||||
- Quick multilingual content with preset speakers (CustomVoice)
|
||||
- **Creative voice design from text descriptions** (VoiceDesign) - **unique to Qwen3-TTS**
|
||||
- High-quality voice cloning with reference audio (Base)
|
||||
- Content requiring specific vocal characteristics defined by text
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h3>📝 Phoneme Text Normalizer</h3></summary>
|
||||
|
||||
@@ -751,8 +877,6 @@ Perfect for:
|
||||
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
> [!TIP]
|
||||
@@ -1172,9 +1296,10 @@ ComfyUI/models/TTS/chatterbox_official_23lang/
|
||||
**Model Selection:**
|
||||
|
||||
- Choose "ChatterBox Official 23-Lang" from Unified TTS Engine dropdown
|
||||
- Select model version (v1 or v2) in the Engine Configuration node
|
||||
- **Auto-download**: Missing v2 files download automatically on first use (only ~2.2GB if v1 already installed)
|
||||
- Both versions can coexist - switch between them without re-downloading
|
||||
- Select model version (v1, v2, or Vietnamese (Viterbox)) in the Engine Configuration node
|
||||
- **Auto-download**: Missing files download automatically on first use
|
||||
- All versions can coexist - switch between them without re-downloading
|
||||
- Vietnamese (Viterbox) is a community finetune with expanded Vietnamese tokenization (24 languages total)
|
||||
|
||||
### 5. F5-TTS Models (Optional)
|
||||
|
||||
@@ -1492,22 +1617,25 @@ ComfyUI/models/TTS/IndexTTS/
|
||||
|
||||
```
|
||||
ComfyUI/models/TTS/step_audio_editx/
|
||||
├── config.json ← Model configuration
|
||||
├── configuration_step1.py ← Step-1 configuration
|
||||
├── modeling_step1.py ← Step-1 modeling code
|
||||
├── model-00001.safetensors ← Main 3B LLM weights (~6.3GB)
|
||||
├── model.safetensors.index.json ← Model index
|
||||
├── tokenizer.model ← SentencePiece tokenizer
|
||||
├── tokenizer_config.json ← Tokenizer configuration
|
||||
├── linguistic_tokenizer.npy ← Linguistic tokenizer (from Step-Audio-Tokenizer)
|
||||
├── speech_tokenizer_v1.onnx ← Speech tokenizer (from Step-Audio-Tokenizer)
|
||||
└── CosyVoice-300M-25Hz/ ← CosyVoice vocoder (24kHz output)
|
||||
├── FLOW_VERSION
|
||||
├── campplus.onnx ← Speaker verification
|
||||
├── cosyvoice.yaml
|
||||
├── flow.pt ← Flow matching model
|
||||
├── hift.pt ← HiFi-GAN vocoder
|
||||
└── speech_tokenizer_v1.onnx ← VQ-VAE tokenizer
|
||||
├── Step-Audio-EditX/ ← Main LLM model directory (~7GB auto-download)
|
||||
│ ├── config.json ← Model configuration
|
||||
│ ├── configuration_step1.py ← Step-1 configuration
|
||||
│ ├── modeling_step1.py ← Step-1 modeling code
|
||||
│ ├── model-00001.safetensors ← Main 3B LLM weights (~6.3GB)
|
||||
│ ├── model.safetensors.index.json ← Model index
|
||||
│ ├── tokenizer.model ← SentencePiece tokenizer
|
||||
│ ├── tokenizer_config.json ← Tokenizer configuration
|
||||
│ ├── linguistic_tokenizer.npy ← Linguistic tokenizer (from Step-Audio-Tokenizer)
|
||||
│ ├── speech_tokenizer_v1.onnx ← Speech tokenizer (from Step-Audio-Tokenizer)
|
||||
│ └── CosyVoice-300M-25Hz/ ← CosyVoice vocoder (24kHz output)
|
||||
│ ├── FLOW_VERSION
|
||||
│ ├── campplus.onnx ← Speaker verification
|
||||
│ ├── cosyvoice.yaml
|
||||
│ ├── flow.pt ← Flow matching model
|
||||
│ ├── hift.pt ← HiFi-GAN vocoder
|
||||
│ └── speech_tokenizer_v1.onnx ← VQ-VAE tokenizer
|
||||
└── FunASR-Paraformer/ ← Speech recognition model (auto-downloaded)
|
||||
└── [FunASR model files]
|
||||
```
|
||||
|
||||
**Available Step Audio EditX Models (Auto-Download):**
|
||||
@@ -1528,7 +1656,83 @@ ComfyUI/models/TTS/step_audio_editx/
|
||||
|
||||
**Usage**: Simply use the 🎨 Step Audio EditX - Audio Editor node → All required models download automatically! Or use inline edit tags with any TTS node for automatic post-processing.
|
||||
|
||||
### 12. Restart ComfyUI
|
||||
### 12. CosyVoice3 Models (NEW in v4.16+)
|
||||
|
||||
**Repository:** [FunAudioLLM/Fun-CosyVoice3-0.5B-2512](https://huggingface.co/FunAudioLLM/Fun-CosyVoice3-0.5B-2512) | **Size:** ~5.4GB | **Auto-Download:** ✅
|
||||
|
||||
**Model Variants:**
|
||||
|
||||
| Model | Description | Auto-Download | Default |
|
||||
|-------|-------------|---------------|---------|
|
||||
| **Fun-CosyVoice3-0.5B-RL** | RL-enhanced (better quality) | ✅ | ✅ |
|
||||
| **Fun-CosyVoice3-0.5B** | Standard model | ✅ | |
|
||||
|
||||
**Installation Structure:**
|
||||
|
||||
```
|
||||
ComfyUI/models/TTS/CosyVoice/
|
||||
└── Fun-CosyVoice3-0.5B/ # Shared folder for both variants
|
||||
├── cosyvoice3.yaml # Model config (shared)
|
||||
├── campplus.onnx # Speaker encoder (shared)
|
||||
├── flow.pt # Flow model (shared)
|
||||
├── hift.pt # HiFi-GAN vocoder (shared)
|
||||
├── llm.pt # Standard LLM (2GB)
|
||||
├── llm.rl.pt # RL-enhanced LLM (2GB)
|
||||
├── speech_tokenizer_v3.onnx # Speech tokenizer (shared)
|
||||
└── CosyVoice-BlankEN/ # Qwen text processor (shared)
|
||||
└── [model files]
|
||||
```
|
||||
|
||||
**Note**:
|
||||
- **Lazy download** - only downloads the LLM file for the variant you select (~5.4GB for first variant)
|
||||
- Switching variants later downloads the other LLM file (~2GB additional)
|
||||
- Shared files (flow.pt, hift.pt, etc.) are downloaded once and reused by both variants
|
||||
|
||||
**Usage**: Select CosyVoice3 from Unified TTS Engine → Choose model variant → Auto-download on first use!
|
||||
|
||||
### 13. Qwen3-TTS Models (NEW in v4.19+)
|
||||
|
||||
**Repository:** [Qwen/Qwen3-TTS](https://huggingface.co/collections/Qwen/qwen3-tts-67898f07e56fcde8e7b57fb1) | **Size:** 0.6B (~1.5GB), 1.7B (~4.2GB) | **Auto-Download:** ✅
|
||||
|
||||
**Model Types & Variants:**
|
||||
|
||||
| Model Type | Size | Description | Repository | Auto-Download |
|
||||
|------------|------|-------------|------------|---------------|
|
||||
| **CustomVoice** | 0.6B / 1.7B | 9 preset speakers + instruction control | [0.6B](https://huggingface.co/Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice) / [1.7B](https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice) | ✅ |
|
||||
| **VoiceDesign** | 1.7B only | **Text-to-voice design** (unique) | [1.7B](https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign) | ✅ |
|
||||
| **Base** | 0.6B / 1.7B | Zero-shot voice cloning | [0.6B](https://huggingface.co/Qwen/Qwen3-TTS-12Hz-0.6B-Base) / [1.7B](https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-Base) | ✅ |
|
||||
|
||||
**Model Sizes:**
|
||||
- **0.6B**: ~1.5GB (faster, good quality) - CustomVoice & Base only
|
||||
- **1.7B**: ~4.2GB (slower, better quality) - All 3 model types
|
||||
|
||||
**Installation Structure:**
|
||||
|
||||
```
|
||||
ComfyUI/models/TTS/qwen3_tts/
|
||||
├── Qwen3-TTS-12Hz-0.6B-CustomVoice/ # ~1.5GB
|
||||
├── Qwen3-TTS-12Hz-1.7B-CustomVoice/ # ~4.2GB
|
||||
├── Qwen3-TTS-12Hz-1.7B-VoiceDesign/ # ~4.2GB (1.7B only)
|
||||
├── Qwen3-TTS-12Hz-0.6B-Base/ # ~1.5GB
|
||||
├── Qwen3-TTS-12Hz-1.7B-Base/ # ~4.2GB
|
||||
└── qwen2-audio-encoder/ # ~0.5GB (shared tokenizer)
|
||||
```
|
||||
|
||||
**Note**:
|
||||
- **Intelligent model selection** - automatically loads the right model type based on voice_preset selection
|
||||
- **Shared tokenizer** - qwen2-audio-encoder downloaded once, used by all models
|
||||
- Only downloads the specific model type + size you select
|
||||
- Switching between model types/sizes downloads the new variant
|
||||
- **⚡ torch.compile optimizations available** - Optional 1.7x speedup with PyTorch 2.10+ → [Setup Guide](docs/qwen3_tts_optimizations.md)
|
||||
|
||||
**Usage**:
|
||||
- Select Qwen3-TTS from Unified TTS Engine
|
||||
- Choose model size (0.6B or 1.7B)
|
||||
- Choose voice preset (for CustomVoice) or "None" (for Base cloning)
|
||||
- Use Voice Designer node for VoiceDesign model
|
||||
- Auto-download on first use!
|
||||
|
||||
### 14. Restart ComfyUI
|
||||
|
||||
<div align="right"><a href="#-table-of-contents">Back to top</a></div>
|
||||
|
||||
|
||||
+99
-21
@@ -8,17 +8,28 @@ Unified architecture supporting ChatterBox, F5-TTS, and future engines like RVC:
|
||||
• 🎭 Character Voices (voice reference management)
|
||||
"""
|
||||
|
||||
# Note: PYTORCH_ALLOC_CONF should be set in ComfyUI launch script if needed
|
||||
# Setting it here causes "allocator mismatch" errors because ComfyUI already imported torch
|
||||
|
||||
# Import from the main nodes.py file which handles the new unified architecture
|
||||
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.
|
||||
#
|
||||
# Note: PyTorch inductor patches removed - not needed for PyTorch 2.10+ with triton-windows 3.6+
|
||||
# Qwen3-TTS torch.compile optimizations require:
|
||||
# - PyTorch 2.10.0+ with CUDA 13.0
|
||||
# - triton-windows 3.6.0+ (Windows) or triton 3.6.0+ (Linux)
|
||||
# See docs/qwen3_tts_optimizations.md for installation instructions
|
||||
|
||||
# Enable TensorFloat32 for better performance on Ampere+ GPUs (RTX 30xx+)
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
torch.set_float32_matmul_precision('high')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 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
|
||||
@@ -53,10 +64,15 @@ except Exception as e:
|
||||
|
||||
# Smart Numba Compatibility System - tests and applies fixes only when needed
|
||||
try:
|
||||
from utils.compatibility import setup_numba_compatibility
|
||||
# Load numba_compat directly by file path to avoid package import issues
|
||||
numba_compat_path = os.path.join(os.path.dirname(__file__), "utils", "compatibility", "numba_compat.py")
|
||||
spec = importlib.util.spec_from_file_location("numba_compat_module", numba_compat_path)
|
||||
numba_compat_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(numba_compat_module)
|
||||
|
||||
# Apply smart compatibility setup (fast startup test)
|
||||
compatibility_results = setup_numba_compatibility(quick_startup=True, verbose=True)
|
||||
except ImportError:
|
||||
compatibility_results = numba_compat_module.setup_numba_compatibility(quick_startup=True, verbose=False)
|
||||
except Exception:
|
||||
# Fallback to simple approach if compatibility module not found
|
||||
import sys
|
||||
import os
|
||||
@@ -121,8 +137,13 @@ def print_critical_versions():
|
||||
def check_ffmpeg_availability():
|
||||
"""Check ffmpeg availability and log status"""
|
||||
try:
|
||||
from utils.ffmpeg_utils import FFmpegUtils
|
||||
if FFmpegUtils.is_available():
|
||||
# Load ffmpeg_utils directly by file path to avoid package import issues
|
||||
ffmpeg_utils_path = os.path.join(os.path.dirname(__file__), "utils", "ffmpeg_utils.py")
|
||||
spec = importlib.util.spec_from_file_location("ffmpeg_utils_module", ffmpeg_utils_path)
|
||||
ffmpeg_utils_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ffmpeg_utils_module)
|
||||
|
||||
if ffmpeg_utils_module.FFmpegUtils.is_available():
|
||||
# Only show when unavailable (problem)
|
||||
pass
|
||||
else:
|
||||
@@ -185,6 +206,39 @@ def check_old_extension_conflict():
|
||||
# Perform conflict check
|
||||
OLD_EXTENSION_CONFLICT = check_old_extension_conflict()
|
||||
|
||||
# CRITICAL FIX FOR ISSUE #191: Clear poisoned utils from sys.modules
|
||||
# Some custom nodes (e.g., LG_HotReload) have a utils.py file that gets loaded
|
||||
# into sys.modules['utils'], shadowing our utils/ directory package.
|
||||
# This causes "No module named 'utils.models'; 'utils' is not a package" errors
|
||||
# when our code tries to import from utils submodules.
|
||||
# We must clear it BEFORE loading nodes.py which imports from utils.
|
||||
if 'utils' in sys.modules:
|
||||
utils_module = sys.modules['utils']
|
||||
# Check if it's a poisoned utils (single .py file, not a package directory)
|
||||
# Real packages have __path__ attribute, single files don't
|
||||
if not hasattr(utils_module, '__path__'):
|
||||
# It's a single .py file masquerading as utils - this will break our imports
|
||||
utils_file = getattr(utils_module, '__file__', 'unknown')
|
||||
print(f"\n{'='*80}")
|
||||
print(f"⚠️ UTILS NAMESPACE CONFLICT DETECTED")
|
||||
print(f"{'='*80}")
|
||||
print(f"Another custom node has a 'utils.py' file in sys.modules['utils']:")
|
||||
print(f" Source: {utils_file}")
|
||||
print(f"")
|
||||
print(f"This conflicts with TTS Audio Suite's 'utils/' package directory.")
|
||||
print(f"Removing the conflicting module to allow TTS Audio Suite to load.")
|
||||
print(f"")
|
||||
print(f"If this causes issues with another custom node, that node should:")
|
||||
print(f"• Use relative imports (from .utils import X)")
|
||||
print(f"• Or use a unique name instead of 'utils'")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
# Delete the poisoned utils module and any attempted submodules
|
||||
del sys.modules['utils']
|
||||
to_delete = [key for key in sys.modules.keys() if key.startswith('utils.')]
|
||||
for key in to_delete:
|
||||
del sys.modules[key]
|
||||
|
||||
# Get the path to the nodes.py file
|
||||
nodes_py_path = os.path.join(os.path.dirname(__file__), "nodes.py")
|
||||
|
||||
@@ -226,10 +280,15 @@ def setup_api_routes():
|
||||
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())
|
||||
# Load voice discovery directly by file path to avoid package import issues
|
||||
voice_discovery_path = os.path.join(os.path.dirname(__file__), "utils", "voice", "discovery.py")
|
||||
spec = importlib.util.spec_from_file_location("voice_discovery_module", voice_discovery_path)
|
||||
voice_discovery_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(voice_discovery_module)
|
||||
|
||||
characters = list(voice_discovery_module.get_available_characters())
|
||||
# Also get character aliases
|
||||
aliases = list(voice_discovery._character_aliases.keys()) if hasattr(voice_discovery, '_character_aliases') else []
|
||||
aliases = list(voice_discovery_module.voice_discovery._character_aliases.keys()) if hasattr(voice_discovery_module.voice_discovery, '_character_aliases') else []
|
||||
# Combine and deduplicate
|
||||
all_chars = sorted(set(characters + aliases))
|
||||
return web.json_response({"characters": all_chars})
|
||||
@@ -241,9 +300,14 @@ def setup_api_routes():
|
||||
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
|
||||
# Load language_mapper directly by file path to avoid package import issues
|
||||
language_mapper_path = os.path.join(os.path.dirname(__file__), "utils", "models", "language_mapper.py")
|
||||
spec = importlib.util.spec_from_file_location("language_mapper_module", language_mapper_path)
|
||||
language_mapper_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(language_mapper_module)
|
||||
|
||||
# Get all unique canonical language codes (the values in LANGUAGE_ALIASES)
|
||||
languages = sorted(set(LANGUAGE_ALIASES.values()))
|
||||
languages = sorted(set(language_mapper_module.LANGUAGE_ALIASES.values()))
|
||||
return web.json_response({"languages": languages})
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error retrieving available languages: {e}")
|
||||
@@ -252,20 +316,34 @@ def setup_api_routes():
|
||||
|
||||
@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"""
|
||||
"""API endpoint to receive settings from frontend for inline edit tags and restore VC"""
|
||||
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")
|
||||
vc_engine = data.get("vc_engine", "chatterbox_23lang")
|
||||
cosyvoice_variant = data.get("cosyvoice_variant", "RL")
|
||||
|
||||
print(f"🔧 Received settings: precision={precision}, device={device}")
|
||||
print(f"🔧 Received settings: precision={precision}, device={device}, vc_engine={vc_engine}, cosyvoice_variant={cosyvoice_variant}")
|
||||
|
||||
# Import edit_post_processor using normal import to ensure we get the same module instance
|
||||
# that will be used during workflow execution
|
||||
# CRITICAL: Must use the same module instance, not create a new one via importlib!
|
||||
try:
|
||||
from utils.audio import edit_post_processor as edit_post_processor_module
|
||||
except ImportError:
|
||||
# Fallback: Load directly by file path if normal import fails
|
||||
edit_post_processor_path = os.path.join(os.path.dirname(__file__), "utils", "audio", "edit_post_processor.py")
|
||||
spec = importlib.util.spec_from_file_location("utils.audio.edit_post_processor", edit_post_processor_path)
|
||||
edit_post_processor_module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["utils.audio.edit_post_processor"] = edit_post_processor_module # Register in sys.modules!
|
||||
spec.loader.exec_module(edit_post_processor_module)
|
||||
|
||||
# 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)
|
||||
edit_post_processor_module.set_inline_tag_settings(precision=precision, device=device, vc_engine=vc_engine, cosyvoice_variant=cosyvoice_variant)
|
||||
|
||||
return web.json_response({"status": "success", "precision": precision, "device": device})
|
||||
return web.json_response({"status": "success", "precision": precision, "device": device, "vc_engine": vc_engine, "cosyvoice_variant": cosyvoice_variant})
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error setting inline tag settings: {e}")
|
||||
return web.json_response({"status": "error", "error": str(e)})
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
# CosyVoice3 Paralinguistic Tags Guide
|
||||
|
||||
## Overview
|
||||
|
||||
CosyVoice3 has **native paralinguistic tag support** built into the model. Unlike Step Audio EditX post-processing, these tags are processed **during TTS generation** for natural integration.
|
||||
|
||||
**⚠️ IMPORTANT**: Step Audio EditX post-processing is **DISABLED** for CosyVoice3. EditX syntax like `<Laughter:2>`, `<emotion:happy>`, `<style:whisper>` will be stripped but **not processed**. Use CosyVoice3 native tags instead.
|
||||
|
||||
---
|
||||
|
||||
## Supported Tags
|
||||
|
||||
### Single Tags (Sound Insertion)
|
||||
|
||||
| Tag | Effect | Example |
|
||||
|-----|--------|---------|
|
||||
| `<breath>` | Breathing sound | `I'm tired <breath> let's rest` |
|
||||
| `<quick_breath>` | Quick breath | `Running <quick_breath> almost there` |
|
||||
| `<laughter>` | Laughter | `That's hilarious <laughter>!` |
|
||||
| `<cough>` | Cough | `Excuse me <cough> sorry` |
|
||||
| `<sigh>` | Sigh | `Fine <sigh> I'll do it` |
|
||||
| `<gasp>` | Gasp | `Oh no <gasp> what happened?` |
|
||||
| `<noise>` | Background noise | `Walking <noise> through forest` |
|
||||
| `<hissing>` | Hissing sound | `The snake <hissing> away` |
|
||||
| `<vocalized-noise>` | Vocalized noise | `Hmm <vocalized-noise> interesting` |
|
||||
| `<lipsmack>` | Lip smack | `Delicious <lipsmack> food` |
|
||||
| `<mn>` | "mn" sound | `I think <mn> maybe` |
|
||||
| `<clucking>` | Clucking sound | `Disapproving <clucking>` |
|
||||
| `<accent>` | Accent emphasis | `Very <accent> important` |
|
||||
|
||||
**Position matters**: Tags insert sounds where you place them.
|
||||
|
||||
### Wrapper Tags (Text Emphasis)
|
||||
|
||||
| Tag | Effect | Example |
|
||||
|-----|--------|---------|
|
||||
| `<laughing>text</laughing>` | Apply laughter to text | `<laughing>so funny</laughing>!` |
|
||||
| `<strong>text</strong>` | Emphasize text | `<strong>very important</strong>` |
|
||||
|
||||
**Note**: Use `<laughing>` for wrapper tags (auto-converted to `<laughter>` internally).
|
||||
|
||||
### Language Switching
|
||||
|
||||
CosyVoice3 supports 4 native languages (EN, ZH, JA, KO) and 5 cross-lingual languages (DE, ES, FR, IT, RU).
|
||||
|
||||
**TTS Audio Suite bracket syntax** (auto-converted):
|
||||
```
|
||||
[en:Alice] Hello world
|
||||
[zh:Bob] 你好世界
|
||||
[ja:Alice] こんにちは
|
||||
[ko:Bob] 안녕하세요
|
||||
|
||||
Or without character name:
|
||||
[en:] Hello world
|
||||
[zh:] 你好世界
|
||||
```
|
||||
|
||||
**CosyVoice3 native syntax** (manual):
|
||||
```
|
||||
<|en|>Hello world
|
||||
<|zh|>你好世界
|
||||
<|ja|>こんにちは
|
||||
<|ko|>안녕하세요
|
||||
```
|
||||
|
||||
Both work. Bracket syntax is recommended for consistency with other engines.
|
||||
|
||||
---
|
||||
|
||||
## Tag Syntax
|
||||
|
||||
### User-Friendly Format (Recommended)
|
||||
|
||||
**You write**: `<breath>`, `<laughter>`, `<cough>`
|
||||
|
||||
**Auto-converted to**: `[breath]`, `[laughter]`, `[cough]`
|
||||
|
||||
This prevents conflicts with character switching `[CharacterName]` brackets.
|
||||
|
||||
### Direct Format (Advanced)
|
||||
|
||||
You can also use native CosyVoice3 format: `[breath]`, `[laughter]`, `[cough]`
|
||||
|
||||
Both formats work identically.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic
|
||||
|
||||
```
|
||||
[Alice] Hello there <breath> nice to meet you!
|
||||
```
|
||||
|
||||
### Multiple Effects
|
||||
|
||||
```
|
||||
[Bob] I can't believe it <gasp> that's <laughter> amazing!
|
||||
```
|
||||
|
||||
### Wrapper Tags
|
||||
|
||||
```
|
||||
[Alice] This is <laughing>hilarious</laughing>!
|
||||
[Narrator] The news was <strong>absolutely shocking</strong>.
|
||||
```
|
||||
|
||||
### Natural Conversation
|
||||
|
||||
```
|
||||
[Alice] Did you hear the news? <breath>
|
||||
[Bob] Yes <gasp> I couldn't believe it!
|
||||
[Alice] I know right <laughter> it's crazy!
|
||||
```
|
||||
|
||||
### SRT Subtitles
|
||||
|
||||
```
|
||||
1
|
||||
00:00:00,000 --> 00:00:03,000
|
||||
[Alice] Welcome to the show <breath> everyone!
|
||||
|
||||
2
|
||||
00:00:03,500 --> 00:00:06,000
|
||||
[Bob] Thanks for having me <laughter>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Position Tags Naturally
|
||||
|
||||
```
|
||||
✅ GOOD: "I'm so tired <sigh> let's take a break"
|
||||
❌ AWKWARD: "<sigh> <breath> I'm tired"
|
||||
```
|
||||
|
||||
### 2. Don't Overuse
|
||||
|
||||
```
|
||||
✅ GOOD: "Hello <breath> nice to meet you"
|
||||
❌ BAD: "Hello <breath> there <laughter> nice <sigh> to you"
|
||||
```
|
||||
|
||||
**Recommended**: 1-2 tags per sentence, 3-4 maximum per segment.
|
||||
|
||||
### 3. Match Context
|
||||
|
||||
```
|
||||
# Tired
|
||||
[Alice] I can't go on <sigh> I need rest <breath>
|
||||
|
||||
# Happy
|
||||
[Bob] That was hilarious <laughter> tell me more!
|
||||
|
||||
# Surprised
|
||||
[Alice] Wait what? <gasp> Are you serious?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CosyVoice3 Tags vs Step Audio EditX
|
||||
|
||||
| Feature | CosyVoice3 Native | Step Audio EditX |
|
||||
|---------|------------------|-----------------|
|
||||
| **Syntax** | `<breath>`, `<laughter>` | `<Laughter:2>`, `<emotion:happy>` |
|
||||
| **Processing** | During TTS | Post-processing (disabled for CosyVoice3) |
|
||||
| **Effects** | 13 paralinguistic sounds | 13 paralinguistic + 14 emotions + 32 styles + speed + restore |
|
||||
| **Iterations** | Single pass | 1-30 customizable |
|
||||
| **Engine Support** | CosyVoice3 only | All other engines |
|
||||
|
||||
**When to use CosyVoice3 tags**: Generating with CosyVoice3 engine, need paralinguistic effects
|
||||
|
||||
**When to use EditX tags**: Need emotion/style/speed control, OR using other engines (ChatterBox, F5-TTS, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
### ⚠️ No Step Audio EditX Support
|
||||
|
||||
```
|
||||
❌ WON'T WORK: <Laughter:2> (EditX - will be stripped)
|
||||
❌ WON'T WORK: <emotion:happy> (EditX - will be stripped)
|
||||
❌ WON'T WORK: <style:whisper> (EditX - will be stripped)
|
||||
|
||||
✅ USE INSTEAD: <laughter> (CosyVoice native)
|
||||
✅ USE INSTEAD: <strong>text</strong>
|
||||
```
|
||||
|
||||
For emotion/style control, use ChatterBox, IndexTTS-2, or other engines with EditX support.
|
||||
|
||||
### Tag Case
|
||||
|
||||
Tags are **case-insensitive**: `<breath>` = `<BREATH>` = `<Breath>`
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Can I use Step Audio EditX tags with CosyVoice3?**
|
||||
A: No. EditX syntax (`<Laughter:2>`, `<emotion:happy>`) will be stripped but not processed.
|
||||
|
||||
**Q: Can I control effect strength like EditX iterations?**
|
||||
A: No. CosyVoice3 tags are single-pass. Effect strength is determined by the model.
|
||||
|
||||
**Q: Which languages support tags best?**
|
||||
A: All supported languages (EN, ZH, JA, KO, DE, ES, FR, IT, RU) work with tags.
|
||||
|
||||
**Q: Can I use tags in SRT mode?**
|
||||
A: Yes! Include tags in subtitle text.
|
||||
|
||||
---
|
||||
|
||||
## Related Guides
|
||||
|
||||
- **[Inline Edit Tags Guide](INLINE_EDIT_TAGS_USER_GUIDE.md)** - Step Audio EditX tags for other engines
|
||||
- **[Parameter Switching Guide](PARAMETER_SWITCHING_GUIDE.md)** - Per-segment parameter control
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-29
|
||||
**Compatible with**: TTS Audio Suite v4.16.0+
|
||||
@@ -210,4 +210,94 @@ def generate_speech(self, TTS_engine, ...):
|
||||
|
||||
The refactoring successfully transformed a collection of engine-specific nodes into a unified, extensible architecture while preserving all existing functionality. The smart delegation pattern ensures zero feature loss while providing a clean foundation for future engine integrations.
|
||||
|
||||
**Next Steps**: Test thoroughly, add RVC engine support, and potentially submit to ComfyUI Manager as the new universal TTS solution.
|
||||
**Next Steps**: Test thoroughly, add RVC engine support, and potentially submit to ComfyUI Manager as the new universal TTS solution.
|
||||
|
||||
---
|
||||
|
||||
## Future Refactoring Tasks
|
||||
|
||||
### Architecture Inconsistency: Processor Location
|
||||
|
||||
**Issue Identified**: 2025-01-28
|
||||
|
||||
CosyVoice and IndexTTS have TTS processors in a different location than all other engines, causing inconsistent import patterns and architecture confusion.
|
||||
|
||||
**Current State:**
|
||||
|
||||
**Standard Pattern (9 engines):**
|
||||
- TTS processor location: `nodes/engine_name/engine_processor.py`
|
||||
- Import pattern: File-based import using `importlib.util.spec_from_file_location()`
|
||||
- Examples:
|
||||
- `nodes/qwen3_tts/qwen3_tts_processor.py`
|
||||
- `nodes/step_audio_editx/step_audio_editx_processor.py`
|
||||
- `nodes/vibevoice/vibevoice_processor.py`
|
||||
- `nodes/higgs_audio/higgs_audio_tts_processor.py`
|
||||
- `nodes/chatterbox_official_23lang/chatterbox_official_23lang_processor.py`
|
||||
|
||||
**Inconsistent Pattern (2 engines):**
|
||||
- TTS processor location: `engines/processors/`
|
||||
- Import pattern: Package import (`from engines.processors import`)
|
||||
- Examples:
|
||||
- `engines/processors/cosyvoice_processor.py`
|
||||
- `engines/processors/index_tts_processor.py`
|
||||
|
||||
**Why This Is a Problem:**
|
||||
|
||||
1. **Confusing Architecture**: Why are some TTS processors in `engines/processors/` and some in `nodes/engine_name/`?
|
||||
2. **Inconsistent Import Patterns**: Some use package imports, some use file-based imports
|
||||
3. **Documentation Mismatch**: The implementation guide assumes processors are in `nodes/`
|
||||
4. **Harder to Maintain**: New developers don't know which pattern to follow
|
||||
5. **Breaks Conventions**: All other engines follow the standard pattern
|
||||
|
||||
**Proposed Fix:**
|
||||
|
||||
Move TTS processors to match the standard architecture:
|
||||
|
||||
```bash
|
||||
# Move CosyVoice processor
|
||||
engines/processors/cosyvoice_processor.py → nodes/cosyvoice/cosyvoice_processor.py
|
||||
|
||||
# Move IndexTTS processor
|
||||
engines/processors/index_tts_processor.py → nodes/index_tts/index_tts_processor.py
|
||||
```
|
||||
|
||||
**Files to Update:**
|
||||
|
||||
1. **Move processor files** to `nodes/engine_name/`
|
||||
|
||||
2. **Update imports in SRT processors:**
|
||||
- `nodes/cosyvoice/cosyvoice_srt_processor.py` line 34
|
||||
- `nodes/index_tts/index_tts_srt_processor.py` (if exists)
|
||||
|
||||
Change from:
|
||||
```python
|
||||
from engines.processors.cosyvoice_processor import CosyVoiceProcessor
|
||||
```
|
||||
|
||||
To file-based import:
|
||||
```python
|
||||
import importlib.util
|
||||
processor_path = os.path.join(current_dir, "cosyvoice_processor.py")
|
||||
processor_spec = importlib.util.spec_from_file_location("cosyvoice_processor_module", processor_path)
|
||||
processor_module = importlib.util.module_from_spec(processor_spec)
|
||||
processor_spec.loader.exec_module(processor_module)
|
||||
CosyVoiceProcessor = processor_module.CosyVoiceProcessor
|
||||
```
|
||||
|
||||
3. **Update any other imports** that reference `engines.processors.cosyvoice_processor` or `engines.processors.index_tts_processor`
|
||||
|
||||
4. **Update PROJECT_INDEX.md** to reflect new locations
|
||||
|
||||
5. **Test thoroughly** to ensure no import breakage
|
||||
|
||||
**Priority**: Medium - Not breaking functionality, but important for maintainability and consistency
|
||||
|
||||
**Estimated Effort**: 1-2 hours (move files, update imports, test)
|
||||
|
||||
**Benefits:**
|
||||
- Consistent architecture across all engines
|
||||
- Easier to understand and maintain
|
||||
- Matches documentation and implementation guide
|
||||
- Clearer separation of concerns (engines/ = low-level, nodes/ = high-level orchestration)
|
||||
|
||||
**Note**: The `engines/processors/` directory can be removed after this refactoring, or kept for future use if there's a valid architectural reason (currently unclear).
|
||||
@@ -109,3 +109,16 @@
|
||||
- **Cache key components**: `engine_name`, `model_type`, `model_name`, `device`, `quantization`, etc.
|
||||
- **Fix Step Audio EditX**: Updated adapter and Audio Editor node to use unified interface with `model_type="tts"`
|
||||
- **Prevention**: When adding new engines, ALWAYS use unified interface in ALL code paths (adapters, nodes, processors)
|
||||
|
||||
### Engine Wrapper Implementation - Always Compare First
|
||||
- **CRITICAL**: Before writing engine wrapper, open existing reference engine (Step Audio EditX, F5-TTS) side-by-side and compare patterns
|
||||
- **Common mistakes when NOT comparing**:
|
||||
- Missing required imports (`folder_paths`, `resolve_torch_device`, `unified_model_interface`, `extra_paths`)
|
||||
- Manual device/dtype resolution instead of using utils functions
|
||||
- Missing `_find_model_directory()` with `extra_model_paths` support
|
||||
- Using `load_model()` method instead of `_ensure_model_loaded()` pattern
|
||||
- Direct model instantiation instead of `unified_model_interface.load_model()`
|
||||
- Missing `_model_config` tracking (needed for unload)
|
||||
- Not using `tts_model_manager.ensure_device()` before fallback to `.to()`
|
||||
- Missing GPU capability detection for dtype auto-selection
|
||||
- **Prevention**: ALWAYS open Step Audio EditX engine file as reference when creating new engine wrapper, copy the patterns
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
# Pytest Testing Framework Implementation Report
|
||||
|
||||
**Date:** 2024-12-18
|
||||
**Author:** AI Assistant
|
||||
**Status:** ✅ Working (60/64 tests passing)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Implementing pytest for a ComfyUI custom node is **non-trivial** due to the way Python imports work with packages that have complex `__init__.py` files. This document summarizes the approaches tried, failures encountered, and the final working solution.
|
||||
|
||||
---
|
||||
|
||||
## The Core Problem
|
||||
|
||||
When pytest discovers and imports test files, Python's import system also imports the parent package's `__init__.py`. For TTS-Audio-Suite, this `__init__.py`:
|
||||
|
||||
1. Applies compatibility patches (numba, PyTorch, transformers)
|
||||
2. Imports `nodes.py` which imports `folder_paths` (ComfyUI-only module)
|
||||
3. Loads all node modules dynamically
|
||||
4. Registers API endpoints with ComfyUI server
|
||||
|
||||
**Result:** Running `pytest tests/unit/` triggers the entire ComfyUI node loading chain, which fails because ComfyUI modules (`folder_paths`, `comfy`, `server`) don't exist in the test context.
|
||||
|
||||
---
|
||||
|
||||
## Approaches Tried (and Why They Failed)
|
||||
|
||||
### ❌ Approach 1: Environment Variable Guard in `__init__.py`
|
||||
|
||||
**Idea:** Check `os.environ['COMFYUI_TESTING']` at the top of `__init__.py` and skip loading.
|
||||
|
||||
```python
|
||||
# __init__.py
|
||||
if os.environ.get('COMFYUI_TESTING') == '1':
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
# ... skip rest of file
|
||||
```
|
||||
|
||||
**Why it failed:** Would require wrapping 300+ lines of code in an `if/else` block, causing massive indentation changes. Python doesn't have a "skip rest of file" statement.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Approach 2: `collect_ignore` in conftest.py
|
||||
|
||||
**Idea:** Tell pytest to ignore `__init__.py` during collection.
|
||||
|
||||
```python
|
||||
# conftest.py
|
||||
collect_ignore = ["__init__.py", "nodes.py"]
|
||||
```
|
||||
|
||||
**Why it failed:** `collect_ignore` prevents pytest from *collecting tests* from those files, but doesn't prevent Python from *importing* them when test modules import utilities.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Approach 3: Moving conftest.py to tests/ directory
|
||||
|
||||
**Idea:** If conftest.py is in `tests/`, pytest won't treat the parent as a package.
|
||||
|
||||
**Why it failed:** Pytest still finds `pytest.ini` in the parent and treats the directory structure as packages. The import chain is triggered when test files do `from utils.timing.parser import ...`.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Approach 4: Using `--import-mode=importlib`
|
||||
|
||||
**Idea:** Pytest's alternative import mode might avoid `__init__.py` loading.
|
||||
|
||||
```bash
|
||||
pytest --import-mode=importlib tests/
|
||||
```
|
||||
|
||||
**Why it failed:** Same underlying issue - Python still imports parent packages.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Approach 5: Using importlib.util in test files
|
||||
|
||||
**Idea:** Bypass package imports entirely by loading modules directly.
|
||||
|
||||
```python
|
||||
# test_srt_parser.py
|
||||
import importlib.util
|
||||
parser_path = custom_node_root / "utils" / "timing" / "parser.py"
|
||||
spec = importlib.util.spec_from_file_location("parser_module", parser_path)
|
||||
parser_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(parser_module)
|
||||
```
|
||||
|
||||
**Why it failed:** Conftest.py is still loaded first by pytest, and conftest.py's mere existence in the package triggers `__init__.py` import.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Approach 6: Mock ComfyUI Modules at conftest.py Module Level (WORKING)
|
||||
|
||||
**The Solution:** Mock all ComfyUI modules *before* pytest imports anything else.
|
||||
|
||||
```python
|
||||
# tests/conftest.py - MUST be at very top
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
os.environ['COMFYUI_TESTING'] = '1'
|
||||
os.environ['PYTEST_CURRENT_TEST'] = 'true'
|
||||
|
||||
# Mock ComfyUI modules BEFORE any other imports
|
||||
_MOCK_MODULES = [
|
||||
'comfy', 'comfy.model_management', 'comfy.utils',
|
||||
'nodes', 'folder_paths', 'server', 'execution', 'comfy_extras',
|
||||
]
|
||||
|
||||
for _module_name in _MOCK_MODULES:
|
||||
if _module_name not in sys.modules:
|
||||
sys.modules[_module_name] = MagicMock()
|
||||
|
||||
# NOW safe to import pytest, etc.
|
||||
import pytest
|
||||
```
|
||||
|
||||
**Why it works:** When `__init__.py` executes `import folder_paths`, Python finds a MagicMock already in `sys.modules` and uses that instead of trying to find the real module.
|
||||
|
||||
---
|
||||
|
||||
## Additional Bug Found
|
||||
|
||||
During testing, discovered a **real bug** in `nodes.py`:
|
||||
|
||||
```python
|
||||
# BEFORE (buggy)
|
||||
try:
|
||||
from utils.compatibility import setup_numba_compatibility
|
||||
setup_numba_compatibility(...)
|
||||
except ImportError:
|
||||
import sys # ← Only imported in except block!
|
||||
import os # ← Only imported in except block!
|
||||
|
||||
# Later in file...
|
||||
current_dir = os.path.dirname(__file__) # ← NameError if try succeeded!
|
||||
```
|
||||
|
||||
**Fix:** Move `import sys` and `import os` to the top of the file, outside the try/except block.
|
||||
|
||||
---
|
||||
|
||||
## Guidance for Future Developers
|
||||
|
||||
### 1. Test File Structure
|
||||
|
||||
```
|
||||
TTS-Audio-Suite/
|
||||
├── __init__.py # Complex, avoid importing during tests
|
||||
├── nodes.py # Node registration
|
||||
├── pytest.ini # Test configuration
|
||||
├── tests/
|
||||
│ ├── conftest.py # CRITICAL: Mock ComfyUI here
|
||||
│ ├── unit/
|
||||
│ │ ├── test_srt_parser.py
|
||||
│ │ └── test_audio_processing.py
|
||||
│ └── integration/
|
||||
│ └── test_workflows.py
|
||||
```
|
||||
|
||||
### 2. conftest.py Template
|
||||
|
||||
```python
|
||||
"""Mock ComfyUI at module level BEFORE any imports"""
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Environment flags
|
||||
os.environ['COMFYUI_TESTING'] = '1'
|
||||
|
||||
# Mock ComfyUI modules
|
||||
for mod in ['folder_paths', 'comfy', 'server', 'execution']:
|
||||
if mod not in sys.modules:
|
||||
sys.modules[mod] = MagicMock()
|
||||
|
||||
# Now safe for normal imports
|
||||
import pytest
|
||||
# ... rest of fixtures
|
||||
```
|
||||
|
||||
### 3. Unit Test Pattern
|
||||
|
||||
```python
|
||||
# test_my_utility.py
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
custom_node_root = Path(__file__).parent.parent.parent
|
||||
module_path = custom_node_root / "utils" / "my_utility.py"
|
||||
|
||||
spec = importlib.util.spec_from_file_location("my_utility", module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Now use module.MyClass, module.my_function, etc.
|
||||
```
|
||||
|
||||
### 4. Running Tests
|
||||
|
||||
```bash
|
||||
# Unit tests (fast, no server)
|
||||
python -m pytest tests/unit/ -m unit -v
|
||||
|
||||
# Integration tests (requires ComfyUI running)
|
||||
python -m pytest tests/integration/ -m integration -v
|
||||
```
|
||||
|
||||
### 5. Key Gotchas
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| `ModuleNotFoundError: folder_paths` | Add to mock list in conftest.py |
|
||||
| `NameError: os not defined` | Check all imports are at module top level |
|
||||
| Tests pass locally but fail in CI | Ensure conftest.py loads first |
|
||||
| `__init__.py` still runs | Mock must happen at conftest.py *module level*, not in a function |
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The working solution requires:
|
||||
1. **Mocking at conftest.py module level** (not in `pytest_configure` hook - too late)
|
||||
2. **Ensuring `os`/`sys` imports are at top level** in production code
|
||||
3. **Using importlib for utility imports** in test files (optional but cleaner)
|
||||
|
||||
This approach allows unit tests to run without ComfyUI while integration tests can still use the real server.
|
||||
@@ -0,0 +1,585 @@
|
||||
# TTS Audio Suite Engines - Complete Reference Data
|
||||
# This is the single source of truth for all engine documentation tables
|
||||
|
||||
engines:
|
||||
- id: f5-tts
|
||||
name: F5-TTS
|
||||
models: "Base, v1, E2TTS + 8 lang models"
|
||||
size: "~1.2GB each"
|
||||
|
||||
capabilities:
|
||||
tts: true
|
||||
srt: true
|
||||
vc: false
|
||||
|
||||
special_features:
|
||||
- "Targeted Word/Speech Editing"
|
||||
- "Speed control"
|
||||
|
||||
languages:
|
||||
en: { supported: true, flag: "🇺🇸", notes: "" }
|
||||
zh: { supported: false, flag: "🇨🇳", notes: "" }
|
||||
de: { supported: true, flag: "🇩🇪", notes: "" }
|
||||
es: { supported: true, flag: "🇪🇸", notes: "" }
|
||||
fr: { supported: true, flag: "🇫🇷", notes: "" }
|
||||
it: { supported: true, flag: "🇮🇹", notes: "" }
|
||||
ja: { supported: true, flag: "🇯🇵", notes: "" }
|
||||
ko: { supported: false, flag: "🇰🇷", notes: "" }
|
||||
ru: { supported: false, flag: "🇷🇺", notes: "" }
|
||||
pt: { supported: true, flag: "🇧🇷", notes: "(BR)" }
|
||||
pl: { supported: true, flag: "🇵🇱", notes: "" }
|
||||
hi: { supported: true, flag: "🇮🇳", notes: "" }
|
||||
ar: { supported: false, flag: "🇦🇪", notes: "" }
|
||||
tr: { supported: false, flag: "🇹🇷", notes: "" }
|
||||
th: { supported: true, flag: "🇹🇭", notes: "" }
|
||||
no: { supported: false, flag: "🇳🇴", notes: "" }
|
||||
vi: { supported: false, flag: "🇻🇳", notes: "" }
|
||||
hy: { supported: false, flag: "🇦🇲", notes: "" }
|
||||
ka: { supported: false, flag: "🇬🇪", notes: "" }
|
||||
da: { supported: false, flag: "🇩🇰", notes: "" }
|
||||
fi: { supported: false, flag: "🇫🇮", notes: "" }
|
||||
el: { supported: false, flag: "🇬🇷", notes: "" }
|
||||
he: { supported: false, flag: "🇮🇱", notes: "" }
|
||||
ms: { supported: false, flag: "🇲🇾", notes: "" }
|
||||
nl: { supported: false, flag: "🇳🇱", notes: "" }
|
||||
sv: { supported: false, flag: "🇸🇪", notes: "" }
|
||||
sw: { supported: false, flag: "🇰🇪", notes: "" }
|
||||
|
||||
features:
|
||||
voice_cloning: { supported: true, notes: "" }
|
||||
native_multi_speaker: { supported: false, notes: "" }
|
||||
voice_conversion: { supported: false, notes: "" }
|
||||
emotion_control: { supported: false, notes: "" }
|
||||
native_long_form: { supported: false, notes: "" }
|
||||
community_finetunes: { supported: true, notes: "" }
|
||||
vram_efficient: { supported: true, notes: "" }
|
||||
speed_performance: { supported: true, notes: "Very Fast" }
|
||||
|
||||
- id: chatterbox
|
||||
name: ChatterBox
|
||||
models: "EN, DE×3, IT, FR, RU, HY, KA, JA, KO, NO"
|
||||
size: "~4.3GB"
|
||||
|
||||
capabilities:
|
||||
tts: true
|
||||
srt: true
|
||||
vc: true
|
||||
|
||||
special_features:
|
||||
- "Expressiveness slider"
|
||||
|
||||
languages:
|
||||
en: { supported: true, flag: "🇺🇸", notes: "" }
|
||||
zh: { supported: false, flag: "🇨🇳", notes: "" }
|
||||
de: { supported: true, flag: "🇩🇪", notes: "(×3)" }
|
||||
es: { supported: false, flag: "🇪🇸", notes: "" }
|
||||
fr: { supported: true, flag: "🇫🇷", notes: "" }
|
||||
it: { supported: true, flag: "🇮🇹", notes: "" }
|
||||
ja: { supported: true, flag: "🇯🇵", notes: "" }
|
||||
ko: { supported: true, flag: "🇰🇷", notes: "" }
|
||||
ru: { supported: true, flag: "🇷🇺", notes: "" }
|
||||
pt: { supported: false, flag: "🇧🇷", notes: "" }
|
||||
pl: { supported: false, flag: "🇵🇱", notes: "" }
|
||||
hi: { supported: false, flag: "🇮🇳", notes: "" }
|
||||
ar: { supported: false, flag: "🇦🇪", notes: "" }
|
||||
tr: { supported: false, flag: "🇹🇷", notes: "" }
|
||||
th: { supported: false, flag: "🇹🇭", notes: "" }
|
||||
no: { supported: true, flag: "🇳🇴", notes: "" }
|
||||
vi: { supported: false, flag: "🇻🇳", notes: "" }
|
||||
hy: { supported: true, flag: "🇦🇲", notes: "" }
|
||||
ka: { supported: true, flag: "🇬🇪", notes: "" }
|
||||
da: { supported: false, flag: "🇩🇰", notes: "" }
|
||||
fi: { supported: false, flag: "🇫🇮", notes: "" }
|
||||
el: { supported: false, flag: "🇬🇷", notes: "" }
|
||||
he: { supported: false, flag: "🇮🇱", notes: "" }
|
||||
ms: { supported: false, flag: "🇲🇾", notes: "" }
|
||||
nl: { supported: false, flag: "🇳🇱", notes: "" }
|
||||
sv: { supported: false, flag: "🇸🇪", notes: "" }
|
||||
sw: { supported: false, flag: "🇰🇪", notes: "" }
|
||||
|
||||
features:
|
||||
voice_cloning: { supported: true, notes: "" }
|
||||
native_multi_speaker: { supported: false, notes: "" }
|
||||
voice_conversion: { supported: true, notes: "" }
|
||||
emotion_control: { supported: false, notes: "" }
|
||||
native_long_form: { supported: false, notes: "" }
|
||||
community_finetunes: { supported: true, notes: "" }
|
||||
vram_efficient: { supported: true, notes: "" }
|
||||
speed_performance: { supported: true, notes: "Fast" }
|
||||
|
||||
- id: chatterbox-23l
|
||||
name: ChatterBox 23L
|
||||
models: "v1, v2, Vietnamese (Viterbox)"
|
||||
size: "~4.3GB"
|
||||
|
||||
capabilities:
|
||||
tts: true
|
||||
srt: true
|
||||
vc: true
|
||||
|
||||
special_features:
|
||||
- "24 languages in single model"
|
||||
- "emotion tokens (v2 - doesn't work)"
|
||||
|
||||
languages:
|
||||
en: { supported: true, flag: "🇺🇸", notes: "" }
|
||||
zh: { supported: true, flag: "🇨🇳", notes: "" }
|
||||
de: { supported: true, flag: "🇩🇪", notes: "" }
|
||||
es: { supported: true, flag: "🇪🇸", notes: "" }
|
||||
fr: { supported: true, flag: "🇫🇷", notes: "" }
|
||||
it: { supported: true, flag: "🇮🇹", notes: "" }
|
||||
ja: { supported: true, flag: "🇯🇵", notes: "" }
|
||||
ko: { supported: true, flag: "🇰🇷", notes: "" }
|
||||
ru: { supported: true, flag: "🇷🇺", notes: "" }
|
||||
pt: { supported: true, flag: "🇵🇹", notes: "" }
|
||||
pl: { supported: true, flag: "🇵🇱", notes: "" }
|
||||
hi: { supported: true, flag: "🇮🇳", notes: "" }
|
||||
ar: { supported: true, flag: "🇦🇪", notes: "" }
|
||||
tr: { supported: true, flag: "🇹🇷", notes: "" }
|
||||
th: { supported: true, flag: "🇹🇭", notes: "" }
|
||||
no: { supported: true, flag: "🇳🇴", notes: "" }
|
||||
vi: { supported: true, flag: "🇻🇳", notes: "(Viterbox)" }
|
||||
hy: { supported: false, flag: "🇦🇲", notes: "" }
|
||||
ka: { supported: false, flag: "🇬🇪", notes: "" }
|
||||
da: { supported: true, flag: "🇩🇰", notes: "" }
|
||||
fi: { supported: true, flag: "🇫🇮", notes: "" }
|
||||
el: { supported: true, flag: "🇬🇷", notes: "" }
|
||||
he: { supported: true, flag: "🇮🇱", notes: "" }
|
||||
ms: { supported: true, flag: "🇲🇾", notes: "" }
|
||||
nl: { supported: true, flag: "🇳🇱", notes: "" }
|
||||
sv: { supported: true, flag: "🇸🇪", notes: "" }
|
||||
sw: { supported: true, flag: "🇰🇪", notes: "" }
|
||||
|
||||
features:
|
||||
voice_cloning: { supported: true, notes: "" }
|
||||
native_multi_speaker: { supported: false, notes: "" }
|
||||
voice_conversion: { supported: true, notes: "" }
|
||||
emotion_control: { supported: "partial", notes: "(v2 tags - doesn't work)" }
|
||||
native_long_form: { supported: false, notes: "" }
|
||||
community_finetunes: { supported: true, notes: "" }
|
||||
vram_efficient: { supported: true, notes: "" }
|
||||
speed_performance: { supported: true, notes: "Fast" }
|
||||
|
||||
- id: vibevoice
|
||||
name: VibeVoice
|
||||
models: "1.5B, 7B, Hindi-1.5B, Hindi-7B"
|
||||
size: "5.4GB / 18GB"
|
||||
|
||||
capabilities:
|
||||
tts: true
|
||||
srt: true
|
||||
vc: false
|
||||
|
||||
special_features:
|
||||
- "90-min long-form"
|
||||
- "Native 4-speaker"
|
||||
- "4-bit quantization"
|
||||
|
||||
languages:
|
||||
en: { supported: true, flag: "🇺🇸", notes: "" }
|
||||
zh: { supported: true, flag: "🇨🇳", notes: "" }
|
||||
de: { supported: false, flag: "🇩🇪", notes: "" }
|
||||
es: { supported: false, flag: "🇪🇸", notes: "" }
|
||||
fr: { supported: false, flag: "🇫🇷", notes: "" }
|
||||
it: { supported: false, flag: "🇮🇹", notes: "" }
|
||||
ja: { supported: false, flag: "🇯🇵", notes: "" }
|
||||
ko: { supported: false, flag: "🇰🇷", notes: "" }
|
||||
ru: { supported: false, flag: "🇷🇺", notes: "" }
|
||||
pt: { supported: false, flag: "🇧🇷", notes: "" }
|
||||
pl: { supported: false, flag: "🇵🇱", notes: "" }
|
||||
hi: { supported: true, flag: "🇮🇳", notes: "" }
|
||||
ar: { supported: false, flag: "🇦🇪", notes: "" }
|
||||
tr: { supported: false, flag: "🇹🇷", notes: "" }
|
||||
th: { supported: false, flag: "🇹🇭", notes: "" }
|
||||
no: { supported: false, flag: "🇳🇴", notes: "" }
|
||||
vi: { supported: false, flag: "🇻🇳", notes: "" }
|
||||
hy: { supported: false, flag: "🇦🇲", notes: "" }
|
||||
ka: { supported: false, flag: "🇬🇪", notes: "" }
|
||||
da: { supported: false, flag: "🇩🇰", notes: "" }
|
||||
fi: { supported: false, flag: "🇫🇮", notes: "" }
|
||||
el: { supported: false, flag: "🇬🇷", notes: "" }
|
||||
he: { supported: false, flag: "🇮🇱", notes: "" }
|
||||
ms: { supported: false, flag: "🇲🇾", notes: "" }
|
||||
nl: { supported: false, flag: "🇳🇱", notes: "" }
|
||||
sv: { supported: false, flag: "🇸🇪", notes: "" }
|
||||
sw: { supported: false, flag: "🇰🇪", notes: "" }
|
||||
|
||||
features:
|
||||
voice_cloning: { supported: true, notes: "" }
|
||||
native_multi_speaker: { supported: true, notes: "" }
|
||||
voice_conversion: { supported: false, notes: "" }
|
||||
emotion_control: { supported: false, notes: "" }
|
||||
native_long_form: { supported: true, notes: "" }
|
||||
community_finetunes: { supported: true, notes: "" }
|
||||
vram_efficient: { supported: "partial", notes: "(5-18GB)" }
|
||||
speed_performance: { supported: "partial", notes: "" }
|
||||
|
||||
- id: higgs-audio-2
|
||||
name: Higgs Audio 2
|
||||
models: "3B"
|
||||
size: "~9GB"
|
||||
|
||||
capabilities:
|
||||
tts: true
|
||||
srt: true
|
||||
vc: false
|
||||
|
||||
special_features:
|
||||
- "3 multi-speaker"
|
||||
- "CUDA graphs (55+ tokens/sec)"
|
||||
|
||||
languages:
|
||||
en: { supported: true, flag: "🇺🇸", notes: "" }
|
||||
zh: { supported: true, flag: "🇨🇳", notes: "(Mandarin)" }
|
||||
de: { supported: true, flag: "🇩🇪", notes: "" }
|
||||
es: { supported: true, flag: "🇪🇸", notes: "" }
|
||||
fr: { supported: false, flag: "🇫🇷", notes: "" }
|
||||
it: { supported: false, flag: "🇮🇹", notes: "" }
|
||||
ja: { supported: false, flag: "🇯🇵", notes: "" }
|
||||
ko: { supported: true, flag: "🇰🇷", notes: "" }
|
||||
ru: { supported: false, flag: "🇷🇺", notes: "" }
|
||||
pt: { supported: false, flag: "🇧🇷", notes: "" }
|
||||
pl: { supported: false, flag: "🇵🇱", notes: "" }
|
||||
hi: { supported: false, flag: "🇮🇳", notes: "" }
|
||||
ar: { supported: false, flag: "🇦🇪", notes: "" }
|
||||
tr: { supported: false, flag: "🇹🇷", notes: "" }
|
||||
th: { supported: false, flag: "🇹🇭", notes: "" }
|
||||
no: { supported: false, flag: "🇳🇴", notes: "" }
|
||||
vi: { supported: false, flag: "🇻🇳", notes: "" }
|
||||
hy: { supported: false, flag: "🇦🇲", notes: "" }
|
||||
ka: { supported: false, flag: "🇬🇪", notes: "" }
|
||||
da: { supported: false, flag: "🇩🇰", notes: "" }
|
||||
fi: { supported: false, flag: "🇫🇮", notes: "" }
|
||||
el: { supported: false, flag: "🇬🇷", notes: "" }
|
||||
he: { supported: false, flag: "🇮🇱", notes: "" }
|
||||
ms: { supported: false, flag: "🇲🇾", notes: "" }
|
||||
nl: { supported: false, flag: "🇳🇱", notes: "" }
|
||||
sv: { supported: false, flag: "🇸🇪", notes: "" }
|
||||
sw: { supported: false, flag: "🇰🇪", notes: "" }
|
||||
|
||||
features:
|
||||
voice_cloning: { supported: true, notes: "" }
|
||||
native_multi_speaker: { supported: true, notes: "" }
|
||||
voice_conversion: { supported: false, notes: "" }
|
||||
emotion_control: { supported: "partial", notes: "(via prompt)" }
|
||||
native_long_form: { supported: false, notes: "" }
|
||||
community_finetunes: { supported: false, notes: "" }
|
||||
vram_efficient: { supported: "partial", notes: "(9GB)" }
|
||||
speed_performance: { supported: "partial", notes: "" }
|
||||
|
||||
- id: indextts-2
|
||||
name: IndexTTS-2
|
||||
models: "IndexTTS-2"
|
||||
size: "?"
|
||||
|
||||
capabilities:
|
||||
tts: true
|
||||
srt: true
|
||||
vc: false
|
||||
|
||||
special_features:
|
||||
- "Emotion Control: 8 vectors"
|
||||
- "Text as reference"
|
||||
- "Audio as reference"
|
||||
|
||||
languages:
|
||||
en: { supported: true, flag: "🇺🇸", notes: "" }
|
||||
zh: { supported: true, flag: "🇨🇳", notes: "" }
|
||||
de: { supported: false, flag: "🇩🇪", notes: "" }
|
||||
es: { supported: false, flag: "🇪🇸", notes: "" }
|
||||
fr: { supported: false, flag: "🇫🇷", notes: "" }
|
||||
it: { supported: false, flag: "🇮🇹", notes: "" }
|
||||
ja: { supported: true, flag: "🇯🇵", notes: "?" }
|
||||
ko: { supported: false, flag: "🇰🇷", notes: "" }
|
||||
ru: { supported: false, flag: "🇷🇺", notes: "" }
|
||||
pt: { supported: false, flag: "🇧🇷", notes: "" }
|
||||
pl: { supported: false, flag: "🇵🇱", notes: "" }
|
||||
hi: { supported: false, flag: "🇮🇳", notes: "" }
|
||||
ar: { supported: false, flag: "🇦🇪", notes: "" }
|
||||
tr: { supported: false, flag: "🇹🇷", notes: "" }
|
||||
th: { supported: false, flag: "🇹🇭", notes: "" }
|
||||
no: { supported: false, flag: "🇳🇴", notes: "" }
|
||||
vi: { supported: false, flag: "🇻🇳", notes: "" }
|
||||
hy: { supported: false, flag: "🇦🇲", notes: "" }
|
||||
ka: { supported: false, flag: "🇬🇪", notes: "" }
|
||||
da: { supported: false, flag: "🇩🇰", notes: "" }
|
||||
fi: { supported: false, flag: "🇫🇮", notes: "" }
|
||||
el: { supported: false, flag: "🇬🇷", notes: "" }
|
||||
he: { supported: false, flag: "🇮🇱", notes: "" }
|
||||
ms: { supported: false, flag: "🇲🇾", notes: "" }
|
||||
nl: { supported: false, flag: "🇳🇱", notes: "" }
|
||||
sv: { supported: false, flag: "🇸🇪", notes: "" }
|
||||
sw: { supported: false, flag: "🇰🇪", notes: "" }
|
||||
|
||||
features:
|
||||
voice_cloning: { supported: true, notes: "" }
|
||||
native_multi_speaker: { supported: false, notes: "" }
|
||||
voice_conversion: { supported: false, notes: "" }
|
||||
emotion_control: { supported: true, notes: "(8 emotions)" }
|
||||
native_long_form: { supported: false, notes: "" }
|
||||
community_finetunes: { supported: false, notes: "" }
|
||||
vram_efficient: { supported: "partial", notes: "(9-12GB)" }
|
||||
speed_performance: { supported: "partial", notes: "" }
|
||||
|
||||
- id: cosyvoice3
|
||||
name: CosyVoice3
|
||||
models: "0.5B, 0.5B-RL"
|
||||
size: "~5.4GB"
|
||||
|
||||
capabilities:
|
||||
tts: true
|
||||
srt: true
|
||||
vc: true
|
||||
|
||||
special_features:
|
||||
- "Paralinguistic tags"
|
||||
|
||||
languages:
|
||||
en: { supported: true, flag: "🇺🇸", notes: "" }
|
||||
zh: { supported: true, flag: "🇨🇳", notes: "+ 18 dialects" }
|
||||
de: { supported: false, flag: "🇩🇪", notes: "" }
|
||||
es: { supported: false, flag: "🇪🇸", notes: "" }
|
||||
fr: { supported: false, flag: "🇫🇷", notes: "" }
|
||||
it: { supported: false, flag: "🇮🇹", notes: "" }
|
||||
ja: { supported: true, flag: "🇯🇵", notes: "" }
|
||||
ko: { supported: true, flag: "🇰🇷", notes: "" }
|
||||
ru: { supported: false, flag: "🇷🇺", notes: "" }
|
||||
pt: { supported: false, flag: "🇧🇷", notes: "" }
|
||||
pl: { supported: false, flag: "🇵🇱", notes: "" }
|
||||
hi: { supported: false, flag: "🇮🇳", notes: "" }
|
||||
ar: { supported: false, flag: "🇦🇪", notes: "" }
|
||||
tr: { supported: false, flag: "🇹🇷", notes: "" }
|
||||
th: { supported: false, flag: "🇹🇭", notes: "" }
|
||||
no: { supported: false, flag: "🇳🇴", notes: "" }
|
||||
vi: { supported: false, flag: "🇻🇳", notes: "" }
|
||||
hy: { supported: false, flag: "🇦🇲", notes: "" }
|
||||
ka: { supported: false, flag: "🇬🇪", notes: "" }
|
||||
da: { supported: false, flag: "🇩🇰", notes: "" }
|
||||
fi: { supported: false, flag: "🇫🇮", notes: "" }
|
||||
el: { supported: false, flag: "🇬🇷", notes: "" }
|
||||
he: { supported: false, flag: "🇮🇱", notes: "" }
|
||||
ms: { supported: false, flag: "🇲🇾", notes: "" }
|
||||
nl: { supported: false, flag: "🇳🇱", notes: "" }
|
||||
sv: { supported: false, flag: "🇸🇪", notes: "" }
|
||||
sw: { supported: false, flag: "🇰🇪", notes: "" }
|
||||
|
||||
features:
|
||||
voice_cloning: { supported: true, notes: "" }
|
||||
native_multi_speaker: { supported: false, notes: "" }
|
||||
voice_conversion: { supported: true, notes: "" }
|
||||
emotion_control: { supported: "partial", notes: "(via instruct)" }
|
||||
native_long_form: { supported: false, notes: "" }
|
||||
community_finetunes: { supported: false, notes: "" }
|
||||
vram_efficient: { supported: true, notes: "(5.4GB)" }
|
||||
speed_performance: { supported: true, notes: "Fast" }
|
||||
|
||||
- id: qwen3-tts
|
||||
name: Qwen3-TTS
|
||||
models: "0.6B, 1.7B (CustomVoice/VoiceDesign/Base)"
|
||||
size: "~3-6GB"
|
||||
|
||||
capabilities:
|
||||
tts: true
|
||||
srt: true
|
||||
vc: false
|
||||
|
||||
special_features:
|
||||
- "Voice design"
|
||||
|
||||
languages:
|
||||
en: { supported: true, flag: "🇺🇸", notes: "" }
|
||||
zh: { supported: true, flag: "🇨🇳", notes: "" }
|
||||
de: { supported: true, flag: "🇩🇪", notes: "" }
|
||||
es: { supported: true, flag: "🇪🇸", notes: "" }
|
||||
fr: { supported: true, flag: "🇫🇷", notes: "" }
|
||||
it: { supported: true, flag: "🇮🇹", notes: "" }
|
||||
ja: { supported: true, flag: "🇯🇵", notes: "" }
|
||||
ko: { supported: true, flag: "🇰🇷", notes: "" }
|
||||
ru: { supported: true, flag: "🇷🇺", notes: "" }
|
||||
pt: { supported: true, flag: "🇵🇹", notes: "(EU/BR*)" }
|
||||
pl: { supported: false, flag: "🇵🇱", notes: "" }
|
||||
hi: { supported: false, flag: "🇮🇳", notes: "" }
|
||||
ar: { supported: false, flag: "🇦🇪", notes: "" }
|
||||
tr: { supported: false, flag: "🇹🇷", notes: "" }
|
||||
th: { supported: false, flag: "🇹🇭", notes: "" }
|
||||
no: { supported: false, flag: "🇳🇴", notes: "" }
|
||||
vi: { supported: false, flag: "🇻🇳", notes: "" }
|
||||
hy: { supported: false, flag: "🇦🇲", notes: "" }
|
||||
ka: { supported: false, flag: "🇬🇪", notes: "" }
|
||||
da: { supported: false, flag: "🇩🇰", notes: "" }
|
||||
fi: { supported: false, flag: "🇫🇮", notes: "" }
|
||||
el: { supported: false, flag: "🇬🇷", notes: "" }
|
||||
he: { supported: false, flag: "🇮🇱", notes: "" }
|
||||
ms: { supported: false, flag: "🇲🇾", notes: "" }
|
||||
nl: { supported: false, flag: "🇳🇱", notes: "" }
|
||||
sv: { supported: false, flag: "🇸🇪", notes: "" }
|
||||
sw: { supported: false, flag: "🇰🇪", notes: "" }
|
||||
|
||||
features:
|
||||
voice_cloning: { supported: true, notes: "(Base model)" }
|
||||
native_multi_speaker: { supported: false, notes: "" }
|
||||
voice_conversion: { supported: false, notes: "" }
|
||||
emotion_control: { supported: "partial", notes: "(via instruct)" }
|
||||
native_long_form: { supported: false, notes: "" }
|
||||
community_finetunes: { supported: false, notes: "" }
|
||||
vram_efficient: { supported: true, notes: "(3-6GB)" }
|
||||
speed_performance: { supported: "partial", notes: "" }
|
||||
|
||||
- id: step-editx
|
||||
name: Step Audio EditX
|
||||
models: "3B LLM + CosyVoice"
|
||||
size: "~7GB"
|
||||
|
||||
capabilities:
|
||||
tts: true
|
||||
srt: true
|
||||
vc: false
|
||||
|
||||
special_features:
|
||||
- "Second Pass Speech Editing Node: 14 emotions"
|
||||
- "32 speaking styles"
|
||||
- "Paralinguistic effects"
|
||||
|
||||
languages:
|
||||
en: { supported: true, flag: "🇺🇸", notes: "" }
|
||||
zh: { supported: true, flag: "🇨🇳", notes: "(Mandarin + Sichuanese, Cantonese)" }
|
||||
de: { supported: false, flag: "🇩🇪", notes: "" }
|
||||
es: { supported: false, flag: "🇪🇸", notes: "" }
|
||||
fr: { supported: false, flag: "🇫🇷", notes: "" }
|
||||
it: { supported: false, flag: "🇮🇹", notes: "" }
|
||||
ja: { supported: true, flag: "🇯🇵", notes: "" }
|
||||
ko: { supported: true, flag: "🇰🇷", notes: "" }
|
||||
ru: { supported: false, flag: "🇷🇺", notes: "" }
|
||||
pt: { supported: false, flag: "🇧🇷", notes: "" }
|
||||
pl: { supported: false, flag: "🇵🇱", notes: "" }
|
||||
hi: { supported: false, flag: "🇮🇳", notes: "" }
|
||||
ar: { supported: false, flag: "🇦🇪", notes: "" }
|
||||
tr: { supported: false, flag: "🇹🇷", notes: "" }
|
||||
th: { supported: false, flag: "🇹🇭", notes: "" }
|
||||
no: { supported: false, flag: "🇳🇴", notes: "" }
|
||||
vi: { supported: false, flag: "🇻🇳", notes: "" }
|
||||
hy: { supported: false, flag: "🇦🇲", notes: "" }
|
||||
ka: { supported: false, flag: "🇬🇪", notes: "" }
|
||||
da: { supported: false, flag: "🇩🇰", notes: "" }
|
||||
fi: { supported: false, flag: "🇫🇮", notes: "" }
|
||||
el: { supported: false, flag: "🇬🇷", notes: "" }
|
||||
he: { supported: false, flag: "🇮🇱", notes: "" }
|
||||
ms: { supported: false, flag: "🇲🇾", notes: "" }
|
||||
nl: { supported: false, flag: "🇳🇱", notes: "" }
|
||||
sv: { supported: false, flag: "🇸🇪", notes: "" }
|
||||
sw: { supported: false, flag: "🇰🇪", notes: "" }
|
||||
|
||||
features:
|
||||
voice_cloning: { supported: true, notes: "" }
|
||||
native_multi_speaker: { supported: false, notes: "" }
|
||||
voice_conversion: { supported: false, notes: "" }
|
||||
emotion_control: { supported: true, notes: "(14 emotions)" }
|
||||
native_long_form: { supported: false, notes: "" }
|
||||
community_finetunes: { supported: false, notes: "" }
|
||||
vram_efficient: { supported: "partial", notes: "(7GB)" }
|
||||
speed_performance: { supported: "partial", notes: "" }
|
||||
|
||||
- id: rvc
|
||||
name: RVC
|
||||
models: "Community .pth"
|
||||
size: "100-300MB"
|
||||
|
||||
capabilities:
|
||||
tts: false
|
||||
srt: false
|
||||
vc: true
|
||||
|
||||
special_features:
|
||||
- "Real-time VC"
|
||||
- "Pitch shift (±14)"
|
||||
- "6 HuBERT models"
|
||||
- "Language-independent"
|
||||
|
||||
languages:
|
||||
en: { supported: true, flag: "🇺🇸", notes: "" }
|
||||
zh: { supported: true, flag: "🇨🇳", notes: "" }
|
||||
de: { supported: true, flag: "🇩🇪", notes: "" }
|
||||
es: { supported: true, flag: "🇪🇸", notes: "" }
|
||||
fr: { supported: true, flag: "🇫🇷", notes: "" }
|
||||
it: { supported: true, flag: "🇮🇹", notes: "" }
|
||||
ja: { supported: true, flag: "🇯🇵", notes: "" }
|
||||
ko: { supported: true, flag: "🇰🇷", notes: "" }
|
||||
ru: { supported: true, flag: "🇷🇺", notes: "" }
|
||||
pt: { supported: true, flag: "🇧🇷", notes: "" }
|
||||
pl: { supported: true, flag: "🇵🇱", notes: "" }
|
||||
hi: { supported: true, flag: "🇮🇳", notes: "" }
|
||||
ar: { supported: true, flag: "🇦🇪", notes: "" }
|
||||
tr: { supported: true, flag: "🇹🇷", notes: "" }
|
||||
th: { supported: true, flag: "🇹🇭", notes: "" }
|
||||
no: { supported: true, flag: "🇳🇴", notes: "" }
|
||||
vi: { supported: true, flag: "🇻🇳", notes: "" }
|
||||
hy: { supported: true, flag: "🇦🇲", notes: "" }
|
||||
ka: { supported: true, flag: "🇬🇪", notes: "" }
|
||||
da: { supported: true, flag: "🇩🇰", notes: "" }
|
||||
fi: { supported: true, flag: "🇫🇮", notes: "" }
|
||||
el: { supported: true, flag: "🇬🇷", notes: "" }
|
||||
he: { supported: true, flag: "🇮🇱", notes: "" }
|
||||
ms: { supported: true, flag: "🇲🇾", notes: "" }
|
||||
nl: { supported: true, flag: "🇳🇱", notes: "" }
|
||||
sv: { supported: true, flag: "🇸🇪", notes: "" }
|
||||
sw: { supported: true, flag: "🇰🇪", notes: "" }
|
||||
|
||||
features:
|
||||
voice_cloning: { supported: "partial", notes: "(needs training)" }
|
||||
native_multi_speaker: { supported: false, notes: "" }
|
||||
voice_conversion: { supported: true, notes: "" }
|
||||
emotion_control: { supported: false, notes: "" }
|
||||
native_long_form: { supported: null, notes: "N/A" }
|
||||
community_finetunes: { supported: true, notes: "" }
|
||||
vram_efficient: { supported: true, notes: "" }
|
||||
speed_performance: { supported: true, notes: "Fast" }
|
||||
|
||||
# Language metadata for reference
|
||||
language_metadata:
|
||||
en: { name: "English", code: "EN" }
|
||||
zh: { name: "Chinese", code: "ZH" }
|
||||
de: { name: "German", code: "DE" }
|
||||
es: { name: "Spanish", code: "ES" }
|
||||
fr: { name: "French", code: "FR" }
|
||||
it: { name: "Italian", code: "IT" }
|
||||
ja: { name: "Japanese", code: "JA" }
|
||||
ko: { name: "Korean", code: "KO" }
|
||||
ru: { name: "Russian", code: "RU" }
|
||||
pt: { name: "Portuguese", code: "PT" }
|
||||
pl: { name: "Polish", code: "PL" }
|
||||
hi: { name: "Hindi", code: "HI" }
|
||||
ar: { name: "Arabic", code: "AR" }
|
||||
tr: { name: "Turkish", code: "TR" }
|
||||
th: { name: "Thai", code: "TH" }
|
||||
no: { name: "Norwegian", code: "NO" }
|
||||
vi: { name: "Vietnamese", code: "VI" }
|
||||
hy: { name: "Armenian", code: "HY" }
|
||||
ka: { name: "Georgian", code: "KA" }
|
||||
da: { name: "Danish", code: "DA" }
|
||||
fi: { name: "Finnish", code: "FI" }
|
||||
el: { name: "Greek", code: "EL" }
|
||||
he: { name: "Hebrew", code: "HE" }
|
||||
ms: { name: "Malay", code: "MS" }
|
||||
nl: { name: "Dutch", code: "NL" }
|
||||
sv: { name: "Swedish", code: "SV" }
|
||||
sw: { name: "Swahili", code: "SW" }
|
||||
|
||||
# Feature metadata for reference
|
||||
feature_metadata:
|
||||
voice_cloning: { name: "Voice Cloning", display: "**Voice Cloning**" }
|
||||
native_multi_speaker: { name: "Native Multi-Speaker", display: "**Native Multi-Speaker**" }
|
||||
voice_conversion: { name: "Voice Conversion", display: "**Voice Conversion**" }
|
||||
emotion_control: { name: "Emotion Control", display: "**Emotion Control**" }
|
||||
native_long_form: { name: "Native Long-form (90min)", display: "**Native Long-form (90min)**" }
|
||||
community_finetunes: { name: "Community Finetunes", display: "**Community Finetunes**" }
|
||||
vram_efficient: { name: "VRAM Efficient", display: "**VRAM Efficient**" }
|
||||
speed_performance: { name: "Speed/Performance", display: "**Speed/Performance**" }
|
||||
|
||||
# Table notes for additional context
|
||||
table_notes:
|
||||
language_support:
|
||||
- "**CosyVoice3 Chinese**: Includes 18+ dialects (Cantonese, Sichuan, Dongbei, Shanghai, etc.)"
|
||||
- "**Higgs Audio 2**: Trained on EN, ZH (Mandarin), KO, DE, ES (English majority) - 10M hours AudioVerse dataset"
|
||||
- "**IndexTTS-2**: Trained on 55K+ hours - ZH, EN, JA primary."
|
||||
- "**RVC**: Language-independent (work as post-processors VC with any TTS output)"
|
||||
- "Qwen3-TTS: Supports pt-BR only with instruction (voice design or harcoded voices). Base can't do instructions, so it will always ouput pt-PT."
|
||||
@@ -0,0 +1,16 @@
|
||||
# TTS Engines Reference Tables
|
||||
|
||||
## Engine Comparison
|
||||
|
||||
| Engine | Models | Size | TTS | SRT | VC | Special Features | Languages |
|
||||
| ------------------ | ----------------------------------------- | ------------ | --- | --- | --- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| **F5-TTS** | Base, v1, E2TTS + 8 lang models | ~1.2GB each | ✅ | ✅ | ❌ | Targeted Word/Speech Editing, Speed control | 🇺🇸🇩🇪🇪🇸🇫🇷🇮🇹🇯🇵🇧🇷🇵🇱🇮🇳🇹🇭 |
|
||||
| **ChatterBox** | EN, DE×3, IT, FR, RU, HY, KA, JA, KO, NO | ~4.3GB | ✅ | ✅ | ✅ | Expressiveness slider | 🇺🇸🇩🇪🇫🇷🇮🇹🇯🇵🇰🇷🇷🇺🇳🇴🇦🇲🇬🇪 |
|
||||
| **ChatterBox 23L** | v1, v2, Vietnamese (Viterbox) | ~4.3GB | ✅ | ✅ | ✅ | 24 languages in single model, emotion tokens (v2 - doesn't work) | 🇺🇸🇨🇳🇩🇪🇪🇸🇫🇷🇮🇹🇯🇵🇰🇷🇷🇺🇵🇹🇵🇱🇮🇳🇦🇪🇹🇷🇹🇭🇳🇴🇻🇳🇩🇰🇫🇮🇬🇷🇮🇱🇲🇾🇳🇱🇸🇪🇰🇪(+9) |
|
||||
| **VibeVoice** | 1.5B, 7B, Hindi-1.5B, Hindi-7B | 5.4GB / 18GB | ✅ | ✅ | ❌ | 90-min long-form, Native 4-speaker, 4-bit quantization | 🇺🇸🇨🇳🇮🇳 |
|
||||
| **Higgs Audio 2** | 3B | ~9GB | ✅ | ✅ | ❌ | 3 multi-speaker, CUDA graphs (55+ tokens/sec) | 🇺🇸🇨🇳🇩🇪🇪🇸🇰🇷 |
|
||||
| **IndexTTS-2** | IndexTTS-2 | ? | ✅ | ✅ | ❌ | Emotion Control: 8 vectors, Text as reference, Audio as reference | 🇺🇸🇨🇳🇯🇵 |
|
||||
| **CosyVoice3** | 0.5B, 0.5B-RL | ~5.4GB | ✅ | ✅ | ✅ | Paralinguistic tags | 🇺🇸🇨🇳🇯🇵🇰🇷 |
|
||||
| **Qwen3-TTS** | 0.6B, 1.7B (CustomVoice/VoiceDesign/Base) | ~3-6GB | ✅ | ✅ | ❌ | Voice design | 🇺🇸🇨🇳🇩🇪🇪🇸🇫🇷🇮🇹🇯🇵🇰🇷🇷🇺🇵🇹 |
|
||||
| **Step Audio EditX** | 3B LLM + CosyVoice | ~7GB | ✅ | ✅ | ❌ | Second Pass Speech Editing Node: 14 emotions, 32 speaking styles, Paralinguistic effects | 🇺🇸🇨🇳🇯🇵🇰🇷 |
|
||||
| **RVC** | Community .pth | 100-300MB | ❌ | ❌ | ✅ | Real-time VC, Pitch shift (±14), 6 HuBERT models, Language-independent | 🇺🇸🇨🇳🇩🇪🇪🇸🇫🇷🇮🇹🇯🇵🇰🇷🇷🇺🇧🇷🇵🇱🇮🇳🇦🇪🇹🇷🇹🇭🇳🇴🇻🇳🇦🇲🇬🇪🇩🇰🇫🇮🇬🇷🇮🇱🇲🇾🇳🇱🇸🇪🇰🇪 |
|
||||
@@ -0,0 +1,14 @@
|
||||
# Feature Comparison Matrix
|
||||
|
||||
## Feature Comparison Matrix
|
||||
|
||||
| Feature | F5-TTS | ChatterBox | ChatterBox 23L | VibeVoice | Higgs Audio 2 | IndexTTS-2 | CosyVoice3 | Qwen3-TTS | Step Audio EditX | RVC |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| **Voice Cloning** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (Base model) | ✅ | ⚠️ (needs training) |
|
||||
| **Native Multi-Speaker** | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **Voice Conversion** | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ |
|
||||
| **Emotion Control** | ❌ | ❌ | ⚠️ (v2 tags - doesn't work) | ❌ | ⚠️ (via prompt) | ✅ (8 emotions) | ⚠️ (via instruct) | ⚠️ (via instruct) | ✅ (14 emotions) | ❌ |
|
||||
| **Native Long-form (90min)** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | N/A |
|
||||
| **Community Finetunes** | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| **VRAM Efficient** | ✅ | ✅ | ✅ | ⚠️ (5-18GB) | ⚠️ (9GB) | ⚠️ (9-12GB) | ✅ (5.4GB) | ✅ (3-6GB) | ⚠️ (7GB) | ✅ |
|
||||
| **Speed/Performance** | ✅ Very Fast | ✅ Fast | ✅ Fast | ⚠️ | ⚠️ | ⚠️ | ✅ Fast | ⚠️ | ⚠️ | ✅ Fast |
|
||||
@@ -0,0 +1,41 @@
|
||||
# Language Support by Engine
|
||||
|
||||
## Language Support by Engine
|
||||
|
||||
| Language | Code | F5-TTS | ChatterBox | ChatterBox 23L | VibeVoice | Higgs Audio 2 | IndexTTS-2 | CosyVoice3 | Qwen3-TTS | Step Audio EditX | RVC |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 🇺🇸 **English** | EN | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 🇨🇳 **Chinese** | ZH | ❌ | ❌ | ✅ | ✅ | ✅ (Mandarin) | ✅ | ✅ + 18 dialects | ✅ | ✅ (Mandarin + Sichuanese, Cantonese) | ✅ |
|
||||
| 🇩🇪 **German** | DE | ✅ | ✅ (×3) | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ |
|
||||
| 🇪🇸 **Spanish** | ES | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ |
|
||||
| 🇫🇷 **French** | FR | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
|
||||
| 🇮🇹 **Italian** | IT | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
|
||||
| 🇯🇵 **Japanese** | JA | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ ? | ✅ | ✅ | ✅ | ✅ |
|
||||
| 🇰🇷 **Korean** | KO | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 🇷🇺 **Russian** | RU | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ |
|
||||
| 🇧🇷 **Portuguese** | PT | ✅ (BR) | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ (EU/BR*) | ❌ | ✅ |
|
||||
| 🇵🇱 **Polish** | PL | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇮🇳 **Hindi** | HI | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇦🇪 **Arabic** | AR | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇹🇷 **Turkish** | TR | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇹🇭 **Thai** | TH | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇳🇴 **Norwegian** | NO | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇻🇳 **Vietnamese** | VI | ❌ | ❌ | ✅ (Viterbox) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇦🇲 **Armenian** | HY | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇬🇪 **Georgian** | KA | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇩🇰 **Danish** | DA | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇫🇮 **Finnish** | FI | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇬🇷 **Greek** | EL | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇮🇱 **Hebrew** | HE | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇲🇾 **Malay** | MS | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇳🇱 **Dutch** | NL | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇸🇪 **Swedish** | SV | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| 🇰🇪 **Swahili** | SW | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
|
||||
**Notes:**
|
||||
|
||||
- **CosyVoice3 Chinese**: Includes 18+ dialects (Cantonese, Sichuan, Dongbei, Shanghai, etc.)
|
||||
- **Higgs Audio 2**: Trained on EN, ZH (Mandarin), KO, DE, ES (English majority) - 10M hours AudioVerse dataset
|
||||
- **IndexTTS-2**: Trained on 55K+ hours - ZH, EN, JA primary.
|
||||
- **RVC**: Language-independent (work as post-processors VC with any TTS output)
|
||||
- Qwen3-TTS: Supports pt-BR only with instruction (voice design or harcoded voices). Base can't do instructions, so it will always ouput pt-PT.
|
||||
@@ -6,6 +6,35 @@
|
||||
|
||||
Don't ever make PLACEHOLDERS during implementation.
|
||||
|
||||
## Implementation Methodology
|
||||
|
||||
**Recommended Approach: Implement First, Then Compare**
|
||||
|
||||
When creating new engine components (wrapper, downloader, adapter, processor), follow this pattern:
|
||||
|
||||
1. **Implement based on understanding**: Write code using:
|
||||
- This guide's patterns and requirements
|
||||
- Implementation plan for the specific engine
|
||||
- `fails_to_avoid_TTS_Engine_Implementation.md` lessons
|
||||
- Engine-specific architecture documents
|
||||
|
||||
2. **Compare with reference implementation**: After writing, compare your code with the corresponding Step Audio EditX file:
|
||||
- Engine wrapper → `engines/step_audio_editx/step_audio_editx.py`
|
||||
- Downloader → `engines/step_audio_editx/step_audio_editx_downloader.py`
|
||||
- Adapter → `engines/adapters/step_audio_editx_adapter.py`
|
||||
- Processor → `nodes/step_audio_editx/step_audio_editx_processor.py`
|
||||
|
||||
3. **Fix discrepancies**: Correct patterns, imports, and methods that don't match established conventions
|
||||
|
||||
**Why this approach works better than copying:**
|
||||
- **Learning reinforcement**: Making and fixing mistakes creates stronger memory patterns
|
||||
- **Adaptation not copying**: Each engine has unique requirements that pure copying might miss
|
||||
- **Faster iteration**: Write quickly, then one focused comparison pass catches issues
|
||||
- **Documents real mistakes**: Comparison reveals actual errors worth adding to fails doc
|
||||
- **Preserves reasoning**: Shows understanding of WHY patterns exist, not just WHAT they are
|
||||
|
||||
This is the pattern that successfully created Step Audio EditX and should be followed for all future engines.
|
||||
|
||||
## Pre-Implementation Analysis
|
||||
|
||||
### 1. Research the Original Implementation
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# Qwen3-TTS Optimization Guide
|
||||
|
||||
This document explains how to enable torch.compile optimizations for Qwen3-TTS to achieve ~1.5-2x speedup.
|
||||
|
||||
## Requirements
|
||||
|
||||
**CRITICAL**: torch.compile optimizations require specific versions tested with the reference implementation:
|
||||
|
||||
- **PyTorch 2.10.0+** with CUDA 13.0 support
|
||||
- **triton-windows 3.6.0+** (Windows) or **triton 3.6.0+** (Linux)
|
||||
- Python 3.12 (recommended)
|
||||
|
||||
**Note**: PyTorch 2.8.x and 2.9.x have compatibility issues with triton on Windows. Only PyTorch 2.10+ is supported.
|
||||
|
||||
### Installation
|
||||
|
||||
#### Windows (Tested Configuration)
|
||||
```bash
|
||||
# Upgrade PyTorch to 2.10 with CUDA 13.0
|
||||
pip install --upgrade torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu130
|
||||
|
||||
# Install triton-windows 3.6.x
|
||||
pip install -U "triton-windows<3.7"
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
python -c "import torch; print(f'PyTorch: {torch.__version__}'); import triton; print(f'Triton: {triton.__version__}')"
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
PyTorch: 2.10.0+cu130
|
||||
Triton: 3.6.0.post25
|
||||
```
|
||||
|
||||
#### Linux
|
||||
```bash
|
||||
# Upgrade PyTorch to 2.10 with CUDA 13.0
|
||||
pip install --upgrade torch==2.10.0 torchvision==0.25.0 torchaudio==2.10.0 --index-url https://download.pytorch.org/whl/cu130
|
||||
|
||||
# triton is usually bundled with PyTorch on Linux
|
||||
# Verify version is 3.6.0+
|
||||
python -c "import triton; print(triton.__version__)"
|
||||
```
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
**No special configuration needed** - PyTorch 2.10 works with the default cudaMallocAsync allocator when using `compile_mode="default"`.
|
||||
|
||||
**Note**: The `reduce-overhead` mode (which includes automatic CUDA graphs) may still have issues with cudaMallocAsync. Use `compile_mode="default"` for best compatibility.
|
||||
|
||||
## Usage in Qwen3-TTS Engine Node
|
||||
|
||||
Once the environment is configured:
|
||||
|
||||
1. Enable `use_torch_compile`: **True**
|
||||
2. Keep `use_cuda_graphs`: **False** (not needed - reduce-overhead includes it)
|
||||
3. Set `compile_mode`: **reduce-overhead** (recommended)
|
||||
|
||||
### Settings Explanation
|
||||
|
||||
- **use_torch_compile**: Enables torch.compile optimization (~1.4-3x speedup)
|
||||
- First generation will be slower (compilation time)
|
||||
- Subsequent generations are much faster
|
||||
|
||||
- **use_cuda_graphs**: Manual CUDA graph capture
|
||||
- **Keep FALSE** when using reduce-overhead mode (it already includes automatic CUDA graphs)
|
||||
- Only enable if using `compile_mode=default` and you want manual graph capture
|
||||
|
||||
- **compile_mode**:
|
||||
- `reduce-overhead` (RECOMMENDED): Includes automatic CUDA graphs, fast compilation
|
||||
- `max-autotune`: Longer compilation, marginal gains
|
||||
- `default`: No automatic CUDA graphs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "'KernelNamespace' object has no attribute 'mm'"
|
||||
**Cause**: Incompatible PyTorch and triton-windows versions.
|
||||
|
||||
**Solution**: Upgrade to PyTorch 2.10.0 + triton-windows 3.6.0 (see Installation section).
|
||||
|
||||
### Error: "operator torchvision::nms does not exist"
|
||||
**Cause**: torchvision version doesn't match PyTorch version.
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
pip install --upgrade torchvision --index-url https://download.pytorch.org/whl/cu130
|
||||
```
|
||||
|
||||
### Error: "cudaMallocAsync does not support checkPoolLiveAllocations"
|
||||
**Cause**: Using `compile_mode="reduce-overhead"` which enables automatic CUDA graphs.
|
||||
|
||||
**Solution**: Use `compile_mode="default"` instead (recommended for Windows).
|
||||
|
||||
### Warning: "TensorFloat32 tensor cores... not enabled"
|
||||
**Info**: This is just a performance hint, not an error. TF32 is automatically enabled by the extension for better performance.
|
||||
|
||||
## Performance Expectations
|
||||
|
||||
**Tested on RTX 4090 with PyTorch 2.10.0 + triton-windows 3.6.0:**
|
||||
|
||||
| Configuration | Speed | Speedup | Notes |
|
||||
|--------------|-------|---------|-------|
|
||||
| **Baseline** (no optimizations) | 5.0 it/s | 1.0x | Standard inference |
|
||||
| **torch.compile mode=default** | 8.5 it/s | 1.7x | ✅ RECOMMENDED for Windows |
|
||||
| **+ manual CUDA graphs** | 8.6 it/s | 1.72x | Negligible gain (~0.1 it/s) |
|
||||
| **mode=reduce-overhead** | FAILS | - | ❌ cudaMallocAsync error on Windows |
|
||||
| **mode=max-autotune** | FAILS | - | ❌ cudaMallocAsync error on Windows |
|
||||
|
||||
### Key Findings
|
||||
|
||||
- **torch.compile alone provides ~1.7x speedup** with mode="default"
|
||||
- **Manual CUDA graphs add almost nothing** on top of torch.compile
|
||||
- **reduce-overhead and max-autotune fail on Windows** (work on Linux)
|
||||
- **First generation is slower** (~30-60s compilation overhead)
|
||||
- **Subsequent generations are fast** (compiled kernels cached)
|
||||
- **Model unloading works correctly** even with CUDA graphs enabled
|
||||
|
||||
## Notes
|
||||
|
||||
- The reference implementation (dffdeeq/Qwen3-TTS-streaming) uses the same approach
|
||||
- torch.compile caches compiled kernels, so subsequent ComfyUI sessions start faster
|
||||
- Model unloading works normally - no special handling needed (unlike manual CUDA graphs)
|
||||
@@ -21,4 +21,17 @@ except ImportError as e:
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise ImportError(f"F5-TTS adapter not available: {e}")
|
||||
|
||||
__all__ = ['ChatterBoxEngineAdapter', 'F5TTSEngineAdapter', 'CHATTERBOX_ADAPTER_AVAILABLE', 'F5TTS_ADAPTER_AVAILABLE']
|
||||
try:
|
||||
from .cosyvoice_adapter import CosyVoiceAdapter
|
||||
COSYVOICE_ADAPTER_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
COSYVOICE_ADAPTER_AVAILABLE = False
|
||||
# Create dummy class for compatibility
|
||||
class CosyVoiceAdapter:
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise ImportError(f"CosyVoice adapter not available: {e}")
|
||||
|
||||
__all__ = [
|
||||
'ChatterBoxEngineAdapter', 'F5TTSEngineAdapter', 'CosyVoiceAdapter',
|
||||
'CHATTERBOX_ADAPTER_AVAILABLE', 'F5TTS_ADAPTER_AVAILABLE', 'COSYVOICE_ADAPTER_AVAILABLE'
|
||||
]
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
"""
|
||||
Qwen3-ASR adapter for unified ASR pipeline.
|
||||
Uses qwen_asr package (transformers backend) and optional forced aligner.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import comfy.model_management as model_management
|
||||
import comfy.utils
|
||||
import time
|
||||
|
||||
from utils.asr.types import ASRRequest, ASRResult, ASRSegment, ASRWord
|
||||
|
||||
|
||||
class Qwen3ASREngineAdapter:
|
||||
def __init__(self, engine_data: Dict[str, Any]):
|
||||
self.engine_data = engine_data
|
||||
self.config = engine_data.get("config", engine_data)
|
||||
|
||||
def _resolve_precision(self, precision: str) -> torch.dtype:
|
||||
if precision == "bf16":
|
||||
return torch.bfloat16
|
||||
if precision == "fp16":
|
||||
return torch.float16
|
||||
if precision == "fp32":
|
||||
return torch.float32
|
||||
# auto
|
||||
if torch.cuda.is_available():
|
||||
major, _minor = torch.cuda.get_device_capability()
|
||||
return torch.bfloat16 if major >= 8 else torch.float16
|
||||
return torch.float32
|
||||
|
||||
def _get_model(self, req: ASRRequest):
|
||||
from utils.models.unified_model_interface import unified_model_interface, ModelLoadConfig
|
||||
from utils.device import resolve_torch_device
|
||||
|
||||
device = self.config.get("device", "auto")
|
||||
if model_management.cpu_mode():
|
||||
device = "cpu"
|
||||
elif device != "cpu":
|
||||
device = resolve_torch_device(device)
|
||||
|
||||
# Infer ASR model variant from existing Qwen3 model_size setting
|
||||
model_size = self.config.get("model_size", "1.7B")
|
||||
model_name = "Qwen3-ASR-0.6B" if model_size == "0.6B" else "Qwen3-ASR-1.7B"
|
||||
model_id = model_name
|
||||
|
||||
precision = self.config.get("dtype", "auto")
|
||||
|
||||
attn = self.config.get("attn_implementation", "auto")
|
||||
if attn == "auto":
|
||||
flash_attn = False
|
||||
else:
|
||||
flash_attn = attn == "flash_attention_2"
|
||||
|
||||
forced_aligner_enabled = self.config.get("asr_use_forced_aligner", False)
|
||||
|
||||
forced_aligner_model = "Qwen3-ForcedAligner-0.6B" if forced_aligner_enabled else ""
|
||||
|
||||
additional_params = {
|
||||
"precision": precision,
|
||||
"attn_implementation": "flash_attention_2" if flash_attn else "sdpa",
|
||||
"max_new_tokens": int(self.config.get("max_new_tokens", 256)),
|
||||
}
|
||||
|
||||
if forced_aligner_enabled:
|
||||
additional_params["forced_aligner"] = forced_aligner_model
|
||||
additional_params["forced_aligner_kwargs"] = None
|
||||
|
||||
config = ModelLoadConfig(
|
||||
engine_name="qwen3_asr",
|
||||
model_type="asr",
|
||||
model_name=model_name,
|
||||
model_path=model_id,
|
||||
device=device,
|
||||
additional_params=additional_params,
|
||||
)
|
||||
|
||||
return unified_model_interface.load_model(config)
|
||||
|
||||
def _prepare_audio(self, audio: Dict[str, Any]) -> torch.Tensor:
|
||||
waveform = audio["waveform"]
|
||||
sample_rate = audio["sample_rate"]
|
||||
|
||||
if waveform.ndim == 3:
|
||||
waveform = waveform[0]
|
||||
if waveform.shape[0] > 1:
|
||||
waveform = torch.mean(waveform, dim=0)
|
||||
else:
|
||||
waveform = waveform[0]
|
||||
|
||||
if sample_rate != 16000:
|
||||
w = waveform.view(1, 1, -1)
|
||||
w = F.interpolate(w, size=int(w.shape[-1] * 16000 / sample_rate), mode="linear", align_corners=False)
|
||||
waveform = w.view(-1)
|
||||
return waveform
|
||||
|
||||
def transcribe(self, req: ASRRequest) -> ASRResult:
|
||||
model_management.throw_exception_if_processing_interrupted()
|
||||
|
||||
model = self._get_model(req)
|
||||
waveform = self._prepare_audio(req.audio)
|
||||
sample_rate = 16000
|
||||
|
||||
language = None if not req.language or req.language.lower() == "auto" else req.language
|
||||
return_ts = req.timestamps == "word" or req.use_forced_aligner or self.config.get("asr_use_forced_aligner", False)
|
||||
|
||||
chunk_size = int(req.chunk_size)
|
||||
overlap = int(req.overlap)
|
||||
|
||||
segments: List[ASRSegment] = []
|
||||
full_text_parts: List[str] = []
|
||||
wav_np = waveform.cpu().numpy()
|
||||
total_samples = len(wav_np)
|
||||
progress_bar = None
|
||||
|
||||
if chunk_size > 0:
|
||||
chunk_samples = chunk_size * sample_rate
|
||||
overlap_samples = overlap * sample_rate
|
||||
step_samples = max(1, chunk_samples - overlap_samples)
|
||||
num_chunks = max(1, int((total_samples - overlap_samples + step_samples - 1) / step_samples))
|
||||
|
||||
if getattr(model, "backend", None) == "transformers":
|
||||
try:
|
||||
from utils.asr.progress_callback import ASRProgressStreamer
|
||||
total_tokens = int(self.config.get("max_new_tokens", 256)) * num_chunks
|
||||
progress_bar = comfy.utils.ProgressBar(total_tokens)
|
||||
model._streamer_factory = lambda max_new_tokens, pb: ASRProgressStreamer(
|
||||
max_new_tokens or int(self.config.get("max_new_tokens", 256)),
|
||||
pb,
|
||||
label="ASR",
|
||||
)
|
||||
model._progress_bar = progress_bar
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Chunk-level fallback progress (matches TTS-style console line)
|
||||
chunk_progress_bar = comfy.utils.ProgressBar(num_chunks)
|
||||
start_time = time.time()
|
||||
last_print = 0.0
|
||||
|
||||
for i in range(num_chunks):
|
||||
model_management.throw_exception_if_processing_interrupted()
|
||||
start = i * step_samples
|
||||
end = min(start + chunk_samples, total_samples)
|
||||
chunk_np = wav_np[start:end]
|
||||
results = model.transcribe(
|
||||
audio=[(chunk_np, sample_rate)],
|
||||
language=language,
|
||||
return_time_stamps=return_ts,
|
||||
)
|
||||
res = results[0]
|
||||
full_text_parts.append(res.text)
|
||||
chunk_offset = start / sample_rate
|
||||
|
||||
if return_ts and hasattr(res, "time_stamps") and res.time_stamps:
|
||||
for ts in res.time_stamps:
|
||||
word = ASRWord(
|
||||
start=ts.start_time + chunk_offset,
|
||||
end=ts.end_time + chunk_offset,
|
||||
text=ts.text,
|
||||
)
|
||||
seg = ASRSegment(
|
||||
start=word.start,
|
||||
end=word.end,
|
||||
text=word.text,
|
||||
words=[word],
|
||||
)
|
||||
segments.append(seg)
|
||||
|
||||
try:
|
||||
chunk_progress_bar.update(1)
|
||||
now = time.time()
|
||||
if now - last_print >= 0.5 or (i + 1) == num_chunks:
|
||||
elapsed = now - start_time
|
||||
its = (i + 1) / elapsed if elapsed > 0 else 0.0
|
||||
eta = (num_chunks - (i + 1)) / its if its > 0 else 0.0
|
||||
bar_width = 12
|
||||
filled = int(((i + 1) / num_chunks) * bar_width)
|
||||
bar = f"[{'█' * filled}{'░' * (bar_width - filled)}]"
|
||||
print(f"\r Progress: {bar} {i+1}/{num_chunks} | {its:.1f} it/s | {elapsed:.0f}s | ETA {eta:.0f}s", end="", flush=True)
|
||||
last_print = now
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
elapsed = time.time() - start_time
|
||||
avg = (num_chunks / elapsed) if elapsed > 0 else 0.0
|
||||
print(f"\r Complete: {num_chunks} chunks in {elapsed:.1f}s (avg {avg:.1f} it/s)" + " " * 20)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
else:
|
||||
if getattr(model, "backend", None) == "transformers":
|
||||
try:
|
||||
from utils.asr.progress_callback import ASRProgressStreamer
|
||||
total_tokens = int(self.config.get("max_new_tokens", 256))
|
||||
progress_bar = comfy.utils.ProgressBar(total_tokens)
|
||||
model._streamer_factory = lambda max_new_tokens, pb: ASRProgressStreamer(
|
||||
max_new_tokens or total_tokens,
|
||||
pb,
|
||||
label="ASR",
|
||||
)
|
||||
model._progress_bar = progress_bar
|
||||
except Exception:
|
||||
pass
|
||||
results = model.transcribe(
|
||||
audio=[(wav_np, sample_rate)],
|
||||
language=language,
|
||||
return_time_stamps=return_ts,
|
||||
)
|
||||
res = results[0]
|
||||
full_text_parts.append(res.text)
|
||||
if return_ts and hasattr(res, "time_stamps") and res.time_stamps:
|
||||
for ts in res.time_stamps:
|
||||
word = ASRWord(start=ts.start_time, end=ts.end_time, text=ts.text)
|
||||
segments.append(ASRSegment(start=word.start, end=word.end, text=word.text, words=[word]))
|
||||
|
||||
text = " ".join([t for t in full_text_parts if t]).strip()
|
||||
|
||||
return ASRResult(text=text, language=getattr(results[0], "language", None), segments=segments, raw=None)
|
||||
@@ -0,0 +1,447 @@
|
||||
"""
|
||||
CosyVoice3 Engine Adapter
|
||||
|
||||
Provides standardized interface for CosyVoice3 integration with TTS Audio Suite.
|
||||
Handles parameter mapping, character switching, and mode selection.
|
||||
"""
|
||||
|
||||
# FIX: PyYAML 6.0+ compatibility patch - MUST be before any yaml imports
|
||||
# Issue: 'Loader' object has no attribute 'max_depth' error (GitHub #220)
|
||||
import yaml
|
||||
if not hasattr(yaml.Loader, 'max_depth'):
|
||||
yaml.Loader.max_depth = 100
|
||||
if not hasattr(yaml.FullLoader, 'max_depth'):
|
||||
yaml.FullLoader.max_depth = 100
|
||||
if not hasattr(yaml.SafeLoader, 'max_depth'):
|
||||
yaml.SafeLoader.max_depth = 100
|
||||
|
||||
import os
|
||||
import torch
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from engines.cosyvoice.cosyvoice import CosyVoiceEngine
|
||||
from engines.cosyvoice.cosyvoice_downloader import cosyvoice_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 CosyVoiceAdapter:
|
||||
"""
|
||||
Adapter for CosyVoice3 engine providing unified interface compatibility.
|
||||
|
||||
Handles:
|
||||
- Parameter mapping between unified interface and CosyVoice3
|
||||
- Character switching with [CharacterName] syntax
|
||||
- Mode selection (zero_shot, instruct, cross_lingual)
|
||||
- Caching integration
|
||||
- Model management
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the CosyVoice3 adapter."""
|
||||
self.engine = None
|
||||
self.audio_cache = get_audio_cache()
|
||||
self.model_variant = None # Track model variant for cache invalidation
|
||||
# Store initialization parameters for lazy loading
|
||||
self._init_params = {
|
||||
"model_path": None,
|
||||
"device": "auto",
|
||||
"use_fp16": True,
|
||||
"load_trt": False,
|
||||
"load_vllm": False
|
||||
}
|
||||
|
||||
def initialize_engine(self,
|
||||
model_path: Optional[str] = None,
|
||||
device: str = "auto",
|
||||
use_fp16: bool = True,
|
||||
load_trt: bool = False,
|
||||
load_vllm: bool = False):
|
||||
"""
|
||||
Initialize CosyVoice3 engine.
|
||||
|
||||
Args:
|
||||
model_path: Path to model directory (auto-downloaded if None)
|
||||
device: Target device
|
||||
use_fp16: Use FP16 for inference
|
||||
load_trt: Load TensorRT engine
|
||||
load_vllm: Load vLLM engine
|
||||
"""
|
||||
# Store initialization parameters for lazy loading
|
||||
self._init_params.update({
|
||||
"model_path": model_path,
|
||||
"device": device,
|
||||
"use_fp16": use_fp16,
|
||||
"load_trt": load_trt,
|
||||
"load_vllm": load_vllm
|
||||
})
|
||||
|
||||
# Auto-download model if not provided
|
||||
if model_path is None or model_path == "Fun-CosyVoice3-0.5B":
|
||||
if not cosyvoice_downloader.is_model_available():
|
||||
print("📥 CosyVoice3 model not found, downloading...")
|
||||
model_path = cosyvoice_downloader.download_model()
|
||||
else:
|
||||
model_path = cosyvoice_downloader.get_model_path()
|
||||
|
||||
# Store model variant for cache key (extract from model_path string)
|
||||
if "RL" in str(model_path):
|
||||
self.model_variant = "RL"
|
||||
else:
|
||||
self.model_variant = "standard"
|
||||
|
||||
# Initialize engine
|
||||
self.engine = CosyVoiceEngine(
|
||||
model_dir=model_path,
|
||||
device=device,
|
||||
use_fp16=use_fp16,
|
||||
load_trt=load_trt,
|
||||
load_vllm=load_vllm
|
||||
)
|
||||
|
||||
def generate(self,
|
||||
text: str,
|
||||
speaker_audio: Optional[str] = None,
|
||||
reference_text: Optional[str] = None,
|
||||
instruct_text: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
speed: float = 1.0,
|
||||
stream: bool = False,
|
||||
model_path: Optional[str] = None,
|
||||
**kwargs) -> torch.Tensor:
|
||||
"""
|
||||
Generate speech with CosyVoice3.
|
||||
|
||||
Args:
|
||||
model_path: Model path/identifier (used to determine variant for caching)
|
||||
|
||||
Mode is auto-detected:
|
||||
- If instruct_text provided → instruct mode
|
||||
- If reference_text from Character Voices → zero_shot mode
|
||||
- Otherwise → cross_lingual mode
|
||||
|
||||
Args:
|
||||
text: Text to synthesize (supports [character] tags)
|
||||
speaker_audio: Speaker reference audio file path
|
||||
reference_text: Transcript of reference audio (from Character Voices .txt)
|
||||
instruct_text: Optional instruction for dialect/emotion/speed control
|
||||
speed: Speech speed multiplier (0.5-2.0)
|
||||
stream: Enable streaming
|
||||
**kwargs: Additional parameters
|
||||
|
||||
Returns:
|
||||
Generated audio tensor [1, samples] at 24000 Hz
|
||||
"""
|
||||
if self.engine is None:
|
||||
self.initialize_engine(**self._init_params)
|
||||
|
||||
# ALWAYS update model_variant from model_path parameter for cache invalidation
|
||||
# This ensures switching variants invalidates audio cache even if engine is reused
|
||||
if model_path:
|
||||
self.model_variant = "RL" if "RL" in str(model_path) else "standard"
|
||||
|
||||
# Auto-detect mode based on available parameters (if not explicitly provided)
|
||||
if mode is None:
|
||||
# print(f"🔍 Mode detection - instruct_text: '{instruct_text}', reference_text: '{reference_text[:50] if reference_text else None}...'")
|
||||
if instruct_text and instruct_text.strip():
|
||||
mode = "instruct"
|
||||
print(f"✅ Selected mode: instruct")
|
||||
if reference_text and reference_text.strip():
|
||||
print("⚠️ CosyVoice3: Both instruction and transcript provided. Using instruction mode (transcript ignored for best dialect/emotion control).")
|
||||
elif reference_text and reference_text.strip():
|
||||
mode = "zero_shot"
|
||||
print(f"✅ Selected mode: zero_shot")
|
||||
else:
|
||||
mode = "cross_lingual"
|
||||
print(f"✅ Selected mode: cross_lingual")
|
||||
else:
|
||||
print(f"✅ Using explicitly provided mode: {mode}")
|
||||
|
||||
# Check if text contains character tags
|
||||
has_character_tags = character_parser.CHARACTER_TAG_PATTERN.search(text) is not None
|
||||
|
||||
if has_character_tags:
|
||||
# Parse character switching tags
|
||||
processed_segments = self._process_character_tags(text)
|
||||
|
||||
if len(processed_segments) > 1:
|
||||
# Multi-segment character switching
|
||||
return self._generate_multi_character_segments(
|
||||
processed_segments,
|
||||
speaker_audio,
|
||||
reference_text,
|
||||
instruct_text=instruct_text,
|
||||
speed=speed,
|
||||
**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')
|
||||
else:
|
||||
processed_text = text
|
||||
character_name = None
|
||||
else:
|
||||
processed_text = text
|
||||
character_name = None
|
||||
|
||||
# Determine final speaker audio
|
||||
final_speaker_audio = speaker_audio
|
||||
final_reference_text = reference_text
|
||||
|
||||
# Only do character mapping if we have character tags
|
||||
if has_character_tags and character_name:
|
||||
character_mapping = get_character_mapping([character_name], engine_type="cosyvoice")
|
||||
|
||||
if character_name in character_mapping:
|
||||
char_audio, char_text = character_mapping[character_name]
|
||||
if char_audio is not None:
|
||||
final_speaker_audio = char_audio
|
||||
# Use character's reference text if available
|
||||
if char_text:
|
||||
final_reference_text = char_text
|
||||
else:
|
||||
# Character has audio but NO text reference - clear reference_text
|
||||
# This forces cross_lingual mode instead of zero_shot with wrong voice
|
||||
final_reference_text = None
|
||||
print(f"🎭 Using character voice: {character_name} -> {final_speaker_audio}")
|
||||
|
||||
# Generate cache key with mode and model variant explicitly included
|
||||
cache_key = self._generate_cache_key(
|
||||
text=processed_text,
|
||||
speaker_audio=self._get_stable_audio_identifier(final_speaker_audio),
|
||||
reference_text=final_reference_text,
|
||||
instruct_text=instruct_text,
|
||||
mode=mode,
|
||||
speed=speed,
|
||||
model_variant=self.model_variant, # Include variant for cache invalidation
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# print(f"🔑 Audio cache key: {cache_key[:100]}... (variant={self.model_variant})")
|
||||
|
||||
# Check cache
|
||||
cached_audio = self.audio_cache.get_cached_audio(cache_key)
|
||||
if cached_audio:
|
||||
print(f"💾 Using cached CosyVoice3 audio for: '{processed_text[:30]}...'")
|
||||
return cached_audio[0]
|
||||
|
||||
# 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)
|
||||
|
||||
# Convert CosyVoice paralinguistic tags from <tag> to [tag]
|
||||
from utils.text.cosyvoice_special_tags import convert_cosyvoice_special_tags
|
||||
processed_text = convert_cosyvoice_special_tags(processed_text)
|
||||
|
||||
# Generate audio
|
||||
audio = self.engine.generate(
|
||||
text=processed_text,
|
||||
prompt_wav=final_speaker_audio,
|
||||
prompt_text=final_reference_text,
|
||||
mode=mode,
|
||||
instruct_text=instruct_text,
|
||||
speed=speed,
|
||||
stream=stream
|
||||
)
|
||||
|
||||
# Cache the result
|
||||
duration = audio.shape[-1] / 24000.0 # CosyVoice3 uses 24000 Hz
|
||||
self.audio_cache.cache_audio(cache_key, audio, duration)
|
||||
|
||||
return audio
|
||||
|
||||
def _process_character_tags(self, text: str) -> List[Dict[str, Any]]:
|
||||
"""Process character switching tags."""
|
||||
from utils.text.character_parser.base_parser import CharacterParser
|
||||
from utils.voice.discovery import get_available_characters, voice_discovery
|
||||
|
||||
temp_parser = CharacterParser()
|
||||
|
||||
# Get available characters and aliases
|
||||
available_chars = get_available_characters()
|
||||
character_aliases = voice_discovery.get_character_aliases()
|
||||
|
||||
all_available = set()
|
||||
if available_chars:
|
||||
all_available.update(available_chars)
|
||||
for alias, target in character_aliases.items():
|
||||
all_available.add(alias.lower())
|
||||
all_available.add(target.lower())
|
||||
|
||||
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)
|
||||
|
||||
# Parse segments with language information
|
||||
segments = temp_parser.split_by_character(text, include_language=True)
|
||||
|
||||
processed_segments = []
|
||||
for character, segment_text, language in segments:
|
||||
segment_info = {
|
||||
'character': character,
|
||||
'text': segment_text,
|
||||
'language': language
|
||||
}
|
||||
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_reference_text: Optional[str],
|
||||
**kwargs
|
||||
) -> torch.Tensor:
|
||||
"""Generate audio for multiple character segments and combine them."""
|
||||
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'])
|
||||
|
||||
character_mapping = {}
|
||||
if unique_characters:
|
||||
character_mapping = get_character_mapping(list(unique_characters), engine_type="cosyvoice")
|
||||
|
||||
print(f"🎭 CosyVoice3: Processing {len(segments)} character segment(s)")
|
||||
|
||||
for segment in segments:
|
||||
character_name = segment.get('character', 'narrator')
|
||||
segment_text = segment.get('text', '').strip()
|
||||
|
||||
if not segment_text:
|
||||
continue
|
||||
|
||||
# Determine speaker audio for this segment
|
||||
speaker_audio = default_speaker_audio
|
||||
reference_text = default_reference_text
|
||||
|
||||
if character_name and character_name in character_mapping:
|
||||
char_audio, char_text = character_mapping[character_name]
|
||||
if char_audio:
|
||||
speaker_audio = char_audio
|
||||
if char_text:
|
||||
reference_text = char_text
|
||||
print(f"📖 Using character voice '{character_name}'")
|
||||
|
||||
# Generate cache key for this segment
|
||||
segment_cache_key = self._generate_cache_key(
|
||||
text=segment_text,
|
||||
speaker_audio=self._get_stable_audio_identifier(speaker_audio),
|
||||
reference_text=reference_text,
|
||||
instruct_text=kwargs.get('instruct_text'),
|
||||
mode=kwargs.get('mode'),
|
||||
speed=kwargs.get('speed', 1.0),
|
||||
model_variant=self.model_variant # Include variant for cache invalidation
|
||||
)
|
||||
|
||||
# Check cache first
|
||||
cached_segment_audio = self.audio_cache.get_cached_audio(segment_cache_key)
|
||||
if cached_segment_audio:
|
||||
print(f"💾 Using cached CosyVoice3 segment for '{character_name}'")
|
||||
segment_audio = cached_segment_audio[0]
|
||||
else:
|
||||
# Convert CosyVoice paralinguistic tags from <tag> to [tag]
|
||||
from utils.text.cosyvoice_special_tags import convert_cosyvoice_special_tags
|
||||
segment_text_converted = convert_cosyvoice_special_tags(segment_text)
|
||||
|
||||
# Generate audio for this segment
|
||||
segment_audio = self.engine.generate(
|
||||
text=segment_text_converted,
|
||||
prompt_wav=speaker_audio,
|
||||
prompt_text=reference_text,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# Cache the segment
|
||||
duration = segment_audio.shape[-1] / 24000.0 # CosyVoice3 uses 24000 Hz
|
||||
self.audio_cache.cache_audio(segment_cache_key, segment_audio, duration)
|
||||
|
||||
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 torch.zeros(1, 24000, dtype=torch.float32) # 1 second at CosyVoice3's 24000 Hz
|
||||
|
||||
def _generate_cache_key(self, **params) -> str:
|
||||
"""Generate cache key for CosyVoice3."""
|
||||
return self.audio_cache.generate_cache_key('cosyvoice', **params)
|
||||
|
||||
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
|
||||
|
||||
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 24000
|
||||
|
||||
def get_supported_languages(self) -> List[str]:
|
||||
"""Get supported languages."""
|
||||
return CosyVoiceEngine.SUPPORTED_LANGUAGES.copy()
|
||||
|
||||
def get_supported_dialects(self) -> List[str]:
|
||||
"""Get supported Chinese dialects."""
|
||||
return CosyVoiceEngine.SUPPORTED_DIALECTS.copy()
|
||||
|
||||
def inference_vc(self,
|
||||
source_wav: str,
|
||||
prompt_wav: str,
|
||||
speed: float = 1.0,
|
||||
stream: bool = False) -> torch.Tensor:
|
||||
"""
|
||||
Voice conversion using CosyVoice3.
|
||||
|
||||
Args:
|
||||
source_wav: Source audio file path (CosyVoice handles resampling)
|
||||
prompt_wav: Reference/target voice file path
|
||||
speed: Speech speed multiplier
|
||||
stream: Enable streaming
|
||||
|
||||
Returns:
|
||||
Converted audio tensor at 24kHz
|
||||
"""
|
||||
if self.engine is None:
|
||||
self.initialize_engine(**self._init_params)
|
||||
|
||||
# Call engine's VC method
|
||||
return self.engine.inference_vc(
|
||||
source_wav=source_wav,
|
||||
prompt_wav=prompt_wav,
|
||||
speed=speed,
|
||||
stream=stream
|
||||
)
|
||||
|
||||
def unload(self):
|
||||
"""Unload the engine to free memory."""
|
||||
if self.engine:
|
||||
self.engine.unload()
|
||||
self.engine = None
|
||||
|
||||
def cleanup(self):
|
||||
"""Alias for unload for compatibility with processor interface."""
|
||||
self.unload()
|
||||
@@ -40,7 +40,8 @@ class IndexTTSAdapter:
|
||||
use_cuda_kernel: Optional[bool] = None,
|
||||
use_deepspeed: bool = False,
|
||||
use_torch_compile: bool = False,
|
||||
use_accel: bool = False):
|
||||
use_accel: bool = False,
|
||||
low_vram: bool = False):
|
||||
"""
|
||||
Initialize IndexTTS-2 engine.
|
||||
|
||||
@@ -52,6 +53,7 @@ class IndexTTSAdapter:
|
||||
use_deepspeed: Use DeepSpeed optimization
|
||||
use_torch_compile: Enable torch.compile optimization for S2Mel stage
|
||||
use_accel: Enable GPT2 acceleration with FlashAttention
|
||||
low_vram: Enable Low VRAM mode (sequential offloading)
|
||||
"""
|
||||
# Auto-download model if not provided or if "auto-download" is specified
|
||||
if model_path is None or model_path == "auto-download":
|
||||
@@ -69,7 +71,8 @@ class IndexTTSAdapter:
|
||||
use_cuda_kernel=use_cuda_kernel,
|
||||
use_deepspeed=use_deepspeed,
|
||||
use_torch_compile=use_torch_compile,
|
||||
use_accel=use_accel
|
||||
use_accel=use_accel,
|
||||
low_vram=low_vram
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,835 @@
|
||||
"""
|
||||
Qwen3-TTS Engine Adapter
|
||||
|
||||
Provides standardized interface for Qwen3-TTS integration with TTS Audio Suite.
|
||||
Handles intelligent model selection, parameter mapping, voice references, and caching.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import torch
|
||||
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.qwen3_tts.qwen3_tts import Qwen3TTSEngine
|
||||
from utils.text.pause_processor import PauseTagProcessor
|
||||
from utils.audio.cache import get_audio_cache
|
||||
import folder_paths
|
||||
|
||||
|
||||
class Qwen3TTSEngineAdapter:
|
||||
"""
|
||||
Adapter for Qwen3-TTS engine providing unified interface compatibility.
|
||||
|
||||
Handles:
|
||||
- Intelligent model selection (CustomVoice/VoiceDesign/Base)
|
||||
- Parameter mapping between unified interface and Qwen3-TTS
|
||||
- Voice reference processing (Base model)
|
||||
- Pause tag processing and timing
|
||||
- Caching integration
|
||||
- Model management
|
||||
"""
|
||||
|
||||
def __init__(self, node_instance):
|
||||
"""
|
||||
Initialize the Qwen3-TTS adapter.
|
||||
|
||||
Args:
|
||||
node_instance: Parent node instance for context
|
||||
"""
|
||||
self.node = node_instance
|
||||
# DO NOT store engine reference - always query unified interface
|
||||
# Storing references causes stale CPU-bound engines when models are unloaded
|
||||
self.audio_cache = get_audio_cache()
|
||||
self.current_model_type = None # Track which model variant is loaded
|
||||
self._last_config = None # Store last ModelLoadConfig for engine retrieval
|
||||
|
||||
# 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 _determine_model_type(self, context: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Auto-select model variant based on context.
|
||||
|
||||
Priority:
|
||||
1. Voice Designer node → VoiceDesign
|
||||
2. Preset voice selected → CustomVoice
|
||||
3. Default → Base (zero-shot)
|
||||
|
||||
Args:
|
||||
context: Context dict with node_type, voice_preset, etc.
|
||||
|
||||
Returns:
|
||||
Model type string: "CustomVoice", "VoiceDesign", or "Base"
|
||||
"""
|
||||
# Priority 1: Voice Designer node
|
||||
if context.get("node_type") == "voice_designer":
|
||||
return "VoiceDesign"
|
||||
|
||||
# Priority 2: Preset voice selected
|
||||
voice_preset = context.get("voice_preset")
|
||||
if voice_preset and voice_preset != "None (Zero-shot / Custom)":
|
||||
return "CustomVoice"
|
||||
|
||||
# Priority 3: Default to Base (zero-shot)
|
||||
return "Base"
|
||||
|
||||
def load_base_model(self,
|
||||
model_path: str,
|
||||
device: str = "auto",
|
||||
dtype: str = "auto",
|
||||
model_size: str = "1.7B",
|
||||
attn_implementation: str = "auto",
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
use_torch_compile: bool = False,
|
||||
use_cuda_graphs: bool = False,
|
||||
compile_mode: str = "reduce-overhead"):
|
||||
"""
|
||||
Load Qwen3-TTS engine via unified interface with intelligent model selection.
|
||||
|
||||
Args:
|
||||
model_path: Model identifier (local:ModelName or ModelName for auto-download)
|
||||
device: Target device (auto/cuda/cpu)
|
||||
dtype: Model precision (bfloat16/float16/float32/auto)
|
||||
model_size: Model size (0.6B/1.7B)
|
||||
attn_implementation: Attention mechanism (auto/flash_attention_2/sdpa/eager)
|
||||
context: Context dict for model type determination
|
||||
use_torch_compile: Enable torch.compile optimization
|
||||
use_cuda_graphs: Enable CUDA graph capture
|
||||
compile_mode: torch.compile mode (reduce-overhead/max-autotune/default)
|
||||
"""
|
||||
from utils.models.unified_model_interface import unified_model_interface, ModelLoadConfig
|
||||
from utils.device import resolve_torch_device
|
||||
|
||||
# Determine which model variant to load
|
||||
context = context or {}
|
||||
model_type = self._determine_model_type(context)
|
||||
|
||||
# VoiceDesign only supports 1.7B
|
||||
if model_type == "VoiceDesign" and model_size == "0.6B":
|
||||
model_size = "1.7B"
|
||||
print("⚠️ VoiceDesign requires 1.7B model, auto-switching from 0.6B")
|
||||
|
||||
# Build model name
|
||||
model_name = f"Qwen3-TTS-12Hz-{model_size}-{model_type}"
|
||||
|
||||
# Track current model type (unified interface handles unloading automatically)
|
||||
self.current_model_type = model_type
|
||||
|
||||
# Loading message is printed by unified_model_interface
|
||||
|
||||
# Create config for unified interface
|
||||
config = ModelLoadConfig(
|
||||
engine_name="qwen3_tts",
|
||||
model_type="tts",
|
||||
model_name=model_name,
|
||||
model_path=model_path if model_path else model_name,
|
||||
device=resolve_torch_device(device),
|
||||
additional_params={
|
||||
"dtype": dtype,
|
||||
"attn_implementation": attn_implementation,
|
||||
"use_torch_compile": use_torch_compile,
|
||||
"use_cuda_graphs": use_cuda_graphs,
|
||||
"compile_mode": compile_mode
|
||||
}
|
||||
)
|
||||
|
||||
# Store config for later engine retrieval (DO NOT store engine reference)
|
||||
self._last_config = config
|
||||
|
||||
# Load model via unified interface (but don't store the reference)
|
||||
engine = unified_model_interface.load_model(config)
|
||||
|
||||
# Enable streaming optimizations if requested
|
||||
if use_torch_compile or use_cuda_graphs:
|
||||
try:
|
||||
engine.enable_streaming_optimizations(
|
||||
use_compile=use_torch_compile,
|
||||
use_cuda_graphs=use_cuda_graphs,
|
||||
compile_mode=compile_mode
|
||||
)
|
||||
print(f"✅ Qwen3-TTS: Streaming optimizations enabled (compile={use_torch_compile}, cuda_graphs={use_cuda_graphs})")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to enable streaming optimizations: {e}")
|
||||
|
||||
def _get_engine(self):
|
||||
"""
|
||||
Get current engine from unified interface.
|
||||
|
||||
CRITICAL: Always query unified interface instead of storing references.
|
||||
This prevents stale CPU-bound engine references when models are unloaded.
|
||||
|
||||
Returns:
|
||||
Current Qwen3-TTS engine from unified interface
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no engine is loaded or config not set
|
||||
"""
|
||||
if self._last_config is None:
|
||||
raise RuntimeError("No model loaded. Call load_base_model() first.")
|
||||
|
||||
# Query unified interface for current engine
|
||||
from utils.models.unified_model_interface import unified_model_interface
|
||||
engine = unified_model_interface.load_model(self._last_config)
|
||||
|
||||
if engine is None:
|
||||
raise RuntimeError("Engine not found in unified interface cache. Model may have been unloaded.")
|
||||
|
||||
return engine
|
||||
|
||||
def _ensure_engine_on_device(self, target_device: str = "auto"):
|
||||
"""
|
||||
Ensure engine is loaded and on the correct device.
|
||||
|
||||
CRITICAL: After unified_model_interface unloads a model variant, the adapter
|
||||
may still hold a reference to a CPU-bound engine. This method detects that
|
||||
and triggers a reload.
|
||||
|
||||
Args:
|
||||
target_device: Target device (auto/cuda/cpu)
|
||||
"""
|
||||
if self.engine is None:
|
||||
raise RuntimeError("Engine not loaded. Call load_base_model() first.")
|
||||
|
||||
# Check if engine has been moved to CPU (likely by model manager unload)
|
||||
# The engine wrapper has a _model attribute that's the actual Qwen3TTSModel
|
||||
if hasattr(self.engine, '_model') and hasattr(self.engine._model, 'model'):
|
||||
actual_model = self.engine._model.model
|
||||
if hasattr(actual_model, 'parameters'):
|
||||
try:
|
||||
first_param = next(actual_model.parameters())
|
||||
current_device = str(first_param.device)
|
||||
|
||||
# If model is on CPU but we want CUDA, it was likely unloaded
|
||||
from utils.device import resolve_torch_device
|
||||
expected_device = resolve_torch_device(target_device)
|
||||
|
||||
if current_device == "cpu" and expected_device != "cpu":
|
||||
print(f"⚠️ Qwen3-TTS engine is on CPU but {expected_device} requested")
|
||||
print(f" This usually means the model was unloaded by unified interface")
|
||||
print(f" Engine reference is stale - please check adapter state")
|
||||
raise RuntimeError(
|
||||
"Qwen3-TTS engine is on wrong device (CPU). "
|
||||
"This indicates the cached processor has a stale engine reference. "
|
||||
"The processor should have been reloaded by update_config()."
|
||||
)
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
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 (for Base model) or None (for CustomVoice/VoiceDesign)
|
||||
params: Generation parameters (language, speaker, instruct, etc.)
|
||||
process_pauses: Whether to process pause tags
|
||||
character_name: Character name for logging
|
||||
|
||||
Returns:
|
||||
Generated audio tensor [1, samples] at 24000 Hz
|
||||
"""
|
||||
# Get current engine from unified interface (always fresh reference)
|
||||
engine = self._get_engine()
|
||||
|
||||
# Extract seed from params and set global torch seed for reproducibility
|
||||
seed = params.get('seed', 0)
|
||||
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, engine)
|
||||
else:
|
||||
# Direct generation without pause processing
|
||||
return self._generate_direct(text, voice_ref, params, character_name, engine)
|
||||
|
||||
def _generate_direct(self,
|
||||
text: str,
|
||||
voice_ref: Optional[Dict[str, Any]],
|
||||
params: Dict[str, Any],
|
||||
character_name: Optional[str] = None,
|
||||
engine = None) -> torch.Tensor:
|
||||
"""
|
||||
Direct generation without pause processing.
|
||||
|
||||
Args:
|
||||
text: Input text (clean, no pause tags)
|
||||
voice_ref: Voice reference dict (for Base model)
|
||||
params: Generation parameters
|
||||
character_name: Character name for logging
|
||||
|
||||
Returns:
|
||||
Audio tensor [1, samples]
|
||||
"""
|
||||
# Get engine if not provided
|
||||
if engine is None:
|
||||
engine = self._get_engine()
|
||||
|
||||
# Determine generation method based on current model type
|
||||
if self.current_model_type == "CustomVoice":
|
||||
return self._generate_custom_voice(text, params, character_name, engine)
|
||||
elif self.current_model_type == "VoiceDesign":
|
||||
return self._generate_voice_design(text, params, character_name, engine)
|
||||
else: # Base
|
||||
return self._generate_voice_clone(text, voice_ref, params, character_name, engine)
|
||||
|
||||
def _generate_custom_voice(self,
|
||||
text: str,
|
||||
params: Dict[str, Any],
|
||||
character_name: Optional[str] = None,
|
||||
engine = None) -> torch.Tensor:
|
||||
"""
|
||||
Generate with CustomVoice model using preset speakers.
|
||||
|
||||
Args:
|
||||
text: Input text
|
||||
params: Must include 'speaker' and 'language'
|
||||
character_name: Character name for logging
|
||||
|
||||
Returns:
|
||||
Audio tensor [1, samples]
|
||||
"""
|
||||
speaker = params.get('voice_preset') or params.get('speaker', 'Vivian')
|
||||
language = params.get('language', 'Auto')
|
||||
instruct = params.get('instruct') # Optional instruction
|
||||
|
||||
# Generate cache key (include attn_implementation since it affects output quality)
|
||||
cache_key = self.audio_cache.generate_cache_key(
|
||||
'qwen3_tts',
|
||||
text=text,
|
||||
model_type='CustomVoice',
|
||||
speaker=speaker,
|
||||
language=language,
|
||||
instruct=instruct,
|
||||
top_k=params.get('top_k', 50),
|
||||
top_p=params.get('top_p', 1.0),
|
||||
temperature=params.get('temperature', 0.9),
|
||||
repetition_penalty=params.get('repetition_penalty', 1.05),
|
||||
max_new_tokens=params.get('max_new_tokens', 2048),
|
||||
seed=params.get('seed', 0),
|
||||
attn_implementation=params.get('attn_implementation', 'auto'),
|
||||
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 Qwen3-TTS audio for '{char_desc}': '{text[:30]}...'")
|
||||
return cached_audio[0]
|
||||
|
||||
# Create ComfyUI progress bar and convert to transformers streamer
|
||||
max_new_tokens = params.get('max_new_tokens', 2048)
|
||||
progress_bar = self._create_progress_bar(max_new_tokens, text)
|
||||
|
||||
# Convert progress_bar to transformers streamer for bundled model
|
||||
streamer = None
|
||||
if progress_bar is not None:
|
||||
from engines.qwen3_tts.progress_callback import Qwen3TTSProgressStreamer
|
||||
streamer = Qwen3TTSProgressStreamer(max_new_tokens, progress_bar, text_input=text)
|
||||
|
||||
# Get engine if not provided
|
||||
if engine is None:
|
||||
engine = self._get_engine()
|
||||
|
||||
# Generate
|
||||
wavs, sr = engine.generate_custom_voice(
|
||||
text=text,
|
||||
language=language,
|
||||
speaker=speaker,
|
||||
instruct=instruct,
|
||||
top_k=params.get('top_k', 50),
|
||||
top_p=params.get('top_p', 1.0),
|
||||
temperature=params.get('temperature', 0.9),
|
||||
repetition_penalty=params.get('repetition_penalty', 1.05),
|
||||
max_new_tokens=max_new_tokens,
|
||||
streamer=streamer # Use transformers streamer kwarg
|
||||
)
|
||||
|
||||
# Convert to tensor
|
||||
audio_tensor = self._convert_output_to_tensor(wavs, sr)
|
||||
|
||||
# Cache the result
|
||||
duration = self.audio_cache._calculate_duration(audio_tensor, 'qwen3_tts')
|
||||
self.audio_cache.cache_audio(cache_key, audio_tensor, duration)
|
||||
|
||||
return audio_tensor
|
||||
|
||||
def _generate_voice_design(self,
|
||||
text: str,
|
||||
params: Dict[str, Any],
|
||||
character_name: Optional[str] = None,
|
||||
engine = None) -> torch.Tensor:
|
||||
"""
|
||||
Generate with VoiceDesign model using text description.
|
||||
|
||||
Args:
|
||||
text: Input text
|
||||
params: Must include 'instruct' (voice description) and 'language'
|
||||
character_name: Character name for logging
|
||||
|
||||
Returns:
|
||||
Audio tensor [1, samples]
|
||||
"""
|
||||
instruct = params.get('instruct') or params.get('voice_description')
|
||||
if not instruct:
|
||||
raise ValueError("VoiceDesign requires 'instruct' parameter with voice description")
|
||||
|
||||
language = params.get('language', 'Auto')
|
||||
|
||||
# Generate cache key (include attn_implementation since it affects output quality)
|
||||
cache_key = self.audio_cache.generate_cache_key(
|
||||
'qwen3_tts',
|
||||
text=text,
|
||||
model_type='VoiceDesign',
|
||||
instruct=instruct,
|
||||
language=language,
|
||||
top_k=params.get('top_k', 50),
|
||||
top_p=params.get('top_p', 1.0),
|
||||
temperature=params.get('temperature', 0.9),
|
||||
repetition_penalty=params.get('repetition_penalty', 1.05),
|
||||
max_new_tokens=params.get('max_new_tokens', 2048),
|
||||
seed=params.get('seed', 0),
|
||||
attn_implementation=params.get('attn_implementation', 'auto'),
|
||||
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 Qwen3-TTS VoiceDesign audio for '{char_desc}': '{text[:30]}...'")
|
||||
return cached_audio[0]
|
||||
|
||||
# Create ComfyUI progress bar and convert to transformers streamer
|
||||
max_new_tokens = params.get('max_new_tokens', 2048)
|
||||
progress_bar = self._create_progress_bar(max_new_tokens, text)
|
||||
|
||||
# Convert progress_bar to transformers streamer for bundled model
|
||||
streamer = None
|
||||
if progress_bar is not None:
|
||||
from engines.qwen3_tts.progress_callback import Qwen3TTSProgressStreamer
|
||||
streamer = Qwen3TTSProgressStreamer(max_new_tokens, progress_bar, text_input=text)
|
||||
|
||||
# Get engine if not provided
|
||||
if engine is None:
|
||||
engine = self._get_engine()
|
||||
|
||||
# Generate
|
||||
wavs, sr = engine.generate_voice_design(
|
||||
text=text,
|
||||
language=language,
|
||||
instruct=instruct,
|
||||
top_k=params.get('top_k', 50),
|
||||
top_p=params.get('top_p', 1.0),
|
||||
temperature=params.get('temperature', 0.9),
|
||||
repetition_penalty=params.get('repetition_penalty', 1.05),
|
||||
max_new_tokens=max_new_tokens,
|
||||
streamer=streamer # Use transformers streamer kwarg
|
||||
)
|
||||
|
||||
# Convert to tensor
|
||||
audio_tensor = self._convert_output_to_tensor(wavs, sr)
|
||||
|
||||
# Cache the result
|
||||
duration = self.audio_cache._calculate_duration(audio_tensor, 'qwen3_tts')
|
||||
self.audio_cache.cache_audio(cache_key, audio_tensor, duration)
|
||||
|
||||
return audio_tensor
|
||||
|
||||
def _generate_voice_clone(self,
|
||||
text: str,
|
||||
voice_ref: Optional[Dict[str, Any]],
|
||||
params: Dict[str, Any],
|
||||
character_name: Optional[str] = None,
|
||||
engine = None) -> torch.Tensor:
|
||||
"""
|
||||
Generate with Base model using zero-shot voice cloning.
|
||||
|
||||
Args:
|
||||
text: Input text
|
||||
voice_ref: Voice reference dict with audio and optional transcript
|
||||
params: Generation parameters
|
||||
character_name: Character name for logging
|
||||
|
||||
Returns:
|
||||
Audio tensor [1, samples]
|
||||
"""
|
||||
# Extract voice reference (returns converted audio + original for cache hashing)
|
||||
ref_audio, ref_text, voice_x_vector_only, ref_audio_original = self._extract_voice_reference(voice_ref)
|
||||
|
||||
language = params.get('language', 'Auto')
|
||||
# Use voice_ref x_vector_only_mode if provided, otherwise use params
|
||||
x_vector_only = voice_x_vector_only if voice_x_vector_only else params.get('x_vector_only_mode', False)
|
||||
|
||||
# Warn user when x_vector_only mode is forced or ignoring available ref text
|
||||
if x_vector_only and ref_text:
|
||||
print(f"⚠️ Qwen3-TTS: x_vector_only mode enabled - ignoring reference text (using speaker embedding only)")
|
||||
|
||||
# Generate cache key using voice_ref dict (contains waveform + sample_rate)
|
||||
# This ensures different voices generate different cache keys
|
||||
from utils.audio.audio_hash import generate_stable_audio_component
|
||||
if voice_ref and isinstance(voice_ref, dict):
|
||||
# Check if voice_ref has audio tensor or file path
|
||||
if "audio" in voice_ref:
|
||||
# Unified Character Voices format: {"audio": {"waveform": ..., "sample_rate": ...}, "audio_path": ..., ...}
|
||||
audio_dict = voice_ref.get("audio")
|
||||
audio_component = generate_stable_audio_component(reference_audio=audio_dict)
|
||||
elif ref_audio_original is not None and isinstance(ref_audio_original, str):
|
||||
# File path format: {"audio_path": "/path/to/file.wav", "reference_text": "..."}
|
||||
audio_component = generate_stable_audio_component(audio_file_path=ref_audio_original)
|
||||
elif "waveform" in voice_ref:
|
||||
# Direct tensor format: {"waveform": tensor, "sample_rate": 24000}
|
||||
audio_component = generate_stable_audio_component(reference_audio=voice_ref)
|
||||
else:
|
||||
audio_component = "default_voice"
|
||||
elif ref_audio_original is not None and isinstance(ref_audio_original, str):
|
||||
# File path case (voice_ref is None but ref_audio_original extracted)
|
||||
audio_component = generate_stable_audio_component(audio_file_path=ref_audio_original)
|
||||
else:
|
||||
audio_component = "default_voice"
|
||||
|
||||
cache_key = self.audio_cache.generate_cache_key(
|
||||
'qwen3_tts',
|
||||
text=text,
|
||||
model_type='Base',
|
||||
audio_component=audio_component,
|
||||
ref_text=ref_text,
|
||||
language=language,
|
||||
x_vector_only=x_vector_only,
|
||||
top_k=params.get('top_k', 50),
|
||||
top_p=params.get('top_p', 1.0),
|
||||
temperature=params.get('temperature', 0.9),
|
||||
repetition_penalty=params.get('repetition_penalty', 1.05),
|
||||
max_new_tokens=params.get('max_new_tokens', 2048),
|
||||
seed=params.get('seed', 0),
|
||||
attn_implementation=params.get('attn_implementation', 'auto'),
|
||||
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 Qwen3-TTS VoiceClone audio for '{char_desc}': '{text[:30]}...'")
|
||||
return cached_audio[0]
|
||||
|
||||
# Create ComfyUI progress bar and convert to transformers streamer
|
||||
max_new_tokens = params.get('max_new_tokens', 2048)
|
||||
progress_bar = self._create_progress_bar(max_new_tokens, text)
|
||||
|
||||
# Convert progress_bar to transformers streamer for bundled model
|
||||
streamer = None
|
||||
if progress_bar is not None:
|
||||
from engines.qwen3_tts.progress_callback import Qwen3TTSProgressStreamer
|
||||
streamer = Qwen3TTSProgressStreamer(max_new_tokens, progress_bar, text_input=text)
|
||||
|
||||
# Get engine if not provided
|
||||
if engine is None:
|
||||
engine = self._get_engine()
|
||||
|
||||
# Generate
|
||||
wavs, sr = engine.generate_voice_clone(
|
||||
text=text,
|
||||
language=language,
|
||||
ref_audio=ref_audio,
|
||||
ref_text=ref_text,
|
||||
x_vector_only_mode=x_vector_only,
|
||||
top_k=params.get('top_k', 50),
|
||||
top_p=params.get('top_p', 1.0),
|
||||
temperature=params.get('temperature', 0.9),
|
||||
repetition_penalty=params.get('repetition_penalty', 1.05),
|
||||
max_new_tokens=max_new_tokens,
|
||||
streamer=streamer # Use transformers streamer kwarg
|
||||
)
|
||||
|
||||
# Convert to tensor
|
||||
audio_tensor = self._convert_output_to_tensor(wavs, sr)
|
||||
|
||||
# Cache the result
|
||||
duration = self.audio_cache._calculate_duration(audio_tensor, 'qwen3_tts')
|
||||
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,
|
||||
engine = None) -> torch.Tensor:
|
||||
"""
|
||||
Generate with pause tag processing.
|
||||
|
||||
Args:
|
||||
text: Text with pause tags ([pause:2], [pause:1.5s], [pause:500ms])
|
||||
voice_ref: Voice reference dict (for Base model)
|
||||
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 # Qwen3-TTS native sample rate
|
||||
|
||||
# Get engine if not provided
|
||||
if engine is None:
|
||||
engine = self._get_engine()
|
||||
|
||||
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, engine)
|
||||
|
||||
# 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 and text from voice_ref dict.
|
||||
|
||||
Args:
|
||||
voice_ref: Voice reference dict (from voice discovery)
|
||||
|
||||
Returns:
|
||||
Tuple of (ref_audio, ref_text, x_vector_only_mode)
|
||||
- ref_audio: Audio in format expected by Qwen3-TTS (string path or tuple (np.ndarray, sr))
|
||||
- ref_text: Reference transcript or None
|
||||
- x_vector_only_mode: Whether to use speaker embedding only (True) or ICL mode (False)
|
||||
"""
|
||||
if voice_ref is None or not isinstance(voice_ref, dict):
|
||||
# For Base model without reference, use default behavior
|
||||
return None, None, False
|
||||
|
||||
# Extract reference audio (multiple possible keys)
|
||||
ref_audio_original = (voice_ref.get('prompt_audio_path') or
|
||||
voice_ref.get('audio_path') or
|
||||
voice_ref.get('audio') or
|
||||
voice_ref.get('waveform'))
|
||||
|
||||
# Extract reference text (multiple possible keys)
|
||||
ref_text = (voice_ref.get('prompt_text') or
|
||||
voice_ref.get('reference_text') or
|
||||
voice_ref.get('text', ''))
|
||||
|
||||
# Extract x_vector_only_mode flag (default to False for ICL mode)
|
||||
x_vector_only_mode = voice_ref.get('x_vector_only_mode', False)
|
||||
|
||||
# Convert ref_audio to format expected by Qwen3-TTS
|
||||
ref_audio_converted = self._convert_audio_to_qwen_format(ref_audio_original, voice_ref)
|
||||
|
||||
# Return: (converted_audio, ref_text, x_vector_mode, original_audio_for_cache)
|
||||
return ref_audio_converted, ref_text, x_vector_only_mode, ref_audio_original
|
||||
|
||||
def _convert_audio_to_qwen_format(self, ref_audio, voice_ref: Dict[str, Any]):
|
||||
"""
|
||||
Convert audio reference to format expected by Qwen3-TTS bundled implementation.
|
||||
|
||||
Qwen3-TTS accepts:
|
||||
- String: File path
|
||||
- Tuple[np.ndarray, int]: (waveform, sample_rate)
|
||||
|
||||
Args:
|
||||
ref_audio: Audio reference (can be torch.Tensor, dict, string, etc.)
|
||||
voice_ref: Full voice reference dict (may contain sample_rate)
|
||||
|
||||
Returns:
|
||||
Audio in Qwen3-TTS expected format
|
||||
"""
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
# Already a string path - return as-is
|
||||
if isinstance(ref_audio, str):
|
||||
return ref_audio
|
||||
|
||||
# ComfyUI audio dict format
|
||||
if isinstance(ref_audio, dict) and 'waveform' in ref_audio:
|
||||
waveform = ref_audio['waveform']
|
||||
sample_rate = ref_audio.get('sample_rate', 24000)
|
||||
|
||||
# Convert torch tensor to numpy
|
||||
if isinstance(waveform, torch.Tensor):
|
||||
# ComfyUI format is [batch, channels, samples]
|
||||
# Convert to [samples] mono
|
||||
waveform_np = waveform.squeeze().cpu().numpy()
|
||||
if waveform_np.ndim > 1:
|
||||
# Average channels if stereo
|
||||
waveform_np = np.mean(waveform_np, axis=0)
|
||||
return (waveform_np.astype(np.float32), int(sample_rate))
|
||||
|
||||
# Raw torch.Tensor (from voice_ref['waveform'])
|
||||
if isinstance(ref_audio, torch.Tensor):
|
||||
# Get sample rate from voice_ref
|
||||
sample_rate = voice_ref.get('sample_rate', 24000)
|
||||
|
||||
# Convert to numpy
|
||||
waveform_np = ref_audio.squeeze().cpu().numpy()
|
||||
if waveform_np.ndim > 1:
|
||||
# Average channels if stereo
|
||||
waveform_np = np.mean(waveform_np, axis=0)
|
||||
return (waveform_np.astype(np.float32), int(sample_rate))
|
||||
|
||||
# Tuple (waveform, sr) - ensure numpy
|
||||
if isinstance(ref_audio, tuple) and len(ref_audio) == 2:
|
||||
waveform, sr = ref_audio
|
||||
if isinstance(waveform, torch.Tensor):
|
||||
waveform = waveform.squeeze().cpu().numpy()
|
||||
if waveform.ndim > 1:
|
||||
waveform = np.mean(waveform, axis=0)
|
||||
return (waveform.astype(np.float32), int(sr))
|
||||
|
||||
# None or unsupported
|
||||
return ref_audio
|
||||
|
||||
def _create_progress_bar(self, max_tokens: int, text: str = ""):
|
||||
"""
|
||||
Create ComfyUI progress bar for generation tracking with accurate estimation.
|
||||
|
||||
Args:
|
||||
max_tokens: Maximum tokens to generate
|
||||
text: Input text for estimating actual token count
|
||||
|
||||
Returns:
|
||||
Progress bar instance or None if ComfyUI utils not available
|
||||
"""
|
||||
try:
|
||||
import comfy.utils
|
||||
|
||||
# Estimate actual tokens based on text length (same heuristic as progress streamer)
|
||||
# Rough heuristic: ~1.5 tokens per character for TTS
|
||||
if text:
|
||||
estimated_tokens = int(len(text) * 1.5)
|
||||
# Cap estimate to max_tokens
|
||||
progress_total = min(estimated_tokens, max_tokens)
|
||||
else:
|
||||
progress_total = max_tokens
|
||||
|
||||
return comfy.utils.ProgressBar(progress_total)
|
||||
except (ImportError, AttributeError):
|
||||
return None # ComfyUI progress not available
|
||||
|
||||
def _convert_output_to_tensor(self, wavs, sample_rate: int) -> torch.Tensor:
|
||||
"""
|
||||
Convert Qwen3-TTS output to tensor format.
|
||||
|
||||
Args:
|
||||
wavs: Output from engine (numpy array or list of numpy arrays)
|
||||
sample_rate: Sample rate (should be 24000)
|
||||
|
||||
Returns:
|
||||
Audio tensor [1, samples]
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
# Handle list of arrays (batch generation)
|
||||
if isinstance(wavs, list):
|
||||
if len(wavs) == 1:
|
||||
wavs = wavs[0]
|
||||
else:
|
||||
# Concatenate multiple outputs
|
||||
wavs = np.concatenate(wavs, axis=-1)
|
||||
|
||||
# Convert numpy to tensor
|
||||
if isinstance(wavs, np.ndarray):
|
||||
audio_tensor = torch.from_numpy(wavs).float()
|
||||
elif torch.is_tensor(wavs):
|
||||
audio_tensor = wavs.float()
|
||||
else:
|
||||
raise ValueError(f"Unsupported audio output type: {type(wavs)}")
|
||||
|
||||
# Ensure 2D shape [1, samples]
|
||||
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]
|
||||
|
||||
return audio_tensor
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Clean up resources.
|
||||
|
||||
Note: We don't auto-unload here - let the model stay in VRAM for reuse.
|
||||
Only explicit unload (via button) should trigger model deletion.
|
||||
"""
|
||||
# Don't auto-unload - let the model stay in VRAM for reuse
|
||||
pass
|
||||
@@ -188,28 +188,47 @@ class RVCEngineAdapter:
|
||||
raise e
|
||||
|
||||
def _find_hubert_model(self) -> Optional[str]:
|
||||
"""Find available Hubert model."""
|
||||
"""Find available Hubert model (respects extra_model_paths.yaml)."""
|
||||
try:
|
||||
import folder_paths
|
||||
models_dir = folder_paths.models_dir
|
||||
|
||||
# Common Hubert model names and locations
|
||||
# Common Hubert model names
|
||||
hubert_candidates = [
|
||||
"content-vec-best.safetensors",
|
||||
"hubert_base.pt",
|
||||
"chinese-hubert-base.pt",
|
||||
"chinese-wav2vec2-base.pt"
|
||||
]
|
||||
|
||||
|
||||
# Search in extra_model_paths.yaml first
|
||||
try:
|
||||
from utils.models.extra_paths import get_all_tts_model_paths
|
||||
all_tts_paths = get_all_tts_model_paths('TTS')
|
||||
|
||||
for base_path in all_tts_paths:
|
||||
for model_name in hubert_candidates:
|
||||
# Check TTS/hubert/ and TTS/ root
|
||||
search_paths = [
|
||||
os.path.join(base_path, "hubert", model_name),
|
||||
os.path.join(base_path, model_name)
|
||||
]
|
||||
for model_path in search_paths:
|
||||
if os.path.exists(model_path):
|
||||
print(f"📄 Found Hubert model: {model_name}")
|
||||
return model_path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback to hardcoded ComfyUI paths
|
||||
import folder_paths
|
||||
models_dir = folder_paths.models_dir
|
||||
|
||||
for model_name in hubert_candidates:
|
||||
# Try TTS path first, then legacy locations
|
||||
search_paths = [
|
||||
os.path.join(models_dir, "TTS", "hubert", model_name),
|
||||
os.path.join(models_dir, "TTS", model_name),
|
||||
os.path.join(models_dir, "hubert", model_name), # Legacy
|
||||
os.path.join(models_dir, model_name) # Legacy - direct in models/
|
||||
]
|
||||
|
||||
|
||||
for model_path in search_paths:
|
||||
if os.path.exists(model_path):
|
||||
print(f"📄 Found Hubert model: {model_name}")
|
||||
|
||||
@@ -92,7 +92,7 @@ class StepAudioEditXEngineAdapter:
|
||||
def load_base_model(self,
|
||||
model_path: str,
|
||||
device: str = "auto",
|
||||
torch_dtype: str = "bfloat16",
|
||||
torch_dtype: str = "auto",
|
||||
quantization: Optional[str] = None):
|
||||
"""
|
||||
Load Step Audio EditX engine via unified interface (with caching).
|
||||
@@ -197,7 +197,7 @@ class StepAudioEditXEngineAdapter:
|
||||
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'),
|
||||
torch_dtype=params.get('torch_dtype', 'auto'),
|
||||
quantization=params.get('quantization', None),
|
||||
character=character_name or 'narrator'
|
||||
)
|
||||
@@ -211,6 +211,16 @@ class StepAudioEditXEngineAdapter:
|
||||
|
||||
# Create ComfyUI progress bar for generation tracking with time prediction
|
||||
max_new_tokens = params.get('max_new_tokens', 8192)
|
||||
|
||||
# Estimate actual tokens based on text length (same heuristic as Qwen3-TTS)
|
||||
# Rough heuristic: ~0.7 tokens per character for TTS (conservative estimate)
|
||||
if text:
|
||||
estimated_tokens = int(len(text) * 0.7)
|
||||
# Cap estimate to max_tokens
|
||||
progress_total = min(estimated_tokens, max_new_tokens)
|
||||
else:
|
||||
progress_total = max_new_tokens
|
||||
|
||||
progress_bar = None
|
||||
try:
|
||||
import comfy.utils
|
||||
@@ -306,7 +316,7 @@ class StepAudioEditXEngineAdapter:
|
||||
# If no remaining text but still processing, show nothing
|
||||
return None
|
||||
|
||||
progress_bar = TimedProgressBar(max_new_tokens, job_tracker)
|
||||
progress_bar = TimedProgressBar(progress_total, job_tracker)
|
||||
except (ImportError, AttributeError):
|
||||
pass # ComfyUI progress not available
|
||||
|
||||
|
||||
@@ -526,9 +526,15 @@ class VibeVoiceEngineAdapter:
|
||||
|
||||
# Build speaker mapping and format text
|
||||
character_map = {}
|
||||
speaker_voices = []
|
||||
# Pre-fill speaker_voices with all 4 speaker slots (some may be None)
|
||||
speaker_voices = [
|
||||
speaker_inputs.get(1), # Speaker 1
|
||||
speaker_inputs.get(2), # Speaker 2
|
||||
speaker_inputs.get(3), # Speaker 3
|
||||
speaker_inputs.get(4) # Speaker 4
|
||||
]
|
||||
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]}")
|
||||
|
||||
@@ -542,18 +548,13 @@ class VibeVoiceEngineAdapter:
|
||||
if speaker_idx >= 4:
|
||||
speaker_idx = 3
|
||||
|
||||
# Get the appropriate voice based on speaker number
|
||||
voice = speaker_inputs.get(manual_speaker)
|
||||
# Voice already in pre-filled speaker_voices array
|
||||
voice = speaker_voices[speaker_idx]
|
||||
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:
|
||||
|
||||
@@ -44,7 +44,7 @@ CHATTERBOX_MODELS = {
|
||||
"Russian": {
|
||||
"repo": "niobures/Chatterbox-TTS",
|
||||
"format": "safetensors",
|
||||
"subdirectory": "ru",
|
||||
"subdirectory": "ru/chatterbox-ru-t3k",
|
||||
"description": "Russian model with training artifacts - Complete fine-tuned model"
|
||||
},
|
||||
"Armenian": {
|
||||
@@ -158,28 +158,39 @@ def get_model_files_for_language(language: str) -> Tuple[str, str]:
|
||||
return config.get("format", "pt"), config.get("repo")
|
||||
|
||||
def find_local_model_path(language: str) -> Optional[str]:
|
||||
"""Find local model path for a given language"""
|
||||
"""
|
||||
Find local model path for a given language.
|
||||
Respects extra_model_paths.yaml configuration.
|
||||
"""
|
||||
from utils.models.extra_paths import get_all_tts_model_paths
|
||||
|
||||
# Get model name (handle "local:" prefix)
|
||||
if language.startswith("local:"):
|
||||
local_name = language[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", local_name)
|
||||
else:
|
||||
model_path = os.path.join(folder_paths.models_dir, "chatterbox", local_name)
|
||||
if os.path.exists(model_path):
|
||||
return model_path
|
||||
model_name = language[6:]
|
||||
else:
|
||||
# Check if we have a local version of a predefined language
|
||||
# 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", language)
|
||||
else:
|
||||
model_path = os.path.join(folder_paths.models_dir, "chatterbox", language)
|
||||
model_name = language
|
||||
|
||||
# Search in all configured TTS model paths (respects extra_model_paths.yaml)
|
||||
try:
|
||||
all_tts_paths = get_all_tts_model_paths('TTS')
|
||||
|
||||
for base_path in all_tts_paths:
|
||||
# Check organized path (models/TTS/chatterbox/English)
|
||||
model_path = os.path.join(base_path, "chatterbox", model_name)
|
||||
if os.path.exists(model_path):
|
||||
return model_path
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback to hardcoded ComfyUI models_dir (legacy)
|
||||
for base_dir in ["TTS", ""]:
|
||||
if base_dir:
|
||||
model_path = os.path.join(folder_paths.models_dir, base_dir, "chatterbox", model_name)
|
||||
else:
|
||||
model_path = os.path.join(folder_paths.models_dir, "chatterbox", model_name)
|
||||
if os.path.exists(model_path):
|
||||
return model_path
|
||||
|
||||
return None
|
||||
|
||||
def detect_model_format(model_path: str) -> str:
|
||||
|
||||
@@ -58,14 +58,36 @@ OFFICIAL_23LANG_MODELS = {
|
||||
]
|
||||
},
|
||||
"multilingual": True
|
||||
},
|
||||
# Vietnamese community finetune by Dolly AI 23
|
||||
"Vietnamese (Viterbox)": {
|
||||
"repo": "dolly-vn/viterbox",
|
||||
"format": "as-is", # Use files exactly as specified (mixed .pt and .safetensors)
|
||||
"description": "Vietnamese-optimized ChatterBox finetune with expanded Vietnamese tokenization",
|
||||
"languages": [
|
||||
"vi", # Vietnamese (primary)
|
||||
"en", # English (secondary)
|
||||
],
|
||||
"required_files": {
|
||||
"v2": [ # Based on v2 architecture
|
||||
"t3_ml24ls_v2.safetensors", # Vietnamese-optimized T3 model (safetensors)
|
||||
"s3gen.pt", # S3Gen model (pt only)
|
||||
"ve.pt", # Voice encoder (pt only)
|
||||
"tokenizer_vi_expanded.json", # Vietnamese-expanded tokenizer
|
||||
"mtl_tokenizer.json", # Multilingual tokenizer (fallback)
|
||||
"conds.pt" # Conditioning (optional)
|
||||
]
|
||||
},
|
||||
"multilingual": False, # Primarily Vietnamese-focused
|
||||
"community_finetune": True
|
||||
}
|
||||
}
|
||||
|
||||
# Language code to display name mapping
|
||||
# Language code to display name mapping
|
||||
SUPPORTED_LANGUAGES = {
|
||||
"ar": "Arabic",
|
||||
"da": "Danish",
|
||||
"de": "German",
|
||||
"de": "German",
|
||||
"el": "Greek",
|
||||
"en": "English",
|
||||
"es": "Spanish",
|
||||
@@ -86,46 +108,53 @@ SUPPORTED_LANGUAGES = {
|
||||
"sw": "Swahili",
|
||||
"tr": "Turkish",
|
||||
"zh": "Chinese",
|
||||
"vi": "Vietnamese (Viterbox only)",
|
||||
}
|
||||
|
||||
def get_official_23lang_models() -> List[str]:
|
||||
"""
|
||||
Get list of available official 23-lang models.
|
||||
This implementation only supports the unified multilingual model.
|
||||
Supports both predefined models and auto-discovered local models.
|
||||
"""
|
||||
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
|
||||
|
||||
# Support flexible T3 model filename detection (t3_*.safetensors pattern)
|
||||
has_model = False
|
||||
item_files = os.listdir(item_path)
|
||||
|
||||
# Check for essential components
|
||||
has_s3gen = "s3gen.pt" in item_files
|
||||
has_ve = "ve.pt" in item_files
|
||||
has_tokenizer = any(f in item_files for f in ["mtl_tokenizer.json", "tokenizer_vi_expanded.json"])
|
||||
|
||||
# Check for T3 model file with flexible pattern matching
|
||||
# Supports: t3_23lang.safetensors, t3_mtl23ls_v2.safetensors, t3_ml24ls_v2.safetensors, etc.
|
||||
has_t3 = any(f.startswith("t3_") and f.endswith(".safetensors") for f in item_files)
|
||||
|
||||
has_model = has_s3gen and has_ve and has_tokenizer and has_t3
|
||||
|
||||
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]:
|
||||
|
||||
@@ -273,39 +273,95 @@ class ChatterboxOfficial23LangTTS:
|
||||
raise FileNotFoundError(f"Voice encoder not found: tried {ve_safetensors_path} and {ve_pt_path}")
|
||||
|
||||
ve.to(actual_device).eval()
|
||||
|
||||
# Load T3 multilingual model based on version
|
||||
print(f"📦 Loading T3 23-Lang {model_version} model...")
|
||||
if model_version == "v2":
|
||||
t3_path = ckpt_dir / "t3_mtl23ls_v2.safetensors"
|
||||
|
||||
# Normalize model_version for file lookups (Vietnamese (Viterbox) -> v2)
|
||||
version_for_files = "v2" if model_version == "Vietnamese (Viterbox)" else model_version
|
||||
|
||||
# Load version-specific multilingual tokenizer FIRST to get vocab size
|
||||
print("📦 Loading multilingual tokenizer...")
|
||||
tokenizer_path = None
|
||||
|
||||
if version_for_files == "v2":
|
||||
# Try v2 enhanced tokenizer first
|
||||
candidate_path = ckpt_dir / "grapheme_mtl_merged_expanded_v1.json"
|
||||
if candidate_path.exists():
|
||||
tokenizer_path = candidate_path
|
||||
else:
|
||||
# Try Vietnamese-expanded tokenizer (community finetunes)
|
||||
candidate_path = ckpt_dir / "tokenizer_vi_expanded.json"
|
||||
if candidate_path.exists():
|
||||
print("🌐 Using Vietnamese-expanded tokenizer")
|
||||
tokenizer_path = candidate_path
|
||||
else:
|
||||
# Fallback to v1 tokenizer
|
||||
print("⚠️ v2 tokenizer not found, falling back to v1 tokenizer")
|
||||
tokenizer_path = ckpt_dir / "mtl_tokenizer.json"
|
||||
else:
|
||||
# v1 model - try standard tokenizer
|
||||
tokenizer_path = ckpt_dir / "mtl_tokenizer.json"
|
||||
|
||||
if not tokenizer_path.exists():
|
||||
raise FileNotFoundError(f"Multilingual tokenizer not found: {tokenizer_path}")
|
||||
|
||||
tokenizer = MTLTokenizer(str(tokenizer_path))
|
||||
|
||||
# Get actual vocab size from tokenizer for T3 config
|
||||
vocab = tokenizer.tokenizer.get_vocab()
|
||||
actual_vocab_size = len(vocab)
|
||||
print(f"📊 Detected tokenizer vocab size: {actual_vocab_size}")
|
||||
|
||||
# Load T3 multilingual model based on version with flexible filename detection
|
||||
print(f"📦 Loading T3 23-Lang {model_version} model...")
|
||||
|
||||
# Support multiple T3 filename patterns:
|
||||
# - Official v1: t3_23lang.safetensors
|
||||
# - Official v2: t3_mtl23ls_v2.safetensors
|
||||
# - Vietnamese Viterbox: t3_ml24ls_v2.safetensors
|
||||
# - Future variants: any t3_*.safetensors
|
||||
t3_path = None
|
||||
if version_for_files == "v2":
|
||||
# Try specific v2 patterns first
|
||||
for pattern in ["t3_mtl23ls_v2.safetensors", "t3_ml24ls_v2.safetensors"]:
|
||||
candidate = ckpt_dir / pattern
|
||||
if candidate.exists():
|
||||
t3_path = candidate
|
||||
break
|
||||
|
||||
# Fallback: find any t3_*_v2.safetensors file
|
||||
if not t3_path:
|
||||
import glob
|
||||
matches = list(ckpt_dir.glob("t3_*_v2.safetensors"))
|
||||
if matches:
|
||||
t3_path = matches[0]
|
||||
else:
|
||||
# v1
|
||||
t3_path = ckpt_dir / "t3_23lang.safetensors"
|
||||
|
||||
if not t3_path.exists():
|
||||
raise FileNotFoundError(f"T3 multilingual {model_version} model not found: {t3_path}")
|
||||
|
||||
# Load T3 with version-specific multilingual config
|
||||
if not t3_path or not t3_path.exists():
|
||||
raise FileNotFoundError(f"T3 multilingual {model_version} model not found in {ckpt_dir}")
|
||||
|
||||
# Load T3 with actual vocab size from tokenizer (supports community finetunes)
|
||||
from .models.t3.t3 import T3Config
|
||||
config = T3Config.multilingual(version=model_version)
|
||||
config = T3Config(text_tokens_dict_size=actual_vocab_size)
|
||||
t3 = T3(config)
|
||||
|
||||
|
||||
t3_state = load_file(t3_path, device=actual_device)
|
||||
if "model" in t3_state.keys():
|
||||
t3_state = t3_state["model"][0]
|
||||
|
||||
|
||||
t3.load_state_dict(t3_state)
|
||||
t3.to(actual_device).eval()
|
||||
|
||||
|
||||
# Load S3Gen (try safetensors first, fallback to pt)
|
||||
print("📦 Loading S3Gen model...")
|
||||
s3gen_safetensors_path = ckpt_dir / "s3gen.safetensors"
|
||||
s3gen_pt_path = ckpt_dir / "s3gen.pt"
|
||||
|
||||
|
||||
s3gen = S3Gen()
|
||||
# Smart loading: prefer official .pt, but support user's safetensors
|
||||
has_safetensors = s3gen_safetensors_path.exists()
|
||||
has_pt = s3gen_pt_path.exists()
|
||||
|
||||
|
||||
if has_pt:
|
||||
# Use official .pt format (guaranteed compatibility)
|
||||
s3gen_state = torch.load(s3gen_pt_path, map_location=map_location, weights_only=True)
|
||||
@@ -317,25 +373,9 @@ class ChatterboxOfficial23LangTTS:
|
||||
s3gen.load_state_dict(s3gen_state, strict=False)
|
||||
else:
|
||||
raise FileNotFoundError(f"S3Gen model not found: tried {s3gen_safetensors_path} and {s3gen_pt_path}")
|
||||
|
||||
|
||||
s3gen.to(actual_device).eval()
|
||||
|
||||
# Load version-specific multilingual tokenizer
|
||||
print("📦 Loading multilingual tokenizer...")
|
||||
if model_version == "v2":
|
||||
tokenizer_path = ckpt_dir / "grapheme_mtl_merged_expanded_v1.json"
|
||||
if not tokenizer_path.exists():
|
||||
# Fallback to v1 tokenizer if v2 tokenizer not found
|
||||
print("⚠️ v2 tokenizer not found, falling back to v1 tokenizer")
|
||||
tokenizer_path = ckpt_dir / "mtl_tokenizer.json"
|
||||
else:
|
||||
tokenizer_path = ckpt_dir / "mtl_tokenizer.json"
|
||||
|
||||
if not tokenizer_path.exists():
|
||||
raise FileNotFoundError(f"Multilingual tokenizer not found: {tokenizer_path}")
|
||||
|
||||
tokenizer = MTLTokenizer(str(tokenizer_path))
|
||||
|
||||
# Load conditioning (optional)
|
||||
conds = None
|
||||
conds_path = ckpt_dir / "conds.pt"
|
||||
@@ -491,17 +531,21 @@ class ChatterboxOfficial23LangTTS:
|
||||
|
||||
repo_id = model_config.get("repo", REPO_ID)
|
||||
model_format = model_config.get("format", "pt")
|
||||
|
||||
|
||||
# Normalize model_version for file lookups
|
||||
# "Vietnamese (Viterbox)" is treated as v2 for file requirements
|
||||
version_for_files = "v2" if model_version == "Vietnamese (Viterbox)" else model_version
|
||||
|
||||
# Silent loading unless errors occur
|
||||
|
||||
|
||||
# Check if local model exists first
|
||||
model_dir = os.path.join(folder_paths.models_dir, "TTS", "chatterbox_official_23lang", model_name)
|
||||
if os.path.exists(model_dir):
|
||||
# Validate model completeness with version
|
||||
is_complete, missing_files = validate_model_completeness(model_dir, model_name, model_version)
|
||||
is_complete, missing_files = validate_model_completeness(model_dir, model_name, version_for_files)
|
||||
if is_complete:
|
||||
print(f"📁 Using existing local {model_version} model: {model_dir}")
|
||||
return cls.from_local(model_dir, device, model_name, model_version)
|
||||
return cls.from_local(model_dir, device, model_name, version_for_files)
|
||||
else:
|
||||
print(f"⚠️ Local {model_version} model incomplete, missing: {missing_files}")
|
||||
print(f"📥 Downloading missing {model_version} files...")
|
||||
@@ -510,10 +554,12 @@ class ChatterboxOfficial23LangTTS:
|
||||
from utils.downloads.unified_downloader import unified_downloader
|
||||
|
||||
# Get files to download based on model version
|
||||
files_to_download = get_model_requirements(model_name, model_version)
|
||||
files_to_download = get_model_requirements(model_name, version_for_files)
|
||||
|
||||
# Handle mixed format models (try safetensors first, fallback to pt)
|
||||
# Exception: conds.pt doesn't have a safetensors version, keep as .pt
|
||||
# Handle format conversion based on model_format
|
||||
# "as-is": Use files exactly as specified (for models with custom mixed formats like Vietnamese Viterbox)
|
||||
# "mixed": Try safetensors first, fallback to pt (for official models)
|
||||
# "pt": Force .pt format
|
||||
if model_format == "mixed":
|
||||
# Try safetensors first, but exclude files that only exist as .pt
|
||||
files_to_download = [
|
||||
@@ -522,6 +568,9 @@ class ChatterboxOfficial23LangTTS:
|
||||
]
|
||||
elif model_format == "pt":
|
||||
files_to_download = [f.replace('.safetensors', '.pt') if f.endswith('.safetensors') else f for f in files_to_download]
|
||||
elif model_format == "as-is":
|
||||
# Don't modify file extensions - use exactly as specified in registry
|
||||
pass
|
||||
|
||||
# Normalize model name for download path (strip "ChatterBox " prefix if present)
|
||||
# This ensures consistent paths: "ChatterBox Official 23-Lang" → "Official 23-Lang"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
CosyVoice3 Engine for TTS Audio Suite
|
||||
|
||||
Fun-CosyVoice3-0.5B integration providing:
|
||||
- 9-language zero-shot voice cloning
|
||||
- 18+ Chinese dialects support
|
||||
- Instruct mode for emotions, speed, and dialects
|
||||
- Cross-lingual voice cloning
|
||||
"""
|
||||
|
||||
from engines.cosyvoice.cosyvoice import CosyVoiceEngine
|
||||
|
||||
__all__ = ["CosyVoiceEngine"]
|
||||
@@ -0,0 +1,642 @@
|
||||
"""
|
||||
CosyVoice3 Engine Wrapper for TTS Audio Suite
|
||||
|
||||
Fun-CosyVoice3-0.5B integration providing:
|
||||
- 9-language zero-shot voice cloning (Chinese, English, Japanese, Korean, German, Spanish, French, Italian, Russian)
|
||||
- 18+ Chinese dialects support
|
||||
- Instruct mode for emotions, speed, and dialects
|
||||
- Cross-lingual voice cloning with fine-grained control
|
||||
"""
|
||||
|
||||
# FIX: PyYAML 6.0+ compatibility patch - MUST be before any yaml imports
|
||||
# Issue: 'Loader' object has no attribute 'max_depth' error (GitHub #220)
|
||||
# This patch ensures yaml loaders have max_depth attribute before hyperpyyaml uses them
|
||||
import yaml
|
||||
if not hasattr(yaml.Loader, 'max_depth'):
|
||||
yaml.Loader.max_depth = 100
|
||||
if not hasattr(yaml.FullLoader, 'max_depth'):
|
||||
yaml.FullLoader.max_depth = 100
|
||||
if not hasattr(yaml.SafeLoader, 'max_depth'):
|
||||
yaml.SafeLoader.max_depth = 100
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import torch
|
||||
import torchaudio
|
||||
import tempfile
|
||||
import folder_paths
|
||||
import numpy as np
|
||||
from typing import Optional, List, Dict, Any, Generator
|
||||
|
||||
from utils.models.unified_model_interface import unified_model_interface
|
||||
from utils.models.factory_config import ModelLoadConfig
|
||||
from utils.models.extra_paths import find_model_in_paths, get_preferred_download_path, get_all_tts_model_paths
|
||||
|
||||
|
||||
class CosyVoiceEngine:
|
||||
"""
|
||||
CosyVoice3 Engine wrapper for TTS Audio Suite integration.
|
||||
|
||||
Supports:
|
||||
- Zero-shot voice cloning (9 languages)
|
||||
- Instruct mode for dialects, emotions, speed
|
||||
- Cross-lingual voice cloning
|
||||
- Fine-grained control with [breath] and other tags
|
||||
"""
|
||||
|
||||
# Supported languages
|
||||
SUPPORTED_LANGUAGES = [
|
||||
"chinese", "english", "japanese", "korean",
|
||||
"german", "spanish", "french", "italian", "russian"
|
||||
]
|
||||
|
||||
# Supported Chinese dialects (for instruct mode)
|
||||
SUPPORTED_DIALECTS = [
|
||||
"guangdong", "minnan", "sichuan", "dongbei", "shan3xi", "shan1xi",
|
||||
"shanghai", "tianjin", "shandong", "ningxia", "gansu"
|
||||
]
|
||||
|
||||
# Generation modes
|
||||
MODES = ["zero_shot", "instruct", "cross_lingual"]
|
||||
|
||||
def __init__(self, model_dir: str = "Fun-CosyVoice3-0.5B-RL", device: str = "auto",
|
||||
use_fp16: bool = False, load_trt: bool = False, load_vllm: bool = False):
|
||||
"""
|
||||
Initialize CosyVoice3 engine.
|
||||
|
||||
Args:
|
||||
model_dir: Model identifier ("Fun-CosyVoice3-0.5B-RL", "Fun-CosyVoice3-0.5B", or "local:...")
|
||||
device: Device to use ("auto", "cuda", "cpu")
|
||||
use_fp16: Use FP16 for faster inference
|
||||
load_trt: Load TensorRT engine (optional optimization)
|
||||
load_vllm: Load vLLM engine (optional optimization)
|
||||
"""
|
||||
# Store original model_identifier to know which variant was requested
|
||||
self.model_identifier = model_dir
|
||||
|
||||
# Resolve model directory using extra_model_paths
|
||||
self.model_dir = self._find_model_directory(model_dir)
|
||||
|
||||
self.device = self._resolve_device(device)
|
||||
self.use_fp16 = use_fp16 and self.device != "cpu"
|
||||
self.load_trt = load_trt
|
||||
self.load_vllm = load_vllm
|
||||
|
||||
self._cosyvoice = None
|
||||
self._model_config = None
|
||||
self._quantized_move_warning_shown = False
|
||||
|
||||
def _find_model_directory(self, model_identifier: str) -> str:
|
||||
"""Find CosyVoice model directory using extra_model_paths configuration."""
|
||||
try:
|
||||
# Handle local: prefix
|
||||
if model_identifier.startswith("local:"):
|
||||
model_name = model_identifier[6:] # Remove "local:" prefix
|
||||
|
||||
# Both variants use the shared folder name
|
||||
folder_name = "Fun-CosyVoice3-0.5B"
|
||||
|
||||
# Search in all configured TTS paths
|
||||
all_tts_paths = get_all_tts_model_paths('TTS')
|
||||
for base_path in all_tts_paths:
|
||||
# Check direct path (models/TTS/Fun-CosyVoice3-0.5B)
|
||||
direct_path = os.path.join(base_path, folder_name)
|
||||
if os.path.exists(os.path.join(direct_path, "cosyvoice3.yaml")):
|
||||
return direct_path
|
||||
|
||||
# Check organized path (models/TTS/CosyVoice/Fun-CosyVoice3-0.5B)
|
||||
organized_path = os.path.join(base_path, "CosyVoice", folder_name)
|
||||
if os.path.exists(os.path.join(organized_path, "cosyvoice3.yaml")):
|
||||
return organized_path
|
||||
|
||||
raise FileNotFoundError(f"Local CosyVoice model '{model_name}' not found in any configured path")
|
||||
|
||||
else:
|
||||
# Auto-download case - return preferred download path with shared folder name
|
||||
base_path = get_preferred_download_path(model_type='TTS', engine_name='CosyVoice')
|
||||
# Both variants use the shared folder name
|
||||
folder_name = "Fun-CosyVoice3-0.5B"
|
||||
model_path = os.path.join(base_path, folder_name)
|
||||
|
||||
# Check if model exists and is complete, if not trigger auto-download
|
||||
needs_download = False
|
||||
if not os.path.exists(model_path):
|
||||
needs_download = True
|
||||
print(f"📥 CosyVoice3 model directory not found, triggering auto-download...")
|
||||
else:
|
||||
# Check model completeness - need to check which variant was requested
|
||||
try:
|
||||
from engines.cosyvoice.cosyvoice_downloader import CosyVoiceDownloader
|
||||
downloader = CosyVoiceDownloader()
|
||||
# Determine variant name for verification (need -RL suffix for RL variant)
|
||||
variant_name = "Fun-CosyVoice3-0.5B-RL" if "RL" in model_identifier else "Fun-CosyVoice3-0.5B"
|
||||
downloader._verify_model(model_path, variant_name, verbose=False)
|
||||
except Exception as verify_error:
|
||||
needs_download = True
|
||||
print(f"📥 CosyVoice3 model incomplete (missing files), triggering re-download...")
|
||||
print(f" Verification error: {verify_error}")
|
||||
|
||||
if needs_download:
|
||||
try:
|
||||
if 'downloader' not in locals():
|
||||
from engines.cosyvoice.cosyvoice_downloader import CosyVoiceDownloader
|
||||
downloader = CosyVoiceDownloader()
|
||||
# Determine variant name for download (need -RL suffix for RL variant)
|
||||
variant_name = "Fun-CosyVoice3-0.5B-RL" if "RL" in model_identifier else "Fun-CosyVoice3-0.5B"
|
||||
downloaded_path = downloader.download_model(variant_name)
|
||||
print(f"✅ CosyVoice3 auto-download completed: {downloaded_path}")
|
||||
return downloaded_path
|
||||
except Exception as download_error:
|
||||
raise RuntimeError(f"CosyVoice3 model not found/incomplete and auto-download failed: {download_error}")
|
||||
|
||||
return model_path
|
||||
|
||||
except Exception:
|
||||
# Fallback to default path
|
||||
model_name = model_identifier.replace("local:", "") if model_identifier.startswith("local:") else model_identifier
|
||||
return os.path.join(folder_paths.models_dir, "TTS", "CosyVoice", model_name)
|
||||
|
||||
def _resolve_device(self, device: str) -> str:
|
||||
"""Resolve device string to actual device."""
|
||||
from utils.device import resolve_torch_device
|
||||
resolved = resolve_torch_device(device)
|
||||
return resolved
|
||||
|
||||
def _ensure_model_loaded(self):
|
||||
"""Load the CosyVoice3 model using unified model interface."""
|
||||
if self._cosyvoice is not None:
|
||||
return
|
||||
|
||||
# Determine which LLM file to use based on model variant
|
||||
llm_filename = "llm.rl.pt" if "RL" in self.model_identifier else "llm.pt"
|
||||
|
||||
# Create model configuration
|
||||
self._model_config = ModelLoadConfig(
|
||||
engine_name="cosyvoice",
|
||||
model_type="tts",
|
||||
model_name="Fun-CosyVoice3-0.5B",
|
||||
device=self.device,
|
||||
model_path=self.model_dir,
|
||||
additional_params={
|
||||
"use_fp16": self.use_fp16,
|
||||
"load_trt": self.load_trt,
|
||||
"load_vllm": self.load_vllm,
|
||||
"llm_filename": llm_filename # Pass variant-specific LLM filename
|
||||
}
|
||||
)
|
||||
|
||||
# Load via unified interface
|
||||
self._cosyvoice = unified_model_interface.load_model(self._model_config)
|
||||
|
||||
def _ensure_device_loaded(self):
|
||||
"""
|
||||
Check and reload model if it was offloaded to CPU.
|
||||
|
||||
Delegates to the ComfyUI wrapper's model_load() method for proper device management.
|
||||
"""
|
||||
if self._cosyvoice is None:
|
||||
return
|
||||
|
||||
from utils.device import resolve_torch_device
|
||||
target_device = resolve_torch_device("auto")
|
||||
|
||||
# The unified interface returns a ComfyUIModelWrapper, not the raw model
|
||||
# Check if wrapper indicates model is not on GPU
|
||||
if hasattr(self._cosyvoice, '_is_loaded_on_gpu') and not self._cosyvoice._is_loaded_on_gpu:
|
||||
# Model was offloaded, use wrapper's model_load to properly reload
|
||||
if hasattr(self._cosyvoice, 'model_load'):
|
||||
self._cosyvoice.model_load(target_device)
|
||||
return
|
||||
|
||||
# Also check actual component devices as a safety check
|
||||
if hasattr(self._cosyvoice, 'model'):
|
||||
cosyvoice_model = self._cosyvoice.model
|
||||
|
||||
# Check llm component device
|
||||
if hasattr(cosyvoice_model, 'llm') and hasattr(cosyvoice_model.llm, 'parameters'):
|
||||
try:
|
||||
first_param = next(cosyvoice_model.llm.parameters())
|
||||
current_device = first_param.device
|
||||
|
||||
# Normalize device comparison (cuda:0 == cuda)
|
||||
if current_device.type != torch.device(target_device).type:
|
||||
# Device mismatch detected, trigger reload
|
||||
if hasattr(self._cosyvoice, 'model_load'):
|
||||
self._cosyvoice.model_load(target_device)
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
def generate_zero_shot(
|
||||
self,
|
||||
text: str,
|
||||
prompt_wav: str,
|
||||
prompt_text: str,
|
||||
speed: float = 1.0,
|
||||
stream: bool = False,
|
||||
progress_bar=None
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Generate speech using zero-shot voice cloning.
|
||||
|
||||
Args:
|
||||
text: Text to synthesize
|
||||
prompt_wav: Reference audio file for voice cloning
|
||||
prompt_text: Transcript of reference audio (REQUIRED)
|
||||
speed: Speech speed multiplier (0.5-2.0)
|
||||
stream: Enable streaming output
|
||||
progress_bar: ComfyUI progress bar for tracking
|
||||
|
||||
Returns:
|
||||
Generated audio as torch.Tensor [1, samples] at 24000 Hz
|
||||
"""
|
||||
self._ensure_model_loaded()
|
||||
self._ensure_device_loaded()
|
||||
|
||||
# CosyVoice3 REQUIRES the prompt_text to be in format:
|
||||
# 'You are a helpful assistant.<|endofprompt|>' + actual_transcript
|
||||
# This is critical for correct audio generation!
|
||||
formatted_prompt_text = prompt_text
|
||||
if not prompt_text.startswith('You are a helpful assistant.'):
|
||||
formatted_prompt_text = f'You are a helpful assistant.<|endofprompt|>{prompt_text}'
|
||||
elif '<|endofprompt|>' not in prompt_text:
|
||||
# Has prefix but missing delimiter
|
||||
formatted_prompt_text = prompt_text.replace('You are a helpful assistant.', 'You are a helpful assistant.<|endofprompt|>')
|
||||
|
||||
# Collect all audio chunks
|
||||
audio_chunks = []
|
||||
|
||||
# Detect if text contains CosyVoice tags (in [tag] format after adapter conversion)
|
||||
# If tags present, disable text_frontend to preserve them
|
||||
cosyvoice_tags = ['[breath]', '[quick_breath]', '[laughter]', '[cough]', '[sigh]',
|
||||
'[gasp]', '[noise]', '[hissing]', '[vocalized-noise]',
|
||||
'[lipsmack]', '[mn]', '[clucking]', '[accent]',
|
||||
'<strong>', '</strong>', '<laughter>', '</laughter>',
|
||||
'<|en|>', '<|zh|>', '<|ja|>', '<|ko|>']
|
||||
has_tags = any(tag in text for tag in cosyvoice_tags)
|
||||
text_frontend = not has_tags
|
||||
|
||||
# Display the actual text being sent to the model
|
||||
print("📝 CosyVoice3 - Generating zero_shot:")
|
||||
print("=" * 60)
|
||||
print(text)
|
||||
print("=" * 60)
|
||||
if has_tags:
|
||||
print(f"⚙️ text_frontend=False (special tags detected)")
|
||||
|
||||
generation_start = time.time()
|
||||
for i, output in enumerate(self._cosyvoice.inference_zero_shot(
|
||||
tts_text=text,
|
||||
prompt_text=formatted_prompt_text,
|
||||
prompt_wav=prompt_wav,
|
||||
stream=stream,
|
||||
speed=speed,
|
||||
text_frontend=text_frontend
|
||||
)):
|
||||
audio_chunk = output['tts_speech']
|
||||
audio_chunks.append(audio_chunk)
|
||||
|
||||
if progress_bar is not None:
|
||||
progress_bar.update(1)
|
||||
|
||||
# Combine all chunks
|
||||
if audio_chunks:
|
||||
audio_tensor = torch.cat(audio_chunks, dim=-1)
|
||||
else:
|
||||
# Return silence if no chunks
|
||||
audio_tensor = torch.zeros(1, self.get_sample_rate(), dtype=torch.float32)
|
||||
|
||||
# Report generation stats
|
||||
generation_time = time.time() - generation_start
|
||||
audio_duration = audio_tensor.shape[-1] / self.get_sample_rate()
|
||||
rtf = generation_time / audio_duration if audio_duration > 0 else 0
|
||||
print(f"✅ Generated {audio_duration:.1f}s audio in {generation_time:.1f}s (RTF: {rtf:.2f}x)")
|
||||
|
||||
# Normalize dimensions to [1, samples]
|
||||
audio_tensor = self._normalize_audio_dims(audio_tensor)
|
||||
|
||||
return audio_tensor
|
||||
|
||||
def generate_instruct(
|
||||
self,
|
||||
text: str,
|
||||
prompt_wav: str,
|
||||
instruct_text: str,
|
||||
speed: float = 1.0,
|
||||
stream: bool = False,
|
||||
progress_bar=None
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Generate speech using instruct mode for controlling emotions, dialects, speed.
|
||||
|
||||
Args:
|
||||
text: Text to synthesize
|
||||
prompt_wav: Reference audio file for voice cloning
|
||||
instruct_text: Instruction text (e.g., "请用广东话表达。<|endofprompt|>")
|
||||
speed: Speech speed multiplier (0.5-2.0)
|
||||
stream: Enable streaming output
|
||||
text_frontend: Use text normalization frontend
|
||||
progress_bar: ComfyUI progress bar for tracking
|
||||
|
||||
Returns:
|
||||
Generated audio as torch.Tensor [1, samples] at 24000 Hz
|
||||
"""
|
||||
self._ensure_model_loaded()
|
||||
self._ensure_device_loaded()
|
||||
|
||||
# CosyVoice3 instruct format MUST be: 'You are a helpful assistant. {instruction}<|endofprompt|>'
|
||||
# Example: 'You are a helpful assistant. 请用广东话表达。<|endofprompt|>'
|
||||
formatted_instruct = instruct_text
|
||||
if not instruct_text.endswith('<|endofprompt|>'):
|
||||
if instruct_text.startswith('You are a helpful assistant'):
|
||||
formatted_instruct = f'{instruct_text}<|endofprompt|>'
|
||||
else:
|
||||
formatted_instruct = f'You are a helpful assistant. {instruct_text}<|endofprompt|>'
|
||||
elif not instruct_text.startswith('You are a helpful assistant'):
|
||||
# Has <|endofprompt|> but missing prefix
|
||||
formatted_instruct = f'You are a helpful assistant. {instruct_text}'
|
||||
|
||||
# Collect all audio chunks
|
||||
audio_chunks = []
|
||||
|
||||
# Detect if text contains CosyVoice tags - disable text_frontend to preserve them
|
||||
cosyvoice_tags = ['[breath]', '[quick_breath]', '[laughter]', '[cough]', '[sigh]',
|
||||
'[gasp]', '[noise]', '[hissing]', '[vocalized-noise]',
|
||||
'[lipsmack]', '[mn]', '[clucking]', '[accent]',
|
||||
'<strong>', '</strong>', '<laughter>', '</laughter>',
|
||||
'<|en|>', '<|zh|>', '<|ja|>', '<|ko|>']
|
||||
has_tags = any(tag in text for tag in cosyvoice_tags)
|
||||
text_frontend = not has_tags
|
||||
|
||||
# Display the actual text being sent to the model
|
||||
print("📝 CosyVoice3 - Generating instruct2:")
|
||||
print("=" * 60)
|
||||
print(text)
|
||||
print("=" * 60)
|
||||
if has_tags:
|
||||
print(f"⚙️ text_frontend=False (special tags detected)")
|
||||
|
||||
generation_start = time.time()
|
||||
for i, output in enumerate(self._cosyvoice.inference_instruct2(
|
||||
tts_text=text,
|
||||
instruct_text=formatted_instruct,
|
||||
prompt_wav=prompt_wav,
|
||||
stream=stream,
|
||||
speed=speed,
|
||||
text_frontend=text_frontend
|
||||
)):
|
||||
audio_chunk = output['tts_speech']
|
||||
audio_chunks.append(audio_chunk)
|
||||
|
||||
if progress_bar is not None:
|
||||
progress_bar.update(1)
|
||||
|
||||
# Combine all chunks
|
||||
if audio_chunks:
|
||||
audio_tensor = torch.cat(audio_chunks, dim=-1)
|
||||
else:
|
||||
audio_tensor = torch.zeros(1, self.get_sample_rate(), dtype=torch.float32)
|
||||
|
||||
# Report generation stats
|
||||
generation_time = time.time() - generation_start
|
||||
audio_duration = audio_tensor.shape[-1] / self.get_sample_rate()
|
||||
rtf = generation_time / audio_duration if audio_duration > 0 else 0
|
||||
print(f"✅ Generated {audio_duration:.1f}s audio in {generation_time:.1f}s (RTF: {rtf:.2f}x)")
|
||||
|
||||
# Normalize dimensions
|
||||
audio_tensor = self._normalize_audio_dims(audio_tensor)
|
||||
|
||||
return audio_tensor
|
||||
|
||||
def generate_cross_lingual(
|
||||
self,
|
||||
text: str,
|
||||
prompt_wav: str,
|
||||
speed: float = 1.0,
|
||||
stream: bool = False,
|
||||
progress_bar=None
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Generate speech using cross-lingual mode with fine-grained control.
|
||||
|
||||
Supports embedded control tags like [breath] in the text.
|
||||
|
||||
Args:
|
||||
text: Text to synthesize (can include [breath] and other tags)
|
||||
prompt_wav: Reference audio file for voice cloning
|
||||
speed: Speech speed multiplier (0.5-2.0)
|
||||
stream: Enable streaming output
|
||||
text_frontend: Use text normalization frontend
|
||||
progress_bar: ComfyUI progress bar for tracking
|
||||
|
||||
Returns:
|
||||
Generated audio as torch.Tensor [1, samples] at 24000 Hz
|
||||
"""
|
||||
self._ensure_model_loaded()
|
||||
self._ensure_device_loaded()
|
||||
|
||||
# CosyVoice3 cross_lingual also requires the system prompt prefix in the text
|
||||
# Example: 'You are a helpful assistant.<|endofprompt|>[breath]...'
|
||||
formatted_text = text
|
||||
if not text.startswith('You are a helpful assistant.'):
|
||||
formatted_text = f'You are a helpful assistant.<|endofprompt|>{text}'
|
||||
elif '<|endofprompt|>' not in text:
|
||||
formatted_text = text.replace('You are a helpful assistant.', 'You are a helpful assistant.<|endofprompt|>')
|
||||
|
||||
# Collect all audio chunks
|
||||
audio_chunks = []
|
||||
|
||||
# Detect if text contains CosyVoice tags - disable text_frontend to preserve them
|
||||
cosyvoice_tags = ['[breath]', '[quick_breath]', '[laughter]', '[cough]', '[sigh]',
|
||||
'[gasp]', '[noise]', '[hissing]', '[vocalized-noise]',
|
||||
'[lipsmack]', '[mn]', '[clucking]', '[accent]',
|
||||
'<strong>', '</strong>', '<laughter>', '</laughter>',
|
||||
'<|en|>', '<|zh|>', '<|ja|>', '<|ko|>']
|
||||
has_tags = any(tag in formatted_text for tag in cosyvoice_tags)
|
||||
text_frontend = not has_tags
|
||||
|
||||
# Display the actual text being sent to the model
|
||||
print("📝 CosyVoice3 - Generating cross_lingual:")
|
||||
print("=" * 60)
|
||||
print(text)
|
||||
print("=" * 60)
|
||||
if has_tags:
|
||||
print(f"⚙️ text_frontend=False (special tags detected)")
|
||||
|
||||
generation_start = time.time()
|
||||
for i, output in enumerate(self._cosyvoice.inference_cross_lingual(
|
||||
tts_text=formatted_text,
|
||||
prompt_wav=prompt_wav,
|
||||
stream=stream,
|
||||
speed=speed,
|
||||
text_frontend=text_frontend
|
||||
)):
|
||||
audio_chunk = output['tts_speech']
|
||||
audio_chunks.append(audio_chunk)
|
||||
|
||||
if progress_bar is not None:
|
||||
progress_bar.update(1)
|
||||
|
||||
# Combine all chunks
|
||||
if audio_chunks:
|
||||
audio_tensor = torch.cat(audio_chunks, dim=-1)
|
||||
else:
|
||||
audio_tensor = torch.zeros(1, self.get_sample_rate(), dtype=torch.float32)
|
||||
|
||||
# Report generation stats
|
||||
generation_time = time.time() - generation_start
|
||||
audio_duration = audio_tensor.shape[-1] / self.get_sample_rate()
|
||||
rtf = generation_time / audio_duration if audio_duration > 0 else 0
|
||||
print(f"✅ Generated {audio_duration:.1f}s audio in {generation_time:.1f}s (RTF: {rtf:.2f}x)")
|
||||
|
||||
# Normalize dimensions
|
||||
audio_tensor = self._normalize_audio_dims(audio_tensor)
|
||||
|
||||
return audio_tensor
|
||||
|
||||
def generate(
|
||||
self,
|
||||
text: str,
|
||||
prompt_wav: str,
|
||||
prompt_text: Optional[str] = None,
|
||||
mode: str = "zero_shot",
|
||||
instruct_text: Optional[str] = None,
|
||||
speed: float = 1.0,
|
||||
stream: bool = False,
|
||||
progress_bar=None,
|
||||
**kwargs
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Unified generation method that routes to appropriate mode.
|
||||
|
||||
Args:
|
||||
text: Text to synthesize
|
||||
prompt_wav: Reference audio file for voice cloning
|
||||
prompt_text: Transcript of reference audio (required for zero_shot mode)
|
||||
mode: Generation mode ("zero_shot", "instruct", "cross_lingual")
|
||||
instruct_text: Instruction for instruct mode
|
||||
speed: Speech speed multiplier (0.5-2.0)
|
||||
stream: Enable streaming output
|
||||
progress_bar: ComfyUI progress bar for tracking
|
||||
**kwargs: Additional parameters (ignored for compatibility)
|
||||
|
||||
Returns:
|
||||
Generated audio as torch.Tensor [1, samples] at 24000 Hz
|
||||
"""
|
||||
if mode == "zero_shot":
|
||||
if not prompt_text:
|
||||
raise ValueError("prompt_text is required for zero_shot mode. "
|
||||
"Provide a transcript of the reference audio.")
|
||||
return self.generate_zero_shot(
|
||||
text=text,
|
||||
prompt_wav=prompt_wav,
|
||||
prompt_text=prompt_text,
|
||||
speed=speed,
|
||||
stream=stream,
|
||||
progress_bar=progress_bar
|
||||
)
|
||||
elif mode == "instruct":
|
||||
if not instruct_text:
|
||||
instruct_text = "You are a helpful assistant.<|endofprompt|>"
|
||||
return self.generate_instruct(
|
||||
text=text,
|
||||
prompt_wav=prompt_wav,
|
||||
instruct_text=instruct_text,
|
||||
speed=speed,
|
||||
stream=stream,
|
||||
progress_bar=progress_bar
|
||||
)
|
||||
elif mode == "cross_lingual":
|
||||
return self.generate_cross_lingual(
|
||||
text=text,
|
||||
prompt_wav=prompt_wav,
|
||||
speed=speed,
|
||||
stream=stream,
|
||||
progress_bar=progress_bar
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown mode: {mode}. Supported: {self.MODES}")
|
||||
|
||||
def _normalize_audio_dims(self, audio_tensor: torch.Tensor) -> torch.Tensor:
|
||||
"""Normalize audio tensor to [1, samples] format."""
|
||||
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]
|
||||
return audio_tensor
|
||||
|
||||
def get_sample_rate(self) -> int:
|
||||
"""Get the native sample rate of the engine."""
|
||||
# CosyVoice3 uses 24000 Hz (from cosyvoice3.yaml)
|
||||
if self._cosyvoice is not None and hasattr(self._cosyvoice, 'sample_rate'):
|
||||
return self._cosyvoice.sample_rate
|
||||
return 24000
|
||||
|
||||
def get_supported_languages(self) -> List[str]:
|
||||
"""Get list of supported languages."""
|
||||
return self.SUPPORTED_LANGUAGES.copy()
|
||||
|
||||
def get_supported_dialects(self) -> List[str]:
|
||||
"""Get list of supported Chinese dialects."""
|
||||
return self.SUPPORTED_DIALECTS.copy()
|
||||
|
||||
def inference_vc(self,
|
||||
source_wav: str,
|
||||
prompt_wav: str,
|
||||
speed: float = 1.0,
|
||||
stream: bool = False) -> torch.Tensor:
|
||||
"""
|
||||
Voice conversion using CosyVoice3's native VC capability.
|
||||
|
||||
Args:
|
||||
source_wav: Source audio file path (CosyVoice handles resampling to 16kHz)
|
||||
prompt_wav: Reference/target voice file path
|
||||
speed: Speech speed multiplier
|
||||
stream: Enable streaming (not implemented for VC)
|
||||
|
||||
Returns:
|
||||
Converted audio tensor at 24kHz
|
||||
"""
|
||||
self._ensure_model_loaded()
|
||||
self._ensure_device_loaded()
|
||||
|
||||
# Call CosyVoice's inference_vc method
|
||||
# Returns generator, collect all output
|
||||
audio_chunks = []
|
||||
for output in self._cosyvoice.inference_vc(
|
||||
source_wav=source_wav,
|
||||
prompt_wav=prompt_wav,
|
||||
stream=stream,
|
||||
speed=speed
|
||||
):
|
||||
audio_chunks.append(output['tts_speech'])
|
||||
|
||||
# Concatenate all chunks
|
||||
if audio_chunks:
|
||||
audio = torch.cat(audio_chunks, dim=-1)
|
||||
else:
|
||||
audio = torch.zeros(1, 24000, dtype=torch.float32)
|
||||
|
||||
return audio
|
||||
|
||||
def to(self, device):
|
||||
"""
|
||||
Move all model components to the specified device.
|
||||
|
||||
Delegates to the CosyVoiceHandler via the ComfyUI wrapper for proper
|
||||
component-level device management.
|
||||
"""
|
||||
self.device = device
|
||||
|
||||
# Delegate to the ComfyUI wrapper's model_load which uses CosyVoiceHandler
|
||||
if self._cosyvoice is not None and hasattr(self._cosyvoice, 'model_load'):
|
||||
self._cosyvoice.model_load(device)
|
||||
|
||||
return self
|
||||
|
||||
def unload(self):
|
||||
"""Unload the model to free memory."""
|
||||
if self._model_config:
|
||||
unified_model_interface.unload_model(self._model_config)
|
||||
self._cosyvoice = None
|
||||
self._model_config = None
|
||||
@@ -0,0 +1,285 @@
|
||||
"""
|
||||
CosyVoice3 Model Downloader
|
||||
|
||||
Handles automatic download and setup of Fun-CosyVoice3-0.5B models using the unified download system.
|
||||
Downloads models to organized TTS/CosyVoice/ structure.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
# 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 utils.downloads.unified_downloader import unified_downloader
|
||||
from utils.models.extra_paths import get_preferred_download_path
|
||||
import folder_paths
|
||||
|
||||
|
||||
class CosyVoiceDownloader:
|
||||
"""Downloader for CosyVoice3 models using unified download system."""
|
||||
|
||||
# Shared files across all variants (downloaded once)
|
||||
SHARED_FILES = [
|
||||
"cosyvoice3.yaml",
|
||||
"campplus.onnx",
|
||||
"flow.pt",
|
||||
"hift.pt",
|
||||
"speech_tokenizer_v3.onnx",
|
||||
# CosyVoice-BlankEN subfolder (Qwen model for text processing)
|
||||
"CosyVoice-BlankEN/config.json",
|
||||
"CosyVoice-BlankEN/generation_config.json",
|
||||
"CosyVoice-BlankEN/merges.txt",
|
||||
"CosyVoice-BlankEN/model.safetensors",
|
||||
"CosyVoice-BlankEN/tokenizer_config.json",
|
||||
"CosyVoice-BlankEN/vocab.json",
|
||||
]
|
||||
|
||||
MODELS = {
|
||||
"Fun-CosyVoice3-0.5B": {
|
||||
"repo_id": "FunAudioLLM/Fun-CosyVoice3-0.5B-2512",
|
||||
"llm_file": "llm.pt", # Standard LLM model (2GB)
|
||||
"description": "Fun-CosyVoice3 0.5B base model (~5.4GB total)"
|
||||
},
|
||||
"Fun-CosyVoice3-0.5B-RL": {
|
||||
"repo_id": "FunAudioLLM/Fun-CosyVoice3-0.5B-2512",
|
||||
"llm_file": "llm.rl.pt", # RL-enhanced LLM model (2GB, better quality)
|
||||
"description": "Fun-CosyVoice3 0.5B RL-enhanced model (better quality) (~5.4GB total)"
|
||||
}
|
||||
}
|
||||
|
||||
# Supported languages
|
||||
SUPPORTED_LANGUAGES = [
|
||||
"chinese", "english", "japanese", "korean",
|
||||
"german", "spanish", "french", "italian", "russian"
|
||||
]
|
||||
|
||||
# Supported Chinese dialects
|
||||
SUPPORTED_DIALECTS = [
|
||||
"guangdong", "minnan", "sichuan", "dongbei", "shan3xi", "shan1xi",
|
||||
"shanghai", "tianjin", "shandong", "ningxia", "gansu"
|
||||
]
|
||||
|
||||
def __init__(self, base_path: Optional[str] = None):
|
||||
"""
|
||||
Initialize downloader.
|
||||
|
||||
Args:
|
||||
base_path: Base directory for CosyVoice models (auto-detected if None)
|
||||
"""
|
||||
if base_path is None:
|
||||
# Use extra_model_paths configuration for downloads
|
||||
try:
|
||||
self.base_path = get_preferred_download_path(model_type='TTS', engine_name='CosyVoice')
|
||||
except Exception:
|
||||
# Fallback to default if extra_paths fails
|
||||
self.base_path = os.path.join(folder_paths.models_dir, "TTS", "CosyVoice")
|
||||
else:
|
||||
self.base_path = base_path
|
||||
|
||||
self.downloader = unified_downloader
|
||||
|
||||
def download_model(self,
|
||||
model_name: str = "Fun-CosyVoice3-0.5B",
|
||||
force_download: bool = False,
|
||||
**kwargs) -> str:
|
||||
"""
|
||||
Download CosyVoice3 model.
|
||||
|
||||
Args:
|
||||
model_name: Model to download ("Fun-CosyVoice3-0.5B" or "Fun-CosyVoice3-0.5B-RL")
|
||||
force_download: Force re-download even if exists
|
||||
**kwargs: Additional download options
|
||||
|
||||
Returns:
|
||||
Path to downloaded model directory
|
||||
|
||||
Raises:
|
||||
ValueError: If model_name is not supported
|
||||
RuntimeError: If download fails
|
||||
"""
|
||||
if model_name not in self.MODELS:
|
||||
raise ValueError(f"Unknown model: {model_name}. Available: {list(self.MODELS.keys())}")
|
||||
|
||||
model_info = self.MODELS[model_name]
|
||||
|
||||
# Both variants use the same folder (like ChatterBox 23-Lang v1/v2)
|
||||
# They share all files except the LLM file (llm.pt vs llm.rl.pt)
|
||||
folder_name = "Fun-CosyVoice3-0.5B"
|
||||
model_path = os.path.join(self.base_path, folder_name)
|
||||
|
||||
# Check if model already exists
|
||||
if not force_download and os.path.exists(model_path):
|
||||
try:
|
||||
self._verify_model(model_path, model_name, verbose=False)
|
||||
# Model already exists and is valid, skip download
|
||||
return model_path
|
||||
except RuntimeError:
|
||||
# Model exists but is invalid, proceed with download
|
||||
pass
|
||||
|
||||
print(f"📥 Downloading CosyVoice3 model: {model_name}")
|
||||
print(f"📁 Target directory: {model_path}")
|
||||
|
||||
try:
|
||||
# Prepare file list: shared files + variant-specific LLM
|
||||
file_list = []
|
||||
|
||||
# Add shared files
|
||||
for file_pattern in self.SHARED_FILES:
|
||||
file_list.append({
|
||||
'remote': file_pattern,
|
||||
'local': file_pattern
|
||||
})
|
||||
|
||||
# Add variant-specific LLM file
|
||||
llm_file = model_info["llm_file"]
|
||||
file_list.append({
|
||||
'remote': llm_file,
|
||||
'local': llm_file # Keep original name (llm.pt or llm.rl.pt)
|
||||
})
|
||||
|
||||
# Download model files using unified downloader
|
||||
result_path = self.downloader.download_huggingface_model(
|
||||
repo_id=model_info["repo_id"],
|
||||
model_name=folder_name,
|
||||
files=file_list,
|
||||
engine_type="CosyVoice",
|
||||
**kwargs
|
||||
)
|
||||
|
||||
if not result_path:
|
||||
raise RuntimeError("HuggingFace download failed")
|
||||
|
||||
# Use the path returned by the unified downloader
|
||||
model_path = result_path
|
||||
|
||||
# Verify essential files
|
||||
self._verify_model(model_path, model_name)
|
||||
|
||||
print(f"✅ {model_name} model downloaded successfully")
|
||||
print(f"📁 Model path: {model_path}")
|
||||
|
||||
return model_path
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to download CosyVoice3 model: {e}")
|
||||
|
||||
def _verify_model(self, model_path: str, model_name: str = "Fun-CosyVoice3-0.5B", verbose: bool = True) -> None:
|
||||
"""
|
||||
Verify downloaded model has all required files.
|
||||
|
||||
Args:
|
||||
model_path: Path to model directory
|
||||
model_name: Model name to get LLM file requirement
|
||||
verbose: Print verification messages (default True)
|
||||
|
||||
Raises:
|
||||
RuntimeError: If verification fails
|
||||
"""
|
||||
# Essential shared files (common to all variants)
|
||||
missing_files = []
|
||||
for file in self.SHARED_FILES:
|
||||
if not os.path.exists(os.path.join(model_path, file)):
|
||||
missing_files.append(file)
|
||||
|
||||
# Check for variant-specific LLM file
|
||||
if model_name in self.MODELS:
|
||||
required_llm = self.MODELS[model_name]["llm_file"]
|
||||
llm_path = os.path.join(model_path, required_llm)
|
||||
if not os.path.exists(llm_path):
|
||||
missing_files.append(required_llm)
|
||||
|
||||
if missing_files:
|
||||
raise RuntimeError(
|
||||
f"CosyVoice3 model verification failed. Missing files: {missing_files}"
|
||||
)
|
||||
|
||||
if verbose:
|
||||
print(f"✅ Model verification passed")
|
||||
|
||||
def is_model_available(self, model_name: str = "Fun-CosyVoice3-0.5B-RL") -> bool:
|
||||
"""
|
||||
Check if model is already downloaded.
|
||||
|
||||
Args:
|
||||
model_name: Model name to check
|
||||
|
||||
Returns:
|
||||
True if model is available locally
|
||||
"""
|
||||
# Both variants use the same folder
|
||||
folder_name = "Fun-CosyVoice3-0.5B"
|
||||
model_path = os.path.join(self.base_path, folder_name)
|
||||
|
||||
try:
|
||||
self._verify_model(model_path, model_name, verbose=False)
|
||||
return True
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
def get_model_info(self, model_name: str = "Fun-CosyVoice3-0.5B") -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get information about a model.
|
||||
|
||||
Args:
|
||||
model_name: Model name
|
||||
|
||||
Returns:
|
||||
Model information dictionary or None if not found
|
||||
"""
|
||||
return self.MODELS.get(model_name)
|
||||
|
||||
def list_available_models(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Get list of all available models."""
|
||||
return self.MODELS.copy()
|
||||
|
||||
def get_model_path(self, model_name: str = "Fun-CosyVoice3-0.5B-RL") -> str:
|
||||
"""
|
||||
Get the local path for a model.
|
||||
|
||||
Args:
|
||||
model_name: Model name (variant doesn't matter, both use same folder)
|
||||
|
||||
Returns:
|
||||
Local model path
|
||||
"""
|
||||
# Both variants use the same folder
|
||||
folder_name = "Fun-CosyVoice3-0.5B"
|
||||
return os.path.join(self.base_path, folder_name)
|
||||
|
||||
|
||||
# Global downloader instance
|
||||
cosyvoice_downloader = CosyVoiceDownloader()
|
||||
|
||||
|
||||
def download_cosyvoice_model(model_name: str = "Fun-CosyVoice3-0.5B-RL",
|
||||
force_download: bool = False,
|
||||
**kwargs) -> str:
|
||||
"""
|
||||
Convenience function to download CosyVoice3 model.
|
||||
|
||||
Args:
|
||||
model_name: Model to download
|
||||
force_download: Force re-download
|
||||
**kwargs: Additional options
|
||||
|
||||
Returns:
|
||||
Path to downloaded model
|
||||
"""
|
||||
return cosyvoice_downloader.download_model(
|
||||
model_name=model_name,
|
||||
force_download=force_download,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def is_cosyvoice_available(model_name: str = "Fun-CosyVoice3-0.5B-RL") -> bool:
|
||||
"""Check if CosyVoice3 model is available locally."""
|
||||
return cosyvoice_downloader.is_model_available(model_name)
|
||||
@@ -0,0 +1,93 @@
|
||||
# Copyright (c) 2020 Mobvoi Inc (Di Wu)
|
||||
# Copyright (c) 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 os
|
||||
import argparse
|
||||
import glob
|
||||
|
||||
import yaml
|
||||
import torch
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(description='average model')
|
||||
parser.add_argument('--dst_model', required=True, help='averaged model')
|
||||
parser.add_argument('--src_path',
|
||||
required=True,
|
||||
help='src model path for average')
|
||||
parser.add_argument('--val_best',
|
||||
action="store_true",
|
||||
help='averaged model')
|
||||
parser.add_argument('--num',
|
||||
default=5,
|
||||
type=int,
|
||||
help='nums for averaged model')
|
||||
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
val_scores = []
|
||||
if args.val_best:
|
||||
yamls = glob.glob('{}/*.yaml'.format(args.src_path))
|
||||
yamls = [
|
||||
f for f in yamls
|
||||
if not (os.path.basename(f).startswith('train')
|
||||
or os.path.basename(f).startswith('init'))
|
||||
]
|
||||
for y in yamls:
|
||||
with open(y, 'r') as f:
|
||||
dic_yaml = yaml.load(f, Loader=yaml.BaseLoader)
|
||||
loss = float(dic_yaml['loss_dict']['loss'])
|
||||
epoch = int(dic_yaml['epoch'])
|
||||
step = int(dic_yaml['step'])
|
||||
tag = dic_yaml['tag']
|
||||
val_scores += [[epoch, step, loss, tag]]
|
||||
sorted_val_scores = sorted(val_scores,
|
||||
key=lambda x: x[2],
|
||||
reverse=False)
|
||||
print("best val (epoch, step, loss, tag) = " +
|
||||
str(sorted_val_scores[:args.num]))
|
||||
path_list = [
|
||||
args.src_path + '/epoch_{}_whole.pt'.format(score[0])
|
||||
for score in sorted_val_scores[:args.num]
|
||||
]
|
||||
print(path_list)
|
||||
avg = {}
|
||||
num = args.num
|
||||
assert num == len(path_list)
|
||||
for path in path_list:
|
||||
print('Processing {}'.format(path))
|
||||
states = torch.load(path, map_location=torch.device('cpu'))
|
||||
for k in states.keys():
|
||||
if k not in ['step', 'epoch']:
|
||||
if k not in avg.keys():
|
||||
avg[k] = states[k].clone()
|
||||
else:
|
||||
avg[k] += states[k]
|
||||
# average
|
||||
for k in avg.keys():
|
||||
if avg[k] is not None:
|
||||
# pytorch 1.6 use true_divide instead of /=
|
||||
avg[k] = torch.true_divide(avg[k], num)
|
||||
print('Saving to {}'.format(args.dst_model))
|
||||
torch.save(avg, args.dst_model)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,99 @@
|
||||
# Copyright (c) 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.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
logging.getLogger('matplotlib').setLevel(logging.WARNING)
|
||||
import os
|
||||
import sys
|
||||
import torch
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append('{}/../..'.format(ROOT_DIR))
|
||||
sys.path.append('{}/../../third_party/Matcha-TTS'.format(ROOT_DIR))
|
||||
from cosyvoice.cli.cosyvoice import AutoModel
|
||||
from cosyvoice.utils.file_utils import logging
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(description='export your model for deployment')
|
||||
parser.add_argument('--model_dir',
|
||||
type=str,
|
||||
default='pretrained_models/CosyVoice-300M',
|
||||
help='local path')
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
return args
|
||||
|
||||
|
||||
def get_optimized_script(model, preserved_attrs=[]):
|
||||
script = torch.jit.script(model)
|
||||
if preserved_attrs != []:
|
||||
script = torch.jit.freeze(script, preserved_attrs=preserved_attrs)
|
||||
else:
|
||||
script = torch.jit.freeze(script)
|
||||
script = torch.jit.optimize_for_inference(script)
|
||||
return script
|
||||
|
||||
|
||||
def main():
|
||||
args = get_args()
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s %(levelname)s %(message)s')
|
||||
|
||||
torch._C._jit_set_fusion_strategy([('STATIC', 1)])
|
||||
torch._C._jit_set_profiling_mode(False)
|
||||
torch._C._jit_set_profiling_executor(False)
|
||||
|
||||
model = AutoModel(model_dir=args.model_dir)
|
||||
|
||||
if model.__class__.__name__ == 'CosyVoice':
|
||||
# 1. export llm text_encoder
|
||||
llm_text_encoder = model.model.llm.text_encoder
|
||||
script = get_optimized_script(llm_text_encoder)
|
||||
script.save('{}/llm.text_encoder.fp32.zip'.format(args.model_dir))
|
||||
script = get_optimized_script(llm_text_encoder.half())
|
||||
script.save('{}/llm.text_encoder.fp16.zip'.format(args.model_dir))
|
||||
logging.info('successfully export llm_text_encoder')
|
||||
|
||||
# 2. export llm llm
|
||||
llm_llm = model.model.llm.llm
|
||||
script = get_optimized_script(llm_llm, ['forward_chunk'])
|
||||
script.save('{}/llm.llm.fp32.zip'.format(args.model_dir))
|
||||
script = get_optimized_script(llm_llm.half(), ['forward_chunk'])
|
||||
script.save('{}/llm.llm.fp16.zip'.format(args.model_dir))
|
||||
logging.info('successfully export llm_llm')
|
||||
|
||||
# 3. export flow encoder
|
||||
flow_encoder = model.model.flow.encoder
|
||||
script = get_optimized_script(flow_encoder)
|
||||
script.save('{}/flow.encoder.fp32.zip'.format(args.model_dir))
|
||||
script = get_optimized_script(flow_encoder.half())
|
||||
script.save('{}/flow.encoder.fp16.zip'.format(args.model_dir))
|
||||
logging.info('successfully export flow_encoder')
|
||||
elif model.__class__.__name__ == 'CosyVoice2':
|
||||
# 1. export flow encoder
|
||||
flow_encoder = model.model.flow.encoder
|
||||
script = get_optimized_script(flow_encoder)
|
||||
script.save('{}/flow.encoder.fp32.zip'.format(args.model_dir))
|
||||
script = get_optimized_script(flow_encoder.half())
|
||||
script.save('{}/flow.encoder.fp16.zip'.format(args.model_dir))
|
||||
logging.info('successfully export flow_encoder')
|
||||
else:
|
||||
raise ValueError('unsupported model type')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,114 @@
|
||||
# Copyright (c) 2024 Antgroup Inc (authors: Zhoubofan, hexisyztem@icloud.com)
|
||||
# Copyright (c) 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.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
logging.getLogger('matplotlib').setLevel(logging.WARNING)
|
||||
import os
|
||||
import sys
|
||||
import onnxruntime
|
||||
import random
|
||||
import torch
|
||||
from tqdm import tqdm
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append('{}/../..'.format(ROOT_DIR))
|
||||
sys.path.append('{}/../../third_party/Matcha-TTS'.format(ROOT_DIR))
|
||||
from cosyvoice.cli.cosyvoice import AutoModel
|
||||
from cosyvoice.utils.file_utils import logging
|
||||
|
||||
|
||||
def get_dummy_input(batch_size, seq_len, out_channels, device):
|
||||
x = torch.rand((batch_size, out_channels, seq_len), dtype=torch.float32, device=device)
|
||||
mask = torch.ones((batch_size, 1, seq_len), dtype=torch.float32, device=device)
|
||||
mu = torch.rand((batch_size, out_channels, seq_len), dtype=torch.float32, device=device)
|
||||
t = torch.rand((batch_size), dtype=torch.float32, device=device)
|
||||
spks = torch.rand((batch_size, out_channels), dtype=torch.float32, device=device)
|
||||
cond = torch.rand((batch_size, out_channels, seq_len), dtype=torch.float32, device=device)
|
||||
return x, mask, mu, t, spks, cond
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(description='export your model for deployment')
|
||||
parser.add_argument('--model_dir',
|
||||
type=str,
|
||||
default='pretrained_models/CosyVoice-300M',
|
||||
help='local path')
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
return args
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def main():
|
||||
args = get_args()
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s %(levelname)s %(message)s')
|
||||
|
||||
model = AutoModel(model_dir=args.model_dir)
|
||||
|
||||
# 1. export flow decoder estimator
|
||||
estimator = model.model.flow.decoder.estimator
|
||||
estimator.eval()
|
||||
|
||||
device = model.model.device
|
||||
batch_size, seq_len = 2, 256
|
||||
out_channels = model.model.flow.decoder.estimator.out_channels
|
||||
x, mask, mu, t, spks, cond = get_dummy_input(batch_size, seq_len, out_channels, device)
|
||||
torch.onnx.export(
|
||||
estimator,
|
||||
(x, mask, mu, t, spks, cond),
|
||||
'{}/flow.decoder.estimator.fp32.onnx'.format(args.model_dir),
|
||||
export_params=True,
|
||||
opset_version=18,
|
||||
do_constant_folding=True,
|
||||
input_names=['x', 'mask', 'mu', 't', 'spks', 'cond'],
|
||||
output_names=['estimator_out'],
|
||||
dynamic_axes={
|
||||
'x': {2: 'seq_len'},
|
||||
'mask': {2: 'seq_len'},
|
||||
'mu': {2: 'seq_len'},
|
||||
'cond': {2: 'seq_len'},
|
||||
'estimator_out': {2: 'seq_len'},
|
||||
}
|
||||
)
|
||||
|
||||
# 2. test computation consistency
|
||||
option = onnxruntime.SessionOptions()
|
||||
option.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL
|
||||
option.intra_op_num_threads = 1
|
||||
providers = ['CUDAExecutionProvider' if torch.cuda.is_available() else 'CPUExecutionProvider']
|
||||
estimator_onnx = onnxruntime.InferenceSession('{}/flow.decoder.estimator.fp32.onnx'.format(args.model_dir),
|
||||
sess_options=option, providers=providers)
|
||||
|
||||
for _ in tqdm(range(10)):
|
||||
x, mask, mu, t, spks, cond = get_dummy_input(batch_size, random.randint(16, 512), out_channels, device)
|
||||
output_pytorch = estimator(x, mask, mu, t, spks, cond)
|
||||
ort_inputs = {
|
||||
'x': x.cpu().numpy(),
|
||||
'mask': mask.cpu().numpy(),
|
||||
'mu': mu.cpu().numpy(),
|
||||
't': t.cpu().numpy(),
|
||||
'spks': spks.cpu().numpy(),
|
||||
'cond': cond.cpu().numpy()
|
||||
}
|
||||
output_onnx = estimator_onnx.run(None, ort_inputs)[0]
|
||||
torch.testing.assert_allclose(output_pytorch, torch.from_numpy(output_onnx).to(device), rtol=1e-2, atol=1e-4)
|
||||
logging.info('successfully export estimator')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,195 @@
|
||||
# Copyright (c) 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.
|
||||
|
||||
from __future__ import print_function
|
||||
import argparse
|
||||
import datetime
|
||||
import logging
|
||||
logging.getLogger('matplotlib').setLevel(logging.WARNING)
|
||||
from copy import deepcopy
|
||||
import os
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import deepspeed
|
||||
|
||||
from hyperpyyaml import load_hyperpyyaml
|
||||
|
||||
from torch.distributed.elastic.multiprocessing.errors import record
|
||||
|
||||
from cosyvoice.utils.losses import DPOLoss
|
||||
from cosyvoice.utils.executor import Executor
|
||||
from cosyvoice.utils.train_utils import (
|
||||
init_distributed,
|
||||
init_dataset_and_dataloader,
|
||||
init_optimizer_and_scheduler,
|
||||
init_summarywriter, save_model,
|
||||
wrap_cuda_model, check_modify_and_save_config)
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(description='training your network')
|
||||
parser.add_argument('--train_engine',
|
||||
default='torch_ddp',
|
||||
choices=['torch_ddp', 'deepspeed'],
|
||||
help='Engine for paralleled training')
|
||||
parser.add_argument('--model', required=True, help='model which will be trained')
|
||||
parser.add_argument('--ref_model', required=False, help='ref model used in dpo')
|
||||
parser.add_argument('--config', required=True, help='config file')
|
||||
parser.add_argument('--train_data', required=True, help='train data file')
|
||||
parser.add_argument('--cv_data', required=True, help='cv data file')
|
||||
parser.add_argument('--qwen_pretrain_path', required=False, help='qwen pretrain path')
|
||||
parser.add_argument('--checkpoint', help='checkpoint model')
|
||||
parser.add_argument('--model_dir', required=True, help='save model dir')
|
||||
parser.add_argument('--tensorboard_dir',
|
||||
default='tensorboard',
|
||||
help='tensorboard log dir')
|
||||
parser.add_argument('--ddp.dist_backend',
|
||||
dest='dist_backend',
|
||||
default='nccl',
|
||||
choices=['nccl', 'gloo'],
|
||||
help='distributed backend')
|
||||
parser.add_argument('--num_workers',
|
||||
default=0,
|
||||
type=int,
|
||||
help='num of subprocess workers for reading')
|
||||
parser.add_argument('--prefetch',
|
||||
default=100,
|
||||
type=int,
|
||||
help='prefetch number')
|
||||
parser.add_argument('--pin_memory',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Use pinned memory buffers used for reading')
|
||||
parser.add_argument('--use_amp',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Use automatic mixed precision training')
|
||||
parser.add_argument('--dpo',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Use Direct Preference Optimization')
|
||||
parser.add_argument('--deepspeed.save_states',
|
||||
dest='save_states',
|
||||
default='model_only',
|
||||
choices=['model_only', 'model+optimizer'],
|
||||
help='save model/optimizer states')
|
||||
parser.add_argument('--timeout',
|
||||
default=60,
|
||||
type=int,
|
||||
help='timeout (in seconds) of cosyvoice_join.')
|
||||
parser = deepspeed.add_config_arguments(parser)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
@record
|
||||
def main():
|
||||
args = get_args()
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s %(levelname)s %(message)s')
|
||||
# gan train has some special initialization logic
|
||||
gan = True if args.model == 'hifigan' else False
|
||||
|
||||
override_dict = {k: None for k in ['llm', 'flow', 'hift', 'hifigan'] if k != args.model}
|
||||
if gan is True:
|
||||
override_dict.pop('hift')
|
||||
try:
|
||||
with open(args.config, 'r') as f:
|
||||
configs = load_hyperpyyaml(f, overrides={**override_dict, 'qwen_pretrain_path': args.qwen_pretrain_path})
|
||||
except Exception:
|
||||
with open(args.config, 'r') as f:
|
||||
configs = load_hyperpyyaml(f, overrides=override_dict)
|
||||
if gan is True:
|
||||
configs['train_conf'] = configs['train_conf_gan']
|
||||
configs['train_conf'].update(vars(args))
|
||||
|
||||
# Init env for ddp
|
||||
init_distributed(args)
|
||||
|
||||
# Get dataset & dataloader
|
||||
train_dataset, cv_dataset, train_data_loader, cv_data_loader = \
|
||||
init_dataset_and_dataloader(args, configs, gan, args.dpo)
|
||||
|
||||
# Do some sanity checks and save config to arsg.model_dir
|
||||
configs = check_modify_and_save_config(args, configs)
|
||||
|
||||
# Tensorboard summary
|
||||
writer = init_summarywriter(args)
|
||||
|
||||
# load checkpoint
|
||||
if args.dpo is True:
|
||||
configs[args.model].forward = configs[args.model].forward_dpo
|
||||
model = configs[args.model]
|
||||
start_step, start_epoch = 0, -1
|
||||
if args.checkpoint is not None:
|
||||
if os.path.exists(args.checkpoint):
|
||||
state_dict = torch.load(args.checkpoint, map_location='cpu')
|
||||
model.load_state_dict(state_dict, strict=False)
|
||||
if 'step' in state_dict:
|
||||
start_step = state_dict['step']
|
||||
if 'epoch' in state_dict:
|
||||
start_epoch = state_dict['epoch']
|
||||
else:
|
||||
logging.warning('checkpoint {} do not exsist!'.format(args.checkpoint))
|
||||
|
||||
# Dispatch model from cpu to gpu
|
||||
model = wrap_cuda_model(args, model)
|
||||
|
||||
# Get optimizer & scheduler
|
||||
model, optimizer, scheduler, optimizer_d, scheduler_d = init_optimizer_and_scheduler(args, configs, model, gan)
|
||||
scheduler.set_step(start_step)
|
||||
if scheduler_d is not None:
|
||||
scheduler_d.set_step(start_step)
|
||||
|
||||
# Save init checkpoints
|
||||
info_dict = deepcopy(configs['train_conf'])
|
||||
info_dict['step'] = start_step
|
||||
info_dict['epoch'] = start_epoch
|
||||
save_model(model, 'init', info_dict)
|
||||
|
||||
# DPO related
|
||||
if args.dpo is True:
|
||||
ref_model = deepcopy(configs[args.model])
|
||||
state_dict = torch.load(args.ref_model, map_location='cpu')
|
||||
ref_model.load_state_dict(state_dict, strict=False)
|
||||
dpo_loss = DPOLoss(beta=0.01, label_smoothing=0.0, ipo=False)
|
||||
# NOTE maybe it is not needed to wrap ref_model as ddp because its parameter is not updated
|
||||
ref_model = wrap_cuda_model(args, ref_model)
|
||||
else:
|
||||
ref_model, dpo_loss = None, None
|
||||
|
||||
# Get executor
|
||||
executor = Executor(gan=gan, ref_model=ref_model, dpo_loss=dpo_loss)
|
||||
executor.step = start_step
|
||||
|
||||
# Init scaler, used for pytorch amp mixed precision training
|
||||
scaler = torch.cuda.amp.GradScaler() if args.use_amp else None
|
||||
print('start step {} start epoch {}'.format(start_step, start_epoch))
|
||||
|
||||
# Start training loop
|
||||
for epoch in range(start_epoch + 1, info_dict['max_epoch']):
|
||||
executor.epoch = epoch
|
||||
train_dataset.set_epoch(epoch)
|
||||
dist.barrier()
|
||||
group_join = dist.new_group(backend="gloo", timeout=datetime.timedelta(seconds=args.timeout))
|
||||
if gan is True:
|
||||
executor.train_one_epoc_gan(model, optimizer, scheduler, optimizer_d, scheduler_d, train_data_loader, cv_data_loader,
|
||||
writer, info_dict, scaler, group_join)
|
||||
else:
|
||||
executor.train_one_epoc(model, optimizer, scheduler, train_data_loader, cv_data_loader, writer, info_dict, scaler, group_join, ref_model=ref_model)
|
||||
dist.destroy_process_group(group_join)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,319 @@
|
||||
# Copyright (c) 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 os
|
||||
import time
|
||||
from typing import Generator
|
||||
from tqdm import tqdm
|
||||
|
||||
# FIX: Bundled code fix - PyYAML 6.0+ compatibility with hyperpyyaml
|
||||
# Issue: 'Loader' object has no attribute 'max_depth' error (GitHub #220)
|
||||
# hyperpyyaml expects max_depth attribute that was removed in PyYAML 6.0+
|
||||
import yaml
|
||||
if not hasattr(yaml.Loader, 'max_depth'):
|
||||
yaml.Loader.max_depth = 100 # Default safe depth limit
|
||||
if not hasattr(yaml.FullLoader, 'max_depth'):
|
||||
yaml.FullLoader.max_depth = 100
|
||||
if not hasattr(yaml.SafeLoader, 'max_depth'):
|
||||
yaml.SafeLoader.max_depth = 100
|
||||
|
||||
from hyperpyyaml import load_hyperpyyaml
|
||||
from modelscope import snapshot_download
|
||||
import torch
|
||||
from cosyvoice.cli.frontend import CosyVoiceFrontEnd
|
||||
from cosyvoice.cli.model import CosyVoiceModel, CosyVoice2Model, CosyVoice3Model
|
||||
from cosyvoice.utils.file_utils import logging
|
||||
from cosyvoice.utils.class_utils import get_model_type
|
||||
|
||||
|
||||
class CosyVoice:
|
||||
# PATCH: Added llm_filename parameter to support model variants (llm.pt vs llm.rl.pt)
|
||||
# This allows lazy downloading of only the requested variant
|
||||
def __init__(self, model_dir, load_jit=False, load_trt=False, fp16=False, trt_concurrent=1, llm_filename='llm.pt'):
|
||||
self.model_dir = model_dir
|
||||
self.fp16 = fp16
|
||||
if not os.path.exists(model_dir):
|
||||
model_dir = snapshot_download(model_dir)
|
||||
hyper_yaml_path = '{}/cosyvoice.yaml'.format(model_dir)
|
||||
if not os.path.exists(hyper_yaml_path):
|
||||
raise ValueError('{} not found!'.format(hyper_yaml_path))
|
||||
with open(hyper_yaml_path, 'r') as f:
|
||||
configs = load_hyperpyyaml(f)
|
||||
assert get_model_type(configs) == CosyVoiceModel, 'do not use {} for CosyVoice initialization!'.format(model_dir)
|
||||
self.frontend = CosyVoiceFrontEnd(configs['get_tokenizer'],
|
||||
configs['feat_extractor'],
|
||||
'{}/campplus.onnx'.format(model_dir),
|
||||
'{}/speech_tokenizer_v1.onnx'.format(model_dir),
|
||||
'{}/spk2info.pt'.format(model_dir),
|
||||
configs['allowed_special'])
|
||||
self.sample_rate = configs['sample_rate']
|
||||
if torch.cuda.is_available() is False and (load_jit is True or load_trt is True or fp16 is True):
|
||||
load_jit, load_trt, fp16 = False, False, False
|
||||
logging.warning('no cuda device, set load_jit/load_trt/fp16 to False')
|
||||
self.model = CosyVoiceModel(configs['llm'], configs['flow'], configs['hift'], fp16)
|
||||
# PATCH: Use llm_filename parameter instead of hardcoded 'llm.pt'
|
||||
self.model.load('{}/{}'.format(model_dir, llm_filename),
|
||||
'{}/flow.pt'.format(model_dir),
|
||||
'{}/hift.pt'.format(model_dir))
|
||||
if load_jit:
|
||||
self.model.load_jit('{}/llm.text_encoder.{}.zip'.format(model_dir, 'fp16' if self.fp16 is True else 'fp32'),
|
||||
'{}/llm.llm.{}.zip'.format(model_dir, 'fp16' if self.fp16 is True else 'fp32'),
|
||||
'{}/flow.encoder.{}.zip'.format(model_dir, 'fp16' if self.fp16 is True else 'fp32'))
|
||||
if load_trt:
|
||||
self.model.load_trt('{}/flow.decoder.estimator.{}.mygpu.plan'.format(model_dir, 'fp16' if self.fp16 is True else 'fp32'),
|
||||
'{}/flow.decoder.estimator.fp32.onnx'.format(model_dir),
|
||||
trt_concurrent,
|
||||
self.fp16)
|
||||
del configs
|
||||
|
||||
def list_available_spks(self):
|
||||
spks = list(self.frontend.spk2info.keys())
|
||||
return spks
|
||||
|
||||
def add_zero_shot_spk(self, prompt_text, prompt_wav, zero_shot_spk_id):
|
||||
assert zero_shot_spk_id != '', 'do not use empty zero_shot_spk_id'
|
||||
model_input = self.frontend.frontend_zero_shot('', prompt_text, prompt_wav, self.sample_rate, '')
|
||||
del model_input['text']
|
||||
del model_input['text_len']
|
||||
self.frontend.spk2info[zero_shot_spk_id] = model_input
|
||||
return True
|
||||
|
||||
def save_spkinfo(self):
|
||||
torch.save(self.frontend.spk2info, '{}/spk2info.pt'.format(self.model_dir))
|
||||
|
||||
def inference_sft(self, tts_text, spk_id, stream=False, speed=1.0, text_frontend=True):
|
||||
for i in tqdm(self.frontend.text_normalize(tts_text, split=True, text_frontend=text_frontend)):
|
||||
model_input = self.frontend.frontend_sft(i, spk_id)
|
||||
start_time = time.time()
|
||||
logging.info('synthesis text {}'.format(i))
|
||||
for model_output in self.model.tts(**model_input, stream=stream, speed=speed):
|
||||
speech_len = model_output['tts_speech'].shape[1] / self.sample_rate
|
||||
logging.debug('yield speech len {}, rtf {}'.format(speech_len, (time.time() - start_time) / speech_len))
|
||||
yield model_output
|
||||
start_time = time.time()
|
||||
|
||||
def inference_zero_shot(self, tts_text, prompt_text, prompt_wav, zero_shot_spk_id='', stream=False, speed=1.0, text_frontend=True):
|
||||
prompt_text = self.frontend.text_normalize(prompt_text, split=False, text_frontend=text_frontend)
|
||||
# Create live progress callback
|
||||
start_time = [time.time()]
|
||||
last_print_time = [start_time[0]]
|
||||
|
||||
def progress_callback(current, total):
|
||||
current_time = time.time()
|
||||
elapsed = current_time - start_time[0]
|
||||
it_per_sec = current / elapsed if elapsed > 0 else 0
|
||||
filled = int(12 * current / total) if total > 0 else 0
|
||||
bar = '█' * filled + '░' * (12 - filled)
|
||||
|
||||
# Calculate ETA
|
||||
remaining = total - current
|
||||
eta_seconds = remaining / it_per_sec if it_per_sec > 0 else 0
|
||||
eta_str = f"{eta_seconds:.0f}s" if eta_seconds < 60 else f"{eta_seconds/60:.1f}m"
|
||||
|
||||
import sys
|
||||
# Force stdout flush for Windows
|
||||
sys.stdout.write(f"\r Progress: [{bar}] {current}/{total} | {it_per_sec:.1f} it/s | {elapsed:.0f}s | ETA: {eta_str} ")
|
||||
sys.stdout.flush()
|
||||
last_print_time[0] = current_time
|
||||
|
||||
# Use live progress with token-level updates
|
||||
for i in self.frontend.text_normalize(tts_text, split=True, text_frontend=text_frontend):
|
||||
# Commented out per-segment warning - consolidated to processor level
|
||||
# if (not isinstance(i, Generator)) and len(i) < 0.5 * len(prompt_text):
|
||||
# logging.warning('\n⚠️ TTS text is shorter than reference transcript - this may affect quality\n TTS text length: {} chars\n Reference length: {} chars'.format(len(i), len(prompt_text)))
|
||||
model_input = self.frontend.frontend_zero_shot(i, prompt_text, prompt_wav, self.sample_rate, zero_shot_spk_id)
|
||||
start_time[0] = time.time()
|
||||
last_print_time[0] = start_time[0]
|
||||
for model_output in self.model.tts(**model_input, stream=stream, speed=speed, progress_callback=progress_callback):
|
||||
yield model_output
|
||||
# Clear progress line
|
||||
print(f"\r{' ' * 80}\r", end='', flush=True)
|
||||
|
||||
def inference_cross_lingual(self, tts_text, prompt_wav, zero_shot_spk_id='', stream=False, speed=1.0, text_frontend=True):
|
||||
# Create live progress callback
|
||||
start_time = [time.time()]
|
||||
last_print_time = [start_time[0]]
|
||||
|
||||
def progress_callback(current, total):
|
||||
current_time = time.time()
|
||||
elapsed = current_time - start_time[0]
|
||||
it_per_sec = current / elapsed if elapsed > 0 else 0
|
||||
filled = int(12 * current / total) if total > 0 else 0
|
||||
bar = '█' * filled + '░' * (12 - filled)
|
||||
|
||||
# Calculate ETA
|
||||
remaining = total - current
|
||||
eta_seconds = remaining / it_per_sec if it_per_sec > 0 else 0
|
||||
eta_str = f"{eta_seconds:.0f}s" if eta_seconds < 60 else f"{eta_seconds/60:.1f}m"
|
||||
|
||||
import sys
|
||||
sys.stdout.write(f"\r Progress: [{bar}] {current}/{total} | {it_per_sec:.1f} it/s | {elapsed:.0f}s | ETA: {eta_str} ")
|
||||
sys.stdout.flush()
|
||||
last_print_time[0] = current_time
|
||||
|
||||
# Use live progress with token-level updates
|
||||
for i in self.frontend.text_normalize(tts_text, split=True, text_frontend=text_frontend):
|
||||
model_input = self.frontend.frontend_cross_lingual(i, prompt_wav, self.sample_rate, zero_shot_spk_id)
|
||||
start_time[0] = time.time()
|
||||
last_print_time[0] = start_time[0]
|
||||
for model_output in self.model.tts(**model_input, stream=stream, speed=speed, progress_callback=progress_callback):
|
||||
yield model_output
|
||||
# Clear progress line
|
||||
print(f"\r{' ' * 80}\r", end='', flush=True)
|
||||
|
||||
def inference_instruct(self, tts_text, spk_id, instruct_text, stream=False, speed=1.0, text_frontend=True):
|
||||
assert self.__class__.__name__ == 'CosyVoice', 'inference_instruct is only implemented for CosyVoice!'
|
||||
instruct_text = self.frontend.text_normalize(instruct_text, split=False, text_frontend=text_frontend)
|
||||
for i in tqdm(self.frontend.text_normalize(tts_text, split=True, text_frontend=text_frontend)):
|
||||
model_input = self.frontend.frontend_instruct(i, spk_id, instruct_text)
|
||||
start_time = time.time()
|
||||
logging.info('synthesis text {}'.format(i))
|
||||
for model_output in self.model.tts(**model_input, stream=stream, speed=speed):
|
||||
speech_len = model_output['tts_speech'].shape[1] / self.sample_rate
|
||||
logging.debug('yield speech len {}, rtf {}'.format(speech_len, (time.time() - start_time) / speech_len))
|
||||
yield model_output
|
||||
start_time = time.time()
|
||||
|
||||
def inference_vc(self, source_wav, prompt_wav, stream=False, speed=1.0):
|
||||
model_input = self.frontend.frontend_vc(source_wav, prompt_wav, self.sample_rate)
|
||||
start_time = time.time()
|
||||
for model_output in self.model.tts(**model_input, stream=stream, speed=speed):
|
||||
speech_len = model_output['tts_speech'].shape[1] / self.sample_rate
|
||||
logging.info('yield speech len {}, rtf {}'.format(speech_len, (time.time() - start_time) / speech_len))
|
||||
yield model_output
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
class CosyVoice2(CosyVoice):
|
||||
# PATCH: Added llm_filename parameter to support model variants (llm.pt vs llm.rl.pt)
|
||||
def __init__(self, model_dir, load_jit=False, load_trt=False, load_vllm=False, fp16=False, trt_concurrent=1, llm_filename='llm.pt'):
|
||||
self.model_dir = model_dir
|
||||
self.fp16 = fp16
|
||||
if not os.path.exists(model_dir):
|
||||
model_dir = snapshot_download(model_dir)
|
||||
hyper_yaml_path = '{}/cosyvoice2.yaml'.format(model_dir)
|
||||
if not os.path.exists(hyper_yaml_path):
|
||||
raise ValueError('{} not found!'.format(hyper_yaml_path))
|
||||
with open(hyper_yaml_path, 'r') as f:
|
||||
configs = load_hyperpyyaml(f, overrides={'qwen_pretrain_path': os.path.join(model_dir, 'CosyVoice-BlankEN')})
|
||||
assert get_model_type(configs) == CosyVoice2Model, 'do not use {} for CosyVoice2 initialization!'.format(model_dir)
|
||||
self.frontend = CosyVoiceFrontEnd(configs['get_tokenizer'],
|
||||
configs['feat_extractor'],
|
||||
'{}/campplus.onnx'.format(model_dir),
|
||||
'{}/speech_tokenizer_v2.onnx'.format(model_dir),
|
||||
'{}/spk2info.pt'.format(model_dir),
|
||||
configs['allowed_special'])
|
||||
self.sample_rate = configs['sample_rate']
|
||||
if torch.cuda.is_available() is False and (load_jit is True or load_trt is True or load_vllm is True or fp16 is True):
|
||||
load_jit, load_trt, load_vllm, fp16 = False, False, False, False
|
||||
logging.warning('no cuda device, set load_jit/load_trt/load_vllm/fp16 to False')
|
||||
self.model = CosyVoice2Model(configs['llm'], configs['flow'], configs['hift'], fp16)
|
||||
# PATCH: Use llm_filename parameter instead of hardcoded 'llm.pt'
|
||||
self.model.load('{}/{}'.format(model_dir, llm_filename),
|
||||
'{}/flow.pt'.format(model_dir),
|
||||
'{}/hift.pt'.format(model_dir))
|
||||
if load_vllm:
|
||||
self.model.load_vllm('{}/vllm'.format(model_dir))
|
||||
if load_jit:
|
||||
self.model.load_jit('{}/flow.encoder.{}.zip'.format(model_dir, 'fp16' if self.fp16 is True else 'fp32'))
|
||||
if load_trt:
|
||||
self.model.load_trt('{}/flow.decoder.estimator.{}.mygpu.plan'.format(model_dir, 'fp16' if self.fp16 is True else 'fp32'),
|
||||
'{}/flow.decoder.estimator.fp32.onnx'.format(model_dir),
|
||||
trt_concurrent,
|
||||
self.fp16)
|
||||
del configs
|
||||
|
||||
def inference_instruct2(self, tts_text, instruct_text, prompt_wav, zero_shot_spk_id='', stream=False, speed=1.0, text_frontend=True):
|
||||
# Create live progress callback
|
||||
start_time = [time.time()]
|
||||
last_print_time = [start_time[0]]
|
||||
|
||||
def progress_callback(current, total):
|
||||
current_time = time.time()
|
||||
elapsed = current_time - start_time[0]
|
||||
it_per_sec = current / elapsed if elapsed > 0 else 0
|
||||
filled = int(12 * current / total) if total > 0 else 0
|
||||
bar = '█' * filled + '░' * (12 - filled)
|
||||
|
||||
# Calculate ETA
|
||||
remaining = total - current
|
||||
eta_seconds = remaining / it_per_sec if it_per_sec > 0 else 0
|
||||
eta_str = f"{eta_seconds:.0f}s" if eta_seconds < 60 else f"{eta_seconds/60:.1f}m"
|
||||
|
||||
import sys
|
||||
sys.stdout.write(f"\r Progress: [{bar}] {current}/{total} | {it_per_sec:.1f} it/s | {elapsed:.0f}s | ETA: {eta_str} ")
|
||||
sys.stdout.flush()
|
||||
last_print_time[0] = current_time
|
||||
|
||||
# Use live progress with token-level updates
|
||||
for i in self.frontend.text_normalize(tts_text, split=True, text_frontend=text_frontend):
|
||||
model_input = self.frontend.frontend_instruct2(i, instruct_text, prompt_wav, self.sample_rate, zero_shot_spk_id)
|
||||
start_time[0] = time.time()
|
||||
last_print_time[0] = start_time[0]
|
||||
for model_output in self.model.tts(**model_input, stream=stream, speed=speed, progress_callback=progress_callback):
|
||||
yield model_output
|
||||
# Clear progress line
|
||||
print(f"\r{' ' * 80}\r", end='', flush=True)
|
||||
|
||||
|
||||
class CosyVoice3(CosyVoice2):
|
||||
# PATCH: Added llm_filename parameter to support model variants (llm.pt vs llm.rl.pt)
|
||||
def __init__(self, model_dir, load_trt=False, load_vllm=False, fp16=False, trt_concurrent=1, llm_filename='llm.pt'):
|
||||
self.model_dir = model_dir
|
||||
self.fp16 = fp16
|
||||
if not os.path.exists(model_dir):
|
||||
model_dir = snapshot_download(model_dir)
|
||||
hyper_yaml_path = '{}/cosyvoice3.yaml'.format(model_dir)
|
||||
if not os.path.exists(hyper_yaml_path):
|
||||
raise ValueError('{} not found!'.format(hyper_yaml_path))
|
||||
with open(hyper_yaml_path, 'r') as f:
|
||||
configs = load_hyperpyyaml(f, overrides={'qwen_pretrain_path': os.path.join(model_dir, 'CosyVoice-BlankEN')})
|
||||
assert get_model_type(configs) == CosyVoice3Model, 'do not use {} for CosyVoice3 initialization!'.format(model_dir)
|
||||
self.frontend = CosyVoiceFrontEnd(configs['get_tokenizer'],
|
||||
configs['feat_extractor'],
|
||||
'{}/campplus.onnx'.format(model_dir),
|
||||
'{}/speech_tokenizer_v3.onnx'.format(model_dir),
|
||||
'{}/spk2info.pt'.format(model_dir),
|
||||
configs['allowed_special'])
|
||||
self.sample_rate = configs['sample_rate']
|
||||
if torch.cuda.is_available() is False and (load_trt is True or fp16 is True):
|
||||
load_trt, fp16 = False, False
|
||||
logging.warning('no cuda device, set load_trt/fp16 to False')
|
||||
self.model = CosyVoice3Model(configs['llm'], configs['flow'], configs['hift'], fp16)
|
||||
# PATCH: Use llm_filename parameter instead of hardcoded 'llm.pt'
|
||||
self.model.load('{}/{}'.format(model_dir, llm_filename),
|
||||
'{}/flow.pt'.format(model_dir),
|
||||
'{}/hift.pt'.format(model_dir))
|
||||
if load_vllm:
|
||||
self.model.load_vllm('{}/vllm'.format(model_dir))
|
||||
if load_trt:
|
||||
if self.fp16 is True:
|
||||
logging.warning('DiT tensorRT fp16 engine have some performance issue, use at caution!')
|
||||
self.model.load_trt('{}/flow.decoder.estimator.{}.mygpu.plan'.format(model_dir, 'fp16' if self.fp16 is True else 'fp32'),
|
||||
'{}/flow.decoder.estimator.fp32.onnx'.format(model_dir),
|
||||
trt_concurrent,
|
||||
self.fp16)
|
||||
del configs
|
||||
|
||||
|
||||
def AutoModel(**kwargs):
|
||||
if not os.path.exists(kwargs['model_dir']):
|
||||
kwargs['model_dir'] = snapshot_download(kwargs['model_dir'])
|
||||
if os.path.exists('{}/cosyvoice.yaml'.format(kwargs['model_dir'])):
|
||||
return CosyVoice(**kwargs)
|
||||
elif os.path.exists('{}/cosyvoice2.yaml'.format(kwargs['model_dir'])):
|
||||
return CosyVoice2(**kwargs)
|
||||
elif os.path.exists('{}/cosyvoice3.yaml'.format(kwargs['model_dir'])):
|
||||
return CosyVoice3(**kwargs)
|
||||
else:
|
||||
raise TypeError('No valid model type found!')
|
||||
@@ -0,0 +1,224 @@
|
||||
# Copyright (c) 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.
|
||||
from functools import partial
|
||||
from typing import Generator
|
||||
import json
|
||||
import onnxruntime
|
||||
import torch
|
||||
import numpy as np
|
||||
import whisper
|
||||
from typing import Callable
|
||||
import torchaudio.compliance.kaldi as kaldi
|
||||
import os
|
||||
import re
|
||||
import inflect
|
||||
from cosyvoice.utils.file_utils import logging, load_wav
|
||||
from cosyvoice.utils.frontend_utils import contains_chinese, replace_blank, replace_corner_mark, remove_bracket, spell_out_number, split_paragraph, is_only_punctuation
|
||||
|
||||
|
||||
class CosyVoiceFrontEnd:
|
||||
|
||||
def __init__(self,
|
||||
get_tokenizer: Callable,
|
||||
feat_extractor: Callable,
|
||||
campplus_model: str,
|
||||
speech_tokenizer_model: str,
|
||||
spk2info: str = '',
|
||||
allowed_special: str = 'all'):
|
||||
self.tokenizer = get_tokenizer()
|
||||
self.feat_extractor = feat_extractor
|
||||
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
option = onnxruntime.SessionOptions()
|
||||
option.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL
|
||||
option.intra_op_num_threads = 1
|
||||
self.campplus_session = onnxruntime.InferenceSession(campplus_model, sess_options=option, providers=["CPUExecutionProvider"])
|
||||
self.speech_tokenizer_session = onnxruntime.InferenceSession(speech_tokenizer_model, sess_options=option,
|
||||
providers=["CUDAExecutionProvider" if torch.cuda.is_available() else
|
||||
"CPUExecutionProvider"])
|
||||
if os.path.exists(spk2info):
|
||||
self.spk2info = torch.load(spk2info, map_location=self.device)
|
||||
else:
|
||||
self.spk2info = {}
|
||||
self.allowed_special = allowed_special
|
||||
self.inflect_parser = inflect.engine()
|
||||
# NOTE compatible when no text frontend tool is avaliable
|
||||
try:
|
||||
import ttsfrd
|
||||
self.frd = ttsfrd.TtsFrontendEngine()
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
assert self.frd.initialize('{}/../../pretrained_models/CosyVoice-ttsfrd/resource'.format(ROOT_DIR)) is True, \
|
||||
'failed to initialize ttsfrd resource'
|
||||
self.frd.set_lang_type('pinyinvg')
|
||||
self.text_frontend = 'ttsfrd'
|
||||
logging.info('use ttsfrd frontend')
|
||||
except:
|
||||
try:
|
||||
from wetext import Normalizer as ZhNormalizer
|
||||
from wetext import Normalizer as EnNormalizer
|
||||
self.zh_tn_model = ZhNormalizer(remove_erhua=False)
|
||||
self.en_tn_model = EnNormalizer()
|
||||
self.text_frontend = 'wetext'
|
||||
# logging.info('use wetext frontend') # Internal implementation detail
|
||||
except:
|
||||
self.text_frontend = ''
|
||||
logging.info('no frontend is avaliable')
|
||||
|
||||
|
||||
def _extract_text_token(self, text):
|
||||
if isinstance(text, Generator):
|
||||
logging.info('get tts_text generator, will return _extract_text_token_generator!')
|
||||
# NOTE add a dummy text_token_len for compatibility
|
||||
return self._extract_text_token_generator(text), torch.tensor([0], dtype=torch.int32).to(self.device)
|
||||
else:
|
||||
text_token = self.tokenizer.encode(text, allowed_special=self.allowed_special)
|
||||
text_token = torch.tensor([text_token], dtype=torch.int32).to(self.device)
|
||||
text_token_len = torch.tensor([text_token.shape[1]], dtype=torch.int32).to(self.device)
|
||||
return text_token, text_token_len
|
||||
|
||||
def _extract_text_token_generator(self, text_generator):
|
||||
for text in text_generator:
|
||||
text_token, _ = self._extract_text_token(text)
|
||||
for i in range(text_token.shape[1]):
|
||||
yield text_token[:, i: i + 1]
|
||||
|
||||
def _extract_speech_token(self, prompt_wav):
|
||||
speech = load_wav(prompt_wav, 16000)
|
||||
assert speech.shape[1] / 16000 <= 30, 'do not support extract speech token for audio longer than 30s'
|
||||
feat = whisper.log_mel_spectrogram(speech, n_mels=128)
|
||||
speech_token = self.speech_tokenizer_session.run(None,
|
||||
{self.speech_tokenizer_session.get_inputs()[0].name:
|
||||
feat.detach().cpu().numpy(),
|
||||
self.speech_tokenizer_session.get_inputs()[1].name:
|
||||
np.array([feat.shape[2]], dtype=np.int32)})[0].flatten().tolist()
|
||||
speech_token = torch.tensor([speech_token], dtype=torch.int32).to(self.device)
|
||||
speech_token_len = torch.tensor([speech_token.shape[1]], dtype=torch.int32).to(self.device)
|
||||
return speech_token, speech_token_len
|
||||
|
||||
def _extract_spk_embedding(self, prompt_wav):
|
||||
speech = load_wav(prompt_wav, 16000)
|
||||
feat = kaldi.fbank(speech,
|
||||
num_mel_bins=80,
|
||||
dither=0,
|
||||
sample_frequency=16000)
|
||||
feat = feat - feat.mean(dim=0, keepdim=True)
|
||||
embedding = self.campplus_session.run(None,
|
||||
{self.campplus_session.get_inputs()[0].name: feat.unsqueeze(dim=0).cpu().numpy()})[0].flatten().tolist()
|
||||
embedding = torch.tensor([embedding]).to(self.device)
|
||||
return embedding
|
||||
|
||||
def _extract_speech_feat(self, prompt_wav):
|
||||
speech = load_wav(prompt_wav, 24000)
|
||||
speech_feat = self.feat_extractor(speech).squeeze(dim=0).transpose(0, 1).to(self.device)
|
||||
speech_feat = speech_feat.unsqueeze(dim=0)
|
||||
speech_feat_len = torch.tensor([speech_feat.shape[1]], dtype=torch.int32).to(self.device)
|
||||
return speech_feat, speech_feat_len
|
||||
|
||||
def text_normalize(self, text, split=True, text_frontend=True):
|
||||
if isinstance(text, Generator):
|
||||
logging.info('get tts_text generator, will skip text_normalize!')
|
||||
return [text]
|
||||
# NOTE skip text_frontend when ssml symbol in text
|
||||
if '<|' in text and '|>' in text:
|
||||
text_frontend = False
|
||||
if text_frontend is False or text == '':
|
||||
return [text] if split is True else text
|
||||
text = text.strip()
|
||||
if self.text_frontend == 'ttsfrd':
|
||||
texts = [i["text"] for i in json.loads(self.frd.do_voicegen_frd(text))["sentences"]]
|
||||
text = ''.join(texts)
|
||||
else:
|
||||
if contains_chinese(text):
|
||||
if self.text_frontend == 'wetext':
|
||||
text = self.zh_tn_model.normalize(text)
|
||||
text = text.replace("\n", "")
|
||||
text = replace_blank(text)
|
||||
text = replace_corner_mark(text)
|
||||
text = text.replace(".", "。")
|
||||
text = text.replace(" - ", ",")
|
||||
text = remove_bracket(text)
|
||||
text = re.sub(r'[,,、]+$', '。', text)
|
||||
texts = list(split_paragraph(text, partial(self.tokenizer.encode, allowed_special=self.allowed_special), "zh", token_max_n=80,
|
||||
token_min_n=60, merge_len=20, comma_split=False))
|
||||
else:
|
||||
if self.text_frontend == 'wetext':
|
||||
text = self.en_tn_model.normalize(text)
|
||||
text = spell_out_number(text, self.inflect_parser)
|
||||
texts = list(split_paragraph(text, partial(self.tokenizer.encode, allowed_special=self.allowed_special), "en", token_max_n=80,
|
||||
token_min_n=60, merge_len=20, comma_split=False))
|
||||
texts = [i for i in texts if not is_only_punctuation(i)]
|
||||
return texts if split is True else text
|
||||
|
||||
def frontend_sft(self, tts_text, spk_id):
|
||||
tts_text_token, tts_text_token_len = self._extract_text_token(tts_text)
|
||||
embedding = self.spk2info[spk_id]['embedding']
|
||||
model_input = {'text': tts_text_token, 'text_len': tts_text_token_len, 'llm_embedding': embedding, 'flow_embedding': embedding}
|
||||
return model_input
|
||||
|
||||
def frontend_zero_shot(self, tts_text, prompt_text, prompt_wav, resample_rate, zero_shot_spk_id):
|
||||
tts_text_token, tts_text_token_len = self._extract_text_token(tts_text)
|
||||
if zero_shot_spk_id == '':
|
||||
prompt_text_token, prompt_text_token_len = self._extract_text_token(prompt_text)
|
||||
speech_feat, speech_feat_len = self._extract_speech_feat(prompt_wav)
|
||||
speech_token, speech_token_len = self._extract_speech_token(prompt_wav)
|
||||
if resample_rate == 24000:
|
||||
# cosyvoice2, force speech_feat % speech_token = 2
|
||||
token_len = min(int(speech_feat.shape[1] / 2), speech_token.shape[1])
|
||||
speech_feat, speech_feat_len[:] = speech_feat[:, :2 * token_len], 2 * token_len
|
||||
speech_token, speech_token_len[:] = speech_token[:, :token_len], token_len
|
||||
embedding = self._extract_spk_embedding(prompt_wav)
|
||||
model_input = {'prompt_text': prompt_text_token, 'prompt_text_len': prompt_text_token_len,
|
||||
'llm_prompt_speech_token': speech_token, 'llm_prompt_speech_token_len': speech_token_len,
|
||||
'flow_prompt_speech_token': speech_token, 'flow_prompt_speech_token_len': speech_token_len,
|
||||
'prompt_speech_feat': speech_feat, 'prompt_speech_feat_len': speech_feat_len,
|
||||
'llm_embedding': embedding, 'flow_embedding': embedding}
|
||||
else:
|
||||
model_input = self.spk2info[zero_shot_spk_id]
|
||||
model_input['text'] = tts_text_token
|
||||
model_input['text_len'] = tts_text_token_len
|
||||
return model_input
|
||||
|
||||
def frontend_cross_lingual(self, tts_text, prompt_wav, resample_rate, zero_shot_spk_id):
|
||||
model_input = self.frontend_zero_shot(tts_text, '', prompt_wav, resample_rate, zero_shot_spk_id)
|
||||
# in cross lingual mode, we remove prompt in llm
|
||||
del model_input['prompt_text']
|
||||
del model_input['prompt_text_len']
|
||||
del model_input['llm_prompt_speech_token']
|
||||
del model_input['llm_prompt_speech_token_len']
|
||||
return model_input
|
||||
|
||||
def frontend_instruct(self, tts_text, spk_id, instruct_text):
|
||||
model_input = self.frontend_sft(tts_text, spk_id)
|
||||
# in instruct mode, we remove spk_embedding in llm due to information leakage
|
||||
del model_input['llm_embedding']
|
||||
instruct_text_token, instruct_text_token_len = self._extract_text_token(instruct_text)
|
||||
model_input['prompt_text'] = instruct_text_token
|
||||
model_input['prompt_text_len'] = instruct_text_token_len
|
||||
return model_input
|
||||
|
||||
def frontend_instruct2(self, tts_text, instruct_text, prompt_wav, resample_rate, zero_shot_spk_id):
|
||||
model_input = self.frontend_zero_shot(tts_text, instruct_text, prompt_wav, resample_rate, zero_shot_spk_id)
|
||||
del model_input['llm_prompt_speech_token']
|
||||
del model_input['llm_prompt_speech_token_len']
|
||||
return model_input
|
||||
|
||||
def frontend_vc(self, source_speech_16k, prompt_wav, resample_rate):
|
||||
prompt_speech_token, prompt_speech_token_len = self._extract_speech_token(prompt_wav)
|
||||
prompt_speech_feat, prompt_speech_feat_len = self._extract_speech_feat(prompt_wav)
|
||||
embedding = self._extract_spk_embedding(prompt_wav)
|
||||
source_speech_token, source_speech_token_len = self._extract_speech_token(source_speech_16k)
|
||||
model_input = {'source_speech_token': source_speech_token, 'source_speech_token_len': source_speech_token_len,
|
||||
'flow_prompt_speech_token': prompt_speech_token, 'flow_prompt_speech_token_len': prompt_speech_token_len,
|
||||
'prompt_speech_feat': prompt_speech_feat, 'prompt_speech_feat_len': prompt_speech_feat_len,
|
||||
'flow_embedding': embedding}
|
||||
return model_input
|
||||
@@ -0,0 +1,445 @@
|
||||
# Copyright (c) 2024 Alibaba Inc (authors: Xiang Lyu)
|
||||
# 2025 Alibaba Inc (authors: Xiang Lyu, Bofan Zhou)
|
||||
#
|
||||
# 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 os
|
||||
from typing import Generator
|
||||
import torch
|
||||
import numpy as np
|
||||
import threading
|
||||
import time
|
||||
from torch.nn import functional as F
|
||||
from contextlib import nullcontext
|
||||
import uuid
|
||||
from cosyvoice.utils.common import fade_in_out
|
||||
from cosyvoice.utils.file_utils import convert_onnx_to_trt, export_cosyvoice2_vllm
|
||||
from cosyvoice.utils.common import TrtContextWrapper
|
||||
|
||||
|
||||
class CosyVoiceModel:
|
||||
|
||||
def __init__(self,
|
||||
llm: torch.nn.Module,
|
||||
flow: torch.nn.Module,
|
||||
hift: torch.nn.Module,
|
||||
fp16: bool = False):
|
||||
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
self.llm = llm
|
||||
self.flow = flow
|
||||
self.hift = hift
|
||||
self.fp16 = fp16
|
||||
self.token_min_hop_len = 2 * self.flow.input_frame_rate
|
||||
self.token_max_hop_len = 4 * self.flow.input_frame_rate
|
||||
self.token_overlap_len = 20
|
||||
# mel fade in out
|
||||
self.mel_overlap_len = int(self.token_overlap_len / self.flow.input_frame_rate * 22050 / 256)
|
||||
self.mel_window = np.hamming(2 * self.mel_overlap_len)
|
||||
# hift cache
|
||||
self.mel_cache_len = 20
|
||||
self.source_cache_len = int(self.mel_cache_len * 256)
|
||||
# speech fade in out
|
||||
self.speech_window = np.hamming(2 * self.source_cache_len)
|
||||
# rtf and decoding related
|
||||
self.stream_scale_factor = 1
|
||||
assert self.stream_scale_factor >= 1, 'stream_scale_factor should be greater than 1, change it according to your actual rtf'
|
||||
self.llm_context = torch.cuda.stream(torch.cuda.Stream(self.device)) if torch.cuda.is_available() else nullcontext()
|
||||
self.lock = threading.Lock()
|
||||
# dict used to store session related variable
|
||||
self.tts_speech_token_dict = {}
|
||||
self.llm_end_dict = {}
|
||||
self.mel_overlap_dict = {}
|
||||
self.flow_cache_dict = {}
|
||||
self.hift_cache_dict = {}
|
||||
|
||||
def load(self, llm_model, flow_model, hift_model):
|
||||
self.llm.load_state_dict(torch.load(llm_model, map_location=self.device), strict=True)
|
||||
self.llm.to(self.device).eval()
|
||||
# FIX: Bundled code fix - Ensure all nested modules are moved to device
|
||||
# Issue: Some nested layers (embed_tokens, etc.) weren't being moved with .to(device)
|
||||
# causing "Expected all tensors to be on the same device" errors (GitHub #216, #212)
|
||||
for module in self.llm.modules():
|
||||
module.to(self.device)
|
||||
|
||||
self.flow.load_state_dict(torch.load(flow_model, map_location=self.device), strict=True)
|
||||
self.flow.to(self.device).eval()
|
||||
# FIX: Bundled code fix - Ensure all nested modules are moved to device
|
||||
for module in self.flow.modules():
|
||||
module.to(self.device)
|
||||
|
||||
# in case hift_model is a hifigan model
|
||||
hift_state_dict = {k.replace('generator.', ''): v for k, v in torch.load(hift_model, map_location=self.device).items()}
|
||||
self.hift.load_state_dict(hift_state_dict, strict=True)
|
||||
self.hift.to(self.device).eval()
|
||||
# FIX: Bundled code fix - Ensure all nested modules are moved to device
|
||||
for module in self.hift.modules():
|
||||
module.to(self.device)
|
||||
|
||||
def load_jit(self, llm_text_encoder_model, llm_llm_model, flow_encoder_model):
|
||||
llm_text_encoder = torch.jit.load(llm_text_encoder_model, map_location=self.device)
|
||||
self.llm.text_encoder = llm_text_encoder
|
||||
llm_llm = torch.jit.load(llm_llm_model, map_location=self.device)
|
||||
self.llm.llm = llm_llm
|
||||
flow_encoder = torch.jit.load(flow_encoder_model, map_location=self.device)
|
||||
self.flow.encoder = flow_encoder
|
||||
|
||||
def load_trt(self, flow_decoder_estimator_model, flow_decoder_onnx_model, trt_concurrent, fp16):
|
||||
assert torch.cuda.is_available(), 'tensorrt only supports gpu!'
|
||||
if not os.path.exists(flow_decoder_estimator_model) or os.path.getsize(flow_decoder_estimator_model) == 0:
|
||||
convert_onnx_to_trt(flow_decoder_estimator_model, self.get_trt_kwargs(), flow_decoder_onnx_model, fp16)
|
||||
del self.flow.decoder.estimator
|
||||
import tensorrt as trt
|
||||
with open(flow_decoder_estimator_model, 'rb') as f:
|
||||
estimator_engine = trt.Runtime(trt.Logger(trt.Logger.INFO)).deserialize_cuda_engine(f.read())
|
||||
assert estimator_engine is not None, 'failed to load trt {}'.format(flow_decoder_estimator_model)
|
||||
self.flow.decoder.estimator = TrtContextWrapper(estimator_engine, trt_concurrent=trt_concurrent, device=self.device)
|
||||
|
||||
def get_trt_kwargs(self):
|
||||
# FIX: Add missing 't' and 'spks' inputs to match ONNX export (x, mask, mu, t, spks, cond)
|
||||
min_shape = [(2, 80, 4), (2, 1, 4), (2, 80, 4), (2,), (2, 80), (2, 80, 4)]
|
||||
opt_shape = [(2, 80, 500), (2, 1, 500), (2, 80, 500), (2,), (2, 80), (2, 80, 500)]
|
||||
max_shape = [(2, 80, 3000), (2, 1, 3000), (2, 80, 3000), (2,), (2, 80), (2, 80, 3000)]
|
||||
input_names = ["x", "mask", "mu", "t", "spks", "cond"]
|
||||
return {'min_shape': min_shape, 'opt_shape': opt_shape, 'max_shape': max_shape, 'input_names': input_names}
|
||||
|
||||
def llm_job(self, text, prompt_text, llm_prompt_speech_token, llm_embedding, uuid, progress_callback=None):
|
||||
with self.llm_context, torch.cuda.amp.autocast(self.fp16 is True and hasattr(self.llm, 'vllm') is False):
|
||||
if isinstance(text, Generator):
|
||||
assert (self.__class__.__name__ != 'CosyVoiceModel') and not hasattr(self.llm, 'vllm'), 'streaming input text is only implemented for CosyVoice2/3 and do not support vllm!'
|
||||
for i in self.llm.inference_bistream(text=text,
|
||||
prompt_text=prompt_text.to(self.device),
|
||||
prompt_text_len=torch.tensor([prompt_text.shape[1]], dtype=torch.int32).to(self.device),
|
||||
prompt_speech_token=llm_prompt_speech_token.to(self.device),
|
||||
prompt_speech_token_len=torch.tensor([llm_prompt_speech_token.shape[1]], dtype=torch.int32).to(self.device),
|
||||
embedding=llm_embedding.to(self.device)):
|
||||
self.tts_speech_token_dict[uuid].append(i)
|
||||
else:
|
||||
for i in self.llm.inference(text=text.to(self.device),
|
||||
text_len=torch.tensor([text.shape[1]], dtype=torch.int32).to(self.device),
|
||||
prompt_text=prompt_text.to(self.device),
|
||||
prompt_text_len=torch.tensor([prompt_text.shape[1]], dtype=torch.int32).to(self.device),
|
||||
prompt_speech_token=llm_prompt_speech_token.to(self.device),
|
||||
prompt_speech_token_len=torch.tensor([llm_prompt_speech_token.shape[1]], dtype=torch.int32).to(self.device),
|
||||
embedding=llm_embedding.to(self.device),
|
||||
uuid=uuid,
|
||||
progress_callback=progress_callback):
|
||||
self.tts_speech_token_dict[uuid].append(i)
|
||||
self.llm_end_dict[uuid] = True
|
||||
|
||||
def vc_job(self, source_speech_token, uuid):
|
||||
self.tts_speech_token_dict[uuid] = source_speech_token.flatten().tolist()
|
||||
self.llm_end_dict[uuid] = True
|
||||
|
||||
def token2wav(self, token, prompt_token, prompt_feat, embedding, uuid, finalize=False, speed=1.0):
|
||||
with torch.cuda.amp.autocast(self.fp16):
|
||||
tts_mel, self.flow_cache_dict[uuid] = self.flow.inference(token=token.to(self.device, dtype=torch.int32),
|
||||
token_len=torch.tensor([token.shape[1]], dtype=torch.int32).to(self.device),
|
||||
prompt_token=prompt_token.to(self.device),
|
||||
prompt_token_len=torch.tensor([prompt_token.shape[1]], dtype=torch.int32).to(self.device),
|
||||
prompt_feat=prompt_feat.to(self.device),
|
||||
prompt_feat_len=torch.tensor([prompt_feat.shape[1]], dtype=torch.int32).to(self.device),
|
||||
embedding=embedding.to(self.device),
|
||||
flow_cache=self.flow_cache_dict[uuid])
|
||||
|
||||
# mel overlap fade in out
|
||||
if self.mel_overlap_dict[uuid].shape[2] != 0:
|
||||
tts_mel = fade_in_out(tts_mel, self.mel_overlap_dict[uuid], self.mel_window)
|
||||
# append hift cache
|
||||
if self.hift_cache_dict[uuid] is not None:
|
||||
hift_cache_mel, hift_cache_source = self.hift_cache_dict[uuid]['mel'], self.hift_cache_dict[uuid]['source']
|
||||
tts_mel = torch.concat([hift_cache_mel, tts_mel], dim=2)
|
||||
else:
|
||||
hift_cache_source = torch.zeros(1, 1, 0)
|
||||
# keep overlap mel and hift cache
|
||||
if finalize is False:
|
||||
self.mel_overlap_dict[uuid] = tts_mel[:, :, -self.mel_overlap_len:]
|
||||
tts_mel = tts_mel[:, :, :-self.mel_overlap_len]
|
||||
tts_speech, tts_source = self.hift.inference(speech_feat=tts_mel, cache_source=hift_cache_source)
|
||||
if self.hift_cache_dict[uuid] is not None:
|
||||
tts_speech = fade_in_out(tts_speech, self.hift_cache_dict[uuid]['speech'], self.speech_window)
|
||||
self.hift_cache_dict[uuid] = {'mel': tts_mel[:, :, -self.mel_cache_len:],
|
||||
'source': tts_source[:, :, -self.source_cache_len:],
|
||||
'speech': tts_speech[:, -self.source_cache_len:]}
|
||||
tts_speech = tts_speech[:, :-self.source_cache_len]
|
||||
else:
|
||||
if speed != 1.0:
|
||||
assert self.hift_cache_dict[uuid] is None, 'speed change only support non-stream inference mode'
|
||||
tts_mel = F.interpolate(tts_mel, size=int(tts_mel.shape[2] / speed), mode='linear')
|
||||
tts_speech, tts_source = self.hift.inference(speech_feat=tts_mel, cache_source=hift_cache_source)
|
||||
if self.hift_cache_dict[uuid] is not None:
|
||||
tts_speech = fade_in_out(tts_speech, self.hift_cache_dict[uuid]['speech'], self.speech_window)
|
||||
return tts_speech
|
||||
|
||||
def tts(self, text=torch.zeros(1, 0, dtype=torch.int32), flow_embedding=torch.zeros(0, 192), llm_embedding=torch.zeros(0, 192),
|
||||
prompt_text=torch.zeros(1, 0, dtype=torch.int32),
|
||||
llm_prompt_speech_token=torch.zeros(1, 0, dtype=torch.int32),
|
||||
flow_prompt_speech_token=torch.zeros(1, 0, dtype=torch.int32),
|
||||
prompt_speech_feat=torch.zeros(1, 0, 80), source_speech_token=torch.zeros(1, 0, dtype=torch.int32), stream=False, speed=1.0, progress_callback=None, **kwargs):
|
||||
# this_uuid is used to track variables related to this inference thread
|
||||
this_uuid = str(uuid.uuid1())
|
||||
with self.lock:
|
||||
self.tts_speech_token_dict[this_uuid], self.llm_end_dict[this_uuid] = [], False
|
||||
self.hift_cache_dict[this_uuid] = None
|
||||
self.mel_overlap_dict[this_uuid] = torch.zeros(1, 80, 0)
|
||||
self.flow_cache_dict[this_uuid] = torch.zeros(1, 80, 0, 2)
|
||||
if source_speech_token.shape[1] == 0:
|
||||
p = threading.Thread(target=self.llm_job, args=(text, prompt_text, llm_prompt_speech_token, llm_embedding, this_uuid, progress_callback))
|
||||
else:
|
||||
p = threading.Thread(target=self.vc_job, args=(source_speech_token, this_uuid))
|
||||
p.start()
|
||||
if stream is True:
|
||||
token_hop_len = self.token_min_hop_len
|
||||
while True:
|
||||
time.sleep(0.1)
|
||||
if len(self.tts_speech_token_dict[this_uuid]) >= token_hop_len + self.token_overlap_len:
|
||||
this_tts_speech_token = torch.tensor(self.tts_speech_token_dict[this_uuid][:token_hop_len + self.token_overlap_len]) \
|
||||
.unsqueeze(dim=0)
|
||||
this_tts_speech = self.token2wav(token=this_tts_speech_token,
|
||||
prompt_token=flow_prompt_speech_token,
|
||||
prompt_feat=prompt_speech_feat,
|
||||
embedding=flow_embedding,
|
||||
uuid=this_uuid,
|
||||
finalize=False)
|
||||
yield {'tts_speech': this_tts_speech.cpu()}
|
||||
with self.lock:
|
||||
self.tts_speech_token_dict[this_uuid] = self.tts_speech_token_dict[this_uuid][token_hop_len:]
|
||||
# increase token_hop_len for better speech quality
|
||||
token_hop_len = min(self.token_max_hop_len, int(token_hop_len * self.stream_scale_factor))
|
||||
if self.llm_end_dict[this_uuid] is True and len(self.tts_speech_token_dict[this_uuid]) < token_hop_len + self.token_overlap_len:
|
||||
break
|
||||
p.join()
|
||||
# deal with remain tokens, make sure inference remain token len equals token_hop_len when cache_speech is not None
|
||||
this_tts_speech_token = torch.tensor(self.tts_speech_token_dict[this_uuid]).unsqueeze(dim=0)
|
||||
this_tts_speech = self.token2wav(token=this_tts_speech_token,
|
||||
prompt_token=flow_prompt_speech_token,
|
||||
prompt_feat=prompt_speech_feat,
|
||||
embedding=flow_embedding,
|
||||
uuid=this_uuid,
|
||||
finalize=True)
|
||||
yield {'tts_speech': this_tts_speech.cpu()}
|
||||
else:
|
||||
# deal with all tokens
|
||||
p.join()
|
||||
this_tts_speech_token = torch.tensor(self.tts_speech_token_dict[this_uuid]).unsqueeze(dim=0)
|
||||
this_tts_speech = self.token2wav(token=this_tts_speech_token,
|
||||
prompt_token=flow_prompt_speech_token,
|
||||
prompt_feat=prompt_speech_feat,
|
||||
embedding=flow_embedding,
|
||||
uuid=this_uuid,
|
||||
finalize=True,
|
||||
speed=speed)
|
||||
yield {'tts_speech': this_tts_speech.cpu()}
|
||||
with self.lock:
|
||||
self.tts_speech_token_dict.pop(this_uuid)
|
||||
self.llm_end_dict.pop(this_uuid)
|
||||
self.mel_overlap_dict.pop(this_uuid)
|
||||
self.hift_cache_dict.pop(this_uuid)
|
||||
self.flow_cache_dict.pop(this_uuid)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.current_stream().synchronize()
|
||||
|
||||
|
||||
class CosyVoice2Model(CosyVoiceModel):
|
||||
|
||||
def __init__(self,
|
||||
llm: torch.nn.Module,
|
||||
flow: torch.nn.Module,
|
||||
hift: torch.nn.Module,
|
||||
fp16: bool = False):
|
||||
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
self.llm = llm
|
||||
self.flow = flow
|
||||
self.hift = hift
|
||||
self.fp16 = fp16
|
||||
# NOTE must matching training static_chunk_size
|
||||
self.token_hop_len = 25
|
||||
# hift cache
|
||||
self.mel_cache_len = 8
|
||||
self.source_cache_len = int(self.mel_cache_len * 480)
|
||||
# speech fade in out
|
||||
self.speech_window = np.hamming(2 * self.source_cache_len)
|
||||
# rtf and decoding related
|
||||
self.llm_context = torch.cuda.stream(torch.cuda.Stream(self.device)) if torch.cuda.is_available() else nullcontext()
|
||||
self.lock = threading.Lock()
|
||||
# dict used to store session related variable
|
||||
self.tts_speech_token_dict = {}
|
||||
self.llm_end_dict = {}
|
||||
self.hift_cache_dict = {}
|
||||
|
||||
def load_jit(self, flow_encoder_model):
|
||||
flow_encoder = torch.jit.load(flow_encoder_model, map_location=self.device)
|
||||
self.flow.encoder = flow_encoder
|
||||
|
||||
def load_vllm(self, model_dir):
|
||||
export_cosyvoice2_vllm(self.llm, model_dir, self.device)
|
||||
from vllm import EngineArgs, LLMEngine
|
||||
engine_args = EngineArgs(model=model_dir,
|
||||
skip_tokenizer_init=True,
|
||||
enable_prompt_embeds=True,
|
||||
gpu_memory_utilization=0.2)
|
||||
self.llm.vllm = LLMEngine.from_engine_args(engine_args)
|
||||
self.llm.lock = threading.Lock()
|
||||
del self.llm.llm.model.model.layers
|
||||
|
||||
def token2wav(self, token, prompt_token, prompt_feat, embedding, token_offset, uuid, stream=False, finalize=False, speed=1.0):
|
||||
with torch.cuda.amp.autocast(self.fp16):
|
||||
tts_mel, _ = self.flow.inference(token=token.to(self.device, dtype=torch.int32),
|
||||
token_len=torch.tensor([token.shape[1]], dtype=torch.int32).to(self.device),
|
||||
prompt_token=prompt_token.to(self.device),
|
||||
prompt_token_len=torch.tensor([prompt_token.shape[1]], dtype=torch.int32).to(self.device),
|
||||
prompt_feat=prompt_feat.to(self.device),
|
||||
prompt_feat_len=torch.tensor([prompt_feat.shape[1]], dtype=torch.int32).to(self.device),
|
||||
embedding=embedding.to(self.device),
|
||||
streaming=stream,
|
||||
finalize=finalize)
|
||||
tts_mel = tts_mel[:, :, token_offset * self.flow.token_mel_ratio:]
|
||||
# append hift cache
|
||||
if self.hift_cache_dict[uuid] is not None:
|
||||
hift_cache_mel, hift_cache_source = self.hift_cache_dict[uuid]['mel'], self.hift_cache_dict[uuid]['source']
|
||||
tts_mel = torch.concat([hift_cache_mel, tts_mel], dim=2)
|
||||
else:
|
||||
hift_cache_source = torch.zeros(1, 1, 0)
|
||||
# keep overlap mel and hift cache
|
||||
if finalize is False:
|
||||
tts_speech, tts_source = self.hift.inference(speech_feat=tts_mel, cache_source=hift_cache_source)
|
||||
if self.hift_cache_dict[uuid] is not None:
|
||||
tts_speech = fade_in_out(tts_speech, self.hift_cache_dict[uuid]['speech'], self.speech_window)
|
||||
self.hift_cache_dict[uuid] = {'mel': tts_mel[:, :, -self.mel_cache_len:],
|
||||
'source': tts_source[:, :, -self.source_cache_len:],
|
||||
'speech': tts_speech[:, -self.source_cache_len:]}
|
||||
tts_speech = tts_speech[:, :-self.source_cache_len]
|
||||
else:
|
||||
if speed != 1.0:
|
||||
assert self.hift_cache_dict[uuid] is None, 'speed change only support non-stream inference mode'
|
||||
tts_mel = F.interpolate(tts_mel, size=int(tts_mel.shape[2] / speed), mode='linear')
|
||||
tts_speech, tts_source = self.hift.inference(speech_feat=tts_mel, cache_source=hift_cache_source)
|
||||
if self.hift_cache_dict[uuid] is not None:
|
||||
tts_speech = fade_in_out(tts_speech, self.hift_cache_dict[uuid]['speech'], self.speech_window)
|
||||
return tts_speech
|
||||
|
||||
def tts(self, text=torch.zeros(1, 0, dtype=torch.int32), flow_embedding=torch.zeros(0, 192), llm_embedding=torch.zeros(0, 192),
|
||||
prompt_text=torch.zeros(1, 0, dtype=torch.int32),
|
||||
llm_prompt_speech_token=torch.zeros(1, 0, dtype=torch.int32),
|
||||
flow_prompt_speech_token=torch.zeros(1, 0, dtype=torch.int32),
|
||||
prompt_speech_feat=torch.zeros(1, 0, 80), source_speech_token=torch.zeros(1, 0, dtype=torch.int32), stream=False, speed=1.0, progress_callback=None, **kwargs):
|
||||
# this_uuid is used to track variables related to this inference thread
|
||||
this_uuid = str(uuid.uuid1())
|
||||
with self.lock:
|
||||
self.tts_speech_token_dict[this_uuid], self.llm_end_dict[this_uuid] = [], False
|
||||
self.hift_cache_dict[this_uuid] = None
|
||||
if source_speech_token.shape[1] == 0:
|
||||
p = threading.Thread(target=self.llm_job, args=(text, prompt_text, llm_prompt_speech_token, llm_embedding, this_uuid, progress_callback))
|
||||
else:
|
||||
p = threading.Thread(target=self.vc_job, args=(source_speech_token, this_uuid))
|
||||
p.start()
|
||||
if stream is True:
|
||||
token_offset = 0
|
||||
prompt_token_pad = int(np.ceil(flow_prompt_speech_token.shape[1] / self.token_hop_len) * self.token_hop_len - flow_prompt_speech_token.shape[1])
|
||||
while True:
|
||||
time.sleep(0.1)
|
||||
this_token_hop_len = self.token_hop_len + prompt_token_pad if token_offset == 0 else self.token_hop_len
|
||||
if len(self.tts_speech_token_dict[this_uuid]) - token_offset >= this_token_hop_len + self.flow.pre_lookahead_len:
|
||||
this_tts_speech_token = torch.tensor(self.tts_speech_token_dict[this_uuid][:token_offset + this_token_hop_len + self.flow.pre_lookahead_len]).unsqueeze(dim=0)
|
||||
this_tts_speech = self.token2wav(token=this_tts_speech_token,
|
||||
prompt_token=flow_prompt_speech_token,
|
||||
prompt_feat=prompt_speech_feat,
|
||||
embedding=flow_embedding,
|
||||
token_offset=token_offset,
|
||||
uuid=this_uuid,
|
||||
stream=stream,
|
||||
finalize=False)
|
||||
token_offset += this_token_hop_len
|
||||
yield {'tts_speech': this_tts_speech.cpu()}
|
||||
if self.llm_end_dict[this_uuid] is True and len(self.tts_speech_token_dict[this_uuid]) - token_offset < this_token_hop_len + self.flow.pre_lookahead_len:
|
||||
break
|
||||
p.join()
|
||||
# deal with remain tokens, make sure inference remain token len equals token_hop_len when cache_speech is not None
|
||||
this_tts_speech_token = torch.tensor(self.tts_speech_token_dict[this_uuid]).unsqueeze(dim=0)
|
||||
this_tts_speech = self.token2wav(token=this_tts_speech_token,
|
||||
prompt_token=flow_prompt_speech_token,
|
||||
prompt_feat=prompt_speech_feat,
|
||||
embedding=flow_embedding,
|
||||
token_offset=token_offset,
|
||||
uuid=this_uuid,
|
||||
finalize=True)
|
||||
yield {'tts_speech': this_tts_speech.cpu()}
|
||||
else:
|
||||
# deal with all tokens
|
||||
p.join()
|
||||
this_tts_speech_token = torch.tensor(self.tts_speech_token_dict[this_uuid]).unsqueeze(dim=0)
|
||||
this_tts_speech = self.token2wav(token=this_tts_speech_token,
|
||||
prompt_token=flow_prompt_speech_token,
|
||||
prompt_feat=prompt_speech_feat,
|
||||
embedding=flow_embedding,
|
||||
token_offset=0,
|
||||
uuid=this_uuid,
|
||||
finalize=True,
|
||||
speed=speed)
|
||||
yield {'tts_speech': this_tts_speech.cpu()}
|
||||
with self.lock:
|
||||
self.tts_speech_token_dict.pop(this_uuid)
|
||||
self.llm_end_dict.pop(this_uuid)
|
||||
self.hift_cache_dict.pop(this_uuid)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.current_stream().synchronize()
|
||||
|
||||
|
||||
class CosyVoice3Model(CosyVoice2Model):
|
||||
|
||||
def __init__(self,
|
||||
llm: torch.nn.Module,
|
||||
flow: torch.nn.Module,
|
||||
hift: torch.nn.Module,
|
||||
fp16: bool = False):
|
||||
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
self.llm = llm
|
||||
self.flow = flow
|
||||
self.hift = hift
|
||||
self.fp16 = fp16
|
||||
# NOTE must matching training static_chunk_size
|
||||
self.token_hop_len = 25
|
||||
# rtf and decoding related
|
||||
self.llm_context = torch.cuda.stream(torch.cuda.Stream(self.device)) if torch.cuda.is_available() else nullcontext()
|
||||
self.lock = threading.Lock()
|
||||
# dict used to store session related variable
|
||||
self.tts_speech_token_dict = {}
|
||||
self.llm_end_dict = {}
|
||||
self.hift_cache_dict = {}
|
||||
|
||||
def token2wav(self, token, prompt_token, prompt_feat, embedding, token_offset, uuid, stream=False, finalize=False, speed=1.0):
|
||||
with torch.cuda.amp.autocast(self.fp16):
|
||||
tts_mel, _ = self.flow.inference(token=token.to(self.device, dtype=torch.int32),
|
||||
token_len=torch.tensor([token.shape[1]], dtype=torch.int32).to(self.device),
|
||||
prompt_token=prompt_token.to(self.device),
|
||||
prompt_token_len=torch.tensor([prompt_token.shape[1]], dtype=torch.int32).to(self.device),
|
||||
prompt_feat=prompt_feat.to(self.device),
|
||||
prompt_feat_len=torch.tensor([prompt_feat.shape[1]], dtype=torch.int32).to(self.device),
|
||||
embedding=embedding.to(self.device),
|
||||
streaming=stream,
|
||||
finalize=finalize)
|
||||
tts_mel = tts_mel[:, :, token_offset * self.flow.token_mel_ratio:]
|
||||
# append mel cache
|
||||
if self.hift_cache_dict[uuid] is not None:
|
||||
hift_cache_mel = self.hift_cache_dict[uuid]['mel']
|
||||
tts_mel = torch.concat([hift_cache_mel, tts_mel], dim=2)
|
||||
self.hift_cache_dict[uuid]['mel'] = tts_mel
|
||||
else:
|
||||
self.hift_cache_dict[uuid] = {'mel': tts_mel, 'speech_offset': 0}
|
||||
if speed != 1.0:
|
||||
assert token_offset == 0 and finalize is True, 'speed change only support non-stream inference mode'
|
||||
tts_mel = F.interpolate(tts_mel, size=int(tts_mel.shape[2] / speed), mode='linear')
|
||||
tts_speech, _ = self.hift.inference(speech_feat=tts_mel, finalize=finalize)
|
||||
tts_speech = tts_speech[:, self.hift_cache_dict[uuid]['speech_offset']:]
|
||||
self.hift_cache_dict[uuid]['speech_offset'] += tts_speech.shape[1]
|
||||
return tts_speech
|
||||
@@ -0,0 +1,151 @@
|
||||
# Copyright (c) 2021 Mobvoi Inc. (authors: 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 random
|
||||
import math
|
||||
from functools import partial
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from torch.utils.data import IterableDataset
|
||||
from cosyvoice.utils.file_utils import read_lists
|
||||
|
||||
|
||||
class Processor(IterableDataset):
|
||||
|
||||
def __init__(self, source, f, *args, **kw):
|
||||
assert callable(f)
|
||||
self.source = source
|
||||
self.f = f
|
||||
self.args = args
|
||||
self.kw = kw
|
||||
|
||||
def set_epoch(self, epoch):
|
||||
self.source.set_epoch(epoch)
|
||||
|
||||
def __iter__(self):
|
||||
""" Return an iterator over the source dataset processed by the
|
||||
given processor.
|
||||
"""
|
||||
assert self.source is not None
|
||||
assert callable(self.f)
|
||||
return self.f(iter(self.source), *self.args, **self.kw)
|
||||
|
||||
def apply(self, f):
|
||||
assert callable(f)
|
||||
return Processor(self, f, *self.args, **self.kw)
|
||||
|
||||
|
||||
class DistributedSampler:
|
||||
|
||||
def __init__(self, shuffle=True, partition=True):
|
||||
self.epoch = -1
|
||||
self.update()
|
||||
self.shuffle = shuffle
|
||||
self.partition = partition
|
||||
|
||||
def update(self):
|
||||
assert dist.is_available()
|
||||
if dist.is_initialized():
|
||||
self.rank = dist.get_rank()
|
||||
self.world_size = dist.get_world_size()
|
||||
else:
|
||||
self.rank = 0
|
||||
self.world_size = 1
|
||||
worker_info = torch.utils.data.get_worker_info()
|
||||
if worker_info is None:
|
||||
self.worker_id = 0
|
||||
self.num_workers = 1
|
||||
else:
|
||||
self.worker_id = worker_info.id
|
||||
self.num_workers = worker_info.num_workers
|
||||
return dict(rank=self.rank,
|
||||
world_size=self.world_size,
|
||||
worker_id=self.worker_id,
|
||||
num_workers=self.num_workers)
|
||||
|
||||
def set_epoch(self, epoch):
|
||||
self.epoch = epoch
|
||||
|
||||
def sample(self, data):
|
||||
""" Sample data according to rank/world_size/num_workers
|
||||
|
||||
Args:
|
||||
data(List): input data list
|
||||
|
||||
Returns:
|
||||
List: data list after sample
|
||||
"""
|
||||
data = list(range(len(data)))
|
||||
# force datalist even
|
||||
if self.partition:
|
||||
if self.shuffle:
|
||||
random.Random(self.epoch).shuffle(data)
|
||||
if len(data) < self.world_size:
|
||||
data = data * math.ceil(self.world_size / len(data))
|
||||
data = data[:self.world_size]
|
||||
data = data[self.rank::self.world_size]
|
||||
if len(data) < self.num_workers:
|
||||
data = data * math.ceil(self.num_workers / len(data))
|
||||
data = data[:self.num_workers]
|
||||
data = data[self.worker_id::self.num_workers]
|
||||
return data
|
||||
|
||||
|
||||
class DataList(IterableDataset):
|
||||
|
||||
def __init__(self, lists, shuffle=True, partition=True):
|
||||
self.lists = lists
|
||||
self.sampler = DistributedSampler(shuffle, partition)
|
||||
|
||||
def set_epoch(self, epoch):
|
||||
self.sampler.set_epoch(epoch)
|
||||
|
||||
def __iter__(self):
|
||||
sampler_info = self.sampler.update()
|
||||
indexes = self.sampler.sample(self.lists)
|
||||
for index in indexes:
|
||||
data = dict(src=self.lists[index])
|
||||
data.update(sampler_info)
|
||||
yield data
|
||||
|
||||
|
||||
def Dataset(data_list_file,
|
||||
data_pipeline,
|
||||
mode='train',
|
||||
gan=False,
|
||||
dpo=False,
|
||||
shuffle=True,
|
||||
partition=True):
|
||||
""" Construct dataset from arguments
|
||||
|
||||
We have two shuffle stage in the Dataset. The first is global
|
||||
shuffle at shards tar/raw file level. The second is global shuffle
|
||||
at training samples level.
|
||||
|
||||
Args:
|
||||
data_type(str): raw/shard
|
||||
tokenizer (BaseTokenizer): tokenizer to tokenize
|
||||
partition(bool): whether to do data partition in terms of rank
|
||||
"""
|
||||
lists = read_lists(data_list_file)
|
||||
dataset = DataList(lists,
|
||||
shuffle=shuffle,
|
||||
partition=partition)
|
||||
# map partial arg to padding func
|
||||
data_pipeline[-1] = partial(data_pipeline[-1], gan=gan, dpo=dpo)
|
||||
for func in data_pipeline:
|
||||
dataset = Processor(dataset, func, mode=mode)
|
||||
return dataset
|
||||
@@ -0,0 +1,443 @@
|
||||
# Copyright (c) 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 logging
|
||||
import random
|
||||
|
||||
import pyarrow.parquet as pq
|
||||
from io import BytesIO
|
||||
import torch
|
||||
import torchaudio
|
||||
from torch.nn.utils.rnn import pad_sequence
|
||||
import torch.nn.functional as F
|
||||
import pyworld as pw
|
||||
|
||||
|
||||
AUDIO_FORMAT_SETS = {'flac', 'mp3', 'm4a', 'ogg', 'opus', 'wav', 'wma'}
|
||||
|
||||
|
||||
def parquet_opener(data, mode='train', tts_data={}):
|
||||
""" Give url or local file, return file descriptor
|
||||
Inplace operation.
|
||||
|
||||
Args:
|
||||
data(Iterable[str]): url or local file list
|
||||
|
||||
Returns:
|
||||
Iterable[{src, stream}]
|
||||
"""
|
||||
for sample in data:
|
||||
assert 'src' in sample
|
||||
url = sample['src']
|
||||
try:
|
||||
for df in pq.ParquetFile(url).iter_batches(batch_size=64):
|
||||
df = df.to_pandas()
|
||||
for i in range(len(df)):
|
||||
sample.update(dict(df.loc[i]))
|
||||
if mode == 'train':
|
||||
# NOTE do not return sample directly, must initialize a new dict
|
||||
yield {**sample}
|
||||
else:
|
||||
for index, text in enumerate(tts_data[df.loc[i, 'utt']]):
|
||||
yield {**sample, 'tts_index': index, 'tts_text': text}
|
||||
except Exception as ex:
|
||||
logging.warning('Failed to open {}, ex info {}'.format(url, ex))
|
||||
|
||||
|
||||
def filter(data,
|
||||
max_length=10240,
|
||||
min_length=10,
|
||||
token_max_length=200,
|
||||
token_min_length=1,
|
||||
min_output_input_ratio=0.0005,
|
||||
max_output_input_ratio=1,
|
||||
mode='train'):
|
||||
""" Filter sample according to feature and label length
|
||||
Inplace operation.
|
||||
|
||||
Args::
|
||||
data: Iterable[{key, wav, label, sample_rate}]
|
||||
max_length: drop utterance which is greater than max_length(10ms)
|
||||
min_length: drop utterance which is less than min_length(10ms)
|
||||
token_max_length: drop utterance which is greater than
|
||||
token_max_length, especially when use char unit for
|
||||
english modeling
|
||||
token_min_length: drop utterance which is
|
||||
less than token_max_length
|
||||
min_output_input_ratio: minimal ration of
|
||||
token_length / feats_length(10ms)
|
||||
max_output_input_ratio: maximum ration of
|
||||
token_length / feats_length(10ms)
|
||||
|
||||
Returns:
|
||||
Iterable[{key, wav, label, sample_rate}]
|
||||
"""
|
||||
for sample in data:
|
||||
sample['speech'], sample['sample_rate'] = torchaudio.load(BytesIO(sample['audio_data']))
|
||||
sample['speech'] = sample['speech'].mean(dim=0, keepdim=True)
|
||||
del sample['audio_data']
|
||||
# sample['wav'] is torch.Tensor, we have 100 frames every second
|
||||
num_frames = sample['speech'].size(1) / sample['sample_rate'] * 100
|
||||
if num_frames < min_length:
|
||||
continue
|
||||
if num_frames > max_length:
|
||||
continue
|
||||
if len(sample['text_token']) < token_min_length:
|
||||
continue
|
||||
if len(sample['text_token']) > token_max_length:
|
||||
continue
|
||||
if len(sample['speech_token']) == 0:
|
||||
continue
|
||||
if 'reject_speech_token' in sample and len(sample['reject_speech_token']) == 0:
|
||||
continue
|
||||
if num_frames != 0:
|
||||
if len(sample['text_token']) / num_frames < min_output_input_ratio:
|
||||
continue
|
||||
if len(sample['text_token']) / num_frames > max_output_input_ratio:
|
||||
continue
|
||||
yield sample
|
||||
|
||||
|
||||
def resample(data, resample_rate=22050, min_sample_rate=16000, mode='train'):
|
||||
""" Resample data.
|
||||
Inplace operation.
|
||||
|
||||
Args:
|
||||
data: Iterable[{key, wav, label, sample_rate}]
|
||||
resample_rate: target resample rate
|
||||
|
||||
Returns:
|
||||
Iterable[{key, wav, label, sample_rate}]
|
||||
"""
|
||||
for sample in data:
|
||||
assert 'sample_rate' in sample
|
||||
assert 'speech' in sample
|
||||
sample_rate = sample['sample_rate']
|
||||
waveform = sample['speech']
|
||||
if sample_rate != resample_rate:
|
||||
if sample_rate < min_sample_rate:
|
||||
continue
|
||||
sample['sample_rate'] = resample_rate
|
||||
sample['speech'] = torchaudio.transforms.Resample(
|
||||
orig_freq=sample_rate, new_freq=resample_rate)(waveform)
|
||||
max_val = sample['speech'].abs().max()
|
||||
if max_val > 1:
|
||||
sample['speech'] /= max_val
|
||||
yield sample
|
||||
|
||||
|
||||
def truncate(data, truncate_length=24576, mode='train'):
|
||||
""" Truncate data.
|
||||
|
||||
Args:
|
||||
data: Iterable[{key, wav, label, sample_rate}]
|
||||
truncate_length: truncate length
|
||||
|
||||
Returns:
|
||||
Iterable[{key, wav, label, sample_rate}]
|
||||
"""
|
||||
for sample in data:
|
||||
waveform = sample['speech']
|
||||
if waveform.shape[1] > truncate_length:
|
||||
start = random.randint(0, waveform.shape[1] - truncate_length)
|
||||
waveform = waveform[:, start: start + truncate_length]
|
||||
else:
|
||||
waveform = torch.concat([waveform, torch.zeros(1, truncate_length - waveform.shape[1])], dim=1)
|
||||
sample['speech'] = waveform
|
||||
yield sample
|
||||
|
||||
|
||||
def compute_fbank(data,
|
||||
feat_extractor,
|
||||
token_mel_ratio=0,
|
||||
mode='train'):
|
||||
""" Extract fbank
|
||||
|
||||
Args:
|
||||
data: Iterable[{key, wav, label, sample_rate}]
|
||||
|
||||
Returns:
|
||||
Iterable[{key, feat, label}]
|
||||
"""
|
||||
for sample in data:
|
||||
assert 'sample_rate' in sample
|
||||
assert 'speech' in sample
|
||||
assert 'utt' in sample
|
||||
assert 'text_token' in sample
|
||||
waveform = sample['speech']
|
||||
feat = feat_extractor(waveform).squeeze(dim=0).transpose(0, 1)
|
||||
if token_mel_ratio != 0:
|
||||
# trim to align speech_token and speech_feat
|
||||
token_len = int(min(feat.shape[0] / token_mel_ratio, sample["speech_token"].shape[0]))
|
||||
feat = feat[:token_mel_ratio * token_len]
|
||||
sample["speech_token"] = sample["speech_token"][:token_len]
|
||||
sample['speech_feat'] = feat
|
||||
yield sample
|
||||
|
||||
|
||||
def compute_f0(data, sample_rate, hop_size, mode='train'):
|
||||
""" Extract f0
|
||||
|
||||
Args:
|
||||
data: Iterable[{key, wav, label, sample_rate}]
|
||||
|
||||
Returns:
|
||||
Iterable[{key, feat, label}]
|
||||
"""
|
||||
frame_period = hop_size * 1000 / sample_rate
|
||||
for sample in data:
|
||||
assert 'sample_rate' in sample
|
||||
assert 'speech' in sample
|
||||
assert 'utt' in sample
|
||||
assert 'text_token' in sample
|
||||
waveform = sample['speech']
|
||||
_f0, t = pw.harvest(waveform.squeeze(dim=0).numpy().astype('double'), sample_rate, frame_period=frame_period)
|
||||
if sum(_f0 != 0) < 5: # this happens when the algorithm fails
|
||||
_f0, t = pw.dio(waveform.squeeze(dim=0).numpy().astype('double'), sample_rate, frame_period=frame_period) # if harvest fails, try dio
|
||||
f0 = pw.stonemask(waveform.squeeze(dim=0).numpy().astype('double'), _f0, t, sample_rate)
|
||||
f0 = F.interpolate(torch.from_numpy(f0).view(1, 1, -1), size=sample['speech_feat'].shape[0], mode='linear').view(-1)
|
||||
sample['pitch_feat'] = f0
|
||||
yield sample
|
||||
|
||||
|
||||
def parse_embedding(data, normalize, mode='train'):
|
||||
""" Parse utt_embedding/spk_embedding
|
||||
|
||||
Args:
|
||||
data: Iterable[{key, wav, label, sample_rate}]
|
||||
|
||||
Returns:
|
||||
Iterable[{key, feat, label}]
|
||||
"""
|
||||
for sample in data:
|
||||
sample['utt_embedding'] = torch.tensor(sample['utt_embedding'], dtype=torch.float32)
|
||||
sample['spk_embedding'] = torch.tensor(sample['spk_embedding'], dtype=torch.float32)
|
||||
if normalize:
|
||||
sample['utt_embedding'] = F.normalize(sample['utt_embedding'], dim=0)
|
||||
sample['spk_embedding'] = F.normalize(sample['spk_embedding'], dim=0)
|
||||
yield sample
|
||||
|
||||
|
||||
def tokenize(data, get_tokenizer, allowed_special, mode='train'):
|
||||
""" Decode text to chars or BPE
|
||||
Inplace operation
|
||||
|
||||
Args:
|
||||
data: Iterable[{key, wav, txt, sample_rate}]
|
||||
|
||||
Returns:
|
||||
Iterable[{key, wav, txt, tokens, label, sample_rate}]
|
||||
"""
|
||||
tokenizer = get_tokenizer()
|
||||
for sample in data:
|
||||
assert 'text' in sample
|
||||
sample['text_token'] = tokenizer.encode(sample['text'], allowed_special=allowed_special)
|
||||
if 'instruct' in sample:
|
||||
sample['instruct_token'] = tokenizer.encode(sample['instruct'], allowed_special=allowed_special)
|
||||
else:
|
||||
sample['instruct_token'] = tokenizer.encode('', allowed_special=allowed_special)
|
||||
yield sample
|
||||
|
||||
|
||||
def shuffle(data, shuffle_size=10000, mode='train'):
|
||||
""" Local shuffle the data
|
||||
|
||||
Args:
|
||||
data: Iterable[{key, feat, label}]
|
||||
shuffle_size: buffer size for shuffle
|
||||
|
||||
Returns:
|
||||
Iterable[{key, feat, label}]
|
||||
"""
|
||||
buf = []
|
||||
for sample in data:
|
||||
buf.append(sample)
|
||||
if len(buf) >= shuffle_size:
|
||||
random.shuffle(buf)
|
||||
for x in buf:
|
||||
yield x
|
||||
buf = []
|
||||
# The sample left over
|
||||
random.shuffle(buf)
|
||||
for x in buf:
|
||||
yield x
|
||||
|
||||
|
||||
def sort(data, sort_size=500, mode='train'):
|
||||
""" Sort the data by feature length.
|
||||
Sort is used after shuffle and before batch, so we can group
|
||||
utts with similar lengths into a batch, and `sort_size` should
|
||||
be less than `shuffle_size`
|
||||
|
||||
Args:
|
||||
data: Iterable[{key, feat, label}]
|
||||
sort_size: buffer size for sort
|
||||
|
||||
Returns:
|
||||
Iterable[{key, feat, label}]
|
||||
"""
|
||||
|
||||
buf = []
|
||||
for sample in data:
|
||||
buf.append(sample)
|
||||
if len(buf) >= sort_size:
|
||||
buf.sort(key=lambda x: x['speech_feat'].size(0))
|
||||
for x in buf:
|
||||
yield x
|
||||
buf = []
|
||||
# The sample left over
|
||||
buf.sort(key=lambda x: x['speech_feat'].size(0))
|
||||
for x in buf:
|
||||
yield x
|
||||
|
||||
|
||||
def static_batch(data, batch_size=16):
|
||||
""" Static batch the data by `batch_size`
|
||||
|
||||
Args:
|
||||
data: Iterable[{key, feat, label}]
|
||||
batch_size: batch size
|
||||
|
||||
Returns:
|
||||
Iterable[List[{key, feat, label}]]
|
||||
"""
|
||||
buf = []
|
||||
for sample in data:
|
||||
buf.append(sample)
|
||||
if len(buf) >= batch_size:
|
||||
yield buf
|
||||
buf = []
|
||||
if len(buf) > 0:
|
||||
yield buf
|
||||
|
||||
|
||||
def dynamic_batch(data, max_frames_in_batch=12000, mode='train'):
|
||||
""" Dynamic batch the data until the total frames in batch
|
||||
reach `max_frames_in_batch`
|
||||
|
||||
Args:
|
||||
data: Iterable[{key, feat, label}]
|
||||
max_frames_in_batch: max_frames in one batch
|
||||
|
||||
Returns:
|
||||
Iterable[List[{key, feat, label}]]
|
||||
"""
|
||||
buf = []
|
||||
longest_frames = 0
|
||||
for sample in data:
|
||||
assert 'speech_feat' in sample
|
||||
assert isinstance(sample['speech_feat'], torch.Tensor)
|
||||
new_sample_frames = sample['speech_feat'].size(0)
|
||||
longest_frames = max(longest_frames, new_sample_frames)
|
||||
frames_after_padding = longest_frames * (len(buf) + 1)
|
||||
if frames_after_padding > max_frames_in_batch:
|
||||
yield buf
|
||||
buf = [sample]
|
||||
longest_frames = new_sample_frames
|
||||
else:
|
||||
buf.append(sample)
|
||||
if len(buf) > 0:
|
||||
yield buf
|
||||
|
||||
|
||||
def batch(data, batch_type='static', batch_size=16, max_frames_in_batch=12000, mode='train'):
|
||||
""" Wrapper for static/dynamic batch
|
||||
"""
|
||||
if batch_type == 'static':
|
||||
return static_batch(data, batch_size)
|
||||
elif batch_type == 'dynamic':
|
||||
return dynamic_batch(data, max_frames_in_batch)
|
||||
else:
|
||||
logging.fatal('Unsupported batch type {}'.format(batch_type))
|
||||
|
||||
|
||||
def padding(data, use_spk_embedding, mode='train', gan=False, dpo=False):
|
||||
""" Padding the data into training data
|
||||
|
||||
Args:
|
||||
data: Iterable[List[{key, feat, label}]]
|
||||
|
||||
Returns:
|
||||
Iterable[Tuple(keys, feats, labels, feats lengths, label lengths)]
|
||||
"""
|
||||
for sample in data:
|
||||
assert isinstance(sample, list)
|
||||
speech_feat_len = torch.tensor([x['speech_feat'].size(1) for x in sample],
|
||||
dtype=torch.int32)
|
||||
order = torch.argsort(speech_feat_len, descending=True)
|
||||
|
||||
utts = [sample[i]['utt'] for i in order]
|
||||
speech = [sample[i]['speech'].squeeze(dim=0) for i in order]
|
||||
speech_len = torch.tensor([i.size(0) for i in speech], dtype=torch.int32)
|
||||
speech = pad_sequence(speech, batch_first=True, padding_value=0)
|
||||
speech_token = [torch.tensor(sample[i]['speech_token']) for i in order]
|
||||
speech_token_len = torch.tensor([i.size(0) for i in speech_token], dtype=torch.int32)
|
||||
speech_token = pad_sequence(speech_token,
|
||||
batch_first=True,
|
||||
padding_value=0)
|
||||
speech_feat = [sample[i]['speech_feat'] for i in order]
|
||||
speech_feat_len = torch.tensor([i.size(0) for i in speech_feat], dtype=torch.int32)
|
||||
speech_feat = pad_sequence(speech_feat,
|
||||
batch_first=True,
|
||||
padding_value=0)
|
||||
text = [sample[i]['text'] for i in order]
|
||||
text_token = [torch.tensor(sample[i]['text_token']) for i in order]
|
||||
text_token_len = torch.tensor([i.size(0) for i in text_token], dtype=torch.int32)
|
||||
text_token = pad_sequence(text_token, batch_first=True, padding_value=0)
|
||||
instruct_token = [torch.tensor(sample[i]['instruct_token']) for i in order]
|
||||
instruct_token_len = torch.tensor([i.size(0) for i in instruct_token], dtype=torch.int32)
|
||||
instruct_token = pad_sequence(instruct_token, batch_first=True, padding_value=0)
|
||||
utt_embedding = torch.stack([sample[i]['utt_embedding'] for i in order], dim=0)
|
||||
spk_embedding = torch.stack([sample[i]['spk_embedding'] for i in order], dim=0)
|
||||
batch = {
|
||||
"utts": utts,
|
||||
"speech": speech,
|
||||
"speech_len": speech_len,
|
||||
"speech_token": speech_token,
|
||||
"speech_token_len": speech_token_len,
|
||||
"speech_feat": speech_feat,
|
||||
"speech_feat_len": speech_feat_len,
|
||||
"text": text,
|
||||
"text_token": text_token,
|
||||
"text_token_len": text_token_len,
|
||||
"instruct_token": instruct_token,
|
||||
"instruct_token_len": instruct_token_len,
|
||||
"utt_embedding": utt_embedding,
|
||||
"spk_embedding": spk_embedding,
|
||||
}
|
||||
if gan is True:
|
||||
# in gan train, we need pitch_feat
|
||||
pitch_feat = [sample[i]['pitch_feat'] for i in order]
|
||||
pitch_feat_len = torch.tensor([i.size(0) for i in pitch_feat], dtype=torch.int32)
|
||||
pitch_feat = pad_sequence(pitch_feat,
|
||||
batch_first=True,
|
||||
padding_value=0)
|
||||
batch["pitch_feat"] = pitch_feat
|
||||
batch["pitch_feat_len"] = pitch_feat_len
|
||||
else:
|
||||
# only gan train needs speech, delete it to save memory
|
||||
del batch["speech"]
|
||||
del batch["speech_len"]
|
||||
if dpo is True:
|
||||
reject_speech_token = [torch.tensor(sample[i]['reject_speech_token']) for i in order]
|
||||
reject_speech_token_len = torch.tensor([i.size(0) for i in reject_speech_token], dtype=torch.int32)
|
||||
reject_speech_token = pad_sequence(reject_speech_token,
|
||||
batch_first=True,
|
||||
padding_value=0)
|
||||
batch['reject_speech_token'] = reject_speech_token
|
||||
batch['reject_speech_token_len'] = reject_speech_token_len
|
||||
if use_spk_embedding is True:
|
||||
batch["embedding"] = batch["spk_embedding"]
|
||||
else:
|
||||
batch["embedding"] = batch["utt_embedding"]
|
||||
yield batch
|
||||
@@ -0,0 +1,176 @@
|
||||
|
||||
"""
|
||||
ein notation:
|
||||
b - batch
|
||||
n - sequence
|
||||
nt - text sequence
|
||||
nw - raw wave length
|
||||
d - dimension
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
import torch.nn.functional as F
|
||||
from einops import repeat
|
||||
from x_transformers.x_transformers import RotaryEmbedding
|
||||
from cosyvoice.utils.mask import add_optional_chunk_mask
|
||||
from cosyvoice.flow.DiT.modules import (
|
||||
TimestepEmbedding,
|
||||
ConvNeXtV2Block,
|
||||
CausalConvPositionEmbedding,
|
||||
DiTBlock,
|
||||
AdaLayerNormZero_Final,
|
||||
precompute_freqs_cis,
|
||||
get_pos_embed_indices,
|
||||
)
|
||||
|
||||
|
||||
# Text embedding
|
||||
|
||||
|
||||
class TextEmbedding(nn.Module):
|
||||
def __init__(self, text_num_embeds, text_dim, conv_layers=0, conv_mult=2):
|
||||
super().__init__()
|
||||
self.text_embed = nn.Embedding(text_num_embeds + 1, text_dim) # use 0 as filler token
|
||||
|
||||
if conv_layers > 0:
|
||||
self.extra_modeling = True
|
||||
self.precompute_max_pos = 4096 # ~44s of 24khz audio
|
||||
self.register_buffer("freqs_cis", precompute_freqs_cis(text_dim, self.precompute_max_pos), persistent=False)
|
||||
self.text_blocks = nn.Sequential(
|
||||
*[ConvNeXtV2Block(text_dim, text_dim * conv_mult) for _ in range(conv_layers)]
|
||||
)
|
||||
else:
|
||||
self.extra_modeling = False
|
||||
|
||||
def forward(self, text: int["b nt"], seq_len, drop_text=False): # noqa: F722
|
||||
batch, text_len = text.shape[0], text.shape[1]
|
||||
text = text + 1 # use 0 as filler token. preprocess of batch pad -1, see list_str_to_idx()
|
||||
text = text[:, :seq_len] # curtail if character tokens are more than the mel spec tokens
|
||||
text = F.pad(text, (0, seq_len - text_len), value=0)
|
||||
|
||||
if drop_text: # cfg for text
|
||||
text = torch.zeros_like(text)
|
||||
|
||||
text = self.text_embed(text) # b n -> b n d
|
||||
|
||||
# possible extra modeling
|
||||
if self.extra_modeling:
|
||||
# sinus pos emb
|
||||
batch_start = torch.zeros((batch,), dtype=torch.long)
|
||||
pos_idx = get_pos_embed_indices(batch_start, seq_len, max_pos=self.precompute_max_pos)
|
||||
text_pos_embed = self.freqs_cis[pos_idx]
|
||||
text = text + text_pos_embed
|
||||
|
||||
# convnextv2 blocks
|
||||
text = self.text_blocks(text)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
# noised input audio and context mixing embedding
|
||||
|
||||
|
||||
class InputEmbedding(nn.Module):
|
||||
def __init__(self, mel_dim, text_dim, out_dim, spk_dim=None):
|
||||
super().__init__()
|
||||
spk_dim = 0 if spk_dim is None else spk_dim
|
||||
self.spk_dim = spk_dim
|
||||
self.proj = nn.Linear(mel_dim * 2 + text_dim + spk_dim, out_dim)
|
||||
self.conv_pos_embed = CausalConvPositionEmbedding(dim=out_dim)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: float["b n d"],
|
||||
cond: float["b n d"],
|
||||
text_embed: float["b n d"],
|
||||
spks: float["b d"],
|
||||
):
|
||||
to_cat = [x, cond, text_embed]
|
||||
if self.spk_dim > 0:
|
||||
spks = repeat(spks, "b c -> b t c", t=x.shape[1])
|
||||
to_cat.append(spks)
|
||||
|
||||
x = self.proj(torch.cat(to_cat, dim=-1))
|
||||
x = self.conv_pos_embed(x) + x
|
||||
return x
|
||||
|
||||
|
||||
# Transformer backbone using DiT blocks
|
||||
|
||||
|
||||
class DiT(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
dim,
|
||||
depth=8,
|
||||
heads=8,
|
||||
dim_head=64,
|
||||
dropout=0.1,
|
||||
ff_mult=4,
|
||||
mel_dim=80,
|
||||
mu_dim=None,
|
||||
long_skip_connection=False,
|
||||
spk_dim=None,
|
||||
out_channels=None,
|
||||
static_chunk_size=50,
|
||||
num_decoding_left_chunks=2
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.time_embed = TimestepEmbedding(dim)
|
||||
if mu_dim is None:
|
||||
mu_dim = mel_dim
|
||||
self.input_embed = InputEmbedding(mel_dim, mu_dim, dim, spk_dim)
|
||||
|
||||
self.rotary_embed = RotaryEmbedding(dim_head)
|
||||
|
||||
self.dim = dim
|
||||
self.depth = depth
|
||||
|
||||
self.transformer_blocks = nn.ModuleList(
|
||||
[DiTBlock(dim=dim, heads=heads, dim_head=dim_head, ff_mult=ff_mult, dropout=dropout) for _ in range(depth)]
|
||||
)
|
||||
self.long_skip_connection = nn.Linear(dim * 2, dim, bias=False) if long_skip_connection else None
|
||||
|
||||
self.norm_out = AdaLayerNormZero_Final(dim) # final modulation
|
||||
self.proj_out = nn.Linear(dim, mel_dim)
|
||||
self.out_channels = out_channels
|
||||
self.static_chunk_size = static_chunk_size
|
||||
self.num_decoding_left_chunks = num_decoding_left_chunks
|
||||
|
||||
def forward(self, x, mask, mu, t, spks=None, cond=None, streaming=False):
|
||||
x = x.transpose(1, 2)
|
||||
mu = mu.transpose(1, 2)
|
||||
cond = cond.transpose(1, 2)
|
||||
spks = spks.unsqueeze(dim=1)
|
||||
batch, seq_len = x.shape[0], x.shape[1]
|
||||
if t.ndim == 0:
|
||||
t = t.repeat(batch)
|
||||
|
||||
# t: conditioning time, c: context (text + masked cond audio), x: noised input audio
|
||||
t = self.time_embed(t)
|
||||
x = self.input_embed(x, cond, mu, spks.squeeze(1))
|
||||
|
||||
rope = self.rotary_embed.forward_from_seq_len(seq_len)
|
||||
|
||||
if self.long_skip_connection is not None:
|
||||
residual = x
|
||||
|
||||
if streaming is True:
|
||||
attn_mask = add_optional_chunk_mask(x, mask.bool(), False, False, 0, self.static_chunk_size, -1).unsqueeze(dim=1)
|
||||
else:
|
||||
attn_mask = add_optional_chunk_mask(x, mask.bool(), False, False, 0, 0, -1).repeat(1, x.size(1), 1).unsqueeze(dim=1)
|
||||
|
||||
for block in self.transformer_blocks:
|
||||
x = block(x, t, mask=attn_mask.bool(), rope=rope)
|
||||
|
||||
if self.long_skip_connection is not None:
|
||||
x = self.long_skip_connection(torch.cat((x, residual), dim=-1))
|
||||
|
||||
x = self.norm_out(x, t)
|
||||
output = self.proj_out(x).transpose(1, 2)
|
||||
return output
|
||||
@@ -0,0 +1,616 @@
|
||||
|
||||
"""
|
||||
ein notation:
|
||||
b - batch
|
||||
n - sequence
|
||||
nt - text sequence
|
||||
nw - raw wave length
|
||||
d - dimension
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
import math
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
import torch.nn.functional as F
|
||||
import torchaudio
|
||||
|
||||
from x_transformers.x_transformers import apply_rotary_pos_emb
|
||||
|
||||
|
||||
# raw wav to mel spec
|
||||
class MelSpec(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
filter_length=1024,
|
||||
hop_length=256,
|
||||
win_length=1024,
|
||||
n_mel_channels=100,
|
||||
target_sample_rate=24_000,
|
||||
normalize=False,
|
||||
power=1,
|
||||
norm=None,
|
||||
center=True,
|
||||
):
|
||||
super().__init__()
|
||||
self.n_mel_channels = n_mel_channels
|
||||
|
||||
self.mel_stft = torchaudio.transforms.MelSpectrogram(
|
||||
sample_rate=target_sample_rate,
|
||||
n_fft=filter_length,
|
||||
win_length=win_length,
|
||||
hop_length=hop_length,
|
||||
n_mels=n_mel_channels,
|
||||
power=power,
|
||||
center=center,
|
||||
normalized=normalize,
|
||||
norm=norm,
|
||||
)
|
||||
|
||||
self.register_buffer("dummy", torch.tensor(0), persistent=False)
|
||||
|
||||
def forward(self, inp):
|
||||
if len(inp.shape) == 3:
|
||||
inp = inp.squeeze(1) # 'b 1 nw -> b nw'
|
||||
|
||||
assert len(inp.shape) == 2
|
||||
|
||||
if self.dummy.device != inp.device:
|
||||
self.to(inp.device)
|
||||
|
||||
mel = self.mel_stft(inp)
|
||||
mel = mel.clamp(min=1e-5).log()
|
||||
return mel
|
||||
|
||||
|
||||
# sinusoidal position embedding
|
||||
|
||||
|
||||
class SinusPositionEmbedding(nn.Module):
|
||||
def __init__(self, dim):
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
|
||||
def forward(self, x, scale=1000):
|
||||
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
|
||||
|
||||
|
||||
# convolutional position embedding
|
||||
|
||||
|
||||
class ConvPositionEmbedding(nn.Module):
|
||||
def __init__(self, dim, kernel_size=31, groups=16):
|
||||
super().__init__()
|
||||
assert kernel_size % 2 != 0
|
||||
self.conv1d = nn.Sequential(
|
||||
nn.Conv1d(dim, dim, kernel_size, groups=groups, padding=kernel_size // 2),
|
||||
nn.Mish(),
|
||||
nn.Conv1d(dim, dim, kernel_size, groups=groups, padding=kernel_size // 2),
|
||||
nn.Mish(),
|
||||
)
|
||||
|
||||
def forward(self, x: float["b n d"], mask: bool["b n"] | None = None): # noqa: F722
|
||||
if mask is not None:
|
||||
mask = mask[..., None]
|
||||
x = x.masked_fill(~mask, 0.0)
|
||||
|
||||
x = x.permute(0, 2, 1)
|
||||
x = self.conv1d(x)
|
||||
out = x.permute(0, 2, 1)
|
||||
|
||||
if mask is not None:
|
||||
out = out.masked_fill(~mask, 0.0)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class CausalConvPositionEmbedding(nn.Module):
|
||||
def __init__(self, dim, kernel_size=31, groups=16):
|
||||
super().__init__()
|
||||
assert kernel_size % 2 != 0
|
||||
self.kernel_size = kernel_size
|
||||
self.conv1 = nn.Sequential(
|
||||
nn.Conv1d(dim, dim, kernel_size, groups=groups, padding=0),
|
||||
nn.Mish(),
|
||||
)
|
||||
self.conv2 = nn.Sequential(
|
||||
nn.Conv1d(dim, dim, kernel_size, groups=groups, padding=0),
|
||||
nn.Mish(),
|
||||
)
|
||||
|
||||
def forward(self, x: float["b n d"], mask: bool["b n"] | None = None): # noqa: F722
|
||||
if mask is not None:
|
||||
mask = mask[..., None]
|
||||
x = x.masked_fill(~mask, 0.0)
|
||||
|
||||
x = x.permute(0, 2, 1)
|
||||
x = F.pad(x, (self.kernel_size - 1, 0, 0, 0))
|
||||
x = self.conv1(x)
|
||||
x = F.pad(x, (self.kernel_size - 1, 0, 0, 0))
|
||||
x = self.conv2(x)
|
||||
out = x.permute(0, 2, 1)
|
||||
|
||||
if mask is not None:
|
||||
out = out.masked_fill(~mask, 0.0)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
# rotary positional embedding related
|
||||
|
||||
|
||||
def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0, theta_rescale_factor=1.0):
|
||||
# proposed by reddit user bloc97, to rescale rotary embeddings to longer sequence length without fine-tuning
|
||||
# has some connection to NTK literature
|
||||
# https://www.reddit.com/r/LocalLLaMA/comments/14lz7j5/ntkaware_scaled_rope_allows_llama_models_to_have/
|
||||
# https://github.com/lucidrains/rotary-embedding-torch/blob/main/rotary_embedding_torch/rotary_embedding_torch.py
|
||||
theta *= theta_rescale_factor ** (dim / (dim - 2))
|
||||
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))
|
||||
t = torch.arange(end, device=freqs.device) # type: ignore
|
||||
freqs = torch.outer(t, freqs).float() # type: ignore
|
||||
freqs_cos = torch.cos(freqs) # real part
|
||||
freqs_sin = torch.sin(freqs) # imaginary part
|
||||
return torch.cat([freqs_cos, freqs_sin], dim=-1)
|
||||
|
||||
|
||||
def get_pos_embed_indices(start, length, max_pos, scale=1.0):
|
||||
# length = length if isinstance(length, int) else length.max()
|
||||
scale = scale * torch.ones_like(start, dtype=torch.float32) # in case scale is a scalar
|
||||
pos = (
|
||||
start.unsqueeze(1)
|
||||
+ (torch.arange(length, device=start.device, dtype=torch.float32).unsqueeze(0) * scale.unsqueeze(1)).long()
|
||||
)
|
||||
# avoid extra long error.
|
||||
pos = torch.where(pos < max_pos, pos, max_pos - 1)
|
||||
return pos
|
||||
|
||||
|
||||
# Global Response Normalization layer (Instance Normalization ?)
|
||||
|
||||
|
||||
class GRN(nn.Module):
|
||||
def __init__(self, dim):
|
||||
super().__init__()
|
||||
self.gamma = nn.Parameter(torch.zeros(1, 1, dim))
|
||||
self.beta = nn.Parameter(torch.zeros(1, 1, dim))
|
||||
|
||||
def forward(self, x):
|
||||
Gx = torch.norm(x, p=2, dim=1, keepdim=True)
|
||||
Nx = Gx / (Gx.mean(dim=-1, keepdim=True) + 1e-6)
|
||||
return self.gamma * (x * Nx) + self.beta + x
|
||||
|
||||
|
||||
# ConvNeXt-V2 Block https://github.com/facebookresearch/ConvNeXt-V2/blob/main/models/convnextv2.py
|
||||
# ref: https://github.com/bfs18/e2_tts/blob/main/rfwave/modules.py#L108
|
||||
|
||||
|
||||
class ConvNeXtV2Block(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
intermediate_dim: int,
|
||||
dilation: int = 1,
|
||||
):
|
||||
super().__init__()
|
||||
padding = (dilation * (7 - 1)) // 2
|
||||
self.dwconv = nn.Conv1d(
|
||||
dim, dim, kernel_size=7, padding=padding, groups=dim, dilation=dilation
|
||||
) # depthwise conv
|
||||
self.norm = nn.LayerNorm(dim, eps=1e-6)
|
||||
self.pwconv1 = nn.Linear(dim, intermediate_dim) # pointwise/1x1 convs, implemented with linear layers
|
||||
self.act = nn.GELU()
|
||||
self.grn = GRN(intermediate_dim)
|
||||
self.pwconv2 = nn.Linear(intermediate_dim, dim)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
residual = x
|
||||
x = x.transpose(1, 2) # b n d -> b d n
|
||||
x = self.dwconv(x)
|
||||
x = x.transpose(1, 2) # b d n -> b n d
|
||||
x = self.norm(x)
|
||||
x = self.pwconv1(x)
|
||||
x = self.act(x)
|
||||
x = self.grn(x)
|
||||
x = self.pwconv2(x)
|
||||
return residual + x
|
||||
|
||||
|
||||
# AdaLayerNormZero
|
||||
# return with modulated x for attn input, and params for later mlp modulation
|
||||
|
||||
|
||||
class AdaLayerNormZero(nn.Module):
|
||||
def __init__(self, dim):
|
||||
super().__init__()
|
||||
|
||||
self.silu = nn.SiLU()
|
||||
self.linear = nn.Linear(dim, dim * 6)
|
||||
|
||||
self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
|
||||
|
||||
def forward(self, x, emb=None):
|
||||
emb = self.linear(self.silu(emb))
|
||||
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = torch.chunk(emb, 6, dim=1)
|
||||
|
||||
x = self.norm(x) * (1 + scale_msa[:, None]) + shift_msa[:, None]
|
||||
return x, gate_msa, shift_mlp, scale_mlp, gate_mlp
|
||||
|
||||
|
||||
# AdaLayerNormZero for final layer
|
||||
# return only with modulated x for attn input, cuz no more mlp modulation
|
||||
|
||||
|
||||
class AdaLayerNormZero_Final(nn.Module):
|
||||
def __init__(self, dim):
|
||||
super().__init__()
|
||||
|
||||
self.silu = nn.SiLU()
|
||||
self.linear = nn.Linear(dim, dim * 2)
|
||||
|
||||
self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
|
||||
|
||||
def forward(self, x, emb):
|
||||
emb = self.linear(self.silu(emb))
|
||||
scale, shift = torch.chunk(emb, 2, dim=1)
|
||||
|
||||
x = self.norm(x) * (1 + scale)[:, None, :] + shift[:, None, :]
|
||||
return x
|
||||
|
||||
|
||||
# FeedForward
|
||||
|
||||
|
||||
class FeedForward(nn.Module):
|
||||
def __init__(self, dim, dim_out=None, mult=4, dropout=0.0, approximate: str = "none"):
|
||||
super().__init__()
|
||||
inner_dim = int(dim * mult)
|
||||
dim_out = dim_out if dim_out is not None else dim
|
||||
|
||||
activation = nn.GELU(approximate=approximate)
|
||||
project_in = nn.Sequential(nn.Linear(dim, inner_dim), activation)
|
||||
self.ff = nn.Sequential(project_in, nn.Dropout(dropout), nn.Linear(inner_dim, dim_out))
|
||||
|
||||
def forward(self, x):
|
||||
return self.ff(x)
|
||||
|
||||
|
||||
# Attention with possible joint part
|
||||
# modified from diffusers/src/diffusers/models/attention_processor.py
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
processor: JointAttnProcessor | AttnProcessor,
|
||||
dim: int,
|
||||
heads: int = 8,
|
||||
dim_head: int = 64,
|
||||
dropout: float = 0.0,
|
||||
context_dim: Optional[int] = None, # if not None -> joint attention
|
||||
context_pre_only=None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
if not hasattr(F, "scaled_dot_product_attention"):
|
||||
raise ImportError("Attention equires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.")
|
||||
|
||||
self.processor = processor
|
||||
|
||||
self.dim = dim
|
||||
self.heads = heads
|
||||
self.inner_dim = dim_head * heads
|
||||
self.dropout = dropout
|
||||
|
||||
self.context_dim = context_dim
|
||||
self.context_pre_only = context_pre_only
|
||||
|
||||
self.to_q = nn.Linear(dim, self.inner_dim)
|
||||
self.to_k = nn.Linear(dim, self.inner_dim)
|
||||
self.to_v = nn.Linear(dim, self.inner_dim)
|
||||
|
||||
if self.context_dim is not None:
|
||||
self.to_k_c = nn.Linear(context_dim, self.inner_dim)
|
||||
self.to_v_c = nn.Linear(context_dim, self.inner_dim)
|
||||
if self.context_pre_only is not None:
|
||||
self.to_q_c = nn.Linear(context_dim, self.inner_dim)
|
||||
|
||||
self.to_out = nn.ModuleList([])
|
||||
self.to_out.append(nn.Linear(self.inner_dim, dim))
|
||||
self.to_out.append(nn.Dropout(dropout))
|
||||
|
||||
if self.context_pre_only is not None and not self.context_pre_only:
|
||||
self.to_out_c = nn.Linear(self.inner_dim, dim)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: float["b n d"], # noised input x # noqa: F722
|
||||
c: float["b n d"] = None, # context c # noqa: F722
|
||||
mask: bool["b n"] | None = None, # noqa: F722
|
||||
rope=None, # rotary position embedding for x
|
||||
c_rope=None, # rotary position embedding for c
|
||||
) -> torch.Tensor:
|
||||
if c is not None:
|
||||
return self.processor(self, x, c=c, mask=mask, rope=rope, c_rope=c_rope)
|
||||
else:
|
||||
return self.processor(self, x, mask=mask, rope=rope)
|
||||
|
||||
|
||||
# Attention processor
|
||||
|
||||
|
||||
class AttnProcessor:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
attn: Attention,
|
||||
x: float["b n d"], # noised input x # noqa: F722
|
||||
mask: bool["b n"] | None = None, # noqa: F722
|
||||
rope=None, # rotary position embedding
|
||||
) -> torch.FloatTensor:
|
||||
batch_size = x.shape[0]
|
||||
|
||||
# `sample` projections.
|
||||
query = attn.to_q(x)
|
||||
key = attn.to_k(x)
|
||||
value = attn.to_v(x)
|
||||
|
||||
# apply rotary position embedding
|
||||
if rope is not None:
|
||||
freqs, xpos_scale = rope
|
||||
q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0)
|
||||
|
||||
query = apply_rotary_pos_emb(query, freqs, q_xpos_scale)
|
||||
key = apply_rotary_pos_emb(key, freqs, k_xpos_scale)
|
||||
|
||||
# attention
|
||||
inner_dim = key.shape[-1]
|
||||
head_dim = inner_dim // attn.heads
|
||||
query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
|
||||
# mask. e.g. inference got a batch with different target durations, mask out the padding
|
||||
if mask is not None:
|
||||
attn_mask = mask
|
||||
if attn_mask.dim() == 2:
|
||||
attn_mask = attn_mask.unsqueeze(1).unsqueeze(1) # 'b n -> b 1 1 n'
|
||||
attn_mask = attn_mask.expand(batch_size, attn.heads, query.shape[-2], key.shape[-2])
|
||||
else:
|
||||
attn_mask = None
|
||||
|
||||
x = F.scaled_dot_product_attention(query, key, value, attn_mask=attn_mask, dropout_p=0.0, is_causal=False)
|
||||
x = x.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim)
|
||||
x = x.to(query.dtype)
|
||||
|
||||
# linear proj
|
||||
x = attn.to_out[0](x)
|
||||
# dropout
|
||||
x = attn.to_out[1](x)
|
||||
|
||||
if mask is not None:
|
||||
if mask.dim() == 2:
|
||||
mask = mask.unsqueeze(-1)
|
||||
else:
|
||||
mask = mask[:, 0, -1].unsqueeze(-1)
|
||||
x = x.masked_fill(~mask, 0.0)
|
||||
|
||||
return x
|
||||
|
||||
|
||||
# Joint Attention processor for MM-DiT
|
||||
# modified from diffusers/src/diffusers/models/attention_processor.py
|
||||
|
||||
|
||||
class JointAttnProcessor:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
attn: Attention,
|
||||
x: float["b n d"], # noised input x # noqa: F722
|
||||
c: float["b nt d"] = None, # context c, here text # noqa: F722
|
||||
mask: bool["b n"] | None = None, # noqa: F722
|
||||
rope=None, # rotary position embedding for x
|
||||
c_rope=None, # rotary position embedding for c
|
||||
) -> torch.FloatTensor:
|
||||
residual = x
|
||||
|
||||
batch_size = c.shape[0]
|
||||
|
||||
# `sample` projections.
|
||||
query = attn.to_q(x)
|
||||
key = attn.to_k(x)
|
||||
value = attn.to_v(x)
|
||||
|
||||
# `context` projections.
|
||||
c_query = attn.to_q_c(c)
|
||||
c_key = attn.to_k_c(c)
|
||||
c_value = attn.to_v_c(c)
|
||||
|
||||
# apply rope for context and noised input independently
|
||||
if rope is not None:
|
||||
freqs, xpos_scale = rope
|
||||
q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0)
|
||||
query = apply_rotary_pos_emb(query, freqs, q_xpos_scale)
|
||||
key = apply_rotary_pos_emb(key, freqs, k_xpos_scale)
|
||||
if c_rope is not None:
|
||||
freqs, xpos_scale = c_rope
|
||||
q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0)
|
||||
c_query = apply_rotary_pos_emb(c_query, freqs, q_xpos_scale)
|
||||
c_key = apply_rotary_pos_emb(c_key, freqs, k_xpos_scale)
|
||||
|
||||
# attention
|
||||
query = torch.cat([query, c_query], dim=1)
|
||||
key = torch.cat([key, c_key], dim=1)
|
||||
value = torch.cat([value, c_value], dim=1)
|
||||
|
||||
inner_dim = key.shape[-1]
|
||||
head_dim = inner_dim // attn.heads
|
||||
query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
|
||||
# mask. e.g. inference got a batch with different target durations, mask out the padding
|
||||
if mask is not None:
|
||||
attn_mask = F.pad(mask, (0, c.shape[1]), value=True) # no mask for c (text)
|
||||
attn_mask = attn_mask.unsqueeze(1).unsqueeze(1) # 'b n -> b 1 1 n'
|
||||
attn_mask = attn_mask.expand(batch_size, attn.heads, query.shape[-2], key.shape[-2])
|
||||
else:
|
||||
attn_mask = None
|
||||
|
||||
x = F.scaled_dot_product_attention(query, key, value, attn_mask=attn_mask, dropout_p=0.0, is_causal=False)
|
||||
x = x.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim)
|
||||
x = x.to(query.dtype)
|
||||
|
||||
# Split the attention outputs.
|
||||
x, c = (
|
||||
x[:, : residual.shape[1]],
|
||||
x[:, residual.shape[1]:],
|
||||
)
|
||||
|
||||
# linear proj
|
||||
x = attn.to_out[0](x)
|
||||
# dropout
|
||||
x = attn.to_out[1](x)
|
||||
if not attn.context_pre_only:
|
||||
c = attn.to_out_c(c)
|
||||
|
||||
if mask is not None:
|
||||
mask = mask.unsqueeze(-1)
|
||||
x = x.masked_fill(~mask, 0.0)
|
||||
# c = c.masked_fill(~mask, 0.) # no mask for c (text)
|
||||
|
||||
return x, c
|
||||
|
||||
|
||||
# DiT Block
|
||||
|
||||
|
||||
class DiTBlock(nn.Module):
|
||||
def __init__(self, dim, heads, dim_head, ff_mult=4, dropout=0.1):
|
||||
super().__init__()
|
||||
|
||||
self.attn_norm = AdaLayerNormZero(dim)
|
||||
self.attn = Attention(
|
||||
processor=AttnProcessor(),
|
||||
dim=dim,
|
||||
heads=heads,
|
||||
dim_head=dim_head,
|
||||
dropout=dropout,
|
||||
)
|
||||
|
||||
self.ff_norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
|
||||
self.ff = FeedForward(dim=dim, mult=ff_mult, dropout=dropout, approximate="tanh")
|
||||
|
||||
def forward(self, x, t, mask=None, rope=None): # x: noised input, t: time embedding
|
||||
# pre-norm & modulation for attention input
|
||||
norm, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.attn_norm(x, emb=t)
|
||||
|
||||
# attention
|
||||
attn_output = self.attn(x=norm, mask=mask, rope=rope)
|
||||
|
||||
# process attention output for input x
|
||||
x = x + gate_msa.unsqueeze(1) * attn_output
|
||||
|
||||
ff_norm = self.ff_norm(x) * (1 + scale_mlp[:, None]) + shift_mlp[:, None]
|
||||
ff_output = self.ff(ff_norm)
|
||||
x = x + gate_mlp.unsqueeze(1) * ff_output
|
||||
|
||||
return x
|
||||
|
||||
|
||||
# MMDiT Block https://arxiv.org/abs/2403.03206
|
||||
|
||||
|
||||
class MMDiTBlock(nn.Module):
|
||||
r"""
|
||||
modified from diffusers/src/diffusers/models/attention.py
|
||||
|
||||
notes.
|
||||
_c: context related. text, cond, etc. (left part in sd3 fig2.b)
|
||||
_x: noised input related. (right part)
|
||||
context_pre_only: last layer only do prenorm + modulation cuz no more ffn
|
||||
"""
|
||||
|
||||
def __init__(self, dim, heads, dim_head, ff_mult=4, dropout=0.1, context_pre_only=False):
|
||||
super().__init__()
|
||||
|
||||
self.context_pre_only = context_pre_only
|
||||
|
||||
self.attn_norm_c = AdaLayerNormZero_Final(dim) if context_pre_only else AdaLayerNormZero(dim)
|
||||
self.attn_norm_x = AdaLayerNormZero(dim)
|
||||
self.attn = Attention(
|
||||
processor=JointAttnProcessor(),
|
||||
dim=dim,
|
||||
heads=heads,
|
||||
dim_head=dim_head,
|
||||
dropout=dropout,
|
||||
context_dim=dim,
|
||||
context_pre_only=context_pre_only,
|
||||
)
|
||||
|
||||
if not context_pre_only:
|
||||
self.ff_norm_c = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
|
||||
self.ff_c = FeedForward(dim=dim, mult=ff_mult, dropout=dropout, approximate="tanh")
|
||||
else:
|
||||
self.ff_norm_c = None
|
||||
self.ff_c = None
|
||||
self.ff_norm_x = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
|
||||
self.ff_x = FeedForward(dim=dim, mult=ff_mult, dropout=dropout, approximate="tanh")
|
||||
|
||||
def forward(self, x, c, t, mask=None, rope=None, c_rope=None): # x: noised input, c: context, t: time embedding
|
||||
# pre-norm & modulation for attention input
|
||||
if self.context_pre_only:
|
||||
norm_c = self.attn_norm_c(c, t)
|
||||
else:
|
||||
norm_c, c_gate_msa, c_shift_mlp, c_scale_mlp, c_gate_mlp = self.attn_norm_c(c, emb=t)
|
||||
norm_x, x_gate_msa, x_shift_mlp, x_scale_mlp, x_gate_mlp = self.attn_norm_x(x, emb=t)
|
||||
|
||||
# attention
|
||||
x_attn_output, c_attn_output = self.attn(x=norm_x, c=norm_c, mask=mask, rope=rope, c_rope=c_rope)
|
||||
|
||||
# process attention output for context c
|
||||
if self.context_pre_only:
|
||||
c = None
|
||||
else: # if not last layer
|
||||
c = c + c_gate_msa.unsqueeze(1) * c_attn_output
|
||||
|
||||
norm_c = self.ff_norm_c(c) * (1 + c_scale_mlp[:, None]) + c_shift_mlp[:, None]
|
||||
c_ff_output = self.ff_c(norm_c)
|
||||
c = c + c_gate_mlp.unsqueeze(1) * c_ff_output
|
||||
|
||||
# process attention output for input x
|
||||
x = x + x_gate_msa.unsqueeze(1) * x_attn_output
|
||||
|
||||
norm_x = self.ff_norm_x(x) * (1 + x_scale_mlp[:, None]) + x_shift_mlp[:, None]
|
||||
x_ff_output = self.ff_x(norm_x)
|
||||
x = x + x_gate_mlp.unsqueeze(1) * x_ff_output
|
||||
|
||||
return c, x
|
||||
|
||||
|
||||
# time step conditioning embedding
|
||||
|
||||
|
||||
class TimestepEmbedding(nn.Module):
|
||||
def __init__(self, dim, freq_embed_dim=256):
|
||||
super().__init__()
|
||||
self.time_embed = SinusPositionEmbedding(freq_embed_dim)
|
||||
self.time_mlp = nn.Sequential(nn.Linear(freq_embed_dim, dim), nn.SiLU(), nn.Linear(dim, dim))
|
||||
|
||||
def forward(self, timestep: float["b"]): # noqa: F821
|
||||
time_hidden = self.time_embed(timestep)
|
||||
time_hidden = time_hidden.to(timestep.dtype)
|
||||
time = self.time_mlp(time_hidden) # b d
|
||||
return time
|
||||
@@ -0,0 +1,494 @@
|
||||
# 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.
|
||||
from typing import Tuple
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from einops import pack, rearrange, repeat
|
||||
from cosyvoice.utils.common import mask_to_bias
|
||||
from cosyvoice.utils.mask import add_optional_chunk_mask
|
||||
from matcha.models.components.decoder import SinusoidalPosEmb, Block1D, ResnetBlock1D, Downsample1D, TimestepEmbedding, Upsample1D
|
||||
from matcha.models.components.transformer import BasicTransformerBlock
|
||||
|
||||
|
||||
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) -> torch.Tensor:
|
||||
x = torch.transpose(x, self.dim0, self.dim1)
|
||||
return x
|
||||
|
||||
|
||||
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
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
x = F.pad(x, (self.causal_padding, 0), value=0.0)
|
||||
x = super(CausalConv1d, self).forward(x)
|
||||
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) -> Tuple[torch.Tensor, 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 ConditionalDecoder(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",
|
||||
):
|
||||
"""
|
||||
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.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(
|
||||
[
|
||||
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 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 = 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 = 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 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()
|
||||
|
||||
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, streaming=False):
|
||||
"""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 = add_optional_chunk_mask(x, mask_down.bool(), False, False, 0, 0, -1).repeat(1, x.size(1), 1)
|
||||
attn_mask = mask_to_bias(attn_mask, 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 = add_optional_chunk_mask(x, mask_mid.bool(), False, False, 0, 0, -1).repeat(1, x.size(1), 1)
|
||||
attn_mask = mask_to_bias(attn_mask, 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 = add_optional_chunk_mask(x, mask_up.bool(), False, False, 0, 0, -1).repeat(1, x.size(1), 1)
|
||||
attn_mask = mask_to_bias(attn_mask, 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
|
||||
|
||||
|
||||
class CausalConditionalDecoder(ConditionalDecoder):
|
||||
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",
|
||||
static_chunk_size=50,
|
||||
num_decoding_left_chunks=2,
|
||||
):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
torch.nn.Module.__init__(self)
|
||||
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.static_chunk_size = static_chunk_size
|
||||
self.num_decoding_left_chunks = num_decoding_left_chunks
|
||||
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 = CausalResnetBlock1D(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)
|
||||
)
|
||||
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)
|
||||
|
||||
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,
|
||||
)
|
||||
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)
|
||||
)
|
||||
self.up_blocks.append(nn.ModuleList([resnet, transformer_blocks, upsample]))
|
||||
self.final_block = CausalBlock1D(channels[-1], channels[-1])
|
||||
self.final_proj = nn.Conv1d(channels[-1], self.out_channels, 1)
|
||||
self.initialize_weights()
|
||||
|
||||
def forward(self, x, mask, mu, t, spks=None, cond=None, streaming=False):
|
||||
"""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()
|
||||
if streaming is True:
|
||||
attn_mask = add_optional_chunk_mask(x, mask_down.bool(), False, False, 0, self.static_chunk_size, -1)
|
||||
else:
|
||||
attn_mask = add_optional_chunk_mask(x, mask_down.bool(), False, False, 0, 0, -1).repeat(1, x.size(1), 1)
|
||||
attn_mask = mask_to_bias(attn_mask, 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()
|
||||
if streaming is True:
|
||||
attn_mask = add_optional_chunk_mask(x, mask_mid.bool(), False, False, 0, self.static_chunk_size, -1)
|
||||
else:
|
||||
attn_mask = add_optional_chunk_mask(x, mask_mid.bool(), False, False, 0, 0, -1).repeat(1, x.size(1), 1)
|
||||
attn_mask = mask_to_bias(attn_mask, 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()
|
||||
if streaming is True:
|
||||
attn_mask = add_optional_chunk_mask(x, mask_up.bool(), False, False, 0, self.static_chunk_size, -1)
|
||||
else:
|
||||
attn_mask = add_optional_chunk_mask(x, mask_up.bool(), False, False, 0, 0, -1).repeat(1, x.size(1), 1)
|
||||
attn_mask = mask_to_bias(attn_mask, 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,432 @@
|
||||
# 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
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.nn import functional as F
|
||||
from omegaconf import DictConfig
|
||||
from cosyvoice.utils.mask import make_pad_mask
|
||||
|
||||
|
||||
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': DictConfig({'sigma_min': 1e-06, 'solver': 'euler', 't_scheduler': 'cosine',
|
||||
'training_cfg_rate': 0.2, 'inference_cfg_rate': 0.7, 'reg_loss_type': 'l1'}),
|
||||
'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'}}):
|
||||
super().__init__()
|
||||
self.input_size = input_size
|
||||
self.output_size = output_size
|
||||
self.decoder_conf = decoder_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}") # Internal model parameter, not user-controllable
|
||||
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)) * 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)
|
||||
# NOTE this is unnecessary, feat/h already same shape
|
||||
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):
|
||||
assert token.shape[0] == 1
|
||||
# xvec projection
|
||||
embedding = F.normalize(embedding, dim=1)
|
||||
embedding = self.spk_embed_affine_layer(embedding)
|
||||
|
||||
# concat speech token and prompt speech token
|
||||
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)
|
||||
token = self.input_embedding(torch.clamp(token, min=0)) * 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,
|
||||
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 = 4096,
|
||||
input_frame_rate: int = 50,
|
||||
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': DictConfig({'sigma_min': 1e-06, 'solver': 'euler', 't_scheduler': 'cosine',
|
||||
'training_cfg_rate': 0.2, 'inference_cfg_rate': 0.7, 'reg_loss_type': 'l1'}),
|
||||
'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'}}):
|
||||
super().__init__()
|
||||
self.input_size = input_size
|
||||
self.output_size = output_size
|
||||
self.decoder_conf = decoder_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}") # Internal model parameter, not user-controllable
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
# NOTE unified training, static_chunk_size > 0 or = 0
|
||||
streaming = True if random.random() < 0.5 else False
|
||||
|
||||
# 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)) * mask
|
||||
|
||||
# text encode
|
||||
h, h_lengths = self.encoder(token, token_len, streaming=streaming)
|
||||
h = self.encoder_proj(h)
|
||||
|
||||
# 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(h_lengths.sum(dim=-1).squeeze(dim=1))).to(h)
|
||||
loss, _ = self.decoder.compute_loss(
|
||||
feat.transpose(1, 2).contiguous(),
|
||||
mask.unsqueeze(1),
|
||||
h.transpose(1, 2).contiguous(),
|
||||
embedding,
|
||||
cond=conds,
|
||||
streaming=streaming,
|
||||
)
|
||||
return {'loss': loss}
|
||||
|
||||
@torch.inference_mode()
|
||||
def inference(self,
|
||||
token,
|
||||
token_len,
|
||||
prompt_token,
|
||||
prompt_token_len,
|
||||
prompt_feat,
|
||||
prompt_feat_len,
|
||||
embedding,
|
||||
streaming,
|
||||
finalize):
|
||||
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)) * mask
|
||||
|
||||
# text encode
|
||||
if finalize is True:
|
||||
h, h_lengths = self.encoder(token, token_len, streaming=streaming)
|
||||
else:
|
||||
token, context = token[:, :-self.pre_lookahead_len], token[:, -self.pre_lookahead_len:]
|
||||
h, h_lengths = self.encoder(token, token_len, context=context, streaming=streaming)
|
||||
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,
|
||||
streaming=streaming
|
||||
)
|
||||
feat = feat[:, :, mel_len1:]
|
||||
assert feat.shape[2] == mel_len2
|
||||
return feat.float(), None
|
||||
|
||||
|
||||
class CausalMaskedDiffWithDiT(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,
|
||||
token_mel_ratio: int = 2,
|
||||
pre_lookahead_len: int = 3,
|
||||
pre_lookahead_layer: 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': DictConfig({'sigma_min': 1e-06, 'solver': 'euler', 't_scheduler': 'cosine',
|
||||
'training_cfg_rate': 0.2, 'inference_cfg_rate': 0.7, 'reg_loss_type': 'l1'}),
|
||||
'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'}}):
|
||||
super().__init__()
|
||||
self.input_size = input_size
|
||||
self.output_size = output_size
|
||||
self.decoder_conf = decoder_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}") # Internal model parameter, not user-controllable
|
||||
self.input_embedding = nn.Embedding(vocab_size, input_size)
|
||||
self.spk_embed_affine_layer = torch.nn.Linear(spk_embed_dim, output_size)
|
||||
self.pre_lookahead_len = pre_lookahead_len
|
||||
self.pre_lookahead_layer = pre_lookahead_layer
|
||||
self.decoder = decoder
|
||||
self.only_mask_loss = only_mask_loss
|
||||
self.token_mel_ratio = token_mel_ratio
|
||||
|
||||
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)
|
||||
|
||||
# NOTE unified training, static_chunk_size > 0 or = 0
|
||||
streaming = True if random.random() < 0.5 else False
|
||||
|
||||
# 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)) * mask
|
||||
|
||||
# text encode
|
||||
h, h_lengths = self.encoder(token, token_len, streaming=streaming)
|
||||
h = self.encoder_proj(h)
|
||||
|
||||
# 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(h_lengths.sum(dim=-1).squeeze(dim=1))).to(h)
|
||||
loss, _ = self.decoder.compute_loss(
|
||||
feat.transpose(1, 2).contiguous(),
|
||||
mask.unsqueeze(1),
|
||||
h.transpose(1, 2).contiguous(),
|
||||
embedding,
|
||||
cond=conds,
|
||||
streaming=streaming,
|
||||
)
|
||||
return {'loss': loss}
|
||||
|
||||
@torch.inference_mode()
|
||||
def inference(self,
|
||||
token,
|
||||
token_len,
|
||||
prompt_token,
|
||||
prompt_token_len,
|
||||
prompt_feat,
|
||||
prompt_feat_len,
|
||||
embedding,
|
||||
streaming,
|
||||
finalize):
|
||||
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)) * mask
|
||||
|
||||
# text encode
|
||||
if finalize is True:
|
||||
h = self.pre_lookahead_layer(token)
|
||||
else:
|
||||
h = self.pre_lookahead_layer(token[:, :-self.pre_lookahead_len], context=token[:, -self.pre_lookahead_len:])
|
||||
h = h.repeat_interleave(self.token_mel_ratio, dim=1)
|
||||
mel_len1, mel_len2 = prompt_feat.shape[1], h.shape[1] - prompt_feat.shape[1]
|
||||
|
||||
# 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,
|
||||
streaming=streaming
|
||||
)
|
||||
feat = feat[:, :, mel_len1:]
|
||||
assert feat.shape[2] == mel_len2
|
||||
return feat.float(), None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
torch.backends.cudnn.deterministic = True
|
||||
torch.backends.cudnn.benchmark = False
|
||||
from hyperpyyaml import load_hyperpyyaml
|
||||
with open('./pretrained_models/Fun-CosyVoice3-0.5B/cosyvoice3.yaml', 'r') as f:
|
||||
configs = load_hyperpyyaml(f, overrides={'llm': None, 'hift': None})
|
||||
model = configs['flow']
|
||||
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
||||
model.to(device)
|
||||
model.eval()
|
||||
max_len = 10 * model.decoder.estimator.static_chunk_size
|
||||
chunk_size = model.decoder.estimator.static_chunk_size
|
||||
context_size = model.pre_lookahead_layer.pre_lookahead_len
|
||||
token = torch.randint(0, 6561, size=(1, max_len)).to(device)
|
||||
token_len = torch.tensor([max_len]).to(device)
|
||||
prompt_token = torch.randint(0, 6561, size=(1, chunk_size)).to(device)
|
||||
prompt_token_len = torch.tensor([chunk_size]).to(device)
|
||||
prompt_feat = torch.rand(1, chunk_size * 2, 80).to(device)
|
||||
prompt_feat_len = torch.tensor([chunk_size * 2]).to(device)
|
||||
prompt_embedding = torch.rand(1, 192).to(device)
|
||||
pred_gt, _ = model.inference(token, token_len, prompt_token, prompt_token_len, prompt_feat, prompt_feat_len, prompt_embedding, streaming=True, finalize=True)
|
||||
for i in range(0, max_len, chunk_size):
|
||||
finalize = True if i + chunk_size + context_size >= max_len else False
|
||||
pred_chunk, _ = model.inference(token[:, :i + chunk_size + context_size], torch.tensor([token[:, :i + chunk_size + context_size].shape[1]]).to(device),
|
||||
prompt_token, prompt_token_len, prompt_feat, prompt_feat_len, prompt_embedding, streaming=True, finalize=finalize)
|
||||
pred_chunk = pred_chunk[:, :, i * model.token_mel_ratio:]
|
||||
print((pred_gt[:, :, i * model.token_mel_ratio: i * model.token_mel_ratio + pred_chunk.shape[2]] - pred_chunk).abs().max().item())
|
||||
@@ -0,0 +1,231 @@
|
||||
# Copyright (c) 2024 Alibaba Inc (authors: Xiang Lyu, Zhihao Du)
|
||||
# 2025 Alibaba Inc (authors: Xiang Lyu, Bofan Zhou)
|
||||
#
|
||||
# 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.functional as F
|
||||
from matcha.models.components.flow_matching import BASECFM
|
||||
from cosyvoice.utils.common import set_all_random_seed
|
||||
|
||||
|
||||
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
|
||||
|
||||
@torch.inference_mode()
|
||||
def forward(self, mu, mask, n_timesteps, temperature=1.0, spks=None, cond=None, prompt_len=0, 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 = cache.shape[2]
|
||||
# fix prompt and overlap part mu and z
|
||||
if cache_size != 0:
|
||||
z[:, :, :cache_size] = cache[:, :, :, 0]
|
||||
mu[:, :, :cache_size] = cache[:, :, :, 1]
|
||||
z_cache = torch.concat([z[:, :, :prompt_len], z[:, :, -34:]], dim=2)
|
||||
mu_cache = torch.concat([mu[:, :, :prompt_len], mu[:, :, -34:]], dim=2)
|
||||
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), cache
|
||||
|
||||
def solve_euler(self, x, t_span, mu, mask, spks, cond, streaming=False):
|
||||
"""
|
||||
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!
|
||||
# NOTE when flow run in amp mode, x.dtype is float32, which cause nan in trt fp16 inference, so set dtype=spks.dtype
|
||||
x_in = torch.zeros([2, 80, x.size(2)], device=x.device, dtype=spks.dtype)
|
||||
mask_in = torch.zeros([2, 1, x.size(2)], device=x.device, dtype=spks.dtype)
|
||||
mu_in = torch.zeros([2, 80, x.size(2)], device=x.device, dtype=spks.dtype)
|
||||
t_in = torch.zeros([2], device=x.device, dtype=spks.dtype)
|
||||
spks_in = torch.zeros([2, 80], device=x.device, dtype=spks.dtype)
|
||||
cond_in = torch.zeros([2, 80, x.size(2)], device=x.device, dtype=spks.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,
|
||||
streaming
|
||||
)
|
||||
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, streaming=False):
|
||||
if isinstance(self.estimator, torch.nn.Module):
|
||||
return self.estimator(x, mask, mu, t, spks, cond, streaming=streaming)
|
||||
else:
|
||||
[estimator, stream], trt_engine = self.estimator.acquire_estimator()
|
||||
# NOTE need to synchronize when switching stream
|
||||
torch.cuda.current_stream().synchronize()
|
||||
with stream:
|
||||
# Use actual batch size from input tensor instead of hardcoded 2
|
||||
batch_size = x.size(0)
|
||||
seq_len = x.size(2)
|
||||
estimator.set_input_shape('x', (batch_size, 80, seq_len))
|
||||
estimator.set_input_shape('mask', (batch_size, 1, seq_len))
|
||||
estimator.set_input_shape('mu', (batch_size, 80, seq_len))
|
||||
estimator.set_input_shape('t', (batch_size,))
|
||||
estimator.set_input_shape('spks', (batch_size, 80))
|
||||
estimator.set_input_shape('cond', (batch_size, 80, seq_len))
|
||||
data_ptrs = [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()]
|
||||
for i, j in enumerate(data_ptrs):
|
||||
estimator.set_tensor_address(trt_engine.get_tensor_name(i), j)
|
||||
# run trt engine
|
||||
assert estimator.execute_async_v3(torch.cuda.current_stream().cuda_stream) is True
|
||||
torch.cuda.current_stream().synchronize()
|
||||
self.estimator.release_estimator(estimator, stream)
|
||||
return x
|
||||
|
||||
def compute_loss(self, x1, mask, mu, spks=None, cond=None, streaming=False):
|
||||
"""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, streaming=streaming)
|
||||
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, cfm_params, n_spks=1, spk_emb_dim=64, estimator: torch.nn.Module = None):
|
||||
super().__init__(in_channels, cfm_params, n_spks, spk_emb_dim, estimator)
|
||||
set_all_random_seed(0)
|
||||
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, streaming=False):
|
||||
"""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, streaming=streaming), None
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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.
|
||||
from typing import Tuple
|
||||
import torch.nn as nn
|
||||
import torch
|
||||
from torch.nn import functional as F
|
||||
from cosyvoice.utils.mask import make_pad_mask
|
||||
|
||||
|
||||
class InterpolateRegulator(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
channels: int,
|
||||
sampling_ratios: Tuple,
|
||||
out_channels: int = None,
|
||||
groups: int = 1,
|
||||
):
|
||||
super().__init__()
|
||||
self.sampling_ratios = sampling_ratios
|
||||
out_channels = out_channels or channels
|
||||
model = nn.ModuleList([])
|
||||
if len(sampling_ratios) > 0:
|
||||
for _ in sampling_ratios:
|
||||
module = nn.Conv1d(channels, channels, 3, 1, 1)
|
||||
norm = nn.GroupNorm(groups, channels)
|
||||
act = nn.Mish()
|
||||
model.extend([module, norm, act])
|
||||
model.append(
|
||||
nn.Conv1d(channels, out_channels, 1, 1)
|
||||
)
|
||||
self.model = nn.Sequential(*model)
|
||||
|
||||
def forward(self, x, ylens=None):
|
||||
# x in (B, T, D)
|
||||
mask = (~make_pad_mask(ylens)).to(x).unsqueeze(-1)
|
||||
x = F.interpolate(x.transpose(1, 2).contiguous(), size=ylens.max(), mode='linear')
|
||||
out = self.model(x).transpose(1, 2).contiguous()
|
||||
olens = ylens
|
||||
return out * mask, olens
|
||||
|
||||
def inference(self, x1, x2, mel_len1, mel_len2, input_frame_rate=50):
|
||||
# in inference mode, interploate prompt token and token(head/mid/tail) seprately, so we can get a clear separation point of mel
|
||||
# NOTE 20 corresponds to token_overlap_len in cosyvoice/cli/model.py
|
||||
# x in (B, T, D)
|
||||
if x2.shape[1] > 40:
|
||||
x2_head = F.interpolate(x2[:, :20].transpose(1, 2).contiguous(), size=int(20 / input_frame_rate * 22050 / 256), mode='linear')
|
||||
x2_mid = F.interpolate(x2[:, 20:-20].transpose(1, 2).contiguous(), size=mel_len2 - int(20 / input_frame_rate * 22050 / 256) * 2,
|
||||
mode='linear')
|
||||
x2_tail = F.interpolate(x2[:, -20:].transpose(1, 2).contiguous(), size=int(20 / input_frame_rate * 22050 / 256), mode='linear')
|
||||
x2 = torch.concat([x2_head, x2_mid, x2_tail], dim=2)
|
||||
else:
|
||||
x2 = F.interpolate(x2.transpose(1, 2).contiguous(), size=mel_len2, mode='linear')
|
||||
if x1.shape[1] != 0:
|
||||
x1 = F.interpolate(x1.transpose(1, 2).contiguous(), size=mel_len1, mode='linear')
|
||||
x = torch.concat([x1, x2], dim=2)
|
||||
else:
|
||||
x = x2
|
||||
out = self.model(x).transpose(1, 2).contiguous()
|
||||
return out, mel_len1 + mel_len2
|
||||
@@ -0,0 +1,230 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
try:
|
||||
from torch.nn.utils.parametrizations import weight_norm, spectral_norm
|
||||
except ImportError:
|
||||
from torch.nn.utils import weight_norm, spectral_norm
|
||||
from typing import List, Optional, Tuple
|
||||
from einops import rearrange
|
||||
from torchaudio.transforms import Spectrogram
|
||||
|
||||
LRELU_SLOPE = 0.1
|
||||
|
||||
|
||||
class MultipleDiscriminator(nn.Module):
|
||||
def __init__(
|
||||
self, mpd: nn.Module, mrd: nn.Module
|
||||
):
|
||||
super().__init__()
|
||||
self.mpd = mpd
|
||||
self.mrd = mrd
|
||||
|
||||
def forward(self, y: torch.Tensor, y_hat: torch.Tensor):
|
||||
y_d_rs, y_d_gs, fmap_rs, fmap_gs = [], [], [], []
|
||||
this_y_d_rs, this_y_d_gs, this_fmap_rs, this_fmap_gs = self.mpd(y.unsqueeze(dim=1), y_hat.unsqueeze(dim=1))
|
||||
y_d_rs += this_y_d_rs
|
||||
y_d_gs += this_y_d_gs
|
||||
fmap_rs += this_fmap_rs
|
||||
fmap_gs += this_fmap_gs
|
||||
this_y_d_rs, this_y_d_gs, this_fmap_rs, this_fmap_gs = self.mrd(y, y_hat)
|
||||
y_d_rs += this_y_d_rs
|
||||
y_d_gs += this_y_d_gs
|
||||
fmap_rs += this_fmap_rs
|
||||
fmap_gs += this_fmap_gs
|
||||
return y_d_rs, y_d_gs, fmap_rs, fmap_gs
|
||||
|
||||
|
||||
class MultiResolutionDiscriminator(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
fft_sizes: Tuple[int, ...] = (2048, 1024, 512),
|
||||
num_embeddings: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
Multi-Resolution Discriminator module adapted from https://github.com/descriptinc/descript-audio-codec.
|
||||
Additionally, it allows incorporating conditional information with a learned embeddings table.
|
||||
|
||||
Args:
|
||||
fft_sizes (tuple[int]): Tuple of window lengths for FFT. Defaults to (2048, 1024, 512).
|
||||
num_embeddings (int, optional): Number of embeddings. None means non-conditional discriminator.
|
||||
Defaults to None.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self.discriminators = nn.ModuleList(
|
||||
[DiscriminatorR(window_length=w, num_embeddings=num_embeddings) for w in fft_sizes]
|
||||
)
|
||||
|
||||
def forward(
|
||||
self, y: torch.Tensor, y_hat: torch.Tensor, bandwidth_id: torch.Tensor = None
|
||||
) -> Tuple[List[torch.Tensor], List[torch.Tensor], List[List[torch.Tensor]], List[List[torch.Tensor]]]:
|
||||
y_d_rs = []
|
||||
y_d_gs = []
|
||||
fmap_rs = []
|
||||
fmap_gs = []
|
||||
|
||||
for d in self.discriminators:
|
||||
y_d_r, fmap_r = d(x=y, cond_embedding_id=bandwidth_id)
|
||||
y_d_g, fmap_g = d(x=y_hat, cond_embedding_id=bandwidth_id)
|
||||
y_d_rs.append(y_d_r)
|
||||
fmap_rs.append(fmap_r)
|
||||
y_d_gs.append(y_d_g)
|
||||
fmap_gs.append(fmap_g)
|
||||
|
||||
return y_d_rs, y_d_gs, fmap_rs, fmap_gs
|
||||
|
||||
|
||||
class DiscriminatorR(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
window_length: int,
|
||||
num_embeddings: Optional[int] = None,
|
||||
channels: int = 32,
|
||||
hop_factor: float = 0.25,
|
||||
bands: Tuple[Tuple[float, float], ...] = ((0.0, 0.1), (0.1, 0.25), (0.25, 0.5), (0.5, 0.75), (0.75, 1.0)),
|
||||
):
|
||||
super().__init__()
|
||||
self.window_length = window_length
|
||||
self.hop_factor = hop_factor
|
||||
self.spec_fn = Spectrogram(
|
||||
n_fft=window_length, hop_length=int(window_length * hop_factor), win_length=window_length, power=None
|
||||
)
|
||||
n_fft = window_length // 2 + 1
|
||||
bands = [(int(b[0] * n_fft), int(b[1] * n_fft)) for b in bands]
|
||||
self.bands = bands
|
||||
convs = lambda: nn.ModuleList(
|
||||
[
|
||||
weight_norm(nn.Conv2d(2, channels, (3, 9), (1, 1), padding=(1, 4))),
|
||||
weight_norm(nn.Conv2d(channels, channels, (3, 9), (1, 2), padding=(1, 4))),
|
||||
weight_norm(nn.Conv2d(channels, channels, (3, 9), (1, 2), padding=(1, 4))),
|
||||
weight_norm(nn.Conv2d(channels, channels, (3, 9), (1, 2), padding=(1, 4))),
|
||||
weight_norm(nn.Conv2d(channels, channels, (3, 3), (1, 1), padding=(1, 1))),
|
||||
]
|
||||
)
|
||||
self.band_convs = nn.ModuleList([convs() for _ in range(len(self.bands))])
|
||||
|
||||
if num_embeddings is not None:
|
||||
self.emb = torch.nn.Embedding(num_embeddings=num_embeddings, embedding_dim=channels)
|
||||
torch.nn.init.zeros_(self.emb.weight)
|
||||
|
||||
self.conv_post = weight_norm(nn.Conv2d(channels, 1, (3, 3), (1, 1), padding=(1, 1)))
|
||||
|
||||
def spectrogram(self, x):
|
||||
# Remove DC offset
|
||||
x = x - x.mean(dim=-1, keepdims=True)
|
||||
# Peak normalize the volume of input audio
|
||||
x = 0.8 * x / (x.abs().max(dim=-1, keepdim=True)[0] + 1e-9)
|
||||
x = self.spec_fn(x)
|
||||
x = torch.view_as_real(x)
|
||||
x = rearrange(x, "b f t c -> b c t f")
|
||||
# Split into bands
|
||||
x_bands = [x[..., b[0]: b[1]] for b in self.bands]
|
||||
return x_bands
|
||||
|
||||
def forward(self, x: torch.Tensor, cond_embedding_id: torch.Tensor = None):
|
||||
x_bands = self.spectrogram(x)
|
||||
fmap = []
|
||||
x = []
|
||||
for band, stack in zip(x_bands, self.band_convs):
|
||||
for i, layer in enumerate(stack):
|
||||
band = layer(band)
|
||||
band = torch.nn.functional.leaky_relu(band, 0.1)
|
||||
if i > 0:
|
||||
fmap.append(band)
|
||||
x.append(band)
|
||||
x = torch.cat(x, dim=-1)
|
||||
if cond_embedding_id is not None:
|
||||
emb = self.emb(cond_embedding_id)
|
||||
h = (emb.view(1, -1, 1, 1) * x).sum(dim=1, keepdims=True)
|
||||
else:
|
||||
h = 0
|
||||
x = self.conv_post(x)
|
||||
fmap.append(x)
|
||||
x += h
|
||||
|
||||
return x, fmap
|
||||
|
||||
|
||||
class MultiResSpecDiscriminator(torch.nn.Module):
|
||||
|
||||
def __init__(self,
|
||||
fft_sizes=[1024, 2048, 512],
|
||||
hop_sizes=[120, 240, 50],
|
||||
win_lengths=[600, 1200, 240],
|
||||
window="hann_window"):
|
||||
|
||||
super(MultiResSpecDiscriminator, self).__init__()
|
||||
self.discriminators = nn.ModuleList([
|
||||
SpecDiscriminator(fft_sizes[0], hop_sizes[0], win_lengths[0], window),
|
||||
SpecDiscriminator(fft_sizes[1], hop_sizes[1], win_lengths[1], window),
|
||||
SpecDiscriminator(fft_sizes[2], hop_sizes[2], win_lengths[2], window)])
|
||||
|
||||
def forward(self, y, y_hat):
|
||||
y_d_rs = []
|
||||
y_d_gs = []
|
||||
fmap_rs = []
|
||||
fmap_gs = []
|
||||
for _, d in enumerate(self.discriminators):
|
||||
y_d_r, fmap_r = d(y)
|
||||
y_d_g, fmap_g = d(y_hat)
|
||||
y_d_rs.append(y_d_r)
|
||||
fmap_rs.append(fmap_r)
|
||||
y_d_gs.append(y_d_g)
|
||||
fmap_gs.append(fmap_g)
|
||||
|
||||
return y_d_rs, y_d_gs, fmap_rs, fmap_gs
|
||||
|
||||
|
||||
def stft(x, fft_size, hop_size, win_length, window):
|
||||
"""Perform STFT and convert to magnitude spectrogram.
|
||||
Args:
|
||||
x (Tensor): Input signal tensor (B, T).
|
||||
fft_size (int): FFT size.
|
||||
hop_size (int): Hop size.
|
||||
win_length (int): Window length.
|
||||
window (str): Window function type.
|
||||
Returns:
|
||||
Tensor: Magnitude spectrogram (B, #frames, fft_size // 2 + 1).
|
||||
"""
|
||||
x_stft = torch.stft(x, fft_size, hop_size, win_length, window, return_complex=True)
|
||||
|
||||
# NOTE(kan-bayashi): clamp is needed to avoid nan or inf
|
||||
return torch.abs(x_stft).transpose(2, 1)
|
||||
|
||||
|
||||
class SpecDiscriminator(nn.Module):
|
||||
"""docstring for Discriminator."""
|
||||
|
||||
def __init__(self, fft_size=1024, shift_size=120, win_length=600, window="hann_window", use_spectral_norm=False):
|
||||
super(SpecDiscriminator, self).__init__()
|
||||
norm_f = weight_norm if use_spectral_norm is False else spectral_norm
|
||||
self.fft_size = fft_size
|
||||
self.shift_size = shift_size
|
||||
self.win_length = win_length
|
||||
self.window = getattr(torch, window)(win_length)
|
||||
self.discriminators = nn.ModuleList([
|
||||
norm_f(nn.Conv2d(1, 32, kernel_size=(3, 9), padding=(1, 4))),
|
||||
norm_f(nn.Conv2d(32, 32, kernel_size=(3, 9), stride=(1, 2), padding=(1, 4))),
|
||||
norm_f(nn.Conv2d(32, 32, kernel_size=(3, 9), stride=(1, 2), padding=(1, 4))),
|
||||
norm_f(nn.Conv2d(32, 32, kernel_size=(3, 9), stride=(1, 2), padding=(1, 4))),
|
||||
norm_f(nn.Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))),
|
||||
])
|
||||
|
||||
self.out = norm_f(nn.Conv2d(32, 1, 3, 1, 1))
|
||||
|
||||
def forward(self, y):
|
||||
|
||||
fmap = []
|
||||
y = y.squeeze(1)
|
||||
y = stft(y, self.fft_size, self.shift_size, self.win_length, self.window.to(y.device))
|
||||
y = y.unsqueeze(1)
|
||||
for _, d in enumerate(self.discriminators):
|
||||
y = d(y)
|
||||
y = F.leaky_relu(y, LRELU_SLOPE)
|
||||
fmap.append(y)
|
||||
|
||||
y = self.out(y)
|
||||
fmap.append(y)
|
||||
|
||||
return torch.flatten(y, 1, -1), fmap
|
||||
@@ -0,0 +1,103 @@
|
||||
# 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
|
||||
try:
|
||||
from torch.nn.utils.parametrizations import weight_norm
|
||||
except ImportError:
|
||||
from torch.nn.utils import weight_norm
|
||||
from cosyvoice.transformer.convolution import CausalConv1d
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
class CausalConvRNNF0Predictor(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(
|
||||
CausalConv1d(in_channels, cond_channels, kernel_size=4, causal_type='right')
|
||||
),
|
||||
nn.ELU(),
|
||||
weight_norm(
|
||||
CausalConv1d(cond_channels, cond_channels, kernel_size=3, causal_type='left')
|
||||
),
|
||||
nn.ELU(),
|
||||
weight_norm(
|
||||
CausalConv1d(cond_channels, cond_channels, kernel_size=3, causal_type='left')
|
||||
),
|
||||
nn.ELU(),
|
||||
weight_norm(
|
||||
CausalConv1d(cond_channels, cond_channels, kernel_size=3, causal_type='left')
|
||||
),
|
||||
nn.ELU(),
|
||||
weight_norm(
|
||||
CausalConv1d(cond_channels, cond_channels, kernel_size=3, causal_type='left')
|
||||
),
|
||||
nn.ELU(),
|
||||
)
|
||||
self.classifier = nn.Linear(in_features=cond_channels, out_features=self.num_class)
|
||||
|
||||
def forward(self, x: torch.Tensor, finalize: bool = True) -> torch.Tensor:
|
||||
if finalize is True:
|
||||
x = self.condnet[0](x)
|
||||
else:
|
||||
x = self.condnet[0](x[:, :, :-self.condnet[0].causal_padding], x[:, :, -self.condnet[0].causal_padding:])
|
||||
for i in range(1, len(self.condnet)):
|
||||
x = self.condnet[i](x)
|
||||
x = x.transpose(1, 2)
|
||||
return torch.abs(self.classifier(x).squeeze(-1))
|
||||
@@ -0,0 +1,746 @@
|
||||
# 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 as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.nn import Conv1d
|
||||
from torch.nn import ConvTranspose1d
|
||||
from torch.nn.utils import remove_weight_norm
|
||||
try:
|
||||
from torch.nn.utils.parametrizations import weight_norm
|
||||
except ImportError:
|
||||
from torch.nn.utils import weight_norm
|
||||
from torch.distributions.uniform import Uniform
|
||||
from cosyvoice.transformer.convolution import CausalConv1d, CausalConv1dDownSample, CausalConv1dUpsample
|
||||
from cosyvoice.transformer.activation import Snake
|
||||
from cosyvoice.utils.common import get_padding
|
||||
from cosyvoice.utils.common import init_weights
|
||||
|
||||
|
||||
"""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],
|
||||
causal: bool = False,
|
||||
):
|
||||
super(ResBlock, self).__init__()
|
||||
self.causal = causal
|
||||
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)) if causal is False else
|
||||
CausalConv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=dilation,
|
||||
causal_type='left'
|
||||
)
|
||||
)
|
||||
)
|
||||
self.convs2.append(
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=1,
|
||||
padding=get_padding(kernel_size, 1)) if causal is False else
|
||||
CausalConv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=1,
|
||||
causal_type='left'
|
||||
)
|
||||
)
|
||||
)
|
||||
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):
|
||||
""" sine_tensor, uv = forward(f0)
|
||||
input F0: tensor(batchsize=1, dim=1, length)
|
||||
f0 for unvoiced steps should be 0
|
||||
output sine_tensor: tensor(batchsize=1, length, dim)
|
||||
output uv: tensor(batchsize=1, length, 1)
|
||||
"""
|
||||
f0 = f0.transpose(1, 2)
|
||||
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.transpose(1, 2), uv.transpose(1, 2), noise
|
||||
|
||||
|
||||
class SineGen2(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, upsample_scale, harmonic_num=0,
|
||||
sine_amp=0.1, noise_std=0.003,
|
||||
voiced_threshold=0,
|
||||
flag_for_pulse=False,
|
||||
causal=False):
|
||||
super(SineGen2, self).__init__()
|
||||
self.sine_amp = sine_amp
|
||||
self.noise_std = noise_std
|
||||
self.harmonic_num = harmonic_num
|
||||
self.dim = self.harmonic_num + 1
|
||||
self.sampling_rate = samp_rate
|
||||
self.voiced_threshold = voiced_threshold
|
||||
self.flag_for_pulse = flag_for_pulse
|
||||
self.upsample_scale = upsample_scale
|
||||
self.causal = causal
|
||||
if causal is True:
|
||||
self.rand_ini = torch.rand(1, 9)
|
||||
self.rand_ini[:, 0] = 0
|
||||
self.sine_waves = torch.rand(1, 300 * 24000, 9)
|
||||
|
||||
def _f02uv(self, f0):
|
||||
# generate uv signal
|
||||
uv = (f0 > self.voiced_threshold).type(torch.float32)
|
||||
return uv
|
||||
|
||||
def _f02sine(self, f0_values):
|
||||
""" f0_values: (batchsize, length, dim)
|
||||
where dim indicates fundamental tone and overtones
|
||||
"""
|
||||
# convert to F0 in rad. The interger part n can be ignored
|
||||
# because 2 * np.pi * n doesn't affect phase
|
||||
rad_values = (f0_values / self.sampling_rate) % 1
|
||||
|
||||
# initial phase noise (no noise for fundamental component)
|
||||
if self.training is False and self.causal is True:
|
||||
rad_values[:, 0, :] = rad_values[:, 0, :] + self.rand_ini.to(rad_values.device)
|
||||
else:
|
||||
rand_ini = torch.rand(f0_values.shape[0], f0_values.shape[2], device=f0_values.device)
|
||||
rand_ini[:, 0] = 0
|
||||
rad_values[:, 0, :] = rad_values[:, 0, :] + rand_ini
|
||||
|
||||
# instantanouse phase sine[t] = sin(2*pi \sum_i=1 ^{t} rad)
|
||||
if not self.flag_for_pulse:
|
||||
rad_values = torch.nn.functional.interpolate(rad_values.transpose(1, 2),
|
||||
scale_factor=1 / self.upsample_scale,
|
||||
mode="linear").transpose(1, 2)
|
||||
|
||||
phase = torch.cumsum(rad_values, dim=1) * 2 * np.pi
|
||||
phase = torch.nn.functional.interpolate(phase.transpose(1, 2) * self.upsample_scale,
|
||||
scale_factor=self.upsample_scale, mode="nearest" if self.causal is True else 'linear').transpose(1, 2)
|
||||
sines = torch.sin(phase)
|
||||
else:
|
||||
# If necessary, make sure that the first time step of every
|
||||
# voiced segments is sin(pi) or cos(0)
|
||||
# This is used for pulse-train generation
|
||||
|
||||
# identify the last time step in unvoiced segments
|
||||
uv = self._f02uv(f0_values)
|
||||
uv_1 = torch.roll(uv, shifts=-1, dims=1)
|
||||
uv_1[:, -1, :] = 1
|
||||
u_loc = (uv < 1) * (uv_1 > 0)
|
||||
|
||||
# get the instantanouse phase
|
||||
tmp_cumsum = torch.cumsum(rad_values, dim=1)
|
||||
# different batch needs to be processed differently
|
||||
for idx in range(f0_values.shape[0]):
|
||||
temp_sum = tmp_cumsum[idx, u_loc[idx, :, 0], :]
|
||||
temp_sum[1:, :] = temp_sum[1:, :] - temp_sum[0:-1, :]
|
||||
# stores the accumulation of i.phase within
|
||||
# each voiced segments
|
||||
tmp_cumsum[idx, :, :] = 0
|
||||
tmp_cumsum[idx, u_loc[idx, :, 0], :] = temp_sum
|
||||
|
||||
# rad_values - tmp_cumsum: remove the accumulation of i.phase
|
||||
# within the previous voiced segment.
|
||||
i_phase = torch.cumsum(rad_values - tmp_cumsum, dim=1)
|
||||
|
||||
# get the sines
|
||||
sines = torch.cos(i_phase * 2 * np.pi)
|
||||
return sines
|
||||
|
||||
def forward(self, f0):
|
||||
""" sine_tensor, uv = forward(f0)
|
||||
input F0: tensor(batchsize=1, length, dim=1)
|
||||
f0 for unvoiced steps should be 0
|
||||
output sine_tensor: tensor(batchsize=1, length, dim)
|
||||
output uv: tensor(batchsize=1, length, 1)
|
||||
"""
|
||||
# fundamental component
|
||||
fn = torch.multiply(f0, torch.FloatTensor([[range(1, self.harmonic_num + 2)]]).to(f0.device))
|
||||
|
||||
# generate sine waveforms
|
||||
sine_waves = self._f02sine(fn) * self.sine_amp
|
||||
|
||||
# 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
|
||||
if self.training is False and self.causal is True:
|
||||
noise = noise_amp * self.sine_waves[:, :sine_waves.shape[1]].to(sine_waves.device)
|
||||
else:
|
||||
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, sinegen_type='1', causal=False):
|
||||
super(SourceModuleHnNSF, self).__init__()
|
||||
|
||||
self.sine_amp = sine_amp
|
||||
self.noise_std = add_noise_std
|
||||
|
||||
# to produce sine waveforms
|
||||
if sinegen_type == '1':
|
||||
self.l_sin_gen = SineGen(sampling_rate, harmonic_num, sine_amp, add_noise_std, voiced_threshod)
|
||||
else:
|
||||
self.l_sin_gen = SineGen2(sampling_rate, upsample_scale, harmonic_num, sine_amp, add_noise_std, voiced_threshod, causal=causal)
|
||||
|
||||
# to merge source harmonics into a single excitation
|
||||
self.l_linear = torch.nn.Linear(harmonic_num + 1, 1)
|
||||
self.l_tanh = torch.nn.Tanh()
|
||||
self.causal = causal
|
||||
if causal is True:
|
||||
self.uv = torch.rand(1, 300 * 24000, 1)
|
||||
|
||||
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)
|
||||
sine_merge = self.l_tanh(self.l_linear(sine_wavs))
|
||||
|
||||
# source for noise branch, in the same shape as uv
|
||||
if self.training is False and self.causal is True:
|
||||
noise = self.uv[:, :uv.shape[1]] * self.sine_amp / 3
|
||||
else:
|
||||
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)
|
||||
# NOTE in CosyVoice2, we use the original SineGen implementation
|
||||
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,
|
||||
sinegen_type='1' if self.sampling_rate == 22050 else '2',
|
||||
causal=False)
|
||||
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
|
||||
|
||||
|
||||
class CausalHiFTGenerator(HiFTGenerator):
|
||||
"""
|
||||
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,
|
||||
conv_pre_look_right: int = 4,
|
||||
f0_predictor: torch.nn.Module = None,
|
||||
):
|
||||
torch.nn.Module.__init__(self)
|
||||
|
||||
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,
|
||||
sinegen_type='1' if self.sampling_rate == 22050 else '2',
|
||||
causal=True)
|
||||
self.upsample_rates = upsample_rates
|
||||
self.f0_upsamp = torch.nn.Upsample(scale_factor=np.prod(upsample_rates) * istft_params["hop_len"])
|
||||
|
||||
self.conv_pre = weight_norm(
|
||||
CausalConv1d(in_channels, base_channels, conv_pre_look_right + 1, 1, causal_type='right')
|
||||
)
|
||||
|
||||
# Up
|
||||
self.ups = nn.ModuleList()
|
||||
for i, (u, k) in enumerate(zip(upsample_rates, upsample_kernel_sizes)):
|
||||
self.ups.append(
|
||||
weight_norm(
|
||||
CausalConv1dUpsample(
|
||||
base_channels // (2**i),
|
||||
base_channels // (2**(i + 1)),
|
||||
k,
|
||||
u,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# 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(
|
||||
CausalConv1d(istft_params["n_fft"] + 2, base_channels // (2 ** (i + 1)), 1, 1, causal_type='left')
|
||||
)
|
||||
else:
|
||||
self.source_downs.append(
|
||||
CausalConv1dDownSample(istft_params["n_fft"] + 2, base_channels // (2 ** (i + 1)), u * 2, u)
|
||||
)
|
||||
|
||||
self.source_resblocks.append(
|
||||
ResBlock(base_channels // (2 ** (i + 1)), k, d, causal=True)
|
||||
)
|
||||
|
||||
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, causal=True))
|
||||
|
||||
self.conv_post = weight_norm(CausalConv1d(ch, istft_params["n_fft"] + 2, 7, 1, causal_type='left'))
|
||||
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.conv_pre_look_right = conv_pre_look_right
|
||||
self.f0_predictor = f0_predictor
|
||||
|
||||
def decode(self, x: torch.Tensor, s: torch.Tensor = torch.zeros(1, 1, 0), finalize: bool = True) -> torch.Tensor:
|
||||
s_stft_real, s_stft_imag = self._stft(s.squeeze(1))
|
||||
if finalize is True:
|
||||
x = self.conv_pre(x)
|
||||
else:
|
||||
x = self.conv_pre(x[:, :, :-self.conv_pre_look_right], x[:, :, -self.conv_pre_look_right:])
|
||||
s_stft_real = s_stft_real[:, :, :-int(np.prod(self.upsample_rates) * self.conv_pre_look_right)]
|
||||
s_stft_imag = s_stft_imag[:, :, :-int(np.prod(self.upsample_rates) * self.conv_pre_look_right)]
|
||||
s_stft = torch.cat([s_stft_real, s_stft_imag], dim=1)
|
||||
|
||||
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)
|
||||
if finalize is False:
|
||||
x = x[:, :-int(np.prod(self.upsample_rates) * self.istft_params['hop_len'])]
|
||||
x = torch.clamp(x, -self.audio_limit, self.audio_limit)
|
||||
return x
|
||||
|
||||
@torch.inference_mode()
|
||||
def inference(self, speech_feat: torch.Tensor, finalize: bool = True) -> torch.Tensor:
|
||||
# mel->f0 NOTE f0_predictor precision is crucial for causal inference, move self.f0_predictor to cpu if necessary
|
||||
self.f0_predictor.to('cpu')
|
||||
f0 = self.f0_predictor(speech_feat.cpu(), finalize=finalize).to(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)
|
||||
if finalize is True:
|
||||
generated_speech = self.decode(x=speech_feat, s=s, finalize=finalize)
|
||||
else:
|
||||
generated_speech = self.decode(x=speech_feat[:, :, :-self.f0_predictor.condnet[0].causal_padding], s=s, finalize=finalize)
|
||||
return generated_speech, s
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
torch.backends.cudnn.deterministic = True
|
||||
torch.backends.cudnn.benchmark = False
|
||||
from hyperpyyaml import load_hyperpyyaml
|
||||
with open('./pretrained_models/Fun-CosyVoice3-0.5B/cosyvoice3.yaml', 'r') as f:
|
||||
configs = load_hyperpyyaml(f, overrides={'llm': None, 'flow': None})
|
||||
model = configs['hift']
|
||||
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
||||
model.to(device)
|
||||
model.eval()
|
||||
max_len, chunk_size, context_size = 300, 30, 8
|
||||
mel = torch.rand(1, 80, max_len).to(device)
|
||||
pred_gt, _ = model.inference(mel)
|
||||
for i in range(0, max_len, chunk_size):
|
||||
finalize = True if i + chunk_size + context_size >= max_len else False
|
||||
pred_chunk, _ = model.inference(mel[:, :, : i + chunk_size + context_size], finalize=finalize)
|
||||
pred_chunk = pred_chunk[:, i * 480:]
|
||||
print((pred_gt[:, i * 480:i * 480 + pred_chunk.shape[1]] - pred_chunk).abs().max().item())
|
||||
@@ -0,0 +1,67 @@
|
||||
from typing import Dict, Optional
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from matcha.hifigan.models import feature_loss, generator_loss, discriminator_loss
|
||||
from cosyvoice.utils.losses import tpr_loss, mel_loss
|
||||
|
||||
|
||||
class HiFiGan(nn.Module):
|
||||
def __init__(self, generator, discriminator, mel_spec_transform,
|
||||
multi_mel_spectral_recon_loss_weight=45, feat_match_loss_weight=2.0,
|
||||
tpr_loss_weight=1.0, tpr_loss_tau=0.04):
|
||||
super(HiFiGan, self).__init__()
|
||||
self.generator = generator
|
||||
self.discriminator = discriminator
|
||||
self.mel_spec_transform = mel_spec_transform
|
||||
self.multi_mel_spectral_recon_loss_weight = multi_mel_spectral_recon_loss_weight
|
||||
self.feat_match_loss_weight = feat_match_loss_weight
|
||||
self.tpr_loss_weight = tpr_loss_weight
|
||||
self.tpr_loss_tau = tpr_loss_tau
|
||||
|
||||
def forward(
|
||||
self,
|
||||
batch: dict,
|
||||
device: torch.device,
|
||||
) -> Dict[str, Optional[torch.Tensor]]:
|
||||
if batch['turn'] == 'generator':
|
||||
return self.forward_generator(batch, device)
|
||||
else:
|
||||
return self.forward_discriminator(batch, device)
|
||||
|
||||
def forward_generator(self, batch, device):
|
||||
real_speech = batch['speech'].to(device)
|
||||
pitch_feat = batch['pitch_feat'].to(device)
|
||||
# 1. calculate generator outputs
|
||||
generated_speech, generated_f0 = self.generator(batch, device)
|
||||
# 2. calculate discriminator outputs
|
||||
y_d_rs, y_d_gs, fmap_rs, fmap_gs = self.discriminator(real_speech, generated_speech)
|
||||
# 3. calculate generator losses, feature loss, mel loss, tpr losses [Optional]
|
||||
loss_gen, _ = generator_loss(y_d_gs)
|
||||
loss_fm = feature_loss(fmap_rs, fmap_gs)
|
||||
loss_mel = mel_loss(real_speech, generated_speech, self.mel_spec_transform)
|
||||
if self.tpr_loss_weight != 0:
|
||||
loss_tpr = tpr_loss(y_d_gs, y_d_rs, self.tpr_loss_tau)
|
||||
else:
|
||||
loss_tpr = torch.zeros(1).to(device)
|
||||
loss_f0 = F.l1_loss(generated_f0, pitch_feat)
|
||||
loss = loss_gen + self.feat_match_loss_weight * loss_fm + \
|
||||
self.multi_mel_spectral_recon_loss_weight * loss_mel + \
|
||||
self.tpr_loss_weight * loss_tpr + loss_f0
|
||||
return {'loss': loss, 'loss_gen': loss_gen, 'loss_fm': loss_fm, 'loss_mel': loss_mel, 'loss_tpr': loss_tpr, 'loss_f0': loss_f0}
|
||||
|
||||
def forward_discriminator(self, batch, device):
|
||||
real_speech = batch['speech'].to(device)
|
||||
# 1. calculate generator outputs
|
||||
with torch.no_grad():
|
||||
generated_speech, generated_f0 = self.generator(batch, device)
|
||||
# 2. calculate discriminator outputs
|
||||
y_d_rs, y_d_gs, fmap_rs, fmap_gs = self.discriminator(real_speech, generated_speech.detach())
|
||||
# 3. calculate discriminator losses, tpr losses [Optional]
|
||||
loss_disc, _, _ = discriminator_loss(y_d_rs, y_d_gs)
|
||||
if self.tpr_loss_weight != 0:
|
||||
loss_tpr = tpr_loss(y_d_rs, y_d_gs, self.tpr_loss_tau)
|
||||
else:
|
||||
loss_tpr = torch.zeros(1).to(device)
|
||||
loss = loss_disc + self.tpr_loss_weight * loss_tpr
|
||||
return {'loss': loss, 'loss_disc': loss_disc, 'loss_tpr': loss_tpr}
|
||||
@@ -0,0 +1,767 @@
|
||||
# Copyright (c) 2024 Alibaba Inc (authors: Xiang Lyu, Zhihao Du)
|
||||
# 2025 Alibaba Inc (authors: Xiang Lyu, Yabin Li, Qihua, Shengqiang Li)
|
||||
#
|
||||
# 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 queue
|
||||
import random
|
||||
import time
|
||||
import threading
|
||||
from typing import Dict, Optional, Callable, List, Generator
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch import nn
|
||||
import torch.nn.functional as F
|
||||
from transformers import Qwen2ForCausalLM
|
||||
from torch.nn.utils.rnn import pad_sequence, unpad_sequence
|
||||
from cosyvoice.utils.common import IGNORE_ID
|
||||
from cosyvoice.transformer.label_smoothing_loss import LabelSmoothingLoss
|
||||
from cosyvoice.utils.common import th_accuracy
|
||||
from cosyvoice.utils.file_utils import logging
|
||||
from cosyvoice.utils.mask import make_pad_mask
|
||||
|
||||
|
||||
class TransformerLM(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
text_encoder_input_size: int,
|
||||
llm_input_size: int,
|
||||
llm_output_size: int,
|
||||
text_token_size: int,
|
||||
speech_token_size: int,
|
||||
text_encoder: torch.nn.Module,
|
||||
llm: torch.nn.Module,
|
||||
sampling: Callable,
|
||||
length_normalized_loss: bool = True,
|
||||
lsm_weight: float = 0.0,
|
||||
spk_embed_dim: int = 192,
|
||||
):
|
||||
super().__init__()
|
||||
self.llm_input_size = llm_input_size
|
||||
self.speech_token_size = speech_token_size
|
||||
# 1. build text token inputs related modules
|
||||
self.text_embedding = torch.nn.Embedding(text_token_size, text_encoder_input_size)
|
||||
self.text_encoder = text_encoder
|
||||
self.text_encoder_affine_layer = nn.Linear(
|
||||
self.text_encoder.output_size(),
|
||||
llm_input_size
|
||||
)
|
||||
|
||||
# 2. build speech token language model related modules
|
||||
self.sos = 0
|
||||
self.task_id = 1
|
||||
self.eos_token = self.speech_token_size
|
||||
self.llm_embedding = torch.nn.Embedding(2, llm_input_size)
|
||||
self.llm = llm
|
||||
self.llm_decoder = nn.Linear(llm_output_size, speech_token_size + 1)
|
||||
self.criterion_ce = LabelSmoothingLoss(
|
||||
size=speech_token_size + 1,
|
||||
padding_idx=IGNORE_ID,
|
||||
smoothing=lsm_weight,
|
||||
normalize_length=length_normalized_loss,
|
||||
)
|
||||
|
||||
# 3. [Optional] build speech token related modules
|
||||
self.speech_embedding = torch.nn.Embedding(speech_token_size, llm_input_size)
|
||||
self.spk_embed_affine_layer = torch.nn.Linear(spk_embed_dim, llm_input_size)
|
||||
|
||||
# 4. sampling method
|
||||
self.sampling = sampling
|
||||
|
||||
def encode(
|
||||
self,
|
||||
text: torch.Tensor,
|
||||
text_lengths: torch.Tensor,
|
||||
):
|
||||
encoder_out, encoder_mask = self.text_encoder(text, text_lengths, decoding_chunk_size=1, num_decoding_left_chunks=-1)
|
||||
encoder_out_lens = encoder_mask.squeeze(1).sum(1)
|
||||
encoder_out = self.text_encoder_affine_layer(encoder_out)
|
||||
return encoder_out, encoder_out_lens
|
||||
|
||||
def pad_unpad_sequence(self, sos_emb, embedding, text_token, text_token_len, task_id_emb, speech_token, speech_token_len):
|
||||
text_token = unpad_sequence(text_token, text_token_len.cpu(), batch_first=True)
|
||||
speech_token = unpad_sequence(speech_token, speech_token_len.cpu(), batch_first=True)
|
||||
lm_input = [torch.concat([sos_emb.squeeze(dim=0), embedding[i], text_token[i], task_id_emb.squeeze(dim=0), speech_token[i]], dim=0)
|
||||
for i in range(len(text_token))]
|
||||
lm_input_len = torch.tensor([i.size(0) for i in lm_input], dtype=torch.int32)
|
||||
lm_input = pad_sequence(lm_input, batch_first=True, padding_value=IGNORE_ID)
|
||||
return lm_input, lm_input_len
|
||||
|
||||
def forward(
|
||||
self,
|
||||
batch: dict,
|
||||
device: torch.device,
|
||||
) -> Dict[str, Optional[torch.Tensor]]:
|
||||
"""
|
||||
Args:
|
||||
text: (B, L, D)
|
||||
text_lengths: (B,)
|
||||
audio: (B, T, N) or (B, T)
|
||||
audio_lengths: (B,)
|
||||
"""
|
||||
text_token = batch['text_token'].to(device)
|
||||
text_token_len = batch['text_token_len'].to(device)
|
||||
speech_token = batch['speech_token'].to(device)
|
||||
speech_token_len = batch['speech_token_len'].to(device)
|
||||
embedding = batch['embedding'].to(device)
|
||||
|
||||
# 1. prepare llm_target
|
||||
lm_target = [torch.tensor([IGNORE_ID] * (2 + text_token_len[i]) + speech_token[i, :speech_token_len[i]].tolist() +
|
||||
[self.speech_token_size]) for i in range(text_token.size(0))]
|
||||
lm_target = pad_sequence(lm_target, batch_first=True, padding_value=IGNORE_ID).to(device)
|
||||
|
||||
# 1. encode text_token
|
||||
text_token = self.text_embedding(text_token)
|
||||
text_token, text_token_len = self.encode(text_token, text_token_len)
|
||||
|
||||
# 2. embedding projection
|
||||
embedding = F.normalize(embedding, dim=1)
|
||||
embedding = self.spk_embed_affine_layer(embedding)
|
||||
embedding = embedding.unsqueeze(1)
|
||||
|
||||
# 3. sos and task_id
|
||||
sos_emb = self.llm_embedding.weight[self.sos].reshape(1, 1, -1)
|
||||
task_id_emb = self.llm_embedding.weight[self.task_id].reshape(1, 1, -1)
|
||||
|
||||
# 4. encode speech_token
|
||||
speech_token = self.speech_embedding(speech_token)
|
||||
|
||||
# 5. unpad and pad
|
||||
lm_input, lm_input_len = self.pad_unpad_sequence(sos_emb, embedding, text_token, text_token_len,
|
||||
task_id_emb, speech_token, speech_token_len)
|
||||
|
||||
# 6. run lm forward
|
||||
lm_output, lm_output_mask = self.llm(lm_input, lm_input_len.to(device))
|
||||
logits = self.llm_decoder(lm_output)
|
||||
loss = self.criterion_ce(logits, lm_target)
|
||||
acc = th_accuracy(logits.view(-1, self.speech_token_size + 1), lm_target, ignore_label=IGNORE_ID)
|
||||
return {'loss': loss, 'acc': acc}
|
||||
|
||||
def sampling_ids(
|
||||
self,
|
||||
weighted_scores: torch.Tensor,
|
||||
decoded_tokens: List,
|
||||
sampling: int,
|
||||
ignore_eos: bool = True,
|
||||
):
|
||||
num_trials, max_trials = 0, 100
|
||||
while True:
|
||||
top_ids = self.sampling(weighted_scores, decoded_tokens, sampling)
|
||||
if (not ignore_eos) or (top_ids < self.speech_token_size):
|
||||
break
|
||||
num_trials += 1
|
||||
if num_trials > max_trials:
|
||||
raise RuntimeError('sampling reaches max_trials {} and still get eos when ignore_eos is True, check your input!'.format(max_trials))
|
||||
return top_ids
|
||||
|
||||
@torch.inference_mode()
|
||||
def inference(
|
||||
self,
|
||||
text: torch.Tensor,
|
||||
text_len: torch.Tensor,
|
||||
prompt_text: torch.Tensor,
|
||||
prompt_text_len: torch.Tensor,
|
||||
prompt_speech_token: torch.Tensor,
|
||||
prompt_speech_token_len: torch.Tensor,
|
||||
embedding: torch.Tensor,
|
||||
sampling: int = 25,
|
||||
max_token_text_ratio: float = 20,
|
||||
min_token_text_ratio: float = 2,
|
||||
uuid: str = '',
|
||||
) -> Generator[torch.Tensor, None, None]:
|
||||
device = text.device
|
||||
text = torch.concat([prompt_text, text], dim=1)
|
||||
text_len += prompt_text_len
|
||||
text = self.text_embedding(text)
|
||||
|
||||
# 1. encode text
|
||||
text, text_len = self.encode(text, text_len)
|
||||
|
||||
# 2. encode embedding
|
||||
if embedding.shape[0] != 0:
|
||||
embedding = F.normalize(embedding, dim=1)
|
||||
embedding = self.spk_embed_affine_layer(embedding)
|
||||
embedding = embedding.unsqueeze(dim=1)
|
||||
else:
|
||||
embedding = torch.zeros(1, 0, self.llm_input_size, dtype=text.dtype).to(device).to(text.dtype)
|
||||
|
||||
# 3. concat llm_input
|
||||
sos_emb = self.llm_embedding.weight[self.sos].reshape(1, 1, -1)
|
||||
task_id_emb = self.llm_embedding.weight[self.task_id].reshape(1, 1, -1)
|
||||
if prompt_speech_token_len != 0:
|
||||
prompt_speech_token_emb = self.speech_embedding(prompt_speech_token)
|
||||
else:
|
||||
prompt_speech_token_emb = torch.zeros(1, 0, self.llm_input_size, dtype=text.dtype).to(device)
|
||||
lm_input = torch.concat([sos_emb, embedding, text, task_id_emb, prompt_speech_token_emb], dim=1)
|
||||
|
||||
# 4. cal min/max_length
|
||||
min_len = int((text_len - prompt_text_len) * min_token_text_ratio)
|
||||
max_len = int((text_len - prompt_text_len) * max_token_text_ratio)
|
||||
|
||||
# 5. step by step decode
|
||||
out_tokens = []
|
||||
offset = 0
|
||||
att_cache, cnn_cache = torch.zeros((0, 0, 0, 0), device=lm_input.device), torch.zeros((0, 0, 0, 0), device=lm_input.device)
|
||||
for i in range(max_len):
|
||||
y_pred, att_cache, cnn_cache = self.llm.forward_chunk(lm_input, offset=offset, required_cache_size=-1,
|
||||
att_cache=att_cache, cnn_cache=cnn_cache,
|
||||
att_mask=torch.tril(torch.ones((1, lm_input.shape[1], lm_input.shape[1]),
|
||||
device=lm_input.device)).to(torch.bool))
|
||||
logp = self.llm_decoder(y_pred[:, -1]).log_softmax(dim=-1)
|
||||
top_ids = self.sampling_ids(logp.squeeze(dim=0), out_tokens, sampling, ignore_eos=True if i < min_len else False)
|
||||
if top_ids == self.eos_token:
|
||||
break
|
||||
# in stream mode, yield token one by one
|
||||
yield top_ids
|
||||
out_tokens.append(top_ids)
|
||||
offset += lm_input.size(1)
|
||||
lm_input = self.speech_embedding.weight[top_ids].reshape(1, 1, -1)
|
||||
|
||||
|
||||
class Qwen2Encoder(torch.nn.Module):
|
||||
def __init__(self, pretrain_path):
|
||||
super().__init__()
|
||||
self.model = Qwen2ForCausalLM.from_pretrained(pretrain_path)
|
||||
|
||||
def forward(self, xs: torch.Tensor, xs_lens: torch.Tensor):
|
||||
T = xs.size(1)
|
||||
masks = ~make_pad_mask(xs_lens, T)
|
||||
outs = self.model(
|
||||
inputs_embeds=xs,
|
||||
attention_mask=masks,
|
||||
output_hidden_states=True,
|
||||
return_dict=True,
|
||||
)
|
||||
return outs.hidden_states[-1], masks.unsqueeze(1)
|
||||
|
||||
def forward_one_step(self, xs, masks, cache=None):
|
||||
# For cached inference, we need to compute the proper attention mask
|
||||
# that covers the full sequence (cached positions + new positions)
|
||||
if cache is not None:
|
||||
# Get the cached sequence length
|
||||
if hasattr(cache, 'get_seq_length'):
|
||||
# DynamicCache (transformers >= 4.36)
|
||||
cached_len = cache.get_seq_length()
|
||||
elif hasattr(cache, '__len__') and len(cache) > 0:
|
||||
# Tuple format (older transformers)
|
||||
cached_len = cache[0][0].shape[2]
|
||||
else:
|
||||
cached_len = 0
|
||||
|
||||
# Total sequence length = cached + new tokens
|
||||
total_len = cached_len + xs.shape[1]
|
||||
# Create attention mask for full sequence (all ones = attend to all positions)
|
||||
input_masks = torch.ones((xs.shape[0], total_len), dtype=torch.long, device=xs.device)
|
||||
else:
|
||||
# No cache - use the provided mask's last row
|
||||
input_masks = masks[:, -1, :]
|
||||
|
||||
outs = self.model(
|
||||
inputs_embeds=xs,
|
||||
attention_mask=input_masks,
|
||||
output_hidden_states=True,
|
||||
return_dict=True,
|
||||
use_cache=True,
|
||||
past_key_values=cache,
|
||||
)
|
||||
xs = outs.hidden_states[-1]
|
||||
new_cache = outs.past_key_values
|
||||
return xs, new_cache
|
||||
|
||||
|
||||
class Qwen2LM(TransformerLM):
|
||||
def __init__(
|
||||
self,
|
||||
llm_input_size: int,
|
||||
llm_output_size: int,
|
||||
speech_token_size: int,
|
||||
llm: torch.nn.Module,
|
||||
sampling: Callable,
|
||||
length_normalized_loss: bool = True,
|
||||
lsm_weight: float = 0.0,
|
||||
mix_ratio: List[int] = [5, 15],
|
||||
):
|
||||
torch.nn.Module.__init__(self)
|
||||
self.llm_input_size = llm_input_size
|
||||
self.llm_output_size = llm_output_size
|
||||
self.speech_token_size = speech_token_size
|
||||
# 2. build speech token language model related modules
|
||||
self.sos = 0
|
||||
self.task_id = 1
|
||||
self.eos_token = speech_token_size
|
||||
self.fill_token = speech_token_size + 2
|
||||
|
||||
self.llm_embedding = torch.nn.Embedding(2, llm_input_size)
|
||||
self.llm = llm
|
||||
self.llm_decoder = nn.Linear(llm_output_size, speech_token_size + 3)
|
||||
self.criterion_ce = LabelSmoothingLoss(
|
||||
size=speech_token_size + 3,
|
||||
padding_idx=IGNORE_ID,
|
||||
smoothing=lsm_weight,
|
||||
normalize_length=length_normalized_loss,
|
||||
)
|
||||
|
||||
# 3. [Optional] build speech token related modules
|
||||
self.speech_embedding = torch.nn.Embedding(speech_token_size + 3, llm_input_size)
|
||||
|
||||
# 4. sampling method
|
||||
self.sampling = sampling
|
||||
self.mix_ratio = mix_ratio
|
||||
|
||||
# 5. vllm related
|
||||
self.stop_token_ids = [speech_token_size + i for i in range(3)]
|
||||
self.vllm_output_queue = {}
|
||||
|
||||
def prepare_lm_input_target(self, sos_emb, text_token, text_token_emb, text_token_len, task_id_emb, speech_token, speech_token_emb, speech_token_len):
|
||||
lm_target, lm_input = [], []
|
||||
text_token = unpad_sequence(text_token, text_token_len.cpu(), batch_first=True)
|
||||
speech_token = unpad_sequence(speech_token, speech_token_len.cpu(), batch_first=True)
|
||||
text_token_emb = unpad_sequence(text_token_emb, text_token_len.cpu(), batch_first=True)
|
||||
speech_token_emb = unpad_sequence(speech_token_emb, speech_token_len.cpu(), batch_first=True)
|
||||
for i in range(len(text_token)):
|
||||
# bistream sequence
|
||||
if random.random() < 0.5 and speech_token_len[i] / text_token_len[i] > self.mix_ratio[1] / self.mix_ratio[0]:
|
||||
this_lm_target, this_lm_input = [], []
|
||||
this_lm_target.append(IGNORE_ID)
|
||||
this_lm_input.append(sos_emb.squeeze(dim=0))
|
||||
for j in range(((text_token_len[i] + 1) / self.mix_ratio[0]).ceil().int().item()):
|
||||
this_text_token = text_token[i][j * self.mix_ratio[0]: (j + 1) * self.mix_ratio[0]].tolist()
|
||||
this_speech_token = speech_token[i][j * self.mix_ratio[1]: (j + 1) * self.mix_ratio[1]].tolist()
|
||||
if len(this_text_token) == self.mix_ratio[0]:
|
||||
assert len(this_speech_token) == self.mix_ratio[1]
|
||||
this_lm_target += [IGNORE_ID] * (self.mix_ratio[0] - 1)
|
||||
this_lm_target += this_speech_token
|
||||
this_lm_target.append(self.fill_token)
|
||||
this_lm_input.append(text_token_emb[i][j * self.mix_ratio[0]: (j + 1) * self.mix_ratio[0]])
|
||||
this_lm_input.append(speech_token_emb[i][j * self.mix_ratio[1]: (j + 1) * self.mix_ratio[1]])
|
||||
else:
|
||||
this_lm_target += [-1] * len(this_text_token)
|
||||
this_lm_target += speech_token[i][j * self.mix_ratio[1]:].tolist()
|
||||
this_lm_target.append(self.eos_token)
|
||||
this_lm_input.append(text_token_emb[i][j * self.mix_ratio[0]:])
|
||||
this_lm_input.append(task_id_emb.squeeze(dim=0))
|
||||
this_lm_input.append(speech_token_emb[i][j * self.mix_ratio[1]:])
|
||||
this_lm_target, this_lm_input = torch.tensor(this_lm_target), torch.concat(this_lm_input, dim=0)
|
||||
# unistream sequence
|
||||
else:
|
||||
this_lm_target = torch.tensor([IGNORE_ID] * (1 + text_token_len[i]) + speech_token[i].tolist() + [self.eos_token])
|
||||
this_lm_input = torch.concat([sos_emb.squeeze(dim=0), text_token_emb[i], task_id_emb.squeeze(dim=0), speech_token_emb[i]], dim=0)
|
||||
lm_target.append(this_lm_target)
|
||||
lm_input.append(this_lm_input)
|
||||
lm_input_len = torch.tensor([i.size(0) for i in lm_input], dtype=torch.int32)
|
||||
lm_input = pad_sequence(lm_input, batch_first=True, padding_value=IGNORE_ID)
|
||||
lm_target = pad_sequence(lm_target, batch_first=True, padding_value=IGNORE_ID)
|
||||
return lm_target, lm_input, lm_input_len
|
||||
|
||||
def forward(
|
||||
self,
|
||||
batch: dict,
|
||||
device: torch.device,
|
||||
) -> Dict[str, Optional[torch.Tensor]]:
|
||||
"""
|
||||
Args:
|
||||
text: (B, L, D)
|
||||
text_lengths: (B,)
|
||||
audio: (B, T, N) or (B, T)
|
||||
audio_lengths: (B,)
|
||||
"""
|
||||
text_token = batch['text_token'].to(device)
|
||||
text_token_len = batch['text_token_len'].to(device)
|
||||
speech_token = batch['speech_token'].to(device)
|
||||
speech_token_len = batch['speech_token_len'].to(device)
|
||||
|
||||
# 1. encode text_token
|
||||
text_token_emb = self.llm.model.model.embed_tokens(text_token)
|
||||
|
||||
# 3. sos and task_id
|
||||
sos_emb = self.llm_embedding.weight[self.sos].reshape(1, 1, -1)
|
||||
task_id_emb = self.llm_embedding.weight[self.task_id].reshape(1, 1, -1)
|
||||
|
||||
# 2. encode speech_token
|
||||
speech_token_emb = self.speech_embedding(speech_token)
|
||||
|
||||
# 3. prepare llm_input/target
|
||||
lm_target, lm_input, lm_input_len = self.prepare_lm_input_target(sos_emb, text_token, text_token_emb, text_token_len, task_id_emb,
|
||||
speech_token, speech_token_emb, speech_token_len)
|
||||
lm_target = lm_target.to(device)
|
||||
|
||||
# 4. run lm forward
|
||||
lm_output, lm_output_mask = self.llm(lm_input, lm_input_len.to(device))
|
||||
logits = self.llm_decoder(lm_output)
|
||||
loss = self.criterion_ce(logits, lm_target.to(device))
|
||||
acc = th_accuracy(logits.view(-1, self.speech_token_size + 3), lm_target, ignore_label=IGNORE_ID)
|
||||
return {'loss': loss, 'acc': acc}
|
||||
|
||||
def forward_dpo(
|
||||
self,
|
||||
batch: dict,
|
||||
device: torch.device,
|
||||
) -> Dict[str, Optional[torch.Tensor]]:
|
||||
text_token = batch['text_token'].to(device)
|
||||
text_token_len = batch['text_token_len'].to(device)
|
||||
speech_token = batch['speech_token'].to(device)
|
||||
speech_token_len = batch['speech_token_len'].to(device)
|
||||
reject_speech_token = batch['reject_speech_token'].to(device)
|
||||
reject_speech_token_len = batch['reject_speech_token_len'].to(device)
|
||||
|
||||
# 1. encode text_token
|
||||
text_token_emb = self.llm.model.model.embed_tokens(text_token)
|
||||
|
||||
# 3. sos and task_id
|
||||
sos_emb = self.llm_embedding.weight[self.sos].reshape(1, 1, -1)
|
||||
task_id_emb = self.llm_embedding.weight[self.task_id].reshape(1, 1, -1)
|
||||
|
||||
# 2. encode speech_token
|
||||
speech_token = unpad_sequence(speech_token, speech_token_len.cpu(), batch_first=True)
|
||||
reject_speech_token = unpad_sequence(reject_speech_token, reject_speech_token_len.cpu(), batch_first=True)
|
||||
speech_token_combined = speech_token + reject_speech_token
|
||||
speech_token_combined = pad_sequence(speech_token_combined, batch_first=True, padding_value=0)
|
||||
speech_token_combined_len = torch.concat([speech_token_len, reject_speech_token_len], dim=0)
|
||||
speech_token_combined_emb = self.speech_embedding(speech_token_combined)
|
||||
|
||||
# 3. prepare llm_input/target
|
||||
lm_target, lm_input, lm_input_len = self.prepare_lm_input_target(sos_emb, text_token.repeat(2, 1), text_token_emb.repeat(2, 1, 1), text_token_len.repeat(2),
|
||||
task_id_emb, speech_token_combined, speech_token_combined_emb, speech_token_combined_len)
|
||||
lm_target = lm_target.to(device)
|
||||
|
||||
# 4. run lm forward
|
||||
lm_output, lm_output_mask = self.llm(lm_input, lm_input_len.to(device))
|
||||
logits = self.llm_decoder(lm_output)
|
||||
chosen_logits = logits[:text_token.shape[0]]
|
||||
rejected_logits = logits[text_token.shape[0]:]
|
||||
chosen_lm_target = lm_target[:text_token.shape[0]]
|
||||
rejected_lm_target = lm_target[text_token.shape[0]:]
|
||||
loss = self.criterion_ce(chosen_logits, chosen_lm_target.to(device))
|
||||
acc = th_accuracy(chosen_logits.view(-1, self.speech_token_size + 3), chosen_lm_target, ignore_label=IGNORE_ID)
|
||||
|
||||
# 5. calculate dpo logits
|
||||
chosen_lm_mask = chosen_lm_target == IGNORE_ID
|
||||
rejected_lm_mask = rejected_lm_target == IGNORE_ID
|
||||
chosen_logps = torch.gather(chosen_logits.log_softmax(dim=-1), dim=2, index=chosen_lm_target.masked_fill(chosen_lm_mask, 0).unsqueeze(dim=-1)).squeeze(dim=-1)
|
||||
rejected_logps = torch.gather(rejected_logits.log_softmax(dim=-1), dim=2, index=rejected_lm_target.masked_fill(rejected_lm_mask, 0).unsqueeze(dim=-1)).squeeze(dim=-1)
|
||||
chosen_logps = (chosen_logps * chosen_lm_mask).sum(dim=-1) / chosen_lm_mask.sum(dim=-1)
|
||||
rejected_logps = (rejected_logps * rejected_lm_mask).sum(dim=-1) / rejected_lm_mask.sum(dim=-1)
|
||||
return {'loss': loss, 'acc': acc, 'chosen_logps': chosen_logps, 'rejected_logps': rejected_logps}
|
||||
|
||||
@torch.inference_mode()
|
||||
def inference(
|
||||
self,
|
||||
text: torch.Tensor,
|
||||
text_len: torch.Tensor,
|
||||
prompt_text: torch.Tensor,
|
||||
prompt_text_len: torch.Tensor,
|
||||
prompt_speech_token: torch.Tensor,
|
||||
prompt_speech_token_len: torch.Tensor,
|
||||
embedding: torch.Tensor,
|
||||
sampling: int = 25,
|
||||
max_token_text_ratio: float = 20,
|
||||
min_token_text_ratio: float = 2,
|
||||
uuid: str = '',
|
||||
progress_callback=None,
|
||||
) -> Generator[torch.Tensor, None, None]:
|
||||
device = text.device
|
||||
text = torch.concat([prompt_text, text], dim=1)
|
||||
text_len += prompt_text_len
|
||||
text = self.llm.model.model.embed_tokens(text)
|
||||
|
||||
# 3. concat llm_input
|
||||
sos_emb = self.llm_embedding.weight[self.sos].reshape(1, 1, -1)
|
||||
task_id_emb = self.llm_embedding.weight[self.task_id].reshape(1, 1, -1)
|
||||
if prompt_speech_token_len != 0:
|
||||
prompt_speech_token_emb = self.speech_embedding(prompt_speech_token)
|
||||
else:
|
||||
prompt_speech_token_emb = torch.zeros(1, 0, self.llm_input_size, dtype=text.dtype).to(device)
|
||||
lm_input = torch.concat([sos_emb, text, task_id_emb, prompt_speech_token_emb], dim=1)
|
||||
|
||||
# 4. cal min/max_length
|
||||
min_len = int((text_len - prompt_text_len) * min_token_text_ratio)
|
||||
max_len = int((text_len - prompt_text_len) * max_token_text_ratio)
|
||||
|
||||
# 5. step by step decode
|
||||
for token in self.inference_wrapper(lm_input, sampling, min_len, max_len, uuid, progress_callback):
|
||||
yield token
|
||||
|
||||
@torch.inference_mode()
|
||||
def inference_wrapper(self, lm_input, sampling, min_len, max_len, uuid, progress_callback=None):
|
||||
if hasattr(self, 'vllm'):
|
||||
from vllm import SamplingParams, RequestOutput
|
||||
sampling_params = SamplingParams(top_k=sampling,
|
||||
stop_token_ids=self.stop_token_ids,
|
||||
min_tokens=min_len,
|
||||
max_tokens=max_len)
|
||||
with self.lock:
|
||||
self.vllm.add_request(uuid, {"prompt_embeds": lm_input.squeeze(0).to(torch.bfloat16).to(lm_input.device)}, sampling_params)
|
||||
self.vllm_output_queue[uuid] = queue.Queue()
|
||||
out_tokens = []
|
||||
while True:
|
||||
with self.lock:
|
||||
if self.vllm_output_queue[uuid].empty() is True:
|
||||
request_outputs: List[RequestOutput] = self.vllm.step()
|
||||
for request_output in request_outputs:
|
||||
top_ids = list(request_output.outputs[0].token_ids)[-1]
|
||||
self.vllm_output_queue[request_output.request_id].put(top_ids)
|
||||
if self.vllm_output_queue[uuid].empty() is False:
|
||||
top_ids = self.vllm_output_queue[uuid].get()
|
||||
if top_ids in self.stop_token_ids:
|
||||
break
|
||||
# Call progress callback
|
||||
if progress_callback is not None:
|
||||
progress_callback(len(out_tokens) + 1, max_len)
|
||||
# in stream mode, yield token one by one
|
||||
yield top_ids
|
||||
out_tokens.append(top_ids)
|
||||
if len(out_tokens) == max_len:
|
||||
break
|
||||
time.sleep(0.001)
|
||||
with self.lock:
|
||||
self.vllm_output_queue.pop(uuid)
|
||||
else:
|
||||
out_tokens = []
|
||||
cache = None
|
||||
for i in range(max_len):
|
||||
y_pred, cache = self.llm.forward_one_step(lm_input,
|
||||
masks=torch.tril(torch.ones((1, lm_input.shape[1], lm_input.shape[1]), device=lm_input.device)).to(torch.bool),
|
||||
cache=cache)
|
||||
logp = self.llm_decoder(y_pred[:, -1]).log_softmax(dim=-1)
|
||||
top_ids = self.sampling_ids(logp.squeeze(dim=0), out_tokens, sampling, ignore_eos=True if i < min_len else False)
|
||||
if top_ids in self.stop_token_ids:
|
||||
break
|
||||
# Call progress callback
|
||||
if progress_callback is not None:
|
||||
progress_callback(i + 1, max_len)
|
||||
# in stream mode, yield token one by one
|
||||
yield top_ids
|
||||
out_tokens.append(top_ids)
|
||||
lm_input = self.speech_embedding.weight[top_ids].reshape(1, 1, -1)
|
||||
|
||||
@torch.inference_mode()
|
||||
def inference_bistream(
|
||||
self,
|
||||
text: Generator,
|
||||
prompt_text: torch.Tensor,
|
||||
prompt_text_len: torch.Tensor,
|
||||
prompt_speech_token: torch.Tensor,
|
||||
prompt_speech_token_len: torch.Tensor,
|
||||
embedding: torch.Tensor,
|
||||
sampling: int = 25,
|
||||
max_token_text_ratio: float = 20,
|
||||
min_token_text_ratio: float = 2,
|
||||
) -> Generator[torch.Tensor, None, None]:
|
||||
|
||||
device = prompt_text.device
|
||||
# 1. prepare input
|
||||
sos_emb = self.llm_embedding.weight[self.sos].reshape(1, 1, -1)
|
||||
task_id_emb = self.llm_embedding.weight[self.task_id].reshape(1, 1, -1)
|
||||
if prompt_speech_token_len != 0:
|
||||
prompt_speech_token_emb = self.speech_embedding(prompt_speech_token)
|
||||
else:
|
||||
prompt_speech_token_emb = torch.zeros(1, 0, self.llm_input_size, dtype=prompt_text.dtype).to(device)
|
||||
lm_input = torch.concat([sos_emb], dim=1)
|
||||
|
||||
# 2. iterate text
|
||||
out_tokens = []
|
||||
cache = None
|
||||
# NOTE init prompt_text as text_cache as it is basically impossible prompt_speech_token/prompt_text < 15/5
|
||||
text_cache = self.llm.model.model.embed_tokens(prompt_text)
|
||||
next_fill_index = (int(prompt_speech_token.shape[1] / self.mix_ratio[1]) + 1) * self.mix_ratio[1] - prompt_speech_token.shape[1]
|
||||
for this_text in text:
|
||||
text_cache = torch.concat([text_cache, self.llm.model.model.embed_tokens(this_text)], dim=1)
|
||||
# prompt_speech_token_emb not empty, try append to lm_input
|
||||
while prompt_speech_token_emb.size(1) != 0:
|
||||
if text_cache.size(1) >= self.mix_ratio[0]:
|
||||
lm_input_text, lm_input_speech = text_cache[:, :self.mix_ratio[0]], prompt_speech_token_emb[:, :self.mix_ratio[1]]
|
||||
logging.info('append {} text token {} speech token'.format(lm_input_text.size(1), lm_input_speech.size(1)))
|
||||
lm_input = torch.concat([lm_input, lm_input_text, lm_input_speech], dim=1)
|
||||
text_cache, prompt_speech_token_emb = text_cache[:, self.mix_ratio[0]:], prompt_speech_token_emb[:, self.mix_ratio[1]:]
|
||||
else:
|
||||
logging.info('not enough text token to decode, wait for more')
|
||||
break
|
||||
# no prompt_speech_token_emb remain, can decode some speech token
|
||||
if prompt_speech_token_emb.size(1) == 0:
|
||||
if (len(out_tokens) != 0 and out_tokens[-1] == self.fill_token) or (len(out_tokens) == 0 and lm_input.size(1) == 1):
|
||||
logging.info('get fill token, need to append more text token')
|
||||
if text_cache.size(1) >= self.mix_ratio[0]:
|
||||
lm_input_text = text_cache[:, :self.mix_ratio[0]]
|
||||
logging.info('append {} text token'.format(lm_input_text.size(1)))
|
||||
if len(out_tokens) != 0 and out_tokens[-1] == self.fill_token:
|
||||
lm_input = lm_input_text
|
||||
else:
|
||||
lm_input = torch.concat([lm_input, lm_input_text], dim=1)
|
||||
text_cache = text_cache[:, self.mix_ratio[0]:]
|
||||
else:
|
||||
logging.info('not enough text token to decode, wait for more')
|
||||
continue
|
||||
while True:
|
||||
seq_len = lm_input.shape[1] if cache is None else lm_input.shape[1] + cache[0][0].size(2)
|
||||
y_pred, cache = self.llm.forward_one_step(lm_input,
|
||||
masks=torch.tril(torch.ones((1, seq_len, seq_len), device=lm_input.device)).to(torch.bool),
|
||||
cache=cache)
|
||||
logp = self.llm_decoder(y_pred[:, -1]).log_softmax(dim=-1)
|
||||
if next_fill_index != -1 and len(out_tokens) == next_fill_index:
|
||||
top_ids = self.fill_token
|
||||
next_fill_index += (self.mix_ratio[1] + 1)
|
||||
else:
|
||||
top_ids = self.sampling_ids(logp.squeeze(dim=0), out_tokens, sampling, ignore_eos=True)
|
||||
if top_ids == self.fill_token:
|
||||
next_fill_index = len(out_tokens) + self.mix_ratio[1] + 1
|
||||
logging.info('fill_token index {} next fill_token index {}'.format(len(out_tokens), next_fill_index))
|
||||
out_tokens.append(top_ids)
|
||||
if top_ids >= self.speech_token_size:
|
||||
if top_ids == self.fill_token:
|
||||
break
|
||||
else:
|
||||
raise ValueError('should not get token {}'.format(top_ids))
|
||||
yield top_ids
|
||||
lm_input = self.speech_embedding.weight[top_ids].reshape(1, 1, -1)
|
||||
|
||||
# 3. final decode
|
||||
lm_input = torch.concat([lm_input, text_cache, task_id_emb], dim=1)
|
||||
logging.info('no more text token, decode until met eos')
|
||||
while True:
|
||||
seq_len = lm_input.shape[1] if cache is None else lm_input.shape[1] + cache[0][0].size(2)
|
||||
y_pred, cache = self.llm.forward_one_step(lm_input,
|
||||
masks=torch.tril(torch.ones((1, seq_len, seq_len), device=lm_input.device)).to(torch.bool),
|
||||
cache=cache)
|
||||
logp = self.llm_decoder(y_pred[:, -1]).log_softmax(dim=-1)
|
||||
top_ids = self.sampling_ids(logp.squeeze(dim=0), out_tokens, sampling, ignore_eos=False)
|
||||
out_tokens.append(top_ids)
|
||||
if top_ids >= self.speech_token_size:
|
||||
if top_ids == self.eos_token:
|
||||
break
|
||||
else:
|
||||
raise ValueError('should not get token {}'.format(top_ids))
|
||||
# in stream mode, yield token one by one
|
||||
yield top_ids
|
||||
lm_input = self.speech_embedding.weight[top_ids].reshape(1, 1, -1)
|
||||
|
||||
|
||||
class CosyVoice3LM(Qwen2LM):
|
||||
def __init__(
|
||||
self,
|
||||
llm_input_size: int,
|
||||
llm_output_size: int,
|
||||
speech_token_size: int,
|
||||
llm: torch.nn.Module,
|
||||
sampling: Callable,
|
||||
length_normalized_loss: bool = True,
|
||||
lsm_weight: float = 0.0,
|
||||
mix_ratio: List[int] = [5, 15],
|
||||
):
|
||||
torch.nn.Module.__init__(self)
|
||||
self.llm_input_size = llm_input_size
|
||||
self.llm_output_size = llm_output_size
|
||||
self.speech_token_size = speech_token_size
|
||||
# 2. build speech token language model related modules
|
||||
self.sos = speech_token_size + 0
|
||||
self.eos_token = speech_token_size + 1
|
||||
self.task_id = speech_token_size + 2
|
||||
self.fill_token = speech_token_size + 3
|
||||
|
||||
self.llm = llm
|
||||
self.llm_decoder = nn.Linear(llm_output_size, speech_token_size + 200, bias=False)
|
||||
self.criterion_ce = LabelSmoothingLoss(
|
||||
size=speech_token_size + 200,
|
||||
padding_idx=IGNORE_ID,
|
||||
smoothing=lsm_weight,
|
||||
normalize_length=length_normalized_loss,
|
||||
)
|
||||
|
||||
# 3. [Optional] build speech token related modules
|
||||
self.speech_embedding = torch.nn.Embedding(speech_token_size + 200, llm_input_size)
|
||||
|
||||
# 4. sampling method
|
||||
self.sampling = sampling
|
||||
self.mix_ratio = mix_ratio
|
||||
|
||||
# 5. vllm related
|
||||
self.stop_token_ids = [speech_token_size + i for i in range(200)]
|
||||
self.vllm_output_queue = {}
|
||||
|
||||
def forward(
|
||||
self,
|
||||
batch: dict,
|
||||
device: torch.device,
|
||||
) -> Dict[str, Optional[torch.Tensor]]:
|
||||
"""
|
||||
Args:
|
||||
text: (B, L, D)
|
||||
text_lengths: (B,)
|
||||
audio: (B, T, N) or (B, T)
|
||||
audio_lengths: (B,)
|
||||
"""
|
||||
text_token = batch['text_token'].to(device)
|
||||
text_token_len = batch['text_token_len'].to(device)
|
||||
speech_token = batch['speech_token'].to(device)
|
||||
speech_token_len = batch['speech_token_len'].to(device)
|
||||
# NOTE should append instruct_token to sequence, not implemented yet
|
||||
instruct_token = batch['instruct_token'].to(device)
|
||||
instruct_token_len = batch['instruct_token_len'].to(device)
|
||||
|
||||
# 1. encode text_token
|
||||
text_token_emb = self.llm.model.model.embed_tokens(text_token)
|
||||
|
||||
# 3. sos and task_id
|
||||
sos_emb = self.speech_embedding.weight[self.sos].reshape(1, 1, -1)
|
||||
task_id_emb = self.speech_embedding.weight[self.task_id].reshape(1, 1, -1)
|
||||
|
||||
# 2. encode speech_token
|
||||
speech_token_emb = self.speech_embedding(speech_token)
|
||||
|
||||
# 3. prepare llm_input/target
|
||||
lm_target, lm_input, lm_input_len = self.prepare_lm_input_target(sos_emb, text_token, text_token_emb, text_token_len, task_id_emb,
|
||||
speech_token, speech_token_emb, speech_token_len)
|
||||
lm_target = lm_target.to(device)
|
||||
|
||||
# 4. run lm forward
|
||||
lm_output, lm_output_mask = self.llm(lm_input, lm_input_len.to(device))
|
||||
logits = self.llm_decoder(lm_output)
|
||||
loss = self.criterion_ce(logits, lm_target.to(device))
|
||||
acc = th_accuracy(logits.view(-1, self.speech_token_size + 3), lm_target, ignore_label=IGNORE_ID)
|
||||
return {'loss': loss, 'acc': acc}
|
||||
|
||||
@torch.inference_mode()
|
||||
def inference(
|
||||
self,
|
||||
text: torch.Tensor,
|
||||
text_len: torch.Tensor,
|
||||
prompt_text: torch.Tensor,
|
||||
prompt_text_len: torch.Tensor,
|
||||
prompt_speech_token: torch.Tensor,
|
||||
prompt_speech_token_len: torch.Tensor,
|
||||
embedding: torch.Tensor,
|
||||
sampling: int = 25,
|
||||
max_token_text_ratio: float = 20,
|
||||
min_token_text_ratio: float = 2,
|
||||
uuid: str = '',
|
||||
progress_callback=None,
|
||||
) -> Generator[torch.Tensor, None, None]:
|
||||
device = text.device
|
||||
text = torch.concat([prompt_text, text], dim=1)
|
||||
text_len += prompt_text_len
|
||||
text = self.llm.model.model.embed_tokens(text)
|
||||
|
||||
# 3. concat llm_input
|
||||
sos_emb = self.speech_embedding.weight[self.sos].reshape(1, 1, -1)
|
||||
task_id_emb = self.speech_embedding.weight[self.task_id].reshape(1, 1, -1)
|
||||
if prompt_speech_token_len != 0:
|
||||
prompt_speech_token_emb = self.speech_embedding(prompt_speech_token)
|
||||
else:
|
||||
prompt_speech_token_emb = torch.zeros(1, 0, self.llm_input_size, dtype=text.dtype).to(device)
|
||||
lm_input = torch.concat([sos_emb, text, task_id_emb, prompt_speech_token_emb], dim=1)
|
||||
|
||||
# 4. cal min/max_length
|
||||
min_len = int((text_len - prompt_text_len) * min_token_text_ratio)
|
||||
max_len = int((text_len - prompt_text_len) * max_token_text_ratio)
|
||||
|
||||
# 5. step by step decode
|
||||
for token in self.inference_wrapper(lm_input, sampling, min_len, max_len, uuid, progress_callback):
|
||||
yield token
|
||||
+58836
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,327 @@
|
||||
import base64
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
import torch
|
||||
from transformers import AutoTokenizer
|
||||
from whisper.tokenizer import Tokenizer
|
||||
|
||||
import tiktoken
|
||||
|
||||
LANGUAGES = {
|
||||
"en": "english",
|
||||
"zh": "chinese",
|
||||
"de": "german",
|
||||
"es": "spanish",
|
||||
"ru": "russian",
|
||||
"ko": "korean",
|
||||
"fr": "french",
|
||||
"ja": "japanese",
|
||||
"pt": "portuguese",
|
||||
"tr": "turkish",
|
||||
"pl": "polish",
|
||||
"ca": "catalan",
|
||||
"nl": "dutch",
|
||||
"ar": "arabic",
|
||||
"sv": "swedish",
|
||||
"it": "italian",
|
||||
"id": "indonesian",
|
||||
"hi": "hindi",
|
||||
"fi": "finnish",
|
||||
"vi": "vietnamese",
|
||||
"he": "hebrew",
|
||||
"uk": "ukrainian",
|
||||
"el": "greek",
|
||||
"ms": "malay",
|
||||
"cs": "czech",
|
||||
"ro": "romanian",
|
||||
"da": "danish",
|
||||
"hu": "hungarian",
|
||||
"ta": "tamil",
|
||||
"no": "norwegian",
|
||||
"th": "thai",
|
||||
"ur": "urdu",
|
||||
"hr": "croatian",
|
||||
"bg": "bulgarian",
|
||||
"lt": "lithuanian",
|
||||
"la": "latin",
|
||||
"mi": "maori",
|
||||
"ml": "malayalam",
|
||||
"cy": "welsh",
|
||||
"sk": "slovak",
|
||||
"te": "telugu",
|
||||
"fa": "persian",
|
||||
"lv": "latvian",
|
||||
"bn": "bengali",
|
||||
"sr": "serbian",
|
||||
"az": "azerbaijani",
|
||||
"sl": "slovenian",
|
||||
"kn": "kannada",
|
||||
"et": "estonian",
|
||||
"mk": "macedonian",
|
||||
"br": "breton",
|
||||
"eu": "basque",
|
||||
"is": "icelandic",
|
||||
"hy": "armenian",
|
||||
"ne": "nepali",
|
||||
"mn": "mongolian",
|
||||
"bs": "bosnian",
|
||||
"kk": "kazakh",
|
||||
"sq": "albanian",
|
||||
"sw": "swahili",
|
||||
"gl": "galician",
|
||||
"mr": "marathi",
|
||||
"pa": "punjabi",
|
||||
"si": "sinhala",
|
||||
"km": "khmer",
|
||||
"sn": "shona",
|
||||
"yo": "yoruba",
|
||||
"so": "somali",
|
||||
"af": "afrikaans",
|
||||
"oc": "occitan",
|
||||
"ka": "georgian",
|
||||
"be": "belarusian",
|
||||
"tg": "tajik",
|
||||
"sd": "sindhi",
|
||||
"gu": "gujarati",
|
||||
"am": "amharic",
|
||||
"yi": "yiddish",
|
||||
"lo": "lao",
|
||||
"uz": "uzbek",
|
||||
"fo": "faroese",
|
||||
"ht": "haitian creole",
|
||||
"ps": "pashto",
|
||||
"tk": "turkmen",
|
||||
"nn": "nynorsk",
|
||||
"mt": "maltese",
|
||||
"sa": "sanskrit",
|
||||
"lb": "luxembourgish",
|
||||
"my": "myanmar",
|
||||
"bo": "tibetan",
|
||||
"tl": "tagalog",
|
||||
"mg": "malagasy",
|
||||
"as": "assamese",
|
||||
"tt": "tatar",
|
||||
"haw": "hawaiian",
|
||||
"ln": "lingala",
|
||||
"ha": "hausa",
|
||||
"ba": "bashkir",
|
||||
"jw": "javanese",
|
||||
"su": "sundanese",
|
||||
"yue": "cantonese",
|
||||
"minnan": "minnan",
|
||||
"wuyu": "wuyu",
|
||||
"dialect": "dialect",
|
||||
"zh/en": "zh/en",
|
||||
"en/zh": "en/zh",
|
||||
}
|
||||
|
||||
# language code lookup by name, with a few language aliases
|
||||
TO_LANGUAGE_CODE = {
|
||||
**{language: code for code, language in LANGUAGES.items()},
|
||||
"burmese": "my",
|
||||
"valencian": "ca",
|
||||
"flemish": "nl",
|
||||
"haitian": "ht",
|
||||
"letzeburgesch": "lb",
|
||||
"pushto": "ps",
|
||||
"panjabi": "pa",
|
||||
"moldavian": "ro",
|
||||
"moldovan": "ro",
|
||||
"sinhalese": "si",
|
||||
"castilian": "es",
|
||||
"mandarin": "zh",
|
||||
}
|
||||
|
||||
AUDIO_EVENT = {
|
||||
"ASR": "ASR",
|
||||
"AED": "AED",
|
||||
"SER": "SER",
|
||||
"Speech": "Speech",
|
||||
"/Speech": "/Speech",
|
||||
"BGM": "BGM",
|
||||
"/BGM": "/BGM",
|
||||
"Laughter": "Laughter",
|
||||
"/Laughter": "/Laughter",
|
||||
"Applause": "Applause",
|
||||
"/Applause": "/Applause",
|
||||
}
|
||||
|
||||
EMOTION = {
|
||||
"HAPPY": "HAPPY",
|
||||
"SAD": "SAD",
|
||||
"ANGRY": "ANGRY",
|
||||
"NEUTRAL": "NEUTRAL",
|
||||
}
|
||||
|
||||
TTS_Vocal_Token = {
|
||||
"TTS/B": "TTS/B",
|
||||
"TTS/O": "TTS/O",
|
||||
"TTS/Q": "TTS/Q",
|
||||
"TTS/A": "TTS/A",
|
||||
"TTS/CO": "TTS/CO",
|
||||
"TTS/CL": "TTS/CL",
|
||||
"TTS/H": "TTS/H",
|
||||
**{f"TTS/SP{i:02d}": f"TTS/SP{i:02d}" for i in range(1, 14)}
|
||||
}
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_encoding(name: str = "gpt2", num_languages: int = 99):
|
||||
vocab_path = os.path.join(os.path.dirname(__file__), "assets", f"{name}.tiktoken")
|
||||
ranks = {
|
||||
base64.b64decode(token): int(rank)
|
||||
for token, rank in (line.split() for line in open(vocab_path) if line)
|
||||
}
|
||||
n_vocab = len(ranks)
|
||||
special_tokens = {}
|
||||
|
||||
specials = [
|
||||
"<|endoftext|>",
|
||||
"<|startoftranscript|>",
|
||||
*[f"<|{lang}|>" for lang in list(LANGUAGES.keys())[:num_languages]],
|
||||
*[f"<|{audio_event}|>" for audio_event in list(AUDIO_EVENT.keys())],
|
||||
*[f"<|{emotion}|>" for emotion in list(EMOTION.keys())],
|
||||
"<|translate|>",
|
||||
"<|transcribe|>",
|
||||
"<|startoflm|>",
|
||||
"<|startofprev|>",
|
||||
"<|nospeech|>",
|
||||
"<|notimestamps|>",
|
||||
*[f"<|SPECIAL_TOKEN_{i}|>" for i in range(1, 31)], # register special tokens for ASR
|
||||
*[f"<|{tts}|>" for tts in list(TTS_Vocal_Token.keys())], # register special tokens for TTS
|
||||
*[f"<|{i * 0.02:.2f}|>" for i in range(1501)],
|
||||
]
|
||||
|
||||
for token in specials:
|
||||
special_tokens[token] = n_vocab
|
||||
n_vocab += 1
|
||||
|
||||
return tiktoken.Encoding(
|
||||
name=os.path.basename(vocab_path),
|
||||
explicit_n_vocab=n_vocab,
|
||||
pat_str=r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+""",
|
||||
mergeable_ranks=ranks,
|
||||
special_tokens=special_tokens,
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_tokenizer(
|
||||
multilingual: bool,
|
||||
*,
|
||||
num_languages: int = 99,
|
||||
language: Optional[str] = None,
|
||||
task: Optional[str] = None, # Literal["transcribe", "translate", None]
|
||||
) -> Tokenizer:
|
||||
if language is not None:
|
||||
language = language.lower()
|
||||
if language not in LANGUAGES:
|
||||
if language in TO_LANGUAGE_CODE:
|
||||
language = TO_LANGUAGE_CODE[language]
|
||||
else:
|
||||
raise ValueError(f"Unsupported language: {language}")
|
||||
|
||||
if multilingual:
|
||||
encoding_name = "multilingual_zh_ja_yue_char_del"
|
||||
language = language or "en"
|
||||
task = task or "transcribe"
|
||||
else:
|
||||
encoding_name = "gpt2"
|
||||
language = None
|
||||
task = None
|
||||
|
||||
encoding = get_encoding(name=encoding_name, num_languages=num_languages)
|
||||
|
||||
return Tokenizer(
|
||||
encoding=encoding, num_languages=num_languages, language=language, task=task
|
||||
)
|
||||
|
||||
|
||||
class CosyVoice2Tokenizer():
|
||||
def __init__(self, token_path, skip_special_tokens=True):
|
||||
super().__init__()
|
||||
# NOTE: non-chat model, all these special tokens keep randomly initialized.
|
||||
special_tokens = {
|
||||
'eos_token': '<|endoftext|>',
|
||||
'pad_token': '<|endoftext|>',
|
||||
'additional_special_tokens': [
|
||||
'<|im_start|>', '<|im_end|>', '<|endofprompt|>',
|
||||
'[breath]', '<strong>', '</strong>', '[noise]',
|
||||
'[laughter]', '[cough]', '[clucking]', '[accent]',
|
||||
'[quick_breath]',
|
||||
"<laughter>", "</laughter>",
|
||||
"[hissing]", "[sigh]", "[vocalized-noise]",
|
||||
"[lipsmack]", "[mn]"
|
||||
]
|
||||
}
|
||||
self.special_tokens = special_tokens
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(token_path)
|
||||
self.tokenizer.add_special_tokens(special_tokens)
|
||||
self.skip_special_tokens = skip_special_tokens
|
||||
|
||||
def encode(self, text, **kwargs):
|
||||
tokens = self.tokenizer([text], return_tensors="pt")
|
||||
tokens = tokens["input_ids"][0].cpu().tolist()
|
||||
return tokens
|
||||
|
||||
def decode(self, tokens):
|
||||
tokens = torch.tensor(tokens, dtype=torch.int64)
|
||||
text = self.tokenizer.batch_decode([tokens], skip_special_tokens=self.skip_special_tokens)[0]
|
||||
return text
|
||||
|
||||
|
||||
class CosyVoice3Tokenizer(CosyVoice2Tokenizer):
|
||||
def __init__(self, token_path, skip_special_tokens=True):
|
||||
# NOTE: non-chat model, all these special tokens keep randomly initialized.
|
||||
special_tokens = {
|
||||
'eos_token': '<|endoftext|>',
|
||||
'pad_token': '<|endoftext|>',
|
||||
'additional_special_tokens': [
|
||||
'<|im_start|>', '<|im_end|>', '<|endofprompt|>',
|
||||
'[breath]', '<strong>', '</strong>', '[noise]',
|
||||
'[laughter]', '[cough]', '[clucking]', '[accent]',
|
||||
'[quick_breath]',
|
||||
"<laughter>", "</laughter>",
|
||||
"[hissing]", "[sigh]", "[vocalized-noise]",
|
||||
"[lipsmack]", "[mn]", "<|endofsystem|>",
|
||||
"[AA]", "[AA0]", "[AA1]", "[AA2]", "[AE]", "[AE0]", "[AE1]", "[AE2]", "[AH]", "[AH0]", "[AH1]", "[AH2]",
|
||||
"[AO]", "[AO0]", "[AO1]", "[AO2]", "[AW]", "[AW0]", "[AW1]", "[AW2]", "[AY]", "[AY0]", "[AY1]", "[AY2]",
|
||||
"[B]", "[CH]", "[D]", "[DH]", "[EH]", "[EH0]", "[EH1]", "[EH2]", "[ER]", "[ER0]", "[ER1]", "[ER2]", "[EY]",
|
||||
"[EY0]", "[EY1]", "[EY2]", "[F]", "[G]", "[HH]", "[IH]", "[IH0]", "[IH1]", "[IH2]", "[IY]", "[IY0]", "[IY1]",
|
||||
"[IY2]", "[JH]", "[K]", "[L]", "[M]", "[N]", "[NG]", "[OW]", "[OW0]", "[OW1]", "[OW2]", "[OY]", "[OY0]",
|
||||
"[OY1]", "[OY2]", "[P]", "[R]", "[S]", "[SH]", "[T]", "[TH]", "[UH]", "[UH0]", "[UH1]", "[UH2]", "[UW]",
|
||||
"[UW0]", "[UW1]", "[UW2]", "[V]", "[W]", "[Y]", "[Z]", "[ZH]",
|
||||
"[a]", "[ai]", "[an]", "[ang]", "[ao]", "[b]", "[c]", "[ch]", "[d]", "[e]", "[ei]", "[en]", "[eng]", "[f]",
|
||||
"[g]", "[h]", "[i]", "[ian]", "[in]", "[ing]", "[iu]", "[ià]", "[iàn]", "[iàng]", "[iào]", "[iá]", "[ián]",
|
||||
"[iáng]", "[iáo]", "[iè]", "[ié]", "[iòng]", "[ióng]", "[iù]", "[iú]", "[iā]", "[iān]", "[iāng]", "[iāo]",
|
||||
"[iē]", "[iě]", "[iōng]", "[iū]", "[iǎ]", "[iǎn]", "[iǎng]", "[iǎo]", "[iǒng]", "[iǔ]", "[j]", "[k]", "[l]",
|
||||
"[m]", "[n]", "[o]", "[ong]", "[ou]", "[p]", "[q]", "[r]", "[s]", "[sh]", "[t]", "[u]", "[uang]", "[ue]",
|
||||
"[un]", "[uo]", "[uà]", "[uài]", "[uàn]", "[uàng]", "[uá]", "[uái]", "[uán]", "[uáng]", "[uè]", "[ué]", "[uì]",
|
||||
"[uí]", "[uò]", "[uó]", "[uā]", "[uāi]", "[uān]", "[uāng]", "[uē]", "[uě]", "[uī]", "[uō]", "[uǎ]", "[uǎi]",
|
||||
"[uǎn]", "[uǎng]", "[uǐ]", "[uǒ]", "[vè]", "[w]", "[x]", "[y]", "[z]", "[zh]", "[à]", "[ài]", "[àn]", "[àng]",
|
||||
"[ào]", "[á]", "[ái]", "[án]", "[áng]", "[áo]", "[è]", "[èi]", "[èn]", "[èng]", "[èr]", "[é]", "[éi]", "[én]",
|
||||
"[éng]", "[ér]", "[ì]", "[ìn]", "[ìng]", "[í]", "[ín]", "[íng]", "[ò]", "[òng]", "[òu]", "[ó]", "[óng]", "[óu]",
|
||||
"[ù]", "[ùn]", "[ú]", "[ún]", "[ā]", "[āi]", "[ān]", "[āng]", "[āo]", "[ē]", "[ēi]", "[ēn]", "[ēng]", "[ě]",
|
||||
"[ěi]", "[ěn]", "[ěng]", "[ěr]", "[ī]", "[īn]", "[īng]", "[ō]", "[ōng]", "[ōu]", "[ū]", "[ūn]", "[ǎ]", "[ǎi]",
|
||||
"[ǎn]", "[ǎng]", "[ǎo]", "[ǐ]", "[ǐn]", "[ǐng]", "[ǒ]", "[ǒng]", "[ǒu]", "[ǔ]", "[ǔn]", "[ǘ]", "[ǚ]", "[ǜ]"
|
||||
]
|
||||
}
|
||||
self.special_tokens = special_tokens
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(token_path)
|
||||
self.tokenizer.add_special_tokens(special_tokens)
|
||||
self.skip_special_tokens = skip_special_tokens
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_qwen_tokenizer(
|
||||
token_path: str,
|
||||
skip_special_tokens: bool,
|
||||
version: str = 'cosyvoice2'
|
||||
):
|
||||
if version == 'cosyvoice2':
|
||||
return CosyVoice2Tokenizer(token_path=token_path, skip_special_tokens=skip_special_tokens)
|
||||
elif version == 'cosyvoice3':
|
||||
return CosyVoice3Tokenizer(token_path=token_path, skip_special_tokens=skip_special_tokens)
|
||||
else:
|
||||
raise ValueError
|
||||
@@ -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).transpose(1, 2)
|
||||
# (batch, head, time1, d_k)
|
||||
q_with_bias_v = (q + self.pos_bias_v).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,258 @@
|
||||
# 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
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# NOTE(Xiang Lyu) causal conv module used in convolution-based vocoder
|
||||
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',
|
||||
causal_type: str = 'left',
|
||||
device=None,
|
||||
dtype=None
|
||||
) -> None:
|
||||
super(CausalConv1d, self).__init__(in_channels, out_channels,
|
||||
kernel_size, stride=1,
|
||||
padding=0, dilation=dilation,
|
||||
groups=groups, bias=bias,
|
||||
padding_mode=padding_mode,
|
||||
device=device, dtype=dtype)
|
||||
assert stride == 1
|
||||
self.causal_padding = int((kernel_size * dilation - dilation) / 2) * 2 + (kernel_size + 1) % 2
|
||||
assert causal_type in ['left', 'right']
|
||||
self.causal_type = causal_type
|
||||
|
||||
def forward(self, x: torch.Tensor, cache: torch.Tensor = torch.zeros(0, 0, 0)) -> Tuple[torch.Tensor]:
|
||||
input_timestep = x.shape[2]
|
||||
if cache.size(2) == 0:
|
||||
cache = torch.zeros(x.shape[0], x.shape[1], self.causal_padding).to(x)
|
||||
assert cache.size(2) == self.causal_padding
|
||||
if self.causal_type == 'left':
|
||||
x = torch.concat([cache, x], dim=2)
|
||||
else:
|
||||
x = torch.concat([x, cache], dim=2)
|
||||
x = super(CausalConv1d, self).forward(x)
|
||||
assert x.shape[2] == input_timestep
|
||||
return x
|
||||
|
||||
|
||||
class CausalConv1dDownSample(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(CausalConv1dDownSample, 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 and dilation == 1
|
||||
assert kernel_size % stride == 0
|
||||
self.causal_padding = stride - 1
|
||||
|
||||
def forward(self, x: torch.Tensor, cache: torch.Tensor = torch.zeros(0, 0, 0)) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
if cache.size(2) == 0:
|
||||
x = F.pad(x, (self.causal_padding, 0), value=0.0)
|
||||
else:
|
||||
assert cache.size(2) == self.causal_padding
|
||||
x = torch.concat([cache, x], dim=2)
|
||||
x = super(CausalConv1dDownSample, self).forward(x)
|
||||
return x
|
||||
|
||||
|
||||
class CausalConv1dUpsample(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(CausalConv1dUpsample, self).__init__(in_channels, out_channels,
|
||||
kernel_size, 1,
|
||||
padding=0, dilation=dilation,
|
||||
groups=groups, bias=bias,
|
||||
padding_mode=padding_mode,
|
||||
device=device, dtype=dtype)
|
||||
assert dilation == 1
|
||||
self.causal_padding = kernel_size - 1
|
||||
self.upsample = torch.nn.Upsample(scale_factor=stride, mode='nearest')
|
||||
|
||||
def forward(self, x: torch.Tensor, cache: torch.Tensor = torch.zeros(0, 0, 0)) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
x = self.upsample(x)
|
||||
input_timestep = x.shape[2]
|
||||
if cache.size(2) == 0:
|
||||
x = F.pad(x, (self.causal_padding, 0), value=0.0)
|
||||
else:
|
||||
assert cache.size(2) == self.causal_padding
|
||||
x = torch.concat([cache, x], dim=2)
|
||||
x = super(CausalConv1dUpsample, self).forward(x)
|
||||
assert input_timestep == x.shape[2]
|
||||
return x
|
||||
@@ -0,0 +1,396 @@
|
||||
# Copyright (c) 2021 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)
|
||||
"""Decoder definition."""
|
||||
from typing import Tuple, List, Optional
|
||||
|
||||
import torch
|
||||
import torch.utils.checkpoint as ckpt
|
||||
import logging
|
||||
|
||||
from cosyvoice.transformer.decoder_layer import DecoderLayer
|
||||
from cosyvoice.transformer.positionwise_feed_forward import PositionwiseFeedForward
|
||||
from cosyvoice.utils.class_utils import (
|
||||
COSYVOICE_EMB_CLASSES,
|
||||
COSYVOICE_ATTENTION_CLASSES,
|
||||
COSYVOICE_ACTIVATION_CLASSES,
|
||||
)
|
||||
from cosyvoice.utils.mask import (subsequent_mask, make_pad_mask)
|
||||
|
||||
|
||||
class TransformerDecoder(torch.nn.Module):
|
||||
"""Base class of Transfomer decoder module.
|
||||
Args:
|
||||
vocab_size: output dim
|
||||
encoder_output_size: dimension of attention
|
||||
attention_heads: the number of heads of multi head attention
|
||||
linear_units: the hidden units number of position-wise feedforward
|
||||
num_blocks: the number of decoder blocks
|
||||
dropout_rate: dropout rate
|
||||
self_attention_dropout_rate: dropout rate for attention
|
||||
input_layer: input layer type
|
||||
use_output_layer: whether to use output layer
|
||||
pos_enc_class: PositionalEncoding or ScaledPositionalEncoding
|
||||
normalize_before:
|
||||
True: use layer_norm before each sub-block of a layer.
|
||||
False: use layer_norm after each sub-block of a layer.
|
||||
src_attention: if false, encoder-decoder cross attention is not
|
||||
applied, such as CIF model
|
||||
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.
|
||||
tie_word_embedding: Tie or clone module weights depending of whether we are
|
||||
using TorchScript or not
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vocab_size: int,
|
||||
encoder_output_size: int,
|
||||
attention_heads: int = 4,
|
||||
linear_units: int = 2048,
|
||||
num_blocks: int = 6,
|
||||
dropout_rate: float = 0.1,
|
||||
positional_dropout_rate: float = 0.1,
|
||||
self_attention_dropout_rate: float = 0.0,
|
||||
src_attention_dropout_rate: float = 0.0,
|
||||
input_layer: str = "embed",
|
||||
use_output_layer: bool = True,
|
||||
normalize_before: bool = True,
|
||||
src_attention: bool = True,
|
||||
key_bias: bool = True,
|
||||
activation_type: str = "relu",
|
||||
gradient_checkpointing: bool = False,
|
||||
tie_word_embedding: bool = False,
|
||||
):
|
||||
super().__init__()
|
||||
attention_dim = encoder_output_size
|
||||
activation = COSYVOICE_ACTIVATION_CLASSES[activation_type]()
|
||||
|
||||
self.embed = torch.nn.Sequential(
|
||||
torch.nn.Identity() if input_layer == "no_pos" else
|
||||
torch.nn.Embedding(vocab_size, attention_dim),
|
||||
COSYVOICE_EMB_CLASSES[input_layer](attention_dim,
|
||||
positional_dropout_rate),
|
||||
)
|
||||
|
||||
self.normalize_before = normalize_before
|
||||
self.after_norm = torch.nn.LayerNorm(attention_dim, eps=1e-5)
|
||||
self.use_output_layer = use_output_layer
|
||||
if use_output_layer:
|
||||
self.output_layer = torch.nn.Linear(attention_dim, vocab_size)
|
||||
else:
|
||||
self.output_layer = torch.nn.Identity()
|
||||
self.num_blocks = num_blocks
|
||||
self.decoders = torch.nn.ModuleList([
|
||||
DecoderLayer(
|
||||
attention_dim,
|
||||
COSYVOICE_ATTENTION_CLASSES["selfattn"](
|
||||
attention_heads, attention_dim,
|
||||
self_attention_dropout_rate, key_bias),
|
||||
COSYVOICE_ATTENTION_CLASSES["selfattn"](
|
||||
attention_heads, attention_dim, src_attention_dropout_rate,
|
||||
key_bias) if src_attention else None,
|
||||
PositionwiseFeedForward(attention_dim, linear_units,
|
||||
dropout_rate, activation),
|
||||
dropout_rate,
|
||||
normalize_before,
|
||||
) for _ in range(self.num_blocks)
|
||||
])
|
||||
|
||||
self.gradient_checkpointing = gradient_checkpointing
|
||||
self.tie_word_embedding = tie_word_embedding
|
||||
|
||||
def forward(
|
||||
self,
|
||||
memory: torch.Tensor,
|
||||
memory_mask: torch.Tensor,
|
||||
ys_in_pad: torch.Tensor,
|
||||
ys_in_lens: torch.Tensor,
|
||||
r_ys_in_pad: torch.Tensor = torch.empty(0),
|
||||
reverse_weight: float = 0.0,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Forward decoder.
|
||||
Args:
|
||||
memory: encoded memory, float32 (batch, maxlen_in, feat)
|
||||
memory_mask: encoder memory mask, (batch, 1, maxlen_in)
|
||||
ys_in_pad: padded input token ids, int64 (batch, maxlen_out)
|
||||
ys_in_lens: input lengths of this batch (batch)
|
||||
r_ys_in_pad: not used in transformer decoder, in order to unify api
|
||||
with bidirectional decoder
|
||||
reverse_weight: not used in transformer decoder, in order to unify
|
||||
api with bidirectional decode
|
||||
Returns:
|
||||
(tuple): tuple containing:
|
||||
x: decoded token score before softmax (batch, maxlen_out,
|
||||
vocab_size) if use_output_layer is True,
|
||||
torch.tensor(0.0), in order to unify api with bidirectional decoder
|
||||
olens: (batch, )
|
||||
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
|
||||
"""
|
||||
tgt = ys_in_pad
|
||||
maxlen = tgt.size(1)
|
||||
# tgt_mask: (B, 1, L)
|
||||
tgt_mask = ~make_pad_mask(ys_in_lens, maxlen).unsqueeze(1)
|
||||
tgt_mask = tgt_mask.to(tgt.device)
|
||||
# m: (1, L, L)
|
||||
m = subsequent_mask(tgt_mask.size(-1),
|
||||
device=tgt_mask.device).unsqueeze(0)
|
||||
# tgt_mask: (B, L, L)
|
||||
tgt_mask = tgt_mask & m
|
||||
x, _ = self.embed(tgt)
|
||||
if self.gradient_checkpointing and self.training:
|
||||
x = self.forward_layers_checkpointed(x, tgt_mask, memory,
|
||||
memory_mask)
|
||||
else:
|
||||
x = self.forward_layers(x, tgt_mask, memory, memory_mask)
|
||||
if self.normalize_before:
|
||||
x = self.after_norm(x)
|
||||
if self.use_output_layer:
|
||||
x = self.output_layer(x)
|
||||
olens = tgt_mask.sum(1)
|
||||
return x, torch.tensor(0.0), olens
|
||||
|
||||
def forward_layers(self, x: torch.Tensor, tgt_mask: torch.Tensor,
|
||||
memory: torch.Tensor,
|
||||
memory_mask: torch.Tensor) -> torch.Tensor:
|
||||
for layer in self.decoders:
|
||||
x, tgt_mask, memory, memory_mask = layer(x, tgt_mask, memory,
|
||||
memory_mask)
|
||||
return x
|
||||
|
||||
@torch.jit.unused
|
||||
def forward_layers_checkpointed(self, x: torch.Tensor,
|
||||
tgt_mask: torch.Tensor,
|
||||
memory: torch.Tensor,
|
||||
memory_mask: torch.Tensor) -> torch.Tensor:
|
||||
for layer in self.decoders:
|
||||
x, tgt_mask, memory, memory_mask = ckpt.checkpoint(
|
||||
layer.__call__, x, tgt_mask, memory, memory_mask)
|
||||
return x
|
||||
|
||||
def forward_one_step(
|
||||
self,
|
||||
memory: torch.Tensor,
|
||||
memory_mask: torch.Tensor,
|
||||
tgt: torch.Tensor,
|
||||
tgt_mask: torch.Tensor,
|
||||
cache: Optional[List[torch.Tensor]] = None,
|
||||
) -> Tuple[torch.Tensor, List[torch.Tensor]]:
|
||||
"""Forward one step.
|
||||
This is only used for decoding.
|
||||
Args:
|
||||
memory: encoded memory, float32 (batch, maxlen_in, feat)
|
||||
memory_mask: encoded memory mask, (batch, 1, maxlen_in)
|
||||
tgt: input token ids, int64 (batch, maxlen_out)
|
||||
tgt_mask: input token mask, (batch, maxlen_out)
|
||||
dtype=torch.uint8 in PyTorch 1.2-
|
||||
dtype=torch.bool in PyTorch 1.2+ (include 1.2)
|
||||
cache: cached output list of (batch, max_time_out-1, size)
|
||||
Returns:
|
||||
y, cache: NN output value and cache per `self.decoders`.
|
||||
y.shape` is (batch, maxlen_out, token)
|
||||
"""
|
||||
x, _ = self.embed(tgt)
|
||||
new_cache = []
|
||||
for i, decoder in enumerate(self.decoders):
|
||||
if cache is None:
|
||||
c = None
|
||||
else:
|
||||
c = cache[i]
|
||||
x, tgt_mask, memory, memory_mask = decoder(x,
|
||||
tgt_mask,
|
||||
memory,
|
||||
memory_mask,
|
||||
cache=c)
|
||||
new_cache.append(x)
|
||||
if self.normalize_before:
|
||||
y = self.after_norm(x[:, -1])
|
||||
else:
|
||||
y = x[:, -1]
|
||||
if self.use_output_layer:
|
||||
y = torch.log_softmax(self.output_layer(y), dim=-1)
|
||||
return y, new_cache
|
||||
|
||||
def tie_or_clone_weights(self, jit_mode: bool = True):
|
||||
"""Tie or clone module weights (between word_emb and output_layer)
|
||||
depending of whether we are using TorchScript or not"""
|
||||
if not self.use_output_layer:
|
||||
return
|
||||
if jit_mode:
|
||||
logging.info("clone emb.weight to output.weight")
|
||||
self.output_layer.weight = torch.nn.Parameter(
|
||||
self.embed[0].weight.clone())
|
||||
else:
|
||||
logging.info("tie emb.weight with output.weight")
|
||||
self.output_layer.weight = self.embed[0].weight
|
||||
|
||||
if getattr(self.output_layer, "bias", None) is not None:
|
||||
self.output_layer.bias.data = torch.nn.functional.pad(
|
||||
self.output_layer.bias.data,
|
||||
(
|
||||
0,
|
||||
self.output_layer.weight.shape[0] -
|
||||
self.output_layer.bias.shape[0],
|
||||
),
|
||||
"constant",
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
class BiTransformerDecoder(torch.nn.Module):
|
||||
"""Base class of Transfomer decoder module.
|
||||
Args:
|
||||
vocab_size: output dim
|
||||
encoder_output_size: dimension of attention
|
||||
attention_heads: the number of heads of multi head attention
|
||||
linear_units: the hidden units number of position-wise feedforward
|
||||
num_blocks: the number of decoder blocks
|
||||
r_num_blocks: the number of right to left decoder blocks
|
||||
dropout_rate: dropout rate
|
||||
self_attention_dropout_rate: dropout rate for attention
|
||||
input_layer: input layer type
|
||||
use_output_layer: whether to use output layer
|
||||
pos_enc_class: PositionalEncoding or ScaledPositionalEncoding
|
||||
normalize_before:
|
||||
True: use layer_norm before each sub-block of a layer.
|
||||
False: use layer_norm after each sub-block of a layer.
|
||||
key_bias: whether use bias in attention.linear_k, False for whisper models.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vocab_size: int,
|
||||
encoder_output_size: int,
|
||||
attention_heads: int = 4,
|
||||
linear_units: int = 2048,
|
||||
num_blocks: int = 6,
|
||||
r_num_blocks: int = 0,
|
||||
dropout_rate: float = 0.1,
|
||||
positional_dropout_rate: float = 0.1,
|
||||
self_attention_dropout_rate: float = 0.0,
|
||||
src_attention_dropout_rate: float = 0.0,
|
||||
input_layer: str = "embed",
|
||||
use_output_layer: bool = True,
|
||||
normalize_before: bool = True,
|
||||
key_bias: bool = True,
|
||||
gradient_checkpointing: bool = False,
|
||||
tie_word_embedding: bool = False,
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
self.tie_word_embedding = tie_word_embedding
|
||||
self.left_decoder = TransformerDecoder(
|
||||
vocab_size,
|
||||
encoder_output_size,
|
||||
attention_heads,
|
||||
linear_units,
|
||||
num_blocks,
|
||||
dropout_rate,
|
||||
positional_dropout_rate,
|
||||
self_attention_dropout_rate,
|
||||
src_attention_dropout_rate,
|
||||
input_layer,
|
||||
use_output_layer,
|
||||
normalize_before,
|
||||
key_bias=key_bias,
|
||||
gradient_checkpointing=gradient_checkpointing,
|
||||
tie_word_embedding=tie_word_embedding)
|
||||
|
||||
self.right_decoder = TransformerDecoder(
|
||||
vocab_size,
|
||||
encoder_output_size,
|
||||
attention_heads,
|
||||
linear_units,
|
||||
r_num_blocks,
|
||||
dropout_rate,
|
||||
positional_dropout_rate,
|
||||
self_attention_dropout_rate,
|
||||
src_attention_dropout_rate,
|
||||
input_layer,
|
||||
use_output_layer,
|
||||
normalize_before,
|
||||
key_bias=key_bias,
|
||||
gradient_checkpointing=gradient_checkpointing,
|
||||
tie_word_embedding=tie_word_embedding)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
memory: torch.Tensor,
|
||||
memory_mask: torch.Tensor,
|
||||
ys_in_pad: torch.Tensor,
|
||||
ys_in_lens: torch.Tensor,
|
||||
r_ys_in_pad: torch.Tensor,
|
||||
reverse_weight: float = 0.0,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Forward decoder.
|
||||
Args:
|
||||
memory: encoded memory, float32 (batch, maxlen_in, feat)
|
||||
memory_mask: encoder memory mask, (batch, 1, maxlen_in)
|
||||
ys_in_pad: padded input token ids, int64 (batch, maxlen_out)
|
||||
ys_in_lens: input lengths of this batch (batch)
|
||||
r_ys_in_pad: padded input token ids, int64 (batch, maxlen_out),
|
||||
used for right to left decoder
|
||||
reverse_weight: used for right to left decoder
|
||||
Returns:
|
||||
(tuple): tuple containing:
|
||||
x: decoded token score before softmax (batch, maxlen_out,
|
||||
vocab_size) if use_output_layer is True,
|
||||
r_x: x: decoded token score (right to left decoder)
|
||||
before softmax (batch, maxlen_out, vocab_size)
|
||||
if use_output_layer is True,
|
||||
olens: (batch, )
|
||||
"""
|
||||
l_x, _, olens = self.left_decoder(memory, memory_mask, ys_in_pad,
|
||||
ys_in_lens)
|
||||
r_x = torch.tensor(0.0)
|
||||
if reverse_weight > 0.0:
|
||||
r_x, _, olens = self.right_decoder(memory, memory_mask,
|
||||
r_ys_in_pad, ys_in_lens)
|
||||
return l_x, r_x, olens
|
||||
|
||||
def forward_one_step(
|
||||
self,
|
||||
memory: torch.Tensor,
|
||||
memory_mask: torch.Tensor,
|
||||
tgt: torch.Tensor,
|
||||
tgt_mask: torch.Tensor,
|
||||
cache: Optional[List[torch.Tensor]] = None,
|
||||
) -> Tuple[torch.Tensor, List[torch.Tensor]]:
|
||||
"""Forward one step.
|
||||
This is only used for decoding.
|
||||
Args:
|
||||
memory: encoded memory, float32 (batch, maxlen_in, feat)
|
||||
memory_mask: encoded memory mask, (batch, 1, maxlen_in)
|
||||
tgt: input token ids, int64 (batch, maxlen_out)
|
||||
tgt_mask: input token mask, (batch, maxlen_out)
|
||||
dtype=torch.uint8 in PyTorch 1.2-
|
||||
dtype=torch.bool in PyTorch 1.2+ (include 1.2)
|
||||
cache: cached output list of (batch, max_time_out-1, size)
|
||||
Returns:
|
||||
y, cache: NN output value and cache per `self.decoders`.
|
||||
y.shape` is (batch, maxlen_out, token)
|
||||
"""
|
||||
return self.left_decoder.forward_one_step(memory, memory_mask, tgt,
|
||||
tgt_mask, cache)
|
||||
|
||||
def tie_or_clone_weights(self, jit_mode: bool = True):
|
||||
"""Tie or clone module weights (between word_emb and output_layer)
|
||||
depending of whether we are using TorchScript or not"""
|
||||
self.left_decoder.tie_or_clone_weights(jit_mode)
|
||||
self.right_decoder.tie_or_clone_weights(jit_mode)
|
||||
@@ -0,0 +1,132 @@
|
||||
# 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.
|
||||
"""Decoder self-attention layer definition."""
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
class DecoderLayer(nn.Module):
|
||||
"""Single decoder layer module.
|
||||
|
||||
Args:
|
||||
size (int): Input dimension.
|
||||
self_attn (torch.nn.Module): Self-attention module instance.
|
||||
`MultiHeadedAttention` instance can be used as the argument.
|
||||
src_attn (torch.nn.Module): Inter-attention module instance.
|
||||
`MultiHeadedAttention` instance can be used as the argument.
|
||||
If `None` is passed, Inter-attention is not used, such as
|
||||
CIF, GPT, and other decoder only model.
|
||||
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: nn.Module,
|
||||
src_attn: Optional[nn.Module],
|
||||
feed_forward: nn.Module,
|
||||
dropout_rate: float,
|
||||
normalize_before: bool = True,
|
||||
):
|
||||
"""Construct an DecoderLayer object."""
|
||||
super().__init__()
|
||||
self.size = size
|
||||
self.self_attn = self_attn
|
||||
self.src_attn = src_attn
|
||||
self.feed_forward = feed_forward
|
||||
self.norm1 = nn.LayerNorm(size, eps=1e-5)
|
||||
self.norm2 = nn.LayerNorm(size, eps=1e-5)
|
||||
self.norm3 = nn.LayerNorm(size, eps=1e-5)
|
||||
self.dropout = nn.Dropout(dropout_rate)
|
||||
self.normalize_before = normalize_before
|
||||
|
||||
def forward(
|
||||
self,
|
||||
tgt: torch.Tensor,
|
||||
tgt_mask: torch.Tensor,
|
||||
memory: torch.Tensor,
|
||||
memory_mask: torch.Tensor,
|
||||
cache: Optional[torch.Tensor] = None
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Compute decoded features.
|
||||
|
||||
Args:
|
||||
tgt (torch.Tensor): Input tensor (#batch, maxlen_out, size).
|
||||
tgt_mask (torch.Tensor): Mask for input tensor
|
||||
(#batch, maxlen_out).
|
||||
memory (torch.Tensor): Encoded memory
|
||||
(#batch, maxlen_in, size).
|
||||
memory_mask (torch.Tensor): Encoded memory mask
|
||||
(#batch, maxlen_in).
|
||||
cache (torch.Tensor): cached tensors.
|
||||
(#batch, maxlen_out - 1, size).
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Output tensor (#batch, maxlen_out, size).
|
||||
torch.Tensor: Mask for output tensor (#batch, maxlen_out).
|
||||
torch.Tensor: Encoded memory (#batch, maxlen_in, size).
|
||||
torch.Tensor: Encoded memory mask (#batch, maxlen_in).
|
||||
|
||||
"""
|
||||
residual = tgt
|
||||
if self.normalize_before:
|
||||
tgt = self.norm1(tgt)
|
||||
|
||||
if cache is None:
|
||||
tgt_q = tgt
|
||||
tgt_q_mask = tgt_mask
|
||||
else:
|
||||
# compute only the last frame query keeping dim: max_time_out -> 1
|
||||
assert cache.shape == (
|
||||
tgt.shape[0],
|
||||
tgt.shape[1] - 1,
|
||||
self.size,
|
||||
), "{cache.shape} == {(tgt.shape[0], tgt.shape[1] - 1, self.size)}"
|
||||
tgt_q = tgt[:, -1:, :]
|
||||
residual = residual[:, -1:, :]
|
||||
tgt_q_mask = tgt_mask[:, -1:, :]
|
||||
|
||||
x = residual + self.dropout(
|
||||
self.self_attn(tgt_q, tgt, tgt, tgt_q_mask)[0])
|
||||
if not self.normalize_before:
|
||||
x = self.norm1(x)
|
||||
|
||||
if self.src_attn is not None:
|
||||
residual = x
|
||||
if self.normalize_before:
|
||||
x = self.norm2(x)
|
||||
x = residual + self.dropout(
|
||||
self.src_attn(x, memory, memory, memory_mask)[0])
|
||||
if not self.normalize_before:
|
||||
x = self.norm2(x)
|
||||
|
||||
residual = x
|
||||
if self.normalize_before:
|
||||
x = self.norm3(x)
|
||||
x = residual + self.dropout(self.feed_forward(x))
|
||||
if not self.normalize_before:
|
||||
x = self.norm3(x)
|
||||
|
||||
if cache is not None:
|
||||
x = torch.cat([cache, x], dim=1)
|
||||
|
||||
return x, tgt_mask, memory, memory_mask
|
||||
@@ -0,0 +1,302 @@
|
||||
# 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
|
||||
"""
|
||||
# How to subscript a Union type:
|
||||
# https://github.com/pytorch/pytorch/issues/69434
|
||||
if isinstance(offset, int):
|
||||
pos_emb = self.pe[
|
||||
:,
|
||||
self.pe.size(1) // 2 - size - offset + 1: self.pe.size(1) // 2 + size + offset,
|
||||
]
|
||||
elif isinstance(offset, torch.Tensor):
|
||||
pos_emb = self.pe[
|
||||
:,
|
||||
self.pe.size(1) // 2 - size - offset + 1: self.pe.size(1) // 2 + size + offset,
|
||||
]
|
||||
return pos_emb
|
||||
@@ -0,0 +1,474 @@
|
||||
# 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
|
||||
import torch.utils.checkpoint as ckpt
|
||||
|
||||
from cosyvoice.transformer.convolution import ConvolutionModule
|
||||
from cosyvoice.transformer.encoder_layer import TransformerEncoderLayer
|
||||
from cosyvoice.transformer.encoder_layer import ConformerEncoderLayer
|
||||
from cosyvoice.transformer.positionwise_feed_forward import PositionwiseFeedForward
|
||||
from cosyvoice.utils.class_utils import (
|
||||
COSYVOICE_EMB_CLASSES,
|
||||
COSYVOICE_SUBSAMPLE_CLASSES,
|
||||
COSYVOICE_ATTENTION_CLASSES,
|
||||
COSYVOICE_ACTIVATION_CLASSES,
|
||||
)
|
||||
from cosyvoice.utils.mask import make_pad_mask
|
||||
from cosyvoice.utils.mask import add_optional_chunk_mask
|
||||
|
||||
|
||||
class BaseEncoder(torch.nn.Module):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_size: int = 256,
|
||||
attention_heads: int = 4,
|
||||
linear_units: int = 2048,
|
||||
num_blocks: int = 6,
|
||||
dropout_rate: float = 0.1,
|
||||
positional_dropout_rate: float = 0.1,
|
||||
attention_dropout_rate: float = 0.0,
|
||||
input_layer: str = "conv2d",
|
||||
pos_enc_layer_type: str = "abs_pos",
|
||||
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,
|
||||
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
|
||||
|
||||
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)
|
||||
if self.gradient_checkpointing and self.training:
|
||||
xs = self.forward_layers_checkpointed(xs, chunk_masks, pos_emb,
|
||||
mask_pad)
|
||||
else:
|
||||
xs = self.forward_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
|
||||
|
||||
@torch.jit.unused
|
||||
def forward_layers_checkpointed(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, _, _ = ckpt.checkpoint(layer.__call__, xs,
|
||||
chunk_masks, pos_emb,
|
||||
mask_pad)
|
||||
return xs
|
||||
|
||||
@torch.jit.export
|
||||
def forward_chunk(
|
||||
self,
|
||||
xs: torch.Tensor,
|
||||
offset: int,
|
||||
required_cache_size: int,
|
||||
att_cache: torch.Tensor = torch.zeros(0, 0, 0, 0),
|
||||
cnn_cache: torch.Tensor = torch.zeros(0, 0, 0, 0),
|
||||
att_mask: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool),
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
""" Forward just one chunk
|
||||
|
||||
Args:
|
||||
xs (torch.Tensor): chunk input, with shape (b=1, time, mel-dim),
|
||||
where `time == (chunk_size - 1) * subsample_rate + \
|
||||
subsample.right_context + 1`
|
||||
offset (int): current offset in encoder output time stamp
|
||||
required_cache_size (int): cache size required for next chunk
|
||||
compuation
|
||||
>=0: actual cache size
|
||||
<0: means all history cache is required
|
||||
att_cache (torch.Tensor): cache tensor for KEY & VALUE in
|
||||
transformer/conformer attention, with shape
|
||||
(elayers, head, cache_t1, d_k * 2), where
|
||||
`head * d_k == hidden-dim` and
|
||||
`cache_t1 == chunk_size * num_decoding_left_chunks`.
|
||||
cnn_cache (torch.Tensor): cache tensor for cnn_module in conformer,
|
||||
(elayers, b=1, hidden-dim, cache_t2), where
|
||||
`cache_t2 == cnn.lorder - 1`
|
||||
|
||||
Returns:
|
||||
torch.Tensor: output of current input xs,
|
||||
with shape (b=1, chunk_size, hidden-dim).
|
||||
torch.Tensor: new attention cache required for next chunk, with
|
||||
dynamic shape (elayers, head, ?, d_k * 2)
|
||||
depending on required_cache_size.
|
||||
torch.Tensor: new conformer cnn cache required for next chunk, with
|
||||
same shape as the original cnn_cache.
|
||||
|
||||
"""
|
||||
assert xs.size(0) == 1
|
||||
# tmp_masks is just for interface compatibility
|
||||
tmp_masks = torch.ones(1,
|
||||
xs.size(1),
|
||||
device=xs.device,
|
||||
dtype=torch.bool)
|
||||
tmp_masks = tmp_masks.unsqueeze(1)
|
||||
if self.global_cmvn is not None:
|
||||
xs = self.global_cmvn(xs)
|
||||
# NOTE(xcsong): Before embed, shape(xs) is (b=1, time, mel-dim)
|
||||
xs, pos_emb, _ = self.embed(xs, tmp_masks, offset)
|
||||
# NOTE(xcsong): After embed, shape(xs) is (b=1, chunk_size, hidden-dim)
|
||||
elayers, cache_t1 = att_cache.size(0), att_cache.size(2)
|
||||
chunk_size = xs.size(1)
|
||||
attention_key_size = cache_t1 + chunk_size
|
||||
pos_emb = self.embed.position_encoding(offset=offset - cache_t1,
|
||||
size=attention_key_size)
|
||||
if required_cache_size < 0:
|
||||
next_cache_start = 0
|
||||
elif required_cache_size == 0:
|
||||
next_cache_start = attention_key_size
|
||||
else:
|
||||
next_cache_start = max(attention_key_size - required_cache_size, 0)
|
||||
r_att_cache = []
|
||||
r_cnn_cache = []
|
||||
for i, layer in enumerate(self.encoders):
|
||||
# NOTE(xcsong): Before layer.forward
|
||||
# shape(att_cache[i:i + 1]) is (1, head, cache_t1, d_k * 2),
|
||||
# shape(cnn_cache[i]) is (b=1, hidden-dim, cache_t2)
|
||||
xs, _, new_att_cache, new_cnn_cache = layer(
|
||||
xs,
|
||||
att_mask,
|
||||
pos_emb,
|
||||
att_cache=att_cache[i:i + 1] if elayers > 0 else att_cache,
|
||||
cnn_cache=cnn_cache[i] if cnn_cache.size(0) > 0 else cnn_cache)
|
||||
# NOTE(xcsong): After layer.forward
|
||||
# shape(new_att_cache) is (1, head, attention_key_size, d_k * 2),
|
||||
# shape(new_cnn_cache) is (b=1, hidden-dim, cache_t2)
|
||||
r_att_cache.append(new_att_cache[:, :, next_cache_start:, :])
|
||||
r_cnn_cache.append(new_cnn_cache.unsqueeze(0))
|
||||
if self.normalize_before:
|
||||
xs = self.after_norm(xs)
|
||||
|
||||
# NOTE(xcsong): shape(r_att_cache) is (elayers, head, ?, d_k * 2),
|
||||
# ? may be larger than cache_t1, it depends on required_cache_size
|
||||
r_att_cache = torch.cat(r_att_cache, dim=0)
|
||||
# NOTE(xcsong): shape(r_cnn_cache) is (e, b=1, hidden-dim, cache_t2)
|
||||
r_cnn_cache = torch.cat(r_cnn_cache, dim=0)
|
||||
|
||||
return (xs, r_att_cache, r_cnn_cache)
|
||||
|
||||
@torch.jit.unused
|
||||
def forward_chunk_by_chunk(
|
||||
self,
|
||||
xs: torch.Tensor,
|
||||
decoding_chunk_size: int,
|
||||
num_decoding_left_chunks: int = -1,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
""" Forward input chunk by chunk with chunk_size like a streaming
|
||||
fashion
|
||||
|
||||
Here we should pay special attention to computation cache in the
|
||||
streaming style forward chunk by chunk. Three things should be taken
|
||||
into account for computation in the current network:
|
||||
1. transformer/conformer encoder layers output cache
|
||||
2. convolution in conformer
|
||||
3. convolution in subsampling
|
||||
|
||||
However, we don't implement subsampling cache for:
|
||||
1. We can control subsampling module to output the right result by
|
||||
overlapping input instead of cache left context, even though it
|
||||
wastes some computation, but subsampling only takes a very
|
||||
small fraction of computation in the whole model.
|
||||
2. Typically, there are several covolution layers with subsampling
|
||||
in subsampling module, it is tricky and complicated to do cache
|
||||
with different convolution layers with different subsampling
|
||||
rate.
|
||||
3. Currently, nn.Sequential is used to stack all the convolution
|
||||
layers in subsampling, we need to rewrite it to make it work
|
||||
with cache, which is not preferred.
|
||||
Args:
|
||||
xs (torch.Tensor): (1, max_len, dim)
|
||||
chunk_size (int): decoding chunk size
|
||||
"""
|
||||
assert decoding_chunk_size > 0
|
||||
# The model is trained by static or dynamic chunk
|
||||
assert self.static_chunk_size > 0 or self.use_dynamic_chunk
|
||||
subsampling = self.embed.subsampling_rate
|
||||
context = self.embed.right_context + 1 # Add current frame
|
||||
stride = subsampling * decoding_chunk_size
|
||||
decoding_window = (decoding_chunk_size - 1) * subsampling + context
|
||||
num_frames = xs.size(1)
|
||||
att_cache: torch.Tensor = torch.zeros((0, 0, 0, 0), device=xs.device)
|
||||
cnn_cache: torch.Tensor = torch.zeros((0, 0, 0, 0), device=xs.device)
|
||||
outputs = []
|
||||
offset = 0
|
||||
required_cache_size = decoding_chunk_size * num_decoding_left_chunks
|
||||
|
||||
# Feed forward overlap input step by step
|
||||
for cur in range(0, num_frames - context + 1, stride):
|
||||
end = min(cur + decoding_window, num_frames)
|
||||
chunk_xs = xs[:, cur:end, :]
|
||||
(y, att_cache,
|
||||
cnn_cache) = self.forward_chunk(chunk_xs, offset,
|
||||
required_cache_size, att_cache,
|
||||
cnn_cache)
|
||||
outputs.append(y)
|
||||
offset += y.size(1)
|
||||
ys = torch.cat(outputs, 1)
|
||||
masks = torch.ones((1, 1, ys.size(1)),
|
||||
device=ys.device,
|
||||
dtype=torch.bool)
|
||||
return ys, masks
|
||||
|
||||
|
||||
class TransformerEncoder(BaseEncoder):
|
||||
"""Transformer encoder module."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_size: int = 256,
|
||||
attention_heads: int = 4,
|
||||
linear_units: int = 2048,
|
||||
num_blocks: int = 6,
|
||||
dropout_rate: float = 0.1,
|
||||
positional_dropout_rate: float = 0.1,
|
||||
attention_dropout_rate: float = 0.0,
|
||||
input_layer: str = "conv2d",
|
||||
pos_enc_layer_type: str = "abs_pos",
|
||||
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,
|
||||
key_bias: bool = True,
|
||||
selfattention_layer_type: str = "selfattn",
|
||||
activation_type: str = "relu",
|
||||
gradient_checkpointing: bool = False,
|
||||
):
|
||||
""" Construct TransformerEncoder
|
||||
|
||||
See Encoder for the meaning of each parameter.
|
||||
"""
|
||||
super().__init__(input_size, output_size, attention_heads,
|
||||
linear_units, num_blocks, dropout_rate,
|
||||
positional_dropout_rate, attention_dropout_rate,
|
||||
input_layer, pos_enc_layer_type, normalize_before,
|
||||
static_chunk_size, use_dynamic_chunk, global_cmvn,
|
||||
use_dynamic_left_chunk, gradient_checkpointing)
|
||||
activation = COSYVOICE_ACTIVATION_CLASSES[activation_type]()
|
||||
self.encoders = torch.nn.ModuleList([
|
||||
TransformerEncoderLayer(
|
||||
output_size,
|
||||
COSYVOICE_ATTENTION_CLASSES[selfattention_layer_type](attention_heads,
|
||||
output_size,
|
||||
attention_dropout_rate,
|
||||
key_bias),
|
||||
PositionwiseFeedForward(output_size, linear_units,
|
||||
dropout_rate, activation),
|
||||
dropout_rate, normalize_before) for _ in range(num_blocks)
|
||||
])
|
||||
|
||||
|
||||
class ConformerEncoder(BaseEncoder):
|
||||
"""Conformer encoder module."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_size: int = 256,
|
||||
attention_heads: int = 4,
|
||||
linear_units: int = 2048,
|
||||
num_blocks: int = 6,
|
||||
dropout_rate: float = 0.1,
|
||||
positional_dropout_rate: float = 0.1,
|
||||
attention_dropout_rate: float = 0.0,
|
||||
input_layer: str = "conv2d",
|
||||
pos_enc_layer_type: str = "rel_pos",
|
||||
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 = True,
|
||||
selfattention_layer_type: str = "rel_selfattn",
|
||||
activation_type: str = "swish",
|
||||
use_cnn_module: bool = True,
|
||||
cnn_module_kernel: int = 15,
|
||||
causal: bool = False,
|
||||
cnn_module_norm: str = "batch_norm",
|
||||
key_bias: bool = True,
|
||||
gradient_checkpointing: bool = False,
|
||||
):
|
||||
"""Construct ConformerEncoder
|
||||
|
||||
Args:
|
||||
input_size to use_dynamic_chunk, see in BaseEncoder
|
||||
positionwise_conv_kernel_size (int): Kernel size of positionwise
|
||||
conv1d layer.
|
||||
macaron_style (bool): Whether to use macaron style for
|
||||
positionwise layer.
|
||||
selfattention_layer_type (str): Encoder attention layer type,
|
||||
the parameter has no effect now, it's just for configure
|
||||
compatibility.
|
||||
activation_type (str): Encoder activation function type.
|
||||
use_cnn_module (bool): Whether to use convolution module.
|
||||
cnn_module_kernel (int): Kernel size of convolution module.
|
||||
causal (bool): whether to use causal convolution or not.
|
||||
key_bias: whether use bias in attention.linear_k, False for whisper models.
|
||||
"""
|
||||
super().__init__(input_size, output_size, attention_heads,
|
||||
linear_units, num_blocks, dropout_rate,
|
||||
positional_dropout_rate, attention_dropout_rate,
|
||||
input_layer, pos_enc_layer_type, normalize_before,
|
||||
static_chunk_size, use_dynamic_chunk, global_cmvn,
|
||||
use_dynamic_left_chunk, 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.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)
|
||||
])
|
||||
@@ -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,96 @@
|
||||
# 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.
|
||||
"""Label smoothing module."""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
class LabelSmoothingLoss(nn.Module):
|
||||
"""Label-smoothing loss.
|
||||
|
||||
In a standard CE loss, the label's data distribution is:
|
||||
[0,1,2] ->
|
||||
[
|
||||
[1.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 1.0],
|
||||
]
|
||||
|
||||
In the smoothing version CE Loss,some probabilities
|
||||
are taken from the true label prob (1.0) and are divided
|
||||
among other labels.
|
||||
|
||||
e.g.
|
||||
smoothing=0.1
|
||||
[0,1,2] ->
|
||||
[
|
||||
[0.9, 0.05, 0.05],
|
||||
[0.05, 0.9, 0.05],
|
||||
[0.05, 0.05, 0.9],
|
||||
]
|
||||
|
||||
Args:
|
||||
size (int): the number of class
|
||||
padding_idx (int): padding class id which will be ignored for loss
|
||||
smoothing (float): smoothing rate (0.0 means the conventional CE)
|
||||
normalize_length (bool):
|
||||
normalize loss by sequence length if True
|
||||
normalize loss by batch size if False
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
size: int,
|
||||
padding_idx: int,
|
||||
smoothing: float,
|
||||
normalize_length: bool = False):
|
||||
"""Construct an LabelSmoothingLoss object."""
|
||||
super(LabelSmoothingLoss, self).__init__()
|
||||
self.criterion = nn.KLDivLoss(reduction="none")
|
||||
self.padding_idx = padding_idx
|
||||
self.confidence = 1.0 - smoothing
|
||||
self.smoothing = smoothing
|
||||
self.size = size
|
||||
self.normalize_length = normalize_length
|
||||
|
||||
def forward(self, x: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
|
||||
"""Compute loss between x and target.
|
||||
|
||||
The model outputs and data labels tensors are flatten to
|
||||
(batch*seqlen, class) shape and a mask is applied to the
|
||||
padding part which should not be calculated for loss.
|
||||
|
||||
Args:
|
||||
x (torch.Tensor): prediction (batch, seqlen, class)
|
||||
target (torch.Tensor):
|
||||
target signal masked with self.padding_id (batch, seqlen)
|
||||
Returns:
|
||||
loss (torch.Tensor) : The KL loss, scalar float value
|
||||
"""
|
||||
assert x.size(2) == self.size
|
||||
batch_size = x.size(0)
|
||||
x = x.view(-1, self.size)
|
||||
target = target.view(-1)
|
||||
# use zeros_like instead of torch.no_grad() for true_dist,
|
||||
# since no_grad() can not be exported by JIT
|
||||
true_dist = torch.zeros_like(x)
|
||||
true_dist.fill_(self.smoothing / (self.size - 1))
|
||||
ignore = target == self.padding_idx # (B,)
|
||||
total = len(target) - ignore.sum().item()
|
||||
target = target.masked_fill(ignore, 0) # avoid -1 index
|
||||
true_dist.scatter_(1, target.unsqueeze(1), self.confidence)
|
||||
kl = self.criterion(torch.log_softmax(x, dim=1), true_dist)
|
||||
denom = total if self.normalize_length else batch_size
|
||||
return kl.masked_fill(ignore.unsqueeze(1), 0).sum() / denom
|
||||
@@ -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,321 @@
|
||||
# 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 cosyvoice.transformer.convolution import ConvolutionModule
|
||||
from cosyvoice.transformer.encoder_layer import ConformerEncoderLayer
|
||||
from cosyvoice.transformer.positionwise_feed_forward import PositionwiseFeedForward
|
||||
from cosyvoice.utils.class_utils import (
|
||||
COSYVOICE_EMB_CLASSES,
|
||||
COSYVOICE_SUBSAMPLE_CLASSES,
|
||||
COSYVOICE_ATTENTION_CLASSES,
|
||||
COSYVOICE_ACTIVATION_CLASSES,
|
||||
)
|
||||
from cosyvoice.utils.mask import make_pad_mask
|
||||
from cosyvoice.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) -> Tuple[torch.Tensor, 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, in_channels: int, channels: int, pre_lookahead_len: int = 1):
|
||||
super().__init__()
|
||||
self.in_channels = in_channels
|
||||
self.channels = channels
|
||||
self.pre_lookahead_len = pre_lookahead_len
|
||||
self.conv1 = nn.Conv1d(
|
||||
in_channels, channels,
|
||||
kernel_size=pre_lookahead_len + 1,
|
||||
stride=1, padding=0,
|
||||
)
|
||||
self.conv2 = nn.Conv1d(
|
||||
channels, in_channels,
|
||||
kernel_size=3, stride=1, padding=0,
|
||||
)
|
||||
|
||||
def forward(self, inputs: torch.Tensor, context: torch.Tensor = torch.zeros(0, 0, 0)) -> torch.Tensor:
|
||||
"""
|
||||
inputs: (batch_size, seq_len, channels)
|
||||
"""
|
||||
outputs = inputs.transpose(1, 2).contiguous()
|
||||
context = context.transpose(1, 2).contiguous()
|
||||
# look ahead
|
||||
if context.size(2) == 0:
|
||||
outputs = F.pad(outputs, (0, self.pre_lookahead_len), mode='constant', value=0.0)
|
||||
else:
|
||||
assert self.training is False, 'you have passed context, make sure that you are running inference mode'
|
||||
assert context.size(2) == self.pre_lookahead_len
|
||||
outputs = F.pad(torch.concat([outputs, context], dim=2), (0, self.pre_lookahead_len - context.size(2)), mode='constant', value=0.0)
|
||||
outputs = F.leaky_relu(self.conv1(outputs))
|
||||
# outputs
|
||||
outputs = F.pad(outputs, (self.conv2.kernel_size[0] - 1, 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,
|
||||
output_size: int = 256,
|
||||
attention_heads: int = 4,
|
||||
linear_units: int = 2048,
|
||||
num_blocks: int = 6,
|
||||
dropout_rate: float = 0.1,
|
||||
positional_dropout_rate: float = 0.1,
|
||||
attention_dropout_rate: float = 0.0,
|
||||
input_layer: str = "conv2d",
|
||||
pos_enc_layer_type: str = "rel_pos",
|
||||
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 = True,
|
||||
selfattention_layer_type: str = "rel_selfattn",
|
||||
activation_type: str = "swish",
|
||||
use_cnn_module: bool = True,
|
||||
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(in_channels=512, 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,
|
||||
context: torch.Tensor = torch.zeros(0, 0, 0),
|
||||
decoding_chunk_size: int = 0,
|
||||
num_decoding_left_chunks: int = -1,
|
||||
streaming: bool = False,
|
||||
) -> 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)
|
||||
if context.size(1) != 0:
|
||||
assert self.training is False, 'you have passed context, make sure that you are running inference mode'
|
||||
context_masks = torch.ones(1, 1, context.size(1)).to(masks)
|
||||
context, _, _ = self.embed(context, context_masks, offset=xs.size(1))
|
||||
mask_pad = masks # (B, 1, T/subsample_rate)
|
||||
chunk_masks = add_optional_chunk_mask(xs, masks, False, False, 0, self.static_chunk_size if streaming is True else 0, -1)
|
||||
# lookahead + conformer encoder
|
||||
xs = self.pre_lookahead_layer(xs, context=context)
|
||||
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, False, False, 0, self.static_chunk_size * self.up_layer.stride if streaming is True else 0, -1)
|
||||
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,85 @@
|
||||
# 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 cosyvoice.transformer.activation import Swish
|
||||
from cosyvoice.transformer.subsampling import (
|
||||
LinearNoSubsampling,
|
||||
EmbedinigNoSubsampling,
|
||||
Conv1dSubsampling2,
|
||||
Conv2dSubsampling4,
|
||||
Conv2dSubsampling6,
|
||||
Conv2dSubsampling8,
|
||||
)
|
||||
from cosyvoice.transformer.embedding import (PositionalEncoding,
|
||||
RelPositionalEncoding,
|
||||
WhisperPositionalEncoding,
|
||||
LearnablePositionalEncoding,
|
||||
NoPositionalEncoding)
|
||||
from cosyvoice.transformer.attention import (MultiHeadedAttention,
|
||||
RelPositionMultiHeadedAttention)
|
||||
from cosyvoice.transformer.embedding import EspnetRelPositionalEncoding
|
||||
from cosyvoice.transformer.subsampling import LegacyLinearNoSubsampling
|
||||
from cosyvoice.llm.llm import TransformerLM, Qwen2LM, CosyVoice3LM
|
||||
from cosyvoice.flow.flow import MaskedDiffWithXvec, CausalMaskedDiffWithXvec, CausalMaskedDiffWithDiT
|
||||
from cosyvoice.hifigan.generator import HiFTGenerator, CausalHiFTGenerator
|
||||
from cosyvoice.cli.model import CosyVoiceModel, CosyVoice2Model, CosyVoice3Model
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
def get_model_type(configs):
|
||||
# NOTE CosyVoice2Model inherits CosyVoiceModel
|
||||
if isinstance(configs['llm'], TransformerLM) and isinstance(configs['flow'], MaskedDiffWithXvec) and isinstance(configs['hift'], HiFTGenerator):
|
||||
return CosyVoiceModel
|
||||
if isinstance(configs['llm'], Qwen2LM) and isinstance(configs['flow'], CausalMaskedDiffWithXvec) and isinstance(configs['hift'], HiFTGenerator):
|
||||
return CosyVoice2Model
|
||||
if isinstance(configs['llm'], CosyVoice3LM) and isinstance(configs['flow'], CausalMaskedDiffWithDiT) and isinstance(configs['hift'], CausalHiFTGenerator):
|
||||
return CosyVoice3Model
|
||||
raise TypeError('No valid model type found!')
|
||||
@@ -0,0 +1,213 @@
|
||||
# Copyright (c) 2020 Mobvoi Inc (Binbin Zhang)
|
||||
# 2024 Alibaba Inc (authors: Xiang Lyu)
|
||||
# 2025 Alibaba Inc (authors: Xiang Lyu, Bofan Zhou)
|
||||
#
|
||||
# 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)
|
||||
"""Unility functions for Transformer."""
|
||||
|
||||
import queue
|
||||
import random
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
IGNORE_ID = -1
|
||||
|
||||
instruct_list = ["You are a helpful assistant. 请用广东话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用东北话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用甘肃话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用贵州话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用河南话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用湖北话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用湖南话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用江西话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用闽南话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用宁夏话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用山西话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用陕西话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用山东话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用上海话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用四川话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用天津话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用云南话表达。<|endofprompt|>",
|
||||
"You are a helpful assistant. Please say a sentence as loudly as possible.<|endofprompt|>",
|
||||
"You are a helpful assistant. Please say a sentence in a very soft voice.<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用尽可能慢地语速说一句话。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请用尽可能快地语速说一句话。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请非常开心地说一句话。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请非常伤心地说一句话。<|endofprompt|>",
|
||||
"You are a helpful assistant. 请非常生气地说一句话。<|endofprompt|>",
|
||||
"You are a helpful assistant. 我想体验一下小猪佩奇风格,可以吗?<|endofprompt|>",
|
||||
"You are a helpful assistant. 你可以尝试用机器人的方式解答吗?<|endofprompt|>"]
|
||||
|
||||
|
||||
def pad_list(xs: List[torch.Tensor], pad_value: int):
|
||||
"""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.]])
|
||||
|
||||
"""
|
||||
max_len = max([len(item) for item in xs])
|
||||
batchs = len(xs)
|
||||
ndim = xs[0].ndim
|
||||
if ndim == 1:
|
||||
pad_res = torch.zeros(batchs,
|
||||
max_len,
|
||||
dtype=xs[0].dtype,
|
||||
device=xs[0].device)
|
||||
elif ndim == 2:
|
||||
pad_res = torch.zeros(batchs,
|
||||
max_len,
|
||||
xs[0].shape[1],
|
||||
dtype=xs[0].dtype,
|
||||
device=xs[0].device)
|
||||
elif ndim == 3:
|
||||
pad_res = torch.zeros(batchs,
|
||||
max_len,
|
||||
xs[0].shape[1],
|
||||
xs[0].shape[2],
|
||||
dtype=xs[0].dtype,
|
||||
device=xs[0].device)
|
||||
else:
|
||||
raise ValueError(f"Unsupported ndim: {ndim}")
|
||||
pad_res.fill_(pad_value)
|
||||
for i in range(batchs):
|
||||
pad_res[i, :len(xs[i])] = xs[i]
|
||||
return pad_res
|
||||
|
||||
|
||||
def th_accuracy(pad_outputs: torch.Tensor, pad_targets: torch.Tensor,
|
||||
ignore_label: int) -> torch.Tensor:
|
||||
"""Calculate accuracy.
|
||||
|
||||
Args:
|
||||
pad_outputs (Tensor): Prediction tensors (B * Lmax, D).
|
||||
pad_targets (LongTensor): Target label tensors (B, Lmax).
|
||||
ignore_label (int): Ignore label id.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Accuracy value (0.0 - 1.0).
|
||||
|
||||
"""
|
||||
pad_pred = pad_outputs.view(pad_targets.size(0), pad_targets.size(1),
|
||||
pad_outputs.size(1)).argmax(2)
|
||||
mask = pad_targets != ignore_label
|
||||
numerator = torch.sum(
|
||||
pad_pred.masked_select(mask) == pad_targets.masked_select(mask))
|
||||
denominator = torch.sum(mask)
|
||||
return (numerator / denominator).detach()
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# Repetition Aware Sampling in VALL-E 2
|
||||
def ras_sampling(weighted_scores, decoded_tokens, sampling, top_p=0.8, top_k=25, win_size=10, tau_r=0.1):
|
||||
top_ids = nucleus_sampling(weighted_scores, top_p=top_p, top_k=top_k)
|
||||
rep_num = (torch.tensor(decoded_tokens[-win_size:]).to(weighted_scores.device) == top_ids).sum().item()
|
||||
if rep_num >= win_size * tau_r:
|
||||
top_ids = random_sampling(weighted_scores, decoded_tokens, sampling)
|
||||
return top_ids
|
||||
|
||||
|
||||
def nucleus_sampling(weighted_scores, top_p=0.8, top_k=25):
|
||||
prob, indices = [], []
|
||||
cum_prob = 0.0
|
||||
sorted_value, sorted_idx = weighted_scores.softmax(dim=0).sort(descending=True, stable=True)
|
||||
for i in range(len(sorted_idx)):
|
||||
# sampling both top-p and numbers.
|
||||
if cum_prob < top_p and len(prob) < top_k:
|
||||
cum_prob += sorted_value[i]
|
||||
prob.append(sorted_value[i])
|
||||
indices.append(sorted_idx[i])
|
||||
else:
|
||||
break
|
||||
prob = torch.tensor(prob).to(weighted_scores)
|
||||
indices = torch.tensor(indices, dtype=torch.long).to(weighted_scores.device)
|
||||
top_ids = indices[prob.multinomial(1, replacement=True)].item()
|
||||
return top_ids
|
||||
|
||||
|
||||
def random_sampling(weighted_scores, decoded_tokens, sampling):
|
||||
top_ids = weighted_scores.softmax(dim=0).multinomial(1, replacement=True).item()
|
||||
return top_ids
|
||||
|
||||
|
||||
def fade_in_out(fade_in_mel, fade_out_mel, window):
|
||||
device = fade_in_mel.device
|
||||
fade_in_mel, fade_out_mel = fade_in_mel.cpu(), fade_out_mel.cpu()
|
||||
mel_overlap_len = int(window.shape[0] / 2)
|
||||
if fade_in_mel.device == torch.device('cpu'):
|
||||
fade_in_mel = fade_in_mel.clone()
|
||||
fade_in_mel[..., :mel_overlap_len] = fade_in_mel[..., :mel_overlap_len] * window[:mel_overlap_len] + \
|
||||
fade_out_mel[..., -mel_overlap_len:] * window[mel_overlap_len:]
|
||||
return fade_in_mel.to(device)
|
||||
|
||||
|
||||
def set_all_random_seed(seed):
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
|
||||
|
||||
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 TrtContextWrapper:
|
||||
def __init__(self, trt_engine, trt_concurrent=1, device='cuda:0'):
|
||||
self.trt_context_pool = queue.Queue(maxsize=trt_concurrent)
|
||||
self.trt_engine = trt_engine
|
||||
for _ in range(trt_concurrent):
|
||||
trt_context = trt_engine.create_execution_context()
|
||||
trt_stream = torch.cuda.stream(torch.cuda.Stream(device))
|
||||
assert trt_context is not None, 'failed to create trt context, maybe not enough CUDA memory, try reduce current trt concurrent {}'.format(trt_concurrent)
|
||||
self.trt_context_pool.put([trt_context, trt_stream])
|
||||
assert self.trt_context_pool.empty() is False, 'no avaialbe estimator context'
|
||||
|
||||
def acquire_estimator(self):
|
||||
return self.trt_context_pool.get(), self.trt_engine
|
||||
|
||||
def release_estimator(self, context, stream):
|
||||
self.trt_context_pool.put([context, stream])
|
||||
@@ -0,0 +1,176 @@
|
||||
# Copyright (c) 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 logging
|
||||
from contextlib import nullcontext
|
||||
import os
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
|
||||
from cosyvoice.utils.train_utils import update_parameter_and_lr, log_per_step, log_per_save, batch_forward, batch_backward, save_model, cosyvoice_join
|
||||
|
||||
|
||||
class Executor:
|
||||
|
||||
def __init__(self, gan: bool = False, ref_model: torch.nn.Module = None, dpo_loss: torch.nn.Module = None):
|
||||
self.gan = gan
|
||||
self.ref_model = ref_model
|
||||
self.dpo_loss = dpo_loss
|
||||
self.step = 0
|
||||
self.epoch = 0
|
||||
self.rank = int(os.environ.get('RANK', 0))
|
||||
self.device = torch.device('cuda:{}'.format(self.rank))
|
||||
|
||||
def train_one_epoc(self, model, optimizer, scheduler, train_data_loader, cv_data_loader, writer, info_dict, scaler, group_join, ref_model=None):
|
||||
''' Train one epoch
|
||||
'''
|
||||
|
||||
lr = optimizer.param_groups[0]['lr']
|
||||
logging.info('Epoch {} TRAIN info lr {} rank {}'.format(self.epoch, lr, self.rank))
|
||||
logging.info('using accumulate grad, new batch size is {} times'
|
||||
' larger than before'.format(info_dict['accum_grad']))
|
||||
# A context manager to be used in conjunction with an instance of
|
||||
# torch.nn.parallel.DistributedDataParallel to be able to train
|
||||
# with uneven inputs across participating processes.
|
||||
model.train()
|
||||
if self.ref_model is not None:
|
||||
self.ref_model.eval()
|
||||
model_context = model.join if info_dict['train_engine'] == 'torch_ddp' else nullcontext
|
||||
with model_context():
|
||||
for batch_idx, batch_dict in enumerate(train_data_loader):
|
||||
info_dict["tag"] = "TRAIN"
|
||||
info_dict["step"] = self.step
|
||||
info_dict["epoch"] = self.epoch
|
||||
info_dict["batch_idx"] = batch_idx
|
||||
if cosyvoice_join(group_join, info_dict):
|
||||
break
|
||||
|
||||
# Disable gradient synchronizations across DDP processes.
|
||||
# Within this context, gradients will be accumulated on module
|
||||
# variables, which will later be synchronized.
|
||||
if info_dict['train_engine'] == 'torch_ddp' and (batch_idx + 1) % info_dict["accum_grad"] != 0:
|
||||
context = model.no_sync
|
||||
# Used for single gpu training and DDP gradient synchronization
|
||||
# processes.
|
||||
else:
|
||||
context = nullcontext
|
||||
|
||||
with context():
|
||||
info_dict = batch_forward(model, batch_dict, scaler, info_dict, ref_model=self.ref_model, dpo_loss=self.dpo_loss)
|
||||
info_dict = batch_backward(model, scaler, info_dict)
|
||||
|
||||
info_dict = update_parameter_and_lr(model, optimizer, scheduler, scaler, info_dict)
|
||||
log_per_step(writer, info_dict)
|
||||
# NOTE specify save_per_step in cosyvoice.yaml if you want to enable step save
|
||||
if info_dict['save_per_step'] > 0 and (self.step + 1) % info_dict['save_per_step'] == 0 and \
|
||||
(batch_idx + 1) % info_dict["accum_grad"] == 0:
|
||||
dist.barrier()
|
||||
self.cv(model, cv_data_loader, writer, info_dict, on_batch_end=False)
|
||||
model.train()
|
||||
if (batch_idx + 1) % info_dict["accum_grad"] == 0:
|
||||
self.step += 1
|
||||
dist.barrier()
|
||||
self.cv(model, cv_data_loader, writer, info_dict, on_batch_end=True)
|
||||
|
||||
def train_one_epoc_gan(self, model, optimizer, scheduler, optimizer_d, scheduler_d, train_data_loader, cv_data_loader,
|
||||
writer, info_dict, scaler, group_join):
|
||||
''' Train one epoch
|
||||
'''
|
||||
|
||||
lr = optimizer.param_groups[0]['lr']
|
||||
logging.info('Epoch {} TRAIN info lr {} rank {}'.format(self.epoch, lr, self.rank))
|
||||
logging.info('using accumulate grad, new batch size is {} times'
|
||||
' larger than before'.format(info_dict['accum_grad']))
|
||||
# A context manager to be used in conjunction with an instance of
|
||||
# torch.nn.parallel.DistributedDataParallel to be able to train
|
||||
# with uneven inputs across participating processes.
|
||||
model.train()
|
||||
model_context = model.join if info_dict['train_engine'] == 'torch_ddp' else nullcontext
|
||||
with model_context():
|
||||
for batch_idx, batch_dict in enumerate(train_data_loader):
|
||||
info_dict["tag"] = "TRAIN"
|
||||
info_dict["step"] = self.step
|
||||
info_dict["epoch"] = self.epoch
|
||||
info_dict["batch_idx"] = batch_idx
|
||||
if cosyvoice_join(group_join, info_dict):
|
||||
break
|
||||
|
||||
# Disable gradient synchronizations across DDP processes.
|
||||
# Within this context, gradients will be accumulated on module
|
||||
# variables, which will later be synchronized.
|
||||
if info_dict['train_engine'] == 'torch_ddp' and (batch_idx + 1) % info_dict["accum_grad"] != 0:
|
||||
context = model.no_sync
|
||||
# Used for single gpu training and DDP gradient synchronization
|
||||
# processes.
|
||||
else:
|
||||
context = nullcontext
|
||||
|
||||
with context():
|
||||
batch_dict['turn'] = 'discriminator'
|
||||
info_dict = batch_forward(model, batch_dict, scaler, info_dict)
|
||||
info_dict = batch_backward(model, scaler, info_dict)
|
||||
info_dict = update_parameter_and_lr(model, optimizer_d, scheduler_d, scaler, info_dict)
|
||||
optimizer.zero_grad()
|
||||
log_per_step(writer, info_dict)
|
||||
with context():
|
||||
batch_dict['turn'] = 'generator'
|
||||
info_dict = batch_forward(model, batch_dict, scaler, info_dict)
|
||||
info_dict = batch_backward(model, scaler, info_dict)
|
||||
info_dict = update_parameter_and_lr(model, optimizer, scheduler, scaler, info_dict)
|
||||
optimizer_d.zero_grad()
|
||||
log_per_step(writer, info_dict)
|
||||
# NOTE specify save_per_step in cosyvoice.yaml if you want to enable step save
|
||||
if info_dict['save_per_step'] > 0 and (self.step + 1) % info_dict['save_per_step'] == 0 and \
|
||||
(batch_idx + 1) % info_dict["accum_grad"] == 0:
|
||||
dist.barrier()
|
||||
self.cv(model, cv_data_loader, writer, info_dict, on_batch_end=False)
|
||||
model.train()
|
||||
if (batch_idx + 1) % info_dict["accum_grad"] == 0:
|
||||
self.step += 1
|
||||
dist.barrier()
|
||||
self.cv(model, cv_data_loader, writer, info_dict, on_batch_end=True)
|
||||
|
||||
@torch.inference_mode()
|
||||
def cv(self, model, cv_data_loader, writer, info_dict, on_batch_end=True):
|
||||
''' Cross validation on
|
||||
'''
|
||||
logging.info('Epoch {} Step {} on_batch_end {} CV rank {}'.format(self.epoch, self.step + 1, on_batch_end, self.rank))
|
||||
model.eval()
|
||||
total_num_utts, total_loss_dict = 0, {} # avoid division by 0
|
||||
for batch_idx, batch_dict in enumerate(cv_data_loader):
|
||||
info_dict["tag"] = "CV"
|
||||
info_dict["step"] = self.step
|
||||
info_dict["epoch"] = self.epoch
|
||||
info_dict["batch_idx"] = batch_idx
|
||||
|
||||
num_utts = len(batch_dict["utts"])
|
||||
total_num_utts += num_utts
|
||||
|
||||
if self.gan is True:
|
||||
batch_dict['turn'] = 'generator'
|
||||
info_dict = batch_forward(model, batch_dict, None, info_dict)
|
||||
|
||||
for k, v in info_dict['loss_dict'].items():
|
||||
if k not in total_loss_dict:
|
||||
total_loss_dict[k] = []
|
||||
total_loss_dict[k].append(v.mean().item() * num_utts)
|
||||
log_per_step(None, info_dict)
|
||||
for k, v in total_loss_dict.items():
|
||||
total_loss_dict[k] = sum(v) / total_num_utts
|
||||
info_dict['loss_dict'] = total_loss_dict
|
||||
log_per_save(writer, info_dict)
|
||||
model_name = 'epoch_{}_whole'.format(self.epoch) if on_batch_end else 'epoch_{}_step_{}'.format(self.epoch, self.step + 1)
|
||||
save_model(model, model_name, info_dict)
|
||||
@@ -0,0 +1,118 @@
|
||||
# Copyright (c) 2021 Mobvoi Inc. (authors: Binbin Zhang)
|
||||
# 2024 Alibaba Inc (authors: Xiang Lyu, Zetao Hu)
|
||||
# 2025 Alibaba Inc (authors: Xiang Lyu, Yabin Li)
|
||||
#
|
||||
# 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 os
|
||||
import json
|
||||
import torch
|
||||
import torchaudio
|
||||
import logging
|
||||
logging.getLogger('matplotlib').setLevel(logging.WARNING)
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s %(levelname)s %(message)s')
|
||||
|
||||
|
||||
def read_lists(list_file):
|
||||
lists = []
|
||||
with open(list_file, 'r', encoding='utf8') as fin:
|
||||
for line in fin:
|
||||
lists.append(line.strip())
|
||||
return lists
|
||||
|
||||
|
||||
def read_json_lists(list_file):
|
||||
lists = read_lists(list_file)
|
||||
results = {}
|
||||
for fn in lists:
|
||||
with open(fn, 'r', encoding='utf8') as fin:
|
||||
results.update(json.load(fin))
|
||||
return results
|
||||
|
||||
|
||||
def load_wav(wav, target_sr, min_sr=16000):
|
||||
speech, sample_rate = torchaudio.load(wav, backend='soundfile')
|
||||
speech = speech.mean(dim=0, keepdim=True)
|
||||
if sample_rate != target_sr:
|
||||
assert sample_rate >= min_sr, 'wav sample rate {} must be greater than {}'.format(sample_rate, target_sr)
|
||||
speech = torchaudio.transforms.Resample(orig_freq=sample_rate, new_freq=target_sr)(speech)
|
||||
return speech
|
||||
|
||||
|
||||
def convert_onnx_to_trt(trt_model, trt_kwargs, onnx_model, fp16):
|
||||
import tensorrt as trt
|
||||
logging.info("Converting onnx to trt...")
|
||||
network_flags = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
|
||||
logger = trt.Logger(trt.Logger.INFO)
|
||||
builder = trt.Builder(logger)
|
||||
network = builder.create_network(network_flags)
|
||||
parser = trt.OnnxParser(network, logger)
|
||||
config = builder.create_builder_config()
|
||||
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 32) # 4GB
|
||||
if fp16:
|
||||
config.set_flag(trt.BuilderFlag.FP16)
|
||||
profile = builder.create_optimization_profile()
|
||||
# load onnx model
|
||||
with open(onnx_model, "rb") as f:
|
||||
if not parser.parse(f.read()):
|
||||
for error in range(parser.num_errors):
|
||||
print(parser.get_error(error))
|
||||
raise ValueError('failed to parse {}'.format(onnx_model))
|
||||
# set input shapes
|
||||
for i in range(len(trt_kwargs['input_names'])):
|
||||
profile.set_shape(trt_kwargs['input_names'][i], trt_kwargs['min_shape'][i], trt_kwargs['opt_shape'][i], trt_kwargs['max_shape'][i])
|
||||
tensor_dtype = trt.DataType.HALF if fp16 else trt.DataType.FLOAT
|
||||
# set input and output data type
|
||||
for i in range(network.num_inputs):
|
||||
input_tensor = network.get_input(i)
|
||||
input_tensor.dtype = tensor_dtype
|
||||
for i in range(network.num_outputs):
|
||||
output_tensor = network.get_output(i)
|
||||
output_tensor.dtype = tensor_dtype
|
||||
config.add_optimization_profile(profile)
|
||||
engine_bytes = builder.build_serialized_network(network, config)
|
||||
# save trt engine
|
||||
with open(trt_model, "wb") as f:
|
||||
f.write(engine_bytes)
|
||||
logging.info("Succesfully convert onnx to trt...")
|
||||
|
||||
|
||||
# NOTE do not support bistream inference as only speech token embedding/head is kept
|
||||
def export_cosyvoice2_vllm(model, model_path, device):
|
||||
if os.path.exists(model_path):
|
||||
return
|
||||
|
||||
dtype = torch.bfloat16
|
||||
# lm_head
|
||||
use_bias = True if model.llm_decoder.bias is not None else False
|
||||
model.llm.model.lm_head = model.llm_decoder
|
||||
# embed_tokens
|
||||
embed_tokens = model.llm.model.model.embed_tokens
|
||||
model.llm.model.set_input_embeddings(model.speech_embedding)
|
||||
model.llm.model.to(device)
|
||||
model.llm.model.to(dtype)
|
||||
tmp_vocab_size = model.llm.model.config.vocab_size
|
||||
tmp_tie_embedding = model.llm.model.config.tie_word_embeddings
|
||||
del model.llm.model.generation_config.eos_token_id
|
||||
del model.llm.model.config.bos_token_id
|
||||
del model.llm.model.config.eos_token_id
|
||||
model.llm.model.config.vocab_size = model.speech_embedding.num_embeddings
|
||||
model.llm.model.config.tie_word_embeddings = False
|
||||
model.llm.model.config.use_bias = use_bias
|
||||
model.llm.model.save_pretrained(model_path)
|
||||
if use_bias is True:
|
||||
os.system('sed -i s@Qwen2ForCausalLM@CosyVoice2ForCausalLM@g {}/config.json'.format(os.path.abspath(model_path)))
|
||||
model.llm.model.config.vocab_size = tmp_vocab_size
|
||||
model.llm.model.config.tie_word_embeddings = tmp_tie_embedding
|
||||
model.llm.model.set_input_embeddings(embed_tokens)
|
||||
@@ -0,0 +1,136 @@
|
||||
# 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 re
|
||||
import regex
|
||||
chinese_char_pattern = re.compile(r'[\u4e00-\u9fff]+')
|
||||
|
||||
|
||||
# whether contain chinese character
|
||||
def contains_chinese(text):
|
||||
return bool(chinese_char_pattern.search(text))
|
||||
|
||||
|
||||
# replace special symbol
|
||||
def replace_corner_mark(text):
|
||||
text = text.replace('²', '平方')
|
||||
text = text.replace('³', '立方')
|
||||
return text
|
||||
|
||||
|
||||
# remove meaningless symbol
|
||||
def remove_bracket(text):
|
||||
text = text.replace('(', '').replace(')', '')
|
||||
text = text.replace('【', '').replace('】', '')
|
||||
text = text.replace('`', '').replace('`', '')
|
||||
text = text.replace("——", " ")
|
||||
return text
|
||||
|
||||
|
||||
# spell Arabic numerals
|
||||
def spell_out_number(text: str, inflect_parser):
|
||||
new_text = []
|
||||
st = None
|
||||
for i, c in enumerate(text):
|
||||
if not c.isdigit():
|
||||
if st is not None:
|
||||
num_str = inflect_parser.number_to_words(text[st: i])
|
||||
new_text.append(num_str)
|
||||
st = None
|
||||
new_text.append(c)
|
||||
else:
|
||||
if st is None:
|
||||
st = i
|
||||
if st is not None and st < len(text):
|
||||
num_str = inflect_parser.number_to_words(text[st:])
|
||||
new_text.append(num_str)
|
||||
return ''.join(new_text)
|
||||
|
||||
|
||||
# split paragrah logic:
|
||||
# 1. per sentence max len token_max_n, min len token_min_n, merge if last sentence len less than merge_len
|
||||
# 2. cal sentence len according to lang
|
||||
# 3. split sentence according to puncatation
|
||||
def split_paragraph(text: str, tokenize, lang="zh", token_max_n=80, token_min_n=60, merge_len=20, comma_split=False):
|
||||
def calc_utt_length(_text: str):
|
||||
if lang == "zh":
|
||||
return len(_text)
|
||||
else:
|
||||
return len(tokenize(_text))
|
||||
|
||||
def should_merge(_text: str):
|
||||
if lang == "zh":
|
||||
return len(_text) < merge_len
|
||||
else:
|
||||
return len(tokenize(_text)) < merge_len
|
||||
|
||||
if lang == "zh":
|
||||
pounc = ['。', '?', '!', ';', ':', '、', '.', '?', '!', ';']
|
||||
else:
|
||||
pounc = ['.', '?', '!', ';', ':']
|
||||
if comma_split:
|
||||
pounc.extend([',', ','])
|
||||
|
||||
if text[-1] not in pounc:
|
||||
if lang == "zh":
|
||||
text += "。"
|
||||
else:
|
||||
text += "."
|
||||
|
||||
st = 0
|
||||
utts = []
|
||||
for i, c in enumerate(text):
|
||||
if c in pounc:
|
||||
if len(text[st: i]) > 0:
|
||||
utts.append(text[st: i] + c)
|
||||
if i + 1 < len(text) and text[i + 1] in ['"', '”']:
|
||||
tmp = utts.pop(-1)
|
||||
utts.append(tmp + text[i + 1])
|
||||
st = i + 2
|
||||
else:
|
||||
st = i + 1
|
||||
|
||||
final_utts = []
|
||||
cur_utt = ""
|
||||
for utt in utts:
|
||||
if calc_utt_length(cur_utt + utt) > token_max_n and calc_utt_length(cur_utt) > token_min_n:
|
||||
final_utts.append(cur_utt)
|
||||
cur_utt = ""
|
||||
cur_utt = cur_utt + utt
|
||||
if len(cur_utt) > 0:
|
||||
if should_merge(cur_utt) and len(final_utts) != 0:
|
||||
final_utts[-1] = final_utts[-1] + cur_utt
|
||||
else:
|
||||
final_utts.append(cur_utt)
|
||||
|
||||
return final_utts
|
||||
|
||||
|
||||
# remove blank between chinese character
|
||||
def replace_blank(text: str):
|
||||
out_str = []
|
||||
for i, c in enumerate(text):
|
||||
if c == " ":
|
||||
if ((text[i + 1].isascii() and text[i + 1] != " ") and
|
||||
(text[i - 1].isascii() and text[i - 1] != " ")):
|
||||
out_str.append(c)
|
||||
else:
|
||||
out_str.append(c)
|
||||
return "".join(out_str)
|
||||
|
||||
|
||||
def is_only_punctuation(text):
|
||||
# Regular expression: Match strings that consist only of punctuation marks or are empty.
|
||||
punctuation_pattern = r'^[\p{P}\p{S}]*$'
|
||||
return bool(regex.fullmatch(punctuation_pattern, text))
|
||||
@@ -0,0 +1,57 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def tpr_loss(disc_real_outputs, disc_generated_outputs, tau):
|
||||
loss = 0
|
||||
for dr, dg in zip(disc_real_outputs, disc_generated_outputs):
|
||||
m_DG = torch.median((dr - dg))
|
||||
L_rel = torch.mean((((dr - dg) - m_DG) ** 2)[dr < dg + m_DG])
|
||||
loss += tau - F.relu(tau - L_rel)
|
||||
return loss
|
||||
|
||||
|
||||
def mel_loss(real_speech, generated_speech, mel_transforms):
|
||||
loss = 0
|
||||
for transform in mel_transforms:
|
||||
mel_r = transform(real_speech)
|
||||
mel_g = transform(generated_speech)
|
||||
loss += F.l1_loss(mel_g, mel_r)
|
||||
return loss
|
||||
|
||||
|
||||
class DPOLoss(torch.nn.Module):
|
||||
"""
|
||||
DPO Loss
|
||||
"""
|
||||
|
||||
def __init__(self, beta: float, label_smoothing: float = 0.0, ipo: bool = False) -> None:
|
||||
super().__init__()
|
||||
self.beta = beta
|
||||
self.label_smoothing = label_smoothing
|
||||
self.ipo = ipo
|
||||
|
||||
def forward(
|
||||
self,
|
||||
policy_chosen_logps: torch.Tensor,
|
||||
policy_rejected_logps: torch.Tensor,
|
||||
reference_chosen_logps: torch.Tensor,
|
||||
reference_rejected_logps: torch.Tensor,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
pi_logratios = policy_chosen_logps - policy_rejected_logps
|
||||
ref_logratios = reference_chosen_logps - reference_rejected_logps
|
||||
logits = pi_logratios - ref_logratios
|
||||
if self.ipo:
|
||||
losses = (logits - 1 / (2 * self.beta)) ** 2 # Eq. 17 of https://arxiv.org/pdf/2310.12036v2.pdf
|
||||
else:
|
||||
# Eq. 3 https://ericmitchell.ai/cdpo.pdf; label_smoothing=0 gives original DPO (Eq. 7 of https://arxiv.org/pdf/2305.18290.pdf)
|
||||
losses = (
|
||||
-F.logsigmoid(self.beta * logits) * (1 - self.label_smoothing)
|
||||
- F.logsigmoid(-self.beta * logits) * self.label_smoothing
|
||||
)
|
||||
loss = losses.mean()
|
||||
chosen_rewards = self.beta * (policy_chosen_logps - reference_chosen_logps).detach()
|
||||
rejected_rewards = self.beta * (policy_rejected_logps - reference_rejected_logps).detach()
|
||||
|
||||
return loss, chosen_rewards, rejected_rewards
|
||||
@@ -0,0 +1,265 @@
|
||||
# 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_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]]
|
||||
"""
|
||||
arange = torch.arange(size, device=device)
|
||||
mask = arange.expand(size, size)
|
||||
arange = arange.unsqueeze(-1)
|
||||
mask = mask <= arange
|
||||
return mask
|
||||
|
||||
|
||||
def subsequent_chunk_mask_deprecated(
|
||||
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]]
|
||||
"""
|
||||
ret = torch.zeros(size, size, device=device, dtype=torch.bool)
|
||||
for i in range(size):
|
||||
if num_left_chunks < 0:
|
||||
start = 0
|
||||
else:
|
||||
start = max((i // chunk_size - num_left_chunks) * chunk_size, 0)
|
||||
ending = min((i // chunk_size + 1) * chunk_size, size)
|
||||
ret[i, start:ending] = True
|
||||
return 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
|
||||
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:
|
||||
print('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,738 @@
|
||||
# Copyright (c) 2020 Mobvoi Inc (Binbin Zhang)
|
||||
# 2022 Ximalaya Inc (Yuguang Yang)
|
||||
# 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.
|
||||
# Modified from ESPnet(https://github.com/espnet/espnet)
|
||||
# NeMo(https://github.com/NVIDIA/NeMo)
|
||||
|
||||
from typing import Union
|
||||
|
||||
import math
|
||||
import warnings
|
||||
import torch
|
||||
from torch.optim.lr_scheduler import _LRScheduler
|
||||
|
||||
|
||||
class WarmupLR(_LRScheduler):
|
||||
"""The WarmupLR scheduler
|
||||
|
||||
This scheduler is almost same as NoamLR Scheduler except for following
|
||||
difference:
|
||||
|
||||
NoamLR:
|
||||
lr = optimizer.lr * model_size ** -0.5
|
||||
* min(step ** -0.5, step * warmup_step ** -1.5)
|
||||
WarmupLR:
|
||||
lr = optimizer.lr * warmup_step ** 0.5
|
||||
* min(step ** -0.5, step * warmup_step ** -1.5)
|
||||
|
||||
Note that the maximum lr equals to optimizer.lr in this scheduler.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
optimizer: torch.optim.Optimizer,
|
||||
warmup_steps: Union[int, float] = 25000,
|
||||
last_epoch: int = -1,
|
||||
):
|
||||
self.warmup_steps = warmup_steps
|
||||
|
||||
# __init__() must be invoked before setting field
|
||||
# because step() is also invoked in __init__()
|
||||
super().__init__(optimizer, last_epoch)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}(warmup_steps={self.warmup_steps})"
|
||||
|
||||
def get_lr(self):
|
||||
step_num = self.last_epoch + 1
|
||||
if self.warmup_steps == 0:
|
||||
return [lr * step_num**-0.5 for lr in self.base_lrs]
|
||||
else:
|
||||
return [
|
||||
lr * self.warmup_steps**0.5 *
|
||||
min(step_num**-0.5, step_num * self.warmup_steps**-1.5)
|
||||
for lr in self.base_lrs
|
||||
]
|
||||
|
||||
def set_step(self, step: int):
|
||||
self.last_epoch = step
|
||||
|
||||
|
||||
class WarmupPolicy(_LRScheduler):
|
||||
"""Adds warmup kwargs and warmup logic to lr policy.
|
||||
All arguments should be passed as kwargs for clarity,
|
||||
Args:
|
||||
warmup_steps: Number of training steps in warmup stage
|
||||
warmup_ratio: Ratio of warmup steps to total steps
|
||||
max_steps: Total number of steps while training or `None` for
|
||||
infinite training
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
*,
|
||||
warmup_steps=None,
|
||||
warmup_ratio=None,
|
||||
max_steps=None,
|
||||
min_lr=0.0,
|
||||
last_epoch=-1):
|
||||
assert not (warmup_steps is not None and warmup_ratio is not None),\
|
||||
"Either use particular number of step or ratio"
|
||||
assert warmup_ratio is None or max_steps is not None, \
|
||||
"If there is a ratio, there should be a total steps"
|
||||
|
||||
# It is necessary to assign all attributes *before* __init__,
|
||||
# as class is wrapped by an inner class.
|
||||
self.max_steps = max_steps
|
||||
if warmup_steps is not None:
|
||||
self.warmup_steps = warmup_steps
|
||||
elif warmup_ratio is not None:
|
||||
self.warmup_steps = int(warmup_ratio * max_steps)
|
||||
else:
|
||||
self.warmup_steps = 0
|
||||
|
||||
self.min_lr = min_lr
|
||||
super().__init__(optimizer, last_epoch)
|
||||
|
||||
def get_lr(self):
|
||||
if not self._get_lr_called_within_step:
|
||||
warnings.warn(
|
||||
"To get the last learning rate computed "
|
||||
"by the scheduler, please use `get_last_lr()`.",
|
||||
UserWarning,
|
||||
stacklevel=2)
|
||||
|
||||
step = self.last_epoch
|
||||
|
||||
if step <= self.warmup_steps and self.warmup_steps > 0:
|
||||
return self._get_warmup_lr(step)
|
||||
|
||||
if step > self.max_steps:
|
||||
return [self.min_lr for _ in self.base_lrs]
|
||||
|
||||
return self._get_lr(step)
|
||||
|
||||
def _get_warmup_lr(self, step):
|
||||
lr_val = (step + 1) / (self.warmup_steps + 1)
|
||||
return [initial_lr * lr_val for initial_lr in self.base_lrs]
|
||||
|
||||
def _get_lr(self, step):
|
||||
"""Simple const lr policy"""
|
||||
return self.base_lrs
|
||||
|
||||
|
||||
class SquareRootConstantPolicy(_LRScheduler):
|
||||
"""Adds warmup kwargs and warmup logic to lr policy.
|
||||
All arguments should be passed as kwargs for clarity,
|
||||
Args:
|
||||
warmup_steps: Number of training steps in warmup stage
|
||||
warmup_ratio: Ratio of warmup steps to total steps
|
||||
max_steps: Total number of steps while training or `None` for
|
||||
infinite training
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
*,
|
||||
constant_steps=None,
|
||||
constant_ratio=None,
|
||||
max_steps=None,
|
||||
min_lr=0.0,
|
||||
last_epoch=-1):
|
||||
assert not (constant_steps is not None
|
||||
and constant_ratio is not None), \
|
||||
"Either use particular number of step or ratio"
|
||||
assert constant_ratio is None or max_steps is not None, \
|
||||
"If there is a ratio, there should be a total steps"
|
||||
|
||||
# It is necessary to assign all attributes *before* __init__,
|
||||
# as class is wrapped by an inner class.
|
||||
self.max_steps = max_steps
|
||||
if constant_steps is not None:
|
||||
self.constant_steps = constant_steps
|
||||
elif constant_ratio is not None:
|
||||
self.constant_steps = int(constant_ratio * max_steps)
|
||||
else:
|
||||
self.constant_steps = 0
|
||||
|
||||
self.constant_lr = 1 / (constant_steps**0.5)
|
||||
self.min_lr = min_lr
|
||||
super().__init__(optimizer, last_epoch)
|
||||
|
||||
def get_lr(self):
|
||||
if not self._get_lr_called_within_step:
|
||||
warnings.warn(
|
||||
"To get the last learning rate computed "
|
||||
"by the scheduler, please use `get_last_lr()`.",
|
||||
UserWarning,
|
||||
stacklevel=2)
|
||||
|
||||
step = self.last_epoch
|
||||
|
||||
if step <= self.constant_steps:
|
||||
return [self.constant_lr for _ in self.base_lrs]
|
||||
|
||||
if step > self.max_steps:
|
||||
return [self.min_lr for _ in self.base_lrs]
|
||||
|
||||
return self._get_lr(step)
|
||||
|
||||
def _get_lr(self, step):
|
||||
"""Simple const lr policy"""
|
||||
return self.base_lrs
|
||||
|
||||
|
||||
class WarmupHoldPolicy(WarmupPolicy):
|
||||
"""Variant of WarmupPolicy which maintains high
|
||||
learning rate for a defined number of steps.
|
||||
All arguments should be passed as kwargs for clarity,
|
||||
Args:
|
||||
warmup_steps: Number of training steps in warmup stage
|
||||
warmup_ratio: Ratio of warmup steps to total steps
|
||||
hold_steps: Number of training steps to
|
||||
hold the learning rate after warm up
|
||||
hold_ratio: Ratio of hold steps to total steps
|
||||
max_steps: Total number of steps while training or `None` for
|
||||
infinite training
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
optimizer,
|
||||
*,
|
||||
warmup_steps=None,
|
||||
warmup_ratio=None,
|
||||
hold_steps=None,
|
||||
hold_ratio=None,
|
||||
max_steps=None,
|
||||
min_lr=0.0,
|
||||
last_epoch=-1,
|
||||
):
|
||||
assert not (hold_steps is not None and hold_ratio is not None), \
|
||||
"Either use particular number of step or ratio"
|
||||
assert hold_ratio is None or max_steps is not None, \
|
||||
"If there is a ratio, there should be a total steps"
|
||||
|
||||
self.min_lr = min_lr
|
||||
self._last_warmup_lr = 0.0
|
||||
|
||||
# Necessary to duplicate as class attributes are hidden in inner class
|
||||
self.max_steps = max_steps
|
||||
if warmup_steps is not None:
|
||||
self.warmup_steps = warmup_steps
|
||||
elif warmup_ratio is not None:
|
||||
self.warmup_steps = int(warmup_ratio * max_steps)
|
||||
else:
|
||||
self.warmup_steps = 0
|
||||
|
||||
if hold_steps is not None:
|
||||
self.hold_steps = hold_steps + self.warmup_steps
|
||||
elif hold_ratio is not None:
|
||||
self.hold_steps = int(hold_ratio * max_steps) + self.warmup_steps
|
||||
else:
|
||||
self.hold_steps = 0
|
||||
|
||||
super().__init__(
|
||||
optimizer,
|
||||
warmup_steps=warmup_steps,
|
||||
warmup_ratio=warmup_ratio,
|
||||
max_steps=max_steps,
|
||||
last_epoch=last_epoch,
|
||||
min_lr=min_lr,
|
||||
)
|
||||
|
||||
def get_lr(self):
|
||||
if not self._get_lr_called_within_step:
|
||||
warnings.warn(
|
||||
"To get the last learning rate computed by the scheduler,"
|
||||
" "
|
||||
"please use `get_last_lr()`.",
|
||||
UserWarning,
|
||||
stacklevel=2)
|
||||
|
||||
step = self.last_epoch
|
||||
|
||||
# Warmup phase
|
||||
if step <= self.warmup_steps and self.warmup_steps > 0:
|
||||
return self._get_warmup_lr(step)
|
||||
|
||||
# Hold phase
|
||||
if (step >= self.warmup_steps) and (step < self.hold_steps):
|
||||
return self.base_lrs
|
||||
|
||||
if step > self.max_steps:
|
||||
return [self.min_lr for _ in self.base_lrs]
|
||||
|
||||
return self._get_lr(step)
|
||||
|
||||
|
||||
class WarmupAnnealHoldPolicy(_LRScheduler):
|
||||
"""Adds warmup kwargs and warmup logic to lr policy.
|
||||
All arguments should be passed as kwargs for clarity,
|
||||
Args:
|
||||
warmup_steps: Number of training steps in warmup stage
|
||||
warmup_ratio: Ratio of warmup steps to total steps
|
||||
max_steps: Total number of steps while training or `None` for
|
||||
infinite training
|
||||
min_lr: Minimum lr to hold the learning rate after decay at.
|
||||
constant_steps: Number of steps to keep lr constant at.
|
||||
constant_ratio: Ratio of steps to keep lr constant.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
optimizer,
|
||||
*,
|
||||
warmup_steps=None,
|
||||
warmup_ratio=None,
|
||||
constant_steps=None,
|
||||
constant_ratio=None,
|
||||
max_steps=None,
|
||||
min_lr=0.0,
|
||||
last_epoch=-1,
|
||||
):
|
||||
assert not (warmup_steps is not None
|
||||
and warmup_ratio is not None), \
|
||||
"Either use particular number of step or ratio"
|
||||
assert not (constant_steps is not None
|
||||
and constant_ratio is not None), \
|
||||
"Either use constant_steps or constant_ratio"
|
||||
assert warmup_ratio is None or max_steps is not None, \
|
||||
"If there is a ratio, there should be a total steps"
|
||||
|
||||
# It is necessary to assign all attributes *before* __init__,
|
||||
# as class is wrapped by an inner class.
|
||||
self.max_steps = max_steps
|
||||
|
||||
if warmup_steps is not None:
|
||||
self.warmup_steps = warmup_steps
|
||||
elif warmup_ratio is not None:
|
||||
self.warmup_steps = int(warmup_ratio * max_steps)
|
||||
else:
|
||||
self.warmup_steps = 0
|
||||
|
||||
if constant_steps is not None:
|
||||
self.constant_steps = constant_steps
|
||||
elif constant_ratio is not None:
|
||||
self.constant_steps = int(constant_ratio * max_steps)
|
||||
else:
|
||||
self.constant_steps = 0
|
||||
|
||||
self.decay_steps = max_steps - (self.constant_steps +
|
||||
self.warmup_steps)
|
||||
|
||||
self.min_lr = min_lr
|
||||
super().__init__(optimizer, last_epoch)
|
||||
|
||||
def get_lr(self):
|
||||
if not self._get_lr_called_within_step:
|
||||
warnings.warn(
|
||||
"To get the last learning rate computed "
|
||||
"by the scheduler, please use `get_last_lr()`.",
|
||||
UserWarning,
|
||||
stacklevel=2)
|
||||
|
||||
step = self.last_epoch
|
||||
|
||||
# Warmup steps
|
||||
if self.warmup_steps > 0 and step <= self.warmup_steps:
|
||||
return self._get_warmup_lr(step)
|
||||
|
||||
# Constant steps after warmup and decay
|
||||
if self.constant_steps > 0 and (
|
||||
self.warmup_steps + self.decay_steps) < step <= self.max_steps:
|
||||
return self._get_constant_lr(step)
|
||||
|
||||
# Min lr after max steps of updates
|
||||
if step > self.max_steps:
|
||||
return [self.min_lr for _ in self.base_lrs]
|
||||
|
||||
return self._get_lr(step)
|
||||
|
||||
def _get_warmup_lr(self, step):
|
||||
lr_val = (step + 1) / (self.warmup_steps + 1)
|
||||
return [initial_lr * lr_val for initial_lr in self.base_lrs]
|
||||
|
||||
def _get_constant_lr(self, step):
|
||||
return [self.min_lr for _ in self.base_lrs]
|
||||
|
||||
def _get_lr(self, step):
|
||||
"""Simple const lr policy"""
|
||||
return self.base_lrs
|
||||
|
||||
|
||||
def _squareroot_annealing(initial_lr, step, max_steps, min_lr):
|
||||
mult = ((max_steps - step) / max_steps)**0.5
|
||||
out_lr = initial_lr * mult
|
||||
out_lr = max(out_lr, min_lr)
|
||||
return out_lr
|
||||
|
||||
|
||||
def _square_annealing(initial_lr, step, max_steps, min_lr):
|
||||
mult = ((max_steps - step) / max_steps)**2
|
||||
out_lr = initial_lr * mult
|
||||
out_lr = max(out_lr, min_lr)
|
||||
return out_lr
|
||||
|
||||
|
||||
def _cosine_annealing(initial_lr, step, max_steps, min_lr):
|
||||
mult = 0.5 * (1 + math.cos(math.pi * step / max_steps))
|
||||
out_lr = (initial_lr - min_lr) * mult + min_lr
|
||||
return out_lr
|
||||
|
||||
|
||||
def _linear_warmup_with_cosine_annealing(max_lr, warmup_steps, step,
|
||||
decay_steps, min_lr):
|
||||
assert max_lr > min_lr
|
||||
# Use linear warmup for the initial part.
|
||||
if warmup_steps > 0 and step <= warmup_steps:
|
||||
return max_lr * float(step) / float(warmup_steps)
|
||||
|
||||
# For any steps larger than `decay_steps`, use `min_lr`.
|
||||
if step > warmup_steps + decay_steps:
|
||||
return min_lr
|
||||
|
||||
# If we are done with the warmup period, use the decay style.
|
||||
num_steps_ = step - warmup_steps
|
||||
decay_steps_ = decay_steps
|
||||
decay_ratio = float(num_steps_) / float(decay_steps_)
|
||||
assert decay_ratio >= 0.0
|
||||
assert decay_ratio <= 1.0
|
||||
delta_lr = max_lr - min_lr
|
||||
|
||||
coeff = 0.5 * (math.cos(math.pi * decay_ratio) + 1.0)
|
||||
|
||||
return min_lr + coeff * delta_lr
|
||||
|
||||
|
||||
def _poly_decay(initial_lr, step, decay_steps, power, min_lr, cycle):
|
||||
if cycle:
|
||||
multiplier = 1.0 if step == 0 else math.ceil(step / decay_steps)
|
||||
decay_steps *= multiplier
|
||||
else:
|
||||
step = min(step, decay_steps)
|
||||
p = step / decay_steps
|
||||
lr = (initial_lr - min_lr) * math.pow(1.0 - p, power)
|
||||
lr += min_lr
|
||||
return lr
|
||||
|
||||
|
||||
def _noam_hold_annealing(initial_lr, step, warmup_steps, hold_steps,
|
||||
decay_rate, min_lr):
|
||||
# hold_steps = total number of steps
|
||||
# to hold the LR, not the warmup + hold steps.
|
||||
T_warmup_decay = max(1, warmup_steps**decay_rate)
|
||||
T_hold_decay = max(1, (step - hold_steps)**decay_rate)
|
||||
lr = (initial_lr * T_warmup_decay) / T_hold_decay
|
||||
lr = max(lr, min_lr)
|
||||
return lr
|
||||
|
||||
|
||||
class SquareAnnealing(WarmupPolicy):
|
||||
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
*,
|
||||
max_steps,
|
||||
min_lr=1e-5,
|
||||
last_epoch=-1,
|
||||
**kwargs):
|
||||
super().__init__(optimizer=optimizer,
|
||||
max_steps=max_steps,
|
||||
last_epoch=last_epoch,
|
||||
min_lr=min_lr,
|
||||
**kwargs)
|
||||
|
||||
def _get_lr(self, step):
|
||||
new_lrs = [
|
||||
_square_annealing(
|
||||
initial_lr=initial_lr,
|
||||
step=step - self.warmup_steps,
|
||||
max_steps=self.max_steps - self.warmup_steps,
|
||||
min_lr=self.min_lr,
|
||||
) for initial_lr in self.base_lrs
|
||||
]
|
||||
return new_lrs
|
||||
|
||||
|
||||
class SquareRootAnnealing(WarmupPolicy):
|
||||
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
*,
|
||||
max_steps,
|
||||
min_lr=0,
|
||||
last_epoch=-1,
|
||||
**kwargs):
|
||||
super().__init__(optimizer=optimizer,
|
||||
max_steps=max_steps,
|
||||
last_epoch=last_epoch,
|
||||
min_lr=min_lr,
|
||||
**kwargs)
|
||||
|
||||
def _get_lr(self, step):
|
||||
new_lrs = [
|
||||
_squareroot_annealing(initial_lr=initial_lr,
|
||||
step=step,
|
||||
max_steps=self.max_steps,
|
||||
min_lr=self.min_lr)
|
||||
for initial_lr in self.base_lrs
|
||||
]
|
||||
return new_lrs
|
||||
|
||||
|
||||
class CosineAnnealing(WarmupAnnealHoldPolicy):
|
||||
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
*,
|
||||
max_steps,
|
||||
min_lr=0,
|
||||
last_epoch=-1,
|
||||
**kwargs):
|
||||
super().__init__(optimizer=optimizer,
|
||||
max_steps=max_steps,
|
||||
last_epoch=last_epoch,
|
||||
min_lr=min_lr,
|
||||
**kwargs)
|
||||
|
||||
def _get_lr(self, step):
|
||||
for initial_lr in self.base_lrs:
|
||||
if initial_lr < self.min_lr:
|
||||
raise ValueError(
|
||||
f"{self} received an initial learning rate "
|
||||
f"that was lower than the minimum learning rate.")
|
||||
|
||||
if self.constant_steps is None or self.constant_steps == 0:
|
||||
new_lrs = [
|
||||
_cosine_annealing(
|
||||
initial_lr=initial_lr,
|
||||
step=step - self.warmup_steps,
|
||||
max_steps=self.max_steps - self.warmup_steps,
|
||||
min_lr=self.min_lr,
|
||||
) for initial_lr in self.base_lrs
|
||||
]
|
||||
else:
|
||||
new_lrs = self._get_linear_warmup_with_cosine_annealing_lr(step)
|
||||
return new_lrs
|
||||
|
||||
def _get_warmup_lr(self, step):
|
||||
if self.constant_steps is None or self.constant_steps == 0:
|
||||
return super()._get_warmup_lr(step)
|
||||
else:
|
||||
# Use linear warmup for the initial part.
|
||||
return self._get_linear_warmup_with_cosine_annealing_lr(step)
|
||||
|
||||
def _get_constant_lr(self, step):
|
||||
# Only called when `constant_steps` > 0.
|
||||
return self._get_linear_warmup_with_cosine_annealing_lr(step)
|
||||
|
||||
def _get_linear_warmup_with_cosine_annealing_lr(self, step):
|
||||
# Cosine Schedule for Megatron LM,
|
||||
# slightly different warmup schedule + constant LR at the end.
|
||||
new_lrs = [
|
||||
_linear_warmup_with_cosine_annealing(
|
||||
max_lr=self.base_lrs[0],
|
||||
warmup_steps=self.warmup_steps,
|
||||
step=step,
|
||||
decay_steps=self.decay_steps,
|
||||
min_lr=self.min_lr,
|
||||
) for _ in self.base_lrs
|
||||
]
|
||||
return new_lrs
|
||||
|
||||
|
||||
class NoamAnnealing(_LRScheduler):
|
||||
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
*,
|
||||
d_model,
|
||||
warmup_steps=None,
|
||||
warmup_ratio=None,
|
||||
max_steps=None,
|
||||
min_lr=0.0,
|
||||
last_epoch=-1):
|
||||
self._normalize = d_model**(-0.5)
|
||||
assert not (warmup_steps is not None and warmup_ratio is not None), \
|
||||
"Either use particular number of step or ratio"
|
||||
assert warmup_ratio is None or max_steps is not None, \
|
||||
"If there is a ratio, there should be a total steps"
|
||||
|
||||
# It is necessary to assign all attributes *before* __init__,
|
||||
# as class is wrapped by an inner class.
|
||||
self.max_steps = max_steps
|
||||
if warmup_steps is not None:
|
||||
self.warmup_steps = warmup_steps
|
||||
elif warmup_ratio is not None:
|
||||
self.warmup_steps = int(warmup_ratio * max_steps)
|
||||
else:
|
||||
self.warmup_steps = 0
|
||||
|
||||
self.min_lr = min_lr
|
||||
super().__init__(optimizer, last_epoch)
|
||||
|
||||
def get_lr(self):
|
||||
if not self._get_lr_called_within_step:
|
||||
warnings.warn(
|
||||
"To get the last learning rate computed "
|
||||
"by the scheduler, please use `get_last_lr()`.",
|
||||
UserWarning,
|
||||
stacklevel=2)
|
||||
|
||||
step = max(1, self.last_epoch)
|
||||
|
||||
for initial_lr in self.base_lrs:
|
||||
if initial_lr < self.min_lr:
|
||||
raise ValueError(
|
||||
f"{self} received an initial learning rate "
|
||||
f"that was lower than the minimum learning rate.")
|
||||
|
||||
new_lrs = [
|
||||
self._noam_annealing(initial_lr=initial_lr, step=step)
|
||||
for initial_lr in self.base_lrs
|
||||
]
|
||||
return new_lrs
|
||||
|
||||
def _noam_annealing(self, initial_lr, step):
|
||||
if self.warmup_steps > 0:
|
||||
mult = self._normalize * min(step**(-0.5),
|
||||
step * (self.warmup_steps**(-1.5)))
|
||||
else:
|
||||
mult = self._normalize * step**(-0.5)
|
||||
|
||||
out_lr = initial_lr * mult
|
||||
if step > self.warmup_steps:
|
||||
out_lr = max(out_lr, self.min_lr)
|
||||
return out_lr
|
||||
|
||||
|
||||
class NoamHoldAnnealing(WarmupHoldPolicy):
|
||||
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
*,
|
||||
max_steps,
|
||||
decay_rate=0.5,
|
||||
min_lr=0.0,
|
||||
last_epoch=-1,
|
||||
**kwargs):
|
||||
"""
|
||||
From Nemo:
|
||||
Implementation of the Noam Hold Annealing policy
|
||||
from the SqueezeFormer paper.
|
||||
|
||||
Unlike NoamAnnealing, the peak learning rate
|
||||
can be explicitly set for this scheduler.
|
||||
The schedule first performs linear warmup,
|
||||
then holds the peak LR, then decays with some schedule for
|
||||
the remainder of the steps.
|
||||
Therefore the min-lr is still dependent
|
||||
on the hyper parameters selected.
|
||||
|
||||
It's schedule is determined by three factors-
|
||||
|
||||
Warmup Steps: Initial stage, where linear warmup
|
||||
occurs uptil the peak LR is reached. Unlike NoamAnnealing,
|
||||
the peak LR is explicitly stated here instead of a scaling factor.
|
||||
|
||||
Hold Steps: Intermediate stage, where the peak LR
|
||||
is maintained for some number of steps. In this region,
|
||||
the high peak LR allows the model to converge faster
|
||||
if training is stable. However the high LR
|
||||
may also cause instability during training.
|
||||
Should usually be a significant fraction of training
|
||||
steps (around 30-40% of the entire training steps).
|
||||
|
||||
Decay Steps: Final stage, where the LR rapidly decays
|
||||
with some scaling rate (set by decay rate).
|
||||
To attain Noam decay, use 0.5,
|
||||
for Squeezeformer recommended decay, use 1.0.
|
||||
The fast decay after prolonged high LR during
|
||||
hold phase allows for rapid convergence.
|
||||
|
||||
References:
|
||||
- [Squeezeformer:
|
||||
An Efficient Transformer for Automatic Speech Recognition]
|
||||
(https://arxiv.org/abs/2206.00888)
|
||||
|
||||
Args:
|
||||
optimizer: Pytorch compatible Optimizer object.
|
||||
warmup_steps: Number of training steps in warmup stage
|
||||
warmup_ratio: Ratio of warmup steps to total steps
|
||||
hold_steps: Number of training steps to
|
||||
hold the learning rate after warm up
|
||||
hold_ratio: Ratio of hold steps to total steps
|
||||
max_steps: Total number of steps while training or `None` for
|
||||
infinite training
|
||||
decay_rate: Float value describing the polynomial decay
|
||||
after the hold period. Default value
|
||||
of 0.5 corresponds to Noam decay.
|
||||
min_lr: Minimum learning rate.
|
||||
"""
|
||||
self.decay_rate = decay_rate
|
||||
super().__init__(optimizer=optimizer,
|
||||
max_steps=max_steps,
|
||||
last_epoch=last_epoch,
|
||||
min_lr=min_lr,
|
||||
**kwargs)
|
||||
|
||||
def _get_lr(self, step):
|
||||
if self.warmup_steps is None or self.warmup_steps == 0:
|
||||
raise ValueError(
|
||||
"Noam scheduler cannot be used without warmup steps")
|
||||
|
||||
if self.hold_steps > 0:
|
||||
hold_steps = self.hold_steps - self.warmup_steps
|
||||
else:
|
||||
hold_steps = 0
|
||||
|
||||
new_lrs = [
|
||||
_noam_hold_annealing(
|
||||
initial_lr,
|
||||
step=step,
|
||||
warmup_steps=self.warmup_steps,
|
||||
hold_steps=hold_steps,
|
||||
decay_rate=self.decay_rate,
|
||||
min_lr=self.min_lr,
|
||||
) for initial_lr in self.base_lrs
|
||||
]
|
||||
return new_lrs
|
||||
|
||||
def set_step(self, step: int):
|
||||
self.last_epoch = step
|
||||
|
||||
|
||||
class ConstantLR(_LRScheduler):
|
||||
"""The ConstantLR scheduler
|
||||
|
||||
This scheduler keeps a constant lr
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
optimizer: torch.optim.Optimizer,
|
||||
):
|
||||
# __init__() must be invoked before setting field
|
||||
# because step() is also invoked in __init__()
|
||||
super().__init__(optimizer)
|
||||
|
||||
def get_lr(self):
|
||||
return self.base_lrs
|
||||
|
||||
def set_step(self, step: int):
|
||||
self.last_epoch = step
|
||||
@@ -0,0 +1,367 @@
|
||||
# Copyright (c) 2021 Mobvoi Inc. (authors: Binbin Zhang)
|
||||
# 2023 Horizon Inc. (authors: 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 logging
|
||||
import os
|
||||
import torch
|
||||
import json
|
||||
import re
|
||||
import datetime
|
||||
import yaml
|
||||
|
||||
import deepspeed
|
||||
import torch.optim as optim
|
||||
import torch.distributed as dist
|
||||
|
||||
from torch.utils.tensorboard import SummaryWriter
|
||||
from torch.utils.data import DataLoader
|
||||
from torch.nn.utils import clip_grad_norm_
|
||||
|
||||
from deepspeed.runtime.zero.stage_1_and_2 import estimate_zero2_model_states_mem_needs_all_live
|
||||
|
||||
from cosyvoice.dataset.dataset import Dataset
|
||||
from cosyvoice.utils.scheduler import WarmupLR, NoamHoldAnnealing, ConstantLR
|
||||
|
||||
|
||||
def init_distributed(args):
|
||||
world_size = int(os.environ.get('WORLD_SIZE', 1))
|
||||
local_rank = int(os.environ.get('LOCAL_RANK', 0))
|
||||
rank = int(os.environ.get('RANK', 0))
|
||||
logging.info('training on multiple gpus, this gpu {}'.format(local_rank) +
|
||||
', rank {}, world_size {}'.format(rank, world_size))
|
||||
if args.train_engine == 'torch_ddp':
|
||||
torch.cuda.set_device(local_rank)
|
||||
dist.init_process_group(args.dist_backend)
|
||||
else:
|
||||
deepspeed.init_distributed(dist_backend=args.dist_backend)
|
||||
return world_size, local_rank, rank
|
||||
|
||||
|
||||
def init_dataset_and_dataloader(args, configs, gan, dpo):
|
||||
data_pipeline = configs['data_pipeline_gan'] if gan is True else configs['data_pipeline']
|
||||
train_dataset = Dataset(args.train_data, data_pipeline=data_pipeline, mode='train', gan=gan, dpo=dpo, shuffle=True, partition=True)
|
||||
cv_dataset = Dataset(args.cv_data, data_pipeline=data_pipeline, mode='train', gan=gan, dpo=dpo, shuffle=False, partition=False)
|
||||
|
||||
# do not use persistent_workers=True, as whisper tokenizer opens tiktoken file each time when the for loop starts
|
||||
train_data_loader = DataLoader(train_dataset,
|
||||
batch_size=None,
|
||||
pin_memory=args.pin_memory,
|
||||
num_workers=args.num_workers,
|
||||
prefetch_factor=args.prefetch)
|
||||
cv_data_loader = DataLoader(cv_dataset,
|
||||
batch_size=None,
|
||||
pin_memory=args.pin_memory,
|
||||
num_workers=args.num_workers,
|
||||
prefetch_factor=args.prefetch)
|
||||
return train_dataset, cv_dataset, train_data_loader, cv_data_loader
|
||||
|
||||
|
||||
def check_modify_and_save_config(args, configs):
|
||||
if args.train_engine == "torch_ddp":
|
||||
configs['train_conf']["dtype"] = 'bf16' if args.use_amp is True else 'fp32'
|
||||
else:
|
||||
with open(args.deepspeed_config, 'r') as fin:
|
||||
ds_configs = json.load(fin)
|
||||
if "fp16" in ds_configs and ds_configs["fp16"]["enabled"]:
|
||||
configs['train_conf']["dtype"] = "fp16"
|
||||
elif "bf16" in ds_configs and ds_configs["bf16"]["enabled"]:
|
||||
configs['train_conf']["dtype"] = "bf16"
|
||||
else:
|
||||
configs['train_conf']["dtype"] = "fp32"
|
||||
assert ds_configs["train_micro_batch_size_per_gpu"] == 1
|
||||
# if use deepspeed, override ddp config
|
||||
configs['train_conf']['save_per_step'] = int(configs['train_conf']['save_per_step'] *
|
||||
configs['train_conf']['accum_grad'] / ds_configs["gradient_accumulation_steps"])
|
||||
configs['train_conf']['accum_grad'] = ds_configs["gradient_accumulation_steps"]
|
||||
configs['train_conf']['grad_clip'] = ds_configs["gradient_clipping"]
|
||||
configs['train_conf']['log_interval'] = ds_configs["steps_per_print"]
|
||||
return configs
|
||||
|
||||
|
||||
def wrap_cuda_model(args, model):
|
||||
local_world_size = int(os.environ.get('LOCAL_WORLD_SIZE', 1))
|
||||
world_size = int(os.environ.get('WORLD_SIZE', 1))
|
||||
if args.train_engine == "torch_ddp": # native pytorch ddp
|
||||
assert (torch.cuda.is_available())
|
||||
model.cuda()
|
||||
model = torch.nn.parallel.DistributedDataParallel(model, find_unused_parameters=True)
|
||||
else:
|
||||
if int(os.environ.get('RANK', 0)) == 0:
|
||||
logging.info("Estimating model states memory needs (zero2)...")
|
||||
estimate_zero2_model_states_mem_needs_all_live(
|
||||
model,
|
||||
num_gpus_per_node=local_world_size,
|
||||
num_nodes=world_size // local_world_size)
|
||||
return model
|
||||
|
||||
|
||||
def init_optimizer_and_scheduler(args, configs, model, gan):
|
||||
if gan is False:
|
||||
if configs['train_conf']['optim'] == 'adam':
|
||||
optimizer = optim.Adam(model.parameters(), **configs['train_conf']['optim_conf'])
|
||||
elif configs['train_conf']['optim'] == 'adamw':
|
||||
optimizer = optim.AdamW(model.parameters(), **configs['train_conf']['optim_conf'])
|
||||
else:
|
||||
raise ValueError("unknown optimizer: " + configs['train_conf'])
|
||||
|
||||
if configs['train_conf']['scheduler'] == 'warmuplr':
|
||||
scheduler_type = WarmupLR
|
||||
scheduler = WarmupLR(optimizer, **configs['train_conf']['scheduler_conf'])
|
||||
elif configs['train_conf']['scheduler'] == 'NoamHoldAnnealing':
|
||||
scheduler_type = NoamHoldAnnealing
|
||||
scheduler = NoamHoldAnnealing(optimizer, **configs['train_conf']['scheduler_conf'])
|
||||
elif configs['train_conf']['scheduler'] == 'constantlr':
|
||||
scheduler_type = ConstantLR
|
||||
scheduler = ConstantLR(optimizer)
|
||||
else:
|
||||
raise ValueError("unknown scheduler: " + configs['train_conf'])
|
||||
|
||||
# use deepspeed optimizer for speedup
|
||||
if args.train_engine == "deepspeed":
|
||||
def scheduler(opt):
|
||||
return scheduler_type(opt, **configs['train_conf']['scheduler_conf'])
|
||||
model, optimizer, _, scheduler = deepspeed.initialize(
|
||||
args=args,
|
||||
model=model,
|
||||
optimizer=None,
|
||||
lr_scheduler=scheduler,
|
||||
model_parameters=model.parameters())
|
||||
|
||||
optimizer_d, scheduler_d = None, None
|
||||
|
||||
else:
|
||||
# currently we wrap generator and discriminator in one model, so we cannot use deepspeed
|
||||
if configs['train_conf']['optim'] == 'adam':
|
||||
optimizer = optim.Adam(model.module.generator.parameters(), **configs['train_conf']['optim_conf'])
|
||||
elif configs['train_conf']['optim'] == 'adamw':
|
||||
optimizer = optim.AdamW(model.module.generator.parameters(), **configs['train_conf']['optim_conf'])
|
||||
else:
|
||||
raise ValueError("unknown optimizer: " + configs['train_conf'])
|
||||
|
||||
if configs['train_conf']['scheduler'] == 'warmuplr':
|
||||
scheduler_type = WarmupLR
|
||||
scheduler = WarmupLR(optimizer, **configs['train_conf']['scheduler_conf'])
|
||||
elif configs['train_conf']['scheduler'] == 'NoamHoldAnnealing':
|
||||
scheduler_type = NoamHoldAnnealing
|
||||
scheduler = NoamHoldAnnealing(optimizer, **configs['train_conf']['scheduler_conf'])
|
||||
elif configs['train_conf']['scheduler'] == 'constantlr':
|
||||
scheduler_type = ConstantLR
|
||||
scheduler = ConstantLR(optimizer)
|
||||
else:
|
||||
raise ValueError("unknown scheduler: " + configs['train_conf'])
|
||||
|
||||
if configs['train_conf']['optim_d'] == 'adam':
|
||||
optimizer_d = optim.Adam(model.module.discriminator.parameters(), **configs['train_conf']['optim_conf'])
|
||||
elif configs['train_conf']['optim_d'] == 'adamw':
|
||||
optimizer_d = optim.AdamW(model.module.discriminator.parameters(), **configs['train_conf']['optim_conf'])
|
||||
else:
|
||||
raise ValueError("unknown optimizer: " + configs['train_conf'])
|
||||
|
||||
if configs['train_conf']['scheduler_d'] == 'warmuplr':
|
||||
scheduler_type = WarmupLR
|
||||
scheduler_d = WarmupLR(optimizer_d, **configs['train_conf']['scheduler_conf'])
|
||||
elif configs['train_conf']['scheduler_d'] == 'NoamHoldAnnealing':
|
||||
scheduler_type = NoamHoldAnnealing
|
||||
scheduler_d = NoamHoldAnnealing(optimizer_d, **configs['train_conf']['scheduler_conf'])
|
||||
elif configs['train_conf']['scheduler'] == 'constantlr':
|
||||
scheduler_type = ConstantLR
|
||||
scheduler_d = ConstantLR(optimizer_d)
|
||||
else:
|
||||
raise ValueError("unknown scheduler: " + configs['train_conf'])
|
||||
return model, optimizer, scheduler, optimizer_d, scheduler_d
|
||||
|
||||
|
||||
def init_summarywriter(args):
|
||||
writer = None
|
||||
if int(os.environ.get('RANK', 0)) == 0:
|
||||
os.makedirs(args.model_dir, exist_ok=True)
|
||||
writer = SummaryWriter(args.tensorboard_dir)
|
||||
return writer
|
||||
|
||||
|
||||
def save_model(model, model_name, info_dict):
|
||||
rank = int(os.environ.get('RANK', 0))
|
||||
model_dir = info_dict["model_dir"]
|
||||
save_model_path = os.path.join(model_dir, '{}.pt'.format(model_name))
|
||||
|
||||
if info_dict["train_engine"] == "torch_ddp":
|
||||
if rank == 0:
|
||||
torch.save({**model.module.state_dict(), 'epoch': info_dict['epoch'], 'step': info_dict['step']}, save_model_path)
|
||||
else:
|
||||
with torch.no_grad():
|
||||
model.save_checkpoint(save_dir=model_dir,
|
||||
tag=model_name,
|
||||
client_state=info_dict)
|
||||
if rank == 0:
|
||||
info_path = re.sub('.pt$', '.yaml', save_model_path)
|
||||
info_dict['save_time'] = datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S')
|
||||
with open(info_path, 'w') as fout:
|
||||
data = yaml.dump(info_dict)
|
||||
fout.write(data)
|
||||
logging.info('[Rank {}] Checkpoint: save to checkpoint {}'.format(rank, save_model_path))
|
||||
|
||||
|
||||
def cosyvoice_join(group_join, info_dict):
|
||||
world_size = int(os.environ.get('WORLD_SIZE', 1))
|
||||
local_rank = int(os.environ.get('LOCAL_RANK', 0))
|
||||
rank = int(os.environ.get('RANK', 0))
|
||||
|
||||
if info_dict["batch_idx"] != 0:
|
||||
# we try to join all rank in both ddp and deepspeed mode, in case different rank has different lr
|
||||
try:
|
||||
dist.monitored_barrier(group=group_join,
|
||||
timeout=group_join.options._timeout)
|
||||
return False
|
||||
except RuntimeError as e:
|
||||
logging.info("Detected uneven workload distribution: {}\n".format(e) +
|
||||
"Break current worker to manually join all workers, " +
|
||||
"world_size {}, current rank {}, current local_rank {}\n".
|
||||
format(world_size, rank, local_rank))
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def batch_forward(model, batch, scaler, info_dict, ref_model=None, dpo_loss=None):
|
||||
device = int(os.environ.get('LOCAL_RANK', 0))
|
||||
|
||||
dtype = info_dict["dtype"]
|
||||
if dtype == "fp16":
|
||||
dtype = torch.float16
|
||||
elif dtype == "bf16":
|
||||
dtype = torch.bfloat16
|
||||
else: # fp32
|
||||
dtype = torch.float32
|
||||
|
||||
if info_dict['train_engine'] == 'torch_ddp':
|
||||
autocast = torch.cuda.amp.autocast(enabled=scaler is not None, dtype=dtype)
|
||||
else:
|
||||
autocast = torch.cuda.amp.autocast(enabled=True, dtype=dtype, cache_enabled=False)
|
||||
|
||||
with autocast:
|
||||
info_dict['loss_dict'] = model(batch, device)
|
||||
if ref_model is not None and dpo_loss is not None:
|
||||
chosen_logps = info_dict['loss_dict']["chosen_logps"]
|
||||
rejected_logps = info_dict['loss_dict']["rejected_logps"]
|
||||
sft_loss = info_dict['loss_dict']['loss']
|
||||
with torch.no_grad():
|
||||
ref_loss_dict = ref_model(batch, device)
|
||||
reference_chosen_logps = ref_loss_dict["chosen_logps"]
|
||||
reference_rejected_logps = ref_loss_dict["rejected_logps"]
|
||||
preference_loss, chosen_reward, reject_reward = dpo_loss(
|
||||
chosen_logps, rejected_logps, reference_chosen_logps, reference_rejected_logps
|
||||
)
|
||||
dpo_acc = (chosen_reward > reject_reward).float().mean()
|
||||
info_dict['loss_dict']["loss"] = preference_loss + sft_loss
|
||||
info_dict['loss_dict']["sft_loss"] = sft_loss
|
||||
info_dict['loss_dict']["dpo_loss"] = preference_loss
|
||||
info_dict['loss_dict']["dpo_acc"] = dpo_acc
|
||||
info_dict['loss_dict']["chosen_reward"] = chosen_reward.mean()
|
||||
info_dict['loss_dict']["reject_reward"] = reject_reward.mean()
|
||||
return info_dict
|
||||
|
||||
|
||||
def batch_backward(model, scaler, info_dict):
|
||||
if info_dict["train_engine"] == "deepspeed":
|
||||
scaled_loss = model.backward(info_dict['loss_dict']['loss'])
|
||||
else:
|
||||
scaled_loss = info_dict['loss_dict']['loss'] / info_dict['accum_grad']
|
||||
if scaler is not None:
|
||||
scaler.scale(scaled_loss).backward()
|
||||
else:
|
||||
scaled_loss.backward()
|
||||
|
||||
info_dict['loss_dict']['loss'] = scaled_loss
|
||||
return info_dict
|
||||
|
||||
|
||||
def update_parameter_and_lr(model, optimizer, scheduler, scaler, info_dict):
|
||||
grad_norm = 0.0
|
||||
if info_dict['train_engine'] == "deepspeed":
|
||||
info_dict["is_gradient_accumulation_boundary"] = model.is_gradient_accumulation_boundary()
|
||||
model.step()
|
||||
grad_norm = model.get_global_grad_norm()
|
||||
elif (info_dict['batch_idx'] + 1) % info_dict["accum_grad"] == 0:
|
||||
# Use mixed precision training
|
||||
if scaler is not None:
|
||||
scaler.unscale_(optimizer)
|
||||
grad_norm = clip_grad_norm_(model.parameters(), info_dict['grad_clip'])
|
||||
# We don't check grad here since that if the gradient
|
||||
# has inf/nan values, scaler.step will skip
|
||||
# optimizer.step().
|
||||
if torch.isfinite(grad_norm):
|
||||
scaler.step(optimizer)
|
||||
else:
|
||||
logging.warning('get infinite grad_norm, check your code/data if it appears frequently')
|
||||
scaler.update()
|
||||
else:
|
||||
grad_norm = clip_grad_norm_(model.parameters(), info_dict['grad_clip'])
|
||||
if torch.isfinite(grad_norm):
|
||||
optimizer.step()
|
||||
else:
|
||||
logging.warning('get infinite grad_norm, check your code/data if it appears frequently')
|
||||
optimizer.zero_grad()
|
||||
scheduler.step()
|
||||
info_dict["lr"] = optimizer.param_groups[0]['lr']
|
||||
info_dict["grad_norm"] = grad_norm
|
||||
return info_dict
|
||||
|
||||
|
||||
def log_per_step(writer, info_dict):
|
||||
tag = info_dict["tag"]
|
||||
epoch = info_dict.get('epoch', 0)
|
||||
step = info_dict["step"]
|
||||
batch_idx = info_dict["batch_idx"]
|
||||
loss_dict = info_dict['loss_dict']
|
||||
rank = int(os.environ.get('RANK', 0))
|
||||
|
||||
# only rank 0 write to tensorboard to avoid multi-process write
|
||||
if writer is not None:
|
||||
if (info_dict['train_engine'] == 'deepspeed' and info_dict['is_gradient_accumulation_boundary'] is True) or \
|
||||
(info_dict['train_engine'] == 'torch_ddp' and (info_dict['batch_idx'] + 1) % info_dict['accum_grad'] == 0):
|
||||
for k in ['epoch', 'lr', 'grad_norm']:
|
||||
writer.add_scalar('{}/{}'.format(tag, k), info_dict[k], step + 1)
|
||||
for k, v in loss_dict.items():
|
||||
writer.add_scalar('{}/{}'.format(tag, k), v, step + 1)
|
||||
|
||||
# TRAIN & CV, Shell log (stdout)
|
||||
if (info_dict['batch_idx'] + 1) % info_dict['log_interval'] == 0:
|
||||
log_str = '{} Batch {}/{} '.format(tag, epoch, batch_idx + 1)
|
||||
for name, value in loss_dict.items():
|
||||
log_str += '{} {:.6f} '.format(name, value)
|
||||
if tag == "TRAIN":
|
||||
log_str += 'lr {:.8f} grad_norm {:.6f}'.format(
|
||||
info_dict["lr"], info_dict['grad_norm'])
|
||||
log_str += ' rank {}'.format(rank)
|
||||
logging.debug(log_str)
|
||||
|
||||
|
||||
def log_per_save(writer, info_dict):
|
||||
tag = info_dict["tag"]
|
||||
epoch = info_dict["epoch"]
|
||||
step = info_dict["step"]
|
||||
loss_dict = info_dict["loss_dict"]
|
||||
lr = info_dict['lr']
|
||||
rank = int(os.environ.get('RANK', 0))
|
||||
logging.info(
|
||||
'Epoch {} Step {} CV info lr {} {} rank {}'.format(
|
||||
epoch, step + 1, lr, rank, ' '.join(['{} {}'.format(k, v) for k, v in loss_dict.items()])))
|
||||
|
||||
if writer is not None:
|
||||
for k in ['epoch', 'lr']:
|
||||
writer.add_scalar('{}/{}'.format(tag, k), info_dict[k], step + 1)
|
||||
for k, v in loss_dict.items():
|
||||
writer.add_scalar('{}/{}'.format(tag, k), v, step + 1)
|
||||
@@ -0,0 +1,103 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Adapted from
|
||||
# https://github.com/huggingface/transformers/blob/v4.28.0/src/transformers/models/qwen2/modeling_qwen2.py
|
||||
# Copyright 2024 The Qwen team.
|
||||
# Copyright 2023 The vLLM team.
|
||||
# Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
|
||||
# and OPT implementations in this library. It has been modified from its
|
||||
# original forms to accommodate minor architectural differences compared
|
||||
# to GPT-NeoX and OPT used by the Meta AI team that trained the model.
|
||||
#
|
||||
# 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.
|
||||
"""Inference-only Qwen2 model compatible with HuggingFace weights."""
|
||||
from typing import Optional, Union, List, Tuple, Iterable
|
||||
from vllm.model_executor.models.qwen2 import *
|
||||
|
||||
|
||||
class CosyVoice2ForCausalLM(nn.Module, SupportsLoRA, SupportsPP):
|
||||
packed_modules_mapping = {
|
||||
"qkv_proj": [
|
||||
"q_proj",
|
||||
"k_proj",
|
||||
"v_proj",
|
||||
],
|
||||
"gate_up_proj": [
|
||||
"gate_proj",
|
||||
"up_proj",
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
config = vllm_config.model_config.hf_config
|
||||
quant_config = vllm_config.quant_config
|
||||
lora_config = vllm_config.lora_config
|
||||
|
||||
self.config = config
|
||||
self.lora_config = lora_config
|
||||
|
||||
self.quant_config = quant_config
|
||||
self.model = Qwen2Model(vllm_config=vllm_config,
|
||||
prefix=maybe_prefix(prefix, "model"))
|
||||
|
||||
if get_pp_group().is_last_rank:
|
||||
if config.tie_word_embeddings:
|
||||
self.lm_head = self.model.embed_tokens
|
||||
else:
|
||||
self.lm_head = ParallelLMHead(config.vocab_size,
|
||||
config.hidden_size,
|
||||
True,
|
||||
quant_config=quant_config,
|
||||
prefix=maybe_prefix(
|
||||
prefix, "lm_head"))
|
||||
else:
|
||||
self.lm_head = PPMissingLayer()
|
||||
|
||||
self.logits_processor = LogitsProcessor(config.vocab_size)
|
||||
|
||||
self.make_empty_intermediate_tensors = (
|
||||
self.model.make_empty_intermediate_tensors)
|
||||
|
||||
def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
return self.model.get_input_embeddings(input_ids)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
intermediate_tensors: Optional[IntermediateTensors] = None,
|
||||
inputs_embeds: Optional[torch.Tensor] = None,
|
||||
) -> Union[torch.Tensor, IntermediateTensors]:
|
||||
hidden_states = self.model(input_ids, positions, intermediate_tensors,
|
||||
inputs_embeds)
|
||||
return hidden_states
|
||||
|
||||
def compute_logits(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
) -> Optional[torch.Tensor]:
|
||||
# Updated for vLLM 0.13.0+ API (removed sampling_metadata parameter)
|
||||
logits = self.logits_processor(self.lm_head, hidden_states)
|
||||
return logits
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str,
|
||||
torch.Tensor]]) -> set[str]:
|
||||
loader = AutoWeightsLoader(
|
||||
self,
|
||||
skip_prefixes=(["lm_head."]
|
||||
if self.config.tie_word_embeddings else None),
|
||||
)
|
||||
return loader.load_weights(weights)
|
||||
@@ -0,0 +1 @@
|
||||
0.0.7.2
|
||||
@@ -0,0 +1,357 @@
|
||||
import tempfile
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
|
||||
import gradio as gr
|
||||
import soundfile as sf
|
||||
import torch
|
||||
|
||||
from matcha.cli import (
|
||||
MATCHA_URLS,
|
||||
VOCODER_URLS,
|
||||
assert_model_downloaded,
|
||||
get_device,
|
||||
load_matcha,
|
||||
load_vocoder,
|
||||
process_text,
|
||||
to_waveform,
|
||||
)
|
||||
from matcha.utils.utils import get_user_data_dir, plot_tensor
|
||||
|
||||
LOCATION = Path(get_user_data_dir())
|
||||
|
||||
args = Namespace(
|
||||
cpu=False,
|
||||
model="matcha_vctk",
|
||||
vocoder="hifigan_univ_v1",
|
||||
spk=0,
|
||||
)
|
||||
|
||||
CURRENTLY_LOADED_MODEL = args.model
|
||||
|
||||
|
||||
def MATCHA_TTS_LOC(x):
|
||||
return LOCATION / f"{x}.ckpt"
|
||||
|
||||
|
||||
def VOCODER_LOC(x):
|
||||
return LOCATION / f"{x}"
|
||||
|
||||
|
||||
LOGO_URL = "https://shivammehta25.github.io/Matcha-TTS/images/logo.png"
|
||||
RADIO_OPTIONS = {
|
||||
"Multi Speaker (VCTK)": {
|
||||
"model": "matcha_vctk",
|
||||
"vocoder": "hifigan_univ_v1",
|
||||
},
|
||||
"Single Speaker (LJ Speech)": {
|
||||
"model": "matcha_ljspeech",
|
||||
"vocoder": "hifigan_T2_v1",
|
||||
},
|
||||
}
|
||||
|
||||
# Ensure all the required models are downloaded
|
||||
assert_model_downloaded(MATCHA_TTS_LOC("matcha_ljspeech"), MATCHA_URLS["matcha_ljspeech"])
|
||||
assert_model_downloaded(VOCODER_LOC("hifigan_T2_v1"), VOCODER_URLS["hifigan_T2_v1"])
|
||||
assert_model_downloaded(MATCHA_TTS_LOC("matcha_vctk"), MATCHA_URLS["matcha_vctk"])
|
||||
assert_model_downloaded(VOCODER_LOC("hifigan_univ_v1"), VOCODER_URLS["hifigan_univ_v1"])
|
||||
|
||||
device = get_device(args)
|
||||
|
||||
# Load default model
|
||||
model = load_matcha(args.model, MATCHA_TTS_LOC(args.model), device)
|
||||
vocoder, denoiser = load_vocoder(args.vocoder, VOCODER_LOC(args.vocoder), device)
|
||||
|
||||
|
||||
def load_model(model_name, vocoder_name):
|
||||
model = load_matcha(model_name, MATCHA_TTS_LOC(model_name), device)
|
||||
vocoder, denoiser = load_vocoder(vocoder_name, VOCODER_LOC(vocoder_name), device)
|
||||
return model, vocoder, denoiser
|
||||
|
||||
|
||||
def load_model_ui(model_type, textbox):
|
||||
model_name, vocoder_name = RADIO_OPTIONS[model_type]["model"], RADIO_OPTIONS[model_type]["vocoder"]
|
||||
|
||||
global model, vocoder, denoiser, CURRENTLY_LOADED_MODEL # pylint: disable=global-statement
|
||||
if CURRENTLY_LOADED_MODEL != model_name:
|
||||
model, vocoder, denoiser = load_model(model_name, vocoder_name)
|
||||
CURRENTLY_LOADED_MODEL = model_name
|
||||
|
||||
if model_name == "matcha_ljspeech":
|
||||
spk_slider = gr.update(visible=False, value=-1)
|
||||
single_speaker_examples = gr.update(visible=True)
|
||||
multi_speaker_examples = gr.update(visible=False)
|
||||
length_scale = gr.update(value=0.95)
|
||||
else:
|
||||
spk_slider = gr.update(visible=True, value=0)
|
||||
single_speaker_examples = gr.update(visible=False)
|
||||
multi_speaker_examples = gr.update(visible=True)
|
||||
length_scale = gr.update(value=0.85)
|
||||
|
||||
return (
|
||||
textbox,
|
||||
gr.update(interactive=True),
|
||||
spk_slider,
|
||||
single_speaker_examples,
|
||||
multi_speaker_examples,
|
||||
length_scale,
|
||||
)
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def process_text_gradio(text):
|
||||
output = process_text(1, text, device)
|
||||
return output["x_phones"][1::2], output["x"], output["x_lengths"]
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def synthesise_mel(text, text_length, n_timesteps, temperature, length_scale, spk):
|
||||
spk = torch.tensor([spk], device=device, dtype=torch.long) if spk >= 0 else None
|
||||
output = model.synthesise(
|
||||
text,
|
||||
text_length,
|
||||
n_timesteps=n_timesteps,
|
||||
temperature=temperature,
|
||||
spks=spk,
|
||||
length_scale=length_scale,
|
||||
)
|
||||
output["waveform"] = to_waveform(output["mel"], vocoder, denoiser)
|
||||
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as fp:
|
||||
sf.write(fp.name, output["waveform"], 22050, "PCM_24")
|
||||
|
||||
return fp.name, plot_tensor(output["mel"].squeeze().cpu().numpy())
|
||||
|
||||
|
||||
def multispeaker_example_cacher(text, n_timesteps, mel_temp, length_scale, spk):
|
||||
global CURRENTLY_LOADED_MODEL # pylint: disable=global-statement
|
||||
if CURRENTLY_LOADED_MODEL != "matcha_vctk":
|
||||
global model, vocoder, denoiser # pylint: disable=global-statement
|
||||
model, vocoder, denoiser = load_model("matcha_vctk", "hifigan_univ_v1")
|
||||
CURRENTLY_LOADED_MODEL = "matcha_vctk"
|
||||
|
||||
phones, text, text_lengths = process_text_gradio(text)
|
||||
audio, mel_spectrogram = synthesise_mel(text, text_lengths, n_timesteps, mel_temp, length_scale, spk)
|
||||
return phones, audio, mel_spectrogram
|
||||
|
||||
|
||||
def ljspeech_example_cacher(text, n_timesteps, mel_temp, length_scale, spk=-1):
|
||||
global CURRENTLY_LOADED_MODEL # pylint: disable=global-statement
|
||||
if CURRENTLY_LOADED_MODEL != "matcha_ljspeech":
|
||||
global model, vocoder, denoiser # pylint: disable=global-statement
|
||||
model, vocoder, denoiser = load_model("matcha_ljspeech", "hifigan_T2_v1")
|
||||
CURRENTLY_LOADED_MODEL = "matcha_ljspeech"
|
||||
|
||||
phones, text, text_lengths = process_text_gradio(text)
|
||||
audio, mel_spectrogram = synthesise_mel(text, text_lengths, n_timesteps, mel_temp, length_scale, spk)
|
||||
return phones, audio, mel_spectrogram
|
||||
|
||||
|
||||
def main():
|
||||
description = """# 🍵 Matcha-TTS: A fast TTS architecture with conditional flow matching
|
||||
### [Shivam Mehta](https://www.kth.se/profile/smehta), [Ruibo Tu](https://www.kth.se/profile/ruibo), [Jonas Beskow](https://www.kth.se/profile/beskow), [Éva Székely](https://www.kth.se/profile/szekely), and [Gustav Eje Henter](https://people.kth.se/~ghe/)
|
||||
We propose 🍵 Matcha-TTS, a new approach to non-autoregressive neural TTS, that uses conditional flow matching (similar to rectified flows) to speed up ODE-based speech synthesis. Our method:
|
||||
|
||||
|
||||
* Is probabilistic
|
||||
* Has compact memory footprint
|
||||
* Sounds highly natural
|
||||
* Is very fast to synthesise from
|
||||
|
||||
|
||||
Check out our [demo page](https://shivammehta25.github.io/Matcha-TTS). Read our [arXiv preprint for more details](https://arxiv.org/abs/2309.03199).
|
||||
Code is available in our [GitHub repository](https://github.com/shivammehta25/Matcha-TTS), along with pre-trained models.
|
||||
|
||||
Cached examples are available at the bottom of the page.
|
||||
"""
|
||||
|
||||
with gr.Blocks(title="🍵 Matcha-TTS: A fast TTS architecture with conditional flow matching") as demo:
|
||||
processed_text = gr.State(value=None)
|
||||
processed_text_len = gr.State(value=None)
|
||||
|
||||
with gr.Box():
|
||||
with gr.Row():
|
||||
gr.Markdown(description, scale=3)
|
||||
with gr.Column():
|
||||
gr.Image(LOGO_URL, label="Matcha-TTS logo", height=50, width=50, scale=1, show_label=False)
|
||||
html = '<br><iframe width="560" height="315" src="https://www.youtube.com/embed/xmvJkz3bqw0?si=jN7ILyDsbPwJCGoa" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>'
|
||||
gr.HTML(html)
|
||||
|
||||
with gr.Box():
|
||||
radio_options = list(RADIO_OPTIONS.keys())
|
||||
model_type = gr.Radio(
|
||||
radio_options, value=radio_options[0], label="Choose a Model", interactive=True, container=False
|
||||
)
|
||||
|
||||
with gr.Row():
|
||||
gr.Markdown("# Text Input")
|
||||
with gr.Row():
|
||||
text = gr.Textbox(value="", lines=2, label="Text to synthesise", scale=3)
|
||||
spk_slider = gr.Slider(
|
||||
minimum=0, maximum=107, step=1, value=args.spk, label="Speaker ID", interactive=True, scale=1
|
||||
)
|
||||
|
||||
with gr.Row():
|
||||
gr.Markdown("### Hyper parameters")
|
||||
with gr.Row():
|
||||
n_timesteps = gr.Slider(
|
||||
label="Number of ODE steps",
|
||||
minimum=1,
|
||||
maximum=100,
|
||||
step=1,
|
||||
value=10,
|
||||
interactive=True,
|
||||
)
|
||||
length_scale = gr.Slider(
|
||||
label="Length scale (Speaking rate)",
|
||||
minimum=0.5,
|
||||
maximum=1.5,
|
||||
step=0.05,
|
||||
value=1.0,
|
||||
interactive=True,
|
||||
)
|
||||
mel_temp = gr.Slider(
|
||||
label="Sampling temperature",
|
||||
minimum=0.00,
|
||||
maximum=2.001,
|
||||
step=0.16675,
|
||||
value=0.667,
|
||||
interactive=True,
|
||||
)
|
||||
|
||||
synth_btn = gr.Button("Synthesise")
|
||||
|
||||
with gr.Box():
|
||||
with gr.Row():
|
||||
gr.Markdown("### Phonetised text")
|
||||
phonetised_text = gr.Textbox(interactive=False, scale=10, label="Phonetised text")
|
||||
|
||||
with gr.Box():
|
||||
with gr.Row():
|
||||
mel_spectrogram = gr.Image(interactive=False, label="mel spectrogram")
|
||||
|
||||
# with gr.Row():
|
||||
audio = gr.Audio(interactive=False, label="Audio")
|
||||
|
||||
with gr.Row(visible=False) as example_row_lj_speech:
|
||||
examples = gr.Examples( # pylint: disable=unused-variable
|
||||
examples=[
|
||||
[
|
||||
"We propose Matcha-TTS, a new approach to non-autoregressive neural TTS, that uses conditional flow matching (similar to rectified flows) to speed up O D E-based speech synthesis.",
|
||||
50,
|
||||
0.677,
|
||||
0.95,
|
||||
],
|
||||
[
|
||||
"The Secret Service believed that it was very doubtful that any President would ride regularly in a vehicle with a fixed top, even though transparent.",
|
||||
2,
|
||||
0.677,
|
||||
0.95,
|
||||
],
|
||||
[
|
||||
"The Secret Service believed that it was very doubtful that any President would ride regularly in a vehicle with a fixed top, even though transparent.",
|
||||
4,
|
||||
0.677,
|
||||
0.95,
|
||||
],
|
||||
[
|
||||
"The Secret Service believed that it was very doubtful that any President would ride regularly in a vehicle with a fixed top, even though transparent.",
|
||||
10,
|
||||
0.677,
|
||||
0.95,
|
||||
],
|
||||
[
|
||||
"The Secret Service believed that it was very doubtful that any President would ride regularly in a vehicle with a fixed top, even though transparent.",
|
||||
50,
|
||||
0.677,
|
||||
0.95,
|
||||
],
|
||||
[
|
||||
"The narrative of these events is based largely on the recollections of the participants.",
|
||||
10,
|
||||
0.677,
|
||||
0.95,
|
||||
],
|
||||
[
|
||||
"The jury did not believe him, and the verdict was for the defendants.",
|
||||
10,
|
||||
0.677,
|
||||
0.95,
|
||||
],
|
||||
],
|
||||
fn=ljspeech_example_cacher,
|
||||
inputs=[text, n_timesteps, mel_temp, length_scale],
|
||||
outputs=[phonetised_text, audio, mel_spectrogram],
|
||||
cache_examples=True,
|
||||
)
|
||||
|
||||
with gr.Row() as example_row_multispeaker:
|
||||
multi_speaker_examples = gr.Examples( # pylint: disable=unused-variable
|
||||
examples=[
|
||||
[
|
||||
"Hello everyone! I am speaker 0 and I am here to tell you that Matcha-TTS is amazing!",
|
||||
10,
|
||||
0.677,
|
||||
0.85,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"Hello everyone! I am speaker 16 and I am here to tell you that Matcha-TTS is amazing!",
|
||||
10,
|
||||
0.677,
|
||||
0.85,
|
||||
16,
|
||||
],
|
||||
[
|
||||
"Hello everyone! I am speaker 44 and I am here to tell you that Matcha-TTS is amazing!",
|
||||
50,
|
||||
0.677,
|
||||
0.85,
|
||||
44,
|
||||
],
|
||||
[
|
||||
"Hello everyone! I am speaker 45 and I am here to tell you that Matcha-TTS is amazing!",
|
||||
50,
|
||||
0.677,
|
||||
0.85,
|
||||
45,
|
||||
],
|
||||
[
|
||||
"Hello everyone! I am speaker 58 and I am here to tell you that Matcha-TTS is amazing!",
|
||||
4,
|
||||
0.677,
|
||||
0.85,
|
||||
58,
|
||||
],
|
||||
],
|
||||
fn=multispeaker_example_cacher,
|
||||
inputs=[text, n_timesteps, mel_temp, length_scale, spk_slider],
|
||||
outputs=[phonetised_text, audio, mel_spectrogram],
|
||||
cache_examples=True,
|
||||
label="Multi Speaker Examples",
|
||||
)
|
||||
|
||||
model_type.change(lambda x: gr.update(interactive=False), inputs=[synth_btn], outputs=[synth_btn]).then(
|
||||
load_model_ui,
|
||||
inputs=[model_type, text],
|
||||
outputs=[text, synth_btn, spk_slider, example_row_lj_speech, example_row_multispeaker, length_scale],
|
||||
)
|
||||
|
||||
synth_btn.click(
|
||||
fn=process_text_gradio,
|
||||
inputs=[
|
||||
text,
|
||||
],
|
||||
outputs=[phonetised_text, processed_text, processed_text_len],
|
||||
api_name="matcha_tts",
|
||||
queue=True,
|
||||
).then(
|
||||
fn=synthesise_mel,
|
||||
inputs=[processed_text, processed_text_len, n_timesteps, mel_temp, length_scale, spk_slider],
|
||||
outputs=[audio, mel_spectrogram],
|
||||
)
|
||||
|
||||
demo.queue().launch(share=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,419 @@
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import os
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
import torch
|
||||
|
||||
from matcha.hifigan.config import v1
|
||||
from matcha.hifigan.denoiser import Denoiser
|
||||
from matcha.hifigan.env import AttrDict
|
||||
from matcha.hifigan.models import Generator as HiFiGAN
|
||||
from matcha.models.matcha_tts import MatchaTTS
|
||||
from matcha.text import sequence_to_text, text_to_sequence
|
||||
from matcha.utils.utils import assert_model_downloaded, get_user_data_dir, intersperse
|
||||
|
||||
MATCHA_URLS = {
|
||||
"matcha_ljspeech": "https://github.com/shivammehta25/Matcha-TTS-checkpoints/releases/download/v1.0/matcha_ljspeech.ckpt",
|
||||
"matcha_vctk": "https://github.com/shivammehta25/Matcha-TTS-checkpoints/releases/download/v1.0/matcha_vctk.ckpt",
|
||||
}
|
||||
|
||||
VOCODER_URLS = {
|
||||
"hifigan_T2_v1": "https://github.com/shivammehta25/Matcha-TTS-checkpoints/releases/download/v1.0/generator_v1", # Old url: https://drive.google.com/file/d/14NENd4equCBLyyCSke114Mv6YR_j_uFs/view?usp=drive_link
|
||||
"hifigan_univ_v1": "https://github.com/shivammehta25/Matcha-TTS-checkpoints/releases/download/v1.0/g_02500000", # Old url: https://drive.google.com/file/d/1qpgI41wNXFcH-iKq1Y42JlBC9j0je8PW/view?usp=drive_link
|
||||
}
|
||||
|
||||
MULTISPEAKER_MODEL = {
|
||||
"matcha_vctk": {"vocoder": "hifigan_univ_v1", "speaking_rate": 0.85, "spk": 0, "spk_range": (0, 107)}
|
||||
}
|
||||
|
||||
SINGLESPEAKER_MODEL = {"matcha_ljspeech": {"vocoder": "hifigan_T2_v1", "speaking_rate": 0.95, "spk": None}}
|
||||
|
||||
|
||||
def plot_spectrogram_to_numpy(spectrogram, filename):
|
||||
fig, ax = plt.subplots(figsize=(12, 3))
|
||||
im = ax.imshow(spectrogram, aspect="auto", origin="lower", interpolation="none")
|
||||
plt.colorbar(im, ax=ax)
|
||||
plt.xlabel("Frames")
|
||||
plt.ylabel("Channels")
|
||||
plt.title("Synthesised Mel-Spectrogram")
|
||||
fig.canvas.draw()
|
||||
plt.savefig(filename)
|
||||
|
||||
|
||||
def process_text(i: int, text: str, device: torch.device):
|
||||
print(f"[{i}] - Input text: {text}")
|
||||
x = torch.tensor(
|
||||
intersperse(text_to_sequence(text, ["english_cleaners2"])[0], 0),
|
||||
dtype=torch.long,
|
||||
device=device,
|
||||
)[None]
|
||||
x_lengths = torch.tensor([x.shape[-1]], dtype=torch.long, device=device)
|
||||
x_phones = sequence_to_text(x.squeeze(0).tolist())
|
||||
print(f"[{i}] - Phonetised text: {x_phones[1::2]}")
|
||||
|
||||
return {"x_orig": text, "x": x, "x_lengths": x_lengths, "x_phones": x_phones}
|
||||
|
||||
|
||||
def get_texts(args):
|
||||
if args.text:
|
||||
texts = [args.text]
|
||||
else:
|
||||
with open(args.file, encoding="utf-8") as f:
|
||||
texts = f.readlines()
|
||||
return texts
|
||||
|
||||
|
||||
def assert_required_models_available(args):
|
||||
save_dir = get_user_data_dir()
|
||||
if not hasattr(args, "checkpoint_path") and args.checkpoint_path is None:
|
||||
model_path = args.checkpoint_path
|
||||
else:
|
||||
model_path = save_dir / f"{args.model}.ckpt"
|
||||
assert_model_downloaded(model_path, MATCHA_URLS[args.model])
|
||||
|
||||
vocoder_path = save_dir / f"{args.vocoder}"
|
||||
assert_model_downloaded(vocoder_path, VOCODER_URLS[args.vocoder])
|
||||
return {"matcha": model_path, "vocoder": vocoder_path}
|
||||
|
||||
|
||||
def load_hifigan(checkpoint_path, device):
|
||||
h = AttrDict(v1)
|
||||
hifigan = HiFiGAN(h).to(device)
|
||||
hifigan.load_state_dict(torch.load(checkpoint_path, map_location=device)["generator"])
|
||||
_ = hifigan.eval()
|
||||
hifigan.remove_weight_norm()
|
||||
return hifigan
|
||||
|
||||
|
||||
def load_vocoder(vocoder_name, checkpoint_path, device):
|
||||
print(f"[!] Loading {vocoder_name}!")
|
||||
vocoder = None
|
||||
if vocoder_name in ("hifigan_T2_v1", "hifigan_univ_v1"):
|
||||
vocoder = load_hifigan(checkpoint_path, device)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Vocoder {vocoder_name} not implemented! define a load_<<vocoder_name>> method for it"
|
||||
)
|
||||
|
||||
denoiser = Denoiser(vocoder, mode="zeros")
|
||||
print(f"[+] {vocoder_name} loaded!")
|
||||
return vocoder, denoiser
|
||||
|
||||
|
||||
def load_matcha(model_name, checkpoint_path, device):
|
||||
print(f"[!] Loading {model_name}!")
|
||||
model = MatchaTTS.load_from_checkpoint(checkpoint_path, map_location=device)
|
||||
_ = model.eval()
|
||||
|
||||
print(f"[+] {model_name} loaded!")
|
||||
return model
|
||||
|
||||
|
||||
def to_waveform(mel, vocoder, denoiser=None, denoiser_strength=0.00025):
|
||||
audio = vocoder(mel).clamp(-1, 1)
|
||||
if denoiser is not None:
|
||||
audio = denoiser(audio.squeeze(), strength=denoiser_strength).cpu().squeeze()
|
||||
|
||||
return audio.cpu().squeeze()
|
||||
|
||||
|
||||
def save_to_folder(filename: str, output: dict, folder: str):
|
||||
folder = Path(folder)
|
||||
folder.mkdir(exist_ok=True, parents=True)
|
||||
plot_spectrogram_to_numpy(np.array(output["mel"].squeeze().float().cpu()), f"{filename}.png")
|
||||
np.save(folder / f"{filename}", output["mel"].cpu().numpy())
|
||||
sf.write(folder / f"{filename}.wav", output["waveform"], 22050, "PCM_24")
|
||||
return folder.resolve() / f"{filename}.wav"
|
||||
|
||||
|
||||
def validate_args(args):
|
||||
assert (
|
||||
args.text or args.file
|
||||
), "Either text or file must be provided Matcha-T(ea)TTS need sometext to whisk the waveforms."
|
||||
assert args.temperature >= 0, "Sampling temperature cannot be negative"
|
||||
assert args.steps > 0, "Number of ODE steps must be greater than 0"
|
||||
|
||||
if args.checkpoint_path is None:
|
||||
# When using pretrained models
|
||||
if args.model in SINGLESPEAKER_MODEL:
|
||||
args = validate_args_for_single_speaker_model(args)
|
||||
|
||||
if args.model in MULTISPEAKER_MODEL:
|
||||
args = validate_args_for_multispeaker_model(args)
|
||||
else:
|
||||
# When using a custom model
|
||||
if args.vocoder != "hifigan_univ_v1":
|
||||
warn_ = "[-] Using custom model checkpoint! I would suggest passing --vocoder hifigan_univ_v1, unless the custom model is trained on LJ Speech."
|
||||
warnings.warn(warn_, UserWarning)
|
||||
if args.speaking_rate is None:
|
||||
args.speaking_rate = 1.0
|
||||
|
||||
if args.batched:
|
||||
assert args.batch_size > 0, "Batch size must be greater than 0"
|
||||
assert args.speaking_rate > 0, "Speaking rate must be greater than 0"
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def validate_args_for_multispeaker_model(args):
|
||||
if args.vocoder is not None:
|
||||
if args.vocoder != MULTISPEAKER_MODEL[args.model]["vocoder"]:
|
||||
warn_ = f"[-] Using {args.model} model! I would suggest passing --vocoder {MULTISPEAKER_MODEL[args.model]['vocoder']}"
|
||||
warnings.warn(warn_, UserWarning)
|
||||
else:
|
||||
args.vocoder = MULTISPEAKER_MODEL[args.model]["vocoder"]
|
||||
|
||||
if args.speaking_rate is None:
|
||||
args.speaking_rate = MULTISPEAKER_MODEL[args.model]["speaking_rate"]
|
||||
|
||||
spk_range = MULTISPEAKER_MODEL[args.model]["spk_range"]
|
||||
if args.spk is not None:
|
||||
assert (
|
||||
args.spk >= spk_range[0] and args.spk <= spk_range[-1]
|
||||
), f"Speaker ID must be between {spk_range} for this model."
|
||||
else:
|
||||
available_spk_id = MULTISPEAKER_MODEL[args.model]["spk"]
|
||||
warn_ = f"[!] Speaker ID not provided! Using speaker ID {available_spk_id}"
|
||||
warnings.warn(warn_, UserWarning)
|
||||
args.spk = available_spk_id
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def validate_args_for_single_speaker_model(args):
|
||||
if args.vocoder is not None:
|
||||
if args.vocoder != SINGLESPEAKER_MODEL[args.model]["vocoder"]:
|
||||
warn_ = f"[-] Using {args.model} model! I would suggest passing --vocoder {SINGLESPEAKER_MODEL[args.model]['vocoder']}"
|
||||
warnings.warn(warn_, UserWarning)
|
||||
else:
|
||||
args.vocoder = SINGLESPEAKER_MODEL[args.model]["vocoder"]
|
||||
|
||||
if args.speaking_rate is None:
|
||||
args.speaking_rate = SINGLESPEAKER_MODEL[args.model]["speaking_rate"]
|
||||
|
||||
if args.spk != SINGLESPEAKER_MODEL[args.model]["spk"]:
|
||||
warn_ = f"[-] Ignoring speaker id {args.spk} for {args.model}"
|
||||
warnings.warn(warn_, UserWarning)
|
||||
args.spk = SINGLESPEAKER_MODEL[args.model]["spk"]
|
||||
|
||||
return args
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def cli():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=" 🍵 Matcha-TTS: A fast TTS architecture with conditional flow matching"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
type=str,
|
||||
default="matcha_ljspeech",
|
||||
help="Model to use",
|
||||
choices=MATCHA_URLS.keys(),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--checkpoint_path",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to the custom model checkpoint",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--vocoder",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Vocoder to use (default: will use the one suggested with the pretrained model))",
|
||||
choices=VOCODER_URLS.keys(),
|
||||
)
|
||||
parser.add_argument("--text", type=str, default=None, help="Text to synthesize")
|
||||
parser.add_argument("--file", type=str, default=None, help="Text file to synthesize")
|
||||
parser.add_argument("--spk", type=int, default=None, help="Speaker ID")
|
||||
parser.add_argument(
|
||||
"--temperature",
|
||||
type=float,
|
||||
default=0.667,
|
||||
help="Variance of the x0 noise (default: 0.667)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--speaking_rate",
|
||||
type=float,
|
||||
default=None,
|
||||
help="change the speaking rate, a higher value means slower speaking rate (default: 1.0)",
|
||||
)
|
||||
parser.add_argument("--steps", type=int, default=10, help="Number of ODE steps (default: 10)")
|
||||
parser.add_argument("--cpu", action="store_true", help="Use CPU for inference (default: use GPU if available)")
|
||||
parser.add_argument(
|
||||
"--denoiser_strength",
|
||||
type=float,
|
||||
default=0.00025,
|
||||
help="Strength of the vocoder bias denoiser (default: 0.00025)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output_folder",
|
||||
type=str,
|
||||
default=os.getcwd(),
|
||||
help="Output folder to save results (default: current dir)",
|
||||
)
|
||||
parser.add_argument("--batched", action="store_true", help="Batched inference (default: False)")
|
||||
parser.add_argument(
|
||||
"--batch_size", type=int, default=32, help="Batch size only useful when --batched (default: 32)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
args = validate_args(args)
|
||||
device = get_device(args)
|
||||
print_config(args)
|
||||
paths = assert_required_models_available(args)
|
||||
|
||||
if args.checkpoint_path is not None:
|
||||
print(f"[🍵] Loading custom model from {args.checkpoint_path}")
|
||||
paths["matcha"] = args.checkpoint_path
|
||||
args.model = "custom_model"
|
||||
|
||||
model = load_matcha(args.model, paths["matcha"], device)
|
||||
vocoder, denoiser = load_vocoder(args.vocoder, paths["vocoder"], device)
|
||||
|
||||
texts = get_texts(args)
|
||||
|
||||
spk = torch.tensor([args.spk], device=device, dtype=torch.long) if args.spk is not None else None
|
||||
if len(texts) == 1 or not args.batched:
|
||||
unbatched_synthesis(args, device, model, vocoder, denoiser, texts, spk)
|
||||
else:
|
||||
batched_synthesis(args, device, model, vocoder, denoiser, texts, spk)
|
||||
|
||||
|
||||
class BatchedSynthesisDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, processed_texts):
|
||||
self.processed_texts = processed_texts
|
||||
|
||||
def __len__(self):
|
||||
return len(self.processed_texts)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.processed_texts[idx]
|
||||
|
||||
|
||||
def batched_collate_fn(batch):
|
||||
x = []
|
||||
x_lengths = []
|
||||
|
||||
for b in batch:
|
||||
x.append(b["x"].squeeze(0))
|
||||
x_lengths.append(b["x_lengths"])
|
||||
|
||||
x = torch.nn.utils.rnn.pad_sequence(x, batch_first=True)
|
||||
x_lengths = torch.concat(x_lengths, dim=0)
|
||||
return {"x": x, "x_lengths": x_lengths}
|
||||
|
||||
|
||||
def batched_synthesis(args, device, model, vocoder, denoiser, texts, spk):
|
||||
total_rtf = []
|
||||
total_rtf_w = []
|
||||
processed_text = [process_text(i, text, "cpu") for i, text in enumerate(texts)]
|
||||
dataloader = torch.utils.data.DataLoader(
|
||||
BatchedSynthesisDataset(processed_text),
|
||||
batch_size=args.batch_size,
|
||||
collate_fn=batched_collate_fn,
|
||||
num_workers=8,
|
||||
)
|
||||
for i, batch in enumerate(dataloader):
|
||||
i = i + 1
|
||||
start_t = dt.datetime.now()
|
||||
b = batch["x"].shape[0]
|
||||
output = model.synthesise(
|
||||
batch["x"].to(device),
|
||||
batch["x_lengths"].to(device),
|
||||
n_timesteps=args.steps,
|
||||
temperature=args.temperature,
|
||||
spks=spk.expand(b) if spk is not None else spk,
|
||||
length_scale=args.speaking_rate,
|
||||
)
|
||||
|
||||
output["waveform"] = to_waveform(output["mel"], vocoder, denoiser, args.denoiser_strength)
|
||||
t = (dt.datetime.now() - start_t).total_seconds()
|
||||
rtf_w = t * 22050 / (output["waveform"].shape[-1])
|
||||
print(f"[🍵-Batch: {i}] Matcha-TTS RTF: {output['rtf']:.4f}")
|
||||
print(f"[🍵-Batch: {i}] Matcha-TTS + VOCODER RTF: {rtf_w:.4f}")
|
||||
total_rtf.append(output["rtf"])
|
||||
total_rtf_w.append(rtf_w)
|
||||
for j in range(output["mel"].shape[0]):
|
||||
base_name = f"utterance_{j:03d}_speaker_{args.spk:03d}" if args.spk is not None else f"utterance_{j:03d}"
|
||||
length = output["mel_lengths"][j]
|
||||
new_dict = {"mel": output["mel"][j][:, :length], "waveform": output["waveform"][j][: length * 256]}
|
||||
location = save_to_folder(base_name, new_dict, args.output_folder)
|
||||
print(f"[🍵-{j}] Waveform saved: {location}")
|
||||
|
||||
print("".join(["="] * 100))
|
||||
print(f"[🍵] Average Matcha-TTS RTF: {np.mean(total_rtf):.4f} ± {np.std(total_rtf)}")
|
||||
print(f"[🍵] Average Matcha-TTS + VOCODER RTF: {np.mean(total_rtf_w):.4f} ± {np.std(total_rtf_w)}")
|
||||
print("[🍵] Enjoy the freshly whisked 🍵 Matcha-TTS!")
|
||||
|
||||
|
||||
def unbatched_synthesis(args, device, model, vocoder, denoiser, texts, spk):
|
||||
total_rtf = []
|
||||
total_rtf_w = []
|
||||
for i, text in enumerate(texts):
|
||||
i = i + 1
|
||||
base_name = f"utterance_{i:03d}_speaker_{args.spk:03d}" if args.spk is not None else f"utterance_{i:03d}"
|
||||
|
||||
print("".join(["="] * 100))
|
||||
text = text.strip()
|
||||
text_processed = process_text(i, text, device)
|
||||
|
||||
print(f"[🍵] Whisking Matcha-T(ea)TS for: {i}")
|
||||
start_t = dt.datetime.now()
|
||||
output = model.synthesise(
|
||||
text_processed["x"],
|
||||
text_processed["x_lengths"],
|
||||
n_timesteps=args.steps,
|
||||
temperature=args.temperature,
|
||||
spks=spk,
|
||||
length_scale=args.speaking_rate,
|
||||
)
|
||||
output["waveform"] = to_waveform(output["mel"], vocoder, denoiser, args.denoiser_strength)
|
||||
# RTF with HiFiGAN
|
||||
t = (dt.datetime.now() - start_t).total_seconds()
|
||||
rtf_w = t * 22050 / (output["waveform"].shape[-1])
|
||||
print(f"[🍵-{i}] Matcha-TTS RTF: {output['rtf']:.4f}")
|
||||
print(f"[🍵-{i}] Matcha-TTS + VOCODER RTF: {rtf_w:.4f}")
|
||||
total_rtf.append(output["rtf"])
|
||||
total_rtf_w.append(rtf_w)
|
||||
|
||||
location = save_to_folder(base_name, output, args.output_folder)
|
||||
print(f"[+] Waveform saved: {location}")
|
||||
|
||||
print("".join(["="] * 100))
|
||||
print(f"[🍵] Average Matcha-TTS RTF: {np.mean(total_rtf):.4f} ± {np.std(total_rtf)}")
|
||||
print(f"[🍵] Average Matcha-TTS + VOCODER RTF: {np.mean(total_rtf_w):.4f} ± {np.std(total_rtf_w)}")
|
||||
print("[🍵] Enjoy the freshly whisked 🍵 Matcha-TTS!")
|
||||
|
||||
|
||||
def print_config(args):
|
||||
print("[!] Configurations: ")
|
||||
print(f"\t- Model: {args.model}")
|
||||
print(f"\t- Vocoder: {args.vocoder}")
|
||||
print(f"\t- Temperature: {args.temperature}")
|
||||
print(f"\t- Speaking rate: {args.speaking_rate}")
|
||||
print(f"\t- Number of ODE steps: {args.steps}")
|
||||
print(f"\t- Speaker: {args.spk}")
|
||||
|
||||
|
||||
def get_device(args):
|
||||
if torch.cuda.is_available() and not args.cpu:
|
||||
print("[+] GPU Available! Using GPU")
|
||||
device = torch.device("cuda")
|
||||
else:
|
||||
print("[-] GPU not available or forced CPU run! Using CPU")
|
||||
device = torch.device("cpu")
|
||||
return device
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
import random
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torchaudio as ta
|
||||
from lightning import LightningDataModule
|
||||
from torch.utils.data.dataloader import DataLoader
|
||||
|
||||
from matcha.text import text_to_sequence
|
||||
from matcha.utils.audio import mel_spectrogram
|
||||
from matcha.utils.model import fix_len_compatibility, normalize
|
||||
from matcha.utils.utils import intersperse
|
||||
|
||||
|
||||
def parse_filelist(filelist_path, split_char="|"):
|
||||
with open(filelist_path, encoding="utf-8") as f:
|
||||
filepaths_and_text = [line.strip().split(split_char) for line in f]
|
||||
return filepaths_and_text
|
||||
|
||||
|
||||
class TextMelDataModule(LightningDataModule):
|
||||
def __init__( # pylint: disable=unused-argument
|
||||
self,
|
||||
name,
|
||||
train_filelist_path,
|
||||
valid_filelist_path,
|
||||
batch_size,
|
||||
num_workers,
|
||||
pin_memory,
|
||||
cleaners,
|
||||
add_blank,
|
||||
n_spks,
|
||||
n_fft,
|
||||
n_feats,
|
||||
sample_rate,
|
||||
hop_length,
|
||||
win_length,
|
||||
f_min,
|
||||
f_max,
|
||||
data_statistics,
|
||||
seed,
|
||||
load_durations,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
# this line allows to access init params with 'self.hparams' attribute
|
||||
# also ensures init params will be stored in ckpt
|
||||
self.save_hyperparameters(logger=False)
|
||||
|
||||
def setup(self, stage: Optional[str] = None): # pylint: disable=unused-argument
|
||||
"""Load data. Set variables: `self.data_train`, `self.data_val`, `self.data_test`.
|
||||
|
||||
This method is called by lightning with both `trainer.fit()` and `trainer.test()`, so be
|
||||
careful not to execute things like random split twice!
|
||||
"""
|
||||
# load and split datasets only if not loaded already
|
||||
|
||||
self.trainset = TextMelDataset( # pylint: disable=attribute-defined-outside-init
|
||||
self.hparams.train_filelist_path,
|
||||
self.hparams.n_spks,
|
||||
self.hparams.cleaners,
|
||||
self.hparams.add_blank,
|
||||
self.hparams.n_fft,
|
||||
self.hparams.n_feats,
|
||||
self.hparams.sample_rate,
|
||||
self.hparams.hop_length,
|
||||
self.hparams.win_length,
|
||||
self.hparams.f_min,
|
||||
self.hparams.f_max,
|
||||
self.hparams.data_statistics,
|
||||
self.hparams.seed,
|
||||
self.hparams.load_durations,
|
||||
)
|
||||
self.validset = TextMelDataset( # pylint: disable=attribute-defined-outside-init
|
||||
self.hparams.valid_filelist_path,
|
||||
self.hparams.n_spks,
|
||||
self.hparams.cleaners,
|
||||
self.hparams.add_blank,
|
||||
self.hparams.n_fft,
|
||||
self.hparams.n_feats,
|
||||
self.hparams.sample_rate,
|
||||
self.hparams.hop_length,
|
||||
self.hparams.win_length,
|
||||
self.hparams.f_min,
|
||||
self.hparams.f_max,
|
||||
self.hparams.data_statistics,
|
||||
self.hparams.seed,
|
||||
self.hparams.load_durations,
|
||||
)
|
||||
|
||||
def train_dataloader(self):
|
||||
return DataLoader(
|
||||
dataset=self.trainset,
|
||||
batch_size=self.hparams.batch_size,
|
||||
num_workers=self.hparams.num_workers,
|
||||
pin_memory=self.hparams.pin_memory,
|
||||
shuffle=True,
|
||||
collate_fn=TextMelBatchCollate(self.hparams.n_spks),
|
||||
)
|
||||
|
||||
def val_dataloader(self):
|
||||
return DataLoader(
|
||||
dataset=self.validset,
|
||||
batch_size=self.hparams.batch_size,
|
||||
num_workers=self.hparams.num_workers,
|
||||
pin_memory=self.hparams.pin_memory,
|
||||
shuffle=False,
|
||||
collate_fn=TextMelBatchCollate(self.hparams.n_spks),
|
||||
)
|
||||
|
||||
def teardown(self, stage: Optional[str] = None):
|
||||
"""Clean up after fit or test."""
|
||||
pass # pylint: disable=unnecessary-pass
|
||||
|
||||
def state_dict(self):
|
||||
"""Extra things to save to checkpoint."""
|
||||
return {}
|
||||
|
||||
def load_state_dict(self, state_dict: Dict[str, Any]):
|
||||
"""Things to do when loading checkpoint."""
|
||||
pass # pylint: disable=unnecessary-pass
|
||||
|
||||
|
||||
class TextMelDataset(torch.utils.data.Dataset):
|
||||
def __init__(
|
||||
self,
|
||||
filelist_path,
|
||||
n_spks,
|
||||
cleaners,
|
||||
add_blank=True,
|
||||
n_fft=1024,
|
||||
n_mels=80,
|
||||
sample_rate=22050,
|
||||
hop_length=256,
|
||||
win_length=1024,
|
||||
f_min=0.0,
|
||||
f_max=8000,
|
||||
data_parameters=None,
|
||||
seed=None,
|
||||
load_durations=False,
|
||||
):
|
||||
self.filepaths_and_text = parse_filelist(filelist_path)
|
||||
self.n_spks = n_spks
|
||||
self.cleaners = cleaners
|
||||
self.add_blank = add_blank
|
||||
self.n_fft = n_fft
|
||||
self.n_mels = n_mels
|
||||
self.sample_rate = sample_rate
|
||||
self.hop_length = hop_length
|
||||
self.win_length = win_length
|
||||
self.f_min = f_min
|
||||
self.f_max = f_max
|
||||
self.load_durations = load_durations
|
||||
|
||||
if data_parameters is not None:
|
||||
self.data_parameters = data_parameters
|
||||
else:
|
||||
self.data_parameters = {"mel_mean": 0, "mel_std": 1}
|
||||
random.seed(seed)
|
||||
random.shuffle(self.filepaths_and_text)
|
||||
|
||||
def get_datapoint(self, filepath_and_text):
|
||||
if self.n_spks > 1:
|
||||
filepath, spk, text = (
|
||||
filepath_and_text[0],
|
||||
int(filepath_and_text[1]),
|
||||
filepath_and_text[2],
|
||||
)
|
||||
else:
|
||||
filepath, text = filepath_and_text[0], filepath_and_text[1]
|
||||
spk = None
|
||||
|
||||
text, cleaned_text = self.get_text(text, add_blank=self.add_blank)
|
||||
mel = self.get_mel(filepath)
|
||||
|
||||
durations = self.get_durations(filepath, text) if self.load_durations else None
|
||||
|
||||
return {"x": text, "y": mel, "spk": spk, "filepath": filepath, "x_text": cleaned_text, "durations": durations}
|
||||
|
||||
def get_durations(self, filepath, text):
|
||||
filepath = Path(filepath)
|
||||
data_dir, name = filepath.parent.parent, filepath.stem
|
||||
|
||||
try:
|
||||
dur_loc = data_dir / "durations" / f"{name}.npy"
|
||||
durs = torch.from_numpy(np.load(dur_loc).astype(int))
|
||||
|
||||
except FileNotFoundError as e:
|
||||
raise FileNotFoundError(
|
||||
f"Tried loading the durations but durations didn't exist at {dur_loc}, make sure you've generate the durations first using: python matcha/utils/get_durations_from_trained_model.py \n"
|
||||
) from e
|
||||
|
||||
assert len(durs) == len(text), f"Length of durations {len(durs)} and text {len(text)} do not match"
|
||||
|
||||
return durs
|
||||
|
||||
def get_mel(self, filepath):
|
||||
audio, sr = ta.load(filepath)
|
||||
assert sr == self.sample_rate
|
||||
mel = mel_spectrogram(
|
||||
audio,
|
||||
self.n_fft,
|
||||
self.n_mels,
|
||||
self.sample_rate,
|
||||
self.hop_length,
|
||||
self.win_length,
|
||||
self.f_min,
|
||||
self.f_max,
|
||||
center=False,
|
||||
).squeeze()
|
||||
mel = normalize(mel, self.data_parameters["mel_mean"], self.data_parameters["mel_std"])
|
||||
return mel
|
||||
|
||||
def get_text(self, text, add_blank=True):
|
||||
text_norm, cleaned_text = text_to_sequence(text, self.cleaners)
|
||||
if self.add_blank:
|
||||
text_norm = intersperse(text_norm, 0)
|
||||
text_norm = torch.IntTensor(text_norm)
|
||||
return text_norm, cleaned_text
|
||||
|
||||
def __getitem__(self, index):
|
||||
datapoint = self.get_datapoint(self.filepaths_and_text[index])
|
||||
return datapoint
|
||||
|
||||
def __len__(self):
|
||||
return len(self.filepaths_and_text)
|
||||
|
||||
|
||||
class TextMelBatchCollate:
|
||||
def __init__(self, n_spks):
|
||||
self.n_spks = n_spks
|
||||
|
||||
def __call__(self, batch):
|
||||
B = len(batch)
|
||||
y_max_length = max([item["y"].shape[-1] for item in batch]) # pylint: disable=consider-using-generator
|
||||
y_max_length = fix_len_compatibility(y_max_length)
|
||||
x_max_length = max([item["x"].shape[-1] for item in batch]) # pylint: disable=consider-using-generator
|
||||
n_feats = batch[0]["y"].shape[-2]
|
||||
|
||||
y = torch.zeros((B, n_feats, y_max_length), dtype=torch.float32)
|
||||
x = torch.zeros((B, x_max_length), dtype=torch.long)
|
||||
durations = torch.zeros((B, x_max_length), dtype=torch.long)
|
||||
|
||||
y_lengths, x_lengths = [], []
|
||||
spks = []
|
||||
filepaths, x_texts = [], []
|
||||
for i, item in enumerate(batch):
|
||||
y_, x_ = item["y"], item["x"]
|
||||
y_lengths.append(y_.shape[-1])
|
||||
x_lengths.append(x_.shape[-1])
|
||||
y[i, :, : y_.shape[-1]] = y_
|
||||
x[i, : x_.shape[-1]] = x_
|
||||
spks.append(item["spk"])
|
||||
filepaths.append(item["filepath"])
|
||||
x_texts.append(item["x_text"])
|
||||
if item["durations"] is not None:
|
||||
durations[i, : item["durations"].shape[-1]] = item["durations"]
|
||||
|
||||
y_lengths = torch.tensor(y_lengths, dtype=torch.long)
|
||||
x_lengths = torch.tensor(x_lengths, dtype=torch.long)
|
||||
spks = torch.tensor(spks, dtype=torch.long) if self.n_spks > 1 else None
|
||||
|
||||
return {
|
||||
"x": x,
|
||||
"x_lengths": x_lengths,
|
||||
"y": y,
|
||||
"y_lengths": y_lengths,
|
||||
"spks": spks,
|
||||
"filepaths": filepaths,
|
||||
"x_texts": x_texts,
|
||||
"durations": durations if not torch.eq(durations, 0).all() else None,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Jungil Kong
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,101 @@
|
||||
# HiFi-GAN: Generative Adversarial Networks for Efficient and High Fidelity Speech Synthesis
|
||||
|
||||
### Jungil Kong, Jaehyeon Kim, Jaekyoung Bae
|
||||
|
||||
In our [paper](https://arxiv.org/abs/2010.05646),
|
||||
we proposed HiFi-GAN: a GAN-based model capable of generating high fidelity speech efficiently.<br/>
|
||||
We provide our implementation and pretrained models as open source in this repository.
|
||||
|
||||
**Abstract :**
|
||||
Several recent work on speech synthesis have employed generative adversarial networks (GANs) to produce raw waveforms.
|
||||
Although such methods improve the sampling efficiency and memory usage,
|
||||
their sample quality has not yet reached that of autoregressive and flow-based generative models.
|
||||
In this work, we propose HiFi-GAN, which achieves both efficient and high-fidelity speech synthesis.
|
||||
As speech audio consists of sinusoidal signals with various periods,
|
||||
we demonstrate that modeling periodic patterns of an audio is crucial for enhancing sample quality.
|
||||
A subjective human evaluation (mean opinion score, MOS) of a single speaker dataset indicates that our proposed method
|
||||
demonstrates similarity to human quality while generating 22.05 kHz high-fidelity audio 167.9 times faster than
|
||||
real-time on a single V100 GPU. We further show the generality of HiFi-GAN to the mel-spectrogram inversion of unseen
|
||||
speakers and end-to-end speech synthesis. Finally, a small footprint version of HiFi-GAN generates samples 13.4 times
|
||||
faster than real-time on CPU with comparable quality to an autoregressive counterpart.
|
||||
|
||||
Visit our [demo website](https://jik876.github.io/hifi-gan-demo/) for audio samples.
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
1. Python >= 3.6
|
||||
2. Clone this repository.
|
||||
3. Install python requirements. Please refer [requirements.txt](requirements.txt)
|
||||
4. Download and extract the [LJ Speech dataset](https://keithito.com/LJ-Speech-Dataset/).
|
||||
And move all wav files to `LJSpeech-1.1/wavs`
|
||||
|
||||
## Training
|
||||
|
||||
```
|
||||
python train.py --config config_v1.json
|
||||
```
|
||||
|
||||
To train V2 or V3 Generator, replace `config_v1.json` with `config_v2.json` or `config_v3.json`.<br>
|
||||
Checkpoints and copy of the configuration file are saved in `cp_hifigan` directory by default.<br>
|
||||
You can change the path by adding `--checkpoint_path` option.
|
||||
|
||||
Validation loss during training with V1 generator.<br>
|
||||

|
||||
|
||||
## Pretrained Model
|
||||
|
||||
You can also use pretrained models we provide.<br/>
|
||||
[Download pretrained models](https://drive.google.com/drive/folders/1-eEYTB5Av9jNql0WGBlRoi-WH2J7bp5Y?usp=sharing)<br/>
|
||||
Details of each folder are as in follows:
|
||||
|
||||
| Folder Name | Generator | Dataset | Fine-Tuned |
|
||||
| ------------ | --------- | --------- | ------------------------------------------------------ |
|
||||
| LJ_V1 | V1 | LJSpeech | No |
|
||||
| LJ_V2 | V2 | LJSpeech | No |
|
||||
| LJ_V3 | V3 | LJSpeech | No |
|
||||
| LJ_FT_T2_V1 | V1 | LJSpeech | Yes ([Tacotron2](https://github.com/NVIDIA/tacotron2)) |
|
||||
| LJ_FT_T2_V2 | V2 | LJSpeech | Yes ([Tacotron2](https://github.com/NVIDIA/tacotron2)) |
|
||||
| LJ_FT_T2_V3 | V3 | LJSpeech | Yes ([Tacotron2](https://github.com/NVIDIA/tacotron2)) |
|
||||
| VCTK_V1 | V1 | VCTK | No |
|
||||
| VCTK_V2 | V2 | VCTK | No |
|
||||
| VCTK_V3 | V3 | VCTK | No |
|
||||
| UNIVERSAL_V1 | V1 | Universal | No |
|
||||
|
||||
We provide the universal model with discriminator weights that can be used as a base for transfer learning to other datasets.
|
||||
|
||||
## Fine-Tuning
|
||||
|
||||
1. Generate mel-spectrograms in numpy format using [Tacotron2](https://github.com/NVIDIA/tacotron2) with teacher-forcing.<br/>
|
||||
The file name of the generated mel-spectrogram should match the audio file and the extension should be `.npy`.<br/>
|
||||
Example:
|
||||
` Audio File : LJ001-0001.wav
|
||||
Mel-Spectrogram File : LJ001-0001.npy`
|
||||
2. Create `ft_dataset` folder and copy the generated mel-spectrogram files into it.<br/>
|
||||
3. Run the following command.
|
||||
```
|
||||
python train.py --fine_tuning True --config config_v1.json
|
||||
```
|
||||
For other command line options, please refer to the training section.
|
||||
|
||||
## Inference from wav file
|
||||
|
||||
1. Make `test_files` directory and copy wav files into the directory.
|
||||
2. Run the following command.
|
||||
` python inference.py --checkpoint_file [generator checkpoint file path]`
|
||||
Generated wav files are saved in `generated_files` by default.<br>
|
||||
You can change the path by adding `--output_dir` option.
|
||||
|
||||
## Inference for end-to-end speech synthesis
|
||||
|
||||
1. Make `test_mel_files` directory and copy generated mel-spectrogram files into the directory.<br>
|
||||
You can generate mel-spectrograms using [Tacotron2](https://github.com/NVIDIA/tacotron2),
|
||||
[Glow-TTS](https://github.com/jaywalnut310/glow-tts) and so forth.
|
||||
2. Run the following command.
|
||||
` python inference_e2e.py --checkpoint_file [generator checkpoint file path]`
|
||||
Generated wav files are saved in `generated_files_from_mel` by default.<br>
|
||||
You can change the path by adding `--output_dir` option.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
We referred to [WaveGlow](https://github.com/NVIDIA/waveglow), [MelGAN](https://github.com/descriptinc/melgan-neurips)
|
||||
and [Tacotron2](https://github.com/NVIDIA/tacotron2) to implement this.
|
||||
@@ -0,0 +1,28 @@
|
||||
v1 = {
|
||||
"resblock": "1",
|
||||
"num_gpus": 0,
|
||||
"batch_size": 16,
|
||||
"learning_rate": 0.0004,
|
||||
"adam_b1": 0.8,
|
||||
"adam_b2": 0.99,
|
||||
"lr_decay": 0.999,
|
||||
"seed": 1234,
|
||||
"upsample_rates": [8, 8, 2, 2],
|
||||
"upsample_kernel_sizes": [16, 16, 4, 4],
|
||||
"upsample_initial_channel": 512,
|
||||
"resblock_kernel_sizes": [3, 7, 11],
|
||||
"resblock_dilation_sizes": [[1, 3, 5], [1, 3, 5], [1, 3, 5]],
|
||||
"resblock_initial_channel": 256,
|
||||
"segment_size": 8192,
|
||||
"num_mels": 80,
|
||||
"num_freq": 1025,
|
||||
"n_fft": 1024,
|
||||
"hop_size": 256,
|
||||
"win_size": 1024,
|
||||
"sampling_rate": 22050,
|
||||
"fmin": 0,
|
||||
"fmax": 8000,
|
||||
"fmax_loss": None,
|
||||
"num_workers": 4,
|
||||
"dist_config": {"dist_backend": "nccl", "dist_url": "tcp://localhost:54321", "world_size": 1},
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
# Code modified from Rafael Valle's implementation https://github.com/NVIDIA/waveglow/blob/5bc2a53e20b3b533362f974cfa1ea0267ae1c2b1/denoiser.py
|
||||
|
||||
"""Waveglow style denoiser can be used to remove the artifacts from the HiFiGAN generated audio."""
|
||||
import torch
|
||||
|
||||
|
||||
class ModeException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Denoiser(torch.nn.Module):
|
||||
"""Removes model bias from audio produced with waveglow"""
|
||||
|
||||
def __init__(self, vocoder, filter_length=1024, n_overlap=4, win_length=1024, mode="zeros"):
|
||||
super().__init__()
|
||||
self.filter_length = filter_length
|
||||
self.hop_length = int(filter_length / n_overlap)
|
||||
self.win_length = win_length
|
||||
|
||||
dtype, device = next(vocoder.parameters()).dtype, next(vocoder.parameters()).device
|
||||
self.device = device
|
||||
if mode == "zeros":
|
||||
mel_input = torch.zeros((1, 80, 88), dtype=dtype, device=device)
|
||||
elif mode == "normal":
|
||||
mel_input = torch.randn((1, 80, 88), dtype=dtype, device=device)
|
||||
else:
|
||||
raise ModeException(f"Mode {mode} if not supported")
|
||||
|
||||
def stft_fn(audio, n_fft, hop_length, win_length, window):
|
||||
spec = torch.stft(
|
||||
audio,
|
||||
n_fft=n_fft,
|
||||
hop_length=hop_length,
|
||||
win_length=win_length,
|
||||
window=window,
|
||||
return_complex=True,
|
||||
)
|
||||
spec = torch.view_as_real(spec)
|
||||
return torch.sqrt(spec.pow(2).sum(-1)), torch.atan2(spec[..., -1], spec[..., 0])
|
||||
|
||||
self.stft = lambda x: stft_fn(
|
||||
audio=x,
|
||||
n_fft=self.filter_length,
|
||||
hop_length=self.hop_length,
|
||||
win_length=self.win_length,
|
||||
window=torch.hann_window(self.win_length, device=device),
|
||||
)
|
||||
self.istft = lambda x, y: torch.istft(
|
||||
torch.complex(x * torch.cos(y), x * torch.sin(y)),
|
||||
n_fft=self.filter_length,
|
||||
hop_length=self.hop_length,
|
||||
win_length=self.win_length,
|
||||
window=torch.hann_window(self.win_length, device=device),
|
||||
)
|
||||
|
||||
with torch.no_grad():
|
||||
bias_audio = vocoder(mel_input).float().squeeze(0)
|
||||
bias_spec, _ = self.stft(bias_audio)
|
||||
|
||||
self.register_buffer("bias_spec", bias_spec[:, :, 0][:, :, None])
|
||||
|
||||
@torch.inference_mode()
|
||||
def forward(self, audio, strength=0.0005):
|
||||
audio_spec, audio_angles = self.stft(audio)
|
||||
audio_spec_denoised = audio_spec - self.bias_spec.to(audio.device) * strength
|
||||
audio_spec_denoised = torch.clamp(audio_spec_denoised, 0.0)
|
||||
audio_denoised = self.istft(audio_spec_denoised, audio_angles)
|
||||
return audio_denoised
|
||||
@@ -0,0 +1,17 @@
|
||||
""" from https://github.com/jik876/hifi-gan """
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
class AttrDict(dict):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__dict__ = self
|
||||
|
||||
|
||||
def build_env(config, config_name, path):
|
||||
t_path = os.path.join(path, config_name)
|
||||
if config != t_path:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
shutil.copyfile(config, os.path.join(path, config_name))
|
||||
@@ -0,0 +1,217 @@
|
||||
""" from https://github.com/jik876/hifi-gan """
|
||||
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.utils.data
|
||||
from librosa.filters import mel as librosa_mel_fn
|
||||
from librosa.util import normalize
|
||||
from scipy.io.wavfile import read
|
||||
|
||||
MAX_WAV_VALUE = 32768.0
|
||||
|
||||
|
||||
def load_wav(full_path):
|
||||
sampling_rate, data = read(full_path)
|
||||
return data, sampling_rate
|
||||
|
||||
|
||||
def dynamic_range_compression(x, C=1, clip_val=1e-5):
|
||||
return np.log(np.clip(x, a_min=clip_val, a_max=None) * C)
|
||||
|
||||
|
||||
def dynamic_range_decompression(x, C=1):
|
||||
return np.exp(x) / C
|
||||
|
||||
|
||||
def dynamic_range_compression_torch(x, C=1, clip_val=1e-5):
|
||||
return torch.log(torch.clamp(x, min=clip_val) * C)
|
||||
|
||||
|
||||
def dynamic_range_decompression_torch(x, C=1):
|
||||
return torch.exp(x) / C
|
||||
|
||||
|
||||
def spectral_normalize_torch(magnitudes):
|
||||
output = dynamic_range_compression_torch(magnitudes)
|
||||
return output
|
||||
|
||||
|
||||
def spectral_de_normalize_torch(magnitudes):
|
||||
output = dynamic_range_decompression_torch(magnitudes)
|
||||
return output
|
||||
|
||||
|
||||
mel_basis = {}
|
||||
hann_window = {}
|
||||
|
||||
|
||||
def mel_spectrogram(y, n_fft, num_mels, sampling_rate, hop_size, win_size, fmin, fmax, center=False):
|
||||
if torch.min(y) < -1.0:
|
||||
print("min value is ", torch.min(y))
|
||||
if torch.max(y) > 1.0:
|
||||
print("max value is ", torch.max(y))
|
||||
|
||||
global mel_basis, hann_window # pylint: disable=global-statement,global-variable-not-assigned
|
||||
if fmax not in mel_basis:
|
||||
mel = librosa_mel_fn(sampling_rate, n_fft, num_mels, fmin, 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
|
||||
|
||||
|
||||
def get_dataset_filelist(a):
|
||||
with open(a.input_training_file, encoding="utf-8") as fi:
|
||||
training_files = [
|
||||
os.path.join(a.input_wavs_dir, x.split("|")[0] + ".wav") for x in fi.read().split("\n") if len(x) > 0
|
||||
]
|
||||
|
||||
with open(a.input_validation_file, encoding="utf-8") as fi:
|
||||
validation_files = [
|
||||
os.path.join(a.input_wavs_dir, x.split("|")[0] + ".wav") for x in fi.read().split("\n") if len(x) > 0
|
||||
]
|
||||
return training_files, validation_files
|
||||
|
||||
|
||||
class MelDataset(torch.utils.data.Dataset):
|
||||
def __init__(
|
||||
self,
|
||||
training_files,
|
||||
segment_size,
|
||||
n_fft,
|
||||
num_mels,
|
||||
hop_size,
|
||||
win_size,
|
||||
sampling_rate,
|
||||
fmin,
|
||||
fmax,
|
||||
split=True,
|
||||
shuffle=True,
|
||||
n_cache_reuse=1,
|
||||
device=None,
|
||||
fmax_loss=None,
|
||||
fine_tuning=False,
|
||||
base_mels_path=None,
|
||||
):
|
||||
self.audio_files = training_files
|
||||
random.seed(1234)
|
||||
if shuffle:
|
||||
random.shuffle(self.audio_files)
|
||||
self.segment_size = segment_size
|
||||
self.sampling_rate = sampling_rate
|
||||
self.split = split
|
||||
self.n_fft = n_fft
|
||||
self.num_mels = num_mels
|
||||
self.hop_size = hop_size
|
||||
self.win_size = win_size
|
||||
self.fmin = fmin
|
||||
self.fmax = fmax
|
||||
self.fmax_loss = fmax_loss
|
||||
self.cached_wav = None
|
||||
self.n_cache_reuse = n_cache_reuse
|
||||
self._cache_ref_count = 0
|
||||
self.device = device
|
||||
self.fine_tuning = fine_tuning
|
||||
self.base_mels_path = base_mels_path
|
||||
|
||||
def __getitem__(self, index):
|
||||
filename = self.audio_files[index]
|
||||
if self._cache_ref_count == 0:
|
||||
audio, sampling_rate = load_wav(filename)
|
||||
audio = audio / MAX_WAV_VALUE
|
||||
if not self.fine_tuning:
|
||||
audio = normalize(audio) * 0.95
|
||||
self.cached_wav = audio
|
||||
if sampling_rate != self.sampling_rate:
|
||||
raise ValueError(f"{sampling_rate} SR doesn't match target {self.sampling_rate} SR")
|
||||
self._cache_ref_count = self.n_cache_reuse
|
||||
else:
|
||||
audio = self.cached_wav
|
||||
self._cache_ref_count -= 1
|
||||
|
||||
audio = torch.FloatTensor(audio)
|
||||
audio = audio.unsqueeze(0)
|
||||
|
||||
if not self.fine_tuning:
|
||||
if self.split:
|
||||
if audio.size(1) >= self.segment_size:
|
||||
max_audio_start = audio.size(1) - self.segment_size
|
||||
audio_start = random.randint(0, max_audio_start)
|
||||
audio = audio[:, audio_start : audio_start + self.segment_size]
|
||||
else:
|
||||
audio = torch.nn.functional.pad(audio, (0, self.segment_size - audio.size(1)), "constant")
|
||||
|
||||
mel = mel_spectrogram(
|
||||
audio,
|
||||
self.n_fft,
|
||||
self.num_mels,
|
||||
self.sampling_rate,
|
||||
self.hop_size,
|
||||
self.win_size,
|
||||
self.fmin,
|
||||
self.fmax,
|
||||
center=False,
|
||||
)
|
||||
else:
|
||||
mel = np.load(os.path.join(self.base_mels_path, os.path.splitext(os.path.split(filename)[-1])[0] + ".npy"))
|
||||
mel = torch.from_numpy(mel)
|
||||
|
||||
if len(mel.shape) < 3:
|
||||
mel = mel.unsqueeze(0)
|
||||
|
||||
if self.split:
|
||||
frames_per_seg = math.ceil(self.segment_size / self.hop_size)
|
||||
|
||||
if audio.size(1) >= self.segment_size:
|
||||
mel_start = random.randint(0, mel.size(2) - frames_per_seg - 1)
|
||||
mel = mel[:, :, mel_start : mel_start + frames_per_seg]
|
||||
audio = audio[:, mel_start * self.hop_size : (mel_start + frames_per_seg) * self.hop_size]
|
||||
else:
|
||||
mel = torch.nn.functional.pad(mel, (0, frames_per_seg - mel.size(2)), "constant")
|
||||
audio = torch.nn.functional.pad(audio, (0, self.segment_size - audio.size(1)), "constant")
|
||||
|
||||
mel_loss = mel_spectrogram(
|
||||
audio,
|
||||
self.n_fft,
|
||||
self.num_mels,
|
||||
self.sampling_rate,
|
||||
self.hop_size,
|
||||
self.win_size,
|
||||
self.fmin,
|
||||
self.fmax_loss,
|
||||
center=False,
|
||||
)
|
||||
|
||||
return (mel.squeeze(), audio.squeeze(0), filename, mel_loss.squeeze())
|
||||
|
||||
def __len__(self):
|
||||
return len(self.audio_files)
|
||||
@@ -0,0 +1,368 @@
|
||||
""" from https://github.com/jik876/hifi-gan """
|
||||
|
||||
import torch
|
||||
import torch.nn as nn # pylint: disable=consider-using-from-import
|
||||
import torch.nn.functional as F
|
||||
from torch.nn import AvgPool1d, Conv1d, Conv2d, ConvTranspose1d
|
||||
from torch.nn.utils import remove_weight_norm, spectral_norm, weight_norm
|
||||
|
||||
from .xutils import get_padding, init_weights
|
||||
|
||||
LRELU_SLOPE = 0.1
|
||||
|
||||
|
||||
class ResBlock1(torch.nn.Module):
|
||||
def __init__(self, h, channels, kernel_size=3, dilation=(1, 3, 5)):
|
||||
super().__init__()
|
||||
self.h = h
|
||||
self.convs1 = nn.ModuleList(
|
||||
[
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=dilation[0],
|
||||
padding=get_padding(kernel_size, dilation[0]),
|
||||
)
|
||||
),
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=dilation[1],
|
||||
padding=get_padding(kernel_size, dilation[1]),
|
||||
)
|
||||
),
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=dilation[2],
|
||||
padding=get_padding(kernel_size, dilation[2]),
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
self.convs1.apply(init_weights)
|
||||
|
||||
self.convs2 = nn.ModuleList(
|
||||
[
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=1,
|
||||
padding=get_padding(kernel_size, 1),
|
||||
)
|
||||
),
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=1,
|
||||
padding=get_padding(kernel_size, 1),
|
||||
)
|
||||
),
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=1,
|
||||
padding=get_padding(kernel_size, 1),
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
self.convs2.apply(init_weights)
|
||||
|
||||
def forward(self, x):
|
||||
for c1, c2 in zip(self.convs1, self.convs2):
|
||||
xt = F.leaky_relu(x, LRELU_SLOPE)
|
||||
xt = c1(xt)
|
||||
xt = F.leaky_relu(xt, LRELU_SLOPE)
|
||||
xt = c2(xt)
|
||||
x = xt + x
|
||||
return x
|
||||
|
||||
def remove_weight_norm(self):
|
||||
for l in self.convs1:
|
||||
remove_weight_norm(l)
|
||||
for l in self.convs2:
|
||||
remove_weight_norm(l)
|
||||
|
||||
|
||||
class ResBlock2(torch.nn.Module):
|
||||
def __init__(self, h, channels, kernel_size=3, dilation=(1, 3)):
|
||||
super().__init__()
|
||||
self.h = h
|
||||
self.convs = nn.ModuleList(
|
||||
[
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=dilation[0],
|
||||
padding=get_padding(kernel_size, dilation[0]),
|
||||
)
|
||||
),
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=dilation[1],
|
||||
padding=get_padding(kernel_size, dilation[1]),
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
self.convs.apply(init_weights)
|
||||
|
||||
def forward(self, x):
|
||||
for c in self.convs:
|
||||
xt = F.leaky_relu(x, LRELU_SLOPE)
|
||||
xt = c(xt)
|
||||
x = xt + x
|
||||
return x
|
||||
|
||||
def remove_weight_norm(self):
|
||||
for l in self.convs:
|
||||
remove_weight_norm(l)
|
||||
|
||||
|
||||
class Generator(torch.nn.Module):
|
||||
def __init__(self, h):
|
||||
super().__init__()
|
||||
self.h = h
|
||||
self.num_kernels = len(h.resblock_kernel_sizes)
|
||||
self.num_upsamples = len(h.upsample_rates)
|
||||
self.conv_pre = weight_norm(Conv1d(80, h.upsample_initial_channel, 7, 1, padding=3))
|
||||
resblock = ResBlock1 if h.resblock == "1" else ResBlock2
|
||||
|
||||
self.ups = nn.ModuleList()
|
||||
for i, (u, k) in enumerate(zip(h.upsample_rates, h.upsample_kernel_sizes)):
|
||||
self.ups.append(
|
||||
weight_norm(
|
||||
ConvTranspose1d(
|
||||
h.upsample_initial_channel // (2**i),
|
||||
h.upsample_initial_channel // (2 ** (i + 1)),
|
||||
k,
|
||||
u,
|
||||
padding=(k - u) // 2,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
self.resblocks = nn.ModuleList()
|
||||
for i in range(len(self.ups)):
|
||||
ch = h.upsample_initial_channel // (2 ** (i + 1))
|
||||
for _, (k, d) in enumerate(zip(h.resblock_kernel_sizes, h.resblock_dilation_sizes)):
|
||||
self.resblocks.append(resblock(h, ch, k, d))
|
||||
|
||||
self.conv_post = weight_norm(Conv1d(ch, 1, 7, 1, padding=3))
|
||||
self.ups.apply(init_weights)
|
||||
self.conv_post.apply(init_weights)
|
||||
|
||||
def forward(self, x):
|
||||
x = self.conv_pre(x)
|
||||
for i in range(self.num_upsamples):
|
||||
x = F.leaky_relu(x, LRELU_SLOPE)
|
||||
x = self.ups[i](x)
|
||||
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)
|
||||
x = torch.tanh(x)
|
||||
|
||||
return x
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class DiscriminatorP(torch.nn.Module):
|
||||
def __init__(self, period, kernel_size=5, stride=3, use_spectral_norm=False):
|
||||
super().__init__()
|
||||
self.period = period
|
||||
norm_f = weight_norm if use_spectral_norm is False else spectral_norm
|
||||
self.convs = nn.ModuleList(
|
||||
[
|
||||
norm_f(Conv2d(1, 32, (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
|
||||
norm_f(Conv2d(32, 128, (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
|
||||
norm_f(Conv2d(128, 512, (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
|
||||
norm_f(Conv2d(512, 1024, (kernel_size, 1), (stride, 1), padding=(get_padding(5, 1), 0))),
|
||||
norm_f(Conv2d(1024, 1024, (kernel_size, 1), 1, padding=(2, 0))),
|
||||
]
|
||||
)
|
||||
self.conv_post = norm_f(Conv2d(1024, 1, (3, 1), 1, padding=(1, 0)))
|
||||
|
||||
def forward(self, x):
|
||||
fmap = []
|
||||
|
||||
# 1d to 2d
|
||||
b, c, t = x.shape
|
||||
if t % self.period != 0: # pad first
|
||||
n_pad = self.period - (t % self.period)
|
||||
x = F.pad(x, (0, n_pad), "reflect")
|
||||
t = t + n_pad
|
||||
x = x.view(b, c, t // self.period, self.period)
|
||||
|
||||
for l in self.convs:
|
||||
x = l(x)
|
||||
x = F.leaky_relu(x, LRELU_SLOPE)
|
||||
fmap.append(x)
|
||||
x = self.conv_post(x)
|
||||
fmap.append(x)
|
||||
x = torch.flatten(x, 1, -1)
|
||||
|
||||
return x, fmap
|
||||
|
||||
|
||||
class MultiPeriodDiscriminator(torch.nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.discriminators = nn.ModuleList(
|
||||
[
|
||||
DiscriminatorP(2),
|
||||
DiscriminatorP(3),
|
||||
DiscriminatorP(5),
|
||||
DiscriminatorP(7),
|
||||
DiscriminatorP(11),
|
||||
]
|
||||
)
|
||||
|
||||
def forward(self, y, y_hat):
|
||||
y_d_rs = []
|
||||
y_d_gs = []
|
||||
fmap_rs = []
|
||||
fmap_gs = []
|
||||
for _, d in enumerate(self.discriminators):
|
||||
y_d_r, fmap_r = d(y)
|
||||
y_d_g, fmap_g = d(y_hat)
|
||||
y_d_rs.append(y_d_r)
|
||||
fmap_rs.append(fmap_r)
|
||||
y_d_gs.append(y_d_g)
|
||||
fmap_gs.append(fmap_g)
|
||||
|
||||
return y_d_rs, y_d_gs, fmap_rs, fmap_gs
|
||||
|
||||
|
||||
class DiscriminatorS(torch.nn.Module):
|
||||
def __init__(self, use_spectral_norm=False):
|
||||
super().__init__()
|
||||
norm_f = weight_norm if use_spectral_norm is False else spectral_norm
|
||||
self.convs = nn.ModuleList(
|
||||
[
|
||||
norm_f(Conv1d(1, 128, 15, 1, padding=7)),
|
||||
norm_f(Conv1d(128, 128, 41, 2, groups=4, padding=20)),
|
||||
norm_f(Conv1d(128, 256, 41, 2, groups=16, padding=20)),
|
||||
norm_f(Conv1d(256, 512, 41, 4, groups=16, padding=20)),
|
||||
norm_f(Conv1d(512, 1024, 41, 4, groups=16, padding=20)),
|
||||
norm_f(Conv1d(1024, 1024, 41, 1, groups=16, padding=20)),
|
||||
norm_f(Conv1d(1024, 1024, 5, 1, padding=2)),
|
||||
]
|
||||
)
|
||||
self.conv_post = norm_f(Conv1d(1024, 1, 3, 1, padding=1))
|
||||
|
||||
def forward(self, x):
|
||||
fmap = []
|
||||
for l in self.convs:
|
||||
x = l(x)
|
||||
x = F.leaky_relu(x, LRELU_SLOPE)
|
||||
fmap.append(x)
|
||||
x = self.conv_post(x)
|
||||
fmap.append(x)
|
||||
x = torch.flatten(x, 1, -1)
|
||||
|
||||
return x, fmap
|
||||
|
||||
|
||||
class MultiScaleDiscriminator(torch.nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.discriminators = nn.ModuleList(
|
||||
[
|
||||
DiscriminatorS(use_spectral_norm=True),
|
||||
DiscriminatorS(),
|
||||
DiscriminatorS(),
|
||||
]
|
||||
)
|
||||
self.meanpools = nn.ModuleList([AvgPool1d(4, 2, padding=2), AvgPool1d(4, 2, padding=2)])
|
||||
|
||||
def forward(self, y, y_hat):
|
||||
y_d_rs = []
|
||||
y_d_gs = []
|
||||
fmap_rs = []
|
||||
fmap_gs = []
|
||||
for i, d in enumerate(self.discriminators):
|
||||
if i != 0:
|
||||
y = self.meanpools[i - 1](y)
|
||||
y_hat = self.meanpools[i - 1](y_hat)
|
||||
y_d_r, fmap_r = d(y)
|
||||
y_d_g, fmap_g = d(y_hat)
|
||||
y_d_rs.append(y_d_r)
|
||||
fmap_rs.append(fmap_r)
|
||||
y_d_gs.append(y_d_g)
|
||||
fmap_gs.append(fmap_g)
|
||||
|
||||
return y_d_rs, y_d_gs, fmap_rs, fmap_gs
|
||||
|
||||
|
||||
def feature_loss(fmap_r, fmap_g):
|
||||
loss = 0
|
||||
for dr, dg in zip(fmap_r, fmap_g):
|
||||
for rl, gl in zip(dr, dg):
|
||||
loss += torch.mean(torch.abs(rl - gl))
|
||||
|
||||
return loss * 2
|
||||
|
||||
|
||||
def discriminator_loss(disc_real_outputs, disc_generated_outputs):
|
||||
loss = 0
|
||||
r_losses = []
|
||||
g_losses = []
|
||||
for dr, dg in zip(disc_real_outputs, disc_generated_outputs):
|
||||
r_loss = torch.mean((1 - dr) ** 2)
|
||||
g_loss = torch.mean(dg**2)
|
||||
loss += r_loss + g_loss
|
||||
r_losses.append(r_loss.item())
|
||||
g_losses.append(g_loss.item())
|
||||
|
||||
return loss, r_losses, g_losses
|
||||
|
||||
|
||||
def generator_loss(disc_outputs):
|
||||
loss = 0
|
||||
gen_losses = []
|
||||
for dg in disc_outputs:
|
||||
l = torch.mean((1 - dg) ** 2)
|
||||
gen_losses.append(l)
|
||||
loss += l
|
||||
|
||||
return loss, gen_losses
|
||||
@@ -0,0 +1,60 @@
|
||||
""" from https://github.com/jik876/hifi-gan """
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
import matplotlib
|
||||
import torch
|
||||
from torch.nn.utils import weight_norm
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pylab as plt
|
||||
|
||||
|
||||
def plot_spectrogram(spectrogram):
|
||||
fig, ax = plt.subplots(figsize=(10, 2))
|
||||
im = ax.imshow(spectrogram, aspect="auto", origin="lower", interpolation="none")
|
||||
plt.colorbar(im, ax=ax)
|
||||
|
||||
fig.canvas.draw()
|
||||
plt.close()
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def apply_weight_norm(m):
|
||||
classname = m.__class__.__name__
|
||||
if classname.find("Conv") != -1:
|
||||
weight_norm(m)
|
||||
|
||||
|
||||
def get_padding(kernel_size, dilation=1):
|
||||
return int((kernel_size * dilation - dilation) / 2)
|
||||
|
||||
|
||||
def load_checkpoint(filepath, device):
|
||||
assert os.path.isfile(filepath)
|
||||
print(f"Loading '{filepath}'")
|
||||
checkpoint_dict = torch.load(filepath, map_location=device)
|
||||
print("Complete.")
|
||||
return checkpoint_dict
|
||||
|
||||
|
||||
def save_checkpoint(filepath, obj):
|
||||
print(f"Saving checkpoint to {filepath}")
|
||||
torch.save(obj, filepath)
|
||||
print("Complete.")
|
||||
|
||||
|
||||
def scan_checkpoint(cp_dir, prefix):
|
||||
pattern = os.path.join(cp_dir, prefix + "????????")
|
||||
cp_list = glob.glob(pattern)
|
||||
if len(cp_list) == 0:
|
||||
return None
|
||||
return sorted(cp_list)[-1]
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
This is a base lightning module that can be used to train a model.
|
||||
The benefit of this abstraction is that all the logic outside of model definition can be reused for different models.
|
||||
"""
|
||||
import inspect
|
||||
from abc import ABC
|
||||
from typing import Any, Dict
|
||||
|
||||
import torch
|
||||
from lightning import LightningModule
|
||||
from lightning.pytorch.utilities import grad_norm
|
||||
|
||||
from matcha import utils
|
||||
from matcha.utils.utils import plot_tensor
|
||||
|
||||
log = utils.get_pylogger(__name__)
|
||||
|
||||
|
||||
class BaseLightningClass(LightningModule, ABC):
|
||||
def update_data_statistics(self, data_statistics):
|
||||
if data_statistics is None:
|
||||
data_statistics = {
|
||||
"mel_mean": 0.0,
|
||||
"mel_std": 1.0,
|
||||
}
|
||||
|
||||
self.register_buffer("mel_mean", torch.tensor(data_statistics["mel_mean"]))
|
||||
self.register_buffer("mel_std", torch.tensor(data_statistics["mel_std"]))
|
||||
|
||||
def configure_optimizers(self) -> Any:
|
||||
optimizer = self.hparams.optimizer(params=self.parameters())
|
||||
if self.hparams.scheduler not in (None, {}):
|
||||
scheduler_args = {}
|
||||
# Manage last epoch for exponential schedulers
|
||||
if "last_epoch" in inspect.signature(self.hparams.scheduler.scheduler).parameters:
|
||||
if hasattr(self, "ckpt_loaded_epoch"):
|
||||
current_epoch = self.ckpt_loaded_epoch - 1
|
||||
else:
|
||||
current_epoch = -1
|
||||
|
||||
scheduler_args.update({"optimizer": optimizer})
|
||||
scheduler = self.hparams.scheduler.scheduler(**scheduler_args)
|
||||
scheduler.last_epoch = current_epoch
|
||||
return {
|
||||
"optimizer": optimizer,
|
||||
"lr_scheduler": {
|
||||
"scheduler": scheduler,
|
||||
"interval": self.hparams.scheduler.lightning_args.interval,
|
||||
"frequency": self.hparams.scheduler.lightning_args.frequency,
|
||||
"name": "learning_rate",
|
||||
},
|
||||
}
|
||||
|
||||
return {"optimizer": optimizer}
|
||||
|
||||
def get_losses(self, batch):
|
||||
x, x_lengths = batch["x"], batch["x_lengths"]
|
||||
y, y_lengths = batch["y"], batch["y_lengths"]
|
||||
spks = batch["spks"]
|
||||
|
||||
dur_loss, prior_loss, diff_loss, *_ = self(
|
||||
x=x,
|
||||
x_lengths=x_lengths,
|
||||
y=y,
|
||||
y_lengths=y_lengths,
|
||||
spks=spks,
|
||||
out_size=self.out_size,
|
||||
durations=batch["durations"],
|
||||
)
|
||||
return {
|
||||
"dur_loss": dur_loss,
|
||||
"prior_loss": prior_loss,
|
||||
"diff_loss": diff_loss,
|
||||
}
|
||||
|
||||
def on_load_checkpoint(self, checkpoint: Dict[str, Any]) -> None:
|
||||
self.ckpt_loaded_epoch = checkpoint["epoch"] # pylint: disable=attribute-defined-outside-init
|
||||
|
||||
def training_step(self, batch: Any, batch_idx: int):
|
||||
loss_dict = self.get_losses(batch)
|
||||
self.log(
|
||||
"step",
|
||||
float(self.global_step),
|
||||
on_step=True,
|
||||
prog_bar=True,
|
||||
logger=True,
|
||||
sync_dist=True,
|
||||
)
|
||||
|
||||
self.log(
|
||||
"sub_loss/train_dur_loss",
|
||||
loss_dict["dur_loss"],
|
||||
on_step=True,
|
||||
on_epoch=True,
|
||||
logger=True,
|
||||
sync_dist=True,
|
||||
)
|
||||
self.log(
|
||||
"sub_loss/train_prior_loss",
|
||||
loss_dict["prior_loss"],
|
||||
on_step=True,
|
||||
on_epoch=True,
|
||||
logger=True,
|
||||
sync_dist=True,
|
||||
)
|
||||
self.log(
|
||||
"sub_loss/train_diff_loss",
|
||||
loss_dict["diff_loss"],
|
||||
on_step=True,
|
||||
on_epoch=True,
|
||||
logger=True,
|
||||
sync_dist=True,
|
||||
)
|
||||
|
||||
total_loss = sum(loss_dict.values())
|
||||
self.log(
|
||||
"loss/train",
|
||||
total_loss,
|
||||
on_step=True,
|
||||
on_epoch=True,
|
||||
logger=True,
|
||||
prog_bar=True,
|
||||
sync_dist=True,
|
||||
)
|
||||
|
||||
return {"loss": total_loss, "log": loss_dict}
|
||||
|
||||
def validation_step(self, batch: Any, batch_idx: int):
|
||||
loss_dict = self.get_losses(batch)
|
||||
self.log(
|
||||
"sub_loss/val_dur_loss",
|
||||
loss_dict["dur_loss"],
|
||||
on_step=True,
|
||||
on_epoch=True,
|
||||
logger=True,
|
||||
sync_dist=True,
|
||||
)
|
||||
self.log(
|
||||
"sub_loss/val_prior_loss",
|
||||
loss_dict["prior_loss"],
|
||||
on_step=True,
|
||||
on_epoch=True,
|
||||
logger=True,
|
||||
sync_dist=True,
|
||||
)
|
||||
self.log(
|
||||
"sub_loss/val_diff_loss",
|
||||
loss_dict["diff_loss"],
|
||||
on_step=True,
|
||||
on_epoch=True,
|
||||
logger=True,
|
||||
sync_dist=True,
|
||||
)
|
||||
|
||||
total_loss = sum(loss_dict.values())
|
||||
self.log(
|
||||
"loss/val",
|
||||
total_loss,
|
||||
on_step=True,
|
||||
on_epoch=True,
|
||||
logger=True,
|
||||
prog_bar=True,
|
||||
sync_dist=True,
|
||||
)
|
||||
|
||||
return total_loss
|
||||
|
||||
def on_validation_end(self) -> None:
|
||||
if self.trainer.is_global_zero:
|
||||
one_batch = next(iter(self.trainer.val_dataloaders))
|
||||
if self.current_epoch == 0:
|
||||
log.debug("Plotting original samples")
|
||||
for i in range(2):
|
||||
y = one_batch["y"][i].unsqueeze(0).to(self.device)
|
||||
self.logger.experiment.add_image(
|
||||
f"original/{i}",
|
||||
plot_tensor(y.squeeze().cpu()),
|
||||
self.current_epoch,
|
||||
dataformats="HWC",
|
||||
)
|
||||
|
||||
log.debug("Synthesising...")
|
||||
for i in range(2):
|
||||
x = one_batch["x"][i].unsqueeze(0).to(self.device)
|
||||
x_lengths = one_batch["x_lengths"][i].unsqueeze(0).to(self.device)
|
||||
spks = one_batch["spks"][i].unsqueeze(0).to(self.device) if one_batch["spks"] is not None else None
|
||||
output = self.synthesise(x[:, :x_lengths], x_lengths, n_timesteps=10, spks=spks)
|
||||
y_enc, y_dec = output["encoder_outputs"], output["decoder_outputs"]
|
||||
attn = output["attn"]
|
||||
self.logger.experiment.add_image(
|
||||
f"generated_enc/{i}",
|
||||
plot_tensor(y_enc.squeeze().cpu()),
|
||||
self.current_epoch,
|
||||
dataformats="HWC",
|
||||
)
|
||||
self.logger.experiment.add_image(
|
||||
f"generated_dec/{i}",
|
||||
plot_tensor(y_dec.squeeze().cpu()),
|
||||
self.current_epoch,
|
||||
dataformats="HWC",
|
||||
)
|
||||
self.logger.experiment.add_image(
|
||||
f"alignment/{i}",
|
||||
plot_tensor(attn.squeeze().cpu()),
|
||||
self.current_epoch,
|
||||
dataformats="HWC",
|
||||
)
|
||||
|
||||
def on_before_optimizer_step(self, optimizer):
|
||||
self.log_dict({f"grad_norm/{k}": v for k, v in grad_norm(self, norm_type=2).items()})
|
||||
+443
@@ -0,0 +1,443 @@
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
import torch.nn as nn # pylint: disable=consider-using-from-import
|
||||
import torch.nn.functional as F
|
||||
from conformer import ConformerBlock
|
||||
from diffusers.models.activations import get_activation
|
||||
from einops import pack, rearrange, repeat
|
||||
|
||||
from matcha.models.components.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
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
from abc import ABC
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from matcha.models.components.decoder import Decoder
|
||||
from matcha.utils.pylogger import get_pylogger
|
||||
|
||||
log = get_pylogger(__name__)
|
||||
|
||||
|
||||
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)
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
""" from https://github.com/jaywalnut310/glow-tts """
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn as nn # pylint: disable=consider-using-from-import
|
||||
from einops import rearrange
|
||||
|
||||
import matcha.utils as utils # pylint: disable=consider-using-from-import
|
||||
from matcha.utils.model import sequence_mask
|
||||
|
||||
log = utils.get_pylogger(__name__)
|
||||
|
||||
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user