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