From e840c6df4d4250db1771fa8249502df54c19f00a Mon Sep 17 00:00:00 2001 From: diodiogod <160325160+diodiogod@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:18:54 -0300 Subject: [PATCH] Version 4.19.2 Add configurable VC engine for tags Technical implementation: - Add ComfyUI settings menu option for VC engine selection - Support three VC engines: ChatterBox 23-Lang, ChatterBox, CosyVoice - Fix module import to use shared sys.modules instance - Implement file-based settings cache for persistence across restarts - Add beforeQueuing hook to ensure settings sync before workflows --- .gitignore | 3 + CHANGELOG.md | 10 +++ README.md | 2 +- __init__.py | 26 +++++--- nodes.py | 2 +- pyproject.toml | 2 +- utils/audio/edit_post_processor.py | 78 +++++++++++++++++++---- web/tts_audio_suite_settings.js | 99 +++++++++++++++++------------- 8 files changed, 157 insertions(+), 65 deletions(-) diff --git a/.gitignore b/.gitignore index 18f5a3b7..4d44a453 100644 --- a/.gitignore +++ b/.gitignore @@ -228,3 +228,6 @@ ignore/ # Test output files tests/output/ + +# TTS Audio Suite settings cache (user preferences) +.inline_tag_settings_cache.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a62a336b..4fd2c1b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ 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.19.2] - 2026-01-28 + +### Added + +- Add user-selectable voice conversion engine for tags +- New ComfyUI settings menu: Voice Restoration ( 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 tag syntax with iteration control ## [4.19.1] - 2026-01-28 ### Added diff --git a/README.md b/README.md index e802a062..3ed0ca69 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Dynamic TOML Badge][version-shield]][version-url] [![Ko-Fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/diogogo) -# TTS Audio Suite v4.19.1 +# TTS Audio Suite v4.19.2 [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/diogogo) diff --git a/__init__.py b/__init__.py index 27074074..327bb572 100644 --- a/__init__.py +++ b/__init__.py @@ -305,25 +305,33 @@ 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") - print(f"🔧 Received settings: precision={precision}, device={device}") + print(f"🔧 Received settings: precision={precision}, device={device}, vc_engine={vc_engine}") - # Load edit_post_processor directly by file path to avoid package import issues - edit_post_processor_path = os.path.join(os.path.dirname(__file__), "utils", "audio", "edit_post_processor.py") - spec = importlib.util.spec_from_file_location("edit_post_processor_module", edit_post_processor_path) - edit_post_processor_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(edit_post_processor_module) + # 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 - edit_post_processor_module.set_inline_tag_settings(precision=precision, device=device) + edit_post_processor_module.set_inline_tag_settings(precision=precision, device=device, vc_engine=vc_engine) - return web.json_response({"status": "success", "precision": precision, "device": device}) + return web.json_response({"status": "success", "precision": precision, "device": device, "vc_engine": vc_engine}) except Exception as e: print(f"⚠️ Error setting inline tag settings: {e}") return web.json_response({"status": "error", "error": str(e)}) diff --git a/nodes.py b/nodes.py index 358f7103..5c1e261c 100644 --- a/nodes.py +++ b/nodes.py @@ -11,7 +11,7 @@ except ImportError: pass # Version and constants -VERSION = "4.19.1" +VERSION = "4.19.2" IS_DEV = False # Set to False for release builds VERSION_DISPLAY = f"v{VERSION}" + (" (dev)" if IS_DEV else "") SEPARATOR = "=" * 70 diff --git a/pyproject.toml b/pyproject.toml index aa771666..f4fafc17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "tts_audio_suite" description = "TTS Audio Suite - Universal multi-engine TTS extension for ComfyUI with unified architecture supporting IndexTTS-2, ChatterBox, Chatterbox Multilingual TTS (Official 23-Lang), F5-TTS, Higgs Audio 2, VibeVoice, and RVC engines. It has character voice management, SRT subtitle TTS support, and audio processing capabilities." -version = "4.19.1" +version = "4.19.2" license = {file = "LICENSE"} [project.urls] diff --git a/utils/audio/edit_post_processor.py b/utils/audio/edit_post_processor.py index 3572b74d..2e3330e7 100644 --- a/utils/audio/edit_post_processor.py +++ b/utils/audio/edit_post_processor.py @@ -28,23 +28,48 @@ _audio_editor_node = None # Global settings for inline edit tags (set from ComfyUI settings menu) _inline_tag_settings = { "precision": "auto", - "device": "auto" + "device": "auto", + "vc_engine": "chatterbox_23lang" # Default VC engine for tags } +# Settings cache file to persist across module reloads +import os +import json +_settings_cache_file = os.path.join(os.path.dirname(__file__), ".inline_tag_settings_cache.json") -def set_inline_tag_settings(precision: str = "auto", device: str = "auto"): +# Load settings from cache on module import +try: + if os.path.exists(_settings_cache_file): + with open(_settings_cache_file, 'r') as f: + cached_settings = json.load(f) + _inline_tag_settings.update(cached_settings) +except Exception: + pass + + +def set_inline_tag_settings(precision: str = "auto", device: str = "auto", vc_engine: str = "chatterbox_23lang"): """ - Set global settings for inline edit tag processing. + Set global settings for inline edit tag processing and voice restoration. Called from the API endpoint when user changes settings in ComfyUI menu. Args: precision: Model precision (auto, fp32, fp16, bf16, int8, int4) device: Device (auto, cuda, cpu, xpu) + vc_engine: Voice conversion engine for tags (chatterbox_23lang, chatterbox, cosyvoice) """ global _inline_tag_settings _inline_tag_settings["precision"] = precision _inline_tag_settings["device"] = device + _inline_tag_settings["vc_engine"] = vc_engine print(f"🎨 Step Audio EditX inline tags: precision={precision}, device={device}") + print(f"🔄 Voice restoration engine: {vc_engine}") + + # Save to cache file to persist across module reloads + try: + with open(_settings_cache_file, 'w') as f: + json.dump(_inline_tag_settings, f) + except Exception as e: + print(f"⚠️ Failed to save settings cache: {e}") def get_inline_tag_settings() -> Dict[str, str]: @@ -194,22 +219,33 @@ def _apply_edit_via_node( # Global cache for Voice Changer node (lazy-loaded) _cached_vc_node = None -def _restore_voice_via_vc(edited_audio_dict, original_voice_dict, iterations=1, language="English"): +def _restore_voice_via_vc(edited_audio_dict, original_voice_dict, iterations=1, language="English", vc_engine=None): """ - Apply ChatterBox Official 23-Lang Voice Changer to restore original voice. + Apply voice conversion to restore original voice. Reuses existing UnifiedVoiceChangerNode logic with its cache system. + Supports multiple VC engines: + - chatterbox_23lang: ChatterBox Official 23-Lang (default, 23 languages) + - chatterbox: ChatterBox Classic (English, German, Norwegian) + - cosyvoice: CosyVoice3 native VC (24kHz output, multilingual) + Args: edited_audio_dict: Audio after edits {waveform, sample_rate} original_voice_dict: Original voice reference {waveform, sample_rate} iterations: Number of VC refinement passes (1-5) language: Language for VC model (e.g., "English", "Polish", etc.) + vc_engine: VC engine to use (defaults to global setting) Returns: dict: Restored audio {waveform, sample_rate} """ global _cached_vc_node + # Get VC engine from settings if not specified + if vc_engine is None: + settings = get_inline_tag_settings() + vc_engine = settings.get("vc_engine", "chatterbox_23lang") + # Lazy-load VC node instance (cache globally like Audio Editor) if _cached_vc_node is None: import os @@ -234,14 +270,32 @@ def _restore_voice_via_vc(edited_audio_dict, original_voice_dict, iterations=1, # Instantiate the VC node _cached_vc_node = vc_module.UnifiedVoiceChangerNode() - # Create engine config for ChatterBox 23-Lang VC - engine_config = { - 'engine_type': 'chatterbox_official_23lang', - 'config': { - 'device': 'auto', - 'language': language + # Create engine config based on selected VC engine + if vc_engine == "cosyvoice": + engine_config = { + 'engine_type': 'cosyvoice', + 'config': { + 'device': 'auto', + 'mode': 'zero_shot', # Use zero_shot for VC + 'speed': 1.0 + } + } + elif vc_engine == "chatterbox": + engine_config = { + 'engine_type': 'chatterbox', + 'config': { + 'device': 'auto', + 'language': language + } + } + else: # chatterbox_23lang (default) + engine_config = { + 'engine_type': 'chatterbox_official_23lang', + 'config': { + 'device': 'auto', + 'language': language + } } - } # Call existing VC node logic with refinement passes result = _cached_vc_node.convert_voice( diff --git a/web/tts_audio_suite_settings.js b/web/tts_audio_suite_settings.js index 7dbeb3d6..3f2ead3a 100644 --- a/web/tts_audio_suite_settings.js +++ b/web/tts_audio_suite_settings.js @@ -2,6 +2,39 @@ import { app } from '../../scripts/app.js' const SETTING_CATEGORY = "TTS Audio Suite"; const SETTING_SECTION_STEP_EDITX = "Step Audio EditX (Inline Tags)"; +const SETTING_SECTION_RESTORE_VC = "Voice Restoration ( Tags)"; + +// Shared function to send settings to backend +async function sendSettingsToBackend() { + try { + const precision = app.ui.settings.getSettingValue("TTSAudioSuite.InlineEditTags.Precision", "auto"); + const device = app.ui.settings.getSettingValue("TTSAudioSuite.InlineEditTags.Device", "auto"); + const vcEngine = app.ui.settings.getSettingValue("TTSAudioSuite.RestoreTags.VCEngine", "chatterbox_23lang"); + + console.log(`TTS Audio Suite: Sending settings to backend - precision=${precision}, device=${device}, vcEngine=${vcEngine}`); + + const response = await fetch("/api/tts-audio-suite/settings", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + precision: precision, + device: device, + vc_engine: vcEngine + }) + }); + + if (!response.ok) { + console.error(`TTS Audio Suite: Failed to send settings to backend (status ${response.status})`); + } else { + const result = await response.json(); + console.log("TTS Audio Suite: Backend confirmed settings:", result); + } + } catch (error) { + console.error("TTS Audio Suite: Error sending settings to backend:", error); + } +} app.registerExtension({ name: "TTS_Audio_Suite.Settings", @@ -29,63 +62,38 @@ app.registerExtension({ tooltip: "Device for Step Audio EditX model when using inline edit tags (, , etc.).\n" + "Auto selects best available device (cuda > xpu > cpu).\n" + - "Note: This ONLY affects inline edit tags. When using the Step Audio EditX node directly, use the node's device parameter instead.\n\n" + + "Note: This ONLY affects inline edit tags. When using the Step Audio EditX node directly, use the node's precision parameter instead.\n\n" + + "⚠️ IMPORTANT: Refresh browser (F5) after changing settings for them to take effect." + }, + { + id: "TTSAudioSuite.RestoreTags.VCEngine", + name: "Voice Conversion Engine for Tags", + type: "combo", + defaultValue: "chatterbox_23lang", + options: ["chatterbox_23lang", "chatterbox", "cosyvoice"], + category: [SETTING_CATEGORY, SETTING_SECTION_RESTORE_VC, "VC Engine"], + tooltip: + "Voice conversion engine used for tags in post-processing.\n" + + "ChatterBox 23-Lang (default): Best quality, supports 23 languages\n" + + "ChatterBox: Original engine, English/German/Norwegian\n" + + "CosyVoice: New option with native CosyVoice3 VC (24kHz output)\n\n" + "⚠️ IMPORTANT: Refresh browser (F5) after changing settings for them to take effect." } ], async setup() { - // Send settings to Python backend whenever they change - const sendSettingsToBackend = async () => { - try { - const precision = app.ui.settings.getSettingValue("TTSAudioSuite.InlineEditTags.Precision", "auto"); - const device = app.ui.settings.getSettingValue("TTSAudioSuite.InlineEditTags.Device", "auto"); - - console.log(`TTS Audio Suite: Sending settings to backend - precision=${precision}, device=${device}`); - - const response = await fetch("/api/tts-audio-suite/settings", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - precision: precision, - device: device - }) - }); - - if (!response.ok) { - console.error(`TTS Audio Suite: Failed to send settings to backend (status ${response.status})`); - } else { - const result = await response.json(); - console.log("TTS Audio Suite: Backend confirmed settings:", result); - } - } catch (error) { - console.error("TTS Audio Suite: Error sending settings to backend:", error); - } - }; - // Send settings on initial load (with delay to ensure settings are loaded) setTimeout(() => { sendSettingsToBackend(); console.log("TTS Audio Suite: Initial settings sent to backend"); }, 1000); - // Send settings before each prompt execution - app.registerExtension({ - name: "TTS_Audio_Suite.Settings.BeforePrompt", - async beforeQueuing() { - await sendSettingsToBackend(); - return null; - } - }); - // Listen for setting changes and send to backend const originalChangeSettings = app.ui.settings.setSettingValue; app.ui.settings.setSettingValue = function(id, value) { originalChangeSettings.call(this, id, value); // If it's one of our settings, send to backend immediately - if (id.startsWith("TTSAudioSuite.InlineEditTags.")) { + if (id.startsWith("TTSAudioSuite.InlineEditTags.") || id.startsWith("TTSAudioSuite.RestoreTags.")) { sendSettingsToBackend(); } }; @@ -93,3 +101,12 @@ app.registerExtension({ console.log("TTS Audio Suite: Settings registered and synced with backend"); } }) + +// Register separate extension for beforeQueuing hook (must be at top level) +app.registerExtension({ + name: "TTS_Audio_Suite.Settings.BeforePrompt", + async beforeQueuing() { + await sendSettingsToBackend(); + return null; + } +})