From f66a79670c444010a59b8513c9f61257ebc7d9b6 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 03 Jun 2026 11:50:35 +0800
Subject: [PATCH] Prepare 2.1.20 Round 2 QA package

---
 02-P-NBL/round-2/todo-list.md                           |   36 +++---
 privatevoice.src/frontend/package.json                  |    2 
 privatevoice.src/app.go                                 |    4 
 privatevoice.src/build/config.yml                       |    2 
 02-P-NBL/round-2/logs/npm-run-build-20260603-2.1.20.log |   38 +++++++
 privatevoice.src/frontend/package-lock.json             |    4 
 02-P-NBL/round-2/logs/go-test-all-20260603-2.1.20.log   |   22 ++++
 02-P-NBL/round-2/qa-report-current.md                   |  121 ++++++++++++++++++++++++
 CHANGELOG.md                                            |   18 +++
 privatevoice.src/build/darwin/Info.plist                |    4 
 privatevoice.src/build/windows/wails.exe.manifest       |    2 
 11 files changed, 226 insertions(+), 27 deletions(-)

diff --git a/02-P-NBL/round-2/logs/go-test-all-20260603-2.1.20.log b/02-P-NBL/round-2/logs/go-test-all-20260603-2.1.20.log
new file mode 100644
index 0000000..8153bc6
--- /dev/null
+++ b/02-P-NBL/round-2/logs/go-test-all-20260603-2.1.20.log
@@ -0,0 +1,22 @@
+?   	voicesnap	[no test files]
+?   	voicesnap/internal/audio	[no test files]
+ok  	voicesnap/internal/config	1.315s
+ok  	voicesnap/internal/correctioncsv	3.369s
+?   	voicesnap/internal/dock	[no test files]
+?   	voicesnap/internal/engine	[no test files]
+ok  	voicesnap/internal/history	2.037s
+?   	voicesnap/internal/hotkey	[no test files]
+?   	voicesnap/internal/input	[no test files]
+ok  	voicesnap/internal/language	2.710s
+?   	voicesnap/internal/logger	[no test files]
+ok  	voicesnap/internal/model	4.887s
+?   	voicesnap/internal/overlay	[no test files]
+?   	voicesnap/internal/paths	[no test files]
+?   	voicesnap/internal/permissions	[no test files]
+?   	voicesnap/internal/singleinstance	[no test files]
+?   	voicesnap/internal/sound	[no test files]
+?   	voicesnap/internal/startup	[no test files]
+?   	voicesnap/internal/textproc	[no test files]
+?   	voicesnap/internal/updater	[no test files]
+ok  	voicesnap/internal/userdict	4.164s
+?   	voicesnap/services	[no test files]
diff --git a/02-P-NBL/round-2/logs/npm-run-build-20260603-2.1.20.log b/02-P-NBL/round-2/logs/npm-run-build-20260603-2.1.20.log
new file mode 100644
index 0000000..770a397
--- /dev/null
+++ b/02-P-NBL/round-2/logs/npm-run-build-20260603-2.1.20.log
@@ -0,0 +1,38 @@
+
+> privatevoice-input-frontend@2.1.20 build
+> vite build
+
+vite v6.4.1 building for production...
+transforming...
+11:46:45 AM [vite-plugin-svelte] src/components/update/UpdateDialog.svelte:28:0 Elements with the 'dialog' interactive role must have a tabindex value
+https://svelte.dev/e/a11y_interactive_supports_focus
+26: 
+27: <!-- svelte-ignore a11y_click_events_have_key_events -->
+28: <div class="overlay" onclick={onclose} role="dialog">
+          ^
+29:   <!-- svelte-ignore a11y_click_events_have_key_events -->
+30:   <div class="dialog" onclick={(e) => e.stopPropagation()} role="document">
+11:46:45 AM [vite-plugin-svelte] src/components/update/UpdateDialog.svelte:30:2 Non-interactive element `<div>` should not be assigned mouse or keyboard event listeners
+https://svelte.dev/e/a11y_no_noninteractive_element_interactions
+28: <div class="overlay" onclick={onclose} role="dialog">
+29:   <!-- svelte-ignore a11y_click_events_have_key_events -->
+30:   <div class="dialog" onclick={(e) => e.stopPropagation()} role="document">
+            ^
+31:     <h2 class="title">{t('update.title')}</h2>
+32:     <p class="version">{t('update.version', { version: info.version })}</p>
+11:46:45 AM [vite-plugin-svelte] src/components/shared/ToggleSwitch.svelte:17:0 Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute
+https://svelte.dev/e/a11y_consider_explicit_label
+15: </script>
+16: 
+17: <button
+           ^
+18:   class="toggle"
+19:   class:active={checked}
+✓ 163 modules transformed.
+rendering chunks...
+computing gzip size...
+dist/index.html                      0.41 kB │ gzip:  0.28 kB
+dist/assets/app-icon-Bs-pS5hx.png  400.98 kB
+dist/assets/index-p3GQqwqB.css      33.39 kB │ gzip:  5.73 kB
+dist/assets/index-yZ25Q-mY.js      137.08 kB │ gzip: 45.83 kB
+✓ built in 783ms
diff --git a/02-P-NBL/round-2/qa-report-current.md b/02-P-NBL/round-2/qa-report-current.md
new file mode 100644
index 0000000..c0026a9
--- /dev/null
+++ b/02-P-NBL/round-2/qa-report-current.md
@@ -0,0 +1,121 @@
+# Round 2 QA 报告:语言检测与语言版本设置
+
+状态:功能性 QA PASS,可进入下一步交付门禁
+日期:2026-06-03
+测试人:Codex
+
+## 1. 测试对象
+
+| 项目 | 内容 |
+|---|---|
+| worktree | `/Users/ar/Projects/VoiceSnap-2.1.1-Round2` |
+| branch | `codex/round2-language-settings` |
+| 实现基线 commit | `516375d` |
+| 最终测试包 commit | 以 `RELEASE_MANIFEST.md` 为准 |
+| version | `2.1.20` |
+| build | `20260603.1145` |
+| 本地 App | `privatevoice.src/build/local/arm64/PrivateVoice Input.app` |
+| 本地 DMG | 最终路径以 `RELEASE_MANIFEST.md` 为准 |
+| DMG SHA256 | 最终 SHA256 以 `RELEASE_MANIFEST.md` 为准 |
+
+## 2. 自动化与构建结果
+
+| 项目 | 结果 | 证据 |
+|---|---|---|
+| `npm run build` | PASS | `logs/npm-run-build-20260603.log` |
+| `go test ./... -count=1` | PASS | `logs/go-test-all-20260603.log` |
+| 本地 App 构建与签名 | PASS | `logs/qa-runtime/build-local-round2-20260603.014931.log` |
+| 本地 DMG verify | PASS | `logs/qa-runtime/build-local-round2-20260603.014931.log` |
+
+说明:前端构建仍有既有 Svelte a11y warning,来源为旧组件 `UpdateDialog.svelte`、`ToggleSwitch.svelte`,本轮未新增阻断。
+
+## 3. 真机测试结果
+
+| ID | 场景 | 结果 | 证据 |
+|---|---|---|---|
+| R2-QA-001 | 老配置升级,无 `LanguageMode` / `LanguageID` | PASS | baseline config 无语言字段;Round 2 App 正常启动 |
+| R2-QA-002 | 中文系统 auto | PASS | `config.json` 默认 `LanguageMode=auto`、`LanguageID=zh-CN` |
+| R2-QA-003 | English 系统 auto | PASS by automated test | `go test ./...` 覆盖 detector normalize |
+| R2-QA-004 | 其他系统语言 auto fallback | PASS by automated test | `go test ./...` 覆盖 fallback 到 English |
+| R2-QA-005 | 手动中文 | PASS | `round2-screen-20260603.014931-zh-manual.png`;配置写入 `manual/zh-CN` |
+| R2-QA-006 | 手动 English | PASS | `round2-screen-20260603.014931-english6.png`;配置写入 `manual/en` |
+| R2-QA-007 | 切回自动 | PASS | `round2-screen-20260603.014931-chinese.png`;配置写入 `auto/zh-CN` |
+| R2-QA-008 | 重启持久化 | PASS | 重启后配置仍为 `manual/zh-CN` |
+| R2-QA-009 | 非法 LanguageMode fallback | PASS by automated test | `go test ./...` |
+| R2-QA-010 | 非法 LanguageID fallback | PASS by automated test | `go test ./...` |
+| R2-QA-011 | 语言切换不改模型 | PASS | `SelectedModelID` 始终为 `sensevoice-zh` |
+| R2-QA-012 | 已有模型时语言切换不下载模型 | PASS | 真实用户模型 hash 不变;日志未出现因语言切换触发下载 |
+| R2-QA-013 | 语言切换不删模型 | PASS | `model.int8.onnx`、`tokens.txt` hash 与 baseline 一致 |
+| R2-QA-014 | 词库保留 | PASS | `userdict.json` hash 与 baseline 一致;replacement 数量 149 |
+| R2-QA-015 | 历史保留 | PASS | `history.json` hash 与 baseline 一致;entries 数量 1708 |
+| R2-QA-016 | 热键保留 | PASS | config 热键字段未丢失;断网真实语音输入已用 `R-Alt` 触发成功 |
+| R2-QA-017 | 断网已有模型识别 | PASS | `logs/qa-runtime/offline-voice-final-20260603.014931.log`;断网期间识别并写入 history |
+| R2-QA-018 | UI 不暴露新模型 | PASS | 设置页只显示现有 SenseVoice 引擎 |
+| R2-QA-019 | English UI 文案 | PASS | `round2-screen-20260603.014931-english6.png` |
+| R2-QA-020 | 中文 UI 文案 | PASS | `round2-screen-20260603.014931-zh-manual.png` |
+
+## 4. 数据完整性
+
+测试前已备份真实用户数据:
+
+`/Users/ar/Projects/VoiceSnap-2.1.1-Round2/QA-backups/round2-pre-qa-20260603.014931`
+
+语言切换阶段完成后,以下文件与 baseline SHA256 完全一致:
+
+- `userdict.json`
+- `history.json`
+- `models/sensevoice/model.int8.onnx`
+- `models/sensevoice/tokens.txt`
+
+`config.json` 有预期变化:新增/写入 `LanguageMode`、`LanguageID`、`SelectedModelID`。
+
+断网真实语音输入复测后,`history.json` 出现预期变化:新增一条识别记录。
+
+## 5. 新用户测试
+
+使用临时 HOME 启动本地 App,未挪动真实 App Support:
+
+`logs/qa-runtime/new-user-home-20260603.014931`
+
+结果:
+
+- 新用户默认配置为 `LanguageMode=auto`、`LanguageID=zh-CN`、`SelectedModelID=sensevoice-zh`。
+- 干净用户首次启动会自动下载 SenseVoice;下载发生在临时 QA 目录,不影响真实用户模型目录。
+- 下载完成后 ASR engine ready。
+
+## 6. 断网启动检查
+
+证据:`logs/qa-runtime/offline-start-check-20260603.014931.log`
+
+结果:
+
+- Wi-Fi 从 On 切到 Off。
+- `curl https://www.apple.com` 返回 `Could not resolve host`,确认断网。
+- 断网状态下重启本地 App。
+- App 使用已有真实 SenseVoice 模型启动,日志显示 `ASR engine ready: SenseVoice · CoreML (Apple Neural Engine)`。
+- Wi-Fi 已恢复为 On。
+
+## 7. 断网真实语音输入复测
+
+证据:`logs/qa-runtime/offline-voice-final-20260603.014931.log`
+
+结果:
+
+- Wi-Fi 从 On 切到 Off。
+- 断网探测返回 `Could not resolve host: www.apple.com`。
+- 用户在断网窗口内按住 `R-Alt` 说测试句。
+- 日志显示 2026-06-03 11:40:36 开始录音,2026-06-03 11:40:41 停止录音并完成识别。
+- 识别文本:`这是 mac 电脑用 SenseVoice 做离线识别。`
+- `history.json` 条目数从 1710 增加到 1711。
+- `history.json` 最新条目 `entries[0]` 为:`这是 mac 电脑用 SenseVoice 做离线识别。`
+- Wi-Fi 已恢复为 On,并通过联网探测。
+
+## 8. 阻断项
+
+无。
+
+## 9. 当前结论
+
+Round 2 的语言设置、i18n 切换、持久化、新用户默认值、老用户数据保护、模型不破坏、断网启动均通过。
+
+Round 2 功能性 QA PASS。
diff --git a/02-P-NBL/round-2/todo-list.md b/02-P-NBL/round-2/todo-list.md
index f379351..abbc03f 100644
--- a/02-P-NBL/round-2/todo-list.md
+++ b/02-P-NBL/round-2/todo-list.md
@@ -1,6 +1,6 @@
 # Round 2 To Do List
 
-状态:待启动
+状态:功能性 QA PASS;等待交付门禁/测试包流程
 所属子项目:`02-P-NBL`
 Round:Round 2
 日期:2026-06-03
@@ -30,24 +30,24 @@
 
 ## 3. QA 准备
 
-- [ ] 准备老用户升级测试数据。
-- [ ] 准备新用户干净安装测试数据。
-- [ ] 准备 detector mock 或测试开关。
-- [ ] 准备非法 config 测试样本。
-- [ ] 准备断网识别回归步骤。
+- [x] 准备老用户升级测试数据。
+- [x] 准备新用户干净安装测试数据。
+- [x] 准备 detector mock 或测试开关。
+- [x] 准备非法 config 测试样本。
+- [x] 准备断网识别回归步骤。
 - [x] 准备 QA 预检记录。
-- [ ] 准备 QA 报告模板。
+- [x] 准备 QA 报告模板。
 
 ## 4. 验收
 
-- [ ] 中文系统 auto 解析为中文。
-- [ ] English 系统 auto 解析为 English。
-- [ ] 其他系统语言 auto 解析为 English。
-- [ ] 手动中文持久化。
-- [ ] 手动 English 持久化。
-- [ ] 语言切换不触发模型下载。
-- [ ] 语言切换不破坏 SenseVoice 模型。
-- [ ] 语言切换不破坏词库。
-- [ ] 语言切换不破坏历史。
-- [ ] 断网识别回归通过。
-- [ ] QA 报告确认功能性 PASS。
+- [x] 中文系统 auto 解析为中文。
+- [x] English 系统 auto 解析为 English。
+- [x] 其他系统语言 auto 解析为 English。
+- [x] 手动中文持久化。
+- [x] 手动 English 持久化。
+- [x] 语言切换不触发模型下载。
+- [x] 语言切换不破坏 SenseVoice 模型。
+- [x] 语言切换不破坏词库。
+- [x] 语言切换不破坏历史。
+- [x] 断网识别回归通过。
+- [x] QA 报告确认功能性 PASS。
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30b7013..8f791c4 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
 # Changelog
 
+## v2.1.20 (2026-06-03)
+
+### QA 候选包
+
+- **Round 2 语言版本设置**:设置页新增“语言版本”,支持自动、中文、English 三种模式。
+- **系统语言解析**:新增语言 profile 和 resolver,中文系统默认中文,English 系统默认 English,其他语言默认 English。
+- **前端 i18n 切换**:语言切换后设置页、侧边栏、关于页等关键文案立即切换。
+- **老用户兼容**:旧 `config.json` 缺少 `LanguageMode` / `LanguageID` 时可正常启动,并自动 fallback。
+- **模型隔离**:本轮语言切换不引入 Moonshine / Parakeet / Qwen3-ASR,不改写或删除 SenseVoice 模型。
+- **QA 资料**:新增 `02-P-NBL/round-2` 下 PRD、研发拆解、QA 计划、QA 报告和验收记录。
+
+### 构建
+
+- build: `20260603.1145`
+- 说明: 本版本为 Round 2 QA 通过后的测试包候选,已完成老用户升级、新用户临时 HOME、语言切换、重启持久化、断网启动和断网真实语音输入复测。
+
+---
+
 ## v2.1.19 (2026-06-02)
 
 ### QA 候选包
diff --git a/privatevoice.src/app.go b/privatevoice.src/app.go
index 552a49d..8a39b1e 100755
--- a/privatevoice.src/app.go
+++ b/privatevoice.src/app.go
@@ -27,8 +27,8 @@
 )
 
 const (
-	appVersion        = "2.1.19"
-	appBuild          = "20260602.2022"
+	appVersion        = "2.1.20"
+	appBuild          = "20260603.1145"
 	appDisplayVersion = appVersion + " (build " + appBuild + ")"
 	appName           = "PrivateVoice Input"
 
diff --git a/privatevoice.src/build/config.yml b/privatevoice.src/build/config.yml
index 24b9e8c..9586ab2 100755
--- a/privatevoice.src/build/config.yml
+++ b/privatevoice.src/build/config.yml
@@ -7,7 +7,7 @@
   description: "Private offline voice input tool"
   copyright: "(c) 2026, PrivateVoice"
   comments: "Private.Offline"
-  version: "2.1.19"
+  version: "2.1.20"
 
 dev_mode:
   root_path: .
diff --git a/privatevoice.src/build/darwin/Info.plist b/privatevoice.src/build/darwin/Info.plist
index 0ccba09..ad2819a 100755
--- a/privatevoice.src/build/darwin/Info.plist
+++ b/privatevoice.src/build/darwin/Info.plist
@@ -15,9 +15,9 @@
     <key>CFBundlePackageType</key>
     <string>APPL</string>
     <key>CFBundleShortVersionString</key>
-    <string>2.1.19</string>
+    <string>2.1.20</string>
     <key>CFBundleVersion</key>
-    <string>20260602.2022</string>
+    <string>20260603.1145</string>
     <key>LSMinimumSystemVersion</key>
     <string>11.0</string>
     <key>NSMicrophoneUsageDescription</key>
diff --git a/privatevoice.src/build/windows/wails.exe.manifest b/privatevoice.src/build/windows/wails.exe.manifest
index 2d78d84..3ad227e 100755
--- a/privatevoice.src/build/windows/wails.exe.manifest
+++ b/privatevoice.src/build/windows/wails.exe.manifest
@@ -3,7 +3,7 @@
   <assemblyIdentity
     type="win32"
     name="PrivateVoice.Input"
-    version="2.1.19.0"
+    version="2.1.20.0"
     processorArchitecture="*"/>
   <dependency>
     <dependentAssembly>
diff --git a/privatevoice.src/frontend/package-lock.json b/privatevoice.src/frontend/package-lock.json
index adbaf04..706ec09 100755
--- a/privatevoice.src/frontend/package-lock.json
+++ b/privatevoice.src/frontend/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "privatevoice-input-frontend",
-  "version": "2.1.19",
+  "version": "2.1.20",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "privatevoice-input-frontend",
-      "version": "2.1.19",
+      "version": "2.1.20",
       "dependencies": {
         "@wailsio/runtime": "latest"
       },
diff --git a/privatevoice.src/frontend/package.json b/privatevoice.src/frontend/package.json
index 4dfbb55..8812421 100755
--- a/privatevoice.src/frontend/package.json
+++ b/privatevoice.src/frontend/package.json
@@ -1,7 +1,7 @@
 {
   "name": "privatevoice-input-frontend",
   "private": true,
-  "version": "2.1.19",
+  "version": "2.1.20",
   "type": "module",
   "scripts": {
     "dev": "vite dev",

--
Gitblit v1.9.3