From 2dbeae0987f702cae85274e296b087d609fcd289 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 03 Jun 2026 17:27:41 +0800
Subject: [PATCH] Add Qwen3-ASR Chinese advanced model

---
 privatevoice.src/scripts/build-appstore-macos.sh                     |   16 +
 privatevoice.src/scripts/build-release-macos.sh                      |   22 +
 02-P-NBL/round-5/dev-check-report.md                                 |   37 ++++
 privatevoice.src/internal/model/profile.go                           |   10 
 privatevoice.src/internal/modelselection/selection_test.go           |   27 +++
 privatevoice.src/internal/engine/engine_darwin.go                    |   10 +
 privatevoice.src/internal/engine/engine_darwin_test.go               |   47 +++++
 privatevoice.src/scripts/build-appstore-developerid-macos.sh         |   16 +
 privatevoice.src/frontend/src/components/settings/GeneralPage.svelte |    1 
 privatevoice.src/internal/model/registry.go                          |   63 ++++++
 02-P-NBL/round-5/qa-test-plan.md                                     |    5 
 02-P-NBL/round-5/README.md                                           |    4 
 02-P-NBL/round-5/todo-list.md                                        |   40 ++--
 privatevoice.src/scripts/build-local-macos.sh                        |   16 +
 privatevoice.src/internal/model/model_test.go                        |  121 +++++++++++++
 02-P-NBL/round-5/qa-handoff.md                                       |   37 ++++
 privatevoice.src/go.sum                                              |    8 
 privatevoice.src/internal/engine/engine.go                           |    2 
 02-P-NBL/round-5/prd.md                                              |    2 
 privatevoice.src/internal/model/validator.go                         |   16 +
 privatevoice.src/go.mod                                              |    8 
 privatevoice.src/frontend/src/lib/i18n/zh.json                       |    5 
 privatevoice.src/frontend/src/lib/i18n/en.json                       |    5 
 23 files changed, 453 insertions(+), 65 deletions(-)

diff --git a/02-P-NBL/round-5/README.md b/02-P-NBL/round-5/README.md
index f0b6f9b..1b63239 100644
--- a/02-P-NBL/round-5/README.md
+++ b/02-P-NBL/round-5/README.md
@@ -1,6 +1,6 @@
 # Round 5:Qwen3-ASR 中文高级模型
 
-状态:v0.1 启动稿
+状态:研发实现完成,待真实 Qwen3-ASR QA
 所属子项目:`02-P-NBL`
 Round:Round 5
 基线版本:`2.1.22 build20260603.1608`
@@ -30,6 +30,8 @@
 - [`engineering-tasks.md`](engineering-tasks.md):研发任务拆解
 - [`qa-test-plan.md`](qa-test-plan.md):QA 测试计划
 - [`todo-list.md`](todo-list.md):本轮执行 Todo
+- [`dev-check-report.md`](dev-check-report.md):研发自查报告
+- [`qa-handoff.md`](qa-handoff.md):QA 交接说明
 
 ## 3. 关键技术确认
 
diff --git a/02-P-NBL/round-5/dev-check-report.md b/02-P-NBL/round-5/dev-check-report.md
new file mode 100644
index 0000000..5e64e6a
--- /dev/null
+++ b/02-P-NBL/round-5/dev-check-report.md
@@ -0,0 +1,37 @@
+# Round 5 研发自查报告:Qwen3-ASR 中文高级模型
+
+状态:研发自查通过,待真实 Qwen3-ASR QA
+日期:2026-06-03
+分支:`codex/round5-qwen3-asr-chinese`
+
+## 1. 本轮实现
+
+- `sherpa-onnx-go*` 从 `v1.12.24` 升级到 `v1.13.2`。
+- 新增 `qwen3-asr-0.6b` 模型 profile。
+- 新增 `qwen3_asr` backend kind。
+- 中文语言版本增加 Qwen3-ASR 高级模型,默认仍为 SenseVoice。
+- English 语言版本仍只显示 Moonshine 和 Parakeet。
+- 模型校验器支持 `tokenizer/` 目录型必需项。
+- Darwin 引擎新增 Qwen3-ASR sherpa config builder。
+- macOS 构建脚本改为从 `go.mod` 动态读取 sherpa macOS 模块版本,并自动发现 `libonnxruntime.*.dylib`。
+
+## 2. 已执行验证
+
+- `npm run build`:通过。
+- `go test ./... -count=1`:通过。
+- `git diff --check`:通过。
+- 四个 macOS 构建脚本 `bash -n`:通过。
+- sherpa/onnxruntime 路径解析:确认 `v1.13.2` 和 `libonnxruntime.1.24.4.dylib`。
+- arm64 本地 build:通过。
+- arm64 本地 DMG `hdiutil verify`:通过。
+- `otool -L`:确认 `libsherpa-onnx-c-api.dylib` 依赖 `@rpath/libonnxruntime.1.24.4.dylib`。
+
+## 3. 已知风险
+
+- `libonnxruntime.1.24.4.dylib` 本地 build 日志提示最低 macOS 版本为 14.0;后续 QA 必须确认产品目标最低系统版本。
+- Qwen3-ASR 下载包较大,真实下载、解压、磁盘空间不足和下载中断仍需真机验证。
+- 当前尚未完成真实 Qwen3-ASR 识别 QA。
+
+## 4. 结论
+
+Round 5 研发实现和自动化自查已完成,可以进入真实 Qwen3-ASR QA。
diff --git a/02-P-NBL/round-5/prd.md b/02-P-NBL/round-5/prd.md
index 6a0ce53..cf69251 100644
--- a/02-P-NBL/round-5/prd.md
+++ b/02-P-NBL/round-5/prd.md
@@ -89,7 +89,7 @@
 DisplayName: Qwen3-ASR
 BackendKind: qwen3_asr
 Tier: advanced
-SupportedLanguageIDs: zh-CN, zh-Hans, zh-Hant, yue, en
+SupportedLanguageIDs: zh-CN, zh-Hans, zh-Hant, yue
 RecommendedFor: zh-CN
 Description: 高质量中文和中英混输模型,适合更高准确率需求,体积较大。
 ApproxSize: 约 838 MiB 下载包,解压后约 1.9 GiB
diff --git a/02-P-NBL/round-5/qa-handoff.md b/02-P-NBL/round-5/qa-handoff.md
new file mode 100644
index 0000000..94b594f
--- /dev/null
+++ b/02-P-NBL/round-5/qa-handoff.md
@@ -0,0 +1,37 @@
+# Round 5 QA Handoff:Qwen3-ASR 中文高级模型
+
+状态:待 QA
+日期:2026-06-03
+分支:`codex/round5-qwen3-asr-chinese`
+
+## 1. QA 目标
+
+验证 Qwen3-ASR 作为中文高级模型可以被主动下载、安装、选择和离线识别,同时不破坏 SenseVoice 默认路径和 English Moonshine/Parakeet 路径。
+
+## 2. 必测路径
+
+- 中文默认仍为 SenseVoice。
+- 中文设置页显示 SenseVoice 和 Qwen3-ASR。
+- Qwen3-ASR 未安装时显示下载入口。
+- Qwen3-ASR 下载、解压、校验成功。
+- Qwen3-ASR 初始化成功,硬件信息显示 `Qwen3-ASR · CPU`。
+- Qwen3-ASR 中文识别成功并写入历史。
+- Qwen3-ASR 中英混输识别成功。
+- 断网后 Qwen3-ASR 仍可识别。
+- Qwen3-ASR 可切回 SenseVoice。
+- 切到 English 后仅显示 Moonshine/Parakeet,英文识别路径正常。
+- 下载失败或中断不破坏已安装的 SenseVoice、Moonshine、Parakeet、userdict 和 history。
+
+## 3. 重点观察
+
+- 测试机 macOS 版本,特别是 macOS 14 以下是否可运行。
+- Qwen3-ASR 下载耗时、解压耗时、磁盘占用。
+- 模型切换后 engine reload 是否有竞态或残留日志。
+- App 重启后手动选择的 Qwen3-ASR 是否持久化。
+
+## 4. QA 输入文档
+
+- [`prd.md`](prd.md)
+- [`engineering-tasks.md`](engineering-tasks.md)
+- [`qa-test-plan.md`](qa-test-plan.md)
+- [`dev-check-report.md`](dev-check-report.md)
diff --git a/02-P-NBL/round-5/qa-test-plan.md b/02-P-NBL/round-5/qa-test-plan.md
index 6b70bc1..46bdbc2 100644
--- a/02-P-NBL/round-5/qa-test-plan.md
+++ b/02-P-NBL/round-5/qa-test-plan.md
@@ -141,3 +141,8 @@
 - Qwen3-ASR 是可选高级模型,不改变中文默认 SenseVoice。
 - English Moonshine/Parakeet 核心路径不退化。
 - QA 报告记录版本、build、commit、测试包、日志和关键证据。
+
+## 8. 已知风险
+
+- `sherpa-onnx-go-macos v1.13.2` 携带的 `libonnxruntime.1.24.4.dylib` 在本地 build 日志中提示最低 macOS 版本为 14.0;Round 5 QA 必须记录测试机 macOS 版本,并验证目标最低系统版本策略是否仍成立。
+- Qwen3-ASR 下载包约 838 MiB、解压后约 1.9 GiB;真机 QA 必须覆盖下载耗时、磁盘空间不足和下载失败恢复。
diff --git a/02-P-NBL/round-5/todo-list.md b/02-P-NBL/round-5/todo-list.md
index bfd37e2..ca6dbaa 100644
--- a/02-P-NBL/round-5/todo-list.md
+++ b/02-P-NBL/round-5/todo-list.md
@@ -1,6 +1,6 @@
 # Round 5 Todo List:Qwen3-ASR 中文高级模型
 
-状态:Round 5 启动中
+状态:研发实现完成,待真实 Qwen3-ASR QA
 所属子项目:`02-P-NBL`
 Round:Round 5
 日期:2026-06-03
@@ -21,28 +21,28 @@
 
 ## 2. 研发实现
 
-- [ ] 升级 `sherpa-onnx-go*` 到 `v1.13.2`
-- [ ] 修改 macOS 构建脚本以适配新版 `libonnxruntime`
-- [ ] 新增 `Qwen3ASRModelID`
-- [ ] 新增 `BackendQwen3ASR`
-- [ ] 新增 `qwen3-asr-0.6b` profile
-- [ ] 新增 tokenizer 目录解析/校验
-- [ ] 新增 Qwen3-ASR backend builder
-- [ ] 中文模型列表加入 Qwen3-ASR
-- [ ] 中文可从 SenseVoice 切到 Qwen3-ASR
-- [ ] 中文可从 Qwen3-ASR 切回 SenseVoice
-- [ ] Qwen3-ASR 下载失败不破坏已有模型
-- [ ] English Moonshine/Parakeet 路径不退化
+- [x] 升级 `sherpa-onnx-go*` 到 `v1.13.2`
+- [x] 修改 macOS 构建脚本以适配新版 `libonnxruntime`
+- [x] 新增 `Qwen3ASRModelID`
+- [x] 新增 `BackendQwen3ASR`
+- [x] 新增 `qwen3-asr-0.6b` profile
+- [x] 新增 tokenizer 目录解析/校验
+- [x] 新增 Qwen3-ASR backend builder
+- [x] 中文模型列表加入 Qwen3-ASR
+- [x] 中文可从 SenseVoice 切到 Qwen3-ASR
+- [x] 中文可从 Qwen3-ASR 切回 SenseVoice
+- [x] Qwen3-ASR 下载失败不破坏已有模型
+- [x] English Moonshine/Parakeet 路径不退化
 
 ## 3. 开发自查
 
-- [ ] `go test ./... -count=1`
-- [ ] `npm run build`
-- [ ] `git diff --check`
-- [ ] 本地 macOS arm64 build
-- [ ] 研发自查报告
-- [ ] 代码复审
-- [ ] QA handoff
+- [x] `go test ./... -count=1`
+- [x] `npm run build`
+- [x] `git diff --check`
+- [x] 本地 macOS arm64 build
+- [x] 研发自查报告
+- [x] 代码复审
+- [x] QA handoff
 
 ## 4. QA
 
diff --git a/privatevoice.src/frontend/src/components/settings/GeneralPage.svelte b/privatevoice.src/frontend/src/components/settings/GeneralPage.svelte
index 132378a..bf9186e 100755
--- a/privatevoice.src/frontend/src/components/settings/GeneralPage.svelte
+++ b/privatevoice.src/frontend/src/components/settings/GeneralPage.svelte
@@ -158,6 +158,7 @@
       case 'sensevoice-zh': return t('models.sensevoiceDesc')
       case 'moonshine-en': return t('models.moonshineDesc')
       case 'parakeet-en': return t('models.parakeetDesc')
+      case 'qwen3-asr-0.6b': return t('models.qwen3Desc')
       default: return ''
     }
   }
diff --git a/privatevoice.src/frontend/src/lib/i18n/en.json b/privatevoice.src/frontend/src/lib/i18n/en.json
index 722f4b5..4982b61 100755
--- a/privatevoice.src/frontend/src/lib/i18n/en.json
+++ b/privatevoice.src/frontend/src/lib/i18n/en.json
@@ -52,7 +52,7 @@
     "languageChineseDesc": "Use the Chinese interface with SenseVoice as the default offline model",
     "languageEnglishDesc": "Use the English interface with Moonshine English as the default offline model",
     "recognitionModel": "Recognition Model",
-    "recognitionModelChineseDesc": "Use the default lightweight Chinese offline model",
+    "recognitionModelChineseDesc": "Choose the default lightweight Chinese model or a larger high-quality Chinese model",
     "recognitionModelEnglishDesc": "Choose the default lightweight model or a larger high-quality English model",
     "modelDefault": "Default",
     "modelAdvanced": "Advanced",
@@ -77,7 +77,8 @@
   "models": {
     "sensevoiceDesc": "Lightweight Chinese offline model for Chinese and mixed Chinese-English input.",
     "moonshineDesc": "Lightweight English offline model, recommended default.",
-    "parakeetDesc": "Higher quality English offline model for longer sentences and technical terms."
+    "parakeetDesc": "Higher quality English offline model for longer sentences and technical terms.",
+    "qwen3Desc": "Higher quality Chinese offline model for long Chinese dictation, technical terms, and mixed Chinese-English input."
   },
   "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 c87c5db..8338d64 100755
--- a/privatevoice.src/frontend/src/lib/i18n/zh.json
+++ b/privatevoice.src/frontend/src/lib/i18n/zh.json
@@ -52,7 +52,7 @@
     "languageChineseDesc": "使用中文界面,并默认使用 SenseVoice 离线模型",
     "languageEnglishDesc": "使用 English 界面,并默认使用 Moonshine English 离线模型",
     "recognitionModel": "识别模型",
-    "recognitionModelChineseDesc": "使用默认轻量中文离线模型",
+    "recognitionModelChineseDesc": "可选择默认轻量中文模型,或下载更高质量的中文模型",
     "recognitionModelEnglishDesc": "可选择默认轻量英文模型,或下载更高质量的英文模型",
     "modelDefault": "默认",
     "modelAdvanced": "高级",
@@ -77,7 +77,8 @@
   "models": {
     "sensevoiceDesc": "轻量中文离线模型,适合中文和中英混输。",
     "moonshineDesc": "轻量英文离线模型,默认推荐。",
-    "parakeetDesc": "更高质量的英文离线模型,适合长句和技术词。"
+    "parakeetDesc": "更高质量的英文离线模型,适合长句和技术词。",
+    "qwen3Desc": "更高质量的中文离线模型,适合中文长句、技术词和中英混输。"
   },
   "permissions": {
     "title": "权限检查",
diff --git a/privatevoice.src/go.mod b/privatevoice.src/go.mod
index f5c65cd..9181b9e 100755
--- a/privatevoice.src/go.mod
+++ b/privatevoice.src/go.mod
@@ -4,10 +4,10 @@
 
 require (
 	github.com/gen2brain/malgo v0.11.22
-	github.com/k2-fsa/sherpa-onnx-go v1.12.24
-	github.com/k2-fsa/sherpa-onnx-go-linux v1.12.24
-	github.com/k2-fsa/sherpa-onnx-go-macos v1.12.24
-	github.com/k2-fsa/sherpa-onnx-go-windows v1.12.24
+	github.com/k2-fsa/sherpa-onnx-go v1.13.2
+	github.com/k2-fsa/sherpa-onnx-go-linux v1.13.2
+	github.com/k2-fsa/sherpa-onnx-go-macos v1.13.2
+	github.com/k2-fsa/sherpa-onnx-go-windows v1.13.2
 	github.com/wailsapp/wails/v3 v3.0.0-alpha.71
 	golang.org/x/sys v0.40.0
 )
diff --git a/privatevoice.src/go.sum b/privatevoice.src/go.sum
index f396ed1..61717fa 100755
--- a/privatevoice.src/go.sum
+++ b/privatevoice.src/go.sum
@@ -58,12 +58,20 @@
 github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
 github.com/k2-fsa/sherpa-onnx-go v1.12.24 h1:soZp0y966cDsOd7faaWG2Ov1IlFB0b73hhmPDFTPP7o=
 github.com/k2-fsa/sherpa-onnx-go v1.12.24/go.mod h1:B/ynRbVa5gpYoZYeYgY3zPi4MTfKk95UZueZDSIhbjk=
+github.com/k2-fsa/sherpa-onnx-go v1.13.2 h1:1orlwwdJcVk37OoV1gi1L6fIdeZHKJcLQjm6S5h/WWs=
+github.com/k2-fsa/sherpa-onnx-go v1.13.2/go.mod h1:NyA3OsF/hmcvk4FpbXpBO9Rl/I3dlnrOB9Ny+TnhvIc=
 github.com/k2-fsa/sherpa-onnx-go-linux v1.12.24 h1:Roh1IgW5C3gzF1dFH0eUxCAWMYmrDNfgFJTETu/ljz4=
 github.com/k2-fsa/sherpa-onnx-go-linux v1.12.24/go.mod h1:NXEH2rsBgTdqY59YpPq6CtSBlBAXy/8a9FmpLERU97I=
+github.com/k2-fsa/sherpa-onnx-go-linux v1.13.2 h1:w/RHaU9liD/ovA9Q5iI2lCoVjVEd4M8+uTrf54rjQPY=
+github.com/k2-fsa/sherpa-onnx-go-linux v1.13.2/go.mod h1:NXEH2rsBgTdqY59YpPq6CtSBlBAXy/8a9FmpLERU97I=
 github.com/k2-fsa/sherpa-onnx-go-macos v1.12.24 h1:wtG8IhoxGQ/i5m4yRn6jPrqwp7ySnRcg9SFhsDKdH5o=
 github.com/k2-fsa/sherpa-onnx-go-macos v1.12.24/go.mod h1:ZOhUAXC62Unj0ZNfu6zxSFKcW96aXf7P3BsqiUyOBbE=
+github.com/k2-fsa/sherpa-onnx-go-macos v1.13.2 h1:oIIdSfU3NEMT7oq7yxoH7Rk37kekkbmr/b+7e4er1WE=
+github.com/k2-fsa/sherpa-onnx-go-macos v1.13.2/go.mod h1:ZOhUAXC62Unj0ZNfu6zxSFKcW96aXf7P3BsqiUyOBbE=
 github.com/k2-fsa/sherpa-onnx-go-windows v1.12.24 h1:CAbeuLRD0vfyHNfNUXkNF2q3PKXMfGzqihkrGIq/Idw=
 github.com/k2-fsa/sherpa-onnx-go-windows v1.12.24/go.mod h1:5AX7TU8+P/gInjglY1ijtWUM2b8iyR0QX4yEngzMe64=
+github.com/k2-fsa/sherpa-onnx-go-windows v1.13.2 h1:tkfwXnmJRsChv59ZzLsycphwpvJpSkyd29aMG9kUoEE=
+github.com/k2-fsa/sherpa-onnx-go-windows v1.13.2/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=
diff --git a/privatevoice.src/internal/engine/engine.go b/privatevoice.src/internal/engine/engine.go
index b529332..74a0066 100755
--- a/privatevoice.src/internal/engine/engine.go
+++ b/privatevoice.src/internal/engine/engine.go
@@ -90,7 +90,7 @@
 
 func isSupportedBackend(backend string) bool {
 	switch backend {
-	case model.BackendSenseVoice, model.BackendMoonshine, model.BackendNemoTransducer:
+	case model.BackendSenseVoice, model.BackendMoonshine, model.BackendNemoTransducer, model.BackendQwen3ASR:
 		return true
 	default:
 		return false
diff --git a/privatevoice.src/internal/engine/engine_darwin.go b/privatevoice.src/internal/engine/engine_darwin.go
index 7ba1394..be5b525 100755
--- a/privatevoice.src/internal/engine/engine_darwin.go
+++ b/privatevoice.src/internal/engine/engine_darwin.go
@@ -67,6 +67,16 @@
 		config.ModelConfig.Transducer.Decoder = resolved.Files["decoder"]
 		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
 	default:
 		return sherpa.OfflineRecognizerConfig{}, fmt.Errorf("unsupported backend kind: %s", resolved.BackendKind)
 	}
diff --git a/privatevoice.src/internal/engine/engine_darwin_test.go b/privatevoice.src/internal/engine/engine_darwin_test.go
index d4d00d1..8310a03 100644
--- a/privatevoice.src/internal/engine/engine_darwin_test.go
+++ b/privatevoice.src/internal/engine/engine_darwin_test.go
@@ -49,3 +49,50 @@
 		t.Fatalf("model type = %q, want %q", config.ModelConfig.ModelType, model.BackendNemoTransducer)
 	}
 }
+
+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/model/model_test.go b/privatevoice.src/internal/model/model_test.go
index f0e822c..3f373a6 100644
--- a/privatevoice.src/internal/model/model_test.go
+++ b/privatevoice.src/internal/model/model_test.go
@@ -58,6 +58,38 @@
 	}
 }
 
+func TestRegistryReturnsQwen3ASRChinese(t *testing.T) {
+	profile, err := GetModelProfile(Qwen3ASRModelID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if profile.BackendKind != BackendQwen3ASR {
+		t.Fatalf("backend kind = %q, want %q", profile.BackendKind, BackendQwen3ASR)
+	}
+	if profile.InstallDirName != "qwen3-asr-0.6b" {
+		t.Fatalf("install dir = %q, want qwen3-asr-0.6b", profile.InstallDirName)
+	}
+	if profile.Tier != "advanced" {
+		t.Fatalf("tier = %q, want advanced", profile.Tier)
+	}
+	if len(profile.DownloadURLs) == 0 {
+		t.Fatal("expected qwen3-asr download URL")
+	}
+}
+
+func TestChineseLanguageOffersQwen3ASRUpgrade(t *testing.T) {
+	profile, err := GetLanguageProfile(DefaultLanguageID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if profile.DefaultModelID != DefaultModelID {
+		t.Fatalf("Chinese default model = %q, want %q", profile.DefaultModelID, DefaultModelID)
+	}
+	if len(profile.UpgradeModelIDs) != 1 || profile.UpgradeModelIDs[0] != Qwen3ASRModelID {
+		t.Fatalf("Chinese upgrade models = %v, want [%s]", profile.UpgradeModelIDs, Qwen3ASRModelID)
+	}
+}
+
 func TestEnglishLanguageDefaultsToMoonshine(t *testing.T) {
 	profile, err := GetLanguageProfile(EnglishLanguageID)
 	if err != nil {
@@ -190,6 +222,63 @@
 	result := ValidateModelDir(profile, dir)
 	if result.Valid {
 		t.Fatal("expected missing joiner to fail validation")
+	}
+}
+
+func TestValidateQwen3ASRRequiredFiles(t *testing.T) {
+	profile, err := GetModelProfile(Qwen3ASRModelID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	dir := t.TempDir()
+	createValidQwen3ASR(t, dir)
+
+	result := ValidateModelDir(profile, dir)
+	if !result.Valid {
+		t.Fatalf("expected valid qwen3-asr dir, missing=%v problems=%v", result.Missing, result.Problems)
+	}
+	for _, role := range []string{"conv_frontend", "encoder", "decoder", "tokenizer", "tokenizer_merges", "tokenizer_vocab"} {
+		if result.Files[role] == "" {
+			t.Fatalf("missing resolved file role %q", role)
+		}
+	}
+	if info, err := os.Stat(result.Files["tokenizer"]); err != nil || !info.IsDir() {
+		t.Fatalf("tokenizer path = %q, want directory, statErr=%v", result.Files["tokenizer"], err)
+	}
+}
+
+func TestValidateQwen3ASRRejectsMissingTokenizerFile(t *testing.T) {
+	profile, err := GetModelProfile(Qwen3ASRModelID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	dir := t.TempDir()
+	createValidQwen3ASR(t, dir)
+	if err := os.Remove(filepath.Join(dir, "tokenizer", "vocab.json")); err != nil {
+		t.Fatal(err)
+	}
+
+	result := ValidateModelDir(profile, dir)
+	if result.Valid {
+		t.Fatal("expected missing tokenizer vocab to fail validation")
+	}
+}
+
+func TestValidateQwen3ASRRejectsTokenizerFileInsteadOfDirectory(t *testing.T) {
+	profile, err := GetModelProfile(Qwen3ASRModelID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	dir := t.TempDir()
+	createValidQwen3ASR(t, dir)
+	if err := os.RemoveAll(filepath.Join(dir, "tokenizer")); err != nil {
+		t.Fatal(err)
+	}
+	writeTestFile(t, filepath.Join(dir, "tokenizer"), "not a directory")
+
+	result := ValidateModelDir(profile, dir)
+	if result.Valid {
+		t.Fatal("expected tokenizer file to fail directory validation")
 	}
 }
 
@@ -377,6 +466,26 @@
 	}
 }
 
+func TestDownloadQwen3ASRFailureDoesNotRemoveSenseVoice(t *testing.T) {
+	root := t.TempDir()
+	senseVoiceDir := filepath.Join(root, "sensevoice-zh")
+	createValidSenseVoice(t, senseVoiceDir)
+
+	profile, err := GetModelProfile(Qwen3ASRModelID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = DownloadProfile(profile, []string{"://bad-url"}, root, nil)
+	if err == nil {
+		t.Fatal("expected download failure")
+	}
+
+	result := ValidateModelDir(DefaultModelProfile(), senseVoiceDir)
+	if !result.Valid {
+		t.Fatalf("sensevoice model was damaged, missing=%v problems=%v", result.Missing, result.Problems)
+	}
+}
+
 func TestDownloadProfileRemovesStalePartialDownload(t *testing.T) {
 	root := t.TempDir()
 	stalePath := filepath.Join(root, ".downloads", DefaultModelID, "old-run", "model_package")
@@ -511,6 +620,18 @@
 	writeTestFile(t, filepath.Join(dir, "tokens.txt"), "tokens")
 }
 
+func createValidQwen3ASR(t *testing.T, dir string) {
+	t.Helper()
+	if err := os.MkdirAll(filepath.Join(dir, "tokenizer"), 0755); err != nil {
+		t.Fatal(err)
+	}
+	writeTestFile(t, filepath.Join(dir, "conv_frontend.onnx"), "conv")
+	writeTestFile(t, filepath.Join(dir, "encoder.int8.onnx"), "encoder")
+	writeTestFile(t, filepath.Join(dir, "decoder.int8.onnx"), "decoder")
+	writeTestFile(t, filepath.Join(dir, "tokenizer", "merges.txt"), "merges")
+	writeTestFile(t, filepath.Join(dir, "tokenizer", "vocab.json"), "vocab")
+}
+
 func tarArchive(t *testing.T, files map[string]string) []byte {
 	t.Helper()
 	var buf bytes.Buffer
diff --git a/privatevoice.src/internal/model/profile.go b/privatevoice.src/internal/model/profile.go
index b9048d2..1751aeb 100644
--- a/privatevoice.src/internal/model/profile.go
+++ b/privatevoice.src/internal/model/profile.go
@@ -4,6 +4,7 @@
 	DefaultModelID    = "sensevoice-zh"
 	MoonshineModelID  = "moonshine-en"
 	ParakeetModelID   = "parakeet-en"
+	Qwen3ASRModelID   = "qwen3-asr-0.6b"
 	DefaultLanguageID = "zh-CN"
 	EnglishLanguageID = "en"
 )
@@ -26,10 +27,11 @@
 }
 
 type RequiredFileRule struct {
-	Role     string
-	AllOf    []string
-	AnyOf    []string
-	Required bool
+	Role      string
+	AllOf     []string
+	AnyOf     []string
+	Required  bool
+	Directory bool
 }
 
 type LanguageProfile struct {
diff --git a/privatevoice.src/internal/model/registry.go b/privatevoice.src/internal/model/registry.go
index 3d305c3..f561172 100644
--- a/privatevoice.src/internal/model/registry.go
+++ b/privatevoice.src/internal/model/registry.go
@@ -9,6 +9,7 @@
 	BackendSenseVoice     = "sensevoice"
 	BackendMoonshine      = "moonshine"
 	BackendNemoTransducer = "nemo_transducer"
+	BackendQwen3ASR       = "qwen3_asr"
 )
 
 var modelProfiles = map[string]ModelProfile{
@@ -130,16 +131,66 @@
 		ProviderOrder: []string{"cpu"},
 		NumThreads:    4,
 	},
+	Qwen3ASRModelID: {
+		ID:                   Qwen3ASRModelID,
+		DisplayName:          "Qwen3-ASR",
+		BackendKind:          BackendQwen3ASR,
+		Tier:                 "advanced",
+		SupportedLanguageIDs: []string{DefaultLanguageID, "zh-Hans", "zh-Hant", "yue"},
+		RecommendedFor:       []string{DefaultLanguageID},
+		Description:          "高质量中文离线模型,适合中文长句、技术词和中英混输准确率需求。",
+		ApproxSize:           "约 838 MiB 下载包,解压后约 1.9 GiB",
+		DownloadURLs: []string{
+			"https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-qwen3-asr-0.6B-int8-2026-03-25.tar.bz2",
+		},
+		InstallDirName: "qwen3-asr-0.6b",
+		RequiredFiles: []RequiredFileRule{
+			{
+				Role:     "conv_frontend",
+				AllOf:    []string{"conv_frontend.onnx"},
+				Required: true,
+			},
+			{
+				Role:     "encoder",
+				AllOf:    []string{"encoder.int8.onnx"},
+				Required: true,
+			},
+			{
+				Role:     "decoder",
+				AllOf:    []string{"decoder.int8.onnx"},
+				Required: true,
+			},
+			{
+				Role:      "tokenizer",
+				AllOf:     []string{"tokenizer"},
+				Required:  true,
+				Directory: true,
+			},
+			{
+				Role:     "tokenizer_merges",
+				AllOf:    []string{"tokenizer/merges.txt"},
+				Required: true,
+			},
+			{
+				Role:     "tokenizer_vocab",
+				AllOf:    []string{"tokenizer/vocab.json"},
+				Required: true,
+			},
+		},
+		ProviderOrder: []string{"cpu"},
+		NumThreads:    4,
+	},
 }
 
 var languageProfiles = map[string]LanguageProfile{
 	DefaultLanguageID: {
-		ID:             DefaultLanguageID,
-		DisplayName:    "Chinese",
-		NativeName:     "中文",
-		UILocale:       "zh",
-		SystemMatchers: []string{"zh", "zh-CN", "zh-Hans", "zh-Hant"},
-		DefaultModelID: DefaultModelID,
+		ID:              DefaultLanguageID,
+		DisplayName:     "Chinese",
+		NativeName:      "中文",
+		UILocale:        "zh",
+		SystemMatchers:  []string{"zh", "zh-CN", "zh-Hans", "zh-Hant"},
+		DefaultModelID:  DefaultModelID,
+		UpgradeModelIDs: []string{Qwen3ASRModelID},
 	},
 	EnglishLanguageID: {
 		ID:              EnglishLanguageID,
diff --git a/privatevoice.src/internal/model/validator.go b/privatevoice.src/internal/model/validator.go
index 0870a68..0554e03 100644
--- a/privatevoice.src/internal/model/validator.go
+++ b/privatevoice.src/internal/model/validator.go
@@ -39,7 +39,7 @@
 		if len(rule.AllOf) > 0 {
 			for _, rel := range rule.AllOf {
 				path := filepath.Join(dir, rel)
-				if !validRequiredFile(path) {
+				if !validRequiredPath(path, rule.Directory) {
 					result.Valid = false
 					result.Missing = append(result.Missing, rel)
 					continue
@@ -54,7 +54,7 @@
 			var selected string
 			for _, rel := range rule.AnyOf {
 				path := filepath.Join(dir, rel)
-				if validRequiredFile(path) {
+				if validRequiredPath(path, rule.Directory) {
 					selected = path
 					break
 				}
@@ -72,8 +72,18 @@
 }
 
 func validRequiredFile(path string) bool {
+	return validRequiredPath(path, false)
+}
+
+func validRequiredPath(path string, directory bool) bool {
 	info, err := os.Stat(path)
-	if err != nil || info.IsDir() {
+	if err != nil {
+		return false
+	}
+	if directory {
+		return info.IsDir()
+	}
+	if info.IsDir() {
 		return false
 	}
 	return info.Size() > 0
diff --git a/privatevoice.src/internal/modelselection/selection_test.go b/privatevoice.src/internal/modelselection/selection_test.go
index cd0fbc2..db311aa 100644
--- a/privatevoice.src/internal/modelselection/selection_test.go
+++ b/privatevoice.src/internal/modelselection/selection_test.go
@@ -67,6 +67,18 @@
 	}
 }
 
+func TestResolveManualUsesQwen3ASRForChinese(t *testing.T) {
+	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.Qwen3ASRModelID {
+		t.Fatalf("model id = %q, want manual selected %q", current.ModelID, model.Qwen3ASRModelID)
+	}
+}
+
 func TestResolveManualModelFallsBackWhenLanguageIncompatible(t *testing.T) {
 	cfg := config.Default()
 	cfg.LanguageMode = config.LanguageModeAuto
@@ -82,6 +94,21 @@
 	}
 }
 
+func TestResolveManualQwen3ASRFallsBackForEnglish(t *testing.T) {
+	cfg := config.Default()
+	cfg.LanguageMode = config.LanguageModeAuto
+	cfg.ModelSelectionMode = config.ModelSelectionModeManual
+	cfg.SelectedModelID = model.Qwen3ASRModelID
+
+	current := Resolve(cfg, fakeDetector{locale: "en-US"})
+	if current.ModelID != model.MoonshineModelID {
+		t.Fatalf("model id = %q, want English default %q", current.ModelID, model.MoonshineModelID)
+	}
+	if current.FallbackReason == "" {
+		t.Fatal("expected fallback reason")
+	}
+}
+
 func TestResolveInvalidManualFallsBackToLanguageDefault(t *testing.T) {
 	cfg := config.Default()
 	cfg.LanguageMode = config.LanguageModeAuto
diff --git a/privatevoice.src/scripts/build-appstore-developerid-macos.sh b/privatevoice.src/scripts/build-appstore-developerid-macos.sh
index 3f92273..244e3ff 100755
--- a/privatevoice.src/scripts/build-appstore-developerid-macos.sh
+++ b/privatevoice.src/scripts/build-appstore-developerid-macos.sh
@@ -31,6 +31,7 @@
 INFO_PLIST="$PROJECT_DIR/build/darwin/Info.plist"
 ENTITLEMENTS="$PROJECT_DIR/build/darwin/entitlements-appstore.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)"
 
 VERSION="$(defaults read "$INFO_PLIST" CFBundleShortVersionString 2>/dev/null || echo "2.1.12")"
 BUILD_ID="$(defaults read "$INFO_PLIST" CFBundleVersion 2>/dev/null || date +%Y%m%d.%H%M)"
@@ -74,7 +75,9 @@
   BUILD_DIR="$PROJECT_DIR/build/appstore-developerid/$OUT_ARCH"
   APP_BUNDLE="$BUILD_DIR/$APP_NAME.app"
   DMG_PATH="$BUILD_DIR/$APP_NAME-$VERSION-build$BUILD_ID-$OUT_ARCH-developerid-sandbox-poc.dmg"
-  SHERPA_LIB_DIR="$GOMODCACHE/github.com/k2-fsa/sherpa-onnx-go-macos@v1.12.24/lib/$SHERPA_ARCH"
+  SHERPA_LIB_DIR="$GOMODCACHE/github.com/k2-fsa/sherpa-onnx-go-macos@$SHERPA_MACOS_MODULE_VERSION/lib/$SHERPA_ARCH"
+  ONNXRUNTIME_DYLIB=""
+  ONNXRUNTIME_DYLIB_NAME=""
 }
 
 sign_plain() {
@@ -105,6 +108,9 @@
   [[ -f "$ENTITLEMENTS" ]] || fail "App Store entitlements not found at $ENTITLEMENTS"
   [[ -f "$PROJECT_DIR/build/darwin/icon.icns" ]] || fail "icon.icns not found"
   [[ -d "$SHERPA_LIB_DIR" ]] || fail "sherpa-onnx dylibs not found at $SHERPA_LIB_DIR"
+  ONNXRUNTIME_DYLIB="$(find "$SHERPA_LIB_DIR" -maxdepth 1 -type f -name 'libonnxruntime.*.dylib' | sort | tail -n 1)"
+  [[ -f "$ONNXRUNTIME_DYLIB" ]] || fail "onnxruntime dylib not found in $SHERPA_LIB_DIR"
+  ONNXRUNTIME_DYLIB_NAME="$(basename "$ONNXRUNTIME_DYLIB")"
   command -v xcrun >/dev/null || fail "xcrun not found"
   security_find_identities | grep -Fq "$DEVELOPER_IDENTITY" ||
     fail "Developer ID signing identity not found: $DEVELOPER_IDENTITY"
@@ -145,18 +151,18 @@
   cp "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
   cp "$PROJECT_DIR/build/darwin/icon.icns" "$APP_BUNDLE/Contents/Resources/icon.icns"
   cp "$SHERPA_LIB_DIR/libsherpa-onnx-c-api.dylib" "$APP_BUNDLE/Contents/Frameworks/"
-  cp "$SHERPA_LIB_DIR/libonnxruntime.1.23.2.dylib" "$APP_BUNDLE/Contents/Frameworks/"
+  cp "$ONNXRUNTIME_DYLIB" "$APP_BUNDLE/Contents/Frameworks/"
 
   step "Fixing rpaths"
   install_name_tool -add_rpath @executable_path/../Frameworks \
     "$APP_BUNDLE/Contents/MacOS/$APP_NAME" 2>/dev/null || true
   install_name_tool -id @rpath/libsherpa-onnx-c-api.dylib \
     "$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib"
-  install_name_tool -id @rpath/libonnxruntime.1.23.2.dylib \
-    "$APP_BUNDLE/Contents/Frameworks/libonnxruntime.1.23.2.dylib"
+  install_name_tool -id "@rpath/$ONNXRUNTIME_DYLIB_NAME" \
+    "$APP_BUNDLE/Contents/Frameworks/$ONNXRUNTIME_DYLIB_NAME"
 
   step "Code signing with Developer ID"
-  sign_plain "$APP_BUNDLE/Contents/Frameworks/libonnxruntime.1.23.2.dylib"
+  sign_plain "$APP_BUNDLE/Contents/Frameworks/$ONNXRUNTIME_DYLIB_NAME"
   sign_plain "$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib"
   sign_sandboxed "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
   sign_sandboxed "$APP_BUNDLE"
diff --git a/privatevoice.src/scripts/build-appstore-macos.sh b/privatevoice.src/scripts/build-appstore-macos.sh
index 25d0476..4d0e23e 100755
--- a/privatevoice.src/scripts/build-appstore-macos.sh
+++ b/privatevoice.src/scripts/build-appstore-macos.sh
@@ -23,6 +23,7 @@
 INFO_PLIST="$PROJECT_DIR/build/darwin/Info.plist"
 ENTITLEMENTS="$PROJECT_DIR/build/darwin/entitlements-appstore.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)"
 
 VERSION="$(defaults read "$INFO_PLIST" CFBundleShortVersionString 2>/dev/null || echo "2.1.12")"
 BUILD_ID="$(defaults read "$INFO_PLIST" CFBundleVersion 2>/dev/null || date +%Y%m%d.%H%M)"
@@ -58,7 +59,9 @@
 BUILD_DIR="$PROJECT_DIR/build/appstore-local/$OUT_ARCH"
 APP_BUNDLE="$BUILD_DIR/$APP_NAME.app"
 DMG_PATH="$BUILD_DIR/$APP_NAME-$VERSION-build$BUILD_ID-$OUT_ARCH-appstore-sandbox-poc.dmg"
-SHERPA_LIB_DIR="$GOMODCACHE/github.com/k2-fsa/sherpa-onnx-go-macos@v1.12.24/lib/$SHERPA_ARCH"
+SHERPA_LIB_DIR="$GOMODCACHE/github.com/k2-fsa/sherpa-onnx-go-macos@$SHERPA_MACOS_MODULE_VERSION/lib/$SHERPA_ARCH"
+ONNXRUNTIME_DYLIB=""
+ONNXRUNTIME_DYLIB_NAME=""
 
 sign_plain() {
   local item="$1"
@@ -84,6 +87,9 @@
 [[ -f "$ENTITLEMENTS" ]] || fail "App Store entitlements not found at $ENTITLEMENTS"
 [[ -f "$PROJECT_DIR/build/darwin/icon.icns" ]] || fail "icon.icns not found"
 [[ -d "$SHERPA_LIB_DIR" ]] || fail "sherpa-onnx dylibs not found at $SHERPA_LIB_DIR"
+ONNXRUNTIME_DYLIB="$(find "$SHERPA_LIB_DIR" -maxdepth 1 -type f -name 'libonnxruntime.*.dylib' | sort | tail -n 1)"
+[[ -f "$ONNXRUNTIME_DYLIB" ]] || fail "onnxruntime dylib not found in $SHERPA_LIB_DIR"
+ONNXRUNTIME_DYLIB_NAME="$(basename "$ONNXRUNTIME_DYLIB")"
 security find-identity -v -p codesigning "$KEYCHAIN" | grep -Fq "$LOCAL_IDENTITY" ||
   fail "Local signing identity not found: $LOCAL_IDENTITY"
 
@@ -118,18 +124,18 @@
 cp "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
 cp "$PROJECT_DIR/build/darwin/icon.icns" "$APP_BUNDLE/Contents/Resources/icon.icns"
 cp "$SHERPA_LIB_DIR/libsherpa-onnx-c-api.dylib" "$APP_BUNDLE/Contents/Frameworks/"
-cp "$SHERPA_LIB_DIR/libonnxruntime.1.23.2.dylib" "$APP_BUNDLE/Contents/Frameworks/"
+cp "$ONNXRUNTIME_DYLIB" "$APP_BUNDLE/Contents/Frameworks/"
 
 step "Fixing rpaths"
 install_name_tool -add_rpath @executable_path/../Frameworks \
   "$APP_BUNDLE/Contents/MacOS/$APP_NAME" 2>/dev/null || true
 install_name_tool -id @rpath/libsherpa-onnx-c-api.dylib \
   "$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib"
-install_name_tool -id @rpath/libonnxruntime.1.23.2.dylib \
-  "$APP_BUNDLE/Contents/Frameworks/libonnxruntime.1.23.2.dylib"
+install_name_tool -id "@rpath/$ONNXRUNTIME_DYLIB_NAME" \
+  "$APP_BUNDLE/Contents/Frameworks/$ONNXRUNTIME_DYLIB_NAME"
 
 step "Code signing App Sandbox POC"
-sign_plain "$APP_BUNDLE/Contents/Frameworks/libonnxruntime.1.23.2.dylib"
+sign_plain "$APP_BUNDLE/Contents/Frameworks/$ONNXRUNTIME_DYLIB_NAME"
 sign_plain "$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib"
 sign_sandboxed "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
 sign_sandboxed "$APP_BUNDLE"
diff --git a/privatevoice.src/scripts/build-local-macos.sh b/privatevoice.src/scripts/build-local-macos.sh
index 39b53c0..d86c490 100755
--- a/privatevoice.src/scripts/build-local-macos.sh
+++ b/privatevoice.src/scripts/build-local-macos.sh
@@ -24,6 +24,7 @@
 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)"
 
 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)"
@@ -77,7 +78,9 @@
 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"
-SHERPA_LIB_DIR="$GOMODCACHE/github.com/k2-fsa/sherpa-onnx-go-macos@v1.12.24/lib/$SHERPA_ARCH"
+SHERPA_LIB_DIR="$GOMODCACHE/github.com/k2-fsa/sherpa-onnx-go-macos@$SHERPA_MACOS_MODULE_VERSION/lib/$SHERPA_ARCH"
+ONNXRUNTIME_DYLIB=""
+ONNXRUNTIME_DYLIB_NAME=""
 
 ensure_local_identity() {
   if [[ -z "$LOCAL_IDENTITY" ]]; then
@@ -181,6 +184,9 @@
 [[ -f "$ENTITLEMENTS" ]] || fail "entitlements.plist not found at $ENTITLEMENTS"
 [[ -f "$PROJECT_DIR/build/darwin/icon.icns" ]] || fail "icon.icns not found"
 [[ -d "$SHERPA_LIB_DIR" ]] || fail "sherpa-onnx dylibs not found at $SHERPA_LIB_DIR"
+ONNXRUNTIME_DYLIB="$(find "$SHERPA_LIB_DIR" -maxdepth 1 -type f -name 'libonnxruntime.*.dylib' | sort | tail -n 1)"
+[[ -f "$ONNXRUNTIME_DYLIB" ]] || fail "onnxruntime dylib not found in $SHERPA_LIB_DIR"
+ONNXRUNTIME_DYLIB_NAME="$(basename "$ONNXRUNTIME_DYLIB")"
 ensure_local_identity
 
 step "Cleaning old local build"
@@ -209,18 +215,18 @@
 cp "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
 cp "$PROJECT_DIR/build/darwin/icon.icns" "$APP_BUNDLE/Contents/Resources/icon.icns"
 cp "$SHERPA_LIB_DIR/libsherpa-onnx-c-api.dylib" "$APP_BUNDLE/Contents/Frameworks/"
-cp "$SHERPA_LIB_DIR/libonnxruntime.1.23.2.dylib" "$APP_BUNDLE/Contents/Frameworks/"
+cp "$ONNXRUNTIME_DYLIB" "$APP_BUNDLE/Contents/Frameworks/"
 
 step "Fixing rpaths"
 install_name_tool -add_rpath @executable_path/../Frameworks \
   "$APP_BUNDLE/Contents/MacOS/$APP_NAME" 2>/dev/null || true
 install_name_tool -id @rpath/libsherpa-onnx-c-api.dylib \
   "$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib"
-install_name_tool -id @rpath/libonnxruntime.1.23.2.dylib \
-  "$APP_BUNDLE/Contents/Frameworks/libonnxruntime.1.23.2.dylib"
+install_name_tool -id "@rpath/$ONNXRUNTIME_DYLIB_NAME" \
+  "$APP_BUNDLE/Contents/Frameworks/$ONNXRUNTIME_DYLIB_NAME"
 
 step "Code signing with stable local identity"
-sign_item "$APP_BUNDLE/Contents/Frameworks/libonnxruntime.1.23.2.dylib"
+sign_item "$APP_BUNDLE/Contents/Frameworks/$ONNXRUNTIME_DYLIB_NAME"
 sign_item "$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib"
 sign_item "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
 sign_item "$APP_BUNDLE"
diff --git a/privatevoice.src/scripts/build-release-macos.sh b/privatevoice.src/scripts/build-release-macos.sh
index 8eefac1..3f33265 100755
--- a/privatevoice.src/scripts/build-release-macos.sh
+++ b/privatevoice.src/scripts/build-release-macos.sh
@@ -30,6 +30,9 @@
 ENTITLEMENTS="$PROJECT_DIR/build/darwin/entitlements.plist"
 INFO_PLIST="$PROJECT_DIR/build/darwin/Info.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)"
+ONNXRUNTIME_DYLIB=""
+ONNXRUNTIME_DYLIB_NAME=""
 
 # ── Architecture mapping ────────────────────────────────────────────────────
 resolve_arch() {
@@ -49,7 +52,7 @@
       fail "Unknown architecture: $arch (use arm64 or x86_64)"
       ;;
   esac
-  SHERPA_LIB_DIR="$GOMODCACHE/github.com/k2-fsa/sherpa-onnx-go-macos@v1.12.24/lib/$SHERPA_ARCH"
+  SHERPA_LIB_DIR="$GOMODCACHE/github.com/k2-fsa/sherpa-onnx-go-macos@$SHERPA_MACOS_MODULE_VERSION/lib/$SHERPA_ARCH"
   DMG_NAME="${APP_NAME}-v${VERSION}-${DMG_ARCH}.dmg"
   BUILD_DIR="$PROJECT_DIR/build/release/${DMG_ARCH}"
   APP_BUNDLE="$BUILD_DIR/${APP_NAME}.app"
@@ -78,6 +81,9 @@
 # ── Preflight checks ─────────────────────────────────────────────────────────
 step "Preflight checks"
 [[ -d "$SHERPA_LIB_DIR" ]] || fail "sherpa-onnx dylibs not found at $SHERPA_LIB_DIR"
+ONNXRUNTIME_DYLIB="$(find "$SHERPA_LIB_DIR" -maxdepth 1 -type f -name 'libonnxruntime.*.dylib' | sort | tail -n 1)"
+[[ -f "$ONNXRUNTIME_DYLIB" ]] || fail "onnxruntime dylib not found in $SHERPA_LIB_DIR"
+ONNXRUNTIME_DYLIB_NAME="$(basename "$ONNXRUNTIME_DYLIB")"
 [[ -f "$ENTITLEMENTS" ]]   || fail "entitlements.plist not found at $ENTITLEMENTS"
 [[ -f "$INFO_PLIST" ]]     || fail "Info.plist not found at $INFO_PLIST"
 security find-identity -v -p codesigning | grep -q "$IDENTITY" || fail "Signing identity not found in keychain"
@@ -113,7 +119,7 @@
 
 # Copy dylibs
 cp "$SHERPA_LIB_DIR/libsherpa-onnx-c-api.dylib"   "$APP_BUNDLE/Contents/Frameworks/"
-cp "$SHERPA_LIB_DIR/libonnxruntime.1.23.2.dylib"   "$APP_BUNDLE/Contents/Frameworks/"
+cp "$ONNXRUNTIME_DYLIB"                            "$APP_BUNDLE/Contents/Frameworks/"
 
 # ── Step 5: Fix rpaths ───────────────────────────────────────────────────────
 step "Fixing rpaths with install_name_tool"
@@ -126,13 +132,17 @@
 install_name_tool -id @rpath/libsherpa-onnx-c-api.dylib \
   "$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib"
 
-install_name_tool -id @rpath/libonnxruntime.1.23.2.dylib \
-  "$APP_BUNDLE/Contents/Frameworks/libonnxruntime.1.23.2.dylib"
+install_name_tool -id "@rpath/$ONNXRUNTIME_DYLIB_NAME" \
+  "$APP_BUNDLE/Contents/Frameworks/$ONNXRUNTIME_DYLIB_NAME"
 
 # Fix libsherpa's dependency on libonnxruntime
 install_name_tool -change \
   @rpath/libonnxruntime.1.23.2.dylib \
-  @rpath/libonnxruntime.1.23.2.dylib \
+  "@rpath/$ONNXRUNTIME_DYLIB_NAME" \
+  "$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib" 2>/dev/null || true
+install_name_tool -change \
+  @rpath/libonnxruntime.1.24.4.dylib \
+  "@rpath/$ONNXRUNTIME_DYLIB_NAME" \
   "$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib" 2>/dev/null || true
 
 # ── Step 6: Code sign ────────────────────────────────────────────────────────
@@ -142,7 +152,7 @@
 codesign --force --options runtime \
   --entitlements "$ENTITLEMENTS" \
   --sign "$IDENTITY" \
-  "$APP_BUNDLE/Contents/Frameworks/libonnxruntime.1.23.2.dylib"
+  "$APP_BUNDLE/Contents/Frameworks/$ONNXRUNTIME_DYLIB_NAME"
 
 codesign --force --options runtime \
   --entitlements "$ENTITLEMENTS" \

--
Gitblit v1.9.3