Show the GPT-SoVITS model version on character cards; v3/v4/v5 imports

Character cards show the version Anomalous_TTS reports (interface 15). The
v3/v4/v5 base models and vocoders are listed, counted and reminded of only
when a character or a dropped SoVITS file needs them; 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>
This commit is contained in:
DemonGatanjieu
2026-10-17 12:29:25 +08:00
co-authored by Claude Opus 5.5
parent cdc8ad2f57
commit 6996f07063
13 changed files with 129 additions and 40 deletions
+4
View File
@@ -11,6 +11,10 @@
### 🧩 Combos (搭配)
- **A combo's models are cover cards**, as in the model library: the cover on top, the name below. In a combo saved from picked nodes, the slot (Main model, LoRA…) is written on the cover and a click on the card changes the model; in a default combo, the main models and LoRAs to pick from are cover cards side by side. They used to be rows as wide as the window, with **Change** far from the name (模型卡片不再横向拉满整个窗口).
### 🎙️ Character voices (角色语音)
- **Each character card shows its GPT-SoVITS model version** (v2, v2ProPlus, v4, v5dev…) after the name, so you can tell at a glance which model a voice runs on (角色卡片显示模型版本). Needs Anomalous_TTS with v3 / v4 / v5 support (its interface 15); with an older one the cards look as before.
- **v3, v4 and v5 models can be imported**: the import no longer refuses them. Each of these versions needs its own official base model and vocoder (about 0.8 GB each, v3 about 1.2 GB); **GPT-SoVITS Settings** lists them only when one of your characters uses that version, and the import card says what is still missing for the model you dropped in. A file whose version cannot be read is refused with a clearer reason; a v3 / v4 file with an older Anomalous_TTS asks you to update it.
### ☁️ ComfyUI on a cloud server (云服务器上使用)
- **How far it goes**: fully supported on overseas cloud servers (RunPod…). On servers in mainland China (AutoDL…) only in part: browsing, offline scans and tidying work, but Civitai is mostly out of reach there, so lookups are unreliable and covers rarely download (海外服务器完整支持;国内服务器只能部分支持). Not fully tested yet: one AutoDL server so far, no overseas one. The cloud guide ends with a section for AI assistants (what is going on, the steps, how to check, what to do when it did not work, what to tell the user about safety), so a user can hand the link to their AI.
- On a rented cloud server (AutoDL, RunPod…) the page is always opened from another device, so since v1.58.1 the plugin only let it look: scans, downloads, moves and saves were refused. Now there are two ways: open ComfyUI through an **SSH tunnel** (the address starts with 127.0.0.1; it counts as the computer ComfyUI runs on, nothing to change), or make an empty file named **`allow_remote_changes`** in the plugin folder on the server and restart ComfyUI. With the file, every device that can open the page may change things; the page can neither make nor remove it, and the MCP endpoint stays for this computer only. How, and the risk: [docs/guides/cloud-server.md](docs/guides/cloud-server.md).
+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
@@ -2262,6 +2262,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: "缺少(可选)",
@@ -2370,7 +2375,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: "在下面的音频里点 ☆,选一段当主声音",
@@ -2416,7 +2422,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",
@@ -4852,6 +4859,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)",
@@ -4960,7 +4972,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",
@@ -5006,7 +5019,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=20261010-wfd-1");
@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=20261010-search-1");
@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;