Files
DemonGatanjieu-Anomalous_Mo…/tests/audio_engines.mjs
T
DemonGatanjieuandClaude Opus 5.5 a3ce3cae94 Reference audio editor: every clip listed, main and auxiliary references
"Reference audio and emotions" replaces Edit emotions: a large window with
the voices on top (main voice, emotions, new emotion) and every clip of the
character below, one row each (listen, file and length, its line editable
in place, where else it is used, Main / Aux for the voice picked), with
filters, search and paging. Auxiliary references (timbre averaged in, v1 /
v2 / v2Pro), fixed lines (`lines`) and lengths need Anomalous_TTS
interface 18. tts_voice_refs.js holds the rules; the old clip search and
merge helper go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:52:19 +08:00

63 lines
3.2 KiB
JavaScript

// Run: node tests/audio_engines.mjs (fetch is stubbed; no ComfyUI needed)
import assert from 'node:assert/strict';
import { fileURLToPath, pathToFileURL } from 'node:url';
import path from 'node:path';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const calls = [];
const character = {
name: '阿罗娜/日配', aliases: ['阿罗娜'], language: 'ja', settings: { format: 1, keep: 1 },
counts: { gpt: 1, sovits: 1, audio: 2 }, version: 'v4', needs: ['v4'],
reference: { audio: 'ref/a.wav', text: 'こんにちは', language: 'ja', source: 'auto' },
emotions: { 开心: { audio: 'ref/a.开心.wav', text: '', language: 'ja', source: 'filename' } },
};
globalThis.fetch = async (url) => {
calls.push(url);
const json = (data, status = 200) => ({ ok: status < 400, status, json: async () => data });
if (url.startsWith('/object_info/')) {
const cls = decodeURIComponent(url.slice('/object_info/'.length));
return json(cls === 'AnomalousTTS_CharacterSpeech' ? { [cls]: {} } : {});
}
if (url.startsWith('/anomalous_tts/characters?name=')) {
return json({ format: 2, character: { ...character, audio: ['ref/a.wav', 'ref/a.开心.wav'] } });
}
if (url.startsWith('/anomalous_tts/characters')) return json({ format: 2, characters: [character, { name: 'broken', error: 'x' }] });
return json(null, 404);
};
const engines = await import(pathToFileURL(path.join(root, 'web/modules/audio_engines.js')).href);
// Summary list → voice groups; main first, filename emotions kept.
const first = await engines.loadVoices();
assert.equal(first.installed, true);
assert.equal(first.error, '');
const arona = first.groups.find(g => g.character === '阿罗娜/日配');
assert.deepEqual(arona.slices.map(s => s.emotion), ['main', '开心']);
assert.equal(arona.has_main, true);
assert.equal(arona.model_version, 'v4'); // interface 15; older nodes give ''
assert.deepEqual(arona.needs, ['v4']);
assert.deepEqual(first.groups.find(g => g.character === 'broken').needs, []);
assert.equal(first.groups.find(g => g.character === 'broken').has_main, false);
// Re-rendering (sidebar click, studio + sidebar together) does not hit the server again.
const before = calls.length;
await Promise.all([engines.loadVoices(), engines.loadVoices(), engines.isTtsInstalled()]);
assert.equal(calls.length, before, `unexpected requests: ${calls.slice(before)}`);
assert.equal(await engines.isTtsInstalled(), true);
assert.ok(!calls.some(url => url.includes('F5TTS')), 'no F5-TTS probe');
// Refresh asks the node to rescan once; later loads use its cache again.
engines.invalidateEngineCache({ rescan: true });
await engines.loadVoices();
assert.ok(calls.includes('/anomalous_tts/characters?refresh=1'));
engines.invalidateEngineCache();
await engines.loadVoices();
assert.equal(calls.at(-1), '/anomalous_tts/characters');
// File lists come from the per-character detail.
const detail = await engines.fetchGptSovitsCharacter('阿罗娜/日配');
assert.deepEqual(detail.audio, ['ref/a.wav', 'ref/a.开心.wav']);
assert.equal(calls.at(-1), `/anomalous_tts/characters?name=${encodeURIComponent('阿罗娜/日配')}`);
console.log('audio_engines: ok');