Voice-over: three layers, whole script / character / line, and a draft
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
db045b8f57
commit
2a7cfe5075
+11
-8
@@ -389,14 +389,17 @@ covered by the `styles.css` manifest.
|
||||
click picks (`ui_script_cast.js`: Ctrl / Shift, the characters' colours), a double-click or
|
||||
✎ edits the text in place, the name opens a menu of characters, emotion chips per the
|
||||
line's character, a tag when the line is set for itself, buttons on hover.
|
||||
The left side only picks; the side panel follows it (`ui_script_tune.js`; a character or
|
||||
lines get the tabs How it reads, Numbers, Settings): nothing picked,
|
||||
the whole script (each character and how it reads); a character clicked in the list
|
||||
(`castScriptCharacter`), its language, speed and mode in this script; picked lines, who
|
||||
reads them and how, each value following the character until set there (↺ follows again).
|
||||
`script_tune.js` holds the rules without DOM: a line's values over its character's, and
|
||||
the tags Anomalous_TTS interface 17 reads for them (the language unless auto, the speed
|
||||
when it is not the character's saved one, the mode unless Auto).
|
||||
The left side only picks; the side panel follows it (`ui_script_tune.js`) in three
|
||||
layers for this script, under a path (Whole script › character › line): nothing picked,
|
||||
the whole script (and each character in it); a character clicked in the list
|
||||
(`castScriptCharacter`), that character in this script; picked lines, who reads them and
|
||||
how. Each layer has the same rows (speed, tone, language) with where each value comes
|
||||
from; lasting settings are links at the foot. `script_tune.js` holds the rules without
|
||||
DOM: the layers (line over character over whole script over the character's usual
|
||||
values), each holding only the fields set on it, and the tags Anomalous_TTS interface 17
|
||||
reads (only where a line differs from what the node gives by itself; the whole script's
|
||||
values go into the node's inputs). The director keeps the script as a draft in
|
||||
`localStorage` (lines, who reads, every layer) and loads it when the panel is made.
|
||||
It puts the cards on one side and generating on the other: beside
|
||||
them once the page is wide, below them when it is narrow (docked browser); on a
|
||||
wide page `ui_split_bar.js` is the divider between them (dragged, remembered).
|
||||
|
||||
+5
-3
@@ -48,10 +48,12 @@
|
||||
|
||||
### 🎭 Several characters in one script (多角色配音)
|
||||
- **Every line has its character**: on Voice-over, click a character in the list on the left, then paste: the lines are its. To give lines to another, pick them (a click; Ctrl / ⌘ adds one, Shift a run; Esc lets go) and choose who reads them on the right, or click the name on a line for a menu of characters. Each line shows who reads it in that character's colour, and its emotions are that character's. The *Who speaks* bar at the top is gone: the list does its job.
|
||||
- **Pick on the left, change it on the right (左边选,右边改)**: the side panel follows what is picked. Nothing picked: the whole script, each character with how it reads (a click opens it). A character clicked in the list: its language, speed and mode **in this script**, for every line of it (*Save as default* keeps the language and speed for next time). Picked lines: who reads them, and their own language, speed and mode; each one shows **Follow** until you set it, the card then carries a small tag with what it set, and ↺ (or *Follow the character in everything*) lets it follow again. Esc or *‹ Whole script* goes back. The modes can be heard on that line right in the panel. With many characters, *Read by* lists the ones in this script (with their colours) and the rest under *Other characters*, a menu with a search box; a card's name menu gets the same search box.
|
||||
- **Less on screen, plainer words (更清爽、更好懂)**: for a character or picked lines the panel has three tabs. **How it reads**: who reads, speed, language (one line while it is detected; *Change* opens it) and the tone with **▶ Listen to this line**. **Numbers**: how the tone compares and its numbers (top_k, temperature …, still editable). **Settings**: *Always read like this*, and the ways to the tones and the character's own settings. The tab you used last stays open. The words are plainer: the mode is now the **tone** (语气), the presets are **Steady / Natural / Lively** (平稳 / 自然 / 生动), *Follow* is *As the character* (同角色), *Save as default* is *Always read like this* (以后都这样念), a character's *Generation settings* are its *Own settings* (专属设置), and the GPT-SoVITS pretrained files are *base files* (基础文件). The names inside the node (stable / standard / lively) are unchanged, so saved workflows and settings keep working.
|
||||
- **Three layers: the whole script, a character, a line (整份剧本 / 角色 / 句子,三层分清)**: the left side picks, the side panel changes. Nothing picked: the **whole script**, what everyone reads with, and each character in it. A character clicked in the list: that character **in this script** (only its lines). Picked lines: **only them**, with who reads them. A path on top, *Whole script › Aris › Line 2*, says which layer you are changing and leads back up (Esc too). Every layer has the same three rows, speed, tone and language, and each row says where its value comes from: **this line** (yellow), **the character** (purple), **the whole script** or **the default** (grey), or the character's **usual** setting; a click on it goes to that layer, ↺ lets this layer follow the one above. The more particular wins: a line over its character over the whole script over the usual settings. The settings that last beyond this script (the tones, a character's own settings) are links at the foot, apart from the rest. The tone can be heard on that line right there; *Numbers* shows how it compares and its numbers. With many characters, *Read by* lists the ones in this script (with their colours) and the rest under *Other characters*, a menu with a search box; a card's name menu gets the same search box.
|
||||
- **The script comes back after a reload (剧本自动保存)**: the lines, who reads them and every layer's settings are kept in this browser and come back when Voice-over opens again.
|
||||
- Editing the script (*Edit text*) keeps each unchanged line's own settings; it used to drop them.
|
||||
- **Plainer words (更好懂)**: the mode is now the **tone** (语气), the presets are **Steady / Natural / Lively** (平稳 / 自然 / 生动), *Follow* is *As the character* (同角色), *Save as default* is *Always read like this* (以后都这样念), a character's *Generation settings* are its *Own settings* (专属设置), and the GPT-SoVITS pretrained files are *base files* (基础文件). The names inside the node (stable / standard / lively) are unchanged, so saved workflows and settings keep working.
|
||||
- **GPT-SoVITS Settings, in tabs (GPT-SoVITS 设置分类)**: a chip beside the title says everything is ready, or what is missing; tabs show one part at a time: **Tones** (which one to use, with a *Simple / Numbers* switch), **Other languages**, **Base files** (a dot when something there needs you, and the window opens there), **Where things are**, **Version**.
|
||||
- So one script can have a calm narrator, a lively character and one line read slower, all in one go. This needs Anomalous_TTS interface 17 (1.1.0), which reads `[lang:…]`, `[speed:…]` and `[preset:…]` on a line; the script pushed or copied to a node carries them too, and a file voiced again from the audio gallery comes back with them. With an older Anomalous_TTS the whole script is read as before, as its first line's character reads, and the panel says so.
|
||||
- So one script can have a calm narrator, a lively character and one line read slower, all in one go. This needs Anomalous_TTS interface 17 (1.1.0): the whole script's values go into the node's own inputs, and only the lines that differ get `[lang:…]`, `[speed:…]` or `[preset:…]`, so the text pushed or copied to a node stays readable; a file voiced again from the audio gallery comes back with them. With an older Anomalous_TTS the whole script is read as before, as its first line's character reads, and the panel says so.
|
||||
- **Picking and editing no longer get in each other's way**: a click on a line picks it; a double-click (or ✎) edits its text in place, Enter keeps it and Esc puts it back.
|
||||
- **The list on the left is the cast there**: narrower, the characters with their colours only; a click there never takes you back to the Characters page.
|
||||
- Generating, ▶ on a line, pushing, dragging and copying all follow who reads each line: the script carries `[name]` where the speaker changes (Anomalous_TTS reads it already), and in Auto mode each character uses its own generation settings. A file voiced again from the audio gallery comes back with its speakers.
|
||||
|
||||
@@ -67,13 +67,17 @@
|
||||
|
||||
页面够宽时分成两栏:左边是台词卡片,右边是 **生成语音** 和设置面板。窗口窄或者浏览器停靠在侧边时,这些放在台词下面。两种情况都只有台词这一块会滚动,句子再多,按钮也一直看得见。
|
||||
|
||||
**左边选,右边改。** 右边的面板跟着你选了什么变:
|
||||
**左边选,右边改;设置分三层。** 右边的面板跟着你选了什么变,最上面一条路径告诉你现在改的是哪一层,比如 `整份剧本 › Aris › 第 2 句`,点前面的就回到上一层(Esc 也行):
|
||||
|
||||
- **什么都没选**:整份剧本,每个角色一行,写着 TA 怎么念(语言 · 快慢 · 语气)、念几句。点一行就打开这个角色。
|
||||
- **在左边列表里点角色**:这个角色在这份剧本里的快慢、语言和语气(平时的 / 平稳 / 自然 / 生动 / 自建的)。改了,TA 的每一句都跟着变。只对这份剧本有效;点 **以后都这样念**,下次也这样(记住语言和快慢)。
|
||||
- **点句子**(Ctrl / Shift 多选):**谁念** 点名字就换人;快慢、语言、语气默认都是“同角色”,改了就只管这几句,卡片上会出现一个小黄标签写着改了什么。点 ↺ 或 **全部改回同角色** 就回去。
|
||||
- **整份剧本**(什么都没选):所有人默认按这个念。下面列出剧本里的每个角色,点一个就只改 TA。
|
||||
- **角色**(在左边列表里点角色):这个角色在这份剧本里怎么念,只影响 TA 的句子。点 **以后都这样念**,把 TA 的快慢和语言存成平时的,以后的剧本也这样。
|
||||
- **句子**(点句子,Ctrl / Shift 多选):**谁念** 点名字就换人;只影响选中的这几句,卡片上会出现一个小黄标签写着改了什么。
|
||||
|
||||
点了角色或句子,面板上方有三个分类:**怎么念**(谁念、快慢、语言、语气和 **▶ 试听这句**;语言是自动识别时只占一行,要换点 **改**)、**看数字**(和平时比差在哪、每一档的具体数字,top_k、temperature 这些可以直接改)、**设置**(以后都这样念、语气档位设置、专属设置)。上次停在哪个分类,下次还是它。按 Esc 或点 **‹ 整份剧本** 回到整份剧本。按角色、按句分开设置需要 Anomalous_TTS 接口版本 17(1.1.0);旧版本时整段按第一句的角色念,面板上会提示。
|
||||
每一层都是同样三行:**快慢、语气、语言**。每行右边的小标签写着这个值是哪一层定的:黄色“这一句”、紫色角色名、灰色“整份剧本”或“默认”,还有“TA 平时”(存过的默认)。点标签就跳到那一层;点 ↺ 让这一层不再单独设,跟上一层走。越具体的越优先:这一句 > 角色 > 整份剧本 > 角色平时的设置。
|
||||
|
||||
语气下面有 **▶ 试听这句** 和 **看数字**(和平时比差在哪、具体数字,top_k、temperature 这些可以直接改)。面板最下面一行是 **永久设置**(语气档位、角色的专属设置),它们不只管这份剧本,所以单独放着。
|
||||
|
||||
剧本会自动保存在这个浏览器里:刷新或重启 ComfyUI 后打开配音页,台词、谁念和各层的设置都还在。按角色、按句分开设置需要 Anomalous_TTS 接口版本 17(1.1.0);旧版本时整段按第一句的角色念,面板上会提示。
|
||||
|
||||
1. 点 **生成语音**。插件直接交给 ComfyUI 生成,画布上的工作流一个不动;ComfyUI 正在出图时会先排队。
|
||||
2. 生成完自动播放,写着“生成好了,用时几秒”。文件存在 `output/audio/角色名/`;点 **已存进音频库 ›** 直接去音频库。
|
||||
|
||||
+21
-2
@@ -2,7 +2,9 @@
|
||||
// (web/modules/script_tune.js), written into the script and read back (audio_script.js).
|
||||
import assert from 'node:assert/strict';
|
||||
import { buildScriptPackage, parseTaggedSpeech } from '../web/modules/audio_script.js';
|
||||
import { clearLineTune, hasOwnTune, lineValues, readTuneTag, setLineTune, sharedValue, tuneTags } from '../web/modules/script_tune.js';
|
||||
import {
|
||||
characterLayer, clearLineTune, hasOwnTune, lineLayer, lineValues, readTuneTag, scriptLayer, setLineTune, sharedValue, tuneTags,
|
||||
} from '../web/modules/script_tune.js';
|
||||
|
||||
const character = { language: 'auto', speed: 1, preset: '' };
|
||||
const stored = { language: 'auto', speed: 1, preset: '' };
|
||||
@@ -22,10 +24,25 @@ setLineTune([line], 'language', 'ja');
|
||||
clearLineTune([line]);
|
||||
assert.equal(hasOwnTune(line), false);
|
||||
|
||||
// Tags: the language unless auto, the speed unless it is the saved one, the mode unless Auto.
|
||||
// Tags: what differs from what the node gives by itself; back to auto is written as auto.
|
||||
assert.equal(tuneTags(character, stored), '');
|
||||
assert.equal(tuneTags({ language: 'ja', speed: 1.15000001, preset: 'stable' }, stored), '[lang:ja][speed:1.15][preset:stable]');
|
||||
assert.equal(tuneTags({ language: 'auto', speed: 1.1, preset: '' }, { speed: 1.1 }), '');
|
||||
assert.equal(tuneTags({ language: 'auto', speed: 1, preset: '' }, { language: 'zh', speed: 1, preset: 'lively' }), '[lang:auto][preset:auto]');
|
||||
|
||||
// Setting a line to what its character reads it with lets it follow again.
|
||||
const same = { text: 'x' };
|
||||
setLineTune([same], 'speed', 1.3, () => 1.3);
|
||||
assert.equal(hasOwnTune(same), false);
|
||||
|
||||
// Three layers, the most particular first, and where each value comes from.
|
||||
const base = { language: 'auto', speed: 1, preset: '' };
|
||||
assert.deepEqual(scriptLayer({ speed: 0.9 }, base), { values: { language: 'auto', speed: 0.9, preset: '' }, from: { language: 'default', speed: 'script', preset: 'default' } });
|
||||
const arisLayer = characterLayer({ preset: 'lively' }, { speed: 0.9, preset: 'stable' }, { language: 'ja', speed: 1.1, preset: '' }, base);
|
||||
assert.deepEqual(arisLayer.values, { language: 'ja', speed: 0.9, preset: 'lively' });
|
||||
assert.deepEqual(arisLayer.from, { language: 'usual', speed: 'script', preset: 'character' });
|
||||
const own = lineLayer({ text: 'a', tune: { speed: 1.2 } }, arisLayer);
|
||||
assert.deepEqual([own.values.speed, own.from.speed, own.from.language], [1.2, 'line', 'usual']);
|
||||
|
||||
// One value over several lines: mixed, all set by the lines, or some.
|
||||
const lines = [{ text: 'a', tune: { speed: 1.2 } }, { text: 'b' }];
|
||||
@@ -38,6 +55,8 @@ assert.deepEqual(readTuneTag('语言:中文'), { field: 'language', value: 'zh
|
||||
assert.deepEqual(readTuneTag('lang:fr'), { field: 'language', value: undefined });
|
||||
assert.deepEqual(readTuneTag('lang:JA'), { field: 'language', value: 'ja' });
|
||||
assert.deepEqual(readTuneTag('预设:custom_a1'), { field: 'preset', value: 'custom_a1' });
|
||||
assert.deepEqual(readTuneTag('preset:auto'), { field: 'preset', value: '' });
|
||||
assert.deepEqual(readTuneTag('lang:auto'), { field: 'language', value: 'auto' });
|
||||
assert.equal(readTuneTag('普拉娜'), null);
|
||||
|
||||
// Written into a bundle (before [take:N], after the speaker and emotion) and read back for "voice it again".
|
||||
|
||||
+43
-55
@@ -2494,14 +2494,13 @@ export const i18n = {
|
||||
scriptRunModeAutoBase: "平时的",
|
||||
scriptRunModeIsAuto: "这个角色平时的语气。",
|
||||
scriptRunModeAutoShort: "按语气档位和这个角色的专属设置(这里是说{lang}时)。",
|
||||
scriptRunModeWhole: "{about}。这个角色的每一句都用它。",
|
||||
scriptRunModeSaved: "已存进“{name}”,所有用它的角色都跟着变。",
|
||||
scriptRunOpenPresets: "语气档位设置",
|
||||
scriptRunOpenTuning: "{name} 的专属设置",
|
||||
scriptRunDefaultsSaved: '已保存',
|
||||
scriptRunDefaultsFailed: '保存默认设置失败:{error}',
|
||||
scriptRunSettingsBroken: '这个角色的设置文件有问题,先修好再保存:{error}',
|
||||
scriptRunSaveDefaultsHint: "记住这个角色的语言和快慢,下次直接用。语气要一直用,去“专属设置”。",
|
||||
scriptRunSaveDefaultsHint: "把 TA 现在的快慢和语言存为平时的,以后的剧本也这样。语气要一直用,去“专属设置”。",
|
||||
scriptRunParamHint_speed: "快慢,1 为正常",
|
||||
scriptRunParamHint_top_k: '每一步只在最可能的 k 个候选里挑。小:更稳、更平;大:更有变化,也更容易出错',
|
||||
scriptRunParamHint_top_p: '只在累计概率达到 p 的候选里挑。1 表示不限制',
|
||||
@@ -2570,27 +2569,35 @@ export const i18n = {
|
||||
scriptCastHint: "点句子选中(Ctrl / Shift 多选),右边就能改谁念、怎么念;双击改台词",
|
||||
scriptCastPicked: "已选 {count} 句:在右边改谁念、怎么念(Esc 取消)",
|
||||
scriptCastListHint: "点角色:右边显示 TA 在这份剧本里怎么念;之后贴进来、新加的句子也由 TA 念",
|
||||
scriptCardTuneHint: "这句单独调过。点它,在右边改,或点 ↺ 改回同角色",
|
||||
scriptCardTuneHint: "这句单独改过。点它在右边改,或点 ↺ 回到上一层",
|
||||
scriptTuneScript: "整份剧本",
|
||||
scriptTuneScriptMeta: "{count} 句 · {speakers} 个角色",
|
||||
scriptTuneScriptHint: "点左边的角色或句子,在这里改它们怎么念。",
|
||||
scriptTuneLineCount: "{count} 句",
|
||||
scriptTuneOwnCount: "其中 {count} 句单独调过",
|
||||
scriptTuneBack: "‹ 整份剧本",
|
||||
scriptTuneCharacter: "{name} · 在这份剧本里",
|
||||
scriptTuneOwnCount: "{count} 句单独改过",
|
||||
scriptTuneCharacterMeta: "念第 {lines} 句,共 {count} 句",
|
||||
scriptTuneCharacterNone: "这份剧本里还没有 TA 的台词",
|
||||
scriptTuneCharacterHint: "改这里,TA 的每一句都跟着变(单独调过的那几项除外)。只用于这份剧本;想以后都这样,点“以后都这样念”。",
|
||||
scriptTuneResetLines: "把单独调过的 {count} 句改回同角色",
|
||||
scriptTuneResetAll: "全部改回同角色",
|
||||
scriptTuneResetLines: "{count} 句单独改过:全部回到 TA 这一层",
|
||||
scriptTuneResetAll: "这几句全部回到上一层",
|
||||
scriptTuneLine: "第 {n} 句",
|
||||
scriptTuneLines: "选中的 {count} 句",
|
||||
scriptTuneReadBy: "由 {name} 念",
|
||||
scriptTuneWho: "谁念",
|
||||
scriptTuneFollow: "同角色",
|
||||
scriptTuneFromLine: "这一句",
|
||||
scriptTuneFromUsual: "{name}平时",
|
||||
scriptTuneFromDefault: "默认",
|
||||
scriptTuneFromMixed: "各不相同",
|
||||
scriptTuneFromHint_line: "这一句自己改的;点 ↺ 回到上一层",
|
||||
scriptTuneFromHint_character: "在这份剧本里给 {name} 改的;点一下去 TA 那一层",
|
||||
scriptTuneFromHint_script: "整份剧本改的;点一下去整份剧本",
|
||||
scriptTuneFromHint_usual: "{name}平时的设置(存过的);点一下去 TA 那一层",
|
||||
scriptTuneFromHint_default: "没人改过,用默认的",
|
||||
scriptTuneUsualHint: "按语气档位和角色的专属设置",
|
||||
scriptTuneNumbers: "看数字",
|
||||
scriptTuneLasting: "永久设置:",
|
||||
scriptTuneResetScript: "整份剧本全部回到默认",
|
||||
scriptTuneResetCharacter: "撤掉这份剧本里给 TA 改的",
|
||||
scriptTuneChangedHere: "本剧本改过",
|
||||
scriptTuneCast: "角色(点一个,只改 TA)",
|
||||
scriptTuneListen: "▶ 试听这句",
|
||||
scriptTuneLanguageIs: "语言:{value}",
|
||||
scriptTuneLanguageChange: "改",
|
||||
ttsPresetsPick: "平时用哪种语气",
|
||||
ttsPresetWhat: "{name}:{about}",
|
||||
ttsStatusReady: "✓ 一切就绪",
|
||||
@@ -2602,24 +2609,11 @@ export const i18n = {
|
||||
ttsSetupTab_version: "版本",
|
||||
ttsTonesSimple: "简单",
|
||||
ttsTonesNumbers: "看数字",
|
||||
scriptTuneTab_how: "怎么念",
|
||||
scriptTuneTab_numbers: "看数字",
|
||||
scriptTuneTab_settings: "设置",
|
||||
scriptTuneLinkPresets: "平稳、自然、生动各是什么数字,所有角色通用",
|
||||
scriptTuneLinkTuning: "这个角色说每种语言时的语气和快慢,以后一直这样",
|
||||
scriptTuneIsDefault: "现在就是 TA 平时的念法",
|
||||
ttsStatusDownloading: "基础文件下载中…",
|
||||
ttsStatusPackages: "缺 {count} 个 Python 包",
|
||||
scriptTuneOtherCharacters: "其他角色({count})▾",
|
||||
scriptDirectorSpeakerSearch: "搜索角色…",
|
||||
scriptTuneFollowValue: "同角色({value})",
|
||||
scriptTuneFollowEach: "各自的",
|
||||
scriptTuneFollowHint: "和念它的角色一样:{value}",
|
||||
scriptTuneFollowing: "同角色",
|
||||
scriptTuneReset: "改回同角色",
|
||||
scriptTuneFollowBase: "{name}平时",
|
||||
scriptTuneModeFollow: "和 {name} 平时一样。",
|
||||
scriptTuneModeLines: "{about}。选中的句子用它。",
|
||||
scriptTuneReset: "回到上一层",
|
||||
scriptTuneOldTts: "按角色、按句分开设置要 Anomalous_TTS 接口 17(1.1.0)。现在整段按第一句的角色念;可以在 GPT-SoVITS 设置里更新。",
|
||||
scriptDirectorCopy: "复制剧本",
|
||||
scriptDirectorCopied: "已复制",
|
||||
@@ -5133,14 +5127,13 @@ export const i18n = {
|
||||
scriptRunModeAutoBase: "usual",
|
||||
scriptRunModeIsAuto: "This character's usual tone.",
|
||||
scriptRunModeAutoShort: "From the tones and this character's own settings (shown: speaking {lang}).",
|
||||
scriptRunModeWhole: "{about}; every line of this character uses it.",
|
||||
scriptRunModeSaved: "Saved into \"{name}\", for every character using it.",
|
||||
scriptRunOpenPresets: "Tones",
|
||||
scriptRunOpenTuning: "{name}'s own settings",
|
||||
scriptRunDefaultsSaved: 'Saved',
|
||||
scriptRunDefaultsFailed: 'Could not save the defaults: {error}',
|
||||
scriptRunSettingsBroken: 'This character\'s settings file has a problem; fix it before saving: {error}',
|
||||
scriptRunSaveDefaultsHint: "Keeps this character's language and speed for next time. For a tone to stay, use Own settings.",
|
||||
scriptRunSaveDefaultsHint: "Keeps this character's speed and language as its usual ones, for later scripts too. For a tone to stay, use Own settings.",
|
||||
scriptRunParamHint_speed: 'Speaking speed, 1 = normal',
|
||||
scriptRunParamHint_top_k: 'Pick among the k likeliest candidates at each step. Lower: steadier and flatter; higher: livelier and more error-prone',
|
||||
scriptRunParamHint_top_p: 'Pick among candidates up to cumulative probability p. 1 = no limit',
|
||||
@@ -5209,27 +5202,35 @@ export const i18n = {
|
||||
scriptCastHint: "Click a line to pick it (Ctrl / Shift for more) and change who reads it and how on the right; double-click to edit it",
|
||||
scriptCastPicked: "{count} {count|line|lines} picked: change who reads {count|it|them} and how on the right (Esc lets go)",
|
||||
scriptCastListHint: "Click a character: the right side shows how it reads in this script; it also reads the lines pasted or added from now on",
|
||||
scriptCardTuneHint: "This line is set for itself. Click it to change that on the right, or ↺ to follow the character again",
|
||||
scriptCardTuneHint: "This line is set for itself. Click it to change that on the right, or ↺ to follow the layer above",
|
||||
scriptTuneScript: "Whole script",
|
||||
scriptTuneScriptMeta: "{count} {count|line|lines} · {speakers} {speakers|character|characters}",
|
||||
scriptTuneScriptHint: "Click a character or lines on the left to change how they are read.",
|
||||
scriptTuneLineCount: "{count} {count|line|lines}",
|
||||
scriptTuneOwnCount: "{count} set for {count|itself|themselves}",
|
||||
scriptTuneBack: "‹ Whole script",
|
||||
scriptTuneCharacter: "{name} in this script",
|
||||
scriptTuneCharacterMeta: "Reads {count|line|lines} {lines} ({count})",
|
||||
scriptTuneCharacterNone: "No lines for this character in the script yet",
|
||||
scriptTuneCharacterHint: "Every line of this character follows this (except what a line set for itself). For this script only; to keep it, press Always read like this.",
|
||||
scriptTuneResetLines: "Let the {count} {count|line|lines} set for {count|itself|themselves} follow the character again",
|
||||
scriptTuneResetAll: "Follow the character in everything",
|
||||
scriptTuneResetLines: "{count} {count|line|lines} set for {count|itself|themselves}: let them follow this character",
|
||||
scriptTuneResetAll: "Let these lines follow the layer above in everything",
|
||||
scriptTuneLine: "Line {n}",
|
||||
scriptTuneLines: "{count} lines picked",
|
||||
scriptTuneReadBy: "Read by {name}",
|
||||
scriptTuneWho: "Read by",
|
||||
scriptTuneFollow: "As the character",
|
||||
scriptTuneFromLine: "This line",
|
||||
scriptTuneFromUsual: "{name}'s usual",
|
||||
scriptTuneFromDefault: "Default",
|
||||
scriptTuneFromMixed: "Varies",
|
||||
scriptTuneFromHint_line: "Set for this line; ↺ follows the layer above again",
|
||||
scriptTuneFromHint_character: "Set for {name} in this script; click to go there",
|
||||
scriptTuneFromHint_script: "Set for the whole script; click to go there",
|
||||
scriptTuneFromHint_usual: "{name}'s usual setting (saved); click to go to {name}",
|
||||
scriptTuneFromHint_default: "Nobody changed it: the default",
|
||||
scriptTuneUsualHint: "From the tones and the character's own settings",
|
||||
scriptTuneNumbers: "Numbers",
|
||||
scriptTuneLasting: "Lasting settings:",
|
||||
scriptTuneResetScript: "Set the whole script back to the defaults",
|
||||
scriptTuneResetCharacter: "Undo what was changed for this character here",
|
||||
scriptTuneChangedHere: "changed here",
|
||||
scriptTuneCast: "Characters (click one to change only it)",
|
||||
scriptTuneListen: "▶ Listen to this line",
|
||||
scriptTuneLanguageIs: "Language: {value}",
|
||||
scriptTuneLanguageChange: "Change",
|
||||
ttsPresetsPick: "Which tone to use",
|
||||
ttsPresetWhat: "{name}: {about}",
|
||||
ttsStatusReady: "✓ All set",
|
||||
@@ -5241,24 +5242,11 @@ export const i18n = {
|
||||
ttsSetupTab_version: "Version",
|
||||
ttsTonesSimple: "Simple",
|
||||
ttsTonesNumbers: "Numbers",
|
||||
scriptTuneTab_how: "How it reads",
|
||||
scriptTuneTab_numbers: "Numbers",
|
||||
scriptTuneTab_settings: "Settings",
|
||||
scriptTuneLinkPresets: "What Steady, Natural and Lively are, for every character",
|
||||
scriptTuneLinkTuning: "This character's tone and speed in each language, from now on",
|
||||
scriptTuneIsDefault: "This is how it usually reads",
|
||||
ttsStatusDownloading: "Downloading base files…",
|
||||
ttsStatusPackages: "{count} Python {count|package|packages} missing",
|
||||
scriptTuneOtherCharacters: "Other characters ({count}) ▾",
|
||||
scriptDirectorSpeakerSearch: "Search characters…",
|
||||
scriptTuneFollowValue: "As the character ({value})",
|
||||
scriptTuneFollowEach: "each its own",
|
||||
scriptTuneFollowHint: "The same as the character reading it: {value}",
|
||||
scriptTuneFollowing: "As the character",
|
||||
scriptTuneReset: "Follow the character again",
|
||||
scriptTuneFollowBase: "{name}'s usual",
|
||||
scriptTuneModeFollow: "The same as {name}'s usual.",
|
||||
scriptTuneModeLines: "{about}; the picked lines use it.",
|
||||
scriptTuneReset: "Follow the layer above",
|
||||
scriptTuneOldTts: "Settings per character and per line need Anomalous_TTS interface 17 (1.1.0). Until then the whole script is read as its first line's character reads; update it in GPT-SoVITS Settings.",
|
||||
scriptDirectorCopy: "Copy script",
|
||||
scriptDirectorCopied: "Copied",
|
||||
|
||||
+71
-18
@@ -1,9 +1,12 @@
|
||||
/**
|
||||
* How each line of a voice-over is read (no DOM): its language, speed and mode. A line follows
|
||||
* its character unless it was set for itself (`line.tune`, only the fields set); the character's
|
||||
* values in this script come from the Script Director's run section (its saved defaults, changed
|
||||
* here for this script). Anomalous_TTS interface 17 reads a line's own values from tags in the
|
||||
* script, `[lang:ja][speed:1.2][preset:lively]`, over every other setting.
|
||||
* 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'];
|
||||
@@ -26,33 +29,83 @@ export function lineValues(line, character) {
|
||||
return Object.fromEntries(TUNE_FIELDS.map(field => [field, own[field] ?? character?.[field]]));
|
||||
}
|
||||
|
||||
/** Sets `field` on each line, or with `value` undefined lets the lines follow their character again. */
|
||||
export function setLineTune(lines, field, value) {
|
||||
/** `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) };
|
||||
if (value === undefined) delete own[field];
|
||||
else own[field] = value;
|
||||
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`: the language unless it is auto
|
||||
* (the node never reads a character's saved language itself), the speed when it is not the one
|
||||
* the node takes from the character's own settings (`stored.speed`), the mode unless it is Auto.
|
||||
* 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, stored) {
|
||||
export function tuneTags(values, node = {}) {
|
||||
const nodeLanguage = node.language ?? 'auto';
|
||||
const nodePreset = node.preset ?? AUTO_MODE;
|
||||
let tags = '';
|
||||
if (values.language && values.language !== 'auto') tags += `[lang:${values.language}]`;
|
||||
if (typeof values.speed === 'number' && values.speed !== stored?.speed) tags += `[speed:${Math.round(values.speed * 100) / 100}]`;
|
||||
if (values.preset) tags += `[preset:${values.preset}]`;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -73,7 +126,7 @@ export function sharedValue(lines, field, valuesOf) {
|
||||
|
||||
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' };
|
||||
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) {
|
||||
@@ -86,7 +139,7 @@ export function readTuneTag(content) {
|
||||
return Number.isFinite(value) ? { field, value } : { field, value: undefined };
|
||||
}
|
||||
if (field === 'language') return { field, value: LANGUAGE[raw.toLowerCase()] };
|
||||
return { field, value: raw };
|
||||
return { field, value: raw.toLowerCase() === 'auto' ? AUTO_MODE : raw };
|
||||
}
|
||||
|
||||
/** "1, 3, 5" from line indexes (0-based in, 1-based out). */
|
||||
|
||||
@@ -17,7 +17,8 @@ import { bindPackageDrag, copyScript, pushToNode } from './ui_script_workflow.js
|
||||
* each line following its character until set for itself) are changed. Choose an emotion per
|
||||
* line, then generate it right here (ui_script_run.js), or put the tagged script into the
|
||||
* workflow (ui_script_workflow.js). The node's own character is the first line's. The panel and
|
||||
* its lines live for the page session; the page mounts it again each time it is shown.
|
||||
* its lines live for the page session; the page mounts it again each time it is shown. The script
|
||||
* is kept as a draft in this browser (localStorage) and comes back after a reload.
|
||||
*/
|
||||
|
||||
// From this width on, the cards get the full height and the generate side sits beside them.
|
||||
@@ -281,6 +282,7 @@ function createPanel() {
|
||||
});
|
||||
runSection = createRunSection({
|
||||
getScript: runScript,
|
||||
getNodeGroup: nodeGroup,
|
||||
getGroups: () => state.groups,
|
||||
onPlay: () => {
|
||||
stopLinePreview();
|
||||
@@ -288,11 +290,15 @@ function createPanel() {
|
||||
},
|
||||
resetTakes: () => state.lines.forEach(line => { line.take = 1; }),
|
||||
onBusyChange: () => renderAll(),
|
||||
onLinesChange: () => renderLines(),
|
||||
onLinesChange: () => {
|
||||
renderLines();
|
||||
saveDraft();
|
||||
},
|
||||
openGallery: () => hooks.openGallery?.(),
|
||||
});
|
||||
scroll.append(linesContainer);
|
||||
footer.append(summary, runSection.bar);
|
||||
if (!state.lines.length) loadDraft();
|
||||
|
||||
// Cards on one side, everything about generating on the other: below the cards
|
||||
// on a narrow page (docked browser), beside them once the page is wide.
|
||||
@@ -464,6 +470,7 @@ function renderPackage() {
|
||||
} else {
|
||||
refs.summary.textContent = t(pkg.error);
|
||||
}
|
||||
saveDraft();
|
||||
refs.copyBtn.disabled = !ok;
|
||||
refs.pushBtn.disabled = !ok;
|
||||
refs.dragHandle.draggable = ok;
|
||||
@@ -471,18 +478,67 @@ function renderPackage() {
|
||||
runSection.render();
|
||||
}
|
||||
|
||||
// ---------- the draft ----------
|
||||
|
||||
const DRAFT_KEY = 'anomalous_sd_draft';
|
||||
const DRAFT_FIELDS = { language: 'string', speed: 'number', preset: 'string' };
|
||||
let draftTimer = null;
|
||||
|
||||
/** Only the line settings a draft may hold, with the right types. */
|
||||
function cleanTune(tune) {
|
||||
const out = {};
|
||||
for (const [field, type] of Object.entries(DRAFT_FIELDS)) if (typeof tune?.[field] === type) out[field] = tune[field];
|
||||
return Object.keys(out).length ? out : null;
|
||||
}
|
||||
|
||||
/** The script as it is, kept in this browser half a second after the last change (back after a reload). */
|
||||
function saveDraft() {
|
||||
clearTimeout(draftTimer);
|
||||
draftTimer = setTimeout(() => {
|
||||
const lines = state.lines.map(line => ({ text: line.text, emotion: line.emotion, speaker: line.speaker, ...(line.tune ? { tune: line.tune } : {}) }));
|
||||
try {
|
||||
if (lines.length) localStorage.setItem(DRAFT_KEY, JSON.stringify({ format: 1, lines, groupKey: state.groupKey, ...runSection?.draft() }));
|
||||
else localStorage.removeItem(DRAFT_KEY);
|
||||
} catch (_) { /* a convenience: the script still works without it */ }
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/** The draft kept last time, once the panel is first made; lines without text or with a bad shape are left out. */
|
||||
function loadDraft() {
|
||||
let draft = null;
|
||||
try { draft = JSON.parse(localStorage.getItem(DRAFT_KEY) || 'null'); } catch (_) { return; }
|
||||
if (draft?.format !== 1 || !Array.isArray(draft.lines)) return;
|
||||
state.lines = draft.lines.filter(line => typeof line?.text === 'string' && line.text.trim()).map(line => ({
|
||||
text: line.text,
|
||||
emotion: typeof line.emotion === 'string' ? line.emotion : 'main',
|
||||
take: 1,
|
||||
speaker: typeof line.speaker === 'string' ? line.speaker : null,
|
||||
...(cleanTune(line.tune) ? { tune: cleanTune(line.tune) } : {}),
|
||||
}));
|
||||
if (typeof draft.groupKey === 'string') state.groupKey = draft.groupKey;
|
||||
const cast = {};
|
||||
for (const [character, own] of Object.entries(draft.cast || {})) if (cleanTune(own)) cast[character] = cleanTune(own);
|
||||
runSection.restoreDraft({ script: cleanTune(draft.script) || {}, cast });
|
||||
state.editing = !state.lines.length;
|
||||
}
|
||||
|
||||
// ---------- actions ----------
|
||||
|
||||
function parseScript(text) {
|
||||
const parsed = splitScriptLines(text, state.splitMode);
|
||||
if (!parsed.length) return;
|
||||
// A line kept word for word keeps who reads it and how.
|
||||
const previous = new Map(state.lines.map(line => [line.text.trim(), line]));
|
||||
state.lines = parsed.map(lineText => ({
|
||||
text: lineText,
|
||||
emotion: previous.get(lineText)?.emotion || 'main',
|
||||
take: previous.get(lineText)?.take || 1,
|
||||
speaker: previous.get(lineText)?.speaker || state.groupKey,
|
||||
}));
|
||||
state.lines = parsed.map((lineText) => {
|
||||
const before = previous.get(lineText);
|
||||
return {
|
||||
text: lineText,
|
||||
emotion: before?.emotion || 'main',
|
||||
take: before?.take || 1,
|
||||
speaker: before?.speaker || state.groupKey,
|
||||
...(before?.tune ? { tune: { ...before.tune } } : {}),
|
||||
};
|
||||
});
|
||||
state.picked = new Set();
|
||||
state.editing = false;
|
||||
renderAll();
|
||||
|
||||
+106
-33
@@ -6,7 +6,9 @@ import { fetchGptSovitsCharacter, loadGptSovitsStatus, saveGptSovitsSettings } f
|
||||
import {
|
||||
PRESETS_FORMAT, SAMPLE_LINES, lineTuning, loadPresets, presetLabel, presetsFile, savePresets, stateFromView, tuningRows,
|
||||
} from './tts_presets_api.js';
|
||||
import { AUTO_MODE, LINE_TAGS_FORMAT, clearLineTune, lineValues, setLineTune, tuneTags } from './script_tune.js';
|
||||
import {
|
||||
AUTO_MODE, LINE_TAGS_FORMAT, TUNE_FIELDS, characterLayer, clearLineTune, lineLayer, lineValues, scriptLayer, setLineTune, setOwn, tuneTags,
|
||||
} from './script_tune.js';
|
||||
import { keepParamFocus } from './ui_tts_preset_switch.js';
|
||||
import { createModePreview } from './ui_tts_mode_preview.js';
|
||||
import { openTtsSetup } from './ui_tts_setup.js';
|
||||
@@ -19,13 +21,14 @@ import { LANGUAGES, renderTunePanel } from './ui_script_tune.js';
|
||||
* plays the result, and offers another take of the whole script (new seed) or of
|
||||
* one line (`[take:N]`, the other lines come from the node's cache).
|
||||
*
|
||||
* How each line is read: every character has its language, speed and mode in this script,
|
||||
* starting from its saved `defaults` (language and speed, which can be saved back) and Auto (the
|
||||
* presets and the character's generation settings decide line by line); a line follows its
|
||||
* character unless it was set for itself (script_tune.js). The side panel showing them follows
|
||||
* what is picked on the left (ui_script_tune.js). Anomalous_TTS interface 17 gets them as tags
|
||||
* on each line; an older node reads the whole script with the first line's character's values
|
||||
* through its inputs. The modes can be heard on a line (ui_tts_mode_preview.js); a preset's
|
||||
* How each line is read: its language, speed and tone in three layers changed for this script
|
||||
* (script_tune.js): the line, its character (kept here), the whole script (kept here); then the
|
||||
* character's usual `defaults` (language and speed, which can be saved back) and the usual tone
|
||||
* (the presets and the character's own settings decide line by line). The side panel showing
|
||||
* them follows what is picked on the left (ui_script_tune.js). Anomalous_TTS interface 17 gets
|
||||
* the whole script's values through the node's inputs and the rest as tags on the lines that
|
||||
* need them; an older node reads the whole script with the first line's character's values
|
||||
* through its inputs. The tones can be heard on a line (ui_tts_mode_preview.js); a preset's
|
||||
* numbers changed here are saved into that preset.
|
||||
*
|
||||
* The director owns the lines; this section owns the job, the characters' values and the
|
||||
@@ -78,20 +81,22 @@ async function loadNodeSpec() {
|
||||
* `getScript()` -> `{ group, lines, listen, speakerOf, pkg, groups, picked, focus, assign, focusCharacter,
|
||||
* focusScript }` (the node's character = the first line's, the director's lines, the one to listen
|
||||
* with, who reads a line, pkg from buildScriptPackage with takes, the characters, the picked line
|
||||
* indexes, the character shown, and the director's calls); `getGroups()` -> every voice group (for
|
||||
* indexes, the character shown, and the director's calls); `getNodeGroup()` -> the node's character
|
||||
* alone (the tags need it, and getScript builds the tagged script); `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); `onLinesChange()` after a line's values changed (the
|
||||
* cards' tags); `openGallery()` for the result's "saved to the audio gallery".
|
||||
*/
|
||||
export function createRunSection({ getScript, getGroups, onPlay, resetTakes, onBusyChange, onLinesChange, openGallery }) {
|
||||
export function createRunSection({ getScript, getNodeGroup, getGroups, onPlay, resetTakes, onBusyChange, onLinesChange, openGallery }) {
|
||||
const state = {
|
||||
seed: newSeed(),
|
||||
spec: null, // node input ranges/defaults; null until loaded
|
||||
presets: null, // loadPresets(); null without interface 16
|
||||
format: 0, // the node's interface
|
||||
local: false, // this computer (presets can be changed)
|
||||
options: new Map(), // character -> { language, speed, preset } in this script
|
||||
script: {}, // the whole script's values changed here (only the fields set)
|
||||
cast: new Map(), // character -> its values changed here for this script (only the fields set)
|
||||
settings: new Map(), // character -> its settings as saved in this session
|
||||
saved: new Map(), // character -> options saved in this session
|
||||
savedNow: null, // the character just saved ("Saved" on its button for a moment)
|
||||
@@ -144,21 +149,59 @@ export function createRunSection({ getScript, getGroups, onPlay, resetTakes, onB
|
||||
};
|
||||
}
|
||||
|
||||
function currentOptions(group) {
|
||||
if (!group) return { language: 'auto', speed: state.spec?.speed?.default ?? 1, preset: AUTO_MODE };
|
||||
if (!state.options.has(group.character)) state.options.set(group.character, { ...storedOptions(group) });
|
||||
const opts = state.options.get(group.character);
|
||||
if (opts.preset !== AUTO_MODE && !state.presets?.presets?.[opts.preset]) opts.preset = AUTO_MODE; // deleted meanwhile
|
||||
return opts;
|
||||
const base = () => ({ language: 'auto', speed: state.spec?.speed?.default ?? 1, preset: AUTO_MODE });
|
||||
|
||||
function castOf(group) {
|
||||
if (!state.cast.has(group.character)) state.cast.set(group.character, {});
|
||||
return state.cast.get(group.character);
|
||||
}
|
||||
|
||||
/** A character's values in this script with where each comes from; a preset deleted meanwhile reads as the usual tone. */
|
||||
function layerOf(group) {
|
||||
const layer = characterLayer(castOf(group), state.script, storedOptions(group), base());
|
||||
if (layer.values.preset !== AUTO_MODE && !state.presets?.presets?.[layer.values.preset]) layer.values.preset = AUTO_MODE;
|
||||
return layer;
|
||||
}
|
||||
|
||||
const currentOptions = group => (group ? layerOf(group).values : scriptLayer(state.script, base()).values);
|
||||
const sameOptions = (a, b) => a.language === b.language && a.speed === b.speed;
|
||||
const valuesOfLine = line => lineValues(line, currentOptions(getScript().speakerOf(line)));
|
||||
|
||||
/**
|
||||
* The node's whole-script speed: its input counts only when it differs from the node's default
|
||||
* and from the first character's own speed (Anomalous_TTS reads it that way), else undefined.
|
||||
*/
|
||||
function nodeSpeed() {
|
||||
const speed = state.script.speed;
|
||||
const first = getNodeGroup();
|
||||
return speed === undefined || speed === base().speed || (first && speed === storedOptions(first).speed) ? undefined : speed;
|
||||
}
|
||||
|
||||
/** What the node gives a line of `speaker` by itself: the whole script's values as inputs, else the character's own. */
|
||||
const nodeValues = speaker => ({
|
||||
language: state.script.language ?? 'auto',
|
||||
speed: nodeSpeed() ?? storedOptions(speaker).speed,
|
||||
preset: state.script.preset ?? AUTO_MODE,
|
||||
});
|
||||
|
||||
/** The tags `line` needs (script_tune.js), for the director's packages; none for an older node. */
|
||||
function tagsOf(line, speaker) {
|
||||
if (!canTag() || !speaker) return '';
|
||||
return tuneTags(lineValues(line, currentOptions(speaker)), storedOptions(speaker));
|
||||
return tuneTags(lineValues(line, currentOptions(speaker)), nodeValues(speaker));
|
||||
}
|
||||
|
||||
/** The node's inputs for the script: the whole script's values (interface 17), else the first line's character's. */
|
||||
function scriptInputs(group) {
|
||||
if (!canTag()) {
|
||||
const { language, speed, preset } = currentOptions(group);
|
||||
return { language, speed, ...(state.presets && preset !== AUTO_MODE ? { preset } : {}) };
|
||||
}
|
||||
const speed = nodeSpeed();
|
||||
return {
|
||||
language: state.script.language ?? 'auto',
|
||||
...(speed !== undefined ? { speed } : {}),
|
||||
...(state.presets && state.script.preset ? { preset: state.script.preset } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** The language Auto's numbers are shown for: the one chosen, else the character's own. */
|
||||
@@ -174,17 +217,24 @@ export function createRunSection({ getScript, getGroups, onPlay, resetTakes, onB
|
||||
return lineTuning(state.presets, tuningRows(characterSettings(group)), refLang, autoKey(group, values)).values;
|
||||
}
|
||||
|
||||
/** One line read by its character (`speaker`) in mode `mode`, for the listen row and a card's ▶. */
|
||||
/**
|
||||
* One line read by its character (`speaker`) in tone `mode`, for the listen row and a card's ▶.
|
||||
* With interface 17 the values go as the line's tags (exactly as they will be read), else as inputs.
|
||||
*/
|
||||
function takeFor(line, mode, speaker = null, values = null) {
|
||||
const who = speaker || getScript().speakerOf(line);
|
||||
const own = values || (line ? valuesOfLine(line) : currentOptions(who));
|
||||
const id = mode ?? own.preset;
|
||||
const words = String(line?.text || '').trim();
|
||||
const refLang = who.raw?.reference?.language || who.language;
|
||||
const text = words
|
||||
? `${line.emotion && line.emotion !== 'main' ? `{${line.emotion}}` : ''}${words}`
|
||||
: SAMPLE_LINES[refLang] || SAMPLE_LINES.zh;
|
||||
return { character: who.character, text, language: own.language, speed: own.speed, mode: id, preset: id, numbers: modeNumbers(who, own, id) };
|
||||
const emotion = line?.emotion && line.emotion !== 'main' ? `{${line.emotion}}` : '';
|
||||
const numbers = modeNumbers(who, own, id);
|
||||
if (canTag()) {
|
||||
const tags = tuneTags({ ...own, preset: id }, { speed: storedOptions(who).speed });
|
||||
return { character: who.character, text: `${emotion}${tags}${words || SAMPLE_LINES[refLang] || SAMPLE_LINES.zh}`, language: 'auto', mode: id, preset: '', numbers };
|
||||
}
|
||||
const text = words ? `${emotion}${words}` : SAMPLE_LINES[refLang] || SAMPLE_LINES.zh;
|
||||
return { character: who.character, text, language: own.language, speed: own.speed, mode: id, preset: id, numbers };
|
||||
}
|
||||
|
||||
async function reloadPresets() {
|
||||
@@ -205,7 +255,6 @@ export function createRunSection({ getScript, getGroups, onPlay, resetTakes, onB
|
||||
} catch (_) {
|
||||
state.supported = false;
|
||||
}
|
||||
state.options.clear(); // options built before the spec arrived lack the node's defaults
|
||||
render();
|
||||
onBusyChange?.(); // the director draws its cards again, now with their ▶
|
||||
}
|
||||
@@ -227,13 +276,7 @@ export function createRunSection({ getScript, getGroups, onPlay, resetTakes, onB
|
||||
const script = getScript();
|
||||
if (!script?.group || script.pkg.error || state.job || !state.supported) return;
|
||||
const character = script.group.character;
|
||||
let inputs = {};
|
||||
if (!canTag()) {
|
||||
// An older node: the whole script as the first line's character reads it.
|
||||
const { language, speed, preset } = currentOptions(script.group);
|
||||
inputs = { language, speed, ...(state.presets && preset !== AUTO_MODE ? { preset } : {}) };
|
||||
}
|
||||
const prompt = buildTtsPrompt({ character, speech: script.pkg.speech, seed: state.seed, ...inputs });
|
||||
const prompt = buildTtsPrompt({ character, speech: script.pkg.speech, seed: state.seed, ...scriptInputs(script.group) });
|
||||
state.status = { state: 'queued' };
|
||||
const started = Date.now();
|
||||
state.job = startPromptJob(prompt, {
|
||||
@@ -280,6 +323,8 @@ export function createRunSection({ getScript, getGroups, onPlay, resetTakes, onB
|
||||
await saveGptSovitsSettings(character, saved);
|
||||
state.settings.set(character, saved);
|
||||
state.saved.set(character, { ...opts, preset: AUTO_MODE });
|
||||
// What was changed for it here is its usual now, unless it still has to go over the whole script's.
|
||||
for (const field of ['language', 'speed']) if (state.script[field] === undefined) delete castOf(group)[field];
|
||||
state.savedNow = character;
|
||||
setTimeout(() => { state.savedNow = null; render(); }, 1600);
|
||||
} catch (error) {
|
||||
@@ -315,16 +360,38 @@ export function createRunSection({ getScript, getGroups, onPlay, resetTakes, onB
|
||||
redraw: render,
|
||||
options: currentOptions,
|
||||
lineValues: valuesOfLine,
|
||||
layers: {
|
||||
script: () => scriptLayer(state.script, base()),
|
||||
character: layerOf,
|
||||
line: line => lineLayer(line, layerOf(script.speakerOf(line))),
|
||||
},
|
||||
changedFor: {
|
||||
script: () => TUNE_FIELDS.some(field => state.script[field] !== undefined),
|
||||
character: group => TUNE_FIELDS.some(field => castOf(group)[field] !== undefined),
|
||||
},
|
||||
setScript: (field, value) => {
|
||||
setOwn(state.script, field, value, base()[field]);
|
||||
state.presetNote = '';
|
||||
changed();
|
||||
},
|
||||
setCharacter: (group, field, value) => {
|
||||
currentOptions(group)[field] = value;
|
||||
setOwn(castOf(group), field, value, state.script[field] ?? storedOptions(group)[field]);
|
||||
state.presetNote = '';
|
||||
changed();
|
||||
},
|
||||
setLines: (indexes, field, value) => {
|
||||
setLineTune(indexes.map(index => script.lines[index]), field, value);
|
||||
setLineTune(indexes.map(index => script.lines[index]), field, value, line => currentOptions(script.speakerOf(line))[field]);
|
||||
state.presetNote = '';
|
||||
changed();
|
||||
},
|
||||
clearScript: () => {
|
||||
state.script = {};
|
||||
changed();
|
||||
},
|
||||
clearCharacter: (group) => {
|
||||
state.cast.delete(group.character);
|
||||
changed();
|
||||
},
|
||||
clearLines: (indexes) => {
|
||||
clearLineTune(indexes.map(index => script.lines[index]));
|
||||
changed();
|
||||
@@ -415,6 +482,12 @@ export function createRunSection({ getScript, getGroups, onPlay, resetTakes, onB
|
||||
},
|
||||
/** The presets, for the tags on the cards (ui_script_tune.js ownTuneText). */
|
||||
presetsView: () => state.presets,
|
||||
/** The whole script's and the characters' values changed here, for the director's draft. */
|
||||
draft: () => ({ script: { ...state.script }, cast: Object.fromEntries([...state.cast].filter(([, own]) => Object.keys(own).length)) }),
|
||||
restoreDraft({ script = {}, cast = {} } = {}) {
|
||||
state.script = { ...script };
|
||||
state.cast = new Map(Object.entries(cast).map(([character, own]) => [character, { ...own }]));
|
||||
},
|
||||
stopPlayback() {
|
||||
player.pause();
|
||||
preview.stop();
|
||||
|
||||
+226
-234
@@ -5,40 +5,36 @@ import { AUTO_MODE, hasOwnTune, lineNumbers, ownTune, sharedValue } from './scri
|
||||
import { speakerColor } from './ui_script_cast.js';
|
||||
import { openSpeakerMenu } from './ui_script_cards.js';
|
||||
import { renderPresetSwitch } from './ui_tts_preset_switch.js';
|
||||
import { remember, remembered, tabStrip } from './ui_tab_strip.js';
|
||||
import { remember, remembered } from './ui_tab_strip.js';
|
||||
|
||||
/**
|
||||
* The Script Director's side panel, which follows what is picked on the left: nothing, the whole
|
||||
* script (each character and how it reads); a character in the cast, how it reads in this script
|
||||
* (every line of it follows); picked lines, who reads them and how, each value following the
|
||||
* character until it is set here (then the card shows a tag and ↺ lets it follow again).
|
||||
* A character or picked lines have three tabs: How it reads (who reads, speed, the language as one
|
||||
* line while it is detected, the tone with its listen button), Numbers and Settings.
|
||||
* The Script Director's side panel, in three layers for this script, each read the same way:
|
||||
* the whole script (what everyone reads with), a character in the cast (only its lines), the
|
||||
* picked lines (only them). On top a path, Whole script › character › line, says which layer is
|
||||
* being changed and leads back up. Each layer has the same three rows, speed, tone and language,
|
||||
* and each row says where its value comes from (this line, the character, the whole script, the
|
||||
* character's usual settings, the default; a click there goes to that layer) with ↺ when this
|
||||
* layer set it. Setting a value to what it would get anyway lets it follow again. Settings that
|
||||
* last beyond this script (the tones, a character's own settings) are links at the foot.
|
||||
* ui_script_run.js keeps the state and draws this again after each change (`ctx`, see
|
||||
* renderTunePanel); the lines are the director's.
|
||||
*/
|
||||
|
||||
export const LANGUAGES = ['auto', 'zh', 'ja', 'en'];
|
||||
const FOLLOW = '__follow'; // a line's mode choice: its character's
|
||||
const ALL_WHO_UP_TO = 6; // more characters than this: "Who reads" lists the script's own, the rest in a menu
|
||||
const TABS = ['how', 'numbers', 'settings'];
|
||||
const TAB_KEY = 'anomalous_sd_tune_tab';
|
||||
const ALL_WHO_UP_TO = 6; // more characters than this: "Who reads" lists the script's own, the rest in a menu
|
||||
const NUMBERS_KEY = 'anomalous_sd_tone_numbers';
|
||||
|
||||
/** "1.2×" */
|
||||
export const speedText = speed => `${Math.round(Number(speed) * 100) / 100}×`;
|
||||
|
||||
/** A mode's name: Auto, a built-in preset or one made in Anomalous. */
|
||||
/** A tone's name: the usual one, a built-in preset or one made in Anomalous. */
|
||||
export function modeName(view, id) {
|
||||
return id === AUTO_MODE || !view ? t('scriptRunModeAuto') : presetLabel(view.labels, id, t);
|
||||
}
|
||||
|
||||
/** "中文 · 1.2× · 有起伏": a set of values in a few words (`skip`: leave out the auto language). */
|
||||
export function valuesText(view, values, { skip = false } = {}) {
|
||||
const parts = [];
|
||||
if (!(skip && values.language === 'auto')) parts.push(t(`scriptRunLang_${values.language || 'auto'}`));
|
||||
if (typeof values.speed === 'number') parts.push(speedText(values.speed));
|
||||
parts.push(modeName(view, values.preset ?? AUTO_MODE));
|
||||
return parts.join(' · ');
|
||||
/** "中文 · 1.2× · 生动": a set of values in a few words. */
|
||||
export function valuesText(view, values) {
|
||||
return [t(`scriptRunLang_${values.language || 'auto'}`), speedText(values.speed), modeName(view, values.preset ?? AUTO_MODE)].join(' · ');
|
||||
}
|
||||
|
||||
/** What a line set for itself, for the tag on its card; '' when it follows its character. */
|
||||
@@ -52,21 +48,6 @@ export function ownTuneText(view, line) {
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
function heading(title, meta, back, hint = '') {
|
||||
const head = el('div', 'anomalous-sd-tune-head');
|
||||
if (hint) head.title = hint;
|
||||
if (back) head.append(button('anomalous-sd-link-btn anomalous-sd-tune-back', t('scriptTuneBack'), back));
|
||||
head.append(el('div', 'anomalous-sd-tune-title', title));
|
||||
if (meta) head.append(el('div', 'anomalous-sd-tune-meta', meta));
|
||||
return head;
|
||||
}
|
||||
|
||||
function row(label, ...content) {
|
||||
const line = el('div', 'anomalous-sd-run-row');
|
||||
line.append(el('span', 'anomalous-sd-run-label', label), ...content);
|
||||
return line;
|
||||
}
|
||||
|
||||
/** Choices in a segment; `checked(id)`; a click on the checked one does nothing. */
|
||||
function segment(choices, checked, onPick, disabled) {
|
||||
const box = el('div', 'anomalous-sd-run-segment');
|
||||
@@ -90,92 +71,167 @@ function segment(choices, checked, onPick, disabled) {
|
||||
return box;
|
||||
}
|
||||
|
||||
/** The speed slider: `value`, `onChange(number)`; `after`: elements after the number. */
|
||||
function speedSlider(ctx, value, onChange, after = []) {
|
||||
/** The path on top: Whole script › character › lines; `steps`: `[{ label, go?, current? }]`. */
|
||||
function path(steps) {
|
||||
const box = el('nav', 'anomalous-sd-tune-path');
|
||||
steps.forEach((step, at) => {
|
||||
if (at) box.append(el('span', 'anomalous-sd-tune-path-sep', '›'));
|
||||
// The first step is the way back up: the browser's Esc presses it (ui_browser_navigation.js).
|
||||
const item = step.go && !step.current
|
||||
? button(`anomalous-sd-tune-path-step${at === 0 ? ' anomalous-sd-tune-back' : ''}`, step.label, step.go)
|
||||
: el('span', `anomalous-sd-tune-path-step${step.current ? ' is-current' : ''}`, step.label);
|
||||
if (step.color) item.style.setProperty('--speaker', step.color);
|
||||
box.append(item);
|
||||
});
|
||||
return box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a value comes from, as a chip in the layer's colour: `from` ('line', 'character',
|
||||
* 'script', 'usual', 'default'), `who` the character's name; a click goes to that layer.
|
||||
*/
|
||||
function sourceChip(ctx, from, who, go, mixed = false) {
|
||||
if (mixed) return el('span', 'anomalous-sd-tune-from is-mixed', t('scriptTuneFromMixed'));
|
||||
const label = {
|
||||
line: t('scriptTuneFromLine'),
|
||||
character: who,
|
||||
script: t('scriptTuneScript'),
|
||||
usual: t('scriptTuneFromUsual', { name: who }),
|
||||
default: t('scriptTuneFromDefault'),
|
||||
}[from];
|
||||
const target = go?.[from];
|
||||
const chip = target ? button(`anomalous-sd-tune-from is-${from}`, label, target) : el('span', `anomalous-sd-tune-from is-${from}`, label);
|
||||
chip.title = t(`scriptTuneFromHint_${from}`, { name: who });
|
||||
return chip;
|
||||
}
|
||||
|
||||
/** One row: its name, the control, where the value comes from, and ↺ when this layer set it (`reset`). */
|
||||
function row(label, control, chip, reset) {
|
||||
const line = el('div', 'anomalous-sd-tune-row');
|
||||
const end = el('span', 'anomalous-sd-tune-row-end');
|
||||
end.append(chip);
|
||||
if (reset) end.append(button('anomalous-sd-icon-btn anomalous-sd-tune-reset', '↺', reset, t('scriptTuneReset')));
|
||||
line.append(el('span', 'anomalous-sd-run-label', label), control, end);
|
||||
return line;
|
||||
}
|
||||
|
||||
/** The speed slider; `value` null when the lines differ. */
|
||||
function speedSlider(ctx, value, onChange) {
|
||||
const range = ctx.spec.speed;
|
||||
const box = el('span', 'anomalous-sd-tune-speed');
|
||||
const slider = el('input', 'anomalous-sd-run-slider');
|
||||
Object.assign(slider, { type: 'range', min: String(range.min), max: String(range.max), step: String(range.step ?? 1) });
|
||||
slider.value = String(value ?? range.default);
|
||||
slider.title = t('scriptRunParamHint_speed');
|
||||
slider.disabled = ctx.busy;
|
||||
const shown = el('span', 'anomalous-sd-run-value', String(value ?? range.default));
|
||||
slider.oninput = () => { shown.textContent = slider.value; };
|
||||
const shown = el('span', 'anomalous-sd-run-value', value === null ? '–' : speedText(value));
|
||||
slider.oninput = () => { shown.textContent = speedText(slider.value); };
|
||||
slider.onchange = () => {
|
||||
const number = Math.min(range.max, Math.max(range.min, Number(slider.value)));
|
||||
onChange(Number.isFinite(number) ? number : range.default);
|
||||
};
|
||||
return row(t('scriptRunSpeed'), slider, shown, ...after);
|
||||
}
|
||||
|
||||
/**
|
||||
* The language: one line ("Language: detected · Change") while it is the detected one and nothing
|
||||
* set it, the choices once "Change" is pressed or another one is in use. `choices` and `checked`
|
||||
* as segment; `shown`: the language in use, in words.
|
||||
*/
|
||||
function languageRow(ctx, { choices, checked, onPick, open, shown }) {
|
||||
const full = () => row(t('scriptRunLanguage'), segment(choices, checked, onPick, ctx.busy));
|
||||
if (open) return full();
|
||||
const line = el('div', 'anomalous-sd-tune-language');
|
||||
const change = button('anomalous-sd-link-btn', t('scriptTuneLanguageChange'), () => line.replaceWith(full()));
|
||||
change.disabled = ctx.busy;
|
||||
line.append(el('span', '', t('scriptTuneLanguageIs', { value: shown })), change);
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* The tone: its choices and ▶ for the line heard (null: a sample) read by `speaker` with `values`
|
||||
* (null: the line's own); with `detail` (the Numbers tab) also how it compares and its numbers.
|
||||
* `choices` and `base` as renderPresetSwitch.
|
||||
*/
|
||||
function modeBox(ctx, { speaker, line, values = null, selected, choices, base, baseName, baseText, describe, numbers, onPick, detail = false }) {
|
||||
const box = el('div', 'anomalous-sd-run-mode');
|
||||
if (!ctx.view) return box;
|
||||
const view = ctx.view;
|
||||
const take = id => ctx.takeFor(line, id === FOLLOW ? ctx.options(speaker).preset : id, speaker, values);
|
||||
const switcher = renderPresetSwitch({
|
||||
choices,
|
||||
selected,
|
||||
base,
|
||||
baseName,
|
||||
baseText,
|
||||
values: numbers,
|
||||
describe,
|
||||
builtin: id => (isBuiltin(id) ? view.builtin[id] : null),
|
||||
editable: id => id !== AUTO_MODE && id !== FOLLOW && ctx.local,
|
||||
onPick,
|
||||
onEdit: ctx.editPreset,
|
||||
listen: ctx.preview.row({ takeFor: take, selected, modes: choices, onPick, disabled: ctx.busy, label: 'scriptTuneListen' }),
|
||||
detail,
|
||||
disabled: ctx.busy,
|
||||
});
|
||||
box.append(el('span', 'anomalous-sd-run-label', t('scriptRunMode')), switcher);
|
||||
if (detail && ctx.presetNote) box.append(el('div', 'anomalous-sd-run-note', ctx.presetNote));
|
||||
box.append(slider, shown);
|
||||
return box;
|
||||
}
|
||||
|
||||
const presetChoices = view => [{ id: AUTO_MODE, label: t('scriptRunModeAuto') },
|
||||
const toneChoices = view => [{ id: AUTO_MODE, label: t('scriptRunModeAuto'), title: t('scriptTuneUsualHint') },
|
||||
...Object.keys(view?.presets || {}).map(name => ({ id: name, label: presetLabel(view.labels, name, t), badge: isBuiltin(name) ? '' : t('ttsPresetCustom') }))];
|
||||
|
||||
const presetAbout = id => (isBuiltin(id) ? t(`ttsPresetDesc_${id}`) : t('ttsPresetDescCustom'));
|
||||
|
||||
/** The Settings tab's ways out: the tones for every character, and this one's own settings. */
|
||||
function links(ctx, speaker) {
|
||||
const box = el('div', 'anomalous-sd-tune-links');
|
||||
const item = (label, about, onClick) => {
|
||||
const entry = button('anomalous-sd-tune-link', '', onClick);
|
||||
entry.append(el('span', 'anomalous-sd-tune-link-name', label), el('span', 'anomalous-sd-tune-link-about', about));
|
||||
box.append(entry);
|
||||
};
|
||||
item(t('scriptRunOpenPresets'), t('scriptTuneLinkPresets'), ctx.openPresets);
|
||||
if (speaker) item(t('scriptRunOpenTuning', { name: speaker.character }), t('scriptTuneLinkTuning'), () => ctx.openTuning(speaker));
|
||||
/**
|
||||
* The three rows for one layer. `value(field)` → `{ value, from, mixed }`, `set(field, value)`
|
||||
* (undefined: follow the layer below), `own(field)`: this layer set it; `who`: the character the
|
||||
* sources name; `go`: where a source chip leads; `speaker` / `line`: who is heard on ▶ and which
|
||||
* line (null: a sample); `values`: the values heard with (null: the line's own).
|
||||
*/
|
||||
function layerRows(ctx, { value, set, own, who, go, speaker, line, values = null }) {
|
||||
const reset = field => (own(field) ? () => set(field, undefined) : null);
|
||||
const chip = field => sourceChip(ctx, value(field).from, who, go, value(field).mixed);
|
||||
const rows = [];
|
||||
|
||||
const speed = value('speed');
|
||||
rows.push(row(t('scriptRunSpeed'), speedSlider(ctx, speed.mixed ? null : speed.value, number => set('speed', number)), chip('speed'), reset('speed')));
|
||||
|
||||
if (ctx.view) {
|
||||
const tone = value('preset');
|
||||
const detail = remembered(NUMBERS_KEY, ['simple', 'numbers'], 'simple') === 'numbers';
|
||||
const lang = ctx.autoLanguage(speaker, values || ctx.lineValues(line) || {});
|
||||
const take = id => ctx.takeFor(line, id, speaker, values);
|
||||
const switcher = renderPresetSwitch({
|
||||
choices: toneChoices(ctx.view),
|
||||
selected: tone.mixed ? null : tone.value,
|
||||
base: AUTO_MODE,
|
||||
baseName: t('scriptRunModeAutoBase'),
|
||||
baseText: t('scriptRunModeIsAuto'),
|
||||
values: id => ctx.modeNumbers(speaker, values || ctx.lineValues(line), id ?? AUTO_MODE),
|
||||
describe: id => (id === AUTO_MODE ? t('scriptRunModeAutoShort', { lang }) : presetAbout(id)),
|
||||
builtin: id => (isBuiltin(id) ? ctx.view.builtin[id] : null),
|
||||
editable: id => id !== AUTO_MODE && ctx.local,
|
||||
onPick: id => set('preset', id),
|
||||
onEdit: ctx.editPreset,
|
||||
detail,
|
||||
disabled: ctx.busy,
|
||||
});
|
||||
rows.push(row(t('scriptRunMode'), switcher, chip('preset'), reset('preset')));
|
||||
const under = el('div', 'anomalous-sd-tune-under');
|
||||
const numbers = button(`anomalous-sd-run-choice anomalous-sd-tune-numbers${detail ? ' is-on' : ''}`, t('scriptTuneNumbers'), () => {
|
||||
remember(NUMBERS_KEY, detail ? 'simple' : 'numbers');
|
||||
ctx.redraw();
|
||||
});
|
||||
numbers.setAttribute('aria-pressed', String(detail));
|
||||
under.append(ctx.preview.row({
|
||||
takeFor: take, selected: tone.mixed ? AUTO_MODE : tone.value, modes: toneChoices(ctx.view),
|
||||
onPick: id => set('preset', id), disabled: ctx.busy, label: 'scriptTuneListen',
|
||||
}), numbers);
|
||||
rows.push(under);
|
||||
if (detail && ctx.presetNote) rows.push(el('div', 'anomalous-sd-run-note', ctx.presetNote));
|
||||
}
|
||||
|
||||
const language = value('language');
|
||||
rows.push(row(t('scriptRunLanguage'),
|
||||
segment(LANGUAGES.map(code => ({ id: code, label: t(`scriptRunLang_${code}`) })), code => !language.mixed && language.value === code, code => set('language', code), ctx.busy),
|
||||
chip('language'), reset('language')));
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** The foot: the settings that last beyond this script. */
|
||||
function lasting(ctx, speaker) {
|
||||
const box = el('div', 'anomalous-sd-tune-lasting');
|
||||
box.append(el('span', '', t('scriptTuneLasting')),
|
||||
button('anomalous-sd-link-btn', t('scriptRunOpenPresets'), ctx.openPresets));
|
||||
if (speaker) box.append(button('anomalous-sd-link-btn', t('scriptRunOpenTuning', { name: speaker.character }), () => ctx.openTuning(speaker)));
|
||||
return box;
|
||||
}
|
||||
|
||||
/** Each character reading in this script, a click opens it. */
|
||||
function resetLink(label, onClick) {
|
||||
const box = el('div', 'anomalous-sd-run-links');
|
||||
box.append(button('anomalous-sd-link-btn', label, onClick));
|
||||
return box;
|
||||
}
|
||||
|
||||
/** The whole script: what everyone reads with, then each character in it (a click opens it). */
|
||||
function renderScriptView(ctx) {
|
||||
const { script } = ctx;
|
||||
const layer = ctx.layers.script();
|
||||
const spoken = script.lines.filter(line => line.text.trim());
|
||||
const cast = script.groups.filter(group => spoken.some(line => script.speakerOf(line) === group));
|
||||
const first = cast[0] || script.group;
|
||||
const parts = [
|
||||
path([{ label: t('scriptTuneScript'), current: true }]),
|
||||
el('div', 'anomalous-sd-tune-meta', t('scriptTuneScriptMeta', { count: spoken.length, speakers: cast.length })),
|
||||
...layerRows(ctx, {
|
||||
value: field => ({ value: layer.values[field], from: layer.from[field] }),
|
||||
set: ctx.setScript,
|
||||
own: field => layer.from[field] === 'script',
|
||||
who: '',
|
||||
go: {},
|
||||
speaker: first,
|
||||
line: script.lines[script.listen] || null,
|
||||
values: layer.values,
|
||||
}),
|
||||
];
|
||||
if (ctx.changedFor.script()) parts.push(resetLink(t('scriptTuneResetScript'), ctx.clearScript));
|
||||
const list = el('div', 'anomalous-sd-tune-cast');
|
||||
for (const group of cast) {
|
||||
const lines = spoken.filter(line => script.speakerOf(line) === group);
|
||||
@@ -188,63 +244,43 @@ function renderScriptView(ctx) {
|
||||
const about = el('span', 'anomalous-sd-tune-person-about', valuesText(ctx.view, ctx.options(group)));
|
||||
const count = el('span', 'anomalous-sd-tune-person-count', t('scriptTuneLineCount', { count: lines.length }));
|
||||
item.append(name, count, about);
|
||||
if (own) item.append(el('span', 'anomalous-sd-tune-person-own', t('scriptTuneOwnCount', { count: own })));
|
||||
const changed = [ctx.changedFor.character(group) ? t('scriptTuneChangedHere') : '', own ? t('scriptTuneOwnCount', { count: own }) : ''].filter(Boolean);
|
||||
if (changed.length) item.append(el('span', 'anomalous-sd-tune-person-own', changed.join(' · ')));
|
||||
list.append(item);
|
||||
}
|
||||
return {
|
||||
head: heading(t('scriptTuneScript'), t('scriptTuneScriptMeta', { count: spoken.length, speakers: cast.length })),
|
||||
body: [list, el('div', 'anomalous-sd-run-note', t('scriptTuneScriptHint')), links(ctx, null)],
|
||||
};
|
||||
parts.push(el('div', 'anomalous-sd-tune-section', t('scriptTuneCast')), list, lasting(ctx, null));
|
||||
return parts;
|
||||
}
|
||||
|
||||
/** One character in this script, for every line of it that follows: speed, language and tone, its numbers, its settings. */
|
||||
function renderCharacterView(ctx, group, tab) {
|
||||
/** One character in this script: its three rows for every line of it that follows. */
|
||||
function renderCharacterView(ctx, group) {
|
||||
const { script } = ctx;
|
||||
const values = ctx.options(group);
|
||||
const layer = ctx.layers.character(group);
|
||||
const indexes = script.lines.map((line, index) => [line, index]).filter(([line]) => line.text.trim() && script.speakerOf(line) === group).map(([, index]) => index);
|
||||
const meta = indexes.length ? t('scriptTuneCharacterMeta', { lines: lineNumbers(indexes), count: indexes.length }) : t('scriptTuneCharacterNone');
|
||||
const head = heading(t('scriptTuneCharacter', { name: group.character }), meta, script.focusScript, t('scriptTuneCharacterHint'));
|
||||
const set = (field, value) => ctx.setCharacter(group, field, value);
|
||||
// The line heard: the one listened to on the left when it is this character's, else its first.
|
||||
const listened = script.lines[script.listen];
|
||||
const line = listened && script.speakerOf(listened) === group ? listened : (script.lines[indexes[0]] || null);
|
||||
const lang = ctx.autoLanguage(group, values);
|
||||
const mode = detail => modeBox(ctx, {
|
||||
speaker: group,
|
||||
line,
|
||||
values,
|
||||
selected: values.preset,
|
||||
choices: presetChoices(ctx.view),
|
||||
base: AUTO_MODE,
|
||||
baseName: t('scriptRunModeAutoBase'),
|
||||
baseText: t('scriptRunModeIsAuto'),
|
||||
describe: id => (id === AUTO_MODE ? t('scriptRunModeAutoShort', { lang }) : t('scriptRunModeWhole', { about: presetAbout(id) })),
|
||||
numbers: id => ctx.modeNumbers(group, values, id),
|
||||
onPick: id => set('preset', id),
|
||||
detail,
|
||||
});
|
||||
if (tab === 'numbers') return { head, body: [mode(true)] };
|
||||
if (tab === 'settings') {
|
||||
const own = indexes.filter(index => hasOwnTune(script.lines[index]));
|
||||
const keep = el('div', 'anomalous-sd-run-links');
|
||||
keep.append(ctx.canSave(group) ? ctx.saveButton(group) : el('span', 'anomalous-sd-tune-follow', t('scriptTuneIsDefault')));
|
||||
if (own.length) keep.append(button('anomalous-sd-link-btn', t('scriptTuneResetLines', { count: own.length }), () => ctx.clearLines(own)));
|
||||
return { head, body: [keep, el('div', 'anomalous-sd-run-note', t('scriptRunSaveDefaultsHint')), links(ctx, group)] };
|
||||
}
|
||||
return {
|
||||
head,
|
||||
body: [
|
||||
speedSlider(ctx, values.speed, value => set('speed', value)),
|
||||
languageRow(ctx, {
|
||||
choices: LANGUAGES.map(code => ({ id: code, label: t(`scriptRunLang_${code}`) })),
|
||||
checked: code => values.language === code,
|
||||
onPick: code => set('language', code),
|
||||
open: values.language !== 'auto',
|
||||
shown: t(`scriptRunLang_${values.language}`),
|
||||
}),
|
||||
mode(false),
|
||||
],
|
||||
};
|
||||
const parts = [
|
||||
path([{ label: t('scriptTuneScript'), go: script.focusScript }, { label: group.character, current: true, color: speakerColor(group.group, script.groups) }]),
|
||||
el('div', 'anomalous-sd-tune-meta', indexes.length ? t('scriptTuneCharacterMeta', { lines: lineNumbers(indexes), count: indexes.length }) : t('scriptTuneCharacterNone')),
|
||||
...layerRows(ctx, {
|
||||
value: field => ({ value: layer.values[field], from: layer.from[field] }),
|
||||
set: (field, value) => ctx.setCharacter(group, field, value),
|
||||
own: field => layer.from[field] === 'character',
|
||||
who: group.character,
|
||||
go: { script: script.focusScript },
|
||||
speaker: group,
|
||||
line,
|
||||
values: layer.values,
|
||||
}),
|
||||
];
|
||||
const own = indexes.filter(index => hasOwnTune(script.lines[index]));
|
||||
const links = el('div', 'anomalous-sd-run-links');
|
||||
if (ctx.changedFor.character(group)) links.append(button('anomalous-sd-link-btn', t('scriptTuneResetCharacter'), () => ctx.clearCharacter(group)));
|
||||
if (own.length) links.append(button('anomalous-sd-link-btn', t('scriptTuneResetLines', { count: own.length }), () => ctx.clearLines(own)));
|
||||
if (ctx.canSave(group)) links.append(ctx.saveButton(group));
|
||||
if (links.childNodes.length) parts.push(links);
|
||||
parts.push(lasting(ctx, group));
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,107 +306,63 @@ function whoReads(ctx, indexes, speakers) {
|
||||
return box;
|
||||
}
|
||||
|
||||
/** Picked lines: who reads them, then each value following its character until set here; the tone's numbers; the settings. */
|
||||
function renderLinesView(ctx, indexes, tab) {
|
||||
/** Picked lines: who reads them, then their three rows, each following the character until set here. */
|
||||
function renderLinesView(ctx, indexes) {
|
||||
const { script } = ctx;
|
||||
const lines = indexes.map(index => script.lines[index]).filter(Boolean);
|
||||
const speakers = [...new Set(lines.map(line => script.speakerOf(line)).filter(Boolean))];
|
||||
const single = speakers.length === 1 ? speakers[0] : null;
|
||||
const title = lines.length === 1 ? t('scriptTuneLine', { n: indexes[0] + 1 }) : t('scriptTuneLines', { count: lines.length });
|
||||
const meta = single ? t('scriptTuneReadBy', { name: single.character })
|
||||
: speakers.map(group => `${group.character} ${lines.filter(line => script.speakerOf(line) === group).length}`).join(' · ');
|
||||
const head = heading(title, meta, script.focusScript);
|
||||
const valuesOf = line => ctx.lineValues(line);
|
||||
const set = (field, value) => ctx.setLines(indexes, field, value);
|
||||
const followText = field => {
|
||||
if (!single) return t('scriptTuneFollowEach');
|
||||
const value = ctx.options(single)[field];
|
||||
return field === 'speed' ? speedText(value) : field === 'language' ? t(`scriptRunLang_${value}`) : modeName(ctx.view, value);
|
||||
const layers = lines.map(line => ctx.layers.line(line));
|
||||
const heard = lines.includes(script.lines[script.listen]) ? script.lines[script.listen] : lines[0];
|
||||
const value = (field) => {
|
||||
const shared = sharedValue(lines, field, line => ctx.lineValues(line));
|
||||
const froms = new Set(layers.map(layer => layer.from[field]));
|
||||
return { value: shared.value, mixed: shared.mixed || froms.size > 1, from: [...froms][0] };
|
||||
};
|
||||
const resetAll = () => {
|
||||
if (!lines.some(hasOwnTune)) return [];
|
||||
const foot = el('div', 'anomalous-sd-run-links');
|
||||
foot.append(button('anomalous-sd-link-btn', t('scriptTuneResetAll'), () => ctx.clearLines(indexes)));
|
||||
return [foot];
|
||||
};
|
||||
|
||||
const mode = (detail) => {
|
||||
if (!ctx.view) return el('div');
|
||||
const shared = sharedValue(lines, 'preset', valuesOf);
|
||||
const selected = !shared.some ? FOLLOW : (shared.own && !shared.mixed ? shared.value : FOLLOW);
|
||||
const speaker = single || speakers[0];
|
||||
const heard = lines.includes(script.lines[script.listen]) ? script.lines[script.listen] : lines[0];
|
||||
const followName = single ? single.character : t('scriptTuneFollowEach');
|
||||
return modeBox(ctx, {
|
||||
speaker: script.speakerOf(heard) || speaker,
|
||||
line: heard,
|
||||
selected,
|
||||
// "Usual" would read like "As the character" here: a line picks a tone or follows.
|
||||
choices: [{ id: FOLLOW, label: t('scriptTuneFollow'), title: t('scriptTuneFollowHint', { value: followText('preset') }) },
|
||||
...presetChoices(ctx.view).filter(choice => choice.id !== AUTO_MODE || selected === AUTO_MODE)],
|
||||
base: FOLLOW,
|
||||
baseName: t('scriptTuneFollowBase', { name: followName }),
|
||||
baseText: t('scriptTuneModeFollow', { name: followName }),
|
||||
describe: id => (id === FOLLOW ? '' : id === AUTO_MODE ? t('scriptRunModeAutoShort', { lang: ctx.autoLanguage(speaker, valuesOf(heard)) })
|
||||
: t('scriptTuneModeLines', { about: presetAbout(id) })),
|
||||
numbers: id => {
|
||||
const owner = script.speakerOf(heard) || speaker;
|
||||
return ctx.modeNumbers(owner, valuesOf(heard), id === FOLLOW ? ctx.options(owner).preset : id);
|
||||
},
|
||||
onPick: id => set('preset', id === FOLLOW ? undefined : id),
|
||||
detail,
|
||||
});
|
||||
};
|
||||
if (tab === 'numbers') return { head, body: [mode(true)] };
|
||||
if (tab === 'settings') return { head, body: [...resetAll(), links(ctx, single)] };
|
||||
|
||||
const body = [];
|
||||
if (script.groups.length > 1) body.push(row(t('scriptTuneWho'), whoReads(ctx, indexes, speakers)));
|
||||
const speed = sharedValue(lines, 'speed', valuesOf);
|
||||
const speedAfter = speed.some
|
||||
? [button('anomalous-sd-icon-btn anomalous-sd-tune-reset', '↺', () => set('speed', undefined), t('scriptTuneReset'))]
|
||||
: [el('span', 'anomalous-sd-tune-follow', t('scriptTuneFollowing'))];
|
||||
body.push(speedSlider(ctx, speed.mixed ? null : speed.value, value => set('speed', value), speedAfter));
|
||||
const language = sharedValue(lines, 'language', valuesOf);
|
||||
const follow = { id: FOLLOW, label: t('scriptTuneFollow'), title: t('scriptTuneFollowHint', { value: followText('language') }) };
|
||||
body.push(languageRow(ctx, {
|
||||
choices: [follow, ...LANGUAGES.map(code => ({ id: code, label: t(`scriptRunLang_${code}`) }))],
|
||||
checked: id => (id === FOLLOW ? !language.some : language.own && !language.mixed && language.value === id),
|
||||
onPick: id => set('language', id === FOLLOW ? undefined : id),
|
||||
open: language.some || language.mixed || language.value !== 'auto',
|
||||
shown: t('scriptTuneFollowValue', { value: followText('language') }),
|
||||
const steps = [{ label: t('scriptTuneScript'), go: script.focusScript }];
|
||||
if (single) steps.push({ label: single.character, go: () => script.focusCharacter(single.group), color: speakerColor(single.group, script.groups) });
|
||||
steps.push({ label: title, current: true });
|
||||
const parts = [path(steps)];
|
||||
if (script.groups.length > 1) {
|
||||
const who = el('div', 'anomalous-sd-tune-row');
|
||||
who.append(el('span', 'anomalous-sd-run-label', t('scriptTuneWho')), whoReads(ctx, indexes, speakers));
|
||||
parts.push(who);
|
||||
}
|
||||
parts.push(...layerRows(ctx, {
|
||||
value,
|
||||
set: (field, next) => ctx.setLines(indexes, field, next),
|
||||
own: field => lines.some(line => ownTune(line)[field] !== undefined),
|
||||
who: single?.character || '',
|
||||
go: single ? { character: () => script.focusCharacter(single.group), usual: () => script.focusCharacter(single.group), script: script.focusScript } : { script: script.focusScript },
|
||||
speaker: script.speakerOf(heard) || single,
|
||||
line: heard,
|
||||
}));
|
||||
body.push(mode(false), ...resetAll());
|
||||
return { head, body };
|
||||
if (lines.some(hasOwnTune)) parts.push(resetLink(t('scriptTuneResetAll'), () => ctx.clearLines(indexes)));
|
||||
parts.push(lasting(ctx, single));
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the panel into `box`: the heading, then for a character or picked lines three tabs, How
|
||||
* it reads, Numbers (the tone compared, its numbers editable) and Settings (keeping it, the ways
|
||||
* to the tones and the character's own settings); the tab is remembered. `ctx`: `script` (the
|
||||
* director's: lines, groups, speakerOf, listen, picked indexes, focus `{ character }` or null,
|
||||
* assign / focusCharacter / focusScript), `view` (the presets or null), `spec`, `busy`, `local`,
|
||||
* `canTag` (the node reads a line's own values), `presetNote`, `preview`, `redraw()`, and the run
|
||||
* section's calls: `options(group)` (its values in this script), `lineValues(line)`,
|
||||
* `setCharacter(group, field, value)`, `setLines(indexes, field, value)`, `clearLines(indexes)`,
|
||||
* `modeNumbers(group, values, id)`, `autoLanguage(group, values)`, `takeFor(line, mode, speaker,
|
||||
* values)`, `editPreset`, `canSave(group)`, `saveButton(group)`, `openPresets()`, `openTuning(group)`.
|
||||
* Draws the panel into `box`: the picked lines' layer, else the character's shown, else the whole
|
||||
* script's. `ctx`: `script` (the director's: lines, groups, group, speakerOf, listen, picked
|
||||
* indexes, focus `{ character }` or null, assign / focusCharacter / focusScript), `view` (the
|
||||
* presets or null), `spec`, `busy`, `local`, `canTag` (the node reads a line's own values),
|
||||
* `presetNote`, `preview`, `redraw()`, and the run section's calls: `layers` (`script()`,
|
||||
* `character(group)`, `line(line)`, each `{ values, from }`), `changedFor` (`script()`,
|
||||
* `character(group)`), `options(group)`, `lineValues(line)`, `setScript(field, value)`,
|
||||
* `setCharacter(group, field, value)`, `setLines(indexes, field, value)`, `clearScript()`,
|
||||
* `clearCharacter(group)`, `clearLines(indexes)`, `modeNumbers(group, values, id)`,
|
||||
* `autoLanguage(group, values)`, `takeFor(line, mode, speaker, values)`, `editPreset`,
|
||||
* `canSave(group)`, `saveButton(group)`, `openPresets()`, `openTuning(group)`.
|
||||
*/
|
||||
export function renderTunePanel(box, ctx) {
|
||||
const { script } = ctx;
|
||||
const group = script.focus?.character ? script.groups.find(item => item.group === script.focus.character) : null;
|
||||
const tab = remembered(TAB_KEY, TABS, 'how');
|
||||
let view;
|
||||
if (script.picked.length) view = renderLinesView(ctx, script.picked, tab);
|
||||
else if (group) view = renderCharacterView(ctx, group, tab);
|
||||
else view = renderScriptView(ctx);
|
||||
const parts = [view.head];
|
||||
if (!ctx.canTag && (script.picked.length || group)) parts.push(el('div', 'anomalous-sd-run-hint is-warning', t('scriptTuneOldTts')));
|
||||
if (script.picked.length || group) {
|
||||
parts.push(tabStrip(TABS.map(id => ({ id, label: t(`scriptTuneTab_${id}`) })), tab, (id) => {
|
||||
remember(TAB_KEY, id);
|
||||
ctx.redraw();
|
||||
}));
|
||||
}
|
||||
box.replaceChildren(...parts, ...view.body);
|
||||
let parts;
|
||||
if (script.picked.length) parts = renderLinesView(ctx, script.picked);
|
||||
else if (group) parts = renderCharacterView(ctx, group);
|
||||
else parts = renderScriptView(ctx);
|
||||
if (!ctx.canTag && (script.picked.length || group)) parts.splice(1, 0, el('div', 'anomalous-sd-run-hint is-warning', t('scriptTuneOldTts')));
|
||||
box.replaceChildren(...parts);
|
||||
}
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@
|
||||
@import url("./styles/10-model-sources.css?v=20261007-theme-2");
|
||||
@import url("./styles/11-audio-studio.css?v=20261018-cards");
|
||||
@import url("./styles/12-tts-setup.css?v=20261017-feel4");
|
||||
@import url("./styles/13-script-director.css?v=20261018-cards2");
|
||||
@import url("./styles/13-script-director.css?v=20261020-layers");
|
||||
@import url("./styles/14-version-manager.css?v=20261007-theme-2");
|
||||
@import url("./styles/15-shell-rail.css?v=20261017-cast");
|
||||
@import url("./styles/16-activity.css?v=20261007-theme-2");
|
||||
@@ -32,4 +32,4 @@
|
||||
@import url("./styles/31-model-info.css?v=20261014-desc-2");
|
||||
@import url("./styles/32-canvas-tools.css?v=20261019-places");
|
||||
@import url("./styles/33-kept-combos.css?v=20261019-hero");
|
||||
@import url("./styles/34-voice-over-panel.css?v=20261020-tabs");
|
||||
@import url("./styles/34-voice-over-panel.css?v=20261020-layers");
|
||||
|
||||
@@ -692,7 +692,7 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* How it is read: one setting per row; the sampling numbers folded under fine-tuning. */
|
||||
/* How it is read: the side panel's layers (ui_script_tune.js; their rows in 34-voice-over-panel.css). */
|
||||
.anomalous-sd-run-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -701,18 +701,6 @@
|
||||
border-top: 1px solid var(--amb-border);
|
||||
}
|
||||
|
||||
.anomalous-sd-run-heading {
|
||||
color: var(--amb-text-dim);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.anomalous-sd-run-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.anomalous-sd-run-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -783,27 +771,6 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* The mode: its label above the choices, the numbers, a note and the links (ui_script_run.js). */
|
||||
.anomalous-sd-run-mode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.anomalous-sd-run-mode > .anomalous-sd-run-label {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* The line the modes are heard on: its own row above the listen buttons. */
|
||||
.anomalous-sd-run-listen-line {
|
||||
flex: 1 1 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--amb-text-soft);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.anomalous-sd-run-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,29 +1,154 @@
|
||||
/* Voice-over's side panel, which follows what is picked on the left (ui_script_tune.js), the tabs
|
||||
it shares with GPT-SoVITS Settings (ui_tab_strip.js), and that dialog's head (ui_tts_setup.js). */
|
||||
|
||||
.anomalous-sd-tune-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-back {
|
||||
padding: 0;
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-title {
|
||||
color: var(--amb-text-main);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-meta {
|
||||
color: var(--amb-text-soft);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* The path on top: Whole script › character › lines; the current layer stands out. */
|
||||
.anomalous-sd-tune-path {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-path-step {
|
||||
padding: 3px 9px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: none;
|
||||
color: var(--amb-text-soft);
|
||||
font: inherit;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
button.anomalous-sd-tune-path-step {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.anomalous-sd-tune-path-step:hover {
|
||||
background: rgba(var(--amb-ink-rgb), 0.06);
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-path-step.is-current {
|
||||
border-color: color-mix(in srgb, var(--speaker, var(--amb-link)) 50%, transparent);
|
||||
background: color-mix(in srgb, var(--speaker, var(--amb-link)) 16%, transparent);
|
||||
color: var(--amb-text-main);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-path-sep {
|
||||
color: var(--amb-text-dim);
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-section {
|
||||
margin-top: 6px;
|
||||
color: var(--amb-text-dim);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
/* One setting per row: its name, the control, then where its value comes from (and ↺). */
|
||||
.anomalous-sd-tune-row {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid var(--amb-border);
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-row > :nth-child(2):last-child {
|
||||
grid-column: 2 / -1;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-row-end {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-speed {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-speed > .anomalous-sd-run-value {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
/* Where a value comes from: grey the whole script or the default, purple the character, yellow the line. */
|
||||
.anomalous-sd-tune-from {
|
||||
padding: 2px 8px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: rgba(var(--amb-ink-rgb), 0.07);
|
||||
color: var(--amb-text-soft);
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
button.anomalous-sd-tune-from {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.anomalous-sd-tune-from:hover {
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-from.is-character,
|
||||
.anomalous-sd-tune-from.is-usual {
|
||||
background: color-mix(in srgb, #a487f5 20%, transparent);
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-from.is-line {
|
||||
background: color-mix(in srgb, var(--amb-warn) 20%, transparent);
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-from.is-mixed {
|
||||
border: 1px dashed var(--amb-border-strong);
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* Under the tone: listening and the Numbers switch. */
|
||||
.anomalous-sd-tune-under {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px 10px;
|
||||
padding: 0 0 6px 54px;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-numbers {
|
||||
border: 1px solid var(--amb-border);
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-numbers.is-on {
|
||||
background: color-mix(in srgb, var(--amb-link) 18%, transparent);
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
/* The foot: settings that last beyond this script. */
|
||||
.anomalous-sd-tune-lasting {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px 12px;
|
||||
margin-top: 6px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--amb-border);
|
||||
color: var(--amb-text-dim);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
/* The whole script: one row per character reading, a click opens it. */
|
||||
.anomalous-sd-tune-cast {
|
||||
display: flex;
|
||||
@@ -113,29 +238,10 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-follow {
|
||||
color: var(--amb-text-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-reset {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* The language while it is detected: one line with "Change". */
|
||||
.anomalous-sd-tune-language {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--amb-text-soft);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* The tone's label above its choices. */
|
||||
.anomalous-sd-run-mode {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* On a line card: what the line set for itself. */
|
||||
.anomalous-sd-line-tune {
|
||||
flex-shrink: 0;
|
||||
@@ -236,42 +342,6 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Voice-over's Settings tab: each way out as a row with a line about it. */
|
||||
.anomalous-sd-tune-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 8px;
|
||||
background: rgba(var(--amb-ink-rgb), 0.03);
|
||||
color: var(--amb-text-main);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-link:hover {
|
||||
background: rgba(var(--amb-ink-rgb), 0.07);
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-link-name {
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-sd-tune-link-about {
|
||||
color: var(--amb-text-soft);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
/* Under the tone choices: what the picked one is, then listening. */
|
||||
.anomalous-tts-switch-listen {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user