Compare commits

...
Author SHA1 Message Date
DemonGatanjieuandClaude Opus 5.5 8697663026 v1.59.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:55:01 +08:00
DemonGatanjieuandClaude Opus 5.5 07a989fbe9 Show the GPT-SoVITS model version on character cards; v3/v4 imports (1.59.1)
From 1.60.1 (6996f07): character cards show the version Anomalous_TTS reports
(interface 15); v3/v4 base models and vocoders are listed and reminded of only
when a character or a dropped SoVITS file needs them, with their own labels;
the import tells an unreadable SoVITS file from a version an older node cannot
use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:48:37 +08:00
DemonGatanjieuandClaude Opus 5.5 f35d68671f README: Chinese pictures from the 1.59 interface
The Chinese part's seven pictures are re-shot in the 1.59 interface on the
isolated demo ComfyUI (demo models only); the combo picture shows picked
nodes saved as a combo and put back, and its text says so. The old
Chinese title picture, no longer used, is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:34:21 +08:00
DemonGatanjieuandClaude Opus 5.5 169d455e1e README: Model Check finds renamed models by the fingerprints in the workflow
Without them it can only offer a file of the same name; the setting that
records them is named.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:25:52 +08:00
22 changed files with 140 additions and 48 deletions
+7
View File
@@ -1,5 +1,12 @@
# 📈 Anomalous Model Browser Changelog
## v1.59.1 Beta (GPT-SoVITS v3 / v4 voices) — 2026-10-17
### 🎙️ Character voices: v3 and v4 models (角色语音支持 v3 / v4 模型)
- **Anomalous_TTS now runs GPT-SoVITS v3 and v4 models** (its first stable release, [v1.0.0](https://github.com/DemonGatanjieu/Anomalous_TTS/releases/tag/v1.0.0); update it in ComfyUI Manager), and this version of the plugin goes with it: v3 / v4 files are imported instead of refused, and a file whose version cannot be read is refused with a clearer reason. With an Anomalous_TTS too old for v3 / v4, the import says to update it.
- **Each character card shows its GPT-SoVITS model version** (v2, v2ProPlus, v4…) after the name (角色卡片显示模型版本).
- **v3 and v4 need their own official base model and vocoder** (v4 about 0.8 GB, v3 about 1.2 GB). **GPT-SoVITS Settings** lists them, with their own labels, only when one of your characters uses that version, and the import card says what is still missing for the model you dropped in. They download on first use too; a GPT-SoVITS package already on the computer is used as it is.
## v1.59.0 Beta (Model Downloads, Backup, Share Codes, Node Combos, MCP, Light Theme) — 2026-10-17
### ⬇️ Download missing models from Model Check (模型检查一键下载缺失模型)
+8 -8
View File
@@ -9,7 +9,7 @@
[![ComfyUI Manager](https://img.shields.io/badge/ComfyUI-Manager-green?style=for-the-badge&logo=comfyui)](https://github.com/ltdrdata/ComfyUI-Manager)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)](LICENSE)
[![Changelog](https://img.shields.io/badge/📖_Changelog-v1.59.0_Beta-blue?style=for-the-badge)](CHANGELOG.md)
[![Changelog](https://img.shields.io/badge/📖_Changelog-v1.59.1_Beta-blue?style=for-the-badge)](CHANGELOG.md)
[![Bilibili Video](https://img.shields.io/badge/Bilibili-视频演示-00A1D6?style=for-the-badge&logo=bilibili)](https://www.bilibili.com/video/BV1boHx61Enc/)
[![Bilibili Voices](https://img.shields.io/badge/Bilibili-配音演示-00A1D6?style=for-the-badge&logo=bilibili)](https://www.bilibili.com/video/BV1bkpZ6uEZQ/)
[![Reddit](https://img.shields.io/badge/Reddit-r%2Fcomfyui-FF4500?style=for-the-badge&logo=reddit)](https://www.reddit.com/r/comfyui/s/n38ihf3DLf)
@@ -40,7 +40,7 @@
<p align="center"><img src="docs/images/en-scan.webp" width="520" alt="Scan results: each file matched on Civitai"></p>
**2. Someone else's workflow opens with red nodes.** Model Check says which models are missing. The ones you already have under another name or in another folder go back in with one press; the rest can be downloaded from Civitai or Hugging Face, and each download is checked against the model's fingerprint.
**2. Someone else's workflow opens with red nodes.** Model Check says which models are missing. When the workflow carries model fingerprints (the plugin writes them into the workflows and images of everyone who has it, unless they turn it off), the ones you already have under another name or in another folder go back in with one press, and the rest can be downloaded from Civitai or Hugging Face, each download checked against its fingerprint. A workflow without fingerprints can only be matched by file name: a file of the same name elsewhere on your computer is offered for you to confirm.
<p align="center"><img src="docs/images/en-model-check.webp" width="760" alt="Model Check puts back three missing models with one press"></p>
@@ -97,7 +97,7 @@ Every page has an icon on the rail at the left of the window.
Tools on the rail:
* **Scan** reads your model folders and fetches covers, trigger words and base models from Civitai (by file hash, no extra Python packages).
* **Model Check** (formerly Model Doctor) checks each workflow you open: when models are missing, a bar over the canvas says so and puts back, with one press, the ones you have under another name or folder (recognised by hash and file size); its page lists the rest with what you can do.
* **Model Check** (formerly Model Doctor) checks each workflow you open: when models are missing, a bar over the canvas says so and puts back, with one press, the ones you have under another name or folder, recognised by the fingerprints (hash and file size) the plugin records in workflows and images (**Settings → Record model fingerprints in workflows**, on by default); without them, a file of the same name is offered for you to confirm. Its page lists the rest with what you can do.
* **Current node** (formerly Node Assistant) shows the model of the node you select, lets you swap it from the covers or insert a LoRA, and lists every set of values saved for that kind of node with exactly what applying it would change; saved values can be deleted there.
* **Each tool on its page**: share and import on Workflows (⇅: a short share code of the canvas workflow, in Chinese characters or letters; paste a code, or drop a workflow file, an image or a backup .zip), Model Sources (where each model can be downloaded) on Models and in Model Check, translation on the prompt boxes in Current node.
* **Settings** (at the bottom): language, font size, thumbnails, video covers, which model folders to show, and feedback. Home and Settings have **Report a problem**, which opens a GitHub issue with your versions and GPU already filled in.
@@ -147,7 +147,7 @@ Open the browser with **Shift + B** (again to close), the floating button on the
<p align="center"><img src="docs/images/scan.webp" width="520" alt="扫描结果:每个文件都在 C 站认出来了"></p>
**2. 打开别人的工作流,一片红框。** 模型检查会告诉你缺哪些模型。你电脑里其实有、只是改了名或换了文件夹的,点一下就换上;真没有的可以从 C 站或 Hugging Face 下载,下载完按模型指纹核对过才用。
**2. 打开别人的工作流,一片红框。** 模型检查会告诉你缺哪些模型。如果工作流里带着模型指纹(装了本插件的人出图、保存工作流时会自动记下,除非他在设置里关掉),你电脑里其实有、只是改了名或换了文件夹的,点一下就换上;真没有的可以从 C 站或 Hugging Face 下载,下载完按指纹核对过才用。没有指纹的工作流只能按文件名找:别处有同名文件,会列出来让你确认。
<p align="center"><img src="docs/images/model-check.webp" width="760" alt="模型检查一键换上 3 个缺失的模型"></p>
@@ -155,11 +155,11 @@ Open the browser with **Shift + B** (again to close), the floating button on the
<p align="center"><img src="docs/images/gallery.webp" width="760" alt="图库搜索后把图拖到画布"></p>
**4. 每次都要重新配一遍模型、LoRA 和提示词。** 存成一个搭配。把卡片拖到画布空白处,就放出一组连好线的节点,画布上原有的东西不动。
**4. 每次都要重新配一遍模型、LoRA 和提示词。** 在画布上选中这几个节点,右键 **存为搭配**,它们就成了画布旁的一张卡片。下次点 **放到画布**(或者直接拖卡片),点哪里,那组连好线的节点就出现在哪里,画布上原有的东西不动。搭配里的模型和提示词以后还能换。
<p align="center"><img src="docs/images/combo.webp" width="760" alt="搭配卡片拖到画布变成连好线的节点"></p>
<p align="center"><img src="docs/images/combo.webp" width="760" alt="选中的节点存为搭配,再放回画布变成连好线的节点"></p>
**5. 提示词太长,改起来麻烦。** 选中提示词节点,提示词工坊把它拆成词块:点一下调权重,拖动换位置,输入就加(中文会自动译成英文)。
**5. 提示词太长,改起来麻烦。** 选中提示词节点,提示词工坊把它拆成词块:点一下调权重,拖动换位置,输入就加(中文会自动译成英文,比如输入“蓝宝石”加进去的是 sapphire)。
<p align="center"><img src="docs/images/prompt-studio.webp" width="760" alt="提示词工坊按词块编辑提示词节点"></p>
@@ -207,7 +207,7 @@ Open the browser with **Shift + B** (again to close), the floating button on the
图标栏上的工具:
* **扫描**:读取模型文件夹,按文件哈希从 C 站获取封面、触发词和底模。不需要额外安装 Python 包。
* **模型检查**(原“模型医生”):打开工作流时自动检查,缺模型就在画布上方提示;本地改过名、换过文件夹的同一个文件(按哈希和文件大小认)一键换上,其余的在检查页里自己挑或去 Civitai 找。
* **模型检查**(原“模型医生”):打开工作流时自动检查,缺模型就在画布上方提示;本地改过名、换过文件夹的同一个文件一键换上,靠的是插件记在工作流和图片里的模型指纹(哈希和文件大小;**设置 → 在工作流里记下模型指纹**,默认打开);没有指纹时,同名文件会列出来让你确认。其余的在检查页里自己挑或去 Civitai 找。
* **当前节点**(原“节点助手”):选中画布上的节点,查看它用的模型。可以看图换模型、插入 LoRA;存过的同类节点参数列在一起,每条写明会改哪几项,一键套用;存下的参数也在这里删除。
* **工具回到各自的页面**:分享与导入在“工作流”页(⇅:把画布上的工作流变成一段短短的分享码,有汉字码和字母码;也能粘贴分享码,或拖入工作流文件、图片、备份压缩包),模型来源(每个模型去哪下载)在“模型”页和模型检查里,翻译在“当前节点”的提示词框上。
* **设置**(最下面):语言、字号、缩略图、视频封面、显示哪些模型文件夹,以及提交反馈。主页和设置里的 **报告问题** 会打开一个已经填好版本和显卡信息的 GitHub 页面。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 19 KiB

+4 -1
View File
@@ -7,7 +7,7 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const calls = [];
const character = {
name: '阿罗娜/日配', aliases: ['阿罗娜'], language: 'ja', settings: { format: 1, keep: 1 },
counts: { gpt: 1, sovits: 1, audio: 2 },
counts: { gpt: 1, sovits: 1, audio: 2 }, version: 'v4', needs: ['v4'],
reference: { audio: 'ref/a.wav', text: 'こんにちは', language: 'ja', source: 'auto' },
emotions: { 开心: { audio: 'ref/a.开心.wav', text: '', language: 'ja', source: 'filename' } },
};
@@ -34,6 +34,9 @@ assert.equal(first.error, '');
const arona = first.groups.find(g => g.character === '阿罗娜/日配');
assert.deepEqual(arona.slices.map(s => s.emotion), ['main', '开心']);
assert.equal(arona.has_main, true);
assert.equal(arona.model_version, 'v4'); // interface 15; older nodes give ''
assert.deepEqual(arona.needs, ['v4']);
assert.deepEqual(first.groups.find(g => g.character === 'broken').needs, []);
assert.equal(first.groups.find(g => g.character === 'broken').has_main, false);
// Re-rendering (sidebar click, studio + sidebar together) does not hit the server again.
+16 -1
View File
@@ -125,7 +125,9 @@ assert.deepEqual(api.importProblem({ character: '普拉娜', rows: weights, conf
['ttsImportNameTaken', { name: '普拉娜' }]);
assert.equal(api.importProblem({ character: 'X', rows: weights }), null);
assert.deepEqual(api.importProblem({ character: 'X', rows: [weights[0], { kind: 'sovits', name: 'v4.pth', supported: false, version: 'v4' }] }),
['ttsImportUnsupportedBlock', { file: 'v4.pth', version: 'v4' }]);
['ttsImportUnsupportedBlock', { file: 'v4.pth', version: 'v4' }]); // an Anomalous_TTS before v4 support
assert.deepEqual(api.importProblem({ character: 'X', rows: [weights[0], { kind: 'sovits', name: 'odd.pth', supported: false }] }),
['ttsImportUnknownBlock', { file: 'odd.pth' }]);
assert.deepEqual([undefined, 2.9, 3, 10, 10.1].map(api.usableAsReference), [true, false, true, true, false]);
// A text file dropped on a clip's text box: a plain file is the line; an annotation file is searched.
@@ -163,6 +165,19 @@ assert.deepEqual(api.pretrainedReminder(pre, ['']).items.map(i => i.id), ['huber
assert.deepEqual(api.missingForLanguage(pre, 'zh').map(i => i.id), ['hubert', 'roberta']);
assert.deepEqual(api.missingForLanguage(pre, 'ja').map(i => i.id), ['hubert']);
// v3 / v4 / v5 files: listed, counted and reminded of only for characters (or dropped files) whose model needs them.
const versions = { pretrained: [...pre.pretrained,
{ id: 'v4', needed_for: 'v4', state: 'missing', size: 800, required: true },
{ id: 'v5dev', needed_for: 'v5dev', state: 'downloading', size: 800, required: true },
{ id: 'v5turbo', needed_for: 'v5turbo', state: 'missing', size: 800, required: true },
] };
assert.deepEqual(api.shownPretrained(versions).map(i => i.id), ['hubert', 'roberta', 'ja_userdic', 'english', 'v5dev']);
assert.deepEqual(api.shownPretrained(versions, ['v4']).map(i => i.id).slice(-2), ['v4', 'v5dev']);
assert.equal(api.setupSummary(versions).missing, 4); // v4 and v5turbo not counted while nobody uses them
assert.deepEqual(api.pretrainedReminder(versions, ['']).items.map(i => i.id), ['hubert', 'roberta', 'ja_userdic']);
assert.deepEqual(api.pretrainedReminder(versions, ['ja'], [], ['v4']).items.map(i => i.id), ['hubert', 'ja_userdic', 'v4']);
assert.deepEqual(api.missingForLanguage(versions, '', ['v4']).map(i => i.id), ['hubert', 'v4']);
// Row tones and section marks in the import form.
assert.equal(api.rowState({ progress: 0.42 }).tone, 'busy');
assert.deepEqual(api.rowState({ progress: 0.42 }).params, { percent: 42 });
+2
View File
@@ -59,6 +59,8 @@ export function gptSovitsGroups(payload) {
group: voiceGroupKey(name),
character: name,
language: item.language || '',
model_version: item.version || '', // GPT-SoVITS v2 / v4 / v5dev… (interface 15)
needs: Array.isArray(item.needs) ? item.needs : [], // pretrained ids its model needs
aliases: Array.isArray(item.aliases) ? item.aliases : [],
error: item.error || item.settings_error || '',
has_main: Boolean(item.reference?.audio) && !item.error,
+18 -4
View File
@@ -1850,6 +1850,11 @@ export const i18n = {
ttsNeededFor_en: "英语",
ttsNeededFor_ja: "日语",
ttsNeededFor_v2pro: "v2Pro 模型",
ttsNeededFor_v3: "v3 模型",
ttsNeededFor_v4: "v4 模型",
ttsNeededFor_v5dev: "v5dev 模型",
ttsNeededFor_v5turbo: "v5turbo 模型",
ttsModelVersion: "GPT-SoVITS {version} 模型",
ttsPretrainedOk: "已就绪",
ttsPretrainedMissing: "缺少",
ttsPretrainedOptional: "缺少(可选)",
@@ -1958,7 +1963,8 @@ export const i18n = {
ttsCheckGptDone: "GPT 模型",
ttsCheckSovits: "放 SoVITS 模型(.pth,在整合包的 SoVITS_weights 文件夹里)",
ttsCheckSovitsDone: "SoVITS 模型",
ttsCheckSovitsUnsupported: "这个 SoVITS 是 {version} 模型,暂不支持,换一个 v1 / v2 / v2Pro 的",
ttsCheckSovitsUnsupported: "这个 SoVITS 是 {version} 模型,装的 Anomalous_TTS 太旧,更新后就能用",
ttsCheckSovitsUnknown: "认不出这个 SoVITS 的版本,换一个 GPT-SoVITS 训练出的 .pth",
ttsCheckAudio: "放几段参考音频(3~10 秒、说得清楚的)",
ttsCheckAudioDone: "参考音频 {count} 段",
ttsCheckMain: "在下面的音频里点 ☆,选一段当主声音",
@@ -2004,7 +2010,8 @@ export const i18n = {
ttsImportNameTakenVariant: "“{name}”已经有这些版本:{variants}。请换一个名字。",
ttsImportSwitchToAdd: "改成添加到这个角色",
ttsImportKeptBest: "{kind}:用了轮数最大的 {file}(另外 {count} 个没用)。",
ttsImportUnsupportedBlock: "{file} 是 {version} 模型,暂不支持。请换一个 v1、v2、v2Pro 或 v2ProPlus 的 SoVITS 权重。",
ttsImportUnsupportedBlock: "{file} 是 {version} 模型,现在装的 Anomalous_TTS 还不支持。在 ComfyUI Manager 里更新 Anomalous_TTS 后再导入。",
ttsImportUnknownBlock: "认不出 {file} 是哪个版本的 SoVITS 权重。请换一个 GPT-SoVITS 训练出的 .pth。",
ttsImportOutOfRange: "不在 3~10 秒,节点用不了,不会导入",
ttsTextSource_txt: "台词来自同名 .txt",
ttsTextSource_lab: "台词来自同名 .lab",
@@ -4028,6 +4035,11 @@ export const i18n = {
ttsNeededFor_en: "English",
ttsNeededFor_ja: "Japanese",
ttsNeededFor_v2pro: "v2Pro models",
ttsNeededFor_v3: "v3 models",
ttsNeededFor_v4: "v4 models",
ttsNeededFor_v5dev: "v5dev models",
ttsNeededFor_v5turbo: "v5turbo models",
ttsModelVersion: "GPT-SoVITS {version} model",
ttsPretrainedOk: "ready",
ttsPretrainedMissing: "missing",
ttsPretrainedOptional: "missing (optional)",
@@ -4136,7 +4148,8 @@ export const i18n = {
ttsCheckGptDone: "GPT model",
ttsCheckSovits: "Add the SoVITS model (.pth, in the package's SoVITS_weights folder)",
ttsCheckSovitsDone: "SoVITS model",
ttsCheckSovitsUnsupported: "This SoVITS is a {version} model, not supported yet; use a v1 / v2 / v2Pro one",
ttsCheckSovitsUnsupported: "This SoVITS is a {version} model; the installed Anomalous_TTS is too old for it, update it",
ttsCheckSovitsUnknown: "This SoVITS file's version is unknown; use a .pth trained with GPT-SoVITS",
ttsCheckAudio: "Add a few reference clips (clear, 3–10 s)",
ttsCheckAudioDone: "{count} reference clips",
ttsCheckMain: "Press ☆ on a clip below to make it the main voice",
@@ -4182,7 +4195,8 @@ export const i18n = {
ttsImportNameTakenVariant: "\"{name}\" already has these versions: {variants}. Please pick another name.",
ttsImportSwitchToAdd: "Add to that character instead",
ttsImportKeptBest: "{kind}: using the latest epoch, {file} ({count} {count|other|others} left out).",
ttsImportUnsupportedBlock: "{file} is a {version} model, which is not supported yet. Use a v1, v2, v2Pro or v2ProPlus SoVITS weight.",
ttsImportUnsupportedBlock: "{file} is a {version} model, which the installed Anomalous_TTS cannot use yet. Update Anomalous_TTS in ComfyUI Manager, then import it.",
ttsImportUnknownBlock: "The version of the SoVITS weight {file} is unknown. Use a .pth trained with GPT-SoVITS.",
ttsImportOutOfRange: "Not 3–10 s long: the node cannot use it, so it is left out",
ttsTextSource_txt: "Line from the .txt of the same name",
ttsTextSource_lab: "Line from the .lab of the same name",
+2 -1
View File
@@ -345,7 +345,8 @@ export function draftChecklist({ target = null, name = '', rows: all, main = nul
return [
{ id: 'gpt', done: Boolean(weight('gpt')), key: weight('gpt') ? 'ttsCheckGptDone' : 'ttsCheckGpt', params: {} },
{ id: 'sovits', done: Boolean(sovits) && sovits.supported !== false,
key: !sovits ? 'ttsCheckSovits' : sovits.supported === false ? 'ttsCheckSovitsUnsupported' : 'ttsCheckSovitsDone', params: { version: sovits?.version || '?' } },
key: !sovits ? 'ttsCheckSovits' : sovits.supported !== false ? 'ttsCheckSovitsDone' : sovits.version ? 'ttsCheckSovitsUnsupported' : 'ttsCheckSovitsUnknown',
params: { version: sovits?.version || '?' } },
{ id: 'audio', done: clips.length > 0, key: clips.length ? 'ttsCheckAudioDone' : skipped ? 'ttsCheckAudioNoneUsable' : 'ttsCheckAudio',
params: { count: clips.length } },
{ id: 'main', done: Boolean(main), key: main ? 'ttsCheckMainDone' : 'ttsCheckMain', params: { file: main || '' } },
+36 -12
View File
@@ -182,7 +182,10 @@ export function importProblem({ target = null, character = '', rows, conflict =
const clash = rows.find(row => row.existing === 'different');
if (clash) return ['ttsImportExistingDifferent', { file: clash.name }];
const unsupported = rows.find(row => row.kind === 'sovits' && row.supported === false);
if (unsupported) return ['ttsImportUnsupportedBlock', { file: unsupported.name, version: unsupported.version || '?' }];
if (unsupported) {
return unsupported.version ? ['ttsImportUnsupportedBlock', { file: unsupported.name, version: unsupported.version }]
: ['ttsImportUnknownBlock', { file: unsupported.name }];
}
const seen = new Set();
for (const row of rows) {
if (row.kind !== 'audio' || !row.emotion) continue;
@@ -214,27 +217,48 @@ export function textFromFile(content, clipName) {
return table.get(name) ?? table.get(`${stem.split('.')[0]}${ext}`) ?? null;
}
// `needed_for` of the pretrained items only v3 / v4 / v5 models use (their base model and vocoder).
const MODEL_VERSIONS = new Set(['v3', 'v4', 'v5dev', 'v5turbo']);
/**
* Pretrained items worth listing: the shared ones, and a model version's only when `needs`
* (ids from the characters' or the dropped files' SoVITS models) asks for it, or while it
* downloads or failed. A library without v5 characters never sees the v5 files.
*/
export function shownPretrained(status, needs = []) {
const wanted = new Set(needs);
return (status?.pretrained || []).filter(item => !MODEL_VERSIONS.has(item.needed_for) || wanted.has(item.id)
|| ['queued', 'downloading', 'error'].includes(item.state));
}
/**
* Pretrained files worth a reminder dot: missing ones that the characters in the
* studio will actually load (the shared ones, English for mixed-in words, v2Pro's
* speaker model, plus their languages; a character with no known language counts
* as every language). No characters, no reminder: an empty studio is only about
* importing. `due` is false once every such file was dismissed with "don't remind
* me"; a newly needed one (say, the first Chinese character) brings the dot back.
* as every language; a v3 / v4 / v5 model's own files when `needs` lists them). No
* characters, no reminder: an empty studio is only about importing. `due` is false
* once every such file was dismissed with "don't remind me"; a newly needed one
* (say, the first Chinese character) brings the dot back.
*/
export function pretrainedReminder(status, languages, dismissed = []) {
export function pretrainedReminder(status, languages, dismissed = [], needs = []) {
const langs = new Set(languages || []);
if (!langs.size) return { items: [], size: 0, due: false };
const needed = (item) => ['all', 'en', 'v2pro'].includes(item.needed_for) || langs.has(item.needed_for) || langs.has('');
const wanted = new Set(needs);
const needed = (item) => (MODEL_VERSIONS.has(item.needed_for) ? wanted.has(item.id)
: ['all', 'en', 'v2pro'].includes(item.needed_for) || langs.has(item.needed_for) || langs.has(''));
const items = (status?.pretrained || []).filter(item => (item.state === 'missing' || item.state === 'error') && needed(item));
const seen = new Set(dismissed);
return { items, size: items.reduce((sum, item) => sum + (item.size || 0), 0), due: items.some(item => !seen.has(item.id)) };
}
/** Required pretrained files still missing for a character in `language` ('' = not chosen yet). */
export function missingForLanguage(status, language) {
/**
* Required pretrained files still missing for a character in `language` ('' = not chosen yet)
* whose SoVITS model needs the pretrained ids `needs` (a v4 model: its base model and vocoder).
*/
export function missingForLanguage(status, language, needs = []) {
const wanted = new Set(needs);
return (status?.pretrained || []).filter(item => item.required && item.state !== 'ok'
&& (item.needed_for === 'all' || (language && item.needed_for === language)));
&& (item.needed_for === 'all' || (language && item.needed_for === language) || wanted.has(item.id)));
}
/**
@@ -253,10 +277,10 @@ export function rowState({ error = '', uploaded = false, queued = true, progress
return { tone: 'ready', key: 'ttsImportRowReady', params: {} };
}
/** Summary for the setup card: what still needs doing. */
export function setupSummary(status) {
/** Summary for the setup card: what still needs doing (`needs`: see shownPretrained). */
export function setupSummary(status, needs = []) {
const characters = (status?.libraries || []).reduce((sum, lib) => sum + (lib.characters || 0), 0);
const missing = (status?.pretrained || []).filter(item => item.state !== 'ok');
const missing = shownPretrained(status, needs).filter(item => item.state !== 'ok');
const packages = Object.values(status?.dependencies || {}).reduce((sum, dep) => sum + (dep.missing?.length || 0), 0);
const downloading = missing.some(item => item.state === 'queued' || item.state === 'downloading');
const requiredMissing = missing.filter(item => item.required).length;
+5 -4
View File
@@ -268,8 +268,8 @@ function renderCharacters(owner, container, groups, { canImport, term }) {
}
/** Bottom entry for GPT-SoVITS settings; the dot says when something wants a look. */
function renderSettingsEntry(owner, status, languages) {
const attention = setupAttention(status, languages);
function renderSettingsEntry(owner, status, languages, needs) {
const attention = setupAttention(status, languages, needs);
const entry = el('button', `anomalous-audio-sidebar-settings${attention.level ? ` is-${attention.level}` : ''}`);
entry.type = 'button';
entry.title = attention.title;
@@ -278,7 +278,7 @@ function renderSettingsEntry(owner, status, languages) {
if (attention.level) label.append(el('span', 'anomalous-audio-sidebar-settings-dot'));
entry.append(label);
if (attention.busy) entry.append(el('span', 'anomalous-audio-sidebar-settings-busy', attention.busy));
entry.onclick = () => openTtsSetup({ languages, onChanged: (options) => redraw(owner, options) });
entry.onclick = () => openTtsSetup({ languages, needs, onChanged: (options) => redraw(owner, options) });
return entry;
}
@@ -327,7 +327,8 @@ export async function renderAudioSidebar(owner) {
if (status) {
const languages = groups.map(group => group.language);
owner.sidebar.append(renderSettingsEntry(owner, status, languages));
const needs = groups.flatMap(group => group.needs);
owner.sidebar.append(renderSettingsEntry(owner, status, languages, needs));
if (canImport) {
bindTtsFileDrop(listContainer, (files, target) => openTtsImport({ files, target, onDone: () => redraw(owner) }));
}
+7
View File
@@ -220,6 +220,13 @@ function renderCharacterCard(group, owner, { onChanged, canImport = false } = {}
name.append(' ', version);
}
name.title = group.character;
if (group.model_version) {
const model = document.createElement('span');
model.className = 'anomalous-character-voice-model';
model.textContent = group.model_version;
model.title = t('ttsModelVersion', { version: group.model_version });
name.append(model);
}
const sub = document.createElement('span');
sub.className = 'anomalous-character-voice-hint';
+5 -2
View File
@@ -348,8 +348,11 @@ export function renderDraftCard(draft, view, ctx, { solo = false } = {}) {
extras.append(language);
}
body.append(extras);
const missing = missingForLanguage(ctx.status, draft.language || draft.detectedLanguage || '');
if (missing.length && !draft.target) {
// A new character's language decides the language files; any draft's SoVITS model its own files (v4: base + vocoder).
const needs = draft.rows.flatMap(row => (row.kind === 'sovits' && row.info?.needs) || []);
const missing = missingForLanguage(ctx.status, draft.target ? '' : draft.language || draft.detectedLanguage || '', needs)
.filter(item => !draft.target || needs.includes(item.id));
if (missing.length) {
body.append(el('div', 'anomalous-tts-card-note', t('ttsImportPretrainedNote', {
files: missing.map(item => item.label).join('、'),
size: formatSize(missing.reduce((sum, item) => sum + (item.size || 0), 0)),
+17 -14
View File
@@ -2,7 +2,7 @@ import { t } from './interface_settings.js';
import { createViewScope } from './ui_lifecycle.js';
import { anomalousAlert } from './ui_dialog.js';
import { loadGptSovitsStatus } from './audio_engines.js';
import { formatSize, pretrainedReminder, setupSummary, startPretrainedDownload } from './tts_setup_api.js';
import { formatSize, pretrainedReminder, setupSummary, shownPretrained, startPretrainedDownload } from './tts_setup_api.js';
/**
* GPT-SoVITS settings, opened from the audio sidebar's footer: the storage place, other
@@ -115,11 +115,12 @@ function pretrainedState(item) {
return state;
}
function renderPretrained(status, local, onChanged) {
function renderPretrained(status, local, onChanged, needs) {
const group = el('div', 'anomalous-tts-setup-group');
group.append(el('div', 'anomalous-tts-section', t('ttsSetupPretrained')));
const missing = status.pretrained.filter(item => item.state === 'missing' || item.state === 'error');
for (const item of status.pretrained) {
const items = shownPretrained(status, needs);
const missing = items.filter(item => item.state === 'missing' || item.state === 'error');
for (const item of items) {
const row = el('div', 'anomalous-tts-setup-row');
const label = el('span', 'anomalous-tts-setup-path', item.label);
label.title = item.path || item.error || '';
@@ -190,9 +191,9 @@ function renderReminder(reminder, local, onChanged, onDismiss) {
}
/** What the sidebar entry shows: `level` is 'blocked' (packages), 'reminder' (pretrained) or ''. */
export function setupAttention(status, languages = []) {
const summary = setupSummary(status);
const reminder = pretrainedReminder(status, languages, storedDismissed());
export function setupAttention(status, languages = [], needs = []) {
const summary = setupSummary(status, needs);
const reminder = pretrainedReminder(status, languages, storedDismissed(), needs);
return {
level: summary.packages ? 'blocked' : reminder.due ? 'reminder' : '',
title: t(summary.packages ? 'ttsSetupDotPackages' : reminder.due ? 'ttsSetupDotPretrained' : 'ttsSetupTitle'),
@@ -203,13 +204,14 @@ export function setupAttention(status, languages = []) {
/**
* The card for one status payload. `onChanged()` redraws the studio (a download
* changes what is ready); `onDismissed()` runs after "don't remind me";
* `languages` are the languages of the characters in the studio (for the reminder).
* `languages` are the languages of the characters in the studio (for the reminder), `needs`
* the pretrained ids their SoVITS models need (v3 / v4 / v5 files are listed only then).
*/
export function renderTtsSetup(status, options) {
const { onChanged, onDismissed, languages = [] } = options;
const summary = setupSummary(status);
const { onChanged, onDismissed, languages = [], needs = [] } = options;
const summary = setupSummary(status, needs);
const local = status.local !== false;
const reminder = pretrainedReminder(status, languages, storedDismissed());
const reminder = pretrainedReminder(status, languages, storedDismissed(), needs);
const card = el('section', `anomalous-tts-setup${summary.packages ? ' is-blocked' : ''}`);
if (!local) card.append(el('div', 'anomalous-tts-setup-notice', t('ttsSetupRemote')));
@@ -222,7 +224,7 @@ export function renderTtsSetup(status, options) {
}
const packages = renderPackages(status);
if (packages) card.append(packages);
card.append(renderStorage(status), renderPretrained(status, local, onChanged));
card.append(renderStorage(status), renderPretrained(status, local, onChanged, needs));
const settingsFile = renderSettingsFile(status);
if (settingsFile) card.append(settingsFile);
if (summary.downloading) pollWhileBusy(card, options);
@@ -235,7 +237,7 @@ let activeScope = null;
* The settings dialog. `onChanged()` redraws the studio and sidebar after a change;
* the dialog then reloads the status and redraws itself.
*/
export async function openTtsSetup({ onChanged, languages = [] }) {
export async function openTtsSetup({ onChanged, languages = [], needs = [] }) {
activeScope?.dispose();
const scope = createViewScope();
activeScope = scope;
@@ -273,6 +275,7 @@ export async function openTtsSetup({ onChanged, languages = [] }) {
const options = {
languages,
needs,
onDismissed: () => onChanged?.({ sidebarOnly: true }),
onChanged: async () => {
onChanged?.();
@@ -281,7 +284,7 @@ export async function openTtsSetup({ onChanged, languages = [] }) {
},
};
const draw = () => {
subtitle.textContent = summaryText(status, setupSummary(status));
subtitle.textContent = summaryText(status, setupSummary(status, needs));
body.replaceChildren(renderTtsSetup(status, options));
};
draw();
+1 -1
View File
@@ -9,7 +9,7 @@
@import url("./styles/08-theme-gallery-overrides.css?v=20261007-theme-2");
@import url("./styles/09-prompt-drawers.css?v=20261007-theme-2");
@import url("./styles/10-model-sources.css?v=20261007-theme-2");
@import url("./styles/11-audio-studio.css?v=20261007-theme-2");
@import url("./styles/11-audio-studio.css?v=20261017-tts-versions");
@import url("./styles/12-tts-setup.css?v=20261007-theme-2");
@import url("./styles/13-script-director.css?v=20261007-theme-2");
@import url("./styles/14-version-manager.css?v=20261007-theme-2");
+12
View File
@@ -305,6 +305,18 @@
color: var(--amb-link);
}
/* GPT-SoVITS model version (v2, v4, v5dev…) after the name */
.anomalous-character-voice-model {
margin-left: 6px;
padding: 1px 6px;
border: 1px solid var(--amb-border);
border-radius: 999px;
font-size: 10px;
font-weight: 600;
color: var(--amb-text-muted);
vertical-align: 1px;
}
.anomalous-character-voice-hint {
overflow: hidden;
font-size: 11px;