From 084cdd26d0f2bfa96d165c0a77a0697406919634 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Mon, 29 Jun 2026 01:19:28 +0800
Subject: [PATCH] Add Monterey compatibility build line
---
privatevoice.src/internal/model/build_capabilities.go | 17 +
privatevoice.src/internal/modelselection/selection_test.go | 21 +
privatevoice.src/internal/engine/engine_darwin.go | 12
privatevoice.src/services/engine_service_qwen3_monterey_test.go | 46 +++
privatevoice.src/scripts/build-local-macos.sh | 58 ++++
privatevoice.src/services/engine_service_qwen3_default_test.go | 40 +++
privatevoice.src/frontend/package.json | 2
privatevoice.src/internal/engine/engine_darwin_qwen3_monterey.go | 14 +
privatevoice.src/go.monterey.mod | 54 ++++
privatevoice.src/internal/model/build_capabilities_default.go | 5
privatevoice.src/frontend/src/components/settings/LanguagePage.svelte | 28 ++
privatevoice.src/frontend/src/lib/i18n/zh.json | 4
privatevoice.src/frontend/src/lib/i18n/en.json | 4
privatevoice.src/internal/engine/engine_darwin_qwen3_monterey_test.go | 35 ++
privatevoice.src/internal/engine/engine_darwin_qwen3_test.go | 57 ++++
privatevoice.src/internal/engine/engine_darwin_test.go | 47 ---
privatevoice.src/scripts/build-local-macos-monterey.sh | 21 +
privatevoice.src/internal/engine/engine_darwin_qwen3.go | 22 +
privatevoice.src/internal/model/build_capabilities_monterey.go | 5
CHANGELOG.md | 16 +
privatevoice.src/go.monterey.sum | 159 +++++++++++++
privatevoice.src/build/darwin/Info.plist | 6
privatevoice.src/internal/modelselection/selection.go | 18 +
privatevoice.src/app.go | 4
privatevoice.src/services/engine_service.go | 6
privatevoice.src/frontend/package-lock.json | 4
26 files changed, 627 insertions(+), 78 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc7b343..2e52d5b 100755
--- a/CHANGELOG.md
+++ b/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)
### 首尾字稳定性修复
diff --git a/privatevoice.src/app.go b/privatevoice.src/app.go
index b40cd98..2fe71b1 100755
--- a/privatevoice.src/app.go
+++ b/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"
diff --git a/privatevoice.src/build/darwin/Info.plist b/privatevoice.src/build/darwin/Info.plist
index bbf395a..40723d3 100755
--- a/privatevoice.src/build/darwin/Info.plist
+++ b/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>
diff --git a/privatevoice.src/frontend/package-lock.json b/privatevoice.src/frontend/package-lock.json
index ff41baa..9c270ba 100755
--- a/privatevoice.src/frontend/package-lock.json
+++ b/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"
},
diff --git a/privatevoice.src/frontend/package.json b/privatevoice.src/frontend/package.json
index 0863451..3c39db4 100755
--- a/privatevoice.src/frontend/package.json
+++ b/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",
diff --git a/privatevoice.src/frontend/src/components/settings/LanguagePage.svelte b/privatevoice.src/frontend/src/components/settings/LanguagePage.svelte
index 39de74f..5d07007 100644
--- a/privatevoice.src/frontend/src/components/settings/LanguagePage.svelte
+++ b/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;
diff --git a/privatevoice.src/frontend/src/lib/i18n/en.json b/privatevoice.src/frontend/src/lib/i18n/en.json
index 6444716..6aa47d2 100755
--- a/privatevoice.src/frontend/src/lib/i18n/en.json
+++ b/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",
diff --git a/privatevoice.src/frontend/src/lib/i18n/zh.json b/privatevoice.src/frontend/src/lib/i18n/zh.json
index c8890f0..8f29858 100755
--- a/privatevoice.src/frontend/src/lib/i18n/zh.json
+++ b/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": "权限检查",
diff --git a/privatevoice.src/go.monterey.mod b/privatevoice.src/go.monterey.mod
new file mode 100644
index 0000000..7bb860a
--- /dev/null
+++ b/privatevoice.src/go.monterey.mod
@@ -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
+)
diff --git a/privatevoice.src/go.monterey.sum b/privatevoice.src/go.monterey.sum
new file mode 100644
index 0000000..e4c9e11
--- /dev/null
+++ b/privatevoice.src/go.monterey.sum
@@ -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=
diff --git a/privatevoice.src/internal/engine/engine_darwin.go b/privatevoice.src/internal/engine/engine_darwin.go
index 5dd090d..b45349d 100755
--- a/privatevoice.src/internal/engine/engine_darwin.go
+++ b/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)
}
diff --git a/privatevoice.src/internal/engine/engine_darwin_qwen3.go b/privatevoice.src/internal/engine/engine_darwin_qwen3.go
new file mode 100644
index 0000000..bad17e7
--- /dev/null
+++ b/privatevoice.src/internal/engine/engine_darwin_qwen3.go
@@ -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
+}
diff --git a/privatevoice.src/internal/engine/engine_darwin_qwen3_monterey.go b/privatevoice.src/internal/engine/engine_darwin_qwen3_monterey.go
new file mode 100644
index 0000000..8b756cc
--- /dev/null
+++ b/privatevoice.src/internal/engine/engine_darwin_qwen3_monterey.go
@@ -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)
+}
diff --git a/privatevoice.src/internal/engine/engine_darwin_qwen3_monterey_test.go b/privatevoice.src/internal/engine/engine_darwin_qwen3_monterey_test.go
new file mode 100644
index 0000000..0ca3b28
--- /dev/null
+++ b/privatevoice.src/internal/engine/engine_darwin_qwen3_monterey_test.go
@@ -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())
+ }
+}
diff --git a/privatevoice.src/internal/engine/engine_darwin_qwen3_test.go b/privatevoice.src/internal/engine/engine_darwin_qwen3_test.go
new file mode 100644
index 0000000..775a86d
--- /dev/null
+++ b/privatevoice.src/internal/engine/engine_darwin_qwen3_test.go
@@ -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)
+ }
+}
diff --git a/privatevoice.src/internal/engine/engine_darwin_test.go b/privatevoice.src/internal/engine/engine_darwin_test.go
index 85191e7..0ffea21 100644
--- a/privatevoice.src/internal/engine/engine_darwin_test.go
+++ b/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{
diff --git a/privatevoice.src/internal/model/build_capabilities.go b/privatevoice.src/internal/model/build_capabilities.go
new file mode 100644
index 0000000..1e75db3
--- /dev/null
+++ b/privatevoice.src/internal/model/build_capabilities.go
@@ -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 ""
+}
diff --git a/privatevoice.src/internal/model/build_capabilities_default.go b/privatevoice.src/internal/model/build_capabilities_default.go
new file mode 100644
index 0000000..2314d90
--- /dev/null
+++ b/privatevoice.src/internal/model/build_capabilities_default.go
@@ -0,0 +1,5 @@
+//go:build !monterey_compat
+
+package model
+
+const qwen3ASRSupportedInCurrentBuild = true
diff --git a/privatevoice.src/internal/model/build_capabilities_monterey.go b/privatevoice.src/internal/model/build_capabilities_monterey.go
new file mode 100644
index 0000000..63e944d
--- /dev/null
+++ b/privatevoice.src/internal/model/build_capabilities_monterey.go
@@ -0,0 +1,5 @@
+//go:build monterey_compat
+
+package model
+
+const qwen3ASRSupportedInCurrentBuild = false
diff --git a/privatevoice.src/internal/modelselection/selection.go b/privatevoice.src/internal/modelselection/selection.go
index 48f1a9a..15b8d2b 100644
--- a/privatevoice.src/internal/modelselection/selection.go
+++ b/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)
}
- 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
diff --git a/privatevoice.src/internal/modelselection/selection_test.go b/privatevoice.src/internal/modelselection/selection_test.go
index 96875d9..718d061 100644
--- a/privatevoice.src/internal/modelselection/selection_test.go
+++ b/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
diff --git a/privatevoice.src/scripts/build-local-macos-monterey.sh b/privatevoice.src/scripts/build-local-macos-monterey.sh
new file mode 100755
index 0000000..c1c28cd
--- /dev/null
+++ b/privatevoice.src/scripts/build-local-macos-monterey.sh
@@ -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" "$@"
diff --git a/privatevoice.src/scripts/build-local-macos.sh b/privatevoice.src/scripts/build-local-macos.sh
index 95b1e1b..bba6647 100755
--- a/privatevoice.src/scripts/build-local-macos.sh
+++ b/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)"
-SHERPA_MACOS_MODULE_VERSION="$(cd "$PROJECT_DIR" && go list -m -f '{{.Version}}' github.com/k2-fsa/sherpa-onnx-go-macos)"
+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"
-CGO_ENABLED=1 GOOS=darwin GOARCH="$GOARCH" \
- go build -buildvcs=false -gcflags=all="-l" -ldflags="-s -w" \
- -o "$BUILD_DIR/$APP_NAME"
+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" \
diff --git a/privatevoice.src/services/engine_service.go b/privatevoice.src/services/engine_service.go
index a428a79..f7608d5 100755
--- a/privatevoice.src/services/engine_service.go
+++ b/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)
diff --git a/privatevoice.src/services/engine_service_qwen3_default_test.go b/privatevoice.src/services/engine_service_qwen3_default_test.go
new file mode 100644
index 0000000..9293685
--- /dev/null
+++ b/privatevoice.src/services/engine_service_qwen3_default_test.go
@@ -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)
+ }
+}
diff --git a/privatevoice.src/services/engine_service_qwen3_monterey_test.go b/privatevoice.src/services/engine_service_qwen3_monterey_test.go
new file mode 100644
index 0000000..13d0e3e
--- /dev/null
+++ b/privatevoice.src/services/engine_service_qwen3_monterey_test.go
@@ -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())
+ }
+}
--
Gitblit v1.9.3