Split the book into narration/dialogue spans from its own quotation marks and ask the LLM only who speaks each dialogue span (POST /api/attribute-spans), so text can no longer be fused, dropped or duplicated and a novel takes ~10 min instead of ~4 h. Book 1 vs the hand-corrected reference: every-word-right 87.0% -> 92.3%, type agreement 93.6% -> 97.4%; Books 2/3 (not tuned on) 90.8% / 89.9%. - static/js/audiobook-spans.js (pure, unit-tested) + audiobook-cast-spans.js - core/attribution_spans.py, route, tests; scripts/score_attribution.py - default on; localStorage ab-span-mode=0 restores the classic engine - architecture/audiobook-attribution.md, Manual, CHANGELOG 1.32.0, TODO Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
34 lines
1.8 KiB
JavaScript
34 lines
1.8 KiB
JavaScript
// @ts-check
|
|
// Browser-level checks for span-mode casting: the modules load with the audiobook
|
|
// section, the pure segmenter works in the real page, and the engine switch honours
|
|
// localStorage. The LLM-driven flow itself is covered by the unit tests and the
|
|
// scored regression runs (architecture/audiobook-attribution.md).
|
|
const { test, expect } = require('@playwright/test');
|
|
|
|
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.audiobookCastSpans === 'function', null, { timeout: 15_000 });
|
|
}
|
|
|
|
test('span-mode modules load and segment quoted speech in the page', async ({ page }) => {
|
|
await openReader(page);
|
|
const out = await page.evaluate(() => {
|
|
const filler = Array.from({ length: 6 }, (_, i) => `»Zeile ${i} ist ein Satz.« sagte er.`).join(' ');
|
|
const spans = window.TTVC.audiobook.spans.segmentByQuotes(`${filler} Der Zwerg schneuzte sich. »Na los!« Er ging.`);
|
|
return spans.slice(-3).map((s) => `${s.kind}:${s.text}`);
|
|
});
|
|
expect(out.some((s) => s === 'dialogue:Na los!')).toBe(true);
|
|
expect(out[out.length - 1].startsWith('narration:')).toBe(true);
|
|
});
|
|
|
|
test('span mode is on by default and can be switched off per browser', async ({ page }) => {
|
|
await openReader(page);
|
|
expect(await page.evaluate(() => window.audiobookSpanModeEnabled())).toBe(true);
|
|
await page.evaluate(() => localStorage.setItem('ab-span-mode', '0'));
|
|
expect(await page.evaluate(() => window.audiobookSpanModeEnabled())).toBe(false);
|
|
await page.evaluate(() => localStorage.removeItem('ab-span-mode'));
|
|
});
|