Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_tts_preset_switch.js
T
DemonGatanjieuandClaude Opus 5.5 db045b8f57 Voice pages: tabs instead of folds
GPT-SoVITS Settings gets a status chip by the title and tabs (tones, other
languages, base files with a dot when they need the user, where things are,
version); the tones have a Simple / Numbers switch. Voice-over's side panel
gets How it reads / Numbers / Settings tabs for a character or picked lines.
The More fold is gone; ui_tab_strip.js replaces ui_tts_fold.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 02:39:53 +08:00

129 lines
6.3 KiB
JavaScript

import { t } from './interface_settings.js';
import { button, el } from './usage_view.js';
import { PARAM_RANGES, SAMPLING, compareFeel, liveliness } from './tts_presets_api.js';
/**
* The tones (generation presets) as a row of choices with the caller's listen row; with `detail`
* also what the picked one means against one fixed tone (the default one in the settings, a
* character's usual on Voice-over): a sentence ("livelier than your default"), a steady-to-lively
* scale with both marked and the four numbers as the Current node card's bars (base → this one),
* each with its plain name and the node's own name (`top_k` …) and editable in place. Shared by
* GPT-SoVITS Settings (ui_tts_presets.js) and Voice-over's side panel (ui_script_tune.js); the
* callers keep the state and draw it again after each change.
*/
const brief = value => String(Math.round(value * 100) / 100);
function numberInput(key, value, onChange) {
const [min, max, step] = PARAM_RANGES[key];
const input = el('input', 'anomalous-tts-number');
Object.assign(input, { type: 'number', min, max, step, value });
input.onchange = () => {
const number = key === 'top_k' ? Math.round(Number(input.value)) : Number(input.value);
if (Number.isFinite(number) && number >= min && number <= max) onChange(number);
else input.value = value;
};
return input;
}
/** One number: two bars (base, this one), "base → this ↑", the built-in value when it was changed. */
function valueRow(key, before, now, { builtin, onEdit }) {
const line = el('div', `anomalous-tts-switch-row${before === now ? ' is-same' : ''}`);
const label = el('span', 'anomalous-tts-switch-label');
label.title = t(`scriptRunParamHint_${key}`);
label.append(el('span', '', t(`scriptRunParamName_${key}`)), el('code', '', key));
const largest = Math.max(Math.abs(before), Math.abs(now)) || 1;
const bars = el('span', 'anomalous-node-param-bars');
for (const [value, kind] of [[before, 'is-now'], [now, 'is-then']]) {
const bar = el('span', `anomalous-node-param-bar ${kind}`);
bar.style.width = `${Math.max(3, (Math.abs(value) / largest) * 100)}%`;
bars.append(bar);
}
const values = el('span', 'anomalous-node-param-nums anomalous-tts-switch-nums');
if (before !== now) values.append(el('span', 'is-now', `${brief(before)} → `));
if (onEdit) {
const input = numberInput(key, now, onEdit);
input.dataset.param = key;
input.title = t('ttsFeelEditHint');
values.append(input);
} else {
values.append(el('strong', '', brief(now)));
}
if (before !== now) values.append(el('span', 'anomalous-node-param-trend', now > before ? '↑' : '↓'));
if (typeof builtin === 'number' && builtin !== now) values.append(el('span', 'anomalous-tts-switch-was', t('ttsPresetWas', { value: brief(builtin) })));
line.append(label, bars, values);
return line;
}
/** The steady-to-lively scale: a hollow ring for the base, a dot for the mode picked. */
function feelScale(base, now, baseName) {
const box = el('div', 'anomalous-tts-feel');
const track = el('div', 'anomalous-tts-feel-track');
for (const [values, kind] of [[base, 'is-base'], [now, 'is-now']]) {
const mark = el('span', `anomalous-tts-feel-mark ${kind}`);
mark.style.left = `${liveliness(values)}%`;
track.append(mark);
}
const ends = el('div', 'anomalous-tts-feel-ends');
ends.append(el('span', '', t('ttsFeelSteady')), el('span', 'anomalous-tts-feel-legend', t('ttsFeelLegend', { base: baseName })),
el('span', '', t('ttsFeelLively')));
box.append(track, ends);
return box;
}
/**
* `choices`: `[{id, label, badge?, title?}]`; `selected` and `base` are ids; `baseName` names the
* base in sentences ("your default (Natural)"), `baseText` is said when the base itself is picked;
* `values(id)` gives a tone's four numbers; `describe(id)` a line about it or ''; `builtin(id)`
* its built-in numbers or null; `editable(id)`; `onPick(id)`; `onEdit(id, key, value)`;
* `extra`: elements after the choices (a "+ New" button); `listen`: the caller's listen row;
* `detail`: the comparison and the numbers too.
*/
export function renderPresetSwitch({
choices, selected, base, baseName, baseText, values, describe = () => '', builtin = () => null,
editable = () => false, onPick, onEdit, extra = [], listen = null, detail = false, disabled = false,
}) {
const box = el('div', 'anomalous-tts-switch');
const row = el('div', 'anomalous-tts-switch-choices');
row.setAttribute('role', 'radiogroup');
for (const choice of choices) {
const chip = button('anomalous-sd-run-choice', choice.label, () => { if (choice.id !== selected) onPick(choice.id); });
chip.setAttribute('role', 'radio');
chip.setAttribute('aria-checked', String(choice.id === selected));
chip.disabled = disabled;
if (choice.title) chip.title = choice.title;
if (choice.badge) chip.append(el('span', 'anomalous-tts-switch-badge', choice.badge));
row.append(chip);
}
row.append(...extra);
box.append(row);
if (listen) box.append(listen);
if (!detail) return box;
const now = values(selected);
const before = values(base) || now;
const said = selected === base ? baseText : t(`ttsFeel_${compareFeel(before, now)}`, { base: baseName });
const sentence = el('div', 'anomalous-tts-feel-say');
sentence.append(el('strong', '', said));
const about = describe(selected);
if (about) sentence.append(document.createTextNode(` ${about}`));
const rows = el('div', 'anomalous-tts-switch-rows');
const own = builtin(selected);
for (const key of SAMPLING) {
rows.append(valueRow(key, before[key], now[key], {
builtin: own?.[key],
onEdit: editable(selected) && !disabled ? value => onEdit(selected, key, value) : null,
}));
}
box.append(sentence, feelScale(before, now, baseName), rows);
return box;
}
/** Draw `root` again, keeping the focus on the number being typed in. */
export function keepParamFocus(root, draw) {
const focused = root.contains(document.activeElement) ? document.activeElement?.dataset?.param : null;
draw();
if (focused) root.querySelector(`[data-param="${focused}"]`)?.focus();
}