CHANGELOG.md
@@ -1,5 +1,21 @@ # Changelog ## v2.1.37 (2026-06-29) ### 双构建线 - **默认 macOS 14+ 构建继续支持 Qwen3-ASR**:主 `go.mod` 保持当前 sherpa-onnx 依赖线,中文高级模型能力不回退。 - **新增 Monterey 兼容构建线**:新增 `monterey_compat` 构建标签和 `go.monterey.mod`,使用 macOS 11 兼容的旧版 sherpa/onnxruntime。 - **兼容包中 Qwen3-ASR 置灰禁用**:Qwen3-ASR 不隐藏,仍显示在中文模型列表中,但标记为不可用并提示需要 macOS 14 或更高版本。 - **构建最低系统版本分离**:默认构建声明 macOS 14.0 起,Monterey 兼容脚本覆盖为 macOS 11.0 起。 ### 构建 - build: `20260629.0059` - 说明: 本版本为双构建线和 Monterey 兼容本地验证候选包。 --- ## v2.1.36 (2026-06-28) ### 首尾字稳定性修复 privatevoice.src/app.go
@@ -28,8 +28,8 @@ ) const ( appVersion = "2.1.36" appBuild = "20260628.2354" appVersion = "2.1.37" appBuild = "20260629.0059" appDisplayVersion = appVersion + " (build " + appBuild + ")" appName = "PrivateVoice Dictation" privatevoice.src/build/darwin/Info.plist
@@ -17,15 +17,15 @@ <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> <string>2.1.36</string> <string>2.1.37</string> <key>CFBundleVersion</key> <string>20260628.2354</string> <string>20260629.0059</string> <key>ITSAppUsesNonExemptEncryption</key> <false/> <key>LSApplicationCategoryType</key> <string>public.app-category.productivity</string> <key>LSMinimumSystemVersion</key> <string>11.0</string> <string>14.0</string> <key>LSUIElement</key> <true/> <key>NSHighResolutionCapable</key> privatevoice.src/frontend/package-lock.json
@@ -1,12 +1,12 @@ { "name": "privatevoice-dictation-frontend", "version": "2.1.36", "version": "2.1.37", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "privatevoice-dictation-frontend", "version": "2.1.36", "version": "2.1.37", "dependencies": { "@wailsio/runtime": "latest" }, privatevoice.src/frontend/package.json
@@ -1,7 +1,7 @@ { "name": "privatevoice-dictation-frontend", "private": true, "version": "2.1.36", "version": "2.1.37", "type": "module", "scripts": { "dev": "vite dev", privatevoice.src/frontend/src/components/settings/LanguagePage.svelte
@@ -14,6 +14,8 @@ isDefault: boolean downloadSize?: string description?: string supportedInBuild?: boolean unsupportedReason?: string } interface LanguageOption { @@ -168,12 +170,25 @@ } function modelTierLabel(option: ModelOption): string { if (!modelSupported(option)) return t('settings.modelUnavailable') if (option.isDefault) return t('settings.modelDefault') if (option.tier === 'advanced') return t('settings.modelAdvanced') return option.tier || '' } function modelSupported(option: ModelOption): boolean { return option.supportedInBuild !== false } function modelUnsupportedText(option: ModelOption): string { if (option.unsupportedReason === 'requires_macos_14') { return t('models.qwen3RequiresMacOS14') } return t('settings.modelUnavailable') } function modelActionLabel(option: ModelOption): string { if (!modelSupported(option)) return t('settings.modelUnavailable') if (modelBusyID === option.modelID) { if (modelBusyKind === 'download') { return modelCancellingID === option.modelID ? t('settings.modelCancelling') : t('settings.modelCancel') @@ -187,6 +202,9 @@ function modelMeta(option: ModelOption): string { let meta = option.installed ? t('settings.modelInstalled') : t('settings.modelNotInstalled') if (!modelSupported(option)) { return `${meta} · ${modelUnsupportedText(option)}` } if (option.downloadSize) { meta += ` · ${option.downloadSize}` } @@ -249,7 +267,7 @@ } async function onModelAction(option: ModelOption) { if ((option.isCurrent && option.installed) || modelBusyID) return if (!modelSupported(option) || (option.isCurrent && option.installed) || modelBusyID) return modelBusyID = option.modelID modelBusyKind = option.installed ? 'select' : 'download' modelProgress = 0 @@ -359,7 +377,7 @@ <div class="model-list"> {#each modelOptions as option} <div class="model-card" class:current={option.isCurrent}> <div class="model-card" class:current={option.isCurrent} class:unsupported={!modelSupported(option)}> <div class="model-main"> <div class="model-title-row"> <span class="model-title">{option.displayName}</span> @@ -376,7 +394,7 @@ <button class="model-action primary" class:cancel={canCancelModel(option)} disabled={!!modelBusyID && modelBusyID !== option.modelID} disabled={!modelSupported(option) || (!!modelBusyID && modelBusyID !== option.modelID)} onclick={() => onModelButton(option)} > {modelActionLabel(option)} @@ -502,6 +520,10 @@ background: rgba(0, 122, 255, 0.055); } .model-card.unsupported { opacity: 0.76; } .model-card.current::before { content: ''; position: absolute; privatevoice.src/frontend/src/lib/i18n/en.json
@@ -69,6 +69,7 @@ "modelCancel": "Cancel", "modelCancelling": "Cancelling", "modelWorking": "Working", "modelUnavailable": "Unavailable", "modelInstalled": "Installed", "modelNotInstalled": "Not installed", "modelActionFailed": "Model action failed", @@ -90,7 +91,8 @@ "parakeetV3Desc": "Multilingual European offline model for French, German, Spanish, Italian, and Portuguese.", "japaneseDesc": "Dedicated Japanese offline model based on ReazonSpeech.", "koreanDesc": "Dedicated Korean offline model for Korean dictation.", "qwen3Desc": "Higher quality Chinese offline model for long Chinese dictation, technical terms, and mixed Chinese-English input." "qwen3Desc": "Higher quality Chinese offline model for long Chinese dictation, technical terms, and mixed Chinese-English input.", "qwen3RequiresMacOS14": "Qwen3-ASR requires macOS 14 or later. This compatibility build does not support downloading it." }, "permissions": { "title": "Permission Check", privatevoice.src/frontend/src/lib/i18n/zh.json
@@ -69,6 +69,7 @@ "modelCancel": "取消", "modelCancelling": "正在取消", "modelWorking": "处理中", "modelUnavailable": "不可用", "modelInstalled": "已安装", "modelNotInstalled": "未安装", "modelActionFailed": "模型操作失败", @@ -90,7 +91,8 @@ "parakeetV3Desc": "欧洲多语种离线模型,适合法语、德语、西班牙语、意大利语和葡萄牙语。", "japaneseDesc": "基于 ReazonSpeech 的日语专项离线模型。", "koreanDesc": "韩语专项离线模型,适合韩语听写。", "qwen3Desc": "更高质量的中文离线模型,适合中文长句、技术词和中英混输。" "qwen3Desc": "更高质量的中文离线模型,适合中文长句、技术词和中英混输。", "qwen3RequiresMacOS14": "Qwen3-ASR 需要 macOS 14 或更高版本,本兼容包暂不支持下载。" }, "permissions": { "title": "权限检查", privatevoice.src/go.monterey.mod
New file @@ -0,0 +1,54 @@ module voicesnap go 1.25 require ( github.com/gen2brain/malgo v0.11.22 github.com/k2-fsa/sherpa-onnx-go v1.12.20 github.com/k2-fsa/sherpa-onnx-go-linux v1.12.20 github.com/k2-fsa/sherpa-onnx-go-macos v1.12.20 github.com/k2-fsa/sherpa-onnx-go-windows v1.12.20 github.com/wailsapp/wails/v3 v3.0.0-alpha.71 golang.org/x/sys v0.40.0 ) require ( dario.cat/mergo v1.0.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/adrg/xdg v0.5.3 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/coder/websocket v1.8.14 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/ebitengine/purego v0.9.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.7.0 // indirect github.com/go-git/go-git/v5 v5.16.4 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/u v1.1.1 // indirect github.com/lmittmann/tint v1.1.2 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/samber/lo v1.52.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/skeema/knownhosts v1.3.2 // indirect github.com/wailsapp/go-webview2 v1.0.23 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/text v0.33.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) privatevoice.src/go.monterey.sum
New file @@ -0,0 +1,159 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/gen2brain/malgo v0.11.22 h1:fRtTbzVI9CDWnfEJGo/GxKxN7pXtCb0NsAeUVUjZk9U= github.com/gen2brain/malgo v0.11.22/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/k2-fsa/sherpa-onnx-go v1.12.20 h1:tQYCk7U2VrL6dO6LRPSJiDZgtXOzISoHaAgMhSa1tkw= github.com/k2-fsa/sherpa-onnx-go v1.12.20/go.mod h1:B/ynRbVa5gpYoZYeYgY3zPi4MTfKk95UZueZDSIhbjk= github.com/k2-fsa/sherpa-onnx-go-linux v1.12.20 h1:0NY5XVRX/unNDLDAkR3q8jXTUZ1WNTLiPP8MUSELWnQ= github.com/k2-fsa/sherpa-onnx-go-linux v1.12.20/go.mod h1:NXEH2rsBgTdqY59YpPq6CtSBlBAXy/8a9FmpLERU97I= github.com/k2-fsa/sherpa-onnx-go-macos v1.12.20 h1:9CrKABoDHx/BsFOv3uZcaG1bDjR+5TSK975gS3bNNjI= github.com/k2-fsa/sherpa-onnx-go-macos v1.12.20/go.mod h1:ZOhUAXC62Unj0ZNfu6zxSFKcW96aXf7P3BsqiUyOBbE= github.com/k2-fsa/sherpa-onnx-go-windows v1.12.20 h1:1Qsp4vkngTkEDxlc+GfA+/1B8ypbxIE0p8fsnfaSlkg= github.com/k2-fsa/sherpa-onnx-go-windows v1.12.20/go.mod h1:5AX7TU8+P/gInjglY1ijtWUM2b8iyR0QX4yEngzMe64= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0= github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/wails/v3 v3.0.0-alpha.71 h1:6ERh+1SJJ+tl5E4W49q8pDyQ4yeyi1yj9IdSppKtMx4= github.com/wailsapp/wails/v3 v3.0.0-alpha.71/go.mod h1:4saK4A4K9970X+X7RkMwP2lyGbLogcUz54wVeq4C/V8= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= privatevoice.src/internal/engine/engine_darwin.go
@@ -128,15 +128,9 @@ config.ModelConfig.Transducer.Joiner = resolved.Files["joiner"] config.ModelConfig.ModelType = model.BackendNemoTransducer case model.BackendQwen3ASR: config.ModelConfig.Qwen3ASR.ConvFrontend = resolved.Files["conv_frontend"] config.ModelConfig.Qwen3ASR.Encoder = resolved.Files["encoder"] config.ModelConfig.Qwen3ASR.Decoder = resolved.Files["decoder"] config.ModelConfig.Qwen3ASR.Tokenizer = resolved.Files["tokenizer"] config.ModelConfig.Qwen3ASR.MaxTotalLen = 1024 config.ModelConfig.Qwen3ASR.MaxNewTokens = 256 config.ModelConfig.Qwen3ASR.Temperature = 0.0 config.ModelConfig.Qwen3ASR.TopP = 0.9 config.ModelConfig.Qwen3ASR.Seed = 0 if err := applyQwen3ASRConfig(&config, resolved); err != nil { return sherpa.OfflineRecognizerConfig{}, err } default: return sherpa.OfflineRecognizerConfig{}, fmt.Errorf("unsupported backend kind: %s", resolved.BackendKind) } privatevoice.src/internal/engine/engine_darwin_qwen3.go
New file @@ -0,0 +1,22 @@ //go:build darwin && !monterey_compat package engine import ( "voicesnap/internal/model" sherpa "github.com/k2-fsa/sherpa-onnx-go/sherpa_onnx" ) func applyQwen3ASRConfig(config *sherpa.OfflineRecognizerConfig, resolved model.ResolvedModel) error { config.ModelConfig.Qwen3ASR.ConvFrontend = resolved.Files["conv_frontend"] config.ModelConfig.Qwen3ASR.Encoder = resolved.Files["encoder"] config.ModelConfig.Qwen3ASR.Decoder = resolved.Files["decoder"] config.ModelConfig.Qwen3ASR.Tokenizer = resolved.Files["tokenizer"] config.ModelConfig.Qwen3ASR.MaxTotalLen = 1024 config.ModelConfig.Qwen3ASR.MaxNewTokens = 256 config.ModelConfig.Qwen3ASR.Temperature = 0.0 config.ModelConfig.Qwen3ASR.TopP = 0.9 config.ModelConfig.Qwen3ASR.Seed = 0 return nil } privatevoice.src/internal/engine/engine_darwin_qwen3_monterey.go
New file @@ -0,0 +1,14 @@ //go:build darwin && monterey_compat package engine import ( "fmt" "voicesnap/internal/model" sherpa "github.com/k2-fsa/sherpa-onnx-go/sherpa_onnx" ) func applyQwen3ASRConfig(_ *sherpa.OfflineRecognizerConfig, _ model.ResolvedModel) error { return fmt.Errorf("%s requires the macOS 14+ build", model.Qwen3ASRModelID) } privatevoice.src/internal/engine/engine_darwin_qwen3_monterey_test.go
New file @@ -0,0 +1,35 @@ //go:build darwin && monterey_compat package engine import ( "strings" "testing" "voicesnap/internal/model" ) func TestOfflineConfigForQwen3ASRRejectsMontereyCompatBuild(t *testing.T) { _, err := offlineConfigForResolvedModel(model.ResolvedModel{ ModelID: model.Qwen3ASRModelID, BackendKind: model.BackendQwen3ASR, Profile: model.ModelProfile{ DisplayName: "Qwen3-ASR", BackendKind: model.BackendQwen3ASR, ProviderOrder: []string{"cpu"}, NumThreads: 4, }, Files: map[string]string{ "conv_frontend": "conv_frontend.onnx", "encoder": "encoder.int8.onnx", "decoder": "decoder.int8.onnx", "tokenizer": "tokenizer", }, }, "cpu") if err == nil { t.Fatal("expected qwen3-asr to be rejected in monterey_compat build") } if !strings.Contains(err.Error(), "macOS 14+") { t.Fatalf("error = %q, want macOS 14+ reason", err.Error()) } } privatevoice.src/internal/engine/engine_darwin_qwen3_test.go
New file @@ -0,0 +1,57 @@ //go:build darwin && !monterey_compat package engine import ( "path/filepath" "testing" "voicesnap/internal/model" ) func TestOfflineConfigForQwen3ASRUsesQwen3Config(t *testing.T) { root := t.TempDir() files := map[string]string{ "conv_frontend": filepath.Join(root, "conv_frontend.onnx"), "encoder": filepath.Join(root, "encoder.int8.onnx"), "decoder": filepath.Join(root, "decoder.int8.onnx"), "tokenizer": filepath.Join(root, "tokenizer"), } resolved := model.ResolvedModel{ ModelID: model.Qwen3ASRModelID, BackendKind: model.BackendQwen3ASR, Profile: model.ModelProfile{ DisplayName: "Qwen3-ASR", BackendKind: model.BackendQwen3ASR, ProviderOrder: []string{"cpu"}, NumThreads: 4, }, Files: files, } config, err := offlineConfigForResolvedModel(resolved, "cpu") if err != nil { t.Fatal(err) } if config.ModelConfig.Qwen3ASR.ConvFrontend != files["conv_frontend"] { t.Fatalf("conv frontend = %q, want %q", config.ModelConfig.Qwen3ASR.ConvFrontend, files["conv_frontend"]) } if config.ModelConfig.Qwen3ASR.Encoder != files["encoder"] { t.Fatalf("encoder = %q, want %q", config.ModelConfig.Qwen3ASR.Encoder, files["encoder"]) } if config.ModelConfig.Qwen3ASR.Decoder != files["decoder"] { t.Fatalf("decoder = %q, want %q", config.ModelConfig.Qwen3ASR.Decoder, files["decoder"]) } if config.ModelConfig.Qwen3ASR.Tokenizer != files["tokenizer"] { t.Fatalf("tokenizer = %q, want %q", config.ModelConfig.Qwen3ASR.Tokenizer, files["tokenizer"]) } if config.ModelConfig.Qwen3ASR.MaxTotalLen != 1024 { t.Fatalf("max total len = %d, want 1024", config.ModelConfig.Qwen3ASR.MaxTotalLen) } if config.ModelConfig.Qwen3ASR.MaxNewTokens != 256 { t.Fatalf("max new tokens = %d, want 256", config.ModelConfig.Qwen3ASR.MaxNewTokens) } if config.ModelConfig.Tokens != "" { t.Fatalf("tokens = %q, want empty for qwen3-asr", config.ModelConfig.Tokens) } } privatevoice.src/internal/engine/engine_darwin_test.go
@@ -93,53 +93,6 @@ } } func TestOfflineConfigForQwen3ASRUsesQwen3Config(t *testing.T) { root := t.TempDir() files := map[string]string{ "conv_frontend": filepath.Join(root, "conv_frontend.onnx"), "encoder": filepath.Join(root, "encoder.int8.onnx"), "decoder": filepath.Join(root, "decoder.int8.onnx"), "tokenizer": filepath.Join(root, "tokenizer"), } resolved := model.ResolvedModel{ ModelID: model.Qwen3ASRModelID, BackendKind: model.BackendQwen3ASR, Profile: model.ModelProfile{ DisplayName: "Qwen3-ASR", BackendKind: model.BackendQwen3ASR, ProviderOrder: []string{"cpu"}, NumThreads: 4, }, Files: files, } config, err := offlineConfigForResolvedModel(resolved, "cpu") if err != nil { t.Fatal(err) } if config.ModelConfig.Qwen3ASR.ConvFrontend != files["conv_frontend"] { t.Fatalf("conv frontend = %q, want %q", config.ModelConfig.Qwen3ASR.ConvFrontend, files["conv_frontend"]) } if config.ModelConfig.Qwen3ASR.Encoder != files["encoder"] { t.Fatalf("encoder = %q, want %q", config.ModelConfig.Qwen3ASR.Encoder, files["encoder"]) } if config.ModelConfig.Qwen3ASR.Decoder != files["decoder"] { t.Fatalf("decoder = %q, want %q", config.ModelConfig.Qwen3ASR.Decoder, files["decoder"]) } if config.ModelConfig.Qwen3ASR.Tokenizer != files["tokenizer"] { t.Fatalf("tokenizer = %q, want %q", config.ModelConfig.Qwen3ASR.Tokenizer, files["tokenizer"]) } if config.ModelConfig.Qwen3ASR.MaxTotalLen != 1024 { t.Fatalf("max total len = %d, want 1024", config.ModelConfig.Qwen3ASR.MaxTotalLen) } if config.ModelConfig.Qwen3ASR.MaxNewTokens != 256 { t.Fatalf("max new tokens = %d, want 256", config.ModelConfig.Qwen3ASR.MaxNewTokens) } if config.ModelConfig.Tokens != "" { t.Fatalf("tokens = %q, want empty for qwen3-asr", config.ModelConfig.Tokens) } } func TestOnlineConfigForXASRUsesStreamingTransducer(t *testing.T) { root := t.TempDir() files := map[string]string{ privatevoice.src/internal/model/build_capabilities.go
New file @@ -0,0 +1,17 @@ package model const UnsupportedReasonRequiresMacOS14 = "requires_macos_14" func IsModelSupportedInCurrentBuild(modelID string) bool { if modelID == Qwen3ASRModelID { return qwen3ASRSupportedInCurrentBuild } return true } func ModelUnsupportedReason(modelID string) string { if modelID == Qwen3ASRModelID && !IsModelSupportedInCurrentBuild(modelID) { return UnsupportedReasonRequiresMacOS14 } return "" } privatevoice.src/internal/model/build_capabilities_default.go
New file @@ -0,0 +1,5 @@ //go:build !monterey_compat package model const qwen3ASRSupportedInCurrentBuild = true privatevoice.src/internal/model/build_capabilities_monterey.go
New file @@ -0,0 +1,5 @@ //go:build monterey_compat package model const qwen3ASRSupportedInCurrentBuild = false privatevoice.src/internal/modelselection/selection.go
@@ -27,16 +27,20 @@ if selectionMode == config.ModelSelectionModeManual { if profile, err := model.GetModelProfile(cfg.SelectedModelID); err == nil { if isModelAvailableForLanguage(profile.ID, settings.EffectiveLanguageID) { if !model.IsModelSupportedInCurrentBuild(profile.ID) { fallbackReason = "manual model not supported in current build" logger.Info("Manual SelectedModelID %q is not supported in this build, falling back to language default %q", cfg.SelectedModelID, modelID) } else if isModelAvailableForLanguage(profile.ID, settings.EffectiveLanguageID) { return CurrentModel{ ModelID: profile.ID, Profile: profile, SelectionMode: selectionMode, LanguageSettings: settings, } } } else { fallbackReason = "manual model not available for language" logger.Info("Manual SelectedModelID %q is not available for language %q, falling back to language default %q", cfg.SelectedModelID, settings.EffectiveLanguageID, modelID) } } else { fallbackReason = "invalid manual model selection" logger.Info("Invalid manual SelectedModelID %q, falling back to language default %q", cfg.SelectedModelID, modelID) @@ -47,6 +51,11 @@ if err != nil { fallbackReason = "invalid language default model" logger.Info("Invalid default model %q for language %q, falling back to %q", modelID, settings.EffectiveLanguageID, model.DefaultModelID) profile = model.DefaultModelProfile() modelID = profile.ID } else if !model.IsModelSupportedInCurrentBuild(profile.ID) { fallbackReason = "language default model not supported in current build" logger.Info("Default model %q for language %q is not supported in this build, falling back to %q", modelID, settings.EffectiveLanguageID, model.DefaultModelID) profile = model.DefaultModelProfile() modelID = profile.ID } @@ -61,6 +70,9 @@ } func isModelAvailableForLanguage(modelID, languageID string) bool { if !model.IsModelSupportedInCurrentBuild(modelID) { return false } profile, err := model.GetLanguageProfile(languageID) if err != nil { return false privatevoice.src/internal/modelselection/selection_test.go
@@ -105,6 +105,9 @@ } func TestResolveManualUsesQwen3ASRForChinese(t *testing.T) { if !model.IsModelSupportedInCurrentBuild(model.Qwen3ASRModelID) { t.Skip("qwen3-asr is disabled in this build") } cfg := config.Default() cfg.LanguageMode = config.LanguageModeAuto cfg.ModelSelectionMode = config.ModelSelectionModeManual @@ -116,6 +119,24 @@ } } func TestResolveManualQwen3ASRFallsBackWhenUnsupported(t *testing.T) { if model.IsModelSupportedInCurrentBuild(model.Qwen3ASRModelID) { t.Skip("qwen3-asr is supported in this build") } cfg := config.Default() cfg.LanguageMode = config.LanguageModeAuto cfg.ModelSelectionMode = config.ModelSelectionModeManual cfg.SelectedModelID = model.Qwen3ASRModelID current := Resolve(cfg, fakeDetector{locale: "zh-CN"}) if current.ModelID != model.DefaultModelID { t.Fatalf("model id = %q, want fallback %q", current.ModelID, model.DefaultModelID) } if current.FallbackReason != "manual model not supported in current build" { t.Fatalf("fallback reason = %q, want unsupported build reason", current.FallbackReason) } } func TestResolveManualUsesXASRForChinese(t *testing.T) { cfg := config.Default() cfg.LanguageMode = config.LanguageModeAuto privatevoice.src/scripts/build-local-macos-monterey.sh
New file @@ -0,0 +1,21 @@ #!/usr/bin/env bash # # Build the local Monterey-compatible DMG. This build uses the legacy # sherpa-onnx/onnxruntime line and disables Qwen3-ASR with the # monterey_compat build tag. # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" export PRIVATEVOICE_GO_MODFILE="${PRIVATEVOICE_GO_MODFILE:-go.monterey.mod}" export PRIVATEVOICE_GO_BUILD_TAGS="${PRIVATEVOICE_GO_BUILD_TAGS:-monterey_compat}" export PRIVATEVOICE_LOCAL_BUILD_FLAVOR="${PRIVATEVOICE_LOCAL_BUILD_FLAVOR:-monterey-local}" export PRIVATEVOICE_LOCAL_MIN_SYSTEM_VERSION="${PRIVATEVOICE_LOCAL_MIN_SYSTEM_VERSION:-11.0}" export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-11.0}" if [[ "$#" -eq 0 ]]; then set -- arm64 fi exec "$SCRIPT_DIR/build-local-macos.sh" "$@" privatevoice.src/scripts/build-local-macos.sh
@@ -17,6 +17,10 @@ LEGACY_LOCAL_IDENTITY="VoiceSnap Local Code Signing" LOCAL_IDENTITY="${PRIVATEVOICE_LOCAL_SIGN_IDENTITY:-${VOICESNAP_LOCAL_SIGN_IDENTITY:-}}" KEYCHAIN="${PRIVATEVOICE_LOCAL_KEYCHAIN:-${VOICESNAP_LOCAL_KEYCHAIN:-$HOME/Library/Keychains/login.keychain-db}}" LOCAL_BUILD_FLAVOR="${PRIVATEVOICE_LOCAL_BUILD_FLAVOR:-local}" LOCAL_MIN_SYSTEM_VERSION="${PRIVATEVOICE_LOCAL_MIN_SYSTEM_VERSION:-}" GO_MODFILE="${PRIVATEVOICE_GO_MODFILE:-}" GO_BUILD_TAGS="${PRIVATEVOICE_GO_BUILD_TAGS:-${PRIVATEVOICE_GO_TAGS:-}}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" @@ -24,10 +28,27 @@ INFO_PLIST="$PROJECT_DIR/build/darwin/Info.plist" ENTITLEMENTS="$PROJECT_DIR/build/darwin/entitlements.plist" GOMODCACHE="$(go env GOMODCACHE)" GO_MOD_ARG="" GO_BUILD_TAG_ARG="" if [[ -n "$GO_MODFILE" ]]; then GO_MOD_ARG="-modfile=$GO_MODFILE" fi if [[ -n "$GO_BUILD_TAGS" ]]; then GO_BUILD_TAG_ARG="-tags=$GO_BUILD_TAGS" fi if [[ -n "$GO_MOD_ARG" ]]; then SHERPA_MACOS_MODULE_VERSION="$(cd "$PROJECT_DIR" && go list "$GO_MOD_ARG" -m -f '{{.Version}}' github.com/k2-fsa/sherpa-onnx-go-macos)" else SHERPA_MACOS_MODULE_VERSION="$(cd "$PROJECT_DIR" && go list -m -f '{{.Version}}' github.com/k2-fsa/sherpa-onnx-go-macos)" fi VERSION="$(defaults read "$INFO_PLIST" CFBundleShortVersionString 2>/dev/null || echo "2.1.1")" BUILD_ID="$(defaults read "$INFO_PLIST" CFBundleVersion 2>/dev/null || date +%Y%m%d.%H%M)" DEFAULT_MIN_SYSTEM_VERSION="$(defaults read "$INFO_PLIST" LSMinimumSystemVersion 2>/dev/null || echo "")" EFFECTIVE_MIN_SYSTEM_VERSION="${LOCAL_MIN_SYSTEM_VERSION:-$DEFAULT_MIN_SYSTEM_VERSION}" if [[ -n "$EFFECTIVE_MIN_SYSTEM_VERSION" && -z "${MACOSX_DEPLOYMENT_TARGET:-}" ]]; then export MACOSX_DEPLOYMENT_TARGET="$EFFECTIVE_MIN_SYSTEM_VERSION" fi TARGET_ARCH="" INSTALL_AFTER_BUILD=0 @@ -76,8 +97,8 @@ BUILD_DIR="$PROJECT_DIR/build/local/$DMG_ARCH" APP_BUNDLE="$BUILD_DIR/$APP_NAME.app" DMG_PATH="$BUILD_DIR/$APP_NAME-$VERSION-build$BUILD_ID-$DMG_ARCH-local.dmg" COMPAT_DMG_PATH="$BUILD_DIR/$APP_NAME-$VERSION-$DMG_ARCH-local.dmg" DMG_PATH="$BUILD_DIR/$APP_NAME-$VERSION-build$BUILD_ID-$DMG_ARCH-$LOCAL_BUILD_FLAVOR.dmg" COMPAT_DMG_PATH="$BUILD_DIR/$APP_NAME-$VERSION-$DMG_ARCH-$LOCAL_BUILD_FLAVOR.dmg" SHERPA_LIB_DIR="$GOMODCACHE/github.com/k2-fsa/sherpa-onnx-go-macos@$SHERPA_MACOS_MODULE_VERSION/lib/$SHERPA_ARCH" ONNXRUNTIME_DYLIB="" ONNXRUNTIME_DYLIB_NAME="" @@ -179,6 +200,13 @@ "$item" } step "Resolving Go dependencies" if [[ -n "$GO_MOD_ARG" ]]; then (cd "$PROJECT_DIR" && go mod download "$GO_MOD_ARG" github.com/k2-fsa/sherpa-onnx-go-macos) else (cd "$PROJECT_DIR" && go mod download github.com/k2-fsa/sherpa-onnx-go-macos) fi step "Preflight checks" [[ -f "$INFO_PLIST" ]] || fail "Info.plist not found at $INFO_PLIST" [[ -f "$ENTITLEMENTS" ]] || fail "entitlements.plist not found at $ENTITLEMENTS" @@ -202,9 +230,23 @@ step "Building Go binary ($DMG_ARCH)" cd "$PROJECT_DIR" if [[ -n "$GO_MOD_ARG" && -n "$GO_BUILD_TAG_ARG" ]]; then CGO_ENABLED=1 GOOS=darwin GOARCH="$GOARCH" \ go build "$GO_MOD_ARG" "$GO_BUILD_TAG_ARG" -buildvcs=false -gcflags=all="-l" -ldflags="-s -w" \ -o "$BUILD_DIR/$APP_NAME" elif [[ -n "$GO_MOD_ARG" ]]; then CGO_ENABLED=1 GOOS=darwin GOARCH="$GOARCH" \ go build "$GO_MOD_ARG" -buildvcs=false -gcflags=all="-l" -ldflags="-s -w" \ -o "$BUILD_DIR/$APP_NAME" elif [[ -n "$GO_BUILD_TAG_ARG" ]]; then CGO_ENABLED=1 GOOS=darwin GOARCH="$GOARCH" \ go build "$GO_BUILD_TAG_ARG" -buildvcs=false -gcflags=all="-l" -ldflags="-s -w" \ -o "$BUILD_DIR/$APP_NAME" else CGO_ENABLED=1 GOOS=darwin GOARCH="$GOARCH" \ go build -buildvcs=false -gcflags=all="-l" -ldflags="-s -w" \ -o "$BUILD_DIR/$APP_NAME" fi step "Assembling $APP_NAME.app" mkdir -p "$APP_BUNDLE/Contents/MacOS" @@ -212,6 +254,10 @@ mkdir -p "$APP_BUNDLE/Contents/Resources" cp "$INFO_PLIST" "$APP_BUNDLE/Contents/Info.plist" if [[ -n "$EFFECTIVE_MIN_SYSTEM_VERSION" ]]; then /usr/libexec/PlistBuddy -c "Set :LSMinimumSystemVersion $EFFECTIVE_MIN_SYSTEM_VERSION" "$APP_BUNDLE/Contents/Info.plist" >/dev/null 2>&1 \ || /usr/libexec/PlistBuddy -c "Add :LSMinimumSystemVersion string $EFFECTIVE_MIN_SYSTEM_VERSION" "$APP_BUNDLE/Contents/Info.plist" fi cp "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/Contents/MacOS/$APP_NAME" cp "$PROJECT_DIR/build/darwin/icon.icns" "$APP_BUNDLE/Contents/Resources/icon.icns" find "$PROJECT_DIR/build/darwin" -maxdepth 1 -type d -name "*.lproj" \ privatevoice.src/services/engine_service.go
@@ -171,6 +171,7 @@ func (s *EngineService) modelStatusMap(profile model.ModelProfile, current modelselection.CurrentModel) map[string]interface{} { resolved, err := model.ResolveModel(profile.ID) installed := err == nil && resolved.IsUsable() supportedInBuild := model.IsModelSupportedInCurrentBuild(profile.ID) status := model.ModelNotInstalled var missing []string var problems []string @@ -198,6 +199,8 @@ "supportedLanguages": profile.SupportedLanguageIDs, "recommendedLanguage": profile.RecommendedFor, "description": profile.Description, "supportedInBuild": supportedInBuild, "unsupportedReason": model.ModelUnsupportedReason(profile.ID), } } @@ -370,6 +373,9 @@ if err != nil { return model.ModelProfile{}, err } if !model.IsModelSupportedInCurrentBuild(profile.ID) { return model.ModelProfile{}, fmt.Errorf("model %s requires macOS 14 or later", profile.ID) } current := s.currentModel() languageProfile, err := model.GetLanguageProfile(current.LanguageSettings.EffectiveLanguageID) privatevoice.src/services/engine_service_qwen3_default_test.go
New file @@ -0,0 +1,40 @@ //go:build !monterey_compat package services import ( "testing" "voicesnap/internal/config" "voicesnap/internal/language" "voicesnap/internal/model" "voicesnap/internal/modelselection" ) func TestQwen3ASRSupportedInDefaultBuild(t *testing.T) { cfg := config.Default() cfg.LanguageMode = config.LanguageModeManual cfg.LanguageID = model.DefaultLanguageID service := NewEngineService(cfg) profile, err := model.GetModelProfile(model.Qwen3ASRModelID) if err != nil { t.Fatal(err) } status := service.modelStatusMap(profile, modelselection.CurrentModel{ ModelID: model.DefaultModelID, Profile: model.DefaultModelProfile(), SelectionMode: config.ModelSelectionModeAuto, LanguageSettings: language.Resolve(config.LanguageModeManual, model.DefaultLanguageID, nil), }) if got := status["supportedInBuild"]; got != true { t.Fatalf("supportedInBuild = %v, want true", got) } if got := status["unsupportedReason"]; got != "" { t.Fatalf("unsupportedReason = %v, want empty", got) } if _, err := service.allowedModelProfile(model.Qwen3ASRModelID); err != nil { t.Fatalf("allowedModelProfile returned error: %v", err) } } privatevoice.src/services/engine_service_qwen3_monterey_test.go
New file @@ -0,0 +1,46 @@ //go:build monterey_compat package services import ( "strings" "testing" "voicesnap/internal/config" "voicesnap/internal/language" "voicesnap/internal/model" "voicesnap/internal/modelselection" ) func TestQwen3ASRVisibleButDisabledInMontereyCompatBuild(t *testing.T) { cfg := config.Default() cfg.LanguageMode = config.LanguageModeManual cfg.LanguageID = model.DefaultLanguageID service := NewEngineService(cfg) profile, err := model.GetModelProfile(model.Qwen3ASRModelID) if err != nil { t.Fatal(err) } status := service.modelStatusMap(profile, modelselection.CurrentModel{ ModelID: model.DefaultModelID, Profile: model.DefaultModelProfile(), SelectionMode: config.ModelSelectionModeAuto, LanguageSettings: language.Resolve(config.LanguageModeManual, model.DefaultLanguageID, nil), }) if got := status["modelID"]; got != model.Qwen3ASRModelID { t.Fatalf("modelID = %v, want %s", got, model.Qwen3ASRModelID) } if got := status["supportedInBuild"]; got != false { t.Fatalf("supportedInBuild = %v, want false", got) } if got := status["unsupportedReason"]; got != model.UnsupportedReasonRequiresMacOS14 { t.Fatalf("unsupportedReason = %v, want %s", got, model.UnsupportedReasonRequiresMacOS14) } if _, err := service.allowedModelProfile(model.Qwen3ASRModelID); err == nil { t.Fatal("expected qwen3-asr to be rejected in Monterey compatibility build") } else if !strings.Contains(err.Error(), "macOS 14") { t.Fatalf("error = %q, want macOS 14 reason", err.Error()) } }