SOURCE / PINNED RELEASE
Made of little things.
Pursuit PIT
- Release
- 565f4981bdc3…
- Author-recorded commit
- 197afdbed44b…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-951414e7fd7
Archive hash verified: 3b0485589942fd5c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Checks the radio clips are pronounced properly: decodes each clip exactly as the game stores
// it (8 kHz ADPCM), has Whisper (whisper.cpp, offline speech recognition) transcribe it, and
// compares the words with the script. A mispronounced word usually gets misheard.
//
// bun tools/voices/verify.ts
//
// Needs ~/.cache/whisper-dl/bin/Release/whisper-cli.exe (github.com/ggml-org/whisper.cpp
// releases) and ~/.cache/whisper-dl/ggml-small.en.bin (huggingface.co/ggerganov/whisper.cpp).
import { mkdirSync, readFileSync, rmSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { decodeAdpcm } from '../../src/game/adpcm.ts';
import index from '../../src/game/radio-index.json';
import { CLIPS } from '../../src/game/radio-script.ts';
import { transcribe, wordErrors, words, writeWav16k } from './asr.ts';
const work = resolve('tools/voices/.verify');
rmSync(work, { recursive: true, force: true });
mkdirSync(work, { recursive: true });
const bytes = new Uint8Array(readFileSync('src/assets/radio-voices.bin'));
const clips = index.clips as Record<string, number[]>;
// A line added to the script but not yet rendered plays as silence in the game.
const unrecorded = Object.keys(CLIPS).filter((key) => !clips[key]);
if (unrecorded.length) {
console.log(`✗ ${unrecorded.length} clip(s) in radio-script.ts have no recording; run bun tools/voices/build.ts:`);
console.log(' ' + unrecorded.join(' '));
process.exit(1);
}
let totalWords = 0, totalErrors = 0;
const problems: string[] = [];
for (const [key, clip] of Object.entries(CLIPS)) {
const [offset, count] = clips[key];
const file = join(work, key.replace(/[^a-z0-9.]+/gi, '_') + '.wav');
writeWav16k(file, decodeAdpcm(bytes, offset, count), index.rate);
const heard = transcribe(file);
const want = words(clip.show ?? clip.say);
const got = words(heard);
const errors = wordErrors(want, got);
totalWords += want.length;
totalErrors += errors;
console.log(`${errors ? '✗' : '✓'} ${key.padEnd(13)} ${heard}`);
if (errors) problems.push(`${key}: expected "${want.join(' ')}"\n${' '.repeat(key.length + 2)}heard "${got.join(' ')}"`);
}
rmSync(work, { recursive: true, force: true });
console.log(`\n${Object.keys(CLIPS).length} clips, ${totalWords} words, ${totalErrors} word differences (${((totalErrors / totalWords) * 100).toFixed(1)}%)`);
if (problems.length) console.log('\nDifferences to review:\n' + problems.join('\n'));
