Files
ComfyUI-Omnivoice/web/multi_speaker.js
T
Ethanfel 95cf706b19 feat: add multi-speaker generation with JS-powered dynamic slots
- Add OmniVoiceSpeaker node (label + ref_audio + ref_text → OMNIVOICE_SPEAKER)
- Add OmniVoiceSpeakers node (roster with dynamic speaker_N inputs driven by
  num_speakers INT widget; slots expand/collapse via ComfyUI JS extension)
- Add web/multi_speaker.js: ComfyUI extension that hooks onNodeCreated and
  onConfigure to sync speaker_N inputs in real time (max 8 speakers)
- Extend OmniVoiceGenerate with optional speakers (OMNIVOICE_SPEAKERS) input;
  when connected it routes each paragraph to the assigned speaker and
  concatenates the results — supports alternate_paragraphs and tagged_speakers modes
- Remove OmniVoiceMultiSpeakerGenerate (generation now lives in the existing
  Generate node)
- Refactor generator.py: extract _write_tmp_wav helper, add _tensors_to_audio

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

71 lines
2.7 KiB
JavaScript

import { app } from "../../scripts/app.js";
const MAX_SPEAKERS = 8;
app.registerExtension({
name: "OmniVoice.MultiSpeaker",
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "OmniVoiceSpeakers") return;
/**
* Ensure the node has exactly `count` speaker_N inputs.
* Safe to call multiple times with the same count (idempotent).
*/
function syncSpeakerInputs(node, count) {
count = Math.max(2, Math.min(MAX_SPEAKERS, Math.floor(count)));
// Add any missing slots
for (let i = 1; i <= count; i++) {
const name = `speaker_${i}`;
if (!node.inputs?.find(inp => inp.name === name)) {
node.addInput(name, "OMNIVOICE_SPEAKER");
}
}
// Remove excess slots (high → low so indices stay valid)
for (let i = MAX_SPEAKERS; i > count; i--) {
const name = `speaker_${i}`;
const idx = node.inputs?.findIndex(inp => inp.name === name) ?? -1;
if (idx === -1) continue;
// Sever any connected link before removing the slot
const linkId = node.inputs[idx].link;
if (linkId != null) node.graph?.removeLink(linkId);
node.removeInput(idx);
}
node.setDirtyCanvas(true, true);
}
/**
* Attach the num_speakers widget callback once per node instance.
* Guarded by a flag so configure() can call it safely on reload.
*/
function attachCallback(node) {
if (node._omnivoiceCbAttached) return;
const w = node.widgets?.find(w => w.name === "num_speakers");
if (!w) return;
node._omnivoiceCbAttached = true;
w.callback = (value) => syncSpeakerInputs(node, value);
}
// --- Fresh node creation ---
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, arguments);
attachCallback(this);
const w = this.widgets?.find(w => w.name === "num_speakers");
if (w) syncSpeakerInputs(this, w.value);
};
// --- Workflow load: called by LiteGraph after widget values are restored ---
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (data) {
onConfigure?.apply(this, arguments);
attachCallback(this);
const w = this.widgets?.find(w => w.name === "num_speakers");
if (w) syncSpeakerInputs(this, w.value);
};
},
});