frontend/lab.html (4937 bytes)
1 <!DOCTYPE html> 2 <meta charset="utf-8"> 3 <title>engine lab</title> 4 <body style="font:14px/1.5 ui-monospace,monospace;padding:20px;max-width:900px"> 5 <h3>Browser engine lab</h3> 6 <p>Proves the client-side pieces one at a time. Not linked from the site.</p> 7 <input type="file" id="f"> <button id="go">run on /vendor/lab/ru.mp3</button> 8 <pre id="out"></pre> 9 <script type="module"> 10 import { Media, quietestPoint } from '/engine/media.js'; 11 import { Recogniser, hasWebGpu } from '/engine/asr.js'; 12 13 const out = document.getElementById('out'); 14 const log = (...a) => { out.textContent += a.join(' ') + '\n'; }; 15 window.lab = { done: false, error: null, results: {} }; 16 17 async function run(file) { 18 try { 19 log('crossOriginIsolated:', crossOriginIsolated, '| threads:', navigator.hardwareConcurrency, 20 '| webgpu:', await hasWebGpu()); 21 let t = performance.now(); 22 const media = await Media.open(); 23 log(`ffmpeg loaded in ${((performance.now() - t) / 1000).toFixed(1)}s`); 24 25 const path = await media.mount(file, '/in'); 26 const info = await media.probe(path); 27 log('probe:', JSON.stringify(info)); 28 29 t = performance.now(); 30 const pcm = await media.pcm(path, 0, 60); 31 log(`decoded ${(pcm.length / 16000).toFixed(1)}s of audio in ${((performance.now() - t) / 1000).toFixed(2)}s;`, 32 'cut at', (quietestPoint(pcm) / 16000).toFixed(2) + 's'); 33 34 t = performance.now(); 35 const asr = await Recogniser.open((p) => { window.lab.results.download = `${p.file} ${Math.round(p.progress)}%`; }); 36 log(`model ready on ${asr.device} in ${((performance.now() - t) / 1000).toFixed(1)}s`); 37 38 const lang = new URLSearchParams(location.search).get('lang') || null; 39 t = performance.now(); 40 await asr.transcribe(pcm.subarray(0, 16000 * 20), lang, 0); 41 log(`warm-up pass (shader compile) ${((performance.now() - t) / 1000).toFixed(1)}s, language=${lang}`); 42 t = performance.now(); 43 const segments = await asr.transcribe(pcm, lang, 0); 44 const took = (performance.now() - t) / 1000; 45 log(`transcribed in ${took.toFixed(1)}s = ${(pcm.length / 16000 / took).toFixed(1)}x real time, ${segments.length} segments`); 46 for (const s of segments.slice(0, 8)) log(` ${s.start.toFixed(2)}-${s.end.toFixed(2)} ${s.text}`); 47 window.lab.results = { info, device: asr.device, took, segments }; 48 } catch (e) { 49 log('FAILED:', e?.stack || e); 50 window.lab.error = String(e?.message || e); 51 } 52 window.lab.done = true; 53 } 54 55 document.getElementById('go').onclick = async () => { 56 const blob = await (await fetch('/vendor/lab/ru.mp3')).blob(); 57 run(new File([blob], 'ru.mp3')); 58 }; 59 // The whole job: decode, transcribe, align, then both videos. 60 window.fullJob = async () => { 61 window.lab = { done: false, error: null, results: {} }; 62 out.textContent = ''; 63 try { 64 const { Job } = await import('/engine/job.js'); 65 const { detectLanguage, readBook } = await import('/engine/book.js'); 66 const get = async (n) => new File([await (await fetch('/vendor/lab/' + n)).blob()], n); 67 const audio = await get('ru.mp3'), book = await get('moskva.epub'); 68 const parsed = await readBook(book); 69 const lang = detectLanguage(parsed.paragraphs); 70 log(`book: ${parsed.paragraphs.length} paragraphs, cover ${parsed.cover ? parsed.cover.name : 'none'}, detected language ${lang}`); 71 let last = ''; 72 const job = new Job({ audio: [audio], book, language: lang, onStatus: (st) => { 73 const line = `${st.phase}: ${st.detail}`; 74 if (line !== last) { last = line; log(' ' + line, st.speed ? `(${st.speed.toFixed(1)}x)` : ''); } 75 }}); 76 let t = performance.now(); 77 const r = await job.run(); 78 log(`job done in ${((performance.now() - t) / 1000).toFixed(1)}s on ${job.device}: ${r.cues.length} cues, ` + 79 `${(r.matchRate * 100).toFixed(0)}% matched, ${r.paragraphsDropped}/${r.paragraphsDropped + r.paragraphsUsed} paragraphs dropped`); 80 log(r.srt.split('\n').slice(0, 16).join('\n')); 81 t = performance.now(); 82 const made = await job.epub(); 83 log(`epub: ${made.file.name} ${(made.file.size / 1e6).toFixed(2)} MB, ${made.located} of ${made.of} lines placed, in ${((performance.now() - t) / 1000).toFixed(1)}s`); 84 window.lab.results.epub = made.file; 85 const again = await readBook(made.file); 86 log(`epub reads back: ${again.paragraphs.length} paragraphs, same text: ${JSON.stringify(again.paragraphs) === JSON.stringify(parsed.paragraphs)}`); 87 for (const kind of ['mkv', 'mp4']) { 88 t = performance.now(); 89 const file = await job.video(kind); 90 log(`${kind}: ${file.name} ${(file.size / 1e6).toFixed(2)} MB in ${((performance.now() - t) / 1000).toFixed(1)}s`); 91 window.lab.results[kind] = file; 92 } 93 window.lab.results.srt = r.srt; 94 } catch (e) { log('FAILED:', e?.stack || e); window.lab.error = String(e?.message || e); } 95 window.lab.done = true; 96 }; 97 98 document.getElementById('f').onchange = (e) => run(e.target.files[0]); 99 </script>