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:
co-authored by
Claude Opus 5.5
parent
a3ce3cae94
commit
596ac9728d
+3
-1
@@ -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
@@ -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.
|
||||
|
||||
@@ -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`。
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user