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-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