Files
DemonGatanjieuandClaude Opus 5.5 89fa1a93bf Voice-over: several characters in one script
Pick lines on Voice-over (click; Ctrl / Cmd adds, Shift a run; Esc lets
go) and click a character in the list on the left to have it read them;
with none picked, the click changes who reads by default. On Voice-over
the list is the cast: narrower, characters with their colours, and a
click never leaves the page. Cards show who reads each line in its
colour, with that character's emotions; the bundle carries [name] where
the speaker changes, so generating, the cards' listen button, pushing,
dragging and copying follow it, and a file voiced again from the audio
gallery keeps its speakers. The character cards' Voice-over button is
gone. The workflow actions move to ui_script_workflow.js and the cast
helpers to ui_script_cast.js, keeping the director under 600 lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:53:03 +08:00

98 lines
4.1 KiB
JavaScript

import { app } from '../../../scripts/app.js';
import { t } from './interface_settings.js';
import { anomalousAlert, anomalousConfirm } from './ui_dialog.js';
import { comboValueForPath } from './audio_script.js';
import { targetForNode } from './audio_node_targets.js';
import { TTS_ENGINE } from './audio_engines.js';
import { bindMaterialDrag } from './material_drag.js';
import { recordCanvasStep } from './canvas_history.js';
/**
* The Script Director's "Into your own workflow": the bundle (buildScriptPackage) copied,
* pushed to the selected / only GPT-SoVITS node, or dragged onto one (supported nodes:
* audio_node_targets.js). Writing the character and script widgets together is one Ctrl+Z step.
*/
function flash(btn, text, restore) {
btn.textContent = text;
btn.classList.add('is-success');
setTimeout(() => {
btn.textContent = restore;
btn.classList.remove('is-success');
}, 1800);
}
export async function copyScript(pkg, btn) {
if (pkg.error || !navigator.clipboard?.writeText) return;
try {
await navigator.clipboard.writeText(pkg.speech);
flash(btn, t('scriptDirectorCopied'), t('scriptDirectorCopy'));
} catch (_) {
// Clipboard permission refused: the line cards still hold the text.
}
}
function findTargetNode() {
const nodes = (app.graph?._nodes || []).filter(targetForNode);
if (!nodes.length) return { error: 'scriptDirectorNoNode' };
const selected = Object.values(app.canvas?.selected_nodes || {}).filter(targetForNode);
if (selected.length === 1) return { node: selected[0] };
if (nodes.length === 1) return { node: nodes[0] };
return { error: 'scriptDirectorPickNode' };
}
export async function pushToNode(pkg, btn) {
if (pkg.error) return;
const { node, error } = findTargetNode();
if (error) {
await anomalousAlert(t(error, { node: TTS_ENGINE.label }));
return;
}
if (await applyPackageToNode(node, pkg)) flash(btn, t('scriptDirectorSuccess'), t('scriptDirectorInject'));
}
/** Write character + script into one node; re-checks the node after any dialog. Returns true when written. */
async function applyPackageToNode(node, pkg) {
const graph = app.graph;
const target = targetForNode(node);
const speechWidget = target && node.widgets?.find(w => w.name === target.speechWidget);
const sampleWidget = target && node.widgets?.find(w => w.name === target.voiceWidget);
if (!speechWidget || !sampleWidget) {
await anomalousAlert(t('scriptDirectorDropNotTts', { node: TTS_ENGINE.label }));
return false;
}
const sampleValue = comboValueForPath(sampleWidget, pkg.sample);
if (sampleValue == null) {
await anomalousAlert(t('scriptDirectorSampleNotListed', { file: pkg.sample, node: TTS_ENGINE.label }));
return false;
}
const previous = speechWidget.value;
if (typeof previous === 'string' && previous.trim() && previous.trim() !== pkg.speech) {
if (!await anomalousConfirm(t('scriptDirectorOverwriteConfirm'))) return false;
if (app.graph !== graph || graph.getNodeById(node.id) !== node || speechWidget.value !== previous) return false;
}
if (sampleWidget.value !== sampleValue) {
sampleWidget.value = sampleValue;
sampleWidget.callback?.(sampleValue, app.canvas, node);
}
speechWidget.value = pkg.speech;
speechWidget.callback?.(pkg.speech, app.canvas, node);
node.setDirtyCanvas?.(true, true);
recordCanvasStep(app);
return true;
}
/** Drag the bundle onto a canvas node; `dragOwner` hides the browser while dragging, `pushBtn()` flashes on success. */
export function bindPackageDrag(handle, dragOwner, getPackage, pushBtn) {
bindMaterialDrag(handle, dragOwner, {
payload: () => {
const pkg = getPackage();
return pkg.error ? null : { ...pkg, dragHint: t('scriptDirectorDragHint', { node: TTS_ENGINE.label }) };
},
accepts: node => Boolean(targetForNode(node)),
drop: async node => {
if (await applyPackageToNode(node, getPackage())) flash(pushBtn(), t('scriptDirectorSuccess'), t('scriptDirectorInject'));
},
});
}