import { chromium } from '@playwright/test'; const base = process.env.TTSC_URL || 'http://localhost:7890'; const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROMIUM_PATH || '/snap/bin/chromium' }); const page = await browser.newPage(); page.on('console', msg => { if (msg.type() === 'error') console.error('[browser]', msg.text()); }); await page.goto(base + '/#s-library', { waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => typeof window.clGetAll === 'function' && typeof window._charAutoDesignVoice === 'function', null, { timeout: 30000 }); await page.evaluate(async () => { if (typeof window.loadVoiceLibrary === 'function') await window.loadVoiceLibrary(); }); const targets = await page.evaluate(async () => { const books = new Set([ 'Hohlbein & Hennen - DSA - Das Jahr des Greifen 01 - Der Sturm', 'Hohlbein & Hennen - DSA - Das Jahr des Greifen 02 - Die Entdeckung', 'Hohlbein & Hennen - DSA - Das Jahr des Greifen 03 - Die Amazone', ]); const chars = await window.clGetAll(); const voices = window._voices || []; const ratings = new Map(voices.map(v => [v.id, Number(v.rating || 0)])); return chars.filter(c => books.has(c.book) && c.voice && ratings.get(typeof c.voice === 'object' ? c.voice.id : c.voice) >= 1 && ratings.get(typeof c.voice === 'object' ? c.voice.id : c.voice) <= 2) .map(c => { const voiceId = typeof c.voice === 'object' ? c.voice.id : c.voice; const voice = voices.find(v => v.id === voiceId) || {}; return { id: c.id, book: c.book, name: c.name, rating: ratings.get(voiceId), feedback: String(voice.note || '').trim() }; }); }); console.log(`Quality pass starting for ${targets.length} voices`); let done = 0; for (const target of targets) { const result = await page.evaluate(async (target) => { const all = await window.clGetAll(); const rec = all.find(c => c.id === target.id); if (!rec) throw new Error('Character no longer exists'); const sheet = rec.sheet || {}; const sample = [sheet.physical, sheet.backstory, sheet.motivation].filter(Boolean).join(' '); const language = typeof window.detectLang === 'function' && sample ? (window.detectLang(sample) || '') : ''; const llm = typeof window.statusLlmTarget === 'function' ? window.statusLlmTarget() : { url: '', model: '' }; const promptResponse = await fetch('/api/character-generate-prompts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: rec.name, book: rec.book || '', sheet, voice_feedback: target.feedback || '', language, llm_url: llm.url, model: llm.model, fields: ['voice_design_prompt'] }), }); if (!promptResponse.ok) throw new Error('Prompt generation: ' + promptResponse.status + ' ' + promptResponse.statusText); const promptData = await promptResponse.json(); const prompt = String(promptData.voice_design_prompt || '').trim(); if (!prompt) throw new Error('Empty regenerated prompt'); rec.sheet = rec.sheet || {}; rec.sheet.voice_design_prompt = prompt; rec.updated = new Date(); await window.clPut(rec); await window._charAutoDesignVoice(rec, true, prompt); return { name: rec.name, prompt }; }, target); done += 1; console.log(`[${done}/${targets.length}] ${target.rating}★ ${target.book} — ${result.name}`); } console.log('Quality pass complete'); await page.evaluate(async () => { const r = await fetch('/api/tts/restart', { method: 'POST' }); if (!r.ok) throw new Error('TTS restart failed: ' + r.status); }); console.log('TTS backend restarted; redesigned audio cache flushed'); await browser.close();