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;