diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f16466c..1cf2d14 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -376,8 +376,10 @@ covered by the `styles.css` manifest. used last). `ui_audio_studio.js` owns the Characters view: the character cards, preview playback, tag copying, dragging a card header onto a node through the shared `bindMaterialDrag` - with `targetHint`/`rejectHint` telling the user what releasing does, and each card's - "Voice-over" button (`owner.openScript(group)`). + with `targetHint`/`rejectHint` telling the user what releasing does, each card's + "Voice-over" button (`owner.openScript(group)`), and its Delete character button + (node interface 18, local only): after a confirm, `removeGptSovitsCharacter` has the node + move the character's folder to the Recycle Bin. `ui_script_page.js` owns the Voice-over view: it loads the characters and mounts the script director, or shows the install steps or the way to import one when there is nothing to voice yet. A character picked in the list speaks there; the audio diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e293b..9190bfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 📈 Anomalous Model Browser Changelog +## v1.61.1 (Delete a Character) — 2026-10-28 + +- 角色卡上加了橘色的 **删除角色** 按钮(Anomalous_TTS 议题 #2):确认以后,角色的整个文件夹(模型、参考音频、设置)移到系统回收站,需要时能从回收站还原;U 盘、网络盘上没有回收站就不删。设置坏了、读不出来的角色也能删。需要 Anomalous_TTS v1.1.1;旧版 TTS 不显示这个按钮,其他照常。 +- Character cards have an orange Delete character button (Anomalous_TTS issue #2): after a confirm, the character's whole folder (models, reference audio, settings) goes to the Recycle Bin, where it can be restored; on a drive without one nothing is deleted. Broken characters can be deleted too. Needs Anomalous_TTS v1.1.1; with an older one the button is not shown and nothing else changes. + ## v1.61.0 (Quick Search, Canvas Menu, Combos From Pictures, Tidier Model Folders, Multi-Character Voice-over) — 2026-10-25 ### 🔎 Quick search on the canvas: Alt+B (画布快速搜索) diff --git a/web/modules/audio_engines.js b/web/modules/audio_engines.js index b9404e4..075fe29 100644 --- a/web/modules/audio_engines.js +++ b/web/modules/audio_engines.js @@ -227,6 +227,21 @@ export async function removeGptSovitsClips(name, paths) { return resp.json(); } +/** Move a whole character's folder (weights, clips, settings) to the Recycle Bin through the node (interface 18). */ +export async function removeGptSovitsCharacter(name) { + const resp = await fetch('/anomalous_tts/character/remove', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ character: name }), + }); + if (!resp.ok) { + const message = await resp.text().catch(() => ''); + throw new Error(message || `HTTP ${resp.status}`); + } + invalidateEngineCache(); + return resp.json(); +} + /** * Replace a character's anomalous_tts.json through the node's API. `settings` must be the * full object (unknown fields preserved by the caller). Returns the refreshed group. diff --git a/web/modules/locales.js b/web/modules/locales.js index 505acac..7819779 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -2110,6 +2110,9 @@ export const i18n = { ttsPronounceFromMissing: '有一条只写了读法({to}),左边要写剧本里的字', ttsPronounceDuplicate: '“{from}”写了两遍,请合并成一条', ttsEditorOpen: "参考音频与情绪", + ttsCharacterRemove: "删除角色", + ttsCharacterRemoveConfirm: "把角色“{character}”整个删掉吗?\n它的文件夹(模型、参考音频、设置)会移到系统回收站,需要时可以从回收站还原。", + ttsCharacterRemoveFailed: "没有删除:{error}", ttsRefsTitle: "参考音频 · {name}", ttsRefsSubtitle: "上面选一个声音,下面给它挑参考音频:主参考决定语气和念法,副参考用来融合音色。保存后写进角色文件夹里的 anomalous_tts.json。", ttsRefsMainVoice: "主声音", @@ -4809,6 +4812,9 @@ export const i18n = { ttsPronounceFromMissing: 'A line has only a reading ({to}); write the script text on the left', ttsPronounceDuplicate: '"{from}" is listed twice; keep one line', ttsEditorOpen: "Reference audio and emotions", + ttsCharacterRemove: "Delete character", + ttsCharacterRemoveConfirm: "Delete the character \"{character}\"?\nIts folder (models, reference audio, settings) goes to the Recycle Bin, where you can restore it.", + ttsCharacterRemoveFailed: "Not deleted: {error}", ttsRefsTitle: "Reference audio · {name}", ttsRefsSubtitle: "Pick a voice on top, then its reference clips below: the main reference sets the tone and way of speaking, auxiliary ones blend in their timbre. Saved to anomalous_tts.json in the character folder.", ttsRefsMainVoice: "Main voice", diff --git a/web/modules/ui_audio_studio.js b/web/modules/ui_audio_studio.js index 87f3e89..76e186d 100644 --- a/web/modules/ui_audio_studio.js +++ b/web/modules/ui_audio_studio.js @@ -2,7 +2,8 @@ import { t } from './interface_settings.js'; import { stopGalleryAudio } from './ui_audio_gallery.js'; import { getActiveAudioFilter, renderAudioSidebar } from './ui_audio_sidebar.js'; import { bindVoiceDrag } from './audio_voice_drag.js'; -import { TTS_ENGINE, invalidateEngineCache, isTtsInstalled, loadGptSovitsStatus, loadVoices } from './audio_engines.js'; +import { TTS_ENGINE, invalidateEngineCache, isTtsInstalled, loadGptSovitsStatus, loadVoices, removeGptSovitsCharacter } from './audio_engines.js'; +import { anomalousAlert, anomalousConfirm } from './ui_dialog.js'; import { PRESETS_FORMAT } from './tts_presets_api.js'; import { openTuningEditor } from './ui_tts_tuning.js'; import { openGptSovitsEditor } from './ui_audio_tts_editor.js'; @@ -12,6 +13,9 @@ import { openTtsImport } from './ui_tts_import.js'; import { stopScriptDirectorPreview } from './ui_script_director.js'; import { renderVoiceTabs } from './ui_voice_tabs.js'; +// The node interface that removes a whole character (POST /anomalous_tts/character/remove). +const REMOVE_CHARACTER_FORMAT = 18; + /** * The Voices page's Characters view: GPT-SoVITS character cards (Anomalous_TTS, detected * at runtime), preview playback, tag copying, dropping a character onto a TTS node, the @@ -37,6 +41,7 @@ const SVG = { TUNE: ``, PRONOUNCE: ``, EDIT: ``, + TRASH: ``, REFRESH: ``, }; @@ -192,7 +197,7 @@ function renderSliceRow(slice, withTag) { return row; } -function renderCharacterCard(group, owner, { onChanged, canImport = false, tuning = false } = {}) { +function renderCharacterCard(group, owner, { onChanged, canImport = false, tuning = false, removable = false } = {}) { const card = document.createElement('div'); card.className = 'anomalous-character-voice-card'; if (canImport && !group.error) card.dataset.ttsCharacter = group.character; // file drop target @@ -262,6 +267,12 @@ function renderCharacterCard(group, owner, { onChanged, canImport = false, tunin headerRight.append(add); } } + // Also on a broken character: those are the ones most often thrown away. + if (removable) { + const remove = createToolButton(SVG.TRASH, t('ttsCharacterRemove'), 'is-compact is-danger'); + remove.onclick = () => requestRemoveCharacter(group, onChanged); + headerRight.append(remove); + } headerRight.append(countBadge); header.append(titleGroup, headerRight); if (group.has_main) { @@ -287,6 +298,18 @@ function renderCharacterCard(group, owner, { onChanged, canImport = false, tunin return card; } +/** Asks, then moves the character's folder to the Recycle Bin and redraws. */ +async function requestRemoveCharacter(group, onChanged) { + if (!await anomalousConfirm(t('ttsCharacterRemoveConfirm', { character: group.character }))) return; + try { + await removeGptSovitsCharacter(group.character); + } catch (e) { + await anomalousAlert(t('ttsCharacterRemoveFailed', { error: e.message })); + return; + } + onChanged?.(); +} + // ---- Toolbar ---- function createToolButton(iconSvg, label, className = '') { @@ -467,6 +490,7 @@ export async function renderAudioStudio(container, { filter = null, owner = null return; } const tuning = Number(ttsStatus?.format) >= PRESETS_FORMAT; // generation settings: Anomalous_TTS 1.1.0+ - characters.forEach(group => grid.appendChild(renderCharacterCard(group, owner, { onChanged, canImport, tuning }))); + const removable = canImport && Number(ttsStatus?.format) >= REMOVE_CHARACTER_FORMAT; + characters.forEach(group => grid.appendChild(renderCharacterCard(group, owner, { onChanged, canImport, tuning, removable }))); studioWrapper.lastChild.replaceWith(grid); } diff --git a/web/styles/11-audio-studio.css b/web/styles/11-audio-studio.css index a2ccd7d..9a6a09c 100644 --- a/web/styles/11-audio-studio.css +++ b/web/styles/11-audio-studio.css @@ -1255,6 +1255,17 @@ font-size: 11px; } +/* Delete character: orange, so it is found and not pressed by accident. */ +.anomalous-audio-tool-btn.is-danger { + color: var(--amb-warn); + border-color: color-mix(in srgb, var(--amb-warn) 45%, transparent); +} + +.anomalous-audio-tool-btn.is-danger:hover { + color: var(--amb-warn-text); + background: color-mix(in srgb, var(--amb-warn) 14%, transparent); +} + /* ---------- GPT-SoVITS editors (pronunciation, tuning) ---------- */ .anomalous-tts-editor { width: 680px;