release: v2.0.0 (2026/10/13)

This commit is contained in:
AILab
2026-10-13 16:51:40 -07:00
parent 6a7dba3511
commit f29ff3a2d1
11 changed files with 946 additions and 118 deletions
+14 -7
View File
@@ -1,15 +1,21 @@
# ComfyUI Audio Nodes
# ComfyUI-EdgeTTS
<p align="center">
<img src="https://img.shields.io/badge/ComfyUI-Custom--Node-blue?style=for-the-badge&logo=comfyui" alt="ComfyUI">
<img src="https://img.shields.io/badge/PyTorch-%3E=2.0-red?style=for-the-badge&logo=pytorch" alt="PyTorch">
<img src="https://img.shields.io/badge/License-GPL--3.0-green?style=for-the-badge" alt="GPL-3.0">
</p>
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
+70 -69
View File
@@ -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"]
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')
+6 -6
View File
@@ -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"
+62 -29
View File
@@ -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):
+397
View File
@@ -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("<II", digest)
key_a |= 1
key_b |= 1
factors = (
(key_a * _CS64_FACTOR) & mask,
(key_b * _CS64_FACTOR) & mask,
)
terms = [
(coefficient * factors[index % 2]) & mask
for index, (coefficient, _) in enumerate(_CS64_COEFFICIENTS)
]
offsets = [offset for _, offset in _CS64_COEFFICIENTS]
for index, word in enumerate(_words_le4_even(b"\x00" + data)):
term = word * (key_a if index % 2 == 0 else key_b)
offset = _rol16(word) * (0xEF0569FB if index % 2 == 0 else 0xC31713DB)
if index == 0:
term += _CS64_ADJUSTMENT0 * factors[0]
offset += _CS64_ADJUSTMENT1
terms.append(term & mask)
offsets.append(offset & mask)
swap_acc = swap_last = state = acc = 0
for index in range(0, len(terms), 2):
x, y = terms[index], terms[index + 1]
t = _rol16((offsets[index] + x) & mask)
q = (t * 0x76F50F34) & mask
swap_acc += (_rol16(q) * 0xC3EFEA97 + q) & mask
t = _rol16((offsets[index + 1] + y) & mask)
q = (t * 0x1881CE3C) & mask
swap_last = (_rol16(q) * 0x35BD1EC9 + q) & mask
swap_acc = (swap_acc + swap_last) & mask
state = (state * key_a + x) & mask
state = (_rol16(state) * 0xCF98B111) & mask
state = (_rol16(state) * 0x87085B9F) & mask
state = (_rol16(state) * 0x12CEB96D) & mask
state = (_rol16(state) * 0x257E1D83) & mask
acc += state
t = (state * key_b + y) & mask
t = (_rol16(t) * 0xA27416F5) & mask
t = (_rol16(t) * 0xD38396FF) & mask
state = (_rol16((_rol16(t) * 0x7C932B89) & mask) * 0xBFA49F69) & mask
acc = (acc + state) & mask
return swap_last ^ state, swap_acc ^ acc
@lru_cache(maxsize=1)
def _token_hash(data: bytes) -> Tuple[str, str]:
digest = _sha256_digest(data)
cs64 = struct.pack("<II", *_cs64_digest(data, digest)).hex().upper()
return cs64, digest.hex().upper()
def generate_sec_ms_gec_v2(unix_timestamp: Optional[float] = None) -> 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)
+11 -4
View File
@@ -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)}")
Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

@@ -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": {}
}
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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
+21
View File
@@ -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