// Test of the transcribe subcommand with real speech, no room needed: builds // a fabricated recording directory whose two "speakers" are both the classic // 11 s JFK sample (downloaded once into the OS temp dir), offset in time, // with a chat message and join events between them — then checks the merged // transcript has the right speakers, ordering, and words. // // Needs network (sample + model download on first run) and an ASR engine // (faster-whisper etc.). Uses --model tiny to keep the download/compute small. import {spawnSync} from 'node:child_process' import * as fs from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' const JFK_URL = 'https://raw.githubusercontent.com/ggml-org/whisper.cpp/master/samples/jfk.wav' const failures: string[] = [] const check = (ok: boolean, what: string) => { process.stdout.write(`${ok ? 'PASS' : 'FAIL'}: ${what}\n`) if (!ok) failures.push(what) } const main = async () => { // ---- fixture ---- 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())) } const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'commonroom-transcribe-test-')) fs.mkdirSync(path.join(dir, 'audio')) fs.copyFileSync(jfkPath, path.join(dir, 'audio', 'Alice-aaaa1111-seg1.wav')) fs.copyFileSync(jfkPath, path.join(dir, 'audio', 'Bob-bbbb2222-seg1.wav')) const t0 = Date.parse('2026-07-23T14:00:00.000Z') const iso = (offsetSec: number) => new Date(t0 + offsetSec * 1000).toISOString() const manifest = { room: 'transcribe-test', recorder: {peerId: 'f'.repeat(64), name: 'Recorder'}, startedAt: iso(0), endedAt: iso(40), participants: {['a'.repeat(64)]: 'Alice', ['b'.repeat(64)]: 'Bob'}, segments: [ { file: 'audio/Alice-aaaa1111-seg1.wav', peerId: 'a'.repeat(64), name: 'Alice', startedAt: iso(2), endedAt: iso(13), durationSec: 11, sampleRate: 16000, channels: 1 }, { file: 'audio/Bob-bbbb2222-seg1.wav', peerId: 'b'.repeat(64), name: 'Bob', startedAt: iso(20), endedAt: iso(31), durationSec: 11, sampleRate: 16000, channels: 1 } ] } fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2)) const events = [ {time: iso(1), type: 'join', peerId: 'a'.repeat(64), name: 'Alice', alreadyHere: true}, {time: iso(15), type: 'chat', peerId: 'a'.repeat(64), name: 'Alice', text: 'over to you, Bob'}, {time: iso(19), type: 'join', peerId: 'b'.repeat(64), name: 'Bob', alreadyHere: false}, {time: iso(35), type: 'left', peerId: 'b'.repeat(64), name: 'Bob'} ] fs.writeFileSync( path.join(dir, 'events.jsonl'), events.map(e => JSON.stringify(e)).join('\n') + '\n' ) // ---- run transcribe ---- const cli = path.join(import.meta.dirname, '..', 'cli.js') const run1 = spawnSync('node', [cli, 'transcribe', dir, '--model', 'tiny'], { encoding: 'utf8', timeout: 600000 }) process.stdout.write(run1.stdout + run1.stderr) check(run1.status === 0, 'transcribe exited 0') const mdPath = path.join(dir, 'transcript.md') check(fs.existsSync(mdPath), 'transcript.md written') const md = fs.readFileSync(mdPath, 'utf8') const aliceIdx = md.indexOf('Alice:** ') const chatIdx = md.indexOf('over to you, Bob') const bobIdx = md.indexOf('Bob:** ') check(aliceIdx !== -1, 'Alice has a speech turn') check(bobIdx !== -1, 'Bob has a speech turn') check(chatIdx !== -1, 'chat message in transcript') check( aliceIdx < chatIdx && chatIdx < bobIdx, 'ordering: Alice speech < chat < Bob speech' ) const countryCount = (md.match(/your country/gi) ?? []).length check(countryCount >= 2, `both clips transcribed ("your country" x${countryCount})`) check(md.includes('Alice was already here'), 'join (already here) event rendered') check(md.includes('Bob joined'), 'join event rendered') check(md.includes('Bob left'), 'left event rendered') check(md.includes('# Transcript: transcribe-test'), 'header present') check(fs.existsSync(path.join(dir, 'transcript.json')), 'transcript.json written') check( fs.existsSync(path.join(dir, 'asr', 'Alice-aaaa1111-seg1.json')), 'ASR cache written' ) // ---- second run must reuse the cache ---- const run2 = spawnSync('node', [cli, 'transcribe', dir, '--model', 'tiny'], { encoding: 'utf8', timeout: 60000 }) check( run2.status === 0 && run2.stdout.includes('cached'), 'second run reuses the ASR cache' ) process.stdout.write( failures.length === 0 ? `\nALL PASS (output kept in ${dir})\n` : `\n${failures.length} FAILURE(S):\n${failures.map(f => ` - ${f}`).join('\n')}\n(output kept in ${dir})\n` ) process.exit(failures.length === 0 ? 0 : 1) } main().catch(err => { process.stderr.write(`transcribe-test fatal: ${err?.stack ?? err}\n`) process.exit(1) })