95cf706b19
- 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>
71 lines
2.7 KiB
JavaScript
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);
|
|
};
|
|
},
|
|
});
|