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