The side panel changes one layer at a time under a path (Whole script › character › line); each layer has the same rows (speed, tone, language), each saying where its value comes from, with ↺ to follow the layer above. The more particular wins: line, character, whole script, the character's usual settings. The whole script's values go into the node's inputs and only lines that differ get tags. Lasting settings are links at the foot. The script is kept as a draft in this browser and comes back after a reload; editing the text keeps unchanged lines' own settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
149 lines
6.4 KiB
JavaScript
149 lines
6.4 KiB
JavaScript
/**
|
||
* How each line of a voice-over is read (no DOM): its language, speed and tone, in three layers
|
||
* changed for this script, the most particular first: the line (`line.tune`), its character,
|
||
* the whole script; then the character's usual (saved) values, then the defaults. Each layer
|
||
* holds only the fields set on it, and a field set to what it would get anyway is dropped, so
|
||
* "where it comes from" always names the layer that decided it (`from`: 'line', 'character',
|
||
* 'script', 'usual' or 'default'). Anomalous_TTS interface 17 reads a line's own values from tags
|
||
* in the script, `[lang:ja][speed:1.2][preset:lively]` (`auto` goes back to detecting / the
|
||
* presets), over every other setting; the whole script's go through the node's inputs.
|
||
*/
|
||
|
||
export const TUNE_FIELDS = ['language', 'speed', 'preset'];
|
||
export const LINE_TAGS_FORMAT = 17; // the node interface that reads [lang:…] [speed:…] [preset:…]
|
||
export const AUTO_MODE = ''; // the mode that lets the presets decide
|
||
|
||
/** What a line set for itself: only those fields. */
|
||
export function ownTune(line) {
|
||
return line?.tune && typeof line.tune === 'object' ? line.tune : {};
|
||
}
|
||
|
||
export function hasOwnTune(line) {
|
||
const own = ownTune(line);
|
||
return TUNE_FIELDS.some(field => own[field] !== undefined);
|
||
}
|
||
|
||
/** A line's values: its own over its character's (`character`: `{ language, speed, preset }`). */
|
||
export function lineValues(line, character) {
|
||
const own = ownTune(line);
|
||
return Object.fromEntries(TUNE_FIELDS.map(field => [field, own[field] ?? character?.[field]]));
|
||
}
|
||
|
||
/** `own[field] = value`, or no entry when `value` is undefined or what the layer below gives (`inherited`). */
|
||
export function setOwn(own, field, value, inherited) {
|
||
if (value === undefined || value === inherited) delete own[field];
|
||
else own[field] = value;
|
||
}
|
||
|
||
/**
|
||
* Sets `field` on each line, or with `value` undefined (or what its character reads it with,
|
||
* `inheritedOf(line)`) lets it follow its character again.
|
||
*/
|
||
export function setLineTune(lines, field, value, inheritedOf = () => undefined) {
|
||
for (const line of lines) {
|
||
if (!line) continue;
|
||
const own = { ...ownTune(line) };
|
||
setOwn(own, field, value, inheritedOf(line));
|
||
if (Object.keys(own).length) line.tune = own;
|
||
else delete line.tune;
|
||
}
|
||
}
|
||
|
||
/** The whole script's values: what was changed for it, else the defaults (`base`). */
|
||
export function scriptLayer(script, base) {
|
||
const values = {};
|
||
const from = {};
|
||
for (const field of TUNE_FIELDS) {
|
||
const own = script?.[field] !== undefined;
|
||
values[field] = own ? script[field] : base[field];
|
||
from[field] = own ? 'script' : 'default';
|
||
}
|
||
return { values, from };
|
||
}
|
||
|
||
/**
|
||
* A character's values in this script: changed for it here (`cast`), else for the whole script,
|
||
* else its usual ones (`stored`; 'usual' only where they differ from `base`).
|
||
*/
|
||
export function characterLayer(cast, script, stored, base) {
|
||
const values = {};
|
||
const from = {};
|
||
for (const field of TUNE_FIELDS) {
|
||
if (cast?.[field] !== undefined) [values[field], from[field]] = [cast[field], 'character'];
|
||
else if (script?.[field] !== undefined) [values[field], from[field]] = [script[field], 'script'];
|
||
else [values[field], from[field]] = [stored[field], stored[field] === base[field] ? 'default' : 'usual'];
|
||
}
|
||
return { values, from };
|
||
}
|
||
|
||
/** A line's values over its character's layer (`character`: `{ values, from }`). */
|
||
export function lineLayer(line, character) {
|
||
const own = ownTune(line);
|
||
const values = {};
|
||
const from = {};
|
||
for (const field of TUNE_FIELDS) {
|
||
const mine = own[field] !== undefined;
|
||
values[field] = mine ? own[field] : character.values[field];
|
||
from[field] = mine ? 'line' : character.from[field];
|
||
}
|
||
return { values, from };
|
||
}
|
||
|
||
/** Lets the lines follow their character in everything again. */
|
||
export function clearLineTune(lines) {
|
||
for (const line of lines) if (line) delete line.tune;
|
||
}
|
||
|
||
/**
|
||
* The tags a line needs for the node to read it with `values` where it would read it with `node`
|
||
* by itself (its inputs, the character's own settings): each field that differs. A language or
|
||
* tone back to auto is `[lang:auto]` / `[preset:auto]`; missing `node` fields are auto.
|
||
*/
|
||
export function tuneTags(values, node = {}) {
|
||
const nodeLanguage = node.language ?? 'auto';
|
||
const nodePreset = node.preset ?? AUTO_MODE;
|
||
let tags = '';
|
||
if (values.language && values.language !== nodeLanguage) tags += `[lang:${values.language}]`;
|
||
if (typeof values.speed === 'number' && values.speed !== node.speed) tags += `[speed:${Math.round(values.speed * 100) / 100}]`;
|
||
if ((values.preset ?? AUTO_MODE) !== nodePreset) tags += `[preset:${values.preset || 'auto'}]`;
|
||
return tags;
|
||
}
|
||
|
||
/**
|
||
* The one value `field` has on all of `lines` (`valuesOf(line)`): `{ value, mixed }`; `own`:
|
||
* whether every line set it for itself, `some` whether any did.
|
||
*/
|
||
export function sharedValue(lines, field, valuesOf) {
|
||
const values = lines.map(line => valuesOf(line)[field]);
|
||
const owned = lines.filter(line => ownTune(line)[field] !== undefined).length;
|
||
return {
|
||
value: values[0],
|
||
mixed: values.some(value => value !== values[0]),
|
||
own: lines.length > 0 && owned === lines.length,
|
||
some: owned > 0,
|
||
};
|
||
}
|
||
|
||
const TAG = /^\s*(speed|语速|preset|预设|lang|language|语言)\s*[::]\s*(\S+)\s*$/i;
|
||
const FIELD = { speed: 'speed', 语速: 'speed', preset: 'preset', 预设: 'preset', lang: 'language', language: 'language', 语言: 'language' };
|
||
const LANGUAGE = { ja: 'ja', jp: 'ja', zh: 'zh', cn: 'zh', en: 'en', 日语: 'ja', 中文: 'zh', 英语: 'en', auto: 'auto', 自动: 'auto' };
|
||
|
||
/** `[speed:1.2]`'s inside -> `{ field: 'speed', value: 1.2 }`; null when it is no line setting (a name). */
|
||
export function readTuneTag(content) {
|
||
const match = TAG.exec(String(content || ''));
|
||
if (!match) return null;
|
||
const field = FIELD[match[1].toLowerCase()];
|
||
const raw = match[2];
|
||
if (field === 'speed') {
|
||
const value = Number(raw);
|
||
return Number.isFinite(value) ? { field, value } : { field, value: undefined };
|
||
}
|
||
if (field === 'language') return { field, value: LANGUAGE[raw.toLowerCase()] };
|
||
return { field, value: raw.toLowerCase() === 'auto' ? AUTO_MODE : raw };
|
||
}
|
||
|
||
/** "1, 3, 5" from line indexes (0-based in, 1-based out). */
|
||
export function lineNumbers(indexes) {
|
||
return indexes.map(index => index + 1).join(', ');
|
||
}
|