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:
co-authored by
Claude Opus 5.5
parent
cdc8ad2f57
commit
6996f07063
@@ -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).
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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 || '' } },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) }));
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user