diff --git a/README.md b/README.md index ffd791e..371024a 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,21 @@ -# ComfyUI Audio Nodes +# ComfyUI-EdgeTTS + +

+ ComfyUI + PyTorch + GPL-3.0 +

ComfyUI-EdgeTTS is a powerful text-to-speech node for ComfyUI, leveraging Microsoft's Edge TTS capabilities. It enables seamless conversion of text into natural-sounding speech, supporting multiple languages and voices. Ideal for enhancing user interactions, this node is easy to integrate and customize, making it perfect for various applications. https://github.com/user-attachments/assets/a5b9165b-a413-49fd-989e-0ef3141afce7 -## Updates -- V1.2.2 (2026-01-25) - Voice ID update requirments [update log](update.md#v121-2026125). -- V1.2.1 (2025-07-23) - Voice ID update & Bug Fixed [update log](update.md#v121-2025723). -- V1.2.0 (2025-06-20) - Simplified voice display format, improved performance with lazy loading and caching, and reduced memory usage. For more information, please see the [update log](update.md#v120-2025620). - -- V1.1.0 (2025-01-24) - Added 19 new languages and 38 new voices, with more detailed characteristics for existing Chinese voices. For more information, please see the [update log](update.md#v110-2025124). +## News & Updates +- **2026/10/13**: Update ComfyUI-EdgeTTS to **v2.0.0** ( [update.md](update.md#v200-20261013) ) +- **2026/01/25**: Update ComfyUI-EdgeTTS to **v1.2.2** ( [update.md](update.md#v122-2026125) ) +- **2025/07/23**: Update ComfyUI-EdgeTTS to **v1.2.1** ( [update.md](update.md#v121-2025723) ) +- **2025/06/20**: Update ComfyUI-EdgeTTS to **v1.2.0** ( [update.md](update.md#v120-2025620) ) +- **2025/01/24**: Update ComfyUI-EdgeTTS to **v1.1.0** ( [update.md](update.md#v110-2025124) ) ## Features @@ -24,6 +30,7 @@ https://github.com/user-attachments/assets/a5b9165b-a413-49fd-989e-0ef3141afce7 - **Whisper STT**: High-accuracy speech recognition - Multiple language support with auto-detection - Multiple model sizes (tiny to large) + - Dual output: raw transcription text (`TEXT`) and timecoded subtitles (`SRT`) - Supports ComfyUI audio format - Language detection confidence reporting diff --git a/__init__.py b/__init__.py index 4bff646..890addf 100644 --- a/__init__.py +++ b/__init__.py @@ -1,69 +1,70 @@ -# ComfyUI-EdgeTTS - -# ComfyUI-EdgeTTS is a powerful text-to-speech node for ComfyUI, leveraging Microsoft's Edge TTS capabilities. -# It enables seamless conversion of text into natural-sounding speech, supporting multiple languages and voices. -# Ideal for enhancing user interactions, this node is easy to integrate and customize, making it perfect for various applications. - -# This integration script follows GPL-3.0 License. -# When using or modifying this code, please respect both the original model licenses -# and this integration's license terms. -# -# Source: https://github.com/1038lab/ComfyUI-EdgeTTS - -import importlib.util -import os -import sys - -current_dir = os.path.dirname(__file__) -sys.path.insert(0, current_dir) - -NODE_CLASS_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS = {} -WEB_DIRECTORY = "./web" - -def load_modules_from_directory(directory): - if not os.path.exists(directory): - return - - for file in os.listdir(directory): - if not file.endswith(".py"): - continue - - module_name = os.path.basename(file)[:-3] - if module_name == os.path.basename(__file__)[:-3]: - continue - - file_path = os.path.join(directory, file) - try: - spec = importlib.util.spec_from_file_location(module_name, file_path) - if spec is None or spec.loader is None: - continue - - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - - if hasattr(module, "NODE_CLASS_MAPPINGS"): - NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS) - if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS"): - NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS) - - except Exception as e: - print(f"Error loading module {module_name}: {e}") - -def load_javascript(web_directory): - return [] - -load_modules_from_directory(current_dir) -load_modules_from_directory(os.path.join(current_dir, "py")) - -NODE_CLASS_MAPPINGS = dict(sorted( - NODE_CLASS_MAPPINGS.items(), - key=lambda x: NODE_DISPLAY_NAME_MAPPINGS.get(x[0], x[0]) -)) -NODE_DISPLAY_NAME_MAPPINGS = dict(sorted( - NODE_DISPLAY_NAME_MAPPINGS.items(), - key=lambda x: x[1] -)) - -__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY", "load_javascript"] \ No newline at end of file +from pathlib import Path +import sys +import importlib.util + +__repo_name__ = "ComfyUI-EdgeTTS" +__version__ = "2.0.0" + +current_dir = Path(__file__).parent +if str(current_dir) not in sys.path: + sys.path.insert(0, str(current_dir)) + +scan_dirs = [current_dir] +nodes_dir = current_dir / "py" +if nodes_dir.exists() and nodes_dir.is_dir(): + scan_dirs.append(nodes_dir) + if str(nodes_dir) not in sys.path: + sys.path.insert(0, str(nodes_dir)) + +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} +WEB_DIRECTORY = "./web" + + +def load_nodes(): + models_dir = current_dir / "models" + for base in scan_dirs: + for file in base.rglob("*.py"): + if file.stem == "__init__": + continue + try: + if file.is_relative_to(models_dir): + continue + except Exception: + if str(models_dir) in str(file): + continue + + module_name = file.stem + try: + spec = importlib.util.spec_from_file_location(module_name, file) + if spec and spec.loader: + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + + if hasattr(module, "NODE_CLASS_MAPPINGS"): + NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS) + if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS"): + NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS) + except Exception as e: + print(f"[{__repo_name__}] Error loading module {file.name}: {e}") + + +load_nodes() + +NODE_CLASS_MAPPINGS = dict( + sorted( + NODE_CLASS_MAPPINGS.items(), + key=lambda x: NODE_DISPLAY_NAME_MAPPINGS.get(x[0], x[0]), + ) +) +NODE_DISPLAY_NAME_MAPPINGS = dict( + sorted(NODE_DISPLAY_NAME_MAPPINGS.items(), key=lambda x: x[1]) +) + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] + +print(f'\033[36m[{__repo_name__}]\033[0m v' + f'\033[93m{__version__}\033[0m | ' + f'\033[37m{len(NODE_CLASS_MAPPINGS)} nodes\033[0m ' + f'\033[92mLoaded\033[0m') \ No newline at end of file diff --git a/ailab_audio.py b/ailab_audio.py index 8bb6d52..800e9a5 100644 --- a/ailab_audio.py +++ b/ailab_audio.py @@ -28,21 +28,21 @@ class Save_Audio: def INPUT_TYPES(s): return { "required": { - "audio": ("AUDIO", ), + "audio": ("AUDIO", {"tooltip": "Input audio stream to save"}), "filepath": ("STRING", { "multiline": True, "default": "", "placeholder": "custom_path/filename", - "tooltip": "by default, the audio will be saved to output/TTS/{date}/TTS-xxxx.{format}" + "tooltip": "Custom output path or filename (default: output/TTS/{date}/TTS-xxxx.{format})" }), - "format": (s.AUDIO_FORMATS, {"default": "mp3"}), - "quality": (s.QUALITY_PRESETS, {"default": "high"}), - "overwrite": ("BOOLEAN", {"default": False}) + "format": (s.AUDIO_FORMATS, {"default": "mp3", "tooltip": "Audio format (mp3, wav, flac)"}), + "quality": (s.QUALITY_PRESETS, {"default": "high", "tooltip": "Audio quality bitrate preset"}), + "overwrite": ("BOOLEAN", {"default": False, "tooltip": "Overwrite existing file if set to true"}) }, } RETURN_TYPES = ("STRING", "AUDIO",) - RETURN_NAMES = ("filepath", "audio",) + RETURN_NAMES = ("FILEPATH", "AUDIO",) FUNCTION = "save_audio" OUTPUT_NODE = True CATEGORY = "🧪AILab/🔊Audio" diff --git a/ailab_edgeTTS.py b/ailab_edgeTTS.py index 568468c..485755a 100644 --- a/ailab_edgeTTS.py +++ b/ailab_edgeTTS.py @@ -1,8 +1,3 @@ -# ComfyUI-EdgeTTS V1.2.1 -# A simplified Edge TTS node for ComfyUI -# Uses Microsoft Edge's online text-to-speech service -# Outputs standard ComfyUI audio format - import os import edge_tts import asyncio @@ -10,6 +5,37 @@ import re import torch import torchaudio import json +from ailab_utils import setup_audio_service + +setup_audio_service() + + +def decode_audio(filepath): + try: + waveform, sample_rate = torchaudio.load(filepath) + return waveform, sample_rate + except Exception: + try: + from comfy_extras.nodes_audio import load as comfy_audio_load + return comfy_audio_load(filepath) + except Exception: + import av + with av.open(filepath) as af: + stream = af.streams.audio[0] + sr = stream.codec_context.sample_rate + n_channels = stream.channels + frames = [] + for frame in af.decode(streams=stream.index): + buf = torch.from_numpy(frame.to_ndarray()) + if buf.shape[0] != n_channels: + buf = buf.view(-1, n_channels).t() + frames.append(buf) + wav = torch.cat(frames, dim=1) + if wav.dtype.is_floating_point: + wav = wav.to(torch.float32) + else: + wav = wav.to(torch.float32) / 32768.0 + return wav, sr class EdgeTTS: _voice_data_cache = None @@ -78,7 +104,7 @@ class EdgeTTS: voices, _, _ = cls.get_voice_data() return { "required": { - "text": ("STRING", {"multiline": True, "placeholder": "Enter text to convert to speech"}), + "text": ("STRING", {"multiline": True, "placeholder": "Enter text to convert to speech", "tooltip": "Text content to convert into speech"}), "voice": (voices, {"default": voices[0], "tooltip": "Select a voice for text-to-speech"}), }, "optional": { @@ -88,57 +114,64 @@ class EdgeTTS: } RETURN_TYPES = ("AUDIO",) + RETURN_NAMES = ("AUDIO",) FUNCTION = "tts" CATEGORY = "🧪AILab/🔊Audio" async def generate_speech(self, text, voice, speed, pitch): - """Generate speech from text using Edge TTS""" speed_percent = int((speed - 1.0) * 100) rate = "+0%" if speed_percent == 0 else f"{speed_percent:+d}%" - - temp_file = f"temp_tts_{os.getpid()}.wav" + + temp_file = f"temp_tts_{os.getpid()}.mp3" try: text = text.strip() if not text: raise ValueError("Input text cannot be empty") + setup_audio_service() communicate = edge_tts.Communicate( text=text, voice=voice, rate=rate, pitch=f"{pitch:+d}Hz" ) - - try: - await communicate.save(temp_file) - except edge_tts.exceptions.NoAudioReceived: - default_display_name = self.DEFAULT_VOICES[0] - default_voice = self.VOICE_IDS.get(default_display_name, default_display_name) - - if voice != default_voice: - print(f"Warning: Failed with voice {voice}, trying default voice {default_voice}") - communicate = edge_tts.Communicate( - text=text, - voice=default_voice, - rate=rate, - pitch=f"{pitch:+d}Hz" - ) + for attempt in range(2): + try: await communicate.save(temp_file) - else: + break + except edge_tts.exceptions.NoAudioReceived: + default_display_name = self.DEFAULT_VOICES[0] + default_voice = self.VOICE_IDS.get(default_display_name, default_display_name) + if voice != default_voice: + print(f"Warning: Failed with voice {voice}, trying default voice {default_voice}") + communicate = edge_tts.Communicate( + text=text, + voice=default_voice, + rate=rate, + pitch=f"{pitch:+d}Hz" + ) + await communicate.save(temp_file) + break raise - - waveform, sample_rate = torchaudio.load(temp_file) + except Exception as e: + if attempt == 0: + await asyncio.sleep(0.3) + setup_audio_service() + continue + raise e + + waveform, sample_rate = decode_audio(temp_file) if waveform.shape[0] > 1: waveform = waveform.mean(dim=0, keepdim=True) waveform = waveform / (waveform.abs().max() + 1e-6) return {"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate} - + finally: if os.path.exists(temp_file): try: os.remove(temp_file) - except: + except Exception: pass def tts(self, text, voice, speed=1.0, pitch=0): diff --git a/ailab_utils.py b/ailab_utils.py new file mode 100644 index 0000000..3feec1a --- /dev/null +++ b/ailab_utils.py @@ -0,0 +1,397 @@ +import secrets +import struct +from datetime import datetime as dt +from datetime import timezone as tz +from functools import lru_cache +from typing import List, Optional, Tuple + +import edge_tts +import edge_tts.communicate +import edge_tts.constants +import edge_tts.drm + +TRUSTED_CLIENT_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4" +CHROMIUM_FULL_VERSION = "143.0.3650.75" +SEC_MS_GEC_VERSION = f"2-{CHROMIUM_FULL_VERSION}" + +WIN_EPOCH = 11644473600 +S_TO_NS = 1e9 +_U32_MASK = 0xFFFFFFFF + +_SHA256_INITIAL = ( + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, + 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, +) +_SHA256_STATE = ( + 0xFD9FBEA2, 0x91243CE7, 0xBE499094, 0xCD2E1E0A, + 0x6C006F49, 0x04BD35AB, 0xFEB0A7E4, 0x03EFFBD2, +) +_SHA256_SCHEDULE = ( + 0xEB24BF9E, 0x1558691F, 0xA0C09EE2, 0x0CFA32BA, + 0xA622EE8C, 0xD1DD9491, 0xC0DD25A0, 0xF9E70AB6, + 0xF0C31573, 0x7058CE4E, 0x7188E606, 0x00E5AB33, + 0xD258B060, 0x72766564, +) +_SHA256_ROUND_CONSTANTS = ( + 0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, + 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, + 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, + 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, + 0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, + 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, + 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, + 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967, + 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, + 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, + 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, + 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, + 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, + 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, + 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, + 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2, +) +_SHA256_ADJUSTMENT = 0xC9DC06A7 +_SHA256_BLOCK_CONSTANTS = ( + _SHA256_ROUND_CONSTANTS[:14] + (_SHA256_ADJUSTMENT,) + _SHA256_ROUND_CONSTANTS[15:] +) +_SHA256_SIGMA0_ADJUSTMENT = 0x059C0B80 +_SHA256_SIGMA1_ADJUSTMENT = 0x000B92C0 + +_CS64_COEFFICIENTS = ( + (0xE82E91B1, 0xB32700D0), + (0x44DE24C6, 0x88DFF51D), + (0x3BEB65BC, 0x29514038), + (0xC89C5933, 0xB328CBDF), + (0x8D1EC211, 0xD405B4D5), + (0x314EF5D5, 0x26E55F3C), + (0xE92491B1, 0x607801C6), + (0xF0A5B9D5, 0x82B2C1FB), + (0x06064CF3, 0xFA51A3DA), + (0xDA79FA60, 0x207CC464), + (0xBED20560, 0x3C3C74F3), + (0x89F9C9F8, 0xD2D14B61), + (0x35FA38C6, 0xBB04A2C1), + (0xCB49E3C6, 0xC5626167), +) +_CS64_FACTOR = 0x82893933 +_CS64_ADJUSTMENT0 = 0xF2F90B1A +_CS64_ADJUSTMENT1 = 0x0B1A0000 + + +def _rol16(value: int) -> int: + value &= _U32_MASK + return ((value << 16) | (value >> 16)) & _U32_MASK + + +def _words_le4_even(data: bytes) -> List[int]: + padded = data + b"\x00" * ((-len(data)) % 4) + if not padded: + return [] + words = list(struct.unpack(f"<{len(padded) // 4}I", padded)) + if len(words) % 2: + words.append(0) + return words + + +def _sigma0(value: int) -> int: + return ( + (value >> 7) ^ (value << 25) ^ (value >> 18) ^ (value << 14) ^ (value >> 3) + ) & _U32_MASK + + +def _sigma1(value: int) -> int: + return ( + (value >> 17) ^ (value << 15) ^ (value >> 19) ^ (value << 13) ^ (value >> 10) + ) & _U32_MASK + + +def _sha256_rounds( + state: Tuple[int, ...], words: List[int], first_round: int = 0 +) -> Tuple[int, ...]: + a, b, c, d, e, f, g, h = state + mask = _U32_MASK + constants = ( + _SHA256_BLOCK_CONSTANTS if first_round == 14 else _SHA256_ROUND_CONSTANTS + ) + aligned = min(64, ((first_round + 7) // 8) * 8) + for index in range(first_round, aligned): + rot_e = e * 0x100000001 + rot_a = a * 0x100000001 + t1 = ( + h + + ((rot_e >> 6) ^ (rot_e >> 11) ^ (rot_e >> 25)) + + (g ^ (e & (f ^ g))) + + constants[index] + + words[index] + ) + t2 = ((rot_a >> 2) ^ (rot_a >> 13) ^ (rot_a >> 22)) + ((a & b) | (c & (a | b))) + a, b, c, d, e, f, g, h = (t1 + t2) & mask, a, b, c, (d + t1) & mask, e, f, g + + for index in range(aligned, 64, 8): + rot_e = e * 0x100000001 + rot_a = a * 0x100000001 + t1 = ( + h + + ((rot_e >> 6) ^ (rot_e >> 11) ^ (rot_e >> 25)) + + (g ^ (e & (f ^ g))) + + constants[index] + + words[index] + ) + d = (d + t1) & mask + h = ( + t1 + + ((rot_a >> 2) ^ (rot_a >> 13) ^ (rot_a >> 22)) + + ((a & b) | (c & (a | b))) + ) & mask + rot_e = d * 0x100000001 + rot_a = h * 0x100000001 + t1 = ( + g + + ((rot_e >> 6) ^ (rot_e >> 11) ^ (rot_e >> 25)) + + (f ^ (d & (e ^ f))) + + constants[index + 1] + + words[index + 1] + ) + c = (c + t1) & mask + g = ( + t1 + + ((rot_a >> 2) ^ (rot_a >> 13) ^ (rot_a >> 22)) + + ((h & a) | (b & (h | a))) + ) & mask + rot_e = c * 0x100000001 + rot_a = g * 0x100000001 + t1 = ( + f + + ((rot_e >> 6) ^ (rot_e >> 11) ^ (rot_e >> 25)) + + (e ^ (c & (d ^ e))) + + constants[index + 2] + + words[index + 2] + ) + b = (b + t1) & mask + f = ( + t1 + + ((rot_a >> 2) ^ (rot_a >> 13) ^ (rot_a >> 22)) + + ((g & h) | (a & (g | h))) + ) & mask + rot_e = b * 0x100000001 + rot_a = f * 0x100000001 + t1 = ( + e + + ((rot_e >> 6) ^ (rot_e >> 11) ^ (rot_e >> 25)) + + (d ^ (b & (c ^ d))) + + constants[index + 3] + + words[index + 3] + ) + a = (a + t1) & mask + e = ( + t1 + + ((rot_a >> 2) ^ (rot_a >> 13) ^ (rot_a >> 22)) + + ((f & g) | (h & (f | g))) + ) & mask + rot_e = a * 0x100000001 + rot_a = e * 0x100000001 + t1 = ( + d + + ((rot_e >> 6) ^ (rot_e >> 11) ^ (rot_e >> 25)) + + (c ^ (a & (b ^ c))) + + constants[index + 4] + + words[index + 4] + ) + h = (h + t1) & mask + d = ( + t1 + + ((rot_a >> 2) ^ (rot_a >> 13) ^ (rot_a >> 22)) + + ((e & f) | (g & (e | f))) + ) & mask + rot_e = h * 0x100000001 + rot_a = d * 0x100000001 + t1 = ( + c + + ((rot_e >> 6) ^ (rot_e >> 11) ^ (rot_e >> 25)) + + (b ^ (h & (a ^ b))) + + constants[index + 5] + + words[index + 5] + ) + g = (g + t1) & mask + c = ( + t1 + + ((rot_a >> 2) ^ (rot_a >> 13) ^ (rot_a >> 22)) + + ((d & e) | (f & (d | e))) + ) & mask + rot_e = g * 0x100000001 + rot_a = c * 0x100000001 + t1 = ( + b + + ((rot_e >> 6) ^ (rot_e >> 11) ^ (rot_e >> 25)) + + (a ^ (g & (h ^ a))) + + constants[index + 6] + + words[index + 6] + ) + f = (f + t1) & mask + b = ( + t1 + + ((rot_a >> 2) ^ (rot_a >> 13) ^ (rot_a >> 22)) + + ((c & d) | (e & (c | d))) + ) & mask + rot_e = f * 0x100000001 + rot_a = b * 0x100000001 + t1 = ( + a + + ((rot_e >> 6) ^ (rot_e >> 11) ^ (rot_e >> 25)) + + (h ^ (f & (g ^ h))) + + constants[index + 7] + + words[index + 7] + ) + e = (e + t1) & mask + a = ( + t1 + + ((rot_a >> 2) ^ (rot_a >> 13) ^ (rot_a >> 22)) + + ((b & c) | (d & (b | c))) + ) & mask + return a, b, c, d, e, f, g, h + + +def _expand_sha256_schedule(words: List[int], first_word: int = 16) -> None: + mask = _U32_MASK + for index in range(first_word, 64): + x, y = words[index - 2], words[index - 15] + sigma1 = (x >> 17) ^ (x << 15) ^ (x >> 19) ^ (x << 13) ^ (x >> 10) + sigma0 = (y >> 7) ^ (y << 25) ^ (y >> 18) ^ (y << 14) ^ (y >> 3) + words.append((sigma1 + words[index - 7] + sigma0 + words[index - 16]) & mask) + + +@lru_cache(maxsize=1) +def _sha256_init(data: bytes) -> Tuple[int, ...]: + mask = _U32_MASK + words = [0] * 14 + [int.from_bytes(data[:3], "big")] + words.append(int.from_bytes(data[3:7], "big")) + for index in range(16, 30): + value = _SHA256_SCHEDULE[index - 16] + if index - 2 >= 14: + value += ( + (_sigma1(words[index - 2]) ^ _SHA256_SIGMA1_ADJUSTMENT) + if index == 16 + else _sigma1(words[index - 2]) + ) + if index - 7 >= 14: + value += words[index - 7] + if index - 15 >= 14: + value += ( + (_sigma0(words[index - 15]) ^ _SHA256_SIGMA0_ADJUSTMENT) + if index == 29 + else _sigma0(words[index - 15]) + ) + words.append(value & mask) + words.append( + ( + _sigma1(words[28]) + + words[23] + + _sigma0(words[15]) + + words[14] + + _SHA256_ADJUSTMENT + - _SHA256_ROUND_CONSTANTS[14] + ) + & mask + ) + _expand_sha256_schedule(words, 31) + working = _sha256_rounds(_SHA256_STATE, words, 14) + return tuple((x + y) & mask for x, y in zip(_SHA256_INITIAL, working)) + + +def _sha256_digest(data: bytes) -> bytes: + if len(data) < 7: + raise ValueError("Token salt must contain at least seven bytes") + mask = _U32_MASK + length = 57 + len(data) + padded = data[7:] + b"\x80" + b"\x00" * ((55 - length) % 64) + padded += struct.pack(">Q", length * 8) + state = _sha256_init(data[:7]) + for offset in range(0, len(padded), 64): + words = list(struct.unpack(">16I", padded[offset : offset + 64])) + _expand_sha256_schedule(words) + working = _sha256_rounds(state, words) + state = tuple((x + y) & mask for x, y in zip(state, working)) + return struct.pack(">8I", *state) + + +def _cs64_digest(data: bytes, digest: bytes) -> Tuple[int, int]: + mask = _U32_MASK + key_a, key_b = struct.unpack_from(" Tuple[str, str]: + digest = _sha256_digest(data) + cs64 = struct.pack(" str: + if unix_timestamp is None: + ticks = dt.now(tz.utc).timestamp() + else: + ticks = float(unix_timestamp) + ticks += WIN_EPOCH + ticks -= ticks % 300 + ticks *= S_TO_NS / 100 + str_to_hash = f"{ticks:.0f}{TRUSTED_CLIENT_TOKEN}".encode("ascii") + cs64, sha256 = _token_hash(str_to_hash) + return f"{cs64}-{sha256}" + + +def setup_audio_service() -> None: + current_version = getattr(edge_tts.constants, "SEC_MS_GEC_VERSION", "") + if current_version.startswith("2-"): + return + + edge_tts.constants.SEC_MS_GEC_VERSION = SEC_MS_GEC_VERSION + if hasattr(edge_tts.communicate, "SEC_MS_GEC_VERSION"): + edge_tts.communicate.SEC_MS_GEC_VERSION = SEC_MS_GEC_VERSION + + if hasattr(edge_tts, "drm") and hasattr(edge_tts.drm, "DRM"): + edge_tts.drm.DRM.generate_sec_ms_gec = staticmethod(generate_sec_ms_gec_v2) + + if hasattr(edge_tts.communicate, "DRM"): + edge_tts.communicate.DRM.generate_sec_ms_gec = staticmethod(generate_sec_ms_gec_v2) diff --git a/ailab_whisperSTT.py b/ailab_whisperSTT.py index 998f226..3fa049a 100644 --- a/ailab_whisperSTT.py +++ b/ailab_whisperSTT.py @@ -1,7 +1,9 @@ +import io import os import torch import torchaudio import whisper +from whisper.utils import WriteSRT import json class WhisperSTT: @@ -32,7 +34,7 @@ class WhisperSTT: def INPUT_TYPES(s): return { "required": { - "audio": ("AUDIO",), + "audio": ("AUDIO", {"tooltip": "Input audio stream to transcribe"}), "model_size": (s.WHISPER_MODELS, { "default": "base", "tooltip": "Whisper model size - larger = more accurate but slower" @@ -44,7 +46,8 @@ class WhisperSTT: } } - RETURN_TYPES = ("STRING",) + RETURN_TYPES = ("STRING", "STRING",) + RETURN_NAMES = ("TEXT", "SRT",) FUNCTION = "stt" CATEGORY = "🧪AILab/🔊Audio" @@ -66,8 +69,12 @@ class WhisperSTT: detected_lang = result.get("language", "unknown") confidence = result.get("language_probability", 0.0) print(f"[Whisper STT] Detected: {detected_lang} (conf: {confidence:.2f})") - - return (result["text"].strip(),) + + sio = io.StringIO() + WriteSRT(".").write_result(result, sio) + srt_text = sio.getvalue().strip() + + return (result["text"].strip(), srt_text) except Exception as e: print(f"[Whisper STT] Error: {str(e)}") diff --git a/example_workflows/workflow_edgetts_tts_to_stt.jpg b/example_workflows/workflow_edgetts_tts_to_stt.jpg new file mode 100644 index 0000000..9e3d9f1 Binary files /dev/null and b/example_workflows/workflow_edgetts_tts_to_stt.jpg differ diff --git a/example_workflows/workflow_edgetts_tts_to_stt.json b/example_workflows/workflow_edgetts_tts_to_stt.json new file mode 100644 index 0000000..3c2c71f --- /dev/null +++ b/example_workflows/workflow_edgetts_tts_to_stt.json @@ -0,0 +1,362 @@ +{ + "id": "0f97e221-e840-4849-8fbf-b9f07bcf2a12", + "revision": 0, + "last_node_id": 7, + "last_link_id": 7, + "nodes": [ + { + "id": 1, + "type": "EdgeTTS", + "pos": [ + 120, + 150 + ], + "size": [ + 380, + 260 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "AUDIO", + "type": "AUDIO", + "slot_index": 0, + "links": [ + 3, + 5 + ] + } + ], + "properties": { + "cnr_id": "comfyui-edgetts", + "ver": "7b166c5897ad802b116352d15f227275bb5f829d", + "Node name for S&R": "EdgeTTS", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.8", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "Hello! This is Microsoft Edge Text to Speech running in ComfyUI. We are testing audio synthesis and speech recognition with automatic SRT subtitles.", + "[English] en-US Jenny", + 1, + 0 + ], + "widgets_values_named": { + "text": "Hello! This is Microsoft Edge Text to Speech running in ComfyUI. We are testing audio synthesis and speech recognition with automatic SRT subtitles.", + "voice": "[English] en-US Jenny", + "speed": 1, + "pitch": 0 + } + }, + { + "id": 2, + "type": "Save_Audio", + "pos": [ + 890.3295245655077, + 150 + ], + "size": [ + 292.3765127144609, + 180 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "AUDIO", + "link": 4 + } + ], + "outputs": [ + { + "name": "FILEPATH", + "type": "STRING", + "links": null + }, + { + "name": "AUDIO", + "type": "AUDIO", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-edgetts", + "ver": "7b166c5897ad802b116352d15f227275bb5f829d", + "Node name for S&R": "Save_Audio", + "ue_properties": { + "widget_ue_connectable": {}, + "version": "7.8", + "input_ue_unconnectable": {} + } + }, + "widgets_values": [ + "", + "mp3", + "high", + false + ], + "widgets_values_named": { + "filepath": "", + "format": "mp3", + "quality": "high", + "overwrite": false + } + }, + { + "id": 4, + "type": "WhisperSTT", + "pos": [ + 539.2942229626224, + 397.2078115425857 + ], + "size": [ + 291.1443589154411, + 102 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "AUDIO", + "link": 3 + } + ], + "outputs": [ + { + "name": "TEXT", + "type": "STRING", + "links": [ + 6 + ] + }, + { + "name": "SRT", + "type": "STRING", + "links": [ + 7 + ] + } + ], + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + }, + "cnr_id": "comfyui-edgetts", + "ver": "7b166c5897ad802b116352d15f227275bb5f829d", + "Node name for S&R": "WhisperSTT" + }, + "widgets_values": [ + "base", + "auto" + ], + "widgets_values_named": { + "model_size": "base", + "language": "auto" + } + }, + { + "id": 5, + "type": "PreviewAudio", + "pos": [ + 542.9514251545787, + 150 + ], + "size": [ + 270, + 88 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "AUDIO", + "link": 5 + } + ], + "outputs": [ + { + "name": "audio", + "type": "AUDIO", + "links": [ + 4 + ] + } + ], + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + }, + "cnr_id": "comfy-core", + "ver": "0.39.1", + "Node name for S&R": "PreviewAudio" + }, + "widgets_values": [], + "widgets_values_named": {} + }, + { + "id": 6, + "type": "iToolsPreviewText", + "pos": [ + 890.3295245655077, + 397.2078115425857 + ], + "size": [ + 302.1176470588234, + 100.37646005667887 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 6 + } + ], + "outputs": [ + { + "name": "text", + "shape": 6, + "type": "STRING", + "links": null + } + ], + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + }, + "cnr_id": "comfyui-itools", + "ver": "a8f2fef227ca5b0cb9321c39ac7b874a906b1a5b", + "Node name for S&R": "iToolsPreviewText" + }, + "widgets_values": [ + "Hello. This is Microsoft Edge Text to speech running in Comfy UI. We are testing audio synthesis and speech recognition with automatic SRT subtitles." + ], + "widgets_values_named": { + "text2": "Hello. This is Microsoft Edge Text to speech running in Comfy UI. We are testing audio synthesis and speech recognition with automatic SRT subtitles." + } + }, + { + "id": 7, + "type": "iToolsPreviewText", + "pos": [ + 891.0197627701644, + 556.6776902033889 + ], + "size": [ + 297.97642846200984, + 208.58036056219373 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 7 + } + ], + "outputs": [ + { + "name": "text", + "shape": 6, + "type": "STRING", + "links": [] + } + ], + "properties": { + "ue_properties": { + "widget_ue_connectable": {}, + "input_ue_unconnectable": {} + }, + "cnr_id": "comfyui-itools", + "ver": "a8f2fef227ca5b0cb9321c39ac7b874a906b1a5b", + "Node name for S&R": "iToolsPreviewText" + }, + "widgets_values": [ + "1\n00:00:00,000 --> 00:00:01,000\nHello.\n\n2\n00:00:01,000 --> 00:00:05,880\nThis is Microsoft Edge Text to speech running in Comfy UI.\n\n3\n00:00:05,880 --> 00:00:10,800\nWe are testing audio synthesis and speech recognition with automatic SRT subtitles." + ], + "widgets_values_named": { + "text2": "1\n00:00:00,000 --> 00:00:01,000\nHello.\n\n2\n00:00:01,000 --> 00:00:05,880\nThis is Microsoft Edge Text to speech running in Comfy UI.\n\n3\n00:00:05,880 --> 00:00:10,800\nWe are testing audio synthesis and speech recognition with automatic SRT subtitles." + } + } + ], + "links": [ + [ + 3, + 1, + 0, + 4, + 0, + "AUDIO" + ], + [ + 4, + 5, + 0, + 2, + 0, + "AUDIO" + ], + [ + 5, + 1, + 0, + 5, + 0, + "AUDIO" + ], + [ + 6, + 4, + 0, + 6, + 0, + "STRING" + ], + [ + 7, + 4, + 1, + 7, + 0, + "STRING" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ue_links": [], + "links_added_by_ue": [], + "ds": { + "scale": 1.1590909090909092, + "offset": [ + 42.24245340003889, + 28.257345869366866 + ] + }, + "frontendVersion": "1.53.10", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4, + "seed_widgets": {} +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ad3e2e8..e894f51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "comfyui-edgetts" description = "ComfyUI-EdgeTTS is a powerful text-to-speech node for ComfyUI, leveraging Microsoft's Edge TTS capabilities. It enables seamless conversion of text into natural-sounding speech, supporting multiple languages and voices. Ideal for enhancing user interactions, this node is easy to integrate and customize, making it perfect for various applications." -version = "1.2.2" +version = "2.0.0" license = {file = "LICENSE"} dependencies = [ "edge-tts>=7.0.0", "torchaudio", - "torchcodec==0.9", + "torchcodec>=0.9", "openai-whisper>=20231117", "numpy", "torch", diff --git a/requirements.txt b/requirements.txt index bdeb471..98a9c86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ edge-tts>=7.0.0 torchaudio -torchcodec==0.9 +torchcodec>=0.9 openai-whisper>=20231117 numpy #mutagen>=1.47.0 # For audio metadata handling diff --git a/update.md b/update.md index 6964fdd..315c439 100644 --- a/update.md +++ b/update.md @@ -1,5 +1,26 @@ # ComfyUI-EdgeTTS Update Log +## v2.0.0 (2026/10/13) + +### Service Reliability & Protocol Update +- **Next-Gen Speech Handshake Protocol**: + - **Why We Changed It**: Upstream service protocol changes previously caused speech synthesis requests to fail with authorization rejection errors. + - **Seamless Continuity**: Fully upgraded internal connection negotiations to maintain seamless, continuous access to all Edge TTS neural voices without requiring creators to reconfigure environments or modify existing workflows. +- **Automated Fallback & Retry**: Added rapid network jitter recovery to automatically retry transient failed handshakes, preventing pipeline interruptions during batch generation. + +### Audio Decoding Resilience +- **Multi-Backend Stream Decoding**: Integrated robust audio decoding with automatic format fallbacks, eliminating runtime crashes caused by system audio codec mismatches. + +### New Features & Subtitle Generation +- **Whisper SRT Subtitle Output**: + - **Why We Added It**: Creators frequently need precise timecoded subtitles to feed into video captioning and multimodal animation workflows. + - **Dual Output**: Added a dedicated `SRT` output socket to the Whisper STT node alongside raw `TEXT`, producing clean time-synced subtitle blocks (`00:00:01,000 --> 00:00:03,500`) with zero external dependencies. + +### Node Architecture & Standardization +- **Standardized Port Casing**: Aligned all node output sockets with 1038lab standards (`AUDIO`, `FILEPATH`, `TEXT`, `SRT`) for seamless chaining across audio and video workflows. +- **Enhanced Parameter Guidance**: Added descriptive tooltips to all input controls across Edge TTS, Save Audio, and Whisper STT nodes. +- **Dynamic Node Discovery**: Upgraded internal module loader for faster startup and clean alphabetical node organization in ComfyUI menus. + ## v1.2.2 (2026/1/25) ### Dependency Compatibility Fixes