[eric] voice: scratch-that discards the take, multilingual Small with auto language detect joins the catalog

This commit is contained in:
ciregenz
2026-08-05 21:19:00 -07:00
parent d53eb2b6fc
commit ba8a9c4038
3 changed files with 11 additions and 1 deletions
+1
View File
@@ -21,6 +21,7 @@ const MODELS = [
{ id: 'base.en-q5_1', file: 'ggml-base.en-q5_1.bin', label: 'Base (compact)', note: 'Base quality and speed, 90MB less to download and hold', bytes: 59721011, sha256: '4baf70dd0d7c4247ba2b81fafd9c01005ac77c2f9ef064e00dcf195d0e2fdd2f' },
{ id: 'base.en', file: 'ggml-base.en.bin', label: 'Base', note: 'Fast and light; the instant fallback while Small downloads', bytes: 147964211, sha256: 'a03779c86df3323075f5e796cb2ce5029f00ec8869eee3fdfb897afe36c6d002' },
{ id: 'small.en-q5_1', file: 'ggml-small.en-q5_1.bin', label: 'Small', note: 'Most accurate, the default; steadier on accents and noise', bytes: 190098681, sha256: 'bfdff4894dcb76bbf647d56263ea2a96645423f1669176f4844a1bf8e478ad30' },
{ id: 'small-q5_1', file: 'ggml-small-q5_1.bin', label: 'Small (multilingual)', note: 'Auto-detects the spoken language; slightly less sharp on English', bytes: 190085487, sha256: 'ae85e4a935d7a567bd102fe55afc16bb595bdb618e11b2fc7591bc08120411bb' },
];
// Measured on an M2 (quiet machine, 3.5s/8.1s/26.4s utterances, median of 5):
// tiny 174/154/241ms base-q5_1 212/414/656ms base 208/398/734ms small 865/1108/1869ms
+4 -1
View File
@@ -145,7 +145,10 @@ async function p_bootServer(resourceDir, userDataDir) {
p_sweepStrays(bin);
const p = await freePort();
// No --convert: our WAV is already 16kHz mono, and the flag makes whisper demand ffmpeg on PATH at boot; a Finder-launched app has no brew PATH, so it exited before ever binding the port.
const child = spawn(bin, ['-m', model, '--port', String(p), '-nt'], {
const args = ['-m', model, '--port', String(p), '-nt'];
// A multilingual model (no .en in the filename) auto-detects the spoken language per utterance.
if (!path.basename(model).includes('.en')) args.push('-l', 'auto');
const child = spawn(bin, args, {
cwd: p_privateCwd(userDataDir), // a writable, EMPTY dir: whisper writes temp files beside cwd, and ggml scans cwd at boot (see p_privateCwd)
// BOTH pipes: whisper writes its fatal reasons to STDOUT and then exits 0, so an ignored stdout
// turns "ffmpeg is missing" into an unexplained failure. Draining also stops the pipe buffer
@@ -223,6 +223,12 @@ export function useVoiceDictation() {
if (res?.ok && res.text) res = { ok: true, text: res.text.replace(/\[[^\]]*\]|\([^)]*\)|\*[^*]*\*|(?:^|\s)>>\s?/g, ' ').replace(/\s+/g, ' ').trim() };
// A transcript with no letter or digit in it is a hallucination artifact (the lone comma), not dictation.
if (res?.ok && res.text && !/[\p{L}\p{N}]/u.test(res.text)) res = { ok: true, text: '' };
// Wispr's command grammar, v1: saying only "scratch that" (or "delete/cancel that") throws the take away.
if (res?.ok && res.text && /^(scratch|delete|cancel) that[.!?]?$/i.test(res.text.trim())) {
setFeedback({ tone: 'ok', icon: 'check', text: 'Scratched.', at: Date.now() });
setState('idle');
return;
}
if (res?.ok && res.text) {
// WhisperFlow-style cleanup: punctuation + filler words via the cheap aux tier, fail-open to
// the raw transcript on any error/timeout so dictation never breaks with the aux down.