Show every preset's numbers side by side; make, rename and delete presets
The numbers table marks values that differ between presets and changed built-in values (with the built-in one), like the compare window; built-in presets reset one by one or together. New presets start as a copy, are named by the user and can be picked everywhere. A character's generation settings show each row's numbers in effect next to the general ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3159249eb2
commit
39ca8ab25c
+8
-5
@@ -233,11 +233,14 @@ covered by the `styles.css` manifest.
|
||||
- Generation presets (Anomalous_TTS interface 16): `tts_presets_api.js` holds the
|
||||
calls (`/anomalous_tts/presets`) and pure helpers (the language pairs the
|
||||
characters can meet, the presets file written back with only what differs from
|
||||
the built-in values, a character's `defaults` / `defaults.by_language` as rows
|
||||
and back, keeping every other field). `ui_tts_presets.js` is the group in the
|
||||
GPT-SoVITS settings dialog (default preset, rules per pair, the numbers),
|
||||
saving each change at once; `ui_tts_tuning.js` is the per-character dialog
|
||||
behind the card's Generation settings button, shown only with interface 16.
|
||||
the built-in values plus the presets made here (`custom_<id>` with a label),
|
||||
where a preset is used and taking it out, a dialog row's numbers in effect
|
||||
next to the general ones, a character's `defaults` / `defaults.by_language` as
|
||||
rows and back, keeping every other field). `ui_tts_presets.js` is the group in
|
||||
the GPT-SoVITS settings dialog (default preset, rules per pair, every preset's
|
||||
numbers side by side marked like the compare window, new / rename / delete /
|
||||
reset), saving each change at once; `ui_tts_tuning.js` is the per-character
|
||||
dialog behind the card's Generation settings button, shown only with interface 16.
|
||||
|
||||
- `web/main.js` coordinates extension registration (with `?v=...` versioned module imports busting aggressive browser ES Module caching and unconditional legacy storage key purging). `browser.js` owns the shared
|
||||
browser class and extracted-method wiring; `browser_entry.js` owns the single
|
||||
|
||||
+3
-2
@@ -14,8 +14,9 @@
|
||||
### 🎙️ Character voices (角色语音)
|
||||
- **Update Anomalous_TTS from GPT-SoVITS Settings (在 GPT-SoVITS 设置里更新 Anomalous_TTS)**: a new *Anomalous_TTS version* line shows the installed version; **Check for updates** looks up its published stable releases (only then does it go online), **Update to v…** switches to the newest one and offers to restart ComfyUI, and **Back to v…** undoes it. Anomalous_TTS now publishes numbered stable releases (the first is v1.0.0), so an update never lands on unfinished work. Only an Anomalous_TTS installed with git from its GitHub repository is changed (as ComfyUI Manager installs it), only from the computer ComfyUI runs on, and never over files you changed. When an import needs a newer Anomalous_TTS, the message points here.
|
||||
|
||||
- **Generation presets (生成预设)**: GPT-SoVITS Settings has three presets, **Steady** (fewer slips, flatter), **Standard** (the old default) and **Lively** (livelier, slips now and then): pick the default one for every character, and one for each pair of languages, such as a Japanese character speaking Chinese (until now every such line was made steadier without asking; that is the rule you start with). Folded below, the three presets' numbers can be changed. With nothing changed, voices sound exactly as before.
|
||||
- **A character's own generation settings (角色特调)**: a new **Generation settings** button on each character card sets the preset and speed for the character as a whole and for each language it speaks, with the numbers themselves when a preset is not enough. The node follows these settings too, so a workflow in ComfyUI sounds like generating here; numbers changed on the node itself still go first. Needs Anomalous_TTS v1.1.0 (update it from the same dialog).
|
||||
- **Generation presets (生成预设)**: GPT-SoVITS Settings has three presets, **Steady** (fewer slips, flatter), **Standard** (the old default) and **Lively** (livelier, slips now and then): pick the default one for every character, and one for each pair of languages, such as a Japanese character speaking Chinese (until now every such line was made steadier without asking; that is the rule you start with). With nothing changed, voices sound exactly as before.
|
||||
- **Every preset's numbers side by side (各档数字并排对比)**: under the presets, a table shows each preset's numbers the way the compare window shows values: numbers that differ between presets in blue, numbers every preset shares in grey (or hidden), a changed built-in number in yellow with its built-in value below. Click a number to change it; **Reset** puts one built-in preset back, or all three at once. **Create** makes your own preset from a copy of any other, with a name of your own (such as 念旁白 for narration); it can be picked everywhere the built-in ones can, renamed, and deleted (the question says what uses it; those places go back to the general setting).
|
||||
- **A character's own generation settings (角色特调)**: a new **Generation settings** button on each character card sets the preset and speed for the character as a whole and for each language it speaks, with the numbers themselves when a preset is not enough. Under each row are the numbers its lines will get, blue where they differ from the general presets. The node follows these settings too, so a workflow in ComfyUI sounds like generating here; numbers changed on the node itself still go first. Needs Anomalous_TTS v1.1.0 (update it from the same dialog).
|
||||
|
||||
### ☁️ ComfyUI on a cloud server (云服务器上使用)
|
||||
- **How far it goes**: fully supported on overseas cloud servers (RunPod…). On servers in mainland China (AutoDL…) only in part: browsing, offline scans and tidying work, but Civitai is mostly out of reach there, so lookups are unreliable and covers rarely download (海外服务器完整支持;国内服务器只能部分支持). Not fully tested yet: one AutoDL server so far, no overseas one. The cloud guide ends with a section for AI assistants (what is going on, the steps, how to check, what to do when it did not work, what to tell the user about safety), so a user can hand the link to their AI.
|
||||
|
||||
@@ -44,4 +44,36 @@ assert.equal(next.keep, 1);
|
||||
const cleared = api.mergeTuning({ defaults: { preset: 'lively' } }, api.tuningRows({}));
|
||||
assert.equal(cleared.defaults, undefined);
|
||||
|
||||
// Presets made here: written whole with their name, the built-in ones only where changed.
|
||||
const mine = { ...structuredClone(builtin), custom_a1: { top_k: 12, top_p: 1, temperature: 0.9, repetition_penalty: 1.35 } };
|
||||
assert.deepEqual(api.presetsFile(view, { defaultName: 'custom_a1', numbers: mine, labels: { custom_a1: '念旁白' }, rules: { 'ja>zh': 'custom_a1' }, cross: 'stable' }),
|
||||
{ format: 1, default: 'custom_a1', presets: { custom_a1: { label: '念旁白', top_k: 12, top_p: 1, temperature: 0.9, repetition_penalty: 1.35 } }, languages: { 'ja>zh': 'custom_a1' } });
|
||||
assert.equal(api.presetLabel({ custom_a1: '念旁白' }, 'custom_a1', key => key), '念旁白');
|
||||
assert.equal(api.presetLabel({}, 'lively', key => key), 'ttsPreset_lively');
|
||||
const id = api.newPresetId(['custom_a1'], 1000);
|
||||
assert.match(id, /^custom_[a-z0-9]{1,12}$/);
|
||||
assert.notEqual(api.newPresetId([id], 1000), id);
|
||||
|
||||
// The table marks a number only when the presets do not all share it.
|
||||
assert.equal(api.presetsDiffer(builtin, 'top_k'), true);
|
||||
assert.equal(api.presetsDiffer(builtin, 'top_p'), false);
|
||||
|
||||
// Deleting a preset: where it was used, and those places back on the general setting.
|
||||
const state = { defaultName: 'custom_a1', numbers: structuredClone(mine), labels: { custom_a1: '念旁白' }, rules: { 'ja>zh': 'custom_a1', 'ja>en': 'lively' }, cross: 'custom_a1' };
|
||||
const users = [{ raw: { name: '阿罗娜', settings: { defaults: { by_language: { zh: { preset: 'custom_a1' } } } } } }, { raw: { name: '爱丽丝', settings: {} } }];
|
||||
assert.deepEqual(api.presetUses(state, 'custom_a1', users), { general: ['default', 'ja>zh', 'cross'], characters: ['阿罗娜'] });
|
||||
api.removePreset(state, 'custom_a1');
|
||||
assert.deepEqual([state.defaultName, state.rules, state.cross, Object.keys(state.numbers), state.labels],
|
||||
['standard', { 'ja>en': 'lively' }, 'stable', ['stable', 'standard', 'lively'], {}]);
|
||||
|
||||
// A dialog row's numbers in effect next to the general ones (layers 1 to 4).
|
||||
const general = { ...view, default: 'standard', languages: { 'ja>en': 'standard' }, cross: 'stable' };
|
||||
const tuning = api.tuningRows({ defaults: { preset: 'lively', speed: 1.2, by_language: { zh: { top_k: 5 } } } });
|
||||
const own = api.lineTuning(general, tuning, 'ja', 'ja');
|
||||
assert.deepEqual([own.values.top_k, own.values.speed, own.general.top_k, own.general.speed], [30, 1.2, 15, 1]);
|
||||
const chinese = api.lineTuning(general, tuning, 'ja', 'zh'); // the rule for other languages over the character's preset, then its own number
|
||||
assert.deepEqual([chinese.values.top_k, chinese.values.temperature, chinese.values.speed, chinese.general.top_k], [5, 0.8, 1.2, 10]);
|
||||
const gone = api.lineTuning(general, api.tuningRows({ defaults: { preset: 'custom_zz' } }), 'ja', 'all'); // deleted since: skipped
|
||||
assert.deepEqual(gone.values, gone.general);
|
||||
|
||||
console.log('tts_presets_api: all checks passed');
|
||||
|
||||
+48
-6
@@ -2277,11 +2277,29 @@ export const i18n = {
|
||||
ttsPresetsLanguages: "按语言:参考音频的语言 → 要说的语言",
|
||||
ttsPresetsPair: "{ref}角色说{lang}",
|
||||
ttsPresetsCross: "其他跨语言组合",
|
||||
ttsPresetsNumbers: "三档的具体数字",
|
||||
ttsPresetsReset: "恢复内置数字",
|
||||
ttsPresetsNumbers: "各档的数字(点数字直接改)",
|
||||
ttsPresetsReset: "内置三档全部恢复",
|
||||
ttsPresetsShowSame: "也显示各档相同的",
|
||||
ttsPresetsLegend: "蓝底:各档不一样的数字。黄底:改过的内置数字,下面写着原值。灰字:各档都一样。",
|
||||
ttsPresetBuiltin: "内置",
|
||||
ttsPresetCustom: "自建",
|
||||
ttsPresetDescCustom: "你自建的",
|
||||
ttsPresetWas: "原 {value}",
|
||||
ttsPresetResetOne: "恢复内置",
|
||||
ttsPresetNewFrom: "照着这一档新建:",
|
||||
ttsPresetNew: "新建一档",
|
||||
ttsPresetNewName: "新预设的名字(最多 {count} 个字)。数字先照抄“{from}”,建好再改。",
|
||||
ttsPresetRename: "改名",
|
||||
ttsPresetRenameAsk: "“{name}”的新名字",
|
||||
ttsPresetNameTaken: "已经有叫“{name}”的预设了,换个名字吧。",
|
||||
ttsPresetTooMany: "自建的预设最多 {count} 个。",
|
||||
ttsPresetDelete: "删除",
|
||||
ttsPresetDeleteAsk: "删除预设“{name}”?",
|
||||
ttsPresetDeleteUses: "正在用它的:{list}。删掉后这些地方改回跟通用设置。",
|
||||
ttsPresetUseDefault: "默认档",
|
||||
ttsTuningOpen: "生成设置",
|
||||
ttsTuningTitle: "生成设置:{name}",
|
||||
ttsTuningSubtitle: "这个角色说每种语言时用哪一档、语速多少。不选就跟 GPT-SoVITS 设置里的通用预设。",
|
||||
ttsTuningSubtitle: "这个角色说每种语言时用哪一档、语速多少。不选就跟 GPT-SoVITS 设置里的通用预设。每行下面是实际用的数字,蓝底的和通用设置不一样。",
|
||||
ttsTuningAll: "所有语言",
|
||||
ttsTuningLang: "说{lang}",
|
||||
ttsTuningFollow: "跟通用({preset})",
|
||||
@@ -2289,6 +2307,9 @@ export const i18n = {
|
||||
ttsTuningNumbers: "调数字",
|
||||
ttsTuningNumbersHint: "空着的用上面这一档的数字(灰字)。",
|
||||
ttsTuningBadNumber: "“{name}”的数字超出范围。",
|
||||
ttsTuningActual: "实际用:",
|
||||
ttsTuningSameAsGeneral: "和通用设置一样:",
|
||||
ttsTuningGeneralValue: "通用设置是 {value}",
|
||||
ttsVersionTitle: "Anomalous_TTS 版本",
|
||||
ttsVersionHint: "Anomalous_TTS 的稳定版本。点“检查更新”才会联网;更新后要重启 ComfyUI 才生效,不合适可以点“回到”退回去。",
|
||||
ttsVersionCheck: "检查更新",
|
||||
@@ -4915,11 +4936,29 @@ export const i18n = {
|
||||
ttsPresetsLanguages: "By language: the reference's language → the spoken one",
|
||||
ttsPresetsPair: "{ref} characters speaking {lang}",
|
||||
ttsPresetsCross: "Any other pair of languages",
|
||||
ttsPresetsNumbers: "The three presets' numbers",
|
||||
ttsPresetsReset: "Back to the built-in numbers",
|
||||
ttsPresetsNumbers: "Each preset's numbers (click one to change it)",
|
||||
ttsPresetsReset: "Reset all built-in presets",
|
||||
ttsPresetsShowSame: "Also show numbers every preset shares",
|
||||
ttsPresetsLegend: "Blue: numbers that differ between presets. Yellow: a changed built-in number, its built-in value below. Grey: the same in every preset.",
|
||||
ttsPresetBuiltin: "built-in",
|
||||
ttsPresetCustom: "yours",
|
||||
ttsPresetDescCustom: "made by you",
|
||||
ttsPresetWas: "was {value}",
|
||||
ttsPresetResetOne: "Reset",
|
||||
ttsPresetNewFrom: "New preset from:",
|
||||
ttsPresetNew: "Create",
|
||||
ttsPresetNewName: "Name of the new preset (up to {count} characters). Its numbers start as a copy of \"{from}\".",
|
||||
ttsPresetRename: "Rename",
|
||||
ttsPresetRenameAsk: "New name for \"{name}\"",
|
||||
ttsPresetNameTaken: "There is already a preset called \"{name}\". Try another name.",
|
||||
ttsPresetTooMany: "You can make up to {count} presets.",
|
||||
ttsPresetDelete: "Delete",
|
||||
ttsPresetDeleteAsk: "Delete the preset \"{name}\"?",
|
||||
ttsPresetDeleteUses: "Used by: {list}. They go back to the general setting.",
|
||||
ttsPresetUseDefault: "the default preset",
|
||||
ttsTuningOpen: "Generation settings",
|
||||
ttsTuningTitle: "Generation settings: {name}",
|
||||
ttsTuningSubtitle: "Which preset and speed this character uses for each language. Left empty, the general presets (GPT-SoVITS Settings) apply.",
|
||||
ttsTuningSubtitle: "Which preset and speed this character uses for each language. Left empty, the general presets (GPT-SoVITS Settings) apply. Under each row are the numbers in effect; blue ones differ from the general setting.",
|
||||
ttsTuningAll: "All languages",
|
||||
ttsTuningLang: "Speaking {lang}",
|
||||
ttsTuningFollow: "General ({preset})",
|
||||
@@ -4927,6 +4966,9 @@ export const i18n = {
|
||||
ttsTuningNumbers: "Numbers",
|
||||
ttsTuningNumbersHint: "Empty ones use the preset's number (shown in grey).",
|
||||
ttsTuningBadNumber: "The number for \"{name}\" is out of range.",
|
||||
ttsTuningActual: "In effect:",
|
||||
ttsTuningSameAsGeneral: "Same as the general setting:",
|
||||
ttsTuningGeneralValue: "General setting: {value}",
|
||||
ttsVersionTitle: "Anomalous_TTS version",
|
||||
ttsVersionHint: "Stable releases of Anomalous_TTS. Nothing goes online until \"Check for updates\"; an update takes effect after restarting ComfyUI, and \"Back to\" undoes it.",
|
||||
ttsVersionCheck: "Check for updates",
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
/**
|
||||
* Anomalous_TTS generation presets (its interface 16, docs/INTERFACE.md §5.4): the presets
|
||||
* file (default preset, rules per pair of languages, the three presets' numbers) and a
|
||||
* character's own settings (`defaults.preset`, `defaults.by_language`). Calls and pure
|
||||
* helpers only; ui_tts_presets.js and ui_tts_tuning.js draw them.
|
||||
* file (default preset, rules per pair of languages, the built-in presets' numbers, presets
|
||||
* made here as custom_<id>) and a character's own settings (`defaults.preset`,
|
||||
* `defaults.by_language`). Calls and pure helpers only; ui_tts_presets.js and
|
||||
* ui_tts_tuning.js draw them.
|
||||
*/
|
||||
|
||||
export const PRESETS_FORMAT = 16; // the node interface with presets
|
||||
export const PRESET_NAMES = ['stable', 'standard', 'lively'];
|
||||
export const BUILTIN = ['stable', 'standard', 'lively'];
|
||||
export const SAMPLING = ['top_k', 'top_p', 'temperature', 'repetition_penalty'];
|
||||
export const LANGS = ['ja', 'zh', 'en'];
|
||||
export const CROSS = 'cross';
|
||||
export const MAX_CUSTOM = 20; // the node's limits for presets made here
|
||||
export const LABEL_LENGTH = 24;
|
||||
|
||||
async function readJson(resp) {
|
||||
if (!resp.ok) {
|
||||
@@ -19,7 +22,7 @@ async function readJson(resp) {
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
/** `{default, presets, builtin, languages, cross, file}`. */
|
||||
/** `{default, presets, labels, builtin, languages, cross, file}`; `presets` lists the built-in ones first. */
|
||||
export async function loadPresets() {
|
||||
return readJson(await fetch('/anomalous_tts/presets'));
|
||||
}
|
||||
@@ -30,6 +33,25 @@ export async function savePresets(data) {
|
||||
}));
|
||||
}
|
||||
|
||||
export const isBuiltin = name => BUILTIN.includes(name);
|
||||
|
||||
/** A preset's shown name: a built-in one translated, one made here as it was named. */
|
||||
export function presetLabel(labels, name, t) {
|
||||
return isBuiltin(name) ? t(`ttsPreset_${name}`) : (labels?.[name] || name);
|
||||
}
|
||||
|
||||
/** A key for a new preset. Never one used before, so a character still naming a deleted preset does not pick up the new one. */
|
||||
export function newPresetId(names, now = Date.now()) {
|
||||
let id = `custom_${now.toString(36)}`;
|
||||
for (let n = 1; names.includes(id); n += 1) id = `custom_${(now + n).toString(36)}`;
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Whether `key` is not the same in every preset (the numbers table marks those). */
|
||||
export function presetsDiffer(numbers, key) {
|
||||
return new Set(Object.values(numbers).map(values => values[key])).size > 1;
|
||||
}
|
||||
|
||||
/** Rows for the language table: every "reference>spoken" pair the characters can meet, sorted. */
|
||||
export function languagePairs(groups, rules = {}) {
|
||||
const refs = new Set((groups || []).map(group => group.raw?.reference?.language || group.language).filter(lang => LANGS.includes(lang)));
|
||||
@@ -40,18 +62,16 @@ export function languagePairs(groups, rules = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The presets file from the dialog's state: only what differs from the built-in values
|
||||
* (a preset's changed numbers, the language rules that differ from the "other pairs" one).
|
||||
* The presets file from the settings group's state: of a built-in preset only the numbers that
|
||||
* differ from the built-in ones, a preset made here whole with its name, and the language rules
|
||||
* that differ from the "other pairs" one.
|
||||
*/
|
||||
export function presetsFile(view, { defaultName, numbers = {}, rules = {}, cross }) {
|
||||
export function presetsFile(view, { defaultName, numbers = {}, labels = {}, rules = {}, cross }) {
|
||||
const presets = {};
|
||||
for (const name of PRESET_NAMES) {
|
||||
const changed = {};
|
||||
for (const key of SAMPLING) {
|
||||
const value = numbers[name]?.[key] ?? view.presets[name][key];
|
||||
if (value !== view.builtin[name][key]) changed[key] = value;
|
||||
}
|
||||
if (Object.keys(changed).length) presets[name] = changed;
|
||||
for (const [name, values] of Object.entries(numbers)) {
|
||||
const block = isBuiltin(name) ? {} : { label: labels[name] };
|
||||
for (const key of SAMPLING) if (!isBuiltin(name) || values[key] !== view.builtin[name][key]) block[key] = values[key];
|
||||
if (Object.keys(block).length) presets[name] = block;
|
||||
}
|
||||
const languages = {};
|
||||
for (const [pair, name] of Object.entries(rules)) if (name && name !== cross) languages[pair] = name;
|
||||
@@ -62,12 +82,56 @@ export function presetsFile(view, { defaultName, numbers = {}, rules = {}, cross
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Where the general settings and the characters use preset `name`: `{general: [...], characters: [...]}`. */
|
||||
export function presetUses(state, name, groups) {
|
||||
const general = [];
|
||||
if (state.defaultName === name) general.push('default');
|
||||
for (const [pair, value] of Object.entries(state.rules)) if (value === name) general.push(pair);
|
||||
if (state.cross === name) general.push(CROSS);
|
||||
const characters = [];
|
||||
for (const group of groups || []) {
|
||||
const defaults = group.raw?.settings?.defaults;
|
||||
if (!defaults || typeof defaults !== 'object') continue;
|
||||
const blocks = [defaults, ...Object.values(defaults.by_language || {})];
|
||||
if (blocks.some(block => block?.preset === name)) characters.push(group.raw?.name || group.character);
|
||||
}
|
||||
return { general, characters };
|
||||
}
|
||||
|
||||
/** Take preset `name` out of the state; what used it goes back to the general setting (characters naming it do so by themselves). */
|
||||
export function removePreset(state, name) {
|
||||
delete state.numbers[name];
|
||||
delete state.labels[name];
|
||||
for (const [pair, value] of Object.entries(state.rules)) if (value === name) delete state.rules[pair];
|
||||
if (state.cross === name) state.cross = 'stable';
|
||||
if (state.defaultName === name) state.defaultName = 'standard';
|
||||
}
|
||||
|
||||
/** The preset a line falls back to without the character's own settings (layers 1 and 3 of §5.4). */
|
||||
export function generalPreset(view, refLang, lang) {
|
||||
if (!refLang || refLang === lang) return view.default;
|
||||
return view.languages[`${refLang}>${lang}`] || view.cross;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a character's lines are sampled with for one dialog row (`all` = in its own language),
|
||||
* next to what the general presets alone give: layers 1 to 4 of §5.4. A preset deleted since is skipped.
|
||||
*/
|
||||
export function lineTuning(view, rows, refLang, key) {
|
||||
const lang = key === 'all' ? refLang : key;
|
||||
const preset = name => view.presets[name] || {};
|
||||
const general = { ...preset(generalPreset(view, refLang, lang)), speed: 1 };
|
||||
const values = { ...preset(view.default), speed: 1 };
|
||||
const apply = row => {
|
||||
Object.assign(values, preset(row.preset), row.numbers);
|
||||
if (typeof row.speed === 'number') values.speed = row.speed;
|
||||
};
|
||||
apply(rows.all);
|
||||
if (refLang && lang !== refLang) Object.assign(values, preset(generalPreset(view, refLang, lang)));
|
||||
if (key !== 'all') apply(rows[key]);
|
||||
return { values, general };
|
||||
}
|
||||
|
||||
/**
|
||||
* A character's own settings as dialog rows: `all` (the `defaults` block) and one per language,
|
||||
* each `{preset: '' | name, numbers: {key: value}, speed: number | null}` ('' = follow the general one).
|
||||
@@ -75,7 +139,7 @@ export function generalPreset(view, refLang, lang) {
|
||||
export function tuningRows(settings) {
|
||||
const defaults = settings?.defaults && typeof settings.defaults === 'object' ? settings.defaults : {};
|
||||
const row = block => ({
|
||||
preset: PRESET_NAMES.includes(block?.preset) ? block.preset : '',
|
||||
preset: typeof block?.preset === 'string' ? block.preset : '',
|
||||
numbers: Object.fromEntries(SAMPLING.filter(key => typeof block?.[key] === 'number').map(key => [key, block[key]])),
|
||||
speed: typeof block?.speed === 'number' ? block.speed : null,
|
||||
});
|
||||
|
||||
+139
-42
@@ -1,28 +1,23 @@
|
||||
import { t } from './interface_settings.js';
|
||||
import { anomalousAlert, anomalousConfirm, anomalousPrompt } from './ui_dialog.js';
|
||||
import { button, el } from './usage_view.js';
|
||||
import { PRESET_NAMES, PRESETS_FORMAT, SAMPLING, languagePairs, loadPresets, presetsFile, savePresets } from './tts_presets_api.js';
|
||||
import {
|
||||
BUILTIN, CROSS, LABEL_LENGTH, MAX_CUSTOM, PRESETS_FORMAT, SAMPLING, isBuiltin, languagePairs, loadPresets,
|
||||
newPresetId, presetLabel, presetUses, presetsDiffer, presetsFile, removePreset, savePresets,
|
||||
} from './tts_presets_api.js';
|
||||
|
||||
/**
|
||||
* "Generation presets" in GPT-SoVITS Settings: the default preset for every character,
|
||||
* which preset a pair of languages uses (a Japanese character speaking Chinese), and,
|
||||
* folded, the three presets' numbers. Every change is saved at once to the node's presets
|
||||
* file; a character's own settings are in ui_tts_tuning.js.
|
||||
* "Generation presets" in GPT-SoVITS Settings: the default preset for every character, which
|
||||
* preset a pair of languages uses (a Japanese character speaking Chinese), and every preset's
|
||||
* numbers side by side the way the compare window shows values: numbers that differ between
|
||||
* presets marked, shared ones grey, changed built-in numbers with their built-in value. The
|
||||
* built-in presets can be changed and reset; new ones are made from a copy, renamed, deleted.
|
||||
* Every change is saved at once to the node's presets file; a character's own settings are in
|
||||
* ui_tts_tuning.js.
|
||||
*/
|
||||
|
||||
const RANGES = { top_k: [1, 100, 1], top_p: [0.05, 1, 0.05], temperature: [0.05, 2, 0.05], repetition_penalty: [1, 2, 0.05] };
|
||||
|
||||
function presetSelect(value, onChange) {
|
||||
const select = el('select', 'anomalous-sd-run-select');
|
||||
for (const name of PRESET_NAMES) {
|
||||
const option = el('option', '', t(`ttsPreset_${name}`));
|
||||
option.value = name;
|
||||
select.append(option);
|
||||
}
|
||||
select.value = value;
|
||||
select.onchange = () => onChange(select.value);
|
||||
return select;
|
||||
}
|
||||
|
||||
function numberInput(key, value, onChange) {
|
||||
const [min, max, step] = RANGES[key];
|
||||
const input = el('input', 'anomalous-tts-number');
|
||||
@@ -48,7 +43,10 @@ export function renderTtsPresets(status, groups, local) {
|
||||
group.append(note, body);
|
||||
let view = null;
|
||||
let state = null;
|
||||
let showSame = true;
|
||||
|
||||
const names = () => Object.keys(state.numbers);
|
||||
const label = name => presetLabel(state.labels, name, t);
|
||||
const save = async () => {
|
||||
try {
|
||||
view = await savePresets(presetsFile(view, state));
|
||||
@@ -63,22 +61,115 @@ export function renderTtsPresets(status, groups, local) {
|
||||
save();
|
||||
};
|
||||
|
||||
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; asks again while it is taken. */
|
||||
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 (from) => {
|
||||
if (names().length - BUILTIN.length >= MAX_CUSTOM) return anomalousAlert(t('ttsPresetTooMany', { count: MAX_CUSTOM }));
|
||||
const wanted = await askName(t('ttsPresetNewName', { from: label(from), count: LABEL_LENGTH }));
|
||||
if (!wanted) return;
|
||||
change(() => {
|
||||
const id = newPresetId(names());
|
||||
state.numbers[id] = { ...state.numbers[from] };
|
||||
state.labels[id] = wanted;
|
||||
});
|
||||
};
|
||||
|
||||
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('、') })}` : '');
|
||||
if (await anomalousConfirm(message, t('ttsPresetsTitle'), { okLabel: t('ttsPresetDelete') })) change(() => removePreset(state, name));
|
||||
};
|
||||
|
||||
/** Every preset's numbers side by side, one column per preset. */
|
||||
const numbersTable = () => {
|
||||
const all = names();
|
||||
const table = el('div', 'anomalous-tts-preset-table');
|
||||
table.style.gridTemplateColumns = `auto repeat(${all.length}, minmax(68px, 1fr))`;
|
||||
table.append(el('span', ''));
|
||||
for (const name of all) {
|
||||
const head = el('span', 'anomalous-tts-preset-head');
|
||||
head.append(el('strong', '', label(name)), el('span', 'anomalous-tts-preset-kind', t(isBuiltin(name) ? 'ttsPresetBuiltin' : 'ttsPresetCustom')));
|
||||
table.append(head);
|
||||
}
|
||||
for (const key of SAMPLING) {
|
||||
const differs = presetsDiffer(state.numbers, key);
|
||||
if (!differs && !showSame) continue;
|
||||
table.append(el('span', `anomalous-tts-setup-meta${differs ? '' : ' is-same'}`, t(`scriptRunParamName_${key}`)));
|
||||
for (const name of all) {
|
||||
const value = state.numbers[name][key];
|
||||
const builtin = isBuiltin(name) ? view.builtin[name][key] : value;
|
||||
const cell = el('span', `anomalous-tts-preset-cell${differs ? ' is-diff' : ' is-same'}${value !== builtin ? ' is-edited' : ''}`);
|
||||
const input = numberInput(key, value, number => change(() => { state.numbers[name][key] = number; }));
|
||||
input.dataset.cell = `${name}:${key}`;
|
||||
input.disabled = !local;
|
||||
cell.append(input);
|
||||
if (value !== builtin) cell.append(el('span', 'anomalous-tts-preset-was', t('ttsPresetWas', { value: builtin })));
|
||||
table.append(cell);
|
||||
}
|
||||
}
|
||||
if (local) {
|
||||
table.append(el('span', ''));
|
||||
for (const name of all) {
|
||||
const actions = el('span', 'anomalous-tts-preset-actions');
|
||||
if (!isBuiltin(name)) {
|
||||
actions.append(button('anomalous-tts-link', t('ttsPresetRename'), () => renamePreset(name)),
|
||||
button('anomalous-tts-link is-danger', t('ttsPresetDelete'), () => deletePreset(name)));
|
||||
} else if (SAMPLING.some(key => state.numbers[name][key] !== view.builtin[name][key])) {
|
||||
actions.append(button('anomalous-tts-link', t('ttsPresetResetOne'), () => change(() => { state.numbers[name] = { ...view.builtin[name] }; })));
|
||||
}
|
||||
table.append(actions);
|
||||
}
|
||||
}
|
||||
return table;
|
||||
};
|
||||
|
||||
const draw = () => {
|
||||
const focused = document.activeElement?.dataset?.cell;
|
||||
const tiles = el('div', 'anomalous-tts-preset-tiles');
|
||||
for (const name of PRESET_NAMES) {
|
||||
for (const name of names()) {
|
||||
const tile = button(`anomalous-tts-preset-tile${state.defaultName === name ? ' is-active' : ''}`, '', () => {
|
||||
if (local && state.defaultName !== name) change(() => { state.defaultName = name; });
|
||||
});
|
||||
tile.append(el('strong', '', t(`ttsPreset_${name}`)), el('span', '', t(`ttsPresetDesc_${name}`)));
|
||||
tile.append(el('strong', '', label(name)), el('span', '', t(isBuiltin(name) ? `ttsPresetDesc_${name}` : 'ttsPresetDescCustom')));
|
||||
tile.disabled = !local;
|
||||
tiles.append(tile);
|
||||
}
|
||||
|
||||
const table = el('div', 'anomalous-tts-preset-rules');
|
||||
const row = (label, select) => {
|
||||
select.disabled = !local;
|
||||
table.append(el('span', 'anomalous-tts-setup-meta', label), select);
|
||||
};
|
||||
const rules = el('div', 'anomalous-tts-preset-rules');
|
||||
const row = (text, select) => rules.append(el('span', 'anomalous-tts-setup-meta', text), select);
|
||||
for (const pair of languagePairs(groups, view.languages)) {
|
||||
const [ref, lang] = pair.split('>');
|
||||
row(t('ttsPresetsPair', { ref: t(`scriptRunLang_${ref}`), lang: t(`scriptRunLang_${lang}`) }),
|
||||
@@ -86,32 +177,38 @@ export function renderTtsPresets(status, groups, local) {
|
||||
}
|
||||
row(t('ttsPresetsCross'), presetSelect(state.cross, value => change(() => { state.cross = value; })));
|
||||
|
||||
const numbers = el('details', 'anomalous-tts-preset-numbers');
|
||||
numbers.append(el('summary', '', t('ttsPresetsNumbers')));
|
||||
const grid = el('div', 'anomalous-tts-preset-grid');
|
||||
grid.append(el('span', ''), ...SAMPLING.map(key => el('span', 'anomalous-tts-setup-meta', t(`scriptRunParamName_${key}`))));
|
||||
for (const name of PRESET_NAMES) {
|
||||
grid.append(el('span', '', t(`ttsPreset_${name}`)));
|
||||
for (const key of SAMPLING) {
|
||||
const input = numberInput(key, state.numbers[name][key], value => change(() => { state.numbers[name][key] = value; }));
|
||||
input.disabled = !local;
|
||||
grid.append(input);
|
||||
const numbersHead = el('div', 'anomalous-tts-preset-numbers-head');
|
||||
const sameBox = el('label', 'anomalous-tts-preset-same');
|
||||
const box = el('input');
|
||||
box.type = 'checkbox';
|
||||
box.checked = showSame;
|
||||
box.onchange = () => { showSame = box.checked; draw(); };
|
||||
sameBox.append(box, document.createTextNode(t('ttsPresetsShowSame')));
|
||||
numbersHead.append(el('span', 'anomalous-tts-setup-meta', t('ttsPresetsNumbers')), sameBox);
|
||||
|
||||
const tools = el('div', 'anomalous-tts-preset-tools');
|
||||
if (local) {
|
||||
const from = presetSelect(state.defaultName, () => {});
|
||||
tools.append(el('span', 'anomalous-tts-setup-meta', t('ttsPresetNewFrom')), from,
|
||||
button('anomalous-tts-link', t('ttsPresetNew'), () => addPreset(from.value)));
|
||||
if (BUILTIN.some(name => SAMPLING.some(key => state.numbers[name][key] !== view.builtin[name][key]))) {
|
||||
tools.append(button('anomalous-tts-link', t('ttsPresetsReset'), () => change(() => {
|
||||
for (const name of BUILTIN) state.numbers[name] = { ...view.builtin[name] };
|
||||
})));
|
||||
}
|
||||
}
|
||||
const reset = button('anomalous-tts-link', t('ttsPresetsReset'), () => change(() => {
|
||||
state.numbers = structuredClone(view.builtin);
|
||||
}));
|
||||
reset.disabled = !local;
|
||||
numbers.append(grid, reset);
|
||||
numbers.open = body.querySelector('details')?.open || false;
|
||||
|
||||
const scroll = el('div', 'anomalous-tts-preset-scroll');
|
||||
scroll.append(numbersTable());
|
||||
body.replaceChildren(el('div', 'anomalous-tts-setup-meta', t('ttsPresetDefault')), tiles,
|
||||
el('div', 'anomalous-tts-setup-meta', t('ttsPresetsLanguages')), table, numbers);
|
||||
el('div', 'anomalous-tts-setup-meta', t('ttsPresetsLanguages')), rules, numbersHead, scroll, tools,
|
||||
el('div', 'anomalous-tts-hint', t('ttsPresetsLegend')));
|
||||
if (focused) body.querySelector(`[data-cell="${focused}"]`)?.focus();
|
||||
};
|
||||
|
||||
loadPresets().then(data => {
|
||||
view = data;
|
||||
state = { defaultName: data.default, rules: { ...data.languages }, cross: data.cross, numbers: structuredClone(data.presets) };
|
||||
state = { defaultName: data.default, rules: { ...data.languages }, cross: data.cross, numbers: structuredClone(data.presets), labels: { ...data.labels } };
|
||||
draw();
|
||||
}).catch(e => {
|
||||
body.textContent = t('ttsPresetsSaveFailed', { error: e.message });
|
||||
|
||||
@@ -3,12 +3,13 @@ import { createViewScope } from './ui_lifecycle.js';
|
||||
import { anomalousAlert } from './ui_dialog.js';
|
||||
import { button, el } from './usage_view.js';
|
||||
import { fetchGptSovitsCharacter, saveGptSovitsSettings } from './audio_engines.js';
|
||||
import { LANGS, PRESET_NAMES, SAMPLING, generalPreset, loadPresets, mergeTuning, tuningRows } from './tts_presets_api.js';
|
||||
import { LANGS, SAMPLING, generalPreset, lineTuning, loadPresets, mergeTuning, presetLabel, tuningRows } from './tts_presets_api.js';
|
||||
|
||||
/**
|
||||
* A character's generation settings (Anomalous_TTS interface 16): a preset and speed for
|
||||
* the character as a whole and for each language it speaks, numbers when a preset is not
|
||||
* enough. Empty = the general presets (GPT-SoVITS Settings). Saving keeps every other
|
||||
* enough. Empty = the general presets (GPT-SoVITS Settings). Each row shows the numbers its
|
||||
* lines get, marked where they differ from the general presets. Saving keeps every other
|
||||
* settings field, `defaults.language` included.
|
||||
*/
|
||||
|
||||
@@ -68,25 +69,55 @@ export function openTuningEditor(group, { onSaved } = {}) {
|
||||
// The character's own language first: it is the row most people change.
|
||||
const order = ['all', ...[...LANGS].sort((a, b) => (b === refLang) - (a === refLang))];
|
||||
|
||||
/** A row as typed so far; numbers out of range are left out until they are fixed. */
|
||||
const typedRow = ({ select, speed, fields }) => {
|
||||
const numbers = {};
|
||||
for (const param of SAMPLING) {
|
||||
const value = readNumber(param, fields[param]);
|
||||
if (typeof value === 'number' && !Number.isNaN(value)) numbers[param] = value;
|
||||
}
|
||||
const speedValue = readNumber('speed', speed);
|
||||
return { preset: select.value, numbers, speed: Number.isNaN(speedValue) ? null : speedValue };
|
||||
};
|
||||
|
||||
// Each row's numbers in effect, marked where they differ from the general presets alone.
|
||||
const showEffect = () => {
|
||||
const rows = Object.fromEntries(Object.entries(inputs).map(([key, row]) => [key, typedRow(row)]));
|
||||
for (const [key, { effect }] of Object.entries(inputs)) {
|
||||
const { values, general } = lineTuning(view, rows, refLang, key);
|
||||
const items = [...SAMPLING, 'speed'].map(param => {
|
||||
const item = el('span', `anomalous-tts-tuning-value${values[param] !== general[param] ? ' is-diff' : ''}`,
|
||||
`${param === 'speed' ? t('ttsTuningSpeed') : t(`scriptRunParamName_${param}`)} ${values[param]}`);
|
||||
if (values[param] !== general[param]) item.title = t('ttsTuningGeneralValue', { value: general[param] });
|
||||
return item;
|
||||
});
|
||||
const same = items.every(item => !item.classList.contains('is-diff'));
|
||||
effect.replaceChildren(el('span', 'anomalous-tts-setup-meta', t(same ? 'ttsTuningSameAsGeneral' : 'ttsTuningActual')), ...items);
|
||||
}
|
||||
};
|
||||
|
||||
const draw = () => {
|
||||
const rows = tuningRows(settings);
|
||||
const names = Object.keys(view.presets);
|
||||
const label = name => presetLabel(view.labels, name, t);
|
||||
inputs = {};
|
||||
rowsBox.replaceChildren();
|
||||
for (const key of order) {
|
||||
const row = rows[key];
|
||||
const general = key === 'all' ? view.default : (key === refLang && rows.all.preset) || generalPreset(view, refLang, key);
|
||||
const general = key === 'all' ? view.default : (key === refLang && view.presets[rows.all.preset] && rows.all.preset) || generalPreset(view, refLang, key);
|
||||
const line = el('div', 'anomalous-tts-tuning-row');
|
||||
const label = key === 'all' ? t('ttsTuningAll') : t('ttsTuningLang', { lang: t(`scriptRunLang_${key}`) });
|
||||
const rowLabel = key === 'all' ? t('ttsTuningAll') : t('ttsTuningLang', { lang: t(`scriptRunLang_${key}`) });
|
||||
const select = el('select', 'anomalous-sd-run-select');
|
||||
const follow = el('option', '', t('ttsTuningFollow', { preset: t(`ttsPreset_${general}`) }));
|
||||
const follow = el('option', '', t('ttsTuningFollow', { preset: label(general) }));
|
||||
follow.value = '';
|
||||
select.append(follow);
|
||||
for (const preset of PRESET_NAMES) {
|
||||
const option = el('option', '', t(`ttsPreset_${preset}`));
|
||||
for (const preset of names) {
|
||||
const option = el('option', '', label(preset));
|
||||
option.value = preset;
|
||||
select.append(option);
|
||||
}
|
||||
select.value = row.preset;
|
||||
// A preset deleted since reads as "general", as the node treats it.
|
||||
select.value = names.includes(row.preset) ? row.preset : '';
|
||||
const speed = numberInput('speed', row.speed, '1.0');
|
||||
const numbers = el('details', 'anomalous-tts-tuning-numbers');
|
||||
numbers.append(el('summary', '', t('ttsTuningNumbers')));
|
||||
@@ -97,15 +128,21 @@ export function openTuningEditor(group, { onSaved } = {}) {
|
||||
fields[param] = numberInput(param, row.numbers[param], shown()[param]);
|
||||
grid.append(el('span', 'anomalous-tts-setup-meta', t(`scriptRunParamName_${param}`)), fields[param]);
|
||||
}
|
||||
select.onchange = () => { for (const param of SAMPLING) fields[param].placeholder = String(shown()[param]); };
|
||||
select.onchange = () => {
|
||||
for (const param of SAMPLING) fields[param].placeholder = String(shown()[param]);
|
||||
showEffect();
|
||||
};
|
||||
numbers.open = Object.keys(row.numbers).length > 0;
|
||||
numbers.append(grid, el('div', 'anomalous-tts-hint', t('ttsTuningNumbersHint')));
|
||||
const speedBox = el('label', 'anomalous-tts-tuning-speed');
|
||||
speedBox.append(el('span', 'anomalous-tts-setup-meta', t('ttsTuningSpeed')), speed);
|
||||
line.append(el('span', 'anomalous-tts-tuning-label', label), select, speedBox, numbers);
|
||||
const effect = el('div', 'anomalous-tts-tuning-effect');
|
||||
line.append(el('span', 'anomalous-tts-tuning-label', rowLabel), select, speedBox, effect, numbers);
|
||||
line.addEventListener('input', showEffect);
|
||||
rowsBox.append(line);
|
||||
inputs[key] = { select, speed, fields };
|
||||
inputs[key] = { select, speed, fields, effect };
|
||||
}
|
||||
showEffect();
|
||||
};
|
||||
|
||||
const footer = el('div', 'anomalous-voice-modal-footer');
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
@import url("./styles/09-prompt-drawers.css?v=20261007-theme-2");
|
||||
@import url("./styles/10-model-sources.css?v=20261007-theme-2");
|
||||
@import url("./styles/11-audio-studio.css?v=20261017-tts-versions");
|
||||
@import url("./styles/12-tts-setup.css?v=20261017-presets");
|
||||
@import url("./styles/12-tts-setup.css?v=20261017-presets2");
|
||||
@import url("./styles/13-script-director.css?v=20261007-theme-2");
|
||||
@import url("./styles/14-version-manager.css?v=20261007-theme-2");
|
||||
@import url("./styles/15-shell-rail.css?v=20261016-remote-3");
|
||||
|
||||
+110
-4
@@ -79,7 +79,7 @@
|
||||
/* Generation presets (ui_tts_presets.js) and a character's generation settings (ui_tts_tuning.js) */
|
||||
.anomalous-tts-preset-tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,113 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-numbers summary,
|
||||
/* Every preset's numbers side by side, marked like the compare window (27-param-compare.css). */
|
||||
.anomalous-tts-preset-numbers-head,
|
||||
.anomalous-tts-preset-tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-same {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
color: var(--amb-text-soft);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-table {
|
||||
display: grid;
|
||||
align-items: start;
|
||||
gap: 6px 8px;
|
||||
min-width: min-content;
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-table > .anomalous-tts-setup-meta {
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-head strong {
|
||||
overflow: hidden;
|
||||
color: var(--amb-text-main);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-kind,
|
||||
.anomalous-tts-preset-was {
|
||||
color: var(--amb-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-cell.is-diff .anomalous-tts-number {
|
||||
background: color-mix(in srgb, var(--amb-link) 24%, transparent);
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-cell.is-edited .anomalous-tts-number {
|
||||
background: color-mix(in srgb, var(--amb-warn) 20%, transparent);
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-cell.is-edited .anomalous-tts-preset-was {
|
||||
color: var(--amb-warn-text);
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-cell.is-same .anomalous-tts-number,
|
||||
.anomalous-tts-preset-table > .anomalous-tts-setup-meta.is-same {
|
||||
color: var(--amb-text-muted);
|
||||
}
|
||||
|
||||
.anomalous-tts-preset-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 8px;
|
||||
}
|
||||
|
||||
.anomalous-tts-link.is-danger {
|
||||
color: var(--amb-danger);
|
||||
}
|
||||
|
||||
/* A dialog row's numbers in effect; blue = not what the general presets give. */
|
||||
.anomalous-tts-tuning-effect {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
grid-column: 1 / -1;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.anomalous-tts-tuning-value {
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
color: var(--amb-text-muted);
|
||||
}
|
||||
|
||||
.anomalous-tts-tuning-value.is-diff {
|
||||
background: color-mix(in srgb, var(--amb-link) 16%, transparent);
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-tts-tuning-numbers summary {
|
||||
cursor: pointer;
|
||||
color: var(--amb-link);
|
||||
@@ -887,7 +993,7 @@
|
||||
.anomalous-tts-card[data-tone="ready"] .anomalous-tts-card-pill { background: rgba(52, 211, 153, 0.14); color: var(--amb-ok); }
|
||||
.anomalous-tts-card[data-tone="needed"] .anomalous-tts-card-pill { background: rgba(251, 191, 36, 0.14); color: var(--amb-warn); }
|
||||
.anomalous-tts-card[data-tone="error"] .anomalous-tts-card-pill { background: rgba(248, 113, 113, 0.14); color: var(--amb-danger); }
|
||||
.anomalous-tts-card[data-tone="busy"] .anomalous-tts-card-pill { background: color-mix(in srgb, var(--amb-link) 14%, transparent); color: var(--amb-link); }
|
||||
.anomalous-tts-card[data-tone="busy"] .anomalous-tts-card-pill { background: color-mix(in srgb, var(--amb-link) 24%, transparent); color: var(--amb-link); }
|
||||
|
||||
.anomalous-tts-card-meta {
|
||||
flex-shrink: 0;
|
||||
@@ -934,7 +1040,7 @@
|
||||
padding: 6px 14px;
|
||||
border: 1px solid color-mix(in srgb, var(--amb-link) 40%, transparent);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--amb-link) 14%, transparent);
|
||||
background: color-mix(in srgb, var(--amb-link) 24%, transparent);
|
||||
color: var(--amb-text-main);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user