// End-to-end test of LIVE transcription (record --transcribe), no browser: // // 1. Start the recorder with --transcribe --model tiny in a random room. // 2. Start a test speaker that plays the 11 s JFK sample into the room. // 3. Check transcript.md appears (and has content) WHILE still recording. // 4. SIGINT the recorder; verify the final transcript has the speaker, the // right words, the chat line, and that the asr/ cache was written. // // Needs network (relays; sample + model download on first run). import {spawn, type ChildProcess} from 'node:child_process' import * as fs from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import {randomBytes} from 'node:crypto' const JFK_URL = 'https://raw.githubusercontent.com/ggml-org/whisper.cpp/master/samples/jfk.wav' const SPEAK_SEC = 25 // after first connect: 11 s of speech, then silence const room = `livetest-${randomBytes(4).toString('hex')}` const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'commonroom-live-test-')) const dist = path.join(import.meta.dirname, '..') const failures: string[] = [] const check = (ok: boolean, what: string) => { process.stdout.write(`${ok ? 'PASS' : 'FAIL'}: ${what}\n`) if (!ok) failures.push(what) } const run = (cmd: string[], label: string): ChildProcess => { const proc = spawn('node', cmd, {stdio: ['ignore', 'pipe', 'pipe']}) proc.stdout!.on('data', d => process.stdout.write(String(d).replace(/^/gm, ` ${label} | `)) ) proc.stderr!.on('data', d => process.stdout.write(String(d).replace(/^/gm, ` ${label} ! `)) ) return proc } const wait = (ms: number) => new Promise(r => setTimeout(r, ms)) const exited = (proc: ChildProcess, timeoutMs: number): Promise => new Promise(resolve => { if (proc.exitCode !== null) return resolve(true) const t = setTimeout(() => { proc.kill('SIGKILL') resolve(false) }, timeoutMs) proc.on('exit', () => { clearTimeout(t) resolve(true) }) }) const main = async () => { const jfkPath = path.join(os.tmpdir(), 'commonroom-recorder-jfk-sample.wav') if (!fs.existsSync(jfkPath)) { process.stdout.write(`downloading ${JFK_URL}\n`) const res = await fetch(JFK_URL) if (!res.ok) throw new Error(`sample download failed: ${res.status}`) fs.writeFileSync(jfkPath, Buffer.from(await res.arrayBuffer())) } process.stdout.write(`room: ${room}\nout: ${outDir}\n\n`) const recorder = run( [ path.join(dist, 'cli.js'), 'record', room, '--out', outDir, '--transcribe', '--model', 'tiny', '--notice', 'recording test' ], 'rec' ) await wait(3000) const speaker = run( [ path.join(dist, 'test/speaker.js'), room, '--wav', jfkPath, '--duration', String(SPEAK_SEC), '--chat', 'live transcription test chat' ], 'spk' ) // The speaker exits SPEAK_SEC after its first connection (plus connect // time); poll for the live transcript while it runs. const mdPath = path.join(outDir, 'transcript.md') let liveSeen = false const speakerDone = exited(speaker, 180000) let done = false void speakerDone.then(() => (done = true)) while (!done) { if (!liveSeen && fs.existsSync(mdPath)) { const md = fs.readFileSync(mdPath, 'utf8') // A SPEECH turn (bold "**[hh:mm:ss] Name:**"), not the chat line. if (/^\*\*\[\d\d:\d\d:\d\d\] TestSpeaker:\*\*/m.test(md)) { liveSeen = true process.stdout.write(' (live transcript has speech — still recording)\n') } } await wait(1000) } check(await speakerDone, 'speaker ran and exited') check(liveSeen, 'transcript.md grew DURING the recording') await wait(1500) recorder.kill('SIGINT') check(await exited(recorder, 120000), 'recorder exited cleanly on SIGINT') check(fs.existsSync(mdPath), 'transcript.md written') const md = fs.readFileSync(mdPath, 'utf8') check( /^\*\*\[\d\d:\d\d:\d\d\] TestSpeaker:\*\*/m.test(md), 'speaker has a speech turn in transcript' ) check(/your country/i.test(md), 'JFK words transcribed') check(md.includes('live transcription test chat'), 'chat line in transcript') check(!md.includes('(in progress)'), 'final render has an end time') const json = JSON.parse(fs.readFileSync(path.join(outDir, 'transcript.json'), 'utf8')) check( Array.isArray(json.items) && json.items.some((i: {type: string}) => i.type === 'speech'), 'transcript.json has speech items' ) const asrFiles = fs.existsSync(path.join(outDir, 'asr')) ? fs.readdirSync(path.join(outDir, 'asr')) : [] check(asrFiles.length >= 1, `asr/ cache written (${asrFiles.length} file(s))`) process.stdout.write( failures.length === 0 ? `\nALL PASS (output kept in ${outDir})\n` : `\n${failures.length} FAILURE(S):\n${failures.map(f => ` - ${f}`).join('\n')}\n(output kept in ${outDir})\n` ) process.exit(failures.length === 0 ? 0 : 1) } main().catch(err => { process.stderr.write(`live-loopback fatal: ${err?.stack ?? err}\n`) process.exit(1) })