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>
This commit is contained in:
DemonGatanjieu
2026-10-17 22:53:03 +08:00
co-authored by Claude Opus 5.5
parent 998f232307
commit 89fa1a93bf
18 changed files with 527 additions and 197 deletions
+14 -12
View File
@@ -76,8 +76,8 @@ async function loadNodeSpec() {
}
/**
* `getScript()` -> `{ group, lines, listen, pkg }` for the chosen character (the director's lines,
* the index of the one to listen with, pkg from buildScriptPackage with takes); `getGroups()` -> every voice group (for the presets' "used by");
* `getScript()` -> `{ group, lines, listen, speakerOf, pkg }` for the chosen character (the director's
* lines, the index of the one to listen with, who reads a line, pkg from buildScriptPackage with takes); `getGroups()` -> every voice group (for the presets' "used by");
* `onPlay()` when the result starts playing;
* `resetTakes()` when a new take of the whole script starts; `onBusyChange()` when a job
* starts or ends (the director re-renders its line cards, whose retake buttons depend on it);
@@ -241,7 +241,13 @@ export function createRunSection({ getScript, getGroups, onPlay, resetTakes, onB
if (!state.presets) return;
const opts = currentOptions(group);
const view = state.presets;
const auto = autoTuning(group, opts);
// The same line for every mode: the one you are on in the director, read by its own character;
// an empty one, a line in the character's language.
const script = getScript();
const index = script?.listen ?? 0;
const chosen = script?.lines?.[index];
const speaker = (chosen && script.speakerOf?.(chosen)) || group;
const auto = autoTuning(speaker, opts);
const values = id => (id === AUTO ? auto.values : view.presets[id]);
if (opts.preset !== AUTO && !view.presets[opts.preset]) opts.preset = AUTO; // deleted in the settings meanwhile
const choices = [{ id: AUTO, label: t('scriptRunModeAuto') },
@@ -251,20 +257,16 @@ export function createRunSection({ getScript, getGroups, onPlay, resetTakes, onB
state.presetNote = '';
render();
};
// The same line for every mode: the one you are on in the director; an empty one, a line in the character's language.
const script = getScript();
const index = script?.listen ?? 0;
const chosen = script?.lines?.[index];
const words = String(chosen?.text || '').trim();
const refLang = group.raw?.reference?.language || group.language;
const refLang = speaker.raw?.reference?.language || speaker.language;
const text = words
? `${chosen.emotion && chosen.emotion !== 'main' ? `{${chosen.emotion}}` : ''}${words}`
: SAMPLE_LINES[refLang] || SAMPLE_LINES.zh;
const lineLabel = el('span', 'anomalous-sd-run-listen-line', words
? t('scriptRunListenLine', { n: index + 1, text: words })
: t('scriptRunListenSample', { text }));
const lineLabel = el('span', 'anomalous-sd-run-listen-line', !words ? t('scriptRunListenSample', { text })
: speaker !== group ? t('scriptRunListenLineAs', { n: index + 1, text: words, who: speaker.character })
: t('scriptRunListenLine', { n: index + 1, text: words }));
lineLabel.title = t('scriptRunListenLineHint');
const take = id => ({ character: group.character, text, language: opts.language, speed: opts.speed, mode: id, preset: id, numbers: values(id) });
const take = id => ({ character: speaker.character, text, language: opts.language, speed: opts.speed, mode: id, preset: id, numbers: values(id) });
state.takeNow = busy ? null : () => take(opts.preset);
const lang = auto.key === 'all' ? t('ttsTuningAll') : t(`scriptRunLang_${auto.key}`);
const modes = renderPresetSwitch({