tts-voice-creator-clone-and.../tests/audiobook-emotion.spec.js
mARTin-B78 e8c0df027e test: fail any spec that would write to the live API
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 21:00:25 +02:00

66 lines
3.7 KiB
JavaScript

// @ts-check
// Audiobook emotion path: span-cast segment emotion -> script -> rehearser line -> instruct -> /api/tts-preview.
// Routes are mocked; nothing here reaches the live TTS API.
const { test, expect } = require('./_guard');
const WAV = Buffer.from('UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YQAAAAA=', 'base64');
async function openReader(page) {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 });
await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 });
await page.evaluate(() => window.navTo('s-reader'));
await page.waitForFunction(() => typeof window.audiobookBuildScript === 'function' && typeof window.fetchTtsPreviewBlob === 'function', null, { timeout: 15_000 });
}
test('span-cast emotion reaches the tts-preview instruct for dialogue only; export shares the path', async ({ page }) => {
const bodies = [];
await page.route('**/api/tts-preview', route => { bodies.push(route.request().postDataJSON()); return route.fulfill({ status: 200, contentType: 'audio/wav', body: WAV }); });
await page.route('**/api/attribute-dialogue', route => route.fulfill({ json: { segments: [] } }));
await openReader(page);
// segments exactly as audiobook-cast-spans.js builds them (emotion from the LLM, narration always '')
const res = await page.evaluate(async () => {
const segs = [
{ speaker: 'Narrator', type: 'narration', text: 'Der Zwerg schneuzte sich.', emotion: '', spanId: 'N0' },
{ speaker: 'Linosch', type: 'dialogue', text: 'Na los!', emotion: 'wütend', spanId: 'D1' },
{ speaker: 'Alrik', type: 'dialogue', text: 'Vielen Dank.', emotion: '', spanId: 'D2' },
];
const { script, emotions } = window.audiobookBuildScript(segs);
rehState.lines = window.parseScript(script);
rehState.cast = { LINOSCH: { voice: 'DE_M_test', instruct: '' }, ALRIK: { voice: 'DE_M_test2', instruct: '' } };
rehState.narratorVoice = 'DE_F_narr';
let k = 0;
rehState.lines.forEach(l => { if (l.type === 'dialog') { const e = emotions[k++]; if (e) l.emotion = e; } });
const out = [];
for (const l of rehState.lines) {
const { voice, instruct } = window.audiobookLineVoice(l);
await window.fetchTtsPreviewBlob(voice, l.text, 'wav', instruct, 'voice_design');
out.push({ text: l.text, type: l.type, emotion: l.emotion || '' });
}
return { out, exportUsesIt: /audiobookLineVoice\(/.test(window.audiobookExport.toString()) };
});
expect(res.exportUsesIt).toBe(true); // the export loop builds its instruct via the same helper
expect(bodies).toHaveLength(3);
const byText = Object.fromEntries(bodies.map(b => [b.text, b.instruct]));
expect(byText['Der Zwerg schneuzte sich.']).toBe('');
expect(byText['Na los!']).toContain('angry'); // German "wütend" translated to the English tag
expect(byText['Na los!']).not.toContain('wütend');
expect(byText['Vielen Dank.']).not.toMatch(/angry/); // empty emotion: no emotion clause
});
test('changing the emotion changes the line cache key (no stale flat audio)', async ({ page }) => {
await openReader(page);
const keys = await page.evaluate(async () => {
window.rehState.cast = { LINOSCH: { voice: 'DE_M_test', instruct: '' } };
const k = async (emo) => {
const { instruct } = window.audiobookLineVoice({ type: 'dialog', speaker: 'LINOSCH', text: 'Na los!', emotion: emo });
return window._lineAudioCacheKey('Na los!', 'DE_M_test', instruct);
};
return [await k(''), await k('wütend'), await k('wütend'), await k('traurig')];
});
expect(keys[0]).not.toBe(keys[1]);
expect(keys[1]).toBe(keys[2]);
expect(keys[1]).not.toBe(keys[3]);
});