From 6996f070635cf505242e60faa53ed4522f6e13af Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <207458273+DemonGatanjieu@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:29:25 +0800 Subject: [PATCH] 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 --- CHANGELOG.md | 4 +++ tests/audio_engines.mjs | 5 +++- tests/tts_setup_api.mjs | 17 ++++++++++- web/modules/audio_engines.js | 2 ++ web/modules/locales.js | 22 +++++++++++--- web/modules/tts_import_groups.js | 3 +- web/modules/tts_setup_api.js | 48 ++++++++++++++++++++++-------- web/modules/ui_audio_sidebar.js | 9 +++--- web/modules/ui_audio_studio.js | 7 +++++ web/modules/ui_tts_import_draft.js | 7 +++-- web/modules/ui_tts_setup.js | 31 ++++++++++--------- web/styles.css | 2 +- web/styles/11-audio-studio.css | 12 ++++++++ 13 files changed, 129 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb106d8..4ab72d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/tests/audio_engines.mjs b/tests/audio_engines.mjs index 34a4170..475cba9 100644 --- a/tests/audio_engines.mjs +++ b/tests/audio_engines.mjs @@ -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. diff --git a/tests/tts_setup_api.mjs b/tests/tts_setup_api.mjs index 1955d98..093536e 100644 --- a/tests/tts_setup_api.mjs +++ b/tests/tts_setup_api.mjs @@ -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 }); diff --git a/web/modules/audio_engines.js b/web/modules/audio_engines.js index b2aaed4..d030b70 100644 --- a/web/modules/audio_engines.js +++ b/web/modules/audio_engines.js @@ -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, diff --git a/web/modules/locales.js b/web/modules/locales.js index fdf2ad7..2e04096 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -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", diff --git a/web/modules/tts_import_groups.js b/web/modules/tts_import_groups.js index 4ec83fb..f688cc3 100644 --- a/web/modules/tts_import_groups.js +++ b/web/modules/tts_import_groups.js @@ -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 || '' } }, diff --git a/web/modules/tts_setup_api.js b/web/modules/tts_setup_api.js index 76f6db9..3b850fe 100644 --- a/web/modules/tts_setup_api.js +++ b/web/modules/tts_setup_api.js @@ -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; diff --git a/web/modules/ui_audio_sidebar.js b/web/modules/ui_audio_sidebar.js index 39427b7..d9af77d 100644 --- a/web/modules/ui_audio_sidebar.js +++ b/web/modules/ui_audio_sidebar.js @@ -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) })); } diff --git a/web/modules/ui_audio_studio.js b/web/modules/ui_audio_studio.js index d91eff4..cbdccf0 100644 --- a/web/modules/ui_audio_studio.js +++ b/web/modules/ui_audio_studio.js @@ -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'; diff --git a/web/modules/ui_tts_import_draft.js b/web/modules/ui_tts_import_draft.js index 5d97809..c334f86 100644 --- a/web/modules/ui_tts_import_draft.js +++ b/web/modules/ui_tts_import_draft.js @@ -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)), diff --git a/web/modules/ui_tts_setup.js b/web/modules/ui_tts_setup.js index 4182294..8bd03c0 100644 --- a/web/modules/ui_tts_setup.js +++ b/web/modules/ui_tts_setup.js @@ -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(); diff --git a/web/styles.css b/web/styles.css index 5a40bb7..cb67586 100644 --- a/web/styles.css +++ b/web/styles.css @@ -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"); diff --git a/web/styles/11-audio-studio.css b/web/styles/11-audio-studio.css index 6094259..b5f95d6 100644 --- a/web/styles/11-audio-studio.css +++ b/web/styles/11-audio-studio.css @@ -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;