mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-02 14:28:59 +02:00
[eric] dictation: ship the model in the app so the first fn press never waits on a download
Costs ~181MB in the DMG; the alternative is pressing fn into silence while 190MB fetches. Checksum-gated so a truncated model fails the build instead of dying at the first press. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018foyDoK19jjbYdudfzQVkZ
This commit is contained in:
co-authored by
Claude Opus 5
parent
db0912d22d
commit
66ca97d773
@@ -0,0 +1,46 @@
|
||||
// Dictation has to work the instant the app opens: press fn, talk, done. The model is 190MB and
|
||||
// lived only as a runtime download, so a fresh install (or anyone offline, or on slow wifi) pressed
|
||||
// fn into silence while it fetched. It is now staged into Resources/whisper at build time.
|
||||
//
|
||||
// resolveModelFile already prefers a bundled model over userData, so these pin the two halves that
|
||||
// can silently drift: the build must stage it, and the resolver must look for the one we staged.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SH = path.join(__dirname, 'scripts', 'build-whisper.sh');
|
||||
const MODELS = require('./voice/whisperModels.js');
|
||||
|
||||
test('the build stages the DEFAULT model, the same one the resolver looks for', () => {
|
||||
const sh = fs.readFileSync(SH, 'utf-8');
|
||||
const wanted = MODELS.modelById(MODELS.DEFAULT_MODEL_ID).file;
|
||||
assert.ok(sh.includes(wanted),
|
||||
`build-whisper.sh must stage ${wanted}; the resolver checks resourceDir for exactly that name`);
|
||||
});
|
||||
|
||||
test('a corrupt model is refused at build time rather than shipped', () => {
|
||||
const sh = fs.readFileSync(SH, 'utf-8');
|
||||
const expected = MODELS.modelById(MODELS.DEFAULT_MODEL_ID).sha256;
|
||||
assert.ok(sh.includes(expected), 'the staged checksum must match the catalog');
|
||||
assert.ok(sh.includes('checksum mismatch') && sh.includes('exit 1'),
|
||||
'a truncated model ships silently and dies at the first press, so it must fail the build');
|
||||
});
|
||||
|
||||
test('a build without the model warns instead of passing quietly', () => {
|
||||
const sh = fs.readFileSync(SH, 'utf-8');
|
||||
assert.ok(sh.includes('WARNING') && sh.includes('WITHOUT a bundled model'),
|
||||
'silently dropping the model reintroduces the first-press wait with nothing saying so');
|
||||
});
|
||||
|
||||
test('the bundled model is NOT swept into app.asar', () => {
|
||||
// It belongs in extraResources. Inside the asar it would cost every user the 181MB twice over,
|
||||
// which is the regression removed on 2026-08-25.
|
||||
const pkg = require('./package.json');
|
||||
assert.ok(pkg.build.files.includes('!whisper') && pkg.build.files.includes('!whisper/**'));
|
||||
// Platform-scoped: the entry lives under build.mac.extraResources, not build.extraResources.
|
||||
const extra = JSON.stringify(pkg.build.mac.extraResources);
|
||||
assert.ok(extra.includes('build-staging/whisper'), 'it reaches Resources via extraResources only');
|
||||
assert.ok(extra.includes('"to":"whisper"') || extra.includes('"to": "whisper"'),
|
||||
'and lands at Resources/whisper, which is what voiceResourceDir() resolves');
|
||||
});
|
||||
@@ -59,3 +59,25 @@ file "$OUT/whisper-server"
|
||||
if otool -L "$OUT/whisper-server" | grep -qE "/opt/homebrew|/usr/local"; then
|
||||
echo "[whisper] ERROR: binary links non-system libraries"; otool -L "$OUT/whisper-server"; exit 1
|
||||
fi
|
||||
|
||||
# Bundle the default dictation model so fn works the instant the app opens, with no network at all.
|
||||
# resolveModelFile (voice/whisperModels.js) already looks for DEFAULT_MODEL_ID here in resourceDir
|
||||
# before anything in userData, so staging the file is the whole change. Costs ~181MB in the DMG;
|
||||
# the alternative is a user pressing fn into silence while 190MB downloads behind them.
|
||||
MODEL_FILE="ggml-small.en-q5_1.bin"
|
||||
MODEL_SHA="bfdff4894dcb76bbf647d56263ea2a96645423f1669176f4844a1bf8e478ad30"
|
||||
MODEL_SRC="$(cd "$(dirname "$0")/.." && pwd)/whisper/$MODEL_FILE"
|
||||
if [[ -f "$MODEL_SRC" ]]; then
|
||||
# Verify BEFORE copying: a truncated model ships silently and dies at the first press.
|
||||
GOT="$(shasum -a 256 "$MODEL_SRC" | cut -d' ' -f1)"
|
||||
if [[ "$GOT" != "$MODEL_SHA" ]]; then
|
||||
echo "[whisper] ERROR: $MODEL_FILE checksum mismatch (got $GOT); refusing to bundle a corrupt model"
|
||||
exit 1
|
||||
fi
|
||||
cp "$MODEL_SRC" "$OUT/$MODEL_FILE"
|
||||
echo "[whisper] bundled model -> $OUT/$MODEL_FILE ($(du -h "$OUT/$MODEL_FILE" | cut -f1))"
|
||||
else
|
||||
# Not fatal: the app still prefetches at boot. But say it, because a build without the model
|
||||
# silently reintroduces the first-press wait this was added to remove.
|
||||
echo "[whisper] WARNING: $MODEL_SRC missing; shipping WITHOUT a bundled model (first fn press will wait on a 190MB download)"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user