From 745422153379e8db590ec9d3bcb58e3b93746b2b Mon Sep 17 00:00:00 2001 From: Tom Vokac Date: Tue, 14 Jul 2026 23:43:09 -0400 Subject: [PATCH 1/2] feat: show app version and transcription model --- build/android/app/build.gradle | 2 +- build/config.yml | 4 +- build/darwin/Info.dev.plist | 6 +- build/darwin/Info.plist | 6 +- build/ios/Info.dev.plist | 6 +- build/ios/Info.plist | 6 +- build/linux/nfpm/nfpm.yaml | 2 +- build/windows/Taskfile.yml | 6 +- build/windows/info.json | 6 +- build/windows/msix/app_manifest.xml | 2 +- build/windows/msix/template.xml | 2 +- build/windows/nsis/project.nsi | 2 +- build/windows/nsis/wails_tools.nsh | 4 +- build/windows/wails.exe.manifest | 4 +- .../github.com/tomvokac/parley/index.ts | 1 + .../tomvokac/parley/meetingservice.ts | 10 +++ .../github.com/tomvokac/parley/models.ts | 15 ++++ .../wailsapp/wails/v3/internal/eventdata.d.ts | 1 + frontend/src/App.tsx | 41 ++++++++- frontend/src/components/SettingsDialog.tsx | 7 +- main.go | 1 + meeting_service.go | 90 +++++++++++++++++-- meeting_service_test.go | 55 ++++++++++-- version.go | 6 ++ 24 files changed, 240 insertions(+), 45 deletions(-) create mode 100644 version.go diff --git a/build/android/app/build.gradle b/build/android/app/build.gradle index 630f03e..94afdf6 100644 --- a/build/android/app/build.gradle +++ b/build/android/app/build.gradle @@ -15,7 +15,7 @@ android { minSdk 21 targetSdk 34 versionCode 1 - versionName "1.0" + versionName "0.1.1" // Configure supported ABIs ndk { diff --git a/build/config.yml b/build/config.yml index 9950104..a60ff22 100644 --- a/build/config.yml +++ b/build/config.yml @@ -11,7 +11,7 @@ info: description: "Local-first meeting assistant" # The application description copyright: "© 2026 Tom Vokac" # Copyright text comments: "Local-first meeting assistant" # Comments - version: "0.1.0" # The application version + version: "0.1.1" # The application version # cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional) # # Should match the name of your .icon file without the extension # # If not set and Assets.car exists, defaults to "appicon" @@ -76,4 +76,4 @@ fileAssociations: # Other data other: - - name: My Other Data \ No newline at end of file + - name: My Other Data diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist index 0b25a4f..4e972ae 100644 --- a/build/darwin/Info.dev.plist +++ b/build/darwin/Info.dev.plist @@ -10,11 +10,11 @@ CFBundleIdentifier dev.vokac.parley CFBundleVersion - 0.1.0 + 0.1.1 CFBundleGetInfoString This is a comment CFBundleShortVersionString - 0.1.0 + 0.1.1 CFBundleIconFile icons CFBundleIconName @@ -31,4 +31,4 @@ - \ No newline at end of file + diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist index ac75c4b..b1bcdab 100644 --- a/build/darwin/Info.plist +++ b/build/darwin/Info.plist @@ -10,11 +10,11 @@ CFBundleIdentifier dev.vokac.parley CFBundleVersion - 0.1.0 + 0.1.1 CFBundleGetInfoString This is a comment CFBundleShortVersionString - 0.1.0 + 0.1.1 CFBundleIconFile icons CFBundleIconName @@ -26,4 +26,4 @@ NSHumanReadableCopyright © 2026, My Company - \ No newline at end of file + diff --git a/build/ios/Info.dev.plist b/build/ios/Info.dev.plist index bd1887a..6903296 100644 --- a/build/ios/Info.dev.plist +++ b/build/ios/Info.dev.plist @@ -13,9 +13,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.1.0-dev + 0.1.1-dev CFBundleVersion - 0.1.0 + 0.1.1 LSRequiresIPhoneOS MinimumOSVersion @@ -58,4 +58,4 @@ This is a comment - \ No newline at end of file + diff --git a/build/ios/Info.plist b/build/ios/Info.plist index a65670d..fae9e2b 100644 --- a/build/ios/Info.plist +++ b/build/ios/Info.plist @@ -13,9 +13,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.1.0 + 0.1.1 CFBundleVersion - 0.1.0 + 0.1.1 LSRequiresIPhoneOS MinimumOSVersion @@ -56,4 +56,4 @@ This is a comment - \ No newline at end of file + diff --git a/build/linux/nfpm/nfpm.yaml b/build/linux/nfpm/nfpm.yaml index 4f6d7b9..8459536 100644 --- a/build/linux/nfpm/nfpm.yaml +++ b/build/linux/nfpm/nfpm.yaml @@ -6,7 +6,7 @@ name: "parley.exe" arch: ${GOARCH} platform: "linux" -version: "0.1.0" +version: "0.1.1" section: "default" priority: "extra" maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> diff --git a/build/windows/Taskfile.yml b/build/windows/Taskfile.yml index 586e350..faa73cb 100644 --- a/build/windows/Taskfile.yml +++ b/build/windows/Taskfile.yml @@ -26,9 +26,11 @@ tasks: EXTRA_TAGS: '{{.EXTRA_TAGS}}' OBFUSCATED: '{{.OBFUSCATED}}' GARBLE_ARGS: '{{.GARBLE_ARGS}}' + PRODUCT_VERSION: '{{.PRODUCT_VERSION}}' vars: # Default to CGO_ENABLED=0 if not explicitly set CGO_ENABLED: '{{.CGO_ENABLED | default "1"}}' + PRODUCT_VERSION: '{{.PRODUCT_VERSION | default ""}}' build:native: summary: Builds the application using native Go cross-compilation @@ -57,7 +59,7 @@ tasks: - cmd: rm -f *.syso platforms: [linux, darwin] vars: - BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}' + BUILD_FLAGS: '{{if eq .DEV "true"}}{{if or .EXTRA_TAGS (eq .OBFUSCATED "true")}}-tags {{if eq .OBFUSCATED "true"}}wails_obfuscated{{if .EXTRA_TAGS}},{{end}}{{end}}{{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if eq .OBFUSCATED "true"}},wails_obfuscated{{end}}{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui -X main.appVersion={{if .PRODUCT_VERSION}}{{.PRODUCT_VERSION}}{{else}}0.1.1{{end}}"{{end}}' env: GOOS: windows CGO_ENABLED: '{{.CGO_ENABLED | default "1"}}' @@ -119,6 +121,8 @@ tasks: dir: build/windows/nsis deps: - task: build + vars: + PRODUCT_VERSION: '{{.PRODUCT_VERSION}}' preconditions: - sh: '[ "{{.INSTALL_SCOPE}}" = "user" ] || [ "{{.INSTALL_SCOPE}}" = "machine" ]' msg: "INSTALL_SCOPE must be 'user' or 'machine', got '{{.INSTALL_SCOPE}}'" diff --git a/build/windows/info.json b/build/windows/info.json index b5ec510..f3a9b0d 100644 --- a/build/windows/info.json +++ b/build/windows/info.json @@ -1,10 +1,10 @@ { "fixed": { - "file_version": "0.1.0" + "file_version": "0.1.1" }, "info": { "0000": { - "ProductVersion": "0.1.0", + "ProductVersion": "0.1.1", "CompanyName": "Vokac", "FileDescription": "Local-first meeting assistant", "LegalCopyright": "© 2026 Tom Vokac", @@ -12,4 +12,4 @@ "Comments": "Local-first meeting assistant" } } -} \ No newline at end of file +} diff --git a/build/windows/msix/app_manifest.xml b/build/windows/msix/app_manifest.xml index 7c5c32b..b8742d7 100644 --- a/build/windows/msix/app_manifest.xml +++ b/build/windows/msix/app_manifest.xml @@ -10,7 +10,7 @@ diff --git a/build/windows/msix/template.xml b/build/windows/msix/template.xml index 67d39de..68e8cb6 100644 --- a/build/windows/msix/template.xml +++ b/build/windows/msix/template.xml @@ -17,7 +17,7 @@ PackageDisplayName="Parley" PublisherName="CN=Vokac" PublisherDisplayName="Vokac" - Version="0.1.0.0" + Version="0.1.1.0" PackageDescription="Local-first meeting assistant"> diff --git a/build/windows/nsis/project.nsi b/build/windows/nsis/project.nsi index 581928d..0de1c7a 100644 --- a/build/windows/nsis/project.nsi +++ b/build/windows/nsis/project.nsi @@ -22,7 +22,7 @@ Unicode true ## !define INFO_PROJECTNAME "my-project" # Default "Parley" ## !define INFO_COMPANYNAME "My Company" # Default "Vokac" ## !define INFO_PRODUCTNAME "My Product Name" # Default "Parley" -## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.1" ## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© 2026, My Company" ### ## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" diff --git a/build/windows/nsis/wails_tools.nsh b/build/windows/nsis/wails_tools.nsh index 9fcb781..f29b8e5 100644 --- a/build/windows/nsis/wails_tools.nsh +++ b/build/windows/nsis/wails_tools.nsh @@ -14,7 +14,7 @@ !define INFO_PRODUCTNAME "Parley" !endif !ifndef INFO_PRODUCTVERSION - !define INFO_PRODUCTVERSION "0.1.0" + !define INFO_PRODUCTVERSION "0.1.1" !endif !ifndef INFO_COPYRIGHT !define INFO_COPYRIGHT "© 2026, My Company" @@ -258,4 +258,4 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" !macro wails.unassociateCustomProtocols ; Delete app custom protocol associations -!macroend \ No newline at end of file +!macroend diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest index e9ca547..e8515de 100644 --- a/build/windows/wails.exe.manifest +++ b/build/windows/wails.exe.manifest @@ -1,6 +1,6 @@ - + @@ -19,4 +19,4 @@ - \ No newline at end of file + diff --git a/frontend/bindings/github.com/tomvokac/parley/index.ts b/frontend/bindings/github.com/tomvokac/parley/index.ts index 7a7875a..55ae7f4 100644 --- a/frontend/bindings/github.com/tomvokac/parley/index.ts +++ b/frontend/bindings/github.com/tomvokac/parley/index.ts @@ -11,5 +11,6 @@ export { export type { AnalysisStatusEvent, LoadedSession, + RuntimeInfo, StatusEvent } from "./models.js"; diff --git a/frontend/bindings/github.com/tomvokac/parley/meetingservice.ts b/frontend/bindings/github.com/tomvokac/parley/meetingservice.ts index 28b354f..de542bb 100644 --- a/frontend/bindings/github.com/tomvokac/parley/meetingservice.ts +++ b/frontend/bindings/github.com/tomvokac/parley/meetingservice.ts @@ -45,6 +45,16 @@ export function ExportMarkdown(sessionID: number): $CancellablePromise { return $Call.ByID(3378443135, sessionID); } +/** + * GetRuntimeInfo reports the exact app version and the transcription model that + * has actually been selected. In particular, this reflects a CPU fallback when + * Nemotron was preferred but could not start, rather than merely echoing the + * configured model filename. + */ +export function GetRuntimeInfo(): $CancellablePromise<$models.RuntimeInfo> { + return $Call.ByID(3989995776); +} + /** * IsRunning reports whether a session is active. */ diff --git a/frontend/bindings/github.com/tomvokac/parley/models.ts b/frontend/bindings/github.com/tomvokac/parley/models.ts index 6694c8a..fd55e1f 100644 --- a/frontend/bindings/github.com/tomvokac/parley/models.ts +++ b/frontend/bindings/github.com/tomvokac/parley/models.ts @@ -34,6 +34,21 @@ export interface LoadedSession { "liveNotes": store$0.LiveNote[] | null; } +/** + * RuntimeInfo is the build and transcription metadata currently in use. The + * frontend displays it persistently so packaged-version or model-selection + * problems can be identified without opening log files. + */ +export interface RuntimeInfo { + "appVersion": string; + "transcriptionModel": string; + + /** + * loading | ready | error + */ + "transcriptionStatus": string; +} + /** * StatusEvent is broadcast whenever the capture/transcription state changes. */ diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts index 62f8e90..4ccb69c 100644 --- a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts +++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts @@ -20,6 +20,7 @@ declare module "@wailsio/runtime" { interface CustomEvents { "analysis": analysis$0.State; "analysisStatus": main$0.AnalysisStatusEvent; + "runtimeInfo": main$0.RuntimeInfo; "status": main$0.StatusEvent; "transcript": stt$0.Segment; } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8e77da3..5dc462b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,7 +15,7 @@ import { } from "lucide-react"; import { MeetingService, LibraryService } from "../bindings/github.com/tomvokac/parley"; -import type { StatusEvent } from "../bindings/github.com/tomvokac/parley"; +import type { RuntimeInfo, StatusEvent } from "../bindings/github.com/tomvokac/parley"; import type { Segment } from "../bindings/github.com/tomvokac/parley/internal/stt/models"; import type { State as AnalysisState, Suggestion } from "../bindings/github.com/tomvokac/parley/internal/analysis/models"; import type { LiveNote, LLMConnection } from "../bindings/github.com/tomvokac/parley/internal/store/models"; @@ -149,6 +149,11 @@ function App() { const [conns, setConns] = useState([]); const [activeConnId, setActiveConnId] = useState(0); const [activeContext, setActiveContext] = useState(null); + const [runtimeInfo, setRuntimeInfo] = useState({ + appVersion: "", + transcriptionModel: "Loading local model…", + transcriptionStatus: "loading", + }); // Session-scoped suggestion pins/dismissals, keyed by normalized text. The // `analysis` listener is registered once, so it reads these via refs to avoid a // stale closure. @@ -248,6 +253,21 @@ function App() { }) .catch(() => {}); + // Subscribe before requesting the snapshot so a fast model-load completion + // cannot race between the initial response and event registration. + const offRuntimeInfo = Events.On("runtimeInfo", (e: { data: RuntimeInfo }) => { + setRuntimeInfo(e.data); + }); + MeetingService.GetRuntimeInfo() + .then(setRuntimeInfo) + .catch(() => + setRuntimeInfo((info) => ({ + ...info, + transcriptionModel: "Model status unavailable", + transcriptionStatus: "error", + })) + ); + const offStatus = Events.On("status", (e: { data: StatusEvent }) => { setStatus(e.data); setBusy(false); @@ -306,6 +326,7 @@ function App() { offTranscript(); offAnalysisStatus(); offAnalysis(); + offRuntimeInfo(); }; }, []); @@ -704,6 +725,24 @@ function App() { /> +
+ + Parley{runtimeInfo.appVersion ? ` v${runtimeInfo.appVersion}` : ""} + + + {runtimeInfo.transcriptionStatus === "loading" && ( + + )} + Voice-to-text: + + {runtimeInfo.transcriptionModel} + + +
+ diff --git a/frontend/src/components/SettingsDialog.tsx b/frontend/src/components/SettingsDialog.tsx index 310e1bf..79235aa 100644 --- a/frontend/src/components/SettingsDialog.tsx +++ b/frontend/src/components/SettingsDialog.tsx @@ -381,13 +381,14 @@ export function SettingsDialog({ onChange={(e) => setSettings({ ...settings, sttBaseURL: e.target.value })} />

- Blank = transcribe locally with the bundled Whisper model (private, - no setup). Set this to a whisper.cpp server URL (e.g. + Blank = transcribe locally (private, no setup), using Nemotron on a + supported NVIDIA GPU or the bundled CPU Whisper fallback. Set this + to a compatible server URL (e.g. http://192.168.1.10:8765) to offload transcription to another machine.

- + Date: Tue, 14 Jul 2026 23:49:10 -0400 Subject: [PATCH 2/2] feat: offer Nemotron on installer upgrades --- build/windows/nsis/project.nsi | 68 ++++++++++++++++++++------- internal/buildcheck/subsystem_test.go | 22 +++++++++ 2 files changed, 72 insertions(+), 18 deletions(-) diff --git a/build/windows/nsis/project.nsi b/build/windows/nsis/project.nsi index 0de1c7a..4579ebc 100644 --- a/build/windows/nsis/project.nsi +++ b/build/windows/nsis/project.nsi @@ -123,27 +123,59 @@ Section RMDir /r "$INSTDIR\resources\whisper" File /r "..\..\..\resources" - # On a fresh NVIDIA installation, provision Nemotron from its upstream - # repositories. This is deliberately non-fatal: an offline machine, an old - # driver, or a failed model download leaves bundled CPU Whisper available. - # Upgrades reuse the existing runtime/model and never redownload it. - ${If} $IsUpgrade == "0" + # A complete Nemotron install is persistent and automatically preferred by + # the app. Fresh NVIDIA installs provision it automatically. On upgrades + # where it is missing (including an earlier partial download), ask before + # downloading several GB. Silent upgrades never start a surprise download. + # Every failure/decline remains non-fatal because CPU Whisper is bundled. + IfFileExists "$INSTDIR\resources\nemotron\.ready" nemotron_present nemotron_probe_gpu + + nemotron_probe_gpu: nsExec::ExecToStack 'cmd /C nvidia-smi -L' Pop $1 Pop $2 - ${If} $1 == 0 - DetailPrint "NVIDIA GPU detected; provisioning Nemotron 3.5 ASR Streaming..." - nsExec::ExecToLog '"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\nemotron\setup.ps1" -InstallRoot "$INSTDIR\resources\nemotron"' - Pop $1 - ${If} $1 != 0 - DetailPrint "Nemotron provisioning did not complete (exit $1). Parley will use bundled CPU Whisper." - ${EndIf} - ${Else} - DetailPrint "No NVIDIA GPU detected; using bundled CPU Whisper." - ${EndIf} - ${Else} - DetailPrint "Upgrade detected; preserving the existing Nemotron installation." - ${EndIf} + StrCmp $1 "0" nemotron_gpu_found nemotron_no_gpu + + nemotron_gpu_found: + StrCmp $IsUpgrade "0" nemotron_provision + IfSilent nemotron_silent_skip + MessageBox MB_YESNO|MB_ICONQUESTION "Parley found an NVIDIA GPU, but Nemotron 3.5 ASR Streaming is not installed.$\r$\n$\r$\nDownload and install it now? This can download several gigabytes and may take several minutes. CPU Whisper remains available if you choose No." /SD IDNO IDYES nemotron_provision IDNO nemotron_declined + + nemotron_provision: + DetailPrint "NVIDIA GPU detected; provisioning Nemotron 3.5 ASR Streaming..." + nsExec::ExecToLog '"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\nemotron\setup.ps1" -InstallRoot "$INSTDIR\resources\nemotron"' + Pop $1 + StrCmp $1 "0" 0 nemotron_provision_failed + IfFileExists "$INSTDIR\resources\nemotron\.ready" nemotron_provisioned nemotron_not_ready + + nemotron_provisioned: + DetailPrint "Nemotron provisioning completed; Parley will use it automatically." + Goto nemotron_done + + nemotron_present: + DetailPrint "Existing Nemotron installation detected; Parley will use it automatically." + Goto nemotron_done + + nemotron_no_gpu: + DetailPrint "No NVIDIA GPU detected; using bundled CPU Whisper." + Goto nemotron_done + + nemotron_declined: + DetailPrint "Nemotron download declined; using bundled CPU Whisper." + Goto nemotron_done + + nemotron_silent_skip: + DetailPrint "Silent upgrade detected; skipping optional Nemotron download and using bundled CPU Whisper." + Goto nemotron_done + + nemotron_not_ready: + DetailPrint "Nemotron was not provisioned for this GPU; Parley will use bundled CPU Whisper." + Goto nemotron_done + + nemotron_provision_failed: + DetailPrint "Nemotron provisioning did not complete (exit $1). Parley will use bundled CPU Whisper." + + nemotron_done: SetOutPath $INSTDIR diff --git a/internal/buildcheck/subsystem_test.go b/internal/buildcheck/subsystem_test.go index 98a06a5..efcdf34 100644 --- a/internal/buildcheck/subsystem_test.go +++ b/internal/buildcheck/subsystem_test.go @@ -24,3 +24,25 @@ func TestWindowsProductionBuildIsGUISubsystem(t *testing.T) { "BUILD_FLAGS ldflags.", path) } } + +// Existing Parley users may predate optional Nemotron provisioning. An upgrade +// must preserve a complete install, but offer an interactive download when the +// readiness marker is absent instead of silently leaving GPU users on Whisper. +func TestInstallerOffersMissingNemotronOnUpgrade(t *testing.T) { + path := filepath.Join("..", "..", "build", "windows", "nsis", "project.nsi") + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + installer := string(data) + for _, required := range []string{ + `IfFileExists "$INSTDIR\resources\nemotron\.ready"`, + `StrCmp $IsUpgrade "0" nemotron_provision`, + `MessageBox MB_YESNO|MB_ICONQUESTION`, + `IfSilent nemotron_silent_skip`, + } { + if !strings.Contains(installer, required) { + t.Fatalf("%s no longer contains %q; missing Nemotron upgrades will not be handled safely", path, required) + } + } +}