Files
ComfyUI-Omnivoice/nodes/generator.py
T
Ethanfel 30f46fc3ef Revert transformers cap; catch torchcodec ASR failure with clear message
install.py: restore transformers>=5.0.0 (capping it would break other nodes).
generator.py: catch the torchcodec RuntimeError that fires when ref_text is
blank and transformers 5.x auto-transcription requires missing FFmpeg libs.
Raises a human-readable error telling the user to fill in ref_text manually.
Also updates the ref_text tooltip to recommend providing it explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 17:35:54 +02:00

147 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import tempfile
import os
import torch
import soundfile as sf
class OmniVoiceGenerate:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("OMNIVOICE_MODEL", {
"tooltip": "OmniVoice model loaded by the OmniVoice Model Loader node.",
}),
"text": ("STRING", {
"multiline": True,
"default": "",
"tooltip": (
"Text to synthesize. Supports inline tags for expression and pronunciation:\n"
"\n"
"NON-VERBAL SOUNDS:\n"
" [laughter] insert a laugh\n"
" [sigh] insert a sigh\n"
"\n"
"QUESTION / CONFIRMATION:\n"
" [question-en] rising English question intonation\n"
" [confirmation-en] confirmation sound\n"
"\n"
"SURPRISE:\n"
" [surprise-ah] [surprise-oh] [surprise-wa] [surprise-yo]\n"
"\n"
"DISSATISFACTION:\n"
" [dissatisfaction-hnn]\n"
"\n"
"ENGLISH PRONUNCIATION (CMU phoneme override):\n"
" You could probably still make [IH1 T] look good.\n"
"\n"
"CHINESE PRONUNCIATION (pinyin + tone number):\n"
" 严重SHE2本了\n"
"\n"
"EXAMPLE:\n"
" [laughter] You really got me. I didn't see that coming at all."
),
}),
"mode": (
["voice_cloning", "voice_design", "auto_voice"],
{
"default": "voice_cloning",
"tooltip": (
"voice_cloning clone the voice from ref_audio (requires ref_audio)\n"
"voice_design describe a voice with the instruct field (requires instruct)\n"
"auto_voice model picks a voice automatically"
),
},
),
},
"optional": {
"ref_audio": ("AUDIO", {
"tooltip": "Reference audio clip to clone the voice from. Used in voice_cloning mode.",
}),
"ref_text": ("STRING", {
"default": "",
"tooltip": "Transcription of ref_audio. Strongly recommended: type it manually. Auto-transcription requires FFmpeg shared libraries; if absent (e.g. some Docker images), generation will fail with a clear error message.",
}),
"instruct": ("STRING", {
"default": "",
"tooltip": (
"Voice description for voice_design mode. Combine attributes freely.\n"
"\n"
"GENDER: male, female\n"
"AGE: child, teenager, young adult, middle-aged, elderly\n"
"PITCH: very low, low, moderate, high, very high\n"
"STYLE: whisper\n"
"\n"
"ENGLISH ACCENTS (text must be English):\n"
" american, british, australian, canadian,\n"
" indian, chinese, korean, japanese, portuguese, russian\n"
"\n"
"EXAMPLE: female, high pitch, british accent"
),
}),
"speed": ("FLOAT", {
"default": 1.0, "min": 0.1, "max": 3.0, "step": 0.1,
"tooltip": "Playback speed multiplier. 1.0 = normal, >1.0 = faster, <1.0 = slower.",
}),
"num_step": ("INT", {
"default": 32, "min": 1, "max": 100,
"tooltip": "Diffusion steps. 32 = default quality. 16 = faster, slightly lower quality.",
}),
},
}
RETURN_TYPES = ("AUDIO",)
RETURN_NAMES = ("audio",)
FUNCTION = "generate"
CATEGORY = "OmniVoice"
def generate(self, model, text, mode, ref_audio=None, ref_text="", instruct="", speed=1.0, num_step=32):
kwargs = {"text": text, "speed": speed, "num_step": num_step}
if mode == "voice_cloning" and ref_audio is None:
raise ValueError("voice_cloning mode requires ref_audio to be connected")
if mode == "voice_design" and not instruct:
raise ValueError("voice_design mode requires an instruct string (e.g. 'female, low pitch')")
if mode == "voice_cloning":
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
tmp_path = tmp.name
tmp.close()
try:
ref_waveform = ref_audio["waveform"].squeeze(0).cpu() # (channels, samples)
audio_np = ref_waveform.numpy()
# soundfile expects (samples,) for mono or (samples, channels) for multi-channel
sf.write(tmp_path, audio_np[0] if audio_np.shape[0] == 1 else audio_np.T, int(ref_audio["sample_rate"]))
kwargs["ref_audio"] = tmp_path
if ref_text:
kwargs["ref_text"] = ref_text
try:
audio_tensors = model.generate(**kwargs)
except RuntimeError as e:
if "torchcodec" in str(e).lower() or "libtorchcodec" in str(e).lower():
raise RuntimeError(
"Auto-transcription of the reference audio failed because FFmpeg is not "
"available in this environment (required by transformers 5.x for Whisper ASR). "
"Fix: type the transcript of your reference audio into the ref_text field."
) from None
raise
finally:
try:
os.unlink(tmp_path)
except OSError:
pass
elif mode == "voice_design" and instruct:
kwargs["instruct"] = instruct
audio_tensors = model.generate(**kwargs)
else: # auto_voice or fallback
audio_tensors = model.generate(**kwargs)
# Concatenate chunks: each tensor is (1, T) → concat along T → (1, T_total)
combined = torch.cat(audio_tensors, dim=1).cpu() # (1, T_total) on CPU
# ComfyUI AUDIO format: (batch, channels, samples)
waveform = combined.unsqueeze(0) # (1, 1, T_total)
return ({"waveform": waveform, "sample_rate": 24000},)