Skip to content

Commit 3003681

Browse files
committed
v2.0.1: fix Windows installer, move models to userData, installer model picker
1 parent 41bcdb6 commit 3003681

8 files changed

Lines changed: 168 additions & 209 deletions

File tree

.github/workflows/release.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ jobs:
2828
working-directory: electron
2929
run: npm ci --legacy-peer-deps
3030

31-
- name: Bundle whisper.cpp binary + base model
32-
working-directory: electron
33-
run: npm run setup:whisper
34-
3531
- name: Build renderer + main
3632
working-directory: electron
3733
run: npm run build
Lines changed: 92 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,96 @@
1-
; Custom NSIS uninstall hook — asks the user whether to keep downloaded
2-
; Whisper models. If they choose No, models are copied to %APPDATA%\Echo\
3-
; models-preserved\ before the installation directory is wiped; the app
4-
; restores them to the install dir on next launch.
1+
; Custom install + uninstall UI for Echo.
2+
;
3+
; Install: user picks which Whisper model the app should download on first
4+
; launch. The choice is written to %APPDATA%\Echo\install-choice.json and
5+
; consumed + deleted by the app on its first boot.
6+
;
7+
; Uninstall: user chooses whether to also wipe %APPDATA%\Echo\ (downloaded
8+
; whisper.cpp binary + models + saved settings) or keep it in place for a
9+
; future reinstall.
510

6-
!macro customUnInstall
7-
; Only prompt if there's at least one .bin model on disk
8-
IfFileExists "$INSTDIR\resources\app.asar.unpacked\node_modules\nodejs-whisper\cpp\whisper.cpp\models\ggml-*.bin" 0 skip_model_prompt
11+
!include "nsDialogs.nsh"
12+
!include "LogicLib.nsh"
13+
14+
; ─── Install-time model-choice page ─────────────────────────────────────────
15+
16+
Var ModelChoice
17+
Var MC_Dialog
18+
Var MC_RbTiny
19+
Var MC_RbBase
20+
Var MC_RbSmall
21+
Var MC_RbMedium
22+
Var MC_RbLargeTurbo
23+
24+
Function ModelChoicePage
25+
!insertmacro MUI_HEADER_TEXT "Choose a Whisper model" "Echo downloads this model the first time it runs. You can switch or download others later from Settings."
26+
nsDialogs::Create 1018
27+
Pop $MC_Dialog
28+
${If} $MC_Dialog == error
29+
Abort
30+
${EndIf}
31+
32+
${NSD_CreateRadioButton} 0 0u 100% 12u "Tiny – 75 MB (fastest)"
33+
Pop $MC_RbTiny
34+
${NSD_CreateRadioButton} 0 15u 100% 12u "Base – 142 MB (recommended)"
35+
Pop $MC_RbBase
36+
${NSD_CreateRadioButton} 0 30u 100% 12u "Small – 488 MB (better accuracy)"
37+
Pop $MC_RbSmall
38+
${NSD_CreateRadioButton} 0 45u 100% 12u "Medium – 1.5 GB (great accuracy)"
39+
Pop $MC_RbMedium
40+
${NSD_CreateRadioButton} 0 60u 100% 12u "Large v3 Turbo – 1.6 GB (best accuracy)"
41+
Pop $MC_RbLargeTurbo
42+
43+
${NSD_Check} $MC_RbBase
44+
45+
nsDialogs::Show
46+
FunctionEnd
947

48+
Function ModelChoicePageLeave
49+
${NSD_GetState} $MC_RbTiny $0
50+
${If} $0 == ${BST_CHECKED}
51+
StrCpy $ModelChoice "tiny"
52+
Return
53+
${EndIf}
54+
${NSD_GetState} $MC_RbSmall $0
55+
${If} $0 == ${BST_CHECKED}
56+
StrCpy $ModelChoice "small"
57+
Return
58+
${EndIf}
59+
${NSD_GetState} $MC_RbMedium $0
60+
${If} $0 == ${BST_CHECKED}
61+
StrCpy $ModelChoice "medium"
62+
Return
63+
${EndIf}
64+
${NSD_GetState} $MC_RbLargeTurbo $0
65+
${If} $0 == ${BST_CHECKED}
66+
StrCpy $ModelChoice "large-v3-turbo"
67+
Return
68+
${EndIf}
69+
; Anything else → base
70+
StrCpy $ModelChoice "base"
71+
FunctionEnd
72+
73+
!macro customPageAfterChangeDir
74+
Page custom ModelChoicePage ModelChoicePageLeave
75+
!macroend
76+
77+
!macro customInstall
78+
CreateDirectory "$APPDATA\Echo"
79+
FileOpen $0 "$APPDATA\Echo\install-choice.json" w
80+
FileWrite $0 '{"initialModelSize":"$ModelChoice"}'
81+
FileClose $0
82+
!macroend
83+
84+
; ─── Uninstall prompt ───────────────────────────────────────────────────────
85+
86+
!macro customUnInstall
87+
; Default to "No" — keep models/settings unless the user explicitly opts in
88+
; to wiping them.
1089
MessageBox MB_YESNO|MB_ICONQUESTION \
11-
"Remove downloaded Whisper models as well?$\r$\n$\r$\n\
12-
Choose No to keep them — they'll be restored automatically the next time you install Echo." \
13-
/SD IDYES \
14-
IDYES skip_model_prompt
15-
16-
; User chose No — preserve the models outside the install dir
17-
CreateDirectory "$APPDATA\Echo\models-preserved"
18-
CopyFiles /SILENT \
19-
"$INSTDIR\resources\app.asar.unpacked\node_modules\nodejs-whisper\cpp\whisper.cpp\models\*.bin" \
20-
"$APPDATA\Echo\models-preserved\"
21-
22-
skip_model_prompt:
90+
"Also delete Echo's downloaded models and saved settings?$\r$\n$\r$\n\
91+
Choose No to keep them for a future reinstall." \
92+
/SD IDNO \
93+
IDNO skip_appdata_wipe
94+
RMDir /r "$APPDATA\Echo"
95+
skip_appdata_wipe:
2396
!macroend

electron/electron-builder.config.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,29 @@ module.exports = {
1919
files: [
2020
"dist/**",
2121
"!dist/renderer/**/*.map",
22+
// electron-builder's default patterns are replaced when `files` is set
23+
// explicitly, so spell out node_modules ourselves. Without this the asar
24+
// ships with no runtime deps and the app crashes on launch with
25+
// `Cannot find module 'electron-store'`.
26+
"node_modules/**/*",
27+
"package.json",
2228
"!node_modules/uiohook-napi/**",
29+
// Trim dev noise without touching directories we need (avoid `*.cpp`
30+
// because it matches `whisper.cpp/` as a path).
31+
"!node_modules/**/*.{md,map,ts,tsx}",
32+
"!node_modules/**/{test,tests,__tests__,example,examples,docs,.github}/**",
33+
"!node_modules/**/{LICENSE,license,LICENCE,licence,CHANGELOG,changelog,README,readme}{,.md,.txt,.markdown}",
2334
],
2435
asarUnpack: [
2536
"node_modules/nodejs-whisper/**",
2637
"node_modules/koffi/**",
2738
"node_modules/@nut-tree-fork/**",
2839
],
40+
// koffi ships pre-built Electron binaries — no native compilation needed.
41+
// With npmRebuild left at its default (`true`) electron-builder's rebuild
42+
// step mangles the dep tree on Windows, leaving electron-store and others
43+
// out of the asar. This was the v2.0.0 regression.
44+
npmRebuild: false,
2945
beforeBuild: async (context) => {
3046
// uiohook-napi is non-Windows only; scrub it before Electron's build step
3147
// so its MSVC-requiring gyp scripts don't even try to run.

electron/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "echo",
3-
"version": "2.0.0",
3+
"version": "2.0.1",
44
"description": "Local push-to-talk voice-to-text for Windows",
55
"main": "dist/main/main/index.js",
66
"author": "Kenshiin13",
@@ -17,8 +17,7 @@
1717
"build:main": "tsc --project tsconfig.main.json",
1818
"dist": "npm run build && electron-builder --win",
1919
"dist:win": "npm run build && electron-builder --win",
20-
"rebuild": "electron-builder install-app-deps",
21-
"setup:whisper": "node scripts/setup-whisper.mjs"
20+
"rebuild": "electron-builder install-app-deps"
2221
},
2322
"dependencies": {
2423
"@mantine/core": "^7.17.0",

electron/scripts/setup-whisper.mjs

Lines changed: 0 additions & 171 deletions
This file was deleted.

electron/src/main/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ async function main() {
6262
const config = new ConfigStore();
6363
const sysInfo = await getSystemInfo();
6464

65+
// The NSIS installer writes <userData>/install-choice.json with the
66+
// user's chosen model size. Apply it to config on first launch, then
67+
// delete the file so later runs don't reset the setting.
68+
applyInstallChoice(config);
69+
6570
// On first run, default the backend to whatever the hardware recommends
6671
if (config.isFirstRun() && sysInfo.recommendedBackend !== "cpu") {
6772
config.save({ ...config.get(), backend: sysInfo.recommendedBackend });
@@ -204,6 +209,23 @@ async function main() {
204209
log.info(`Echo started (v${app.getVersion()}, ${sysInfo.platform}, backend=${config.get().backend})`);
205210
}
206211

212+
function applyInstallChoice(config: ConfigStore): void {
213+
const choicePath = path.join(app.getPath("userData"), "install-choice.json");
214+
if (!fs.existsSync(choicePath)) return;
215+
try {
216+
const raw = fs.readFileSync(choicePath, "utf8");
217+
const parsed = JSON.parse(raw) as { initialModelSize?: string };
218+
const validSizes = ["tiny", "base", "small", "medium", "large-v3-turbo"] as const;
219+
if (parsed.initialModelSize && (validSizes as readonly string[]).includes(parsed.initialModelSize)) {
220+
config.save({ ...config.get(), modelSize: parsed.initialModelSize as (typeof validSizes)[number] });
221+
log.info(`Applied installer choice: modelSize=${parsed.initialModelSize}`);
222+
}
223+
} catch (err) {
224+
log.error(`Failed to parse install-choice.json: ${err}`);
225+
}
226+
try { fs.unlinkSync(choicePath); } catch { /* non-fatal */ }
227+
}
228+
207229
function mimeFor(filePath: string): string {
208230
const ext = path.extname(filePath).toLowerCase();
209231
switch (ext) {

0 commit comments

Comments
 (0)