#!/usr/bin/env node // Say each language's everyday phrases (phrases.json) with every voice in the // built packs, into WAV files and a manifest, for intelligibility.py to // transcribe. Nothing is sent anywhere. // // node tools/voices/intelligibility.mjs OUTDIR [--out PACKS] [voice-or-language ...] import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; const HERE = path.dirname(fileURLToPath(import.meta.url)); const argv = process.argv.slice(2); const dir = path.resolve(argv.shift() || ''); let packs = process.env.AAC_VOICES_DIR || path.resolve(HERE, '../../../../../aac-voice-packs'); const only = []; for (let i = 0; i < argv.length; i++) { if (argv[i] === '--out') packs = path.resolve(argv[++i]); else only.push(argv[i]); } const phrases = JSON.parse(fs.readFileSync(path.join(HERE, 'phrases.json'), 'utf8')).phrases; const pointer = JSON.parse(fs.readFileSync(path.join(packs, 'index.json'), 'utf8')); const cat = JSON.parse(fs.readFileSync(path.join(packs, pointer.current), 'utf8')); const rt = Object.values(cat.runtimes)[0]; const { default: createModule } = await import(pathToFileURL(path.join(packs, rt.module)).href); const m = await createModule({ wasmBinary: fs.readFileSync(path.join(packs, rt.wasm)), print: () => {}, printErr: () => {} }); const bytes = (f) => new Uint8Array(fs.readFileSync(path.join(packs, f.url))); function wav(audio, sr) { const b = Buffer.alloc(44 + audio.length * 2); b.write('RIFF', 0); b.writeUInt32LE(36 + audio.length * 2, 4); b.write('WAVEfmt ', 8); b.writeUInt32LE(16, 16); b.writeUInt16LE(1, 20); b.writeUInt16LE(1, 22); b.writeUInt32LE(sr, 24); b.writeUInt32LE(sr * 2, 28); b.writeUInt16LE(2, 32); b.writeUInt16LE(16, 34); b.write('data', 36); b.writeUInt32LE(audio.length * 2, 40); for (let i = 0; i < audio.length; i++) b.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(audio[i] * 32767))), 44 + i * 2); return b; } fs.mkdirSync(dir, { recursive: true }); const manifest = []; for (const [code, lang] of Object.entries(cat.languages)) { if (!phrases[code]) continue; for (const pack of lang.packs) { if (only.length && !only.includes(pack.id) && !only.includes(code)) continue; const files = [...pack.requires.flatMap((c) => cat.components[c].files), ...pack.files]; const tts = new m.TextToSpeech(pack.lang_key, files.map((f) => f.name), files.map(bytes), Object.keys(pack.options), Object.values(pack.options)); phrases[code].forEach((text, i) => { const r = tts.say(text); const file = path.join(dir, `${pack.id}.${i}.wav`); fs.writeFileSync(file, wav(r.audio, r.sampleRate)); manifest.push({ pack: pack.id, lang: code, text, file, seconds: Number((r.audio.length / r.sampleRate).toFixed(2)) }); }); tts.delete?.(); console.log(`said ${phrases[code].length} phrases with ${pack.id}`); } } fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 1)); process.exit(0);