Delete a character from its card, to the Recycle Bin

Anomalous_TTS issue #2: there was no way to remove a character. Each card
now has an orange Delete character button (also on broken characters);
after a confirm, the node (interface 18, local only) moves the character's
folder to the Recycle Bin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DemonGatanjieu
2026-10-28 20:56:42 +08:00
co-authored by Claude Opus 5.5
parent 3679f42095
commit f1ae47f21a
6 changed files with 68 additions and 5 deletions
+4 -2
View File
@@ -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
+5
View File
@@ -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 (画布快速搜索)
+15
View File
@@ -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.
+6
View File
@@ -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",
+27 -3
View File
@@ -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: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h10"/><path d="M18 6h2"/><circle cx="16" cy="6" r="2"/><path d="M4 12h4"/><path d="M12 12h8"/><circle cx="10" cy="12" r="2"/><path d="M4 18h12"/><circle cx="18" cy="18" r="2"/></svg>`,
PRONOUNCE: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V5h10v2"/><path d="M9 5v14"/><path d="M7 19h4"/><path d="M15 13h6"/><path d="M18 10v9"/></svg>`,
EDIT: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>`,
TRASH: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`,
REFRESH: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><polyline points="21 3 21 9 15 9"/></svg>`,
};
@@ -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);
}
+11
View File
@@ -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;