Reference audio editor: move a clip to the Recycle Bin from its row

🗑 on a row asks first (saying where the clip is used), then has the node
move it with its own .txt / .lab to the Recycle Bin (never a permanent
delete) and take it out of the settings; the window drops it from its
voices too. Needs Anomalous_TTS interface 18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DemonGatanjieu
2026-10-20 13:38:03 +08:00
co-authored by Claude Opus 5.5
parent a3ce3cae94
commit 596ac9728d
8 changed files with 108 additions and 8 deletions
+3 -1
View File
@@ -302,7 +302,9 @@ covered by the `styles.css` manifest.
line editable in place, where else it is used, Main / Aux for the voice picked), with filters,
a search and the first 60 rows then more. It opens at once and loads the character's clips,
their lines and lengths (`/anomalous_tts/reference_lines`) in the background (save waits for
them), and keeps settings fields it does not know. `tts_voice_refs.js` holds its rules without
them), and keeps settings fields it does not know; 🗑 on a row moves that clip to the
Recycle Bin through the node (`removeGptSovitsClips`, which also takes it out of the saved
settings) and out of the window's voices. `tts_voice_refs.js` holds its rules without
DOM: the voices read from the settings and the node's view (an emotion found from a file
name is written only once changed), one main reference and any number of auxiliary ones per
voice, the lines fixed here (`lines`; a voice's own `text` moves there), what keeps it from
+2 -1
View File
@@ -65,8 +65,9 @@
- **Reference audio and emotions** (the character card's button, formerly *Edit emotions*) is now a large window. On top, the **voices**: the main voice, each emotion with how many references it has (*1 main, 2 aux*), and **+ New emotion**. Below, **every clip** of the character, one row each: ▶ to hear it, its file and length, its line, and where else it is used (*Main voice · aux*). For the voice picked, a row is its **Main** reference or one of its **Aux** references, or nothing; unused clips just stay in the list. Filters (*All*, *This voice*, *In use*, *No line*) and a search by line or file name; the first 60 rows, then more.
- **Auxiliary references (副参考音频)**: a voice keeps one main reference (its tone and way of speaking, 3–10 s, with a line) and can take any number of auxiliary ones, whose timbre is averaged in when it speaks, as GPT-SoVITS does with several references; they need no line. Only v1 / v2 / v2Pro models use them (the controls say so for v3 / v4).
- **A clip's line can be fixed right in its row** (改参考台词): the fixed line is kept in the character's settings and used before its .txt / .list files, which stay as they are. A line that is missing shows *No line found: click to type it*.
- **🗑 on a row moves that clip to the Recycle Bin** (删除参考音频), with its own .txt / .lab, after saying where it is used; it is taken out of those voices and the settings. Never a permanent delete: where there is no Recycle Bin (a USB stick, a network drive) nothing is deleted and the window says so.
- Clips too long or too short to be a main reference are marked (they can still be auxiliary). The main voice can go back to the node's own pick; an emotion can be renamed or deleted (one found from a file name is changed by renaming the file). Closing with unsaved changes asks first.
- Auxiliary references, fixed lines and lengths need Anomalous_TTS interface 18 (1.1.0); with an older version the window works as before for main references and says what needs the update.
- Auxiliary references, fixed lines, lengths and 🗑 need Anomalous_TTS interface 18 (1.1.0); with an older version the window works as before for main references and says what needs the update.
### 🔍 Scanning (扫描)
- **The scan page opens at once, also with many models**: it shows the last counts right away, says it is updating them, and shows the fresh ones when they come. What each model file's header says is now kept on disk (`user/anomalous/model_heads.json`, by file, read again only when the file changes), so a big library or a network drive is not read again after every restart either; requests that come while the page counts share that one count.
+3 -2
View File
@@ -214,9 +214,10 @@
3. **主参考**:决定这个声音的语气和念法。每个声音一条,要 3~10 秒、有台词(太长太短的会标黄,不能当主参考)。主声音也可以 **改回自动挑**,让节点自己选。
4. **副参考**:只用来融合音色,让声音更稳、更像本人。可以勾好几条,不需要台词,建议同一个人、音质干净的 2~4 条。只有 v1 / v2 / v2Pro 模型用得上,v3 / v4 的角色这一列是灰的。
5. **改台词**:直接点一行的台词就能改;找不到台词的会写“没找到台词,点这里填”。改过的台词存在角色的设置里,比 `.txt` / 标注文件优先,原来的文件不动。清空就换回文件里的台词。
6. 点 **保存**。设置会写进角色文件夹里的 `anomalous_tts.json`,由节点检查后保存。有问题(比如某个情绪还没选主参考)会提示,什么都不会改。没保存就关窗口会先问一句。
6. **删除**:点一行最右边的 🗑,确认后这条音频和它同名的 `.txt` / `.lab` 一起**移到系统回收站**(随时能还原,从不永久删除;U 盘、网络盘上没有回收站就不删,会说明)。它用在哪些声音里,确认框里会写,删掉后自动从这些声音和设置里去掉。
7. 点 **保存**。设置会写进角色文件夹里的 `anomalous_tts.json`,由节点检查后保存。有问题(比如某个情绪还没选主参考)会提示,什么都不会改。没保存就关窗口会先问一句。
副参考、改台词和音频长度需要 Anomalous_TTS 接口版本 18(1.1.0);旧版本只能选主参考,窗口里会说明。
副参考、改台词、音频长度和删除需要 Anomalous_TTS 接口版本 18(1.1.0);旧版本只能选主参考,窗口里会说明。
文件名本身带情绪的音频(例如 `Arona_Talk.开心.wav`)会被自动识别,不用再填一遍。只是扩展名的部分不算情绪,例如 `Arona_Talk.ogg.wav`、`Arona_Talk.ogg (1).ogg`。
+19
View File
@@ -208,6 +208,25 @@ export async function fetchGptSovitsDetail(name, signal) {
// ---------- GPT-SoVITS settings (Anomalous writes, the node reads) ----------
/**
* Move a character's reference clips (and their own .txt / .lab) to the Recycle Bin through
* the node (interface 18); it also takes them out of the settings. Returns the node's answer
* (`removed`, `text_files`, `character`: the detail after).
*/
export async function removeGptSovitsClips(name, paths) {
const resp = await fetch('/anomalous_tts/audio/remove', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ character: name, paths }),
});
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.
+10
View File
@@ -2154,6 +2154,11 @@ export const i18n = {
ttsRefsShowMore: "再显示一些(还有 {count} 条)",
ttsRefsNoMain: "情绪“{name}”还没选主参考。",
ttsRefsCloseUnsaved: "有改动还没保存,确定关闭吗?",
ttsRefsRemove: "移到回收站",
ttsRefsRemoveConfirm: "把 {file} 移到回收站?它同名的台词文件(.txt / .lab)也会一起移走,需要时可以从回收站还原。",
ttsRefsRemoveUses: "它现在是 {uses} 的参考,会从这些声音里去掉。",
ttsRefsRemoveFailed: "没有删除:{error}",
ttsRefsRemoved: "已把 {file} 移到回收站。这个角色还有 {count} 条音频。",
ttsEditorRemove: "删除这一行",
ttsEditorSave: "保存",
ttsEditorSaveFailed: "保存失败:{error}",
@@ -4846,6 +4851,11 @@ export const i18n = {
ttsRefsShowMore: "Show more ({count} left)",
ttsRefsNoMain: "The emotion “{name}” has no main reference yet.",
ttsRefsCloseUnsaved: "There are unsaved changes. Close anyway?",
ttsRefsRemove: "Move to the Recycle Bin",
ttsRefsRemoveConfirm: "Move {file} to the Recycle Bin? Its own line file (.txt / .lab) goes with it; both can be restored from the Recycle Bin.",
ttsRefsRemoveUses: "It is now a reference of {uses}, and is taken out of those voices.",
ttsRefsRemoveFailed: "Nothing was deleted: {error}",
ttsRefsRemoved: "{file} is in the Recycle Bin. {count} audio files left in this character.",
ttsEditorRemove: "Remove this row",
ttsEditorSave: "Save",
ttsEditorSaveFailed: "Could not save: {error}",
+47 -3
View File
@@ -1,7 +1,7 @@
import { t } from './interface_settings.js';
import { createViewScope } from './ui_lifecycle.js';
import { anomalousAlert, anomalousConfirm, anomalousPrompt } from './ui_dialog.js';
import { fetchGptSovitsDetail, fetchReferenceClips, saveGptSovitsSettings, ttsAudioUrl } from './audio_engines.js';
import { fetchGptSovitsDetail, fetchReferenceClips, gptSovitsGroups, removeGptSovitsClips, saveGptSovitsSettings, ttsAudioUrl } from './audio_engines.js';
import {
AUX_FORMAT, AUX_VERSIONS, LINE_LIMIT, MAIN, clearMain, fitsMain, listClips, nameProblem, readVoices, roleOf,
setMain, toggleAux, usesOf, voiceCount, voicesProblem, writeSettings,
@@ -15,8 +15,10 @@ import {
* **auxiliary references** (their timbre is averaged in; any number, no line needed). A clip's
* line can be fixed right in its row. The rules are in tts_voice_refs.js; the node owns the
* file, this window only sends the new settings through its API, keeping every field it does
* not edit. Auxiliary references and fixed lines need Anomalous_TTS interface 18 (and
* auxiliary references a v1 / v2 / v2Pro model); otherwise those controls are off and say why.
* not edit. A clip can be moved to the Recycle Bin from its row (the node moves it with its own
* .txt / .lab and takes it out of the settings). Auxiliary references, fixed lines and removing
* need Anomalous_TTS interface 18 (and auxiliary references a v1 / v2 / v2Pro model); otherwise
* those controls are off or hidden and say why.
*
* The window opens at once and reads the character's clips, their lines and lengths in the
* background (a folder can hold a thousand clips: the list shows the first ones, then more).
@@ -320,10 +322,52 @@ export function openGptSovitsEditor(group, { onSaved } = {}) {
auxBtn.disabled = !current || !canAux() || role === 'main';
if (!canAux()) auxBtn.title = auxReason();
roles.append(mainBtn, auxBtn);
if (canFix()) {
roles.append(button('anomalous-tts-refs-remove', '🗑', () => removeClip(path), t('ttsRefsRemove')));
}
row.append(playBtn, file, middle, roles);
return row;
}
/** One clip to the Recycle Bin (after asking), then out of this window's voices too. */
async function removeClip(path) {
const uses = usesOf(state.voices, path).map(use => t(`ttsRefsUse_${use.role}`, { name: use.name === MAIN ? t('ttsRefsMainVoice') : use.name }));
const message = [
t('ttsRefsRemoveConfirm', { file: baseName(path) }),
uses.length ? t('ttsRefsRemoveUses', { uses: uses.join('、') }) : '',
].filter(Boolean).join('\n');
if (!await anomalousConfirm(message, t('ttsRefsRemove'))) return;
if (state.playing?.path === path) stopPlaying();
let answer;
try {
answer = await removeGptSovitsClips(name, [path]);
} catch (e) {
if (!scope.signal.aborted) await anomalousAlert(t('ttsRefsRemoveFailed', { error: e.message }));
return;
}
if (scope.signal.aborted) return;
state.audio = state.audio.filter(item => item !== path);
state.found.delete(path);
state.seconds.delete(path);
state.fixes.delete(path);
state.cleared.delete(path);
// The node took it out of the saved settings; an emotion it was the main reference of went
// there, and goes here too unless it was changed in this window (then it waits for another).
state.voices = state.voices.filter(item => !(item.main === path && item.name !== MAIN && !item.touched));
for (const item of state.voices) {
item.aux = item.aux.filter(other => other !== path);
if (item.main === path) item.main = null;
}
if (!state.voices.some(item => item.key === state.voiceKey)) state.voiceKey = 'main';
const after = answer?.character;
if (after?.settings && typeof after.settings === 'object') state.settings = after.settings;
if (after) raw.reference = after.reference;
status.textContent = t('ttsRefsRemoved', { file: baseName(path), count: state.audio.length });
order = order.filter(item => item !== path);
renderAll(false);
onSaved?.(after ? gptSovitsGroups({ characters: [after] })[0] : null);
}
function renderRow(path) {
const old = rows.get(path);
if (!old) return;
+1 -1
View File
@@ -34,4 +34,4 @@
@import url("./styles/33-kept-combos.css?v=20261019-hero");
@import url("./styles/34-voice-over-panel.css?v=20261020-layers");
@import url("./styles/35-voice-over-drafts.css?v=20261020-drafts");
@import url("./styles/36-reference-audio.css?v=20261020-refs");
@import url("./styles/36-reference-audio.css?v=20261020-remove");
+23
View File
@@ -279,6 +279,29 @@
cursor: not-allowed;
}
/* To the Recycle Bin: quiet until pointed at. */
.anomalous-tts-refs-remove {
width: 30px;
padding: 4px 0;
border: 1px solid transparent;
border-radius: 6px;
background: none;
color: var(--amb-text-dim);
font-size: 13px;
cursor: pointer;
opacity: 0.55;
}
.anomalous-tts-refs-row:hover .anomalous-tts-refs-remove {
opacity: 1;
}
.anomalous-tts-refs-remove:hover {
border-color: color-mix(in srgb, var(--amb-danger) 50%, transparent);
background: color-mix(in srgb, var(--amb-danger) 14%, transparent);
color: var(--amb-danger-text);
}
.anomalous-tts-refs-more,
.anomalous-tts-refs-empty {
display: block;