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>
98 lines
4.1 KiB
JavaScript
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'));
|
|
},
|
|
});
|
|
}
|