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