Files
Ethanfel aedbe2e7d9 fix: declare speaker_1..8 in INPUT_TYPES so ComfyUI validation accepts them
Dynamic JS inputs that are not listed in INPUT_TYPES may be rejected by
ComfyUI's prompt validator and not passed to the Python function. Declaring
all 8 slots as optional fixes this while JS still controls which slots are
visible on the node.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 09:41:56 +02:00

104 lines
4.0 KiB
Python
Raw Permalink 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.
class OmniVoiceSpeaker:
"""Bundle a label, reference audio, and optional transcript into a speaker slot."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"label": ("STRING", {
"default": "Narrator",
"tooltip": (
"Name used to identify this speaker.\n"
"In tagged_speakers mode, prefix paragraphs with [Label]:\n"
" [Narrator] Once upon a time...\n"
"In alternate_paragraphs mode the label is informational only."
),
}),
"ref_audio": ("AUDIO", {
"tooltip": "Reference audio clip for this speaker's voice.",
}),
},
"optional": {
"ref_text": ("STRING", {
"default": "",
"tooltip": "Transcript of ref_audio. Improves cloning quality.",
}),
},
}
RETURN_TYPES = ("OMNIVOICE_SPEAKER",)
RETURN_NAMES = ("speaker",)
FUNCTION = "build"
CATEGORY = "OmniVoice"
def build(self, label, ref_audio, ref_text=""):
return ({"label": label, "ref_audio": ref_audio, "ref_text": ref_text},)
class OmniVoiceSpeakers:
"""Collect multiple speakers into a roster for multi-speaker generation.
The number of speaker input slots expands dynamically when num_speakers changes
(requires the OmniVoice web extension to be loaded by ComfyUI).
Connect one OmniVoice Speaker node per slot.
"""
@classmethod
def INPUT_TYPES(cls):
# speaker_1…speaker_8 are declared here so ComfyUI validation accepts them.
# Visibility is controlled by the JS extension (web/multi_speaker.js):
# only the first num_speakers slots are shown as live inputs.
optional_speakers = {
f"speaker_{i}": ("OMNIVOICE_SPEAKER", {})
for i in range(1, 9)
}
return {
"required": {
"num_speakers": ("INT", {
"default": 2, "min": 2, "max": 8, "step": 1,
"tooltip": (
"Number of active speaker slots.\n"
"Changing this value adds or removes speaker_N inputs on the node."
),
}),
"mode": (
["alternate_paragraphs", "tagged_speakers"],
{
"default": "alternate_paragraphs",
"tooltip": (
"alternate_paragraphs paragraphs (separated by blank lines) rotate\n"
" through speakers in order: 1 → 2 → 3 → 1 → …\n"
"\n"
"tagged_speakers prefix each paragraph with [Label] to assign\n"
" a specific speaker. Labels must match those on the Speaker nodes.\n"
" Unrecognised tags fall back to speaker 1.\n"
"\n"
" Example:\n"
" [Narrator] The door creaked open.\n"
"\n"
" [Alice] Who is there?"
),
},
),
},
"optional": optional_speakers,
}
RETURN_TYPES = ("OMNIVOICE_SPEAKERS",)
RETURN_NAMES = ("speakers",)
FUNCTION = "build"
CATEGORY = "OmniVoice"
def build(self, num_speakers, mode, **kwargs):
speakers = []
for i in range(1, num_speakers + 1):
spk = kwargs.get(f"speaker_{i}")
if spk is not None:
speakers.append(spk)
if len(speakers) < 2:
raise ValueError(
f"OmniVoice Speakers: at least 2 speakers must be connected "
f"(got {len(speakers)})."
)
return ({"speakers": speakers, "mode": mode},)