Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_tts_presets.js
T
DemonGatanjieuandClaude Opus 5.5 d778aec946 Fix what a walk through every page found before 1.63
Every page in both languages, all themes and at 480:
- Give the window its own text colour: text with none took ComfyUI's, white on the light theme (Tidy headings and file names, the site chip).
- Keep a task's link chip in its row, its x in reach, and colour the next-task chip.
- Let choices in Settings wrap, the Models search shrink, and a workflow's preset row go to two lines.
- Add --amb-on-link for text on the accent: pale in the dark themes, so white was hard to read.
- Lists and colons follow the language (listSeparator, wordColon); tests/words_in_language.mjs keeps it so.
- Right plurals in English, "AI" on the rail, one line left in Chinese translated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 03:18:07 +08:00

246 lines
12 KiB
JavaScript

import { t } from './interface_settings.js';
import { anomalousAlert, anomalousConfirm, anomalousPrompt } from './ui_dialog.js';
import { button, el } from './usage_view.js';
import {
BUILTIN, CROSS, LABEL_LENGTH, MAX_CUSTOM, PRESETS_FORMAT, SAMPLE_LINES, SAMPLING, isBuiltin, languagePairs, loadPresets,
newPresetId, presetLabel, presetUses, presetsFile, removePreset, savePresets, stateFromView,
} from './tts_presets_api.js';
import { keepParamFocus, renderPresetSwitch } from './ui_tts_preset_switch.js';
import { createModePreview } from './ui_tts_mode_preview.js';
import { remember, remembered } from './ui_tab_strip.js';
/**
* The tones (the node's generation presets) in GPT-SoVITS Settings, two tabs' worth. "Which tone
* to use": a row of choices; the one picked says in a few words what it is, can be heard read by
* a character next to the others (ui_tts_mode_preview.js), and with "Numbers" switched on is shown
* against the default one with its numbers, changed in place (ui_tts_preset_switch.js); beside it
* "Make default", "Reset" for a changed built-in one, "Rename" and "Delete" for one made here, and
* "+ New" copies the one picked. "Speaking another language": which tone a pair of languages uses
* (a Japanese character speaking Chinese). Every change is saved at once to the node's presets
* file; a character's own settings are in ui_tts_tuning.js.
*/
const DETAIL_KEY = 'anomalous_tts_tones_detail';
/**
* `{ tones, languages }`, the two parts; `status` is the node's status (its `format` tells whether
* it has presets).
*/
export function renderTtsPresets(status, groups, local) {
const group = el('div', 'anomalous-tts-setup-group anomalous-tts-presets');
const languages = el('div', 'anomalous-tts-setup-group');
if (Number(status?.format) < PRESETS_FORMAT) {
group.append(el('div', 'anomalous-tts-hint', t('ttsPresetsNeedsNode')));
languages.append(el('div', 'anomalous-tts-hint', t('ttsPresetsNeedsNode')));
return { tones: group, languages };
}
let detail = remembered(DETAIL_KEY, ['simple', 'numbers'], 'simple') === 'numbers';
const head = el('div', 'anomalous-tts-presets-head');
const body = el('div', 'anomalous-tts-presets-body', '…');
const note = el('div', 'anomalous-tts-hint');
note.hidden = true;
group.append(head, body, note);
let view = null;
let state = null;
let selected = null;
// Listening: a character (the first one by default) and a line in its language, which can be changed.
let speaker = groups[0] || null;
let line = null;
const speakerLine = () => line ?? SAMPLE_LINES[speaker?.raw?.reference?.language || speaker?.language] ?? SAMPLE_LINES.zh;
const preview = createModePreview(() => draw());
const names = () => Object.keys(state.numbers);
const label = name => presetLabel(state.labels, name, t);
const changed = name => isBuiltin(name) && SAMPLING.some(key => state.numbers[name][key] !== view.builtin[name][key]);
const save = async () => {
try {
view = await savePresets(presetsFile(view, state));
note.textContent = t('ttsPresetsSaved');
} catch (e) {
note.textContent = t('ttsPresetsSaveFailed', { error: e.message });
}
note.hidden = false;
};
const change = (apply) => {
apply();
keepParamFocus(body, draw);
save();
};
const pick = (name) => {
selected = name;
draw();
};
const presetSelect = (value, onChange) => {
const select = el('select', 'anomalous-sd-run-select');
for (const name of names()) {
const option = el('option', '', label(name));
option.value = name;
select.append(option);
}
select.value = value;
select.disabled = !local;
select.onchange = () => onChange(select.value);
return select;
};
/** A name for a new or renamed preset, or null; a taken one is refused. */
const askName = async (message, current = '') => {
const wanted = String(await anomalousPrompt(message, current, t('ttsPresetsTitle'), { maxLength: LABEL_LENGTH }) || '').trim();
if (!wanted || wanted === current) return null;
if (names().some(name => label(name) === wanted)) {
await anomalousAlert(t('ttsPresetNameTaken', { name: wanted }));
return null;
}
return wanted;
};
const addPreset = async () => {
if (names().length - BUILTIN.length >= MAX_CUSTOM) return anomalousAlert(t('ttsPresetTooMany', { count: MAX_CUSTOM }));
const wanted = await askName(t('ttsPresetNewName', { from: label(selected), count: LABEL_LENGTH }));
if (!wanted) return;
const id = newPresetId(names());
change(() => {
state.numbers[id] = { ...state.numbers[selected] };
state.labels[id] = wanted;
selected = id;
});
};
const renamePreset = async (name) => {
const wanted = await askName(t('ttsPresetRenameAsk', { name: label(name) }), label(name));
if (wanted) change(() => { state.labels[name] = wanted; });
};
const deletePreset = async (name) => {
const uses = presetUses(state, name, groups);
const where = [
...uses.general.map(use => (use === 'default' ? t('ttsPresetUseDefault')
: use === CROSS ? t('ttsPresetsCross')
: t('ttsPresetsPair', { ref: t(`scriptRunLang_${use.split('>')[0]}`), lang: t(`scriptRunLang_${use.split('>')[1]}`) }))),
...uses.characters,
];
const message = t('ttsPresetDeleteAsk', { name: label(name) }) + (where.length ? `\n${t('ttsPresetDeleteUses', { list: where.join(t('listSeparator')) })}` : '');
if (!await anomalousConfirm(message, t('ttsPresetsTitle'), { okLabel: t('ttsPresetDelete') })) return;
change(() => {
removePreset(state, name);
selected = state.defaultName;
});
};
/** Who reads which line, then the listen buttons. */
const listenRow = (choices) => {
const box = el('div', 'anomalous-tts-listen-setup');
const who = el('select', 'anomalous-sd-run-select');
for (const group of groups) {
const option = el('option', '', group.character);
option.value = group.character;
who.append(option);
}
who.value = speaker?.character || '';
who.onchange = () => {
speaker = groups.find(group => group.character === who.value) || null;
line = null;
draw();
};
const text = el('input', 'anomalous-tts-listen-text');
Object.assign(text, { type: 'text', value: speakerLine(), maxLength: 80 });
text.onchange = () => { line = text.value.trim(); draw(); };
const take = name => ({ character: speaker?.character, text: speakerLine(), language: 'auto', mode: name, preset: name, numbers: state.numbers[name] });
const row = preview.row({
takeFor: take,
selected,
modes: choices,
onPick: pick,
});
box.append(el('span', 'anomalous-tts-setup-meta', t('ttsListenWho')), who, text, row);
return box;
};
const draw = () => {
const newChip = button('anomalous-sd-run-choice anomalous-tts-switch-new', t('ttsPresetNew'), addPreset);
newChip.hidden = !local;
const choices = names().map(name => ({
id: name,
label: label(name),
badge: name === state.defaultName ? t('ttsPresetDefaultBadge') : (isBuiltin(name) ? '' : t('ttsPresetCustom')),
}));
// Under the choices: what the picked one is and what can be done with it, then listening.
const about = el('div', 'anomalous-tts-switch-about');
about.append(el('span', 'anomalous-tts-switch-what', t('ttsPresetWhat', {
name: label(selected), about: isBuiltin(selected) ? t(`ttsPresetDesc_${selected}`) : t('ttsPresetDescCustom'),
})));
if (local) {
if (selected === state.defaultName) about.append(el('span', 'anomalous-tts-setup-meta', t('ttsPresetIsDefault')));
else about.append(button('anomalous-tts-link', t('ttsPresetMakeDefault'), () => change(() => { state.defaultName = selected; })));
if (changed(selected)) {
about.append(button('anomalous-tts-link', t('ttsPresetResetOne'), () => change(() => { state.numbers[selected] = { ...view.builtin[selected] }; })));
}
if (!isBuiltin(selected)) {
about.append(button('anomalous-tts-link', t('ttsPresetRename'), () => renamePreset(selected)),
button('anomalous-tts-link is-danger', t('ttsPresetDelete'), () => deletePreset(selected)));
}
}
const listening = el('div', 'anomalous-tts-switch-listen');
listening.append(about);
if (local && groups.length) listening.append(listenRow(choices));
const modes = renderPresetSwitch({
choices,
selected,
base: state.defaultName,
baseName: t('ttsFeelYourDefault', { name: label(state.defaultName) }),
baseText: t('ttsFeelIsDefault'),
values: name => state.numbers[name],
describe: name => (isBuiltin(name) ? t(`ttsPresetDesc_${name}`) : t('ttsPresetDescCustom')),
builtin: name => (isBuiltin(name) ? view.builtin[name] : null),
editable: () => local,
onPick: pick,
onEdit: (name, key, value) => change(() => { state.numbers[name][key] = value; }),
extra: [newChip],
listen: listening,
detail,
});
const rules = el('div', 'anomalous-tts-preset-rules');
const row = (text, select) => rules.append(el('span', 'anomalous-tts-setup-meta', text), select);
rules.append(el('div', 'anomalous-tts-hint anomalous-tts-preset-rules-hint', t('ttsPresetsLanguages')));
for (const pair of languagePairs(groups, view.languages)) {
const [ref, lang] = pair.split('>');
row(t('ttsPresetsPair', { ref: t(`scriptRunLang_${ref}`), lang: t(`scriptRunLang_${lang}`) }),
presetSelect(state.rules[pair] || state.cross, value => change(() => { state.rules[pair] = value; })));
}
row(t('ttsPresetsCross'), presetSelect(state.cross, value => change(() => { state.cross = value; })));
// "Simple / Numbers" beside the title: the comparison and the numbers only when asked for.
const title = el('div', 'anomalous-tts-section', t('ttsPresetsPick'));
title.title = t('ttsPresetsHint');
const toggle = el('div', 'anomalous-sd-run-segment');
toggle.setAttribute('role', 'radiogroup');
for (const [id, key] of [['simple', 'ttsTonesSimple'], ['numbers', 'ttsTonesNumbers']]) {
const chip = button('anomalous-sd-run-choice', t(key), () => {
if ((id === 'numbers') === detail) return;
detail = id === 'numbers';
remember(DETAIL_KEY, id);
draw();
});
chip.setAttribute('role', 'radio');
chip.setAttribute('aria-checked', String((id === 'numbers') === detail));
toggle.append(chip);
}
head.replaceChildren(title, toggle);
body.replaceChildren(modes);
languages.replaceChildren(rules);
};
loadPresets().then(data => {
view = data;
state = stateFromView(data);
selected = state.defaultName;
draw();
}).catch(e => {
body.textContent = t('ttsPresetsSaveFailed', { error: e.message });
});
return { tones: group, languages };
}