5 files modified
1 files deleted
346 files renamed
32 files added
2626 ■■■■ changed files
.codebase-memory/.gitattributes 3 ●●●●● patch | view | raw | blame | history
.codebase-memory/artifact.json 11 ●●●●● patch | view | raw | blame | history
.codebase-memory/graph.db.zst patch | view | raw | blame | history
.gitignore 28 ●●●●● patch | view | raw | blame | history
AGENTS.md 69 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/Taskfile.yml patch | view | raw | blame | history
C1.source/privatevoice.src/app.go 260 ●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/app_live_caption_test.go 101 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/build/Taskfile.yml patch | view | raw | blame | history
C1.source/privatevoice.src/build/config.yml 2 ●●● patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/Info.plist 4 ●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/Taskfile.yml patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/VoiceSnap-approved.icns patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/VoiceSnap-macos26-mic.icns patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/VoiceSnap-refined-mic.icns patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/VoiceSnap-selected-B-shadow.icns patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/app_icon.png patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/app_icon_source.png patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/approved_app_icon.png patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/archive/legacy-icons/VoiceSnap-original.icns patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/en.lproj/InfoPlist.strings patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/entitlements-appstore.plist patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/entitlements.plist patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/icon.icns patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/macos26_mic_app_icon.png patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/original_app_icon.png patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/refined_mic_app_icon.png patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/tray_icon.png patch | view | raw | blame | history
C1.source/privatevoice.src/build/darwin/zh-Hans.lproj/InfoPlist.strings patch | view | raw | blame | history
C1.source/privatevoice.src/build/linux/privatevoice-input.desktop patch | view | raw | blame | history
C1.source/privatevoice.src/build/windows/Taskfile.yml patch | view | raw | blame | history
C1.source/privatevoice.src/build/windows/icon.ico patch | view | raw | blame | history
C1.source/privatevoice.src/build/windows/wails.exe.manifest patch | view | raw | blame | history
C1.source/privatevoice.src/cmd/benchmark-recognition/main.go 115 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_A_pure-blue-mic.png patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_A_pure-blue-mic.svg patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_B_shadow_preview.png patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_B_shadow_preview.svg patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_B_thin-blue-glyph.png patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_B_thin-blue-glyph.svg patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_C_taglauncher-style.png patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_C_taglauncher-style.svg patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_D_glass-circle.png patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_D_glass-circle.svg patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_E_compact-liquid.png patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_E_compact-liquid.svg patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_candidates_preview.png patch | view | raw | blame | history
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_candidates_preview.svg patch | view | raw | blame | history
C1.source/privatevoice.src/embed.go patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/index.html patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/instructions.md patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/package-lock.json 4 ●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/package.json 2 ●●● patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/App.svelte 40 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/assets/app-icon.png patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/indicator/FloatingIndicator.svelte patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/onboarding/OnboardingView.svelte 9 ●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/settings/AboutPage.svelte patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/settings/CorrectionImportPreviewDialog.svelte patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/settings/GeneralPage.svelte 22 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/settings/HistoryPage.svelte patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/settings/HotkeysPage.svelte 22 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/settings/LanguagePage.svelte 16 ●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/settings/PermissionsPage.svelte 17 ●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/settings/SettingsWindow.svelte 5 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/settings/UserDictPage.svelte patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/shared/AppIcon.svelte patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/shared/ToggleSwitch.svelte patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/components/update/UpdateDialog.svelte patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/i18n/de.json patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/i18n/en.json patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/i18n/es.json patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/i18n/fr.json patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/i18n/index.ts patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/i18n/it.json patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/i18n/ja.json patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/i18n/ko.json patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/i18n/pt.json patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/i18n/zh.json patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/stores/app.ts patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/stores/config.ts patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/stores/indicator.ts patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/lib/stores/update.ts patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/main.ts patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/styles/global.css patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/src/vite-env.d.ts patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/svelte.config.js patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/tsconfig.json patch | view | raw | blame | history
C1.source/privatevoice.src/frontend/vite.config.ts patch | view | raw | blame | history
C1.source/privatevoice.src/go.mod patch | view | raw | blame | history
C1.source/privatevoice.src/go.monterey.mod patch | view | raw | blame | history
C1.source/privatevoice.src/go.monterey.sum patch | view | raw | blame | history
C1.source/privatevoice.src/go.sum patch | view | raw | blame | history
C1.source/privatevoice.src/instructions.md patch | view | raw | blame | history
C1.source/privatevoice.src/internal/audio/recorder.go 41 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/audio/recorder_test.go 21 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/config/config.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/config/config_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/correctioncsv/correctioncsv.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/correctioncsv/correctioncsv_e2e_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/correctioncsv/correctioncsv_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/dock/dock.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/dock/dock_darwin.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/dock/dock_other.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/engine/engine.go 14 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/engine/engine_darwin.go 58 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/engine/engine_darwin_qwen3.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/engine/engine_darwin_qwen3_monterey.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/engine/engine_darwin_qwen3_monterey_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/engine/engine_darwin_qwen3_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/engine/engine_darwin_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/engine/engine_linux.go 28 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/engine/engine_windows.go 28 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/engine/release_tail.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/history/history.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/history/history_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/hotkey/hotkey.go 2 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/hotkey/hotkey_darwin.go 41 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/hotkey/hotkey_linux.go 2 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/hotkey/hotkey_windows.go 2 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/input/paste.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/input/paste_darwin.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/input/paste_darwin_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/input/paste_linux.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/input/paste_windows.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/language/detect.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/language/detect_darwin.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/language/detect_other.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/language/language_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/language/resolver.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/logger/logger.go 109 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/model/build_capabilities.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/model/build_capabilities_default.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/model/build_capabilities_monterey.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/model/build_capabilities_windows.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/model/downloader.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/model/install_state.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/model/model_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/model/profile.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/model/registry.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/model/resolver.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/model/validator.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/modelselection/selection.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/modelselection/selection_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/overlay/overlay.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/overlay/overlay_darwin.go 35 ●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/overlay/overlay_other.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/overlay/overlay_windows.go 21 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/internal/paths/paths.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/permissions/permissions.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/permissions/permissions_darwin.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/permissions/permissions_other.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/singleinstance/singleinstance.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/singleinstance/singleinstance_unix.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/singleinstance/singleinstance_windows.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/sound/sound_other.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/sound/sound_windows.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/startup/startup.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/startup/startup_darwin.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/startup/startup_darwin_appstore.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/startup/startup_linux.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/startup/startup_windows.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/textproc/textproc.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/textproc/textproc_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/updater/updater.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/userdict/userdict.go patch | view | raw | blame | history
C1.source/privatevoice.src/internal/userdict/userdict_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/locale_other.go patch | view | raw | blame | history
C1.source/privatevoice.src/locale_windows.go patch | view | raw | blame | history
C1.source/privatevoice.src/main.go patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/QA-WINDOWS-PREVIEW.ps1 patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/RUN-WINDOWS-QA.cmd patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/benchmark-recognition.sh 21 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/build-appstore-developerid-macos.sh patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/build-appstore-macos.sh patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/build-local-macos-monterey.sh patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/build-local-macos-universal.sh patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/build-local-macos.sh patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/build-macappstore-pkg-macos.sh patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/build-release-macos.sh patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/build-windows-preview.sh 4 ●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/install-x-asr-zh-en-960ms.sh patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/qa-settings-window-macos.sh patch | view | raw | blame | history
C1.source/privatevoice.src/scripts/verify-windows-qa-evidence.sh patch | view | raw | blame | history
C1.source/privatevoice.src/services/app_service.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/audio_service.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/config_service.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/correction_csv_service.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/diagnostics_service.go 196 ●●●●● patch | view | raw | blame | history
C1.source/privatevoice.src/services/engine_service.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/engine_service_qwen3_default_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/engine_service_qwen3_monterey_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/engine_service_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/engine_service_windows_test.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/history_service.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/hotkey_service.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/permission_service.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/updater_service.go patch | view | raw | blame | history
C1.source/privatevoice.src/services/userdict_service.go patch | view | raw | blame | history
C1.source/privatevoice.src/system_load_darwin.go patch | view | raw | blame | history
C1.source/privatevoice.src/system_load_other.go patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/README.md patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/models-incomplete-missing-model/sensevoice-zh/tokens.txt patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/models-incomplete-missing-tokens/sensevoice-zh/model.int8.onnx patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/models-valid-legacy/sensevoice/model.int8.onnx patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/models-valid-legacy/sensevoice/tokens.txt patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/models-valid-new/sensevoice-zh/model.int8.onnx patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/models-valid-new/sensevoice-zh/tokens.txt patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/models-zero-byte/sensevoice-zh/tokens.txt patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/state-invalid-json/state.json patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/state-points-to-broken/broken/tokens.txt patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/state-points-to-broken/sensevoice/model.int8.onnx patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/state-points-to-broken/sensevoice/tokens.txt patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/state-points-to-broken/state.json patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/state-points-to-legacy/sensevoice/model.int8.onnx patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/state-points-to-legacy/sensevoice/tokens.txt patch | view | raw | blame | history
C1.source/privatevoice.src/testdata/round1/state-points-to-legacy/state.json patch | view | raw | blame | history
C1.source/privatevoice.src/voicesnap.rc patch | view | raw | blame | history
C1.source/privatevoice.src/wails.json patch | view | raw | blame | history
C2.builds/PrivateVoice-Dictation-2.2.0-build20260702.0303/RELEASE_MANIFEST.md 66 ●●●●● patch | view | raw | blame | history
C2.builds/PrivateVoice-Dictation-2.2.0-build20260702.0303/SHA256SUMS.txt 4 ●●●● patch | view | raw | blame | history
C2.builds/PrivateVoice-Dictation-2.2.2-build20260703.0335/RELEASE_MANIFEST.md 106 ●●●●● patch | view | raw | blame | history
C2.builds/PrivateVoice-Dictation-2.2.2-build20260703.0335/SHA256SUMS.txt 4 ●●●● patch | view | raw | blame | history
C2.builds/README.md 13 ●●●●● patch | view | raw | blame | history
C3.tools/README.md 19 ●●●●● patch | view | raw | blame | history
CHANGELOG.md 16 ●●●●● patch | view | raw | blame | history
CODEGRAPH.md 96 ●●●● patch | view | raw | blame | history
K1.业务知识/成果地图.md 41 ●●●●● patch | view | raw | blame | history
K2.项目管理/QA地图.md 48 ●●●●● patch | view | raw | blame | history
K2.项目管理/R221-1-开发自查.md 101 ●●●●● patch | view | raw | blame | history
K2.项目管理/R221-3-QA-evidence.md 96 ●●●●● patch | view | raw | blame | history
K2.项目管理/handoffs/2026-07-02-迁移交接.md 48 ●●●●● patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/engineering-plan.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/freeze/20260603-directory-consolidation.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/freeze/20260603-independent-repo-and-archive.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/freeze/20260604-mac-app-store-category-hotfix.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/freeze/20260604-mac-app-store-release-freeze.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/freeze/20260605-mac-app-store-application-identifier-hotfix.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/freeze/20260605-mac-app-store-clean-rebuild.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/freeze/20260605-mac-app-store-quarantine-hotfix.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/freeze/20260605-mac-app-store-upload-preflight.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/freeze/20260607-x-asr-experiment-branch.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/freeze/20260607-x-asr-experimental-build.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/plan.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/product-plan.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/README.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/dev-checklist-current.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/dev-checklist.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/engineering-tasks.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/go-test-all-20260602.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/go-test-internal-model-20260602.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/npm-run-build-20260602.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/app-log-tail-20260602.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/network-sample-20260602.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-clean-install-20260602.231851.launch.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-clean-install-20260602.231851.recognition.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-clean-install-20260602.231851.restore.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-clean-install-20260602.231851.setup.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-recognition-20260603.005122.launch.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-recognition-20260603.005122.recognition.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-recognition-20260603.005122.restore.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-recognition-20260603.005122.setup.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/offline-restart-app-log-20260602.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/offline-restart-postcheck-20260602.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/offline-restart-test-20260602.223742.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/offline-restart-test-20260602.225709.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/offline-restart-test-20260602.225709.pid patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/prd.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/qa-handoff-current.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/qa-precheck-current.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/qa-report-20260602-2.1.19.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/qa-report-template.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/qa-test-plan.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-1/todo-list.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/README.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/dev-checklist-current.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/engineering-tasks.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/go-test-all-20260603-2.1.20.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/go-test-all-20260603.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/npm-run-build-20260603-2.1.20.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/npm-run-build-20260603.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/build-local-round2-20260603.014931.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/current-run-id.txt patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/new-user-run-20260603.014931.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/new-user-run-20260603.014931.pid patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/offline-start-check-20260603.014931.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/offline-voice-final-20260603.014931.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/offline-voice-safety-20260603.014931.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/offline-voice-safety-20260603.014931.pid patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/offline-voice-start-20260603.014931.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-local-run-20260603.014931.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-local-run-20260603.014931.pid patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-activated.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-after-restart.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-chinese.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-cursor-1095-523.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-cursor-1260-565.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-cursor-693-310.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-english.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-english2.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-english3.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-english4.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-english5.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-english6.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-initial.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-new-user.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-zh-manual.png patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/prd.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/qa-handoff-current.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/qa-precheck-current.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/qa-report-current.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/qa-test-plan.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-2/todo-list.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/README.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/code-review-current.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/dev-checklist-current.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/engineering-tasks.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/logs/download-failure-protection-20260603.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/logs/git-diff-check-20260603-2.1.21.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/logs/git-diff-check-20260603.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/logs/go-test-all-20260603-2.1.21.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/logs/go-test-all-20260603.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/logs/npm-run-build-20260603-2.1.21.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/logs/npm-run-build-20260603.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/prd.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/qa-handoff-current.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/qa-precheck-current.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/qa-report-current.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/qa-test-plan.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-3/todo-list.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/README.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/code-review.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/dev-self-check.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/engineering-tasks.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/logs/git-diff-check-20260603-2.1.22.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/logs/git-diff-check-20260603-round4-after-qa-fix.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/logs/git-diff-check-20260603-round4.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/logs/go-test-all-20260603-2.1.22.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/logs/go-test-all-20260603-round4-after-qa-fix.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/logs/go-test-all-20260603-round4.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/logs/local-build-install-20260603-round4-after-qa-fix.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/logs/local-build-install-20260603-round4.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/logs/npm-ci-20260603-round4.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/logs/npm-run-build-20260603-2.1.22.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/logs/npm-run-build-20260603-round4-after-qa-fix.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/logs/npm-run-build-20260603-round4.log patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/prd.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/qa-handoff.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/qa-report.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/qa-test-plan.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-4/todo-list.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-5/README.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-5/dev-check-report.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-5/engineering-tasks.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-5/prd.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-5/qa-handoff.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-5/qa-report.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-5/qa-test-plan.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-5/todo-list.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-6/README.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-6/requirements-dev.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/round-6/todo-dev.md patch | view | raw | blame | history
K2.项目管理/legacy/02-P-NBL/x-asr-realtime-insertion-report.md patch | view | raw | blame | history
K2.项目管理/legacy/Docs/KM2.项目管理-非业务直接相关/AppStore审核拒绝-20260610.md patch | view | raw | blame | history
K2.项目管理/legacy/Docs/KM2.项目管理-非业务直接相关/AppStore打包连续返工经验教训-20260611.md patch | view | raw | blame | history
K2.项目管理/legacy/Docs/KM2.项目管理-非业务直接相关/QA工作-经验与教训.md patch | view | raw | blame | history
K2.项目管理/legacy/TODO-旧任务清单.md patch | view | raw | blame | history
K2.项目管理/moved-assets.jsonl 5 ●●●●● patch | view | raw | blame | history
K2.项目管理/restore-notes.md 22 ●●●●● patch | view | raw | blame | history
K2.项目管理/session-topics/00-Topics.md 15 ●●●●● patch | view | raw | blame | history
K2.项目管理/session-topics/AppStore与独立站.md 28 ●●●●● patch | view | raw | blame | history
K2.项目管理/session-topics/Windows预览.md 24 ●●●●● patch | view | raw | blame | history
K2.项目管理/session-topics/发布与打包.md 32 ●●●●● patch | view | raw | blame | history
K2.项目管理/session-topics/多语种与粤语.md 23 ●●●●● patch | view | raw | blame | history
K2.项目管理/session-topics/性能与稳定性.md 23 ●●●●● patch | view | raw | blame | history
K2.项目管理/发布地图.md 43 ●●●●● patch | view | raw | blame | history
K2.项目管理/接收验证记录.md 28 ●●●●● patch | view | raw | blame | history
K2.项目管理/新窗口接手指南.md 52 ●●●●● patch | view | raw | blame | history
K2.项目管理/迁移资产清单.md 45 ●●●●● patch | view | raw | blame | history
K3.运营与发布资料/README.md 12 ●●●●● patch | view | raw | blame | history
K3.运营与发布资料/legacy-运营资料/README.md 10 ●●●●● patch | view | raw | blame | history
K3.运营与发布资料/screenshot.png patch | view | raw | blame | history
README.md 187 ●●●● patch | view | raw | blame | history
privatevoice.src/internal/logger/logger.go 66 ●●●●● patch | view | raw | blame | history
.codebase-memory/.gitattributes
New file
@@ -0,0 +1,3 @@
# Auto-generated by codebase-memory-mcp
# Prevent merge conflicts on compressed artifact
graph.db.zst merge=ours binary
.codebase-memory/artifact.json
New file
@@ -0,0 +1,11 @@
{
    "schema_version": 1,
    "commit": "72b84399db9001a3b26ddad00c307438d940d8ee",
    "indexed_at": "2026-07-02T21:25:42Z",
    "project": "Users-ar-Projects-PrivateVoice-03-O",
    "nodes": 3601,
    "edges": 7844,
    "original_size": 10551296,
    "compressed_size": 1694833,
    "compression_level": 3
}
.codebase-memory/graph.db.zst
Binary files differ
.gitignore
@@ -1,5 +1,6 @@
# OS/editor noise
.DS_Store
._*
*.swp
*.swo
@@ -8,6 +9,8 @@
VoiceSnapGo/frontend/dist/
privatevoice.src/frontend/node_modules/
privatevoice.src/frontend/dist/
C1.source/privatevoice.src/frontend/node_modules/
C1.source/privatevoice.src/frontend/dist/
# Go/build artifacts
VoiceSnapGo/build/local/
@@ -21,6 +24,15 @@
privatevoice.src/build/windows-preview/
privatevoice.src/build/*.log
privatevoice.src/PrivateVoice
C1.source/privatevoice.src/build/local/
C1.source/privatevoice.src/build/appstore-local/
C1.source/privatevoice.src/build/deps/
C1.source/privatevoice.src/build/local-build-logs/
C1.source/privatevoice.src/build/macappstore*/
C1.source/privatevoice.src/build/qa/
C1.source/privatevoice.src/build/windows-preview/
C1.source/privatevoice.src/build/*.log
C1.source/privatevoice.src/PrivateVoice
*.app/
*.dmg
@@ -29,6 +41,8 @@
VoiceSnapGo/build/darwin/approved_app_icon.iconset/
privatevoice.src/build/darwin/icon.iconset/
privatevoice.src/build/darwin/approved_app_icon.iconset/
C1.source/privatevoice.src/build/darwin/icon.iconset/
C1.source/privatevoice.src/build/darwin/approved_app_icon.iconset/
# Runtime data should live in Application Support, not source control.
config.json
@@ -44,8 +58,22 @@
/.codex/
/.worktrees/
# Recovery input snapshots are not active source roots.
/C1.source/privatevoice-2.2.1-recovered.src/
/C1.source/privatevoice-2.2.1-recovered.src.RECOVERY.md
# Release packages, certificates, logs, and upload artifacts are local outputs.
/Release/
# Migrated local build packages are present in this project folder for handoff,
# but binary packages are not committed into the slim Git repo.
/C2.builds/**/*.dmg
/C2.builds/**/*.zip
# Local product/operation reference assets.
/X-运营资料/
# Migrated operation assets are local handoff materials; keep only the index.
/K3.运营与发布资料/legacy-运营资料/**
!/K3.运营与发布资料/legacy-运营资料/
!/K3.运营与发布资料/legacy-运营资料/README.md
AGENTS.md
@@ -1,56 +1,35 @@
## 项目属性
项目类型:研发项目
主要产物:Wails / Go 桌面语音输入 App
主要产物:Wails v3 / Go 桌面语音输入 App
## 1. 命令输出保护
## 读取顺序
保护上下文用量。
如果当前工作目录在 `03-O` 内,先回到项目根入口读取:
任何输出量未知或可能很大的命令,都必须限制输出字节数。
1. `/Users/ar/Projects/PrivateVoice/AGENTS.md`
2. `/Users/ar/Projects/PrivateVoice/PROJECT.md`
3. `/Users/ar/Projects/PrivateVoice/INDEX.md`
4. `/Users/ar/Projects/PrivateVoice/02-P/进度地图.md`
5. `/Users/ar/Projects/PrivateVoice/03-O/CODEGRAPH.md`
默认安全写法:
旧 `K2.项目管理/legacy/` 资料只作证据附件,不能作为活跃任务来源。
```bash
COMMAND 2>&1 | head -c 6000
```
## 路径边界
## 2. 发布版本规则
- 项目根:`/Users/ar/Projects/PrivateVoice`
- Git 根:`/Users/ar/Projects/PrivateVoice/03-O`
- 源码根:`/Users/ar/Projects/PrivateVoice/03-O/C1.source/privatevoice.src`
- 构建/发布候选包:`/Users/ar/Projects/PrivateVoice/03-O/C2.builds`
- 构建与 QA 脚本:源码内 `C1.source/privatevoice.src/scripts/`,`C3.tools/` 只放索引和迁移工具说明。
- 会话记忆目录:`/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/`
- 常规会话主题汇编目录:`/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/00-Topics.md` 和 `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/_topics/`。`K2.项目管理/session-topics/` 只保存旧项目迁移压缩摘要,不得作为新项目常规主题汇编落点。
本项目声明为 `研发项目`,发布或分发安装包前默认遵守 `~/.codex/rules/release-versioning.md`;如本项目 release 文档或构建脚本另有更具体要求,以本项目规则为准。
## 工作规则
## 3. 测试通过后的默认打包规则
当某一轮改动已经完成开发自测,并且 QA 验证也通过后,默认进入测试包交付流程,不需要再单独询问用户是否打包。
执行打包前仍必须遵守发布版本规则:递增 patch 版本号、生成新的 `YYYYMMDD.HHMM` build 编号、更新 App 元数据与 `CHANGELOG.md`,并生成可分发的 macOS `.dmg`。
如果只有开发自测通过,但 QA 尚未验证通过,则不得把该版本作为测试人员可用包交付;此时只说明当前处于待 QA 状态。
## 4. Mac App Store 出口合规规则
如果本 App 未实现、未打包、未接入非 Apple 操作系统提供的加密算法,Mac App Store 构建的 `Info.plist` 应声明:
```xml
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
```
提交 App Store Connect 前必须检查该键是否存在并为 `false`,以避免每次上传 build 后出现 `Missing Compliance` 并需要手动回答加密出口合规问题。
如果后续新增自实现加密、第三方加密库、非系统提供的标准加密算法或专有加密算法,必须重新评估出口合规,不得沿用上述声明。
## 5. 对话记忆保存规则
本项目需要保存重要会话记忆,但不得为了普通会话记录频繁打断用户审批。
- 首选保存目录:项目根目录下的 `.codex-sessionhistory/`,即 `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/`。
- 只在发生以下情况时保存会话记录:关键决策、文件修改、发布 / 构建 / QA 结论、重大问题排查、用户明确要求保存、阶段性收尾。
- 纯询问、路径确认、状态查询、简单说明、无实质决策或无文件变更的对话,不强制保存。
- 文件名格式:`YYYY-MM-DD-主题.md`;主题必须概括本轮 session 的核心内容,控制在 20 个字符以内,优先使用中文短语,避免空格、`/`、`:` 等不适合作为文件名的字符。
- 如果同一天同主题已有记录文件,必须创建新文件并追加序号,避免覆盖:`YYYY-MM-DD-主题-01.md`、`YYYY-MM-DD-主题-02.md`。
- 保存内容至少包括:用户目标、已完成事项、关键决策、文件变更、未决问题、后续建议。
- 如果首选目录在当前沙盒下不可写,不得发起额外权限审批;必须改存到当前 Codex 工作区的 `.codex-sessionhistory/PrivateVoice/`,并在最终答复中说明实际保存位置。
- 只有首选目录和当前工作区备用目录都不可写时,才允许跳过保存,并在最终答复中说明原因。
- 如果确需保存,应一次性保存本轮完整摘要,避免为了会话记录反复修正。
- 跨项目对话以主要项目保存;如果多个项目都有实质决策或改动,应按同样的首选目录 / 当前工作区备用目录规则分别保存摘要。
- 统一活跃台账是 `/Users/ar/Projects/PrivateVoice/02-P/进度地图.md`。
- `CODEGRAPH.md` 是人工维护工程地图;自动索引或缓存不能替代它。
- `codebase-memory` 状态为 `enabled`;标准刷新命令是 `codebase-memory-mcp cli index_repository '{"repo_path":"/Users/ar/Projects/PrivateVoice/03-O"}'`,共享产物为 `.codebase-memory/graph.db.zst` 和 `.codebase-memory/artifact.json`。
- 每次源码冻结、版本冻结、正式可回滚测试包或发布包前必须刷新 `.codebase-memory/`;日常 Coder 中间态变更不强制刷新。
- 发布、QA、签名、打包和版本冻结必须遵守项目根入口声明的全局规则路由。
- 标准验证命令必须保持原始可执行形式,不在命令本身追加输出截断管道。
C1.source/privatevoice.src/Taskfile.yml
C1.source/privatevoice.src/app.go
File was renamed from privatevoice.src/app.go
@@ -3,6 +3,7 @@
import (
    "context"
    "fmt"
    "runtime"
    "strings"
    "sync"
    "sync/atomic"
@@ -17,6 +18,8 @@
    "voicesnap/internal/input"
    "voicesnap/internal/language"
    "voicesnap/internal/logger"
    "voicesnap/internal/model"
    "voicesnap/internal/modelselection"
    "voicesnap/internal/overlay"
    "voicesnap/internal/sound"
    "voicesnap/internal/textproc"
@@ -28,8 +31,8 @@
)
const (
    appVersion        = "2.2.0"
    appBuild          = "20260702.0303"
    appVersion        = "2.2.2"
    appBuild          = "20260703.0335"
    appDisplayVersion = appVersion + " (build " + appBuild + ")"
    appName           = "PrivateVoice Dictation"
@@ -50,14 +53,18 @@
    cancel context.CancelFunc
    wg     sync.WaitGroup
    cfg      *config.Config
    recorder *audio.Recorder
    eng      engine.Engine
    engineMu sync.Mutex
    hk       hotkey.Listener
    paster   input.Paster
    history  *history.Store
    userdict *userdict.Store
    cfg           *config.Config
    recorder      *audio.Recorder
    eng           engine.Engine
    engineMu      sync.Mutex
    engineInitMu  sync.Mutex
    engineInitSeq atomic.Uint64
    engineMeta    engineMetadata
    engineFactory engineFactoryFunc
    hk            hotkey.Listener
    paster        input.Paster
    history       *history.Store
    userdict      *userdict.Store
    engineService  *services.EngineService
    wailsApp       *application.App
@@ -86,6 +93,24 @@
    liveCaptionSeq          uint64
    releaseTailCaptureNanos atomic.Int64
    holdPreCaptureEnabled   atomic.Bool
}
type engineFactoryFunc func(initID uint64) (engine.Engine, engineMetadata, bool, error)
type engineMetadata struct {
    InitID                uint64
    ResolvedModelID       string
    ConfigSelectedModelID string
    SelectionMode         string
    LanguageMode          string
    ConfigLanguageID      string
    EffectiveLanguageID   string
    BackendKind           string
    Provider              string
    LanguageParam         string
    FallbackReason        string
    NumThreads            int
    HardwareInfo          string
}
func RunApp() error {
@@ -132,6 +157,7 @@
    historyService := services.NewHistoryService(app.history)
    userDictService := services.NewUserDictService(app.userdict)
    correctionCSVService := services.NewCorrectionCSVService(app.history, app.userdict)
    diagnosticsService := services.NewDiagnosticsService(app.cfg, appDisplayVersion)
    // Create Wails application
    wailsApp := application.New(application.Options{
@@ -148,6 +174,7 @@
            application.NewService(historyService),
            application.NewService(userDictService),
            application.NewService(correctionCSVService),
            application.NewService(diagnosticsService),
        },
        Assets: application.AssetOptions{
            Handler: application.AssetFileServerFS(assets),
@@ -588,9 +615,20 @@
            case <-a.ctx.Done():
                return
            case <-ticker.C:
                d := time.Since(start)
                a.mu.Lock()
                recording := a.isRecording
                captionActive := a.liveCaptionCancel != nil
                if recording && d >= audio.MaxRecordingDuration {
                    logger.Error("Recording reached max duration; auto-stopping max_seconds=%d", int(audio.MaxRecordingDuration/time.Second))
                    if a.isFreetalking {
                        a.stopTapRecordingLocked()
                    } else {
                        a.stopRecordingLocked(false)
                    }
                    a.mu.Unlock()
                    return
                }
                a.mu.Unlock()
                if !recording {
                    return
@@ -598,7 +636,6 @@
                if captionActive {
                    continue
                }
                d := time.Since(start)
                a.indicator.SetStatus(status, fmt.Sprintf("%d:%02d", int(d.Minutes()), int(d.Seconds())%60))
            }
        }
@@ -860,8 +897,6 @@
    hotkeyVK := a.cfg.HotkeyVK
    autoHide := a.cfg.AutoHide
    copyToClipboard := a.cfg.CopyToClipboard
    selectedModelID := a.cfg.SelectedModelID
    languageID := a.cfg.LanguageID
    a.mu.Unlock()
    logger.Info(
@@ -893,6 +928,7 @@
    a.engineMu.Lock()
    engineLockElapsed := time.Since(engineLockStart)
    eng := a.eng
    engMeta := a.engineMeta
    if eng == nil {
        a.engineMu.Unlock()
        logger.Error("Recognition skipped: engine not ready")
@@ -982,52 +1018,84 @@
    }
    totalMS := perfSinceMS(pipelineStart)
    logger.Info("PERF pipeline_done id=%d result=success total_ms=%d", perfID, totalMS)
    logSlowRecognitionIfNeeded(perfID, eng, selectedModelID, languageID, samples, recognizeElapsedMS, totalMS)
    logSlowRecognitionIfNeeded(perfID, engMeta, samples, recognizeElapsedMS, totalMS)
    a.delayedHideIf(autoHide, doneIndicatorHideDelayMs)
}
func logSlowRecognitionIfNeeded(perfID int64, eng engine.Engine, modelID, languageID string, samples []float32, recognizeMS, totalMS int64) {
func logSlowRecognitionIfNeeded(perfID int64, meta engineMetadata, samples []float32, recognizeMS, totalMS int64) {
    reason := slowRecognitionReason(recognizeMS, totalMS)
    if reason == "" {
        return
    }
    engineInfo := ""
    if eng != nil {
        engineInfo = eng.HardwareInfo()
    audioMS := perfAudioDurationMS(samples)
    rtf := 0.0
    if audioMS > 0 {
        rtf = float64(recognizeMS) / float64(audioMS)
    }
    numCPU := runtime.NumCPU()
    load, hasLoad := currentSystemLoadAverage()
    if hasLoad {
        loadPerCore := 0.0
        if numCPU > 0 {
            loadPerCore = load[0] / float64(numCPU)
        }
        logger.Info(
            "PERF slow_recognition id=%d reason=%s model_id=%q language_id=%q engine=%q audio_ms=%d samples=%d recognize_ms=%d total_ms=%d load1=%.2f load5=%.2f load15=%.2f",
            "PERF slow_recognition id=%d reason=%s model_id=%q resolved_model_id=%q cfg_selected_model_id=%q selection_mode=%q language_mode=%q config_language_id=%q effective_language_id=%q backend=%q provider=%q language_param=%q fallback_reason=%q num_threads=%d engine=%q init_id=%d audio_ms=%d samples=%d recognize_ms=%d total_ms=%d rtf=%.3f load1=%.2f load5=%.2f load15=%.2f load1_per_core=%.2f cpu_count=%d",
            perfID,
            reason,
            modelID,
            languageID,
            engineInfo,
            perfAudioDurationMS(samples),
            meta.ResolvedModelID,
            meta.ResolvedModelID,
            meta.ConfigSelectedModelID,
            meta.SelectionMode,
            meta.LanguageMode,
            meta.ConfigLanguageID,
            meta.EffectiveLanguageID,
            meta.BackendKind,
            meta.Provider,
            meta.LanguageParam,
            meta.FallbackReason,
            meta.NumThreads,
            meta.HardwareInfo,
            meta.InitID,
            audioMS,
            len(samples),
            recognizeMS,
            totalMS,
            rtf,
            load[0],
            load[1],
            load[2],
            loadPerCore,
            numCPU,
        )
        return
    }
    logger.Info(
        "PERF slow_recognition id=%d reason=%s model_id=%q language_id=%q engine=%q audio_ms=%d samples=%d recognize_ms=%d total_ms=%d load_unavailable=true",
        "PERF slow_recognition id=%d reason=%s model_id=%q resolved_model_id=%q cfg_selected_model_id=%q selection_mode=%q language_mode=%q config_language_id=%q effective_language_id=%q backend=%q provider=%q language_param=%q fallback_reason=%q num_threads=%d engine=%q init_id=%d audio_ms=%d samples=%d recognize_ms=%d total_ms=%d rtf=%.3f load_unavailable=true cpu_count=%d",
        perfID,
        reason,
        modelID,
        languageID,
        engineInfo,
        perfAudioDurationMS(samples),
        meta.ResolvedModelID,
        meta.ResolvedModelID,
        meta.ConfigSelectedModelID,
        meta.SelectionMode,
        meta.LanguageMode,
        meta.ConfigLanguageID,
        meta.EffectiveLanguageID,
        meta.BackendKind,
        meta.Provider,
        meta.LanguageParam,
        meta.FallbackReason,
        meta.NumThreads,
        meta.HardwareInfo,
        meta.InitID,
        audioMS,
        len(samples),
        recognizeMS,
        totalMS,
        rtf,
        numCPU,
    )
}
@@ -1106,18 +1174,22 @@
}
func (a *App) initEngine() {
    logger.Info("Initializing ASR engine...")
    if a.engineService != nil {
        a.engineService.SetStatus("loading", "", "")
    }
    a.wailsApp.Event.Emit("engine:status", map[string]interface{}{
        "status": "loading",
    })
    initID := a.engineInitSeq.Add(1)
    modelExists := engine.ModelExists()
    eng, err := engine.New()
    a.engineInitMu.Lock()
    defer a.engineInitMu.Unlock()
    if initID != a.engineInitSeq.Load() {
        logger.Info("Skipping superseded ASR engine init: init_id=%d current_init_id=%d", initID, a.engineInitSeq.Load())
        return
    }
    logger.Info("Initializing ASR engine... init_id=%d", initID)
    a.setEngineStatus("loading", "", "")
    eng, meta, modelExists, err := a.createEngineForInit(initID)
    if err != nil {
        logger.Error("Engine initialization failed: %v", err)
        logger.Error("Engine initialization failed: init_id=%d model_id=%q error=%v", initID, meta.ResolvedModelID, err)
        if a.hasEngine() {
            logger.Info("Keeping existing ASR engine after reload failure")
        } else {
@@ -1127,26 +1199,24 @@
        if modelExists {
            status = "error"
        }
        if a.engineService != nil {
            a.engineService.SetStatus(status, "", err.Error())
        }
        a.wailsApp.Event.Emit("engine:status", map[string]interface{}{
            "status": status,
            "error":  err.Error(),
        })
        a.setEngineStatus(status, "", err.Error())
        return
    }
    a.replaceEngine(eng)
    logger.Info("ASR engine ready: %s", eng.HardwareInfo())
    if a.engineService != nil {
        a.engineService.SetStatus("ready", eng.HardwareInfo(), "")
    if initID != a.engineInitSeq.Load() {
        logger.Info("Discarding stale ASR engine init: init_id=%d current_init_id=%d model_id=%q", initID, a.engineInitSeq.Load(), meta.ResolvedModelID)
        eng.Close()
        return
    }
    a.wailsApp.Event.Emit("engine:status", map[string]interface{}{
        "status":       "ready",
        "hardwareInfo": eng.HardwareInfo(),
    })
    a.replaceEngineWithMetadata(eng, meta)
    logger.Info("ASR engine ready: init_id=%d model_id=%q backend=%s provider=%s hardware=%s", initID, meta.ResolvedModelID, meta.BackendKind, meta.Provider, eng.HardwareInfo())
    a.setEngineStatus("ready", eng.HardwareInfo(), "")
    if a.indicator == nil {
        return
    }
    // Show indicator briefly — snapshot config under lock
    a.mu.Lock()
@@ -1164,6 +1234,77 @@
    a.delayedHideIf(autoHide, 2000)
}
func (a *App) createEngineForInit(initID uint64) (engine.Engine, engineMetadata, bool, error) {
    if a.engineFactory != nil {
        return a.engineFactory(initID)
    }
    cfg, err := config.Load()
    if err != nil {
        logger.Error("Failed to load config for engine init: %v", err)
        cfg = a.cfg
        if cfg == nil {
            cfg = config.Default()
        }
    }
    current := modelselection.Resolve(cfg, language.NewSystemDetector())
    meta := engineMetadata{
        InitID:                initID,
        ResolvedModelID:       current.ModelID,
        ConfigSelectedModelID: cfg.SelectedModelID,
        SelectionMode:         current.SelectionMode,
        LanguageMode:          config.NormalizeLanguageMode(cfg.LanguageMode),
        ConfigLanguageID:      cfg.LanguageID,
        EffectiveLanguageID:   current.LanguageSettings.EffectiveLanguageID,
        FallbackReason:        current.FallbackReason,
    }
    resolved, err := model.ResolveModel(model.NormalizeModelID(current.ModelID))
    if err != nil {
        return nil, meta, false, err
    }
    meta.ResolvedModelID = resolved.ModelID
    meta.BackendKind = resolved.BackendKind
    meta.LanguageParam = resolved.Profile.LanguageParam
    meta.NumThreads = resolved.Profile.NumThreads
    modelExists := resolved.IsUsable()
    if !modelExists {
        return nil, meta, false, fmt.Errorf("model %s is not usable: %s missing=%v problems=%v", resolved.ModelID, resolved.Status, resolved.Missing, resolved.Problems)
    }
    eng, err := engine.NewWithResolvedModel(resolved)
    if err != nil {
        return nil, meta, modelExists, err
    }
    meta.HardwareInfo = eng.HardwareInfo()
    if providerEng, ok := eng.(engine.ProviderEngine); ok {
        meta.Provider = providerEng.Provider()
    }
    return eng, meta, modelExists, nil
}
func (a *App) setEngineStatus(status, hardwareInfo, errText string) {
    if a.engineService != nil {
        a.engineService.SetStatus(status, hardwareInfo, errText)
    }
    if a.wailsApp == nil {
        return
    }
    payload := map[string]interface{}{
        "status": status,
    }
    if hardwareInfo != "" {
        payload["hardwareInfo"] = hardwareInfo
    }
    if errText != "" {
        payload["error"] = errText
    }
    a.wailsApp.Event.Emit("engine:status", payload)
}
func (a *App) hasEngine() bool {
    a.engineMu.Lock()
    defer a.engineMu.Unlock()
@@ -1171,12 +1312,17 @@
}
func (a *App) replaceEngine(eng engine.Engine) {
    a.replaceEngineWithMetadata(eng, engineMetadata{})
}
func (a *App) replaceEngineWithMetadata(eng engine.Engine, meta engineMetadata) {
    a.releaseTailCaptureNanos.Store(int64(releaseTailCaptureDelayForEngine(eng)))
    a.holdPreCaptureEnabled.Store(holdPreCaptureEnabledForEngine(eng))
    a.engineMu.Lock()
    old := a.eng
    a.eng = eng
    a.engineMeta = meta
    a.engineMu.Unlock()
    if old != nil && old != eng {
@@ -1391,9 +1537,13 @@
    if a.recorder != nil {
        a.recorder.Close()
    }
    if a.hk != nil {
        a.hk.Close()
    }
    a.replaceEngine(nil)
    a.wg.Wait()
    logger.Info("Cleanup complete")
    logger.Close()
}
// SetRecordingHotkey is called by the hotkey service when entering hotkey recording mode.
C1.source/privatevoice.src/app_live_caption_test.go
File was renamed from privatevoice.src/app_live_caption_test.go
@@ -2,8 +2,11 @@
import (
    "strings"
    "sync/atomic"
    "testing"
    "time"
    "voicesnap/internal/engine"
)
func TestLiveCaptionDisplayTextFitsOverlayBuffer(t *testing.T) {
@@ -131,6 +134,95 @@
    }
}
func TestInitEngineSerializesAndDiscardsStaleEngine(t *testing.T) {
    first := &closeTrackingTestEngine{name: "first"}
    second := &closeTrackingTestEngine{name: "second"}
    firstStarted := make(chan struct{})
    allowFirst := make(chan struct{})
    calls := atomic.Int32{}
    a := &App{
        engineFactory: func(initID uint64) (engine.Engine, engineMetadata, bool, error) {
            call := calls.Add(1)
            switch call {
            case 1:
                close(firstStarted)
                <-allowFirst
                return first, engineMetadata{InitID: initID, ResolvedModelID: "first", HardwareInfo: "first"}, true, nil
            case 2:
                return second, engineMetadata{InitID: initID, ResolvedModelID: "second", HardwareInfo: "second"}, true, nil
            default:
                t.Fatalf("unexpected engine factory call %d", call)
                return nil, engineMetadata{}, false, nil
            }
        },
    }
    doneFirst := make(chan struct{})
    go func() {
        a.initEngine()
        close(doneFirst)
    }()
    select {
    case <-firstStarted:
    case <-time.After(time.Second):
        t.Fatal("first engine init did not start")
    }
    doneSecond := make(chan struct{})
    go func() {
        a.initEngine()
        close(doneSecond)
    }()
    waitForCondition(t, time.Second, func() bool {
        return a.engineInitSeq.Load() == 2
    })
    close(allowFirst)
    select {
    case <-doneFirst:
    case <-time.After(time.Second):
        t.Fatal("first engine init did not finish")
    }
    select {
    case <-doneSecond:
    case <-time.After(time.Second):
        t.Fatal("second engine init did not finish")
    }
    a.engineMu.Lock()
    gotEngine := a.eng
    gotMeta := a.engineMeta
    a.engineMu.Unlock()
    if gotEngine != second {
        t.Fatalf("final engine = %#v, want second", gotEngine)
    }
    if !first.closed.Load() {
        t.Fatal("stale first engine should be closed")
    }
    if second.closed.Load() {
        t.Fatal("current second engine should remain open")
    }
    if gotMeta.ResolvedModelID != "second" || gotMeta.InitID != 2 {
        t.Fatalf("engine metadata = %+v, want second init metadata", gotMeta)
    }
}
func waitForCondition(t *testing.T, timeout time.Duration, fn func() bool) {
    t.Helper()
    deadline := time.Now().Add(timeout)
    for time.Now().Before(deadline) {
        if fn() {
            return
        }
        time.Sleep(10 * time.Millisecond)
    }
    t.Fatal("condition was not met before timeout")
}
func TestShouldStartHoldPreCaptureLocked(t *testing.T) {
    a := &App{}
    a.holdPreCaptureEnabled.Store(true)
@@ -189,6 +281,15 @@
func (plainTestEngine) HardwareInfo() string                { return "test" }
func (plainTestEngine) Close()                              {}
type closeTrackingTestEngine struct {
    plainTestEngine
    name   string
    closed atomic.Bool
}
func (e *closeTrackingTestEngine) HardwareInfo() string { return e.name }
func (e *closeTrackingTestEngine) Close()               { e.closed.Store(true) }
type tailCaptureTestEngine struct {
    plainTestEngine
    delay time.Duration
C1.source/privatevoice.src/build/Taskfile.yml
C1.source/privatevoice.src/build/config.yml
File was renamed from privatevoice.src/build/config.yml
@@ -7,7 +7,7 @@
  description: "Private offline voice input tool"
  copyright: "(c) 2026, PrivateVoice"
  comments: "Private. Offline."
  version: "2.2.0"
  version: "2.2.2"
dev_mode:
  root_path: .
C1.source/privatevoice.src/build/darwin/Info.plist
File was renamed from privatevoice.src/build/darwin/Info.plist
@@ -17,9 +17,9 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>2.2.0</string>
    <string>2.2.2</string>
    <key>CFBundleVersion</key>
    <string>20260702.0303</string>
    <string>20260703.0335</string>
    <key>ITSAppUsesNonExemptEncryption</key>
    <false/>
    <key>LSApplicationCategoryType</key>
C1.source/privatevoice.src/build/darwin/Taskfile.yml
C1.source/privatevoice.src/build/darwin/VoiceSnap-approved.icns
Binary files differ
C1.source/privatevoice.src/build/darwin/VoiceSnap-macos26-mic.icns
Binary files differ
C1.source/privatevoice.src/build/darwin/VoiceSnap-refined-mic.icns
Binary files differ
C1.source/privatevoice.src/build/darwin/VoiceSnap-selected-B-shadow.icns
Binary files differ
C1.source/privatevoice.src/build/darwin/app_icon.png

C1.source/privatevoice.src/build/darwin/app_icon_source.png

C1.source/privatevoice.src/build/darwin/approved_app_icon.png

C1.source/privatevoice.src/build/darwin/archive/legacy-icons/VoiceSnap-original.icns
Binary files differ
C1.source/privatevoice.src/build/darwin/en.lproj/InfoPlist.strings
C1.source/privatevoice.src/build/darwin/entitlements-appstore.plist
C1.source/privatevoice.src/build/darwin/entitlements.plist
C1.source/privatevoice.src/build/darwin/icon.icns
Binary files differ
C1.source/privatevoice.src/build/darwin/macos26_mic_app_icon.png

C1.source/privatevoice.src/build/darwin/original_app_icon.png

C1.source/privatevoice.src/build/darwin/refined_mic_app_icon.png

C1.source/privatevoice.src/build/darwin/tray_icon.png

C1.source/privatevoice.src/build/darwin/zh-Hans.lproj/InfoPlist.strings
C1.source/privatevoice.src/build/linux/privatevoice-input.desktop
C1.source/privatevoice.src/build/windows/Taskfile.yml
C1.source/privatevoice.src/build/windows/icon.ico

C1.source/privatevoice.src/build/windows/wails.exe.manifest
C1.source/privatevoice.src/cmd/benchmark-recognition/main.go
New file
@@ -0,0 +1,115 @@
package main
import (
    "encoding/binary"
    "flag"
    "fmt"
    "os"
    "time"
    "voicesnap/internal/engine"
)
func main() {
    wavPath := flag.String("wav", "", "16kHz mono PCM16 WAV file")
    runs := flag.Int("runs", 3, "number of recognition runs")
    flag.Parse()
    if *wavPath == "" {
        fmt.Fprintln(os.Stderr, "missing -wav")
        os.Exit(2)
    }
    if *runs < 1 {
        *runs = 1
    }
    samples, sampleRate, err := readPCM16WAV(*wavPath)
    if err != nil {
        fmt.Fprintf(os.Stderr, "read wav: %v\n", err)
        os.Exit(1)
    }
    if sampleRate != 16000 {
        fmt.Fprintf(os.Stderr, "unsupported sample rate %d: provide 16kHz mono PCM16 WAV\n", sampleRate)
        os.Exit(1)
    }
    eng, err := engine.New()
    if err != nil {
        fmt.Fprintf(os.Stderr, "init engine: %v\n", err)
        os.Exit(1)
    }
    defer eng.Close()
    audioMS := int64(len(samples)) * 1000 / 16000
    fmt.Printf("engine=%q samples=%d audio_ms=%d runs=%d\n", eng.HardwareInfo(), len(samples), audioMS, *runs)
    var total int64
    for i := 1; i <= *runs; i++ {
        start := time.Now()
        text, err := eng.Recognize(samples)
        elapsedMS := time.Since(start).Milliseconds()
        if err != nil {
            fmt.Fprintf(os.Stderr, "run=%d error=%v\n", i, err)
            os.Exit(1)
        }
        total += elapsedMS
        fmt.Printf("run=%d recognize_ms=%d rtf=%.3f text_runes=%d\n", i, elapsedMS, float64(elapsedMS)/float64(audioMS), len([]rune(text)))
    }
    fmt.Printf("avg_recognize_ms=%d\n", total/int64(*runs))
}
func readPCM16WAV(path string) ([]float32, int, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, 0, err
    }
    if len(data) < 12 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WAVE" {
        return nil, 0, fmt.Errorf("not a RIFF/WAVE file")
    }
    var audioFormat, channels, bitsPerSample uint16
    var sampleRate uint32
    var pcm []byte
    for offset := 12; offset+8 <= len(data); {
        chunkID := string(data[offset : offset+4])
        chunkSize := int(binary.LittleEndian.Uint32(data[offset+4 : offset+8]))
        chunkStart := offset + 8
        chunkEnd := chunkStart + chunkSize
        if chunkEnd > len(data) {
            return nil, 0, fmt.Errorf("truncated %s chunk", chunkID)
        }
        switch chunkID {
        case "fmt ":
            if chunkSize < 16 {
                return nil, 0, fmt.Errorf("fmt chunk too small")
            }
            audioFormat = binary.LittleEndian.Uint16(data[chunkStart : chunkStart+2])
            channels = binary.LittleEndian.Uint16(data[chunkStart+2 : chunkStart+4])
            sampleRate = binary.LittleEndian.Uint32(data[chunkStart+4 : chunkStart+8])
            bitsPerSample = binary.LittleEndian.Uint16(data[chunkStart+14 : chunkStart+16])
        case "data":
            pcm = data[chunkStart:chunkEnd]
        }
        offset = chunkEnd
        if offset%2 == 1 {
            offset++
        }
    }
    if audioFormat != 1 || channels != 1 || bitsPerSample != 16 {
        return nil, 0, fmt.Errorf("unsupported WAV format: audio_format=%d channels=%d bits_per_sample=%d; want PCM16 mono", audioFormat, channels, bitsPerSample)
    }
    if len(pcm) == 0 {
        return nil, 0, fmt.Errorf("missing data chunk")
    }
    if len(pcm)%2 != 0 {
        return nil, 0, fmt.Errorf("odd PCM data length")
    }
    samples := make([]float32, len(pcm)/2)
    for i := range samples {
        v := int16(binary.LittleEndian.Uint16(pcm[i*2 : i*2+2]))
        samples[i] = float32(v) / 32768.0
    }
    return samples, int(sampleRate), nil
}
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_A_pure-blue-mic.png

C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_A_pure-blue-mic.svg
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_B_shadow_preview.png

C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_B_shadow_preview.svg
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_B_thin-blue-glyph.png

C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_B_thin-blue-glyph.svg
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_C_taglauncher-style.png

C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_C_taglauncher-style.svg
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_D_glass-circle.png

C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_D_glass-circle.svg
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_E_compact-liquid.png

C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_E_compact-liquid.svg
C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_candidates_preview.png

C1.source/privatevoice.src/design/icon-candidates/voicesnap_icon_candidates_preview.svg
C1.source/privatevoice.src/embed.go
C1.source/privatevoice.src/frontend/index.html
C1.source/privatevoice.src/frontend/instructions.md
C1.source/privatevoice.src/frontend/package-lock.json
File was renamed from privatevoice.src/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
  "name": "privatevoice-dictation-frontend",
  "version": "2.2.0",
  "version": "2.2.2",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "privatevoice-dictation-frontend",
      "version": "2.2.0",
      "version": "2.2.2",
      "dependencies": {
        "@wailsio/runtime": "latest"
      },
C1.source/privatevoice.src/frontend/package.json
File was renamed from privatevoice.src/frontend/package.json
@@ -1,7 +1,7 @@
{
  "name": "privatevoice-dictation-frontend",
  "private": true,
  "version": "2.2.0",
  "version": "2.2.2",
  "type": "module",
  "scripts": {
    "dev": "vite dev",
C1.source/privatevoice.src/frontend/src/App.svelte
File was renamed from privatevoice.src/frontend/src/App.svelte
@@ -19,24 +19,26 @@
  let showOnboarding = $state(false)
  onMount(() => {
    const offEvents: Array<() => void> = []
    Call.ByName('voicesnap/services.ConfigService.GetLanguageSettings').then((settings: any) => {
      applyLanguageSettings(settings)
    }).catch(() => {
      languageReady = true
    })
    Events.On('indicator:show', (ev: any) => {
    offEvents.push(Events.On('indicator:show', (ev: any) => {
      indicatorVisible.set(true)
      if (ev?.data?.status) {
        indicatorStatus.set(ev.data.status)
      }
    })
    }))
    Events.On('indicator:hide', () => {
    offEvents.push(Events.On('indicator:hide', () => {
      indicatorVisible.set(false)
    })
    }))
    Events.On('indicator:status', (ev: any) => {
    offEvents.push(Events.On('indicator:status', (ev: any) => {
      const data = ev?.data
      if (data?.status) {
        indicatorStatus.set(data.status)
@@ -44,13 +46,13 @@
      if (data?.hotkeyName) {
        hotkeyName.set(data.hotkeyName)
      }
    })
    }))
    Events.On('indicator:volume', (ev: any) => {
    offEvents.push(Events.On('indicator:volume', (ev: any) => {
      indicatorVolume.set(ev?.data ?? 0)
    })
    }))
    Events.On('engine:status', (ev: any) => {
    offEvents.push(Events.On('engine:status', (ev: any) => {
      const data = ev?.data
      if (data?.status) {
        engineStatus.set(data.status as any)
@@ -61,23 +63,24 @@
      if (data?.hardwareInfo) {
        engineHardwareInfo.set(data.hardwareInfo)
      }
    })
    }))
    Events.On('device:changed', (ev: any) => {
    offEvents.push(Events.On('device:changed', (ev: any) => {
      if (ev?.data?.name) {
        deviceName.set(ev.data.name)
      }
    })
    }))
    Events.On('update:available', (ev: any) => {
    offEvents.push(Events.On('update:available', (ev: any) => {
      const data = ev?.data as UpdateInfo
      updateAvailable.set(data)
      showUpdateDialog = true
    })
    }))
    window.addEventListener('hashchange', () => {
    const onHashChange = () => {
      isIndicatorRoute = window.location.hash === '#/indicator'
    })
    }
    window.addEventListener('hashchange', onHashChange)
    // Query engine status on mount (events emitted before frontend load are lost)
    Call.ByName('voicesnap/services.EngineService.GetStatus').then((data: any) => {
@@ -100,6 +103,11 @@
        hotkeyName.set(name)
      }
    })
    return () => {
      for (const off of offEvents) off()
      window.removeEventListener('hashchange', onHashChange)
    }
  })
  function applyLanguageSettings(settings: any) {
C1.source/privatevoice.src/frontend/src/assets/app-icon.png

C1.source/privatevoice.src/frontend/src/components/indicator/FloatingIndicator.svelte
C1.source/privatevoice.src/frontend/src/components/onboarding/OnboardingView.svelte
File was renamed from privatevoice.src/frontend/src/components/onboarding/OnboardingView.svelte
@@ -14,7 +14,7 @@
  let currentModelID = $state('')
  onMount(() => {
    Events.On('model:download-progress', (ev: any) => {
    const offDownloadProgress = Events.On('model:download-progress', (ev: any) => {
      const data = ev?.data
      if (data?.percent != null) {
        if (data?.modelID) {
@@ -31,7 +31,7 @@
      }
    })
    Events.On('engine:status', (ev: any) => {
    const offEngineStatus = Events.On('engine:status', (ev: any) => {
      const data = ev?.data
      if (data?.status === 'ready') {
        downloadActive = false
@@ -56,6 +56,11 @@
    })
    triggerDownload()
    return () => {
      offDownloadProgress()
      offEngineStatus()
    }
  })
  async function triggerDownload() {
C1.source/privatevoice.src/frontend/src/components/settings/AboutPage.svelte
C1.source/privatevoice.src/frontend/src/components/settings/CorrectionImportPreviewDialog.svelte
C1.source/privatevoice.src/frontend/src/components/settings/GeneralPage.svelte
File was renamed from privatevoice.src/frontend/src/components/settings/GeneralPage.svelte
@@ -1,4 +1,5 @@
<script lang="ts">
  import { onDestroy } from 'svelte'
  import { Call } from '@wailsio/runtime'
  import ToggleSwitch from '../shared/ToggleSwitch.svelte'
  import { t } from '../../lib/i18n'
@@ -26,6 +27,7 @@
  let hintText = $state('')
  let devices = $state<InputDevice[]>([])
  let showDeviceDropdown = $state(false)
  let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null
  const unsub1 = autoHide.subscribe(v => { autoHideVal = v })
  const unsub2 = startAtLogin.subscribe(v => { startAtLoginVal = v })
@@ -36,6 +38,11 @@
  const unsub7 = soundFeedback.subscribe(v => { soundFeedbackVal = v })
  const unsub8 = copyToClipboard.subscribe(v => { copyToClipboardVal = v })
  const unsub9 = hotkeyMode.subscribe(v => { hotkeyModeVal = v })
  onDestroy(() => {
    unsub1(); unsub2(); unsub3(); unsub4(); unsub5(); unsub6(); unsub7(); unsub8(); unsub9()
    clearHotkeyCapture()
  })
  // Load actual device name from backend on mount
  async function loadDeviceName() {
@@ -160,6 +167,7 @@
  }
  async function startRecordingHotkey() {
    clearHotkeyCapture()
    isRecording = true
    hintText = t('hotkeys.pressAnyKey')
@@ -191,13 +199,25 @@
          } catch {}
        })()
        document.removeEventListener('keydown', onKey)
        clearHotkeyCapture()
      }
    }
    activeKeyHandler = onKey
    document.addEventListener('keydown', onKey)
  }
  function clearHotkeyCapture() {
    if (activeKeyHandler) {
      document.removeEventListener('keydown', activeKeyHandler)
      activeKeyHandler = null
    }
    if (isRecording) {
      isRecording = false
      void Call.ByName('voicesnap/services.HotkeyService.StopRecordingHotkey').catch(() => {})
    }
  }
  function mapKeyToVK(e: KeyboardEvent): number {
    switch (e.key) {
      case 'Control': return e.location === 1 ? 0xA2 : e.location === 2 ? 0xA3 : 0x11
C1.source/privatevoice.src/frontend/src/components/settings/HistoryPage.svelte
C1.source/privatevoice.src/frontend/src/components/settings/HotkeysPage.svelte
File was renamed from privatevoice.src/frontend/src/components/settings/HotkeysPage.svelte
@@ -1,4 +1,5 @@
<script lang="ts">
  import { onDestroy } from 'svelte'
  import { Call } from '@wailsio/runtime'
  import { t } from '../../lib/i18n'
  import { hotkeyVK } from '../../lib/stores/config'
@@ -7,10 +8,17 @@
  let currentKeyName = $state('Ctrl')
  let isRecording = $state(false)
  let hintText = $state('')
  let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null
  const unsub = hotkeyName.subscribe(k => { currentKeyName = k })
  onDestroy(() => {
    unsub()
    clearHotkeyCapture()
  })
  async function startRecordingHotkey() {
    clearHotkeyCapture()
    isRecording = true
    hintText = t('hotkeys.pressAnyKey')
@@ -44,13 +52,25 @@
          } catch {}
        })()
        document.removeEventListener('keydown', onKey)
        clearHotkeyCapture()
      }
    }
    activeKeyHandler = onKey
    document.addEventListener('keydown', onKey)
  }
  function clearHotkeyCapture() {
    if (activeKeyHandler) {
      document.removeEventListener('keydown', activeKeyHandler)
      activeKeyHandler = null
    }
    if (isRecording) {
      isRecording = false
      void Call.ByName('voicesnap/services.HotkeyService.StopRecordingHotkey').catch(() => {})
    }
  }
  function mapKeyToVK(e: KeyboardEvent): number {
    // Map browser key codes to Windows VK codes
    switch (e.key) {
C1.source/privatevoice.src/frontend/src/components/settings/LanguagePage.svelte
File was renamed from privatevoice.src/frontend/src/components/settings/LanguagePage.svelte
@@ -1,4 +1,5 @@
<script lang="ts">
  import { onDestroy } from 'svelte'
  import { Call, Events } from '@wailsio/runtime'
  import { t, setLocale } from '../../lib/i18n'
  import { languageMode, languageID, effectiveLanguageID, uiLocale } from '../../lib/stores/config'
@@ -42,7 +43,7 @@
  const unsub2 = languageID.subscribe(v => { languageIDVal = v })
  const unsub3 = effectiveLanguageID.subscribe(v => { effectiveLanguageIDVal = v })
  Events.On('model:download-progress', (ev: any) => {
  const offDownloadProgress = Events.On('model:download-progress', (ev: any) => {
    const data = ev?.data
    if (data?.modelID && typeof data.percent === 'number') {
      modelBusyID = data.modelID
@@ -54,7 +55,7 @@
    }
  })
  Events.On('model:download-cancelled', (ev: any) => {
  const offDownloadCancelled = Events.On('model:download-cancelled', (ev: any) => {
    const data = ev?.data
    if (data?.modelID) {
      modelBusyID = data.modelID
@@ -63,7 +64,7 @@
    }
  })
  Events.On('model:download-finished', (ev: any) => {
  const offDownloadFinished = Events.On('model:download-finished', (ev: any) => {
    const data = ev?.data
    if (!data?.modelID || data.modelID === modelBusyID) {
      clearModelBusy()
@@ -71,6 +72,15 @@
    }
  })
  onDestroy(() => {
    unsub1()
    unsub2()
    unsub3()
    offDownloadProgress()
    offDownloadCancelled()
    offDownloadFinished()
  })
  async function loadSettings() {
    try {
      const lang: any = await Call.ByName('voicesnap/services.ConfigService.GetLanguageSettings')
C1.source/privatevoice.src/frontend/src/components/settings/PermissionsPage.svelte
File was renamed from privatevoice.src/frontend/src/components/settings/PermissionsPage.svelte
@@ -14,11 +14,18 @@
  let statuses = $state<PermissionStatus[]>([])
  let loading = $state(true)
  let refreshTimeout: number | null = null
  onMount(() => {
    refresh()
    const timer = window.setInterval(refresh, 2500)
    return () => window.clearInterval(timer)
    return () => {
      window.clearInterval(timer)
      if (refreshTimeout !== null) {
        window.clearTimeout(refreshTimeout)
        refreshTimeout = null
      }
    }
  })
  async function refresh() {
@@ -40,7 +47,13 @@
        await Call.ByName('voicesnap/services.PermissionService.OpenSettings', id)
      } catch {}
    }
    window.setTimeout(refresh, 800)
    if (refreshTimeout !== null) {
      window.clearTimeout(refreshTimeout)
    }
    refreshTimeout = window.setTimeout(() => {
      refreshTimeout = null
      refresh()
    }, 800)
  }
  function sortStatuses(items: PermissionStatus[]) {
C1.source/privatevoice.src/frontend/src/components/settings/SettingsWindow.svelte
File was renamed from privatevoice.src/frontend/src/components/settings/SettingsWindow.svelte
@@ -1,4 +1,5 @@
<script lang="ts">
  import { onDestroy } from 'svelte'
  import { currentPage } from '../../lib/stores/app'
  import { t } from '../../lib/i18n'
  import GeneralPage from './GeneralPage.svelte'
@@ -21,6 +22,10 @@
  let page = $state('general')
  const unsubPage = currentPage.subscribe(p => { page = p })
  onDestroy(() => {
    unsubPage()
  })
  function navigate(id: string) {
    currentPage.set(id)
  }
C1.source/privatevoice.src/frontend/src/components/settings/UserDictPage.svelte
C1.source/privatevoice.src/frontend/src/components/shared/AppIcon.svelte
C1.source/privatevoice.src/frontend/src/components/shared/ToggleSwitch.svelte
C1.source/privatevoice.src/frontend/src/components/update/UpdateDialog.svelte
C1.source/privatevoice.src/frontend/src/lib/i18n/de.json
C1.source/privatevoice.src/frontend/src/lib/i18n/en.json
C1.source/privatevoice.src/frontend/src/lib/i18n/es.json
C1.source/privatevoice.src/frontend/src/lib/i18n/fr.json
C1.source/privatevoice.src/frontend/src/lib/i18n/index.ts
C1.source/privatevoice.src/frontend/src/lib/i18n/it.json
C1.source/privatevoice.src/frontend/src/lib/i18n/ja.json
C1.source/privatevoice.src/frontend/src/lib/i18n/ko.json
C1.source/privatevoice.src/frontend/src/lib/i18n/pt.json
C1.source/privatevoice.src/frontend/src/lib/i18n/zh.json
C1.source/privatevoice.src/frontend/src/lib/stores/app.ts
C1.source/privatevoice.src/frontend/src/lib/stores/config.ts
C1.source/privatevoice.src/frontend/src/lib/stores/indicator.ts
C1.source/privatevoice.src/frontend/src/lib/stores/update.ts
C1.source/privatevoice.src/frontend/src/main.ts
C1.source/privatevoice.src/frontend/src/styles/global.css
C1.source/privatevoice.src/frontend/src/vite-env.d.ts
C1.source/privatevoice.src/frontend/svelte.config.js
C1.source/privatevoice.src/frontend/tsconfig.json
C1.source/privatevoice.src/frontend/vite.config.ts
C1.source/privatevoice.src/go.mod
C1.source/privatevoice.src/go.monterey.mod
C1.source/privatevoice.src/go.monterey.sum
C1.source/privatevoice.src/go.sum
C1.source/privatevoice.src/instructions.md
C1.source/privatevoice.src/internal/audio/recorder.go
File was renamed from privatevoice.src/internal/audio/recorder.go
@@ -10,11 +10,17 @@
)
const (
    sampleRate       = 16000
    channels         = 1
    bitsPerSample    = 16
    silenceThreshold = 0.05
    sampleRate            = 16000
    channels              = 1
    bitsPerSample         = 16
    silenceThreshold      = 0.05
    maxRecordingSeconds   = 15 * 60
    maxPCMBufferBytes     = sampleRate * channels * (bitsPerSample / 8) * maxRecordingSeconds
    MaxRecordingDuration  = time.Duration(maxRecordingSeconds) * time.Second
    maxPendingDeviceStops = 2
)
var pendingDeviceStops = make(chan struct{}, maxPendingDeviceStops)
// InputDevice represents an audio input device.
type InputDevice struct {
@@ -37,6 +43,7 @@
    // State
    isRecording    bool
    bufferLimitHit bool
    maxVolume      float64
    currentVolume  float64
    volumeCallback func(float64)
@@ -106,6 +113,7 @@
    }
    r.pcmBuf = nil
    r.bufferLimitHit = false
    r.maxVolume = 0
    r.currentVolume = 0
@@ -249,8 +257,18 @@
    if device == nil {
        return
    }
    select {
    case pendingDeviceStops <- struct{}{}:
    case <-time.After(100 * time.Millisecond):
        logger.Error("Audio device stop queue saturated; stopping synchronously")
        stopAndUninitDevice(device)
        return
    }
    done := make(chan struct{})
    go func() {
        defer func() {
            <-pendingDeviceStops
        }()
        stopAndUninitDevice(device)
        close(done)
    }()
@@ -296,6 +314,21 @@
    }
    // Append raw PCM data
    if len(r.pcmBuf) >= maxPCMBufferBytes {
        if !r.bufferLimitHit {
            logger.Error("Recording buffer limit reached; dropping additional audio max_seconds=%d", maxRecordingSeconds)
            r.bufferLimitHit = true
        }
        r.mu.Unlock()
        return
    }
    if len(r.pcmBuf)+len(input) > maxPCMBufferBytes {
        input = input[:maxPCMBufferBytes-len(r.pcmBuf)]
        if !r.bufferLimitHit {
            logger.Error("Recording buffer limit reached; truncating audio max_seconds=%d", maxRecordingSeconds)
            r.bufferLimitHit = true
        }
    }
    r.pcmBuf = append(r.pcmBuf, input...)
    // Calculate RMS volume
C1.source/privatevoice.src/internal/audio/recorder_test.go
File was renamed from privatevoice.src/internal/audio/recorder_test.go
@@ -36,6 +36,27 @@
    }
}
func TestOnDataCapsRecordingBuffer(t *testing.T) {
    r := &Recorder{
        isRecording: true,
        pcmBuf:      make([]byte, maxPCMBufferBytes-2),
    }
    r.onData(pcm16LE(1, 2))
    if got := len(r.pcmBuf); got != maxPCMBufferBytes {
        t.Fatalf("pcm buffer length = %d, want capped %d", got, maxPCMBufferBytes)
    }
    if !r.bufferLimitHit {
        t.Fatal("bufferLimitHit should be set after truncating at cap")
    }
    r.onData(pcm16LE(3))
    if got := len(r.pcmBuf); got != maxPCMBufferBytes {
        t.Fatalf("pcm buffer length after cap = %d, want %d", got, maxPCMBufferBytes)
    }
}
func pcm16LE(values ...int16) []byte {
    buf := make([]byte, len(values)*2)
    for i, value := range values {
C1.source/privatevoice.src/internal/config/config.go
C1.source/privatevoice.src/internal/config/config_test.go
C1.source/privatevoice.src/internal/correctioncsv/correctioncsv.go
C1.source/privatevoice.src/internal/correctioncsv/correctioncsv_e2e_test.go
C1.source/privatevoice.src/internal/correctioncsv/correctioncsv_test.go
C1.source/privatevoice.src/internal/dock/dock.go
C1.source/privatevoice.src/internal/dock/dock_darwin.go
C1.source/privatevoice.src/internal/dock/dock_other.go
C1.source/privatevoice.src/internal/engine/engine.go
File was renamed from privatevoice.src/internal/engine/engine.go
@@ -21,6 +21,12 @@
    Close()
}
// ProviderEngine is implemented by engines that can report the concrete
// sherpa/onnxruntime provider selected at initialization time.
type ProviderEngine interface {
    Provider() string
}
// StreamingEngine is implemented by engines that can expose partial results
// while audio is still being captured.
type StreamingEngine interface {
@@ -116,6 +122,14 @@
    return newPlatformEngine(resolved)
}
func audioDurationMS(samples []float32) int64 {
    return int64(len(samples)) * 1000 / 16000
}
func durationMS(duration time.Duration) int64 {
    return duration.Milliseconds()
}
func isSupportedBackend(backend string) bool {
    switch backend {
    case model.BackendSenseVoice, model.BackendMoonshine, model.BackendTransducer, model.BackendNemoTransducer, model.BackendQwen3ASR, model.BackendXASRStreaming:
C1.source/privatevoice.src/internal/engine/engine_darwin.go
File was renamed from privatevoice.src/internal/engine/engine_darwin.go
@@ -29,11 +29,13 @@
type sherpaEngine struct {
    recognizer  *sherpa.OfflineRecognizer
    backendKind string
    provider    string
    hwInfo      string
}
type xasrStreamingEngine struct {
    recognizer *sherpa.OnlineRecognizer
    provider   string
    hwInfo     string
    mu         sync.Mutex
}
@@ -69,6 +71,7 @@
            return &sherpaEngine{
                recognizer:  recognizer,
                backendKind: resolved.BackendKind,
                provider:    p.provider,
                hwInfo:      info,
            }, nil
        }
@@ -93,6 +96,7 @@
            logger.Info("Engine initialized: %s", info)
            return &xasrStreamingEngine{
                recognizer: recognizer,
                provider:   p.provider,
                hwInfo:     info,
            }, nil
        }
@@ -196,28 +200,72 @@
}
func (e *sherpaEngine) Recognize(samples []float32) (string, error) {
    totalStart := time.Now()
    streamStart := time.Now()
    stream := sherpa.NewOfflineStream(e.recognizer)
    streamMS := durationMS(time.Since(streamStart))
    defer sherpa.DeleteOfflineStream(stream)
    acceptStart := time.Now()
    stream.AcceptWaveform(asrSampleRate, samples)
    acceptMS := durationMS(time.Since(acceptStart))
    decodeStart := time.Now()
    e.recognizer.Decode(stream)
    decodeMS := durationMS(time.Since(decodeStart))
    resultStart := time.Now()
    result := stream.GetResult()
    resultMS := durationMS(time.Since(resultStart))
    logger.Info(
        "PERF engine_recognize_detail backend=%q provider=%q engine=%q samples=%d audio_ms=%d create_stream_ms=%d accept_ms=%d decode_ms=%d result_ms=%d total_ms=%d",
        e.backendKind,
        e.provider,
        e.hwInfo,
        len(samples),
        audioDurationMS(samples),
        streamMS,
        acceptMS,
        decodeMS,
        resultMS,
        durationMS(time.Since(totalStart)),
    )
    return result.Text, nil
}
func (e *xasrStreamingEngine) Recognize(samples []float32) (string, error) {
    totalStart := time.Now()
    sessionStart := time.Now()
    session, err := e.NewStreamingSession()
    if err != nil {
        return "", err
    }
    sessionMS := durationMS(time.Since(sessionStart))
    defer session.Close()
    acceptStart := time.Now()
    if _, err := session.Accept(samples); err != nil {
        return "", err
    }
    return session.Finish()
    acceptMS := durationMS(time.Since(acceptStart))
    finishStart := time.Now()
    text, err := session.Finish()
    finishMS := durationMS(time.Since(finishStart))
    if err == nil {
        logger.Info(
            "PERF engine_recognize_detail backend=%q provider=%q engine=%q samples=%d audio_ms=%d create_session_ms=%d accept_ms=%d finish_ms=%d total_ms=%d",
            model.BackendXASRStreaming,
            e.provider,
            e.hwInfo,
            len(samples),
            audioDurationMS(samples),
            sessionMS,
            acceptMS,
            finishMS,
            durationMS(time.Since(totalStart)),
        )
    }
    return text, err
}
func (e *xasrStreamingEngine) NewStreamingSession() (StreamingSession, error) {
@@ -316,6 +364,10 @@
    return e.hwInfo
}
func (e *sherpaEngine) Provider() string {
    return e.provider
}
func (e *sherpaEngine) ReleaseTailCaptureDelay() time.Duration {
    if e.backendKind == model.BackendSenseVoice {
        return senseVoiceReleaseTailDelay
@@ -327,6 +379,10 @@
    return e.hwInfo
}
func (e *xasrStreamingEngine) Provider() string {
    return e.provider
}
func (e *xasrStreamingEngine) ReleaseTailCaptureDelay() time.Duration {
    return xasrReleaseTailDelay
}
C1.source/privatevoice.src/internal/engine/engine_darwin_qwen3.go
C1.source/privatevoice.src/internal/engine/engine_darwin_qwen3_monterey.go
C1.source/privatevoice.src/internal/engine/engine_darwin_qwen3_monterey_test.go
C1.source/privatevoice.src/internal/engine/engine_darwin_qwen3_test.go
C1.source/privatevoice.src/internal/engine/engine_darwin_test.go
C1.source/privatevoice.src/internal/engine/engine_linux.go
File was renamed from privatevoice.src/internal/engine/engine_linux.go
@@ -14,6 +14,7 @@
type sherpaEngine struct {
    recognizer *sherpa.OfflineRecognizer
    provider   string
    hwInfo     string
}
@@ -36,6 +37,7 @@
        logger.Info("Engine initialized: %s", info)
        return &sherpaEngine{
            recognizer: recognizer,
            provider:   "cpu",
            hwInfo:     info,
        }, nil
    }
@@ -44,13 +46,35 @@
}
func (e *sherpaEngine) Recognize(samples []float32) (string, error) {
    totalStart := time.Now()
    streamStart := time.Now()
    stream := sherpa.NewOfflineStream(e.recognizer)
    streamMS := durationMS(time.Since(streamStart))
    defer sherpa.DeleteOfflineStream(stream)
    acceptStart := time.Now()
    stream.AcceptWaveform(16000, samples)
    acceptMS := durationMS(time.Since(acceptStart))
    decodeStart := time.Now()
    e.recognizer.Decode(stream)
    decodeMS := durationMS(time.Since(decodeStart))
    resultStart := time.Now()
    result := stream.GetResult()
    resultMS := durationMS(time.Since(resultStart))
    logger.Info(
        "PERF engine_recognize_detail backend=%q provider=%q engine=%q samples=%d audio_ms=%d create_stream_ms=%d accept_ms=%d decode_ms=%d result_ms=%d total_ms=%d",
        model.BackendSenseVoice,
        e.provider,
        e.hwInfo,
        len(samples),
        audioDurationMS(samples),
        streamMS,
        acceptMS,
        decodeMS,
        resultMS,
        durationMS(time.Since(totalStart)),
    )
    return result.Text, nil
}
@@ -59,6 +83,10 @@
    return e.hwInfo
}
func (e *sherpaEngine) Provider() string {
    return e.provider
}
func (e *sherpaEngine) ReleaseTailCaptureDelay() time.Duration {
    return senseVoiceReleaseTailDelay
}
C1.source/privatevoice.src/internal/engine/engine_windows.go
File was renamed from privatevoice.src/internal/engine/engine_windows.go
@@ -14,6 +14,7 @@
type sherpaEngine struct {
    recognizer *sherpa.OfflineRecognizer
    provider   string
    hwInfo     string
}
@@ -45,6 +46,7 @@
            logger.Info("Engine initialized: %s", info)
            return &sherpaEngine{
                recognizer: recognizer,
                provider:   p.provider,
                hwInfo:     info,
            }, nil
        }
@@ -55,13 +57,35 @@
}
func (e *sherpaEngine) Recognize(samples []float32) (string, error) {
    totalStart := time.Now()
    streamStart := time.Now()
    stream := sherpa.NewOfflineStream(e.recognizer)
    streamMS := durationMS(time.Since(streamStart))
    defer sherpa.DeleteOfflineStream(stream)
    acceptStart := time.Now()
    stream.AcceptWaveform(16000, samples)
    acceptMS := durationMS(time.Since(acceptStart))
    decodeStart := time.Now()
    e.recognizer.Decode(stream)
    decodeMS := durationMS(time.Since(decodeStart))
    resultStart := time.Now()
    result := stream.GetResult()
    resultMS := durationMS(time.Since(resultStart))
    logger.Info(
        "PERF engine_recognize_detail backend=%q provider=%q engine=%q samples=%d audio_ms=%d create_stream_ms=%d accept_ms=%d decode_ms=%d result_ms=%d total_ms=%d",
        model.BackendSenseVoice,
        e.provider,
        e.hwInfo,
        len(samples),
        audioDurationMS(samples),
        streamMS,
        acceptMS,
        decodeMS,
        resultMS,
        durationMS(time.Since(totalStart)),
    )
    return result.Text, nil
}
@@ -70,6 +94,10 @@
    return e.hwInfo
}
func (e *sherpaEngine) Provider() string {
    return e.provider
}
func (e *sherpaEngine) ReleaseTailCaptureDelay() time.Duration {
    return senseVoiceReleaseTailDelay
}
C1.source/privatevoice.src/internal/engine/release_tail.go
C1.source/privatevoice.src/internal/history/history.go
C1.source/privatevoice.src/internal/history/history_test.go
C1.source/privatevoice.src/internal/hotkey/hotkey.go
File was renamed from privatevoice.src/internal/hotkey/hotkey.go
@@ -13,6 +13,8 @@
    IsAnyOtherKeyPressed(excludeVK int) bool
    // IsAnyOtherKeyPressedSince returns true if another key was pressed after since.
    IsAnyOtherKeyPressedSince(excludeVK int, since time.Time) bool
    // Close releases listener resources.
    Close()
}
// New creates a new platform-specific hotkey listener.
C1.source/privatevoice.src/internal/hotkey/hotkey_darwin.go
File was renamed from privatevoice.src/internal/hotkey/hotkey_darwin.go
@@ -12,8 +12,10 @@
static volatile double g_lastDownAt[128];
static volatile int g_monitorStarted = 0;
static volatile int g_monitorLaunching = 0;
static volatile int g_monitorStopping = 0;
static CFMachPortRef g_eventTap = NULL;
static CFRunLoopSourceRef g_eventTapSource = NULL;
static CFRunLoopRef g_eventTapRunLoop = NULL;
static volatile unsigned long long g_monitorStartAttempts = 0;
static volatile unsigned long long g_eventTapCreateFailures = 0;
static volatile unsigned long long g_eventTapDisabledCount = 0;
@@ -138,8 +140,26 @@
    CGEventTapEnable(g_eventTap, true);
    g_monitorStarted = 1;
    g_monitorLaunching = 0;
    g_eventTapRunLoop = CFRunLoopGetCurrent();
    if (g_eventTapRunLoop != NULL) CFRetain(g_eventTapRunLoop);
    CFRunLoopRun();
    if (g_eventTap != NULL) CGEventTapEnable(g_eventTap, false);
    if (g_eventTapSource != NULL) {
        CFRunLoopRemoveSource(CFRunLoopGetCurrent(), g_eventTapSource, kCFRunLoopCommonModes);
        CFRelease(g_eventTapSource);
        g_eventTapSource = NULL;
    }
    if (g_eventTap != NULL) {
        CFRelease(g_eventTap);
        g_eventTap = NULL;
    }
    if (g_eventTapRunLoop != NULL) {
        CFRelease(g_eventTapRunLoop);
        g_eventTapRunLoop = NULL;
    }
    g_monitorStarted = 0;
    g_monitorStopping = 0;
}
static int ensureAccessibility(void) {
@@ -154,6 +174,12 @@
        reason:@"PrivateVoice Dictation global hotkey listener"];
}
static void endHotkeyActivity(void) {
    if (g_activityToken == nil) return;
    [[NSProcessInfo processInfo] endActivity:g_activityToken];
    g_activityToken = nil;
}
static void* eventTapThreadMain(void* arg) {
    (void)arg;
    @autoreleasepool {
@@ -164,7 +190,7 @@
}
static void startKeyMonitor(void) {
    if (g_monitorStarted || g_monitorLaunching) return;
    if (g_monitorStarted || g_monitorLaunching || g_monitorStopping) return;
    g_monitorLaunching = 1;
    pthread_t thread;
    int err = pthread_create(&thread, NULL, eventTapThreadMain, NULL);
@@ -174,6 +200,15 @@
        return;
    }
    pthread_detach(thread);
}
static void stopKeyMonitor(void) {
    g_monitorStopping = 1;
    if (g_eventTap != NULL) CGEventTapEnable(g_eventTap, false);
    if (g_eventTapRunLoop != NULL) {
        CFRunLoopStop(g_eventTapRunLoop);
    }
    endHotkeyActivity();
}
// isModifierDown polls the current system modifier flags via CGEventSource.
@@ -522,6 +557,10 @@
    return false
}
func (l *darwinListener) Close() {
    C.stopKeyMonitor()
}
func (l *darwinListener) wasPressedSince(vk int, since time.Time) bool {
    if since.IsZero() {
        return l.IsKeyDown(vk)
C1.source/privatevoice.src/internal/hotkey/hotkey_linux.go
File was renamed from privatevoice.src/internal/hotkey/hotkey_linux.go
@@ -96,3 +96,5 @@
func (l *linuxListener) IsAnyOtherKeyPressedSince(excludeVK int, since time.Time) bool {
    return l.IsAnyOtherKeyPressed(excludeVK)
}
func (l *linuxListener) Close() {}
C1.source/privatevoice.src/internal/hotkey/hotkey_windows.go
File was renamed from privatevoice.src/internal/hotkey/hotkey_windows.go
@@ -91,6 +91,8 @@
    return l.IsAnyOtherKeyPressed(excludeVK)
}
func (l *windowsListener) Close() {}
func contains(slice []int, val int) bool {
    for _, v := range slice {
        if v == val {
C1.source/privatevoice.src/internal/input/paste.go
C1.source/privatevoice.src/internal/input/paste_darwin.go
C1.source/privatevoice.src/internal/input/paste_darwin_test.go
C1.source/privatevoice.src/internal/input/paste_linux.go
C1.source/privatevoice.src/internal/input/paste_windows.go
C1.source/privatevoice.src/internal/language/detect.go
C1.source/privatevoice.src/internal/language/detect_darwin.go
C1.source/privatevoice.src/internal/language/detect_other.go
C1.source/privatevoice.src/internal/language/language_test.go
C1.source/privatevoice.src/internal/language/resolver.go
C1.source/privatevoice.src/internal/logger/logger.go
New file
@@ -0,0 +1,109 @@
package logger
import (
    "fmt"
    "os"
    "path/filepath"
    "sync"
    "time"
    "voicesnap/internal/paths"
)
var (
    mu      sync.Mutex
    logFile *os.File
)
const (
    maxLogBytes   = 10 * 1024 * 1024
    maxLogBackups = 3
)
// Init initializes the file logger in the persistent app data directory.
func Init() {
    mu.Lock()
    if logFile != nil {
        logFile.Close()
        logFile = nil
    }
    mu.Unlock()
    dir := paths.AppSupportDir()
    if err := os.MkdirAll(dir, 0755); err != nil {
        fmt.Fprintf(os.Stderr, "failed to create log dir: %v\n", err)
        return
    }
    path := filepath.Join(dir, "app.log")
    if err := rotateIfNeeded(path); err != nil {
        fmt.Fprintf(os.Stderr, "failed to rotate log file: %v\n", err)
    }
    f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
        fmt.Fprintf(os.Stderr, "failed to open log file: %v\n", err)
        return
    }
    mu.Lock()
    defer mu.Unlock()
    logFile = f
}
func rotateIfNeeded(path string) error {
    info, err := os.Stat(path)
    if err != nil {
        if os.IsNotExist(err) {
            return nil
        }
        return err
    }
    if info.Size() < maxLogBytes {
        return nil
    }
    oldest := fmt.Sprintf("%s.%d", path, maxLogBackups)
    if err := os.Remove(oldest); err != nil && !os.IsNotExist(err) {
        return err
    }
    for i := maxLogBackups - 1; i >= 1; i-- {
        from := fmt.Sprintf("%s.%d", path, i)
        to := fmt.Sprintf("%s.%d", path, i+1)
        if err := os.Rename(from, to); err != nil && !os.IsNotExist(err) {
            return err
        }
    }
    return os.Rename(path, path+".1")
}
func write(level, format string, args ...interface{}) {
    msg := fmt.Sprintf(format, args...)
    ts := time.Now().Format("2006-01-02 15:04:05")
    line := fmt.Sprintf("[%s] %s %s\n", ts, level, msg)
    mu.Lock()
    defer mu.Unlock()
    if logFile != nil {
        logFile.WriteString(line)
    }
    fmt.Fprint(os.Stderr, line)
}
// Info logs an informational message.
func Info(format string, args ...interface{}) {
    write("INFO", format, args...)
}
// Error logs an error message.
func Error(format string, args ...interface{}) {
    write("ERROR", format, args...)
}
// Close closes the log file.
func Close() {
    mu.Lock()
    defer mu.Unlock()
    if logFile != nil {
        logFile.Close()
        logFile = nil
    }
}
C1.source/privatevoice.src/internal/model/build_capabilities.go
C1.source/privatevoice.src/internal/model/build_capabilities_default.go
C1.source/privatevoice.src/internal/model/build_capabilities_monterey.go
C1.source/privatevoice.src/internal/model/build_capabilities_windows.go
C1.source/privatevoice.src/internal/model/downloader.go
C1.source/privatevoice.src/internal/model/install_state.go
C1.source/privatevoice.src/internal/model/model_test.go
C1.source/privatevoice.src/internal/model/profile.go
C1.source/privatevoice.src/internal/model/registry.go
C1.source/privatevoice.src/internal/model/resolver.go
C1.source/privatevoice.src/internal/model/validator.go
C1.source/privatevoice.src/internal/modelselection/selection.go
C1.source/privatevoice.src/internal/modelselection/selection_test.go
C1.source/privatevoice.src/internal/overlay/overlay.go
C1.source/privatevoice.src/internal/overlay/overlay_darwin.go
File was renamed from privatevoice.src/internal/overlay/overlay_darwin.go
@@ -151,6 +151,7 @@
static NSWindow* g_window = nil;
static NSWindow* g_dragWindow = nil;
static int g_windowsOrderedOut = 1;
static float desktopTopY(void) {
    NSScreen* mainScreen = [NSScreen mainScreen];
@@ -180,6 +181,17 @@
    if (g_dragWindow) {
        [g_dragWindow setFrameOrigin:dragOriginForVisualOrigin(visualOrigin)];
    }
}
static void orderOverlayWindowsOutOnce(void) {
    if (g_windowsOrderedOut) return;
    if (g_window) [g_window orderOut:nil];
    if (g_dragWindow) [g_dragWindow orderOut:nil];
    g_windowsOrderedOut = 1;
}
static void markOverlayWindowsVisible(void) {
    g_windowsOrderedOut = 0;
}
static void storeDragPositionFromVisualOrigin(NSPoint visualOrigin) {
@@ -669,8 +681,7 @@
    [g_window setAlphaValue:eased];
    if (g_state.fadeProgress <= 0 && g_state.fadeTarget == 0) {
        [g_window orderOut:nil];
        if (g_dragWindow) [g_dragWindow orderOut:nil];
        orderOverlayWindowsOutOnce();
        return;
    }
@@ -683,6 +694,7 @@
            applyOverlayWindowPolicy(g_dragWindow);
            [g_dragWindow orderFrontRegardless];
        }
        markOverlayWindowsVisible();
    }
    // Reposition
@@ -724,6 +736,7 @@
    g_window = window;
    g_view = view;
    g_windowsOrderedOut = 1;
    PrivateVoiceOverlayWindow* dragWindow = [[PrivateVoiceOverlayWindow alloc]
        initWithContentRect:NSMakeRect(0, 0, DRAG_W, DRAG_H)
@@ -789,6 +802,7 @@
        applyOverlayWindowPolicy(g_dragWindow);
        [g_dragWindow orderFrontRegardless];
    }
    markOverlayWindowsVisible();
}
static void overlayShow(void) {
@@ -882,6 +896,7 @@
import "C"
import (
    "sync"
    "time"
    "unsafe"
)
@@ -894,6 +909,7 @@
// darwinOverlay uses a native NSWindow + Core Graphics to render
// the floating indicator, matching the Windows GDI+ implementation.
type darwinOverlay struct {
    mu      sync.Mutex
    dragCb  func(int, int)
    created bool
    done    chan struct{}
@@ -906,12 +922,17 @@
}
func (o *darwinOverlay) ensureCreated() {
    if !o.created {
        C.overlayCreate()
        o.created = true
        time.Sleep(50 * time.Millisecond)
        go o.pollDrag()
    o.mu.Lock()
    if o.created {
        o.mu.Unlock()
        return
    }
    o.created = true
    o.mu.Unlock()
    C.overlayCreate()
    time.Sleep(50 * time.Millisecond)
    go o.pollDrag()
}
func (o *darwinOverlay) Show() {
C1.source/privatevoice.src/internal/overlay/overlay_other.go
C1.source/privatevoice.src/internal/overlay/overlay_windows.go
File was renamed from privatevoice.src/internal/overlay/overlay_windows.go
@@ -5,6 +5,7 @@
import (
    "math"
    "runtime"
    "sync"
    "syscall"
    "time"
    "unsafe"
@@ -257,12 +258,13 @@
}
type winOverlay struct {
    showCh   chan bool
    statusCh chan statusCmd
    volCh    chan float64
    posCh    chan [2]int
    closeCh  chan struct{}
    done     chan struct{}
    showCh    chan bool
    statusCh  chan statusCmd
    volCh     chan float64
    posCh     chan [2]int
    closeCh   chan struct{}
    done      chan struct{}
    closeOnce sync.Once
    barHeights [nBars]float64
    animTime   float64
@@ -307,7 +309,12 @@
func (o *winOverlay) GetPosition() (int, int)         { return o.posX, o.posY }
func (o *winOverlay) Size() (int, int)                { return capW, capH }
func (o *winOverlay) OnDragged(fn func(x, y int))     { o.dragCb = fn }
func (o *winOverlay) Close()                          { close(o.closeCh); <-o.done }
func (o *winOverlay) Close() {
    o.closeOnce.Do(func() {
        close(o.closeCh)
    })
    <-o.done
}
func trySend(ch chan bool, v bool) {
    select {
C1.source/privatevoice.src/internal/paths/paths.go
C1.source/privatevoice.src/internal/permissions/permissions.go
C1.source/privatevoice.src/internal/permissions/permissions_darwin.go
C1.source/privatevoice.src/internal/permissions/permissions_other.go
C1.source/privatevoice.src/internal/singleinstance/singleinstance.go
C1.source/privatevoice.src/internal/singleinstance/singleinstance_unix.go
C1.source/privatevoice.src/internal/singleinstance/singleinstance_windows.go
C1.source/privatevoice.src/internal/sound/sound_other.go
C1.source/privatevoice.src/internal/sound/sound_windows.go
C1.source/privatevoice.src/internal/startup/startup.go
C1.source/privatevoice.src/internal/startup/startup_darwin.go
C1.source/privatevoice.src/internal/startup/startup_darwin_appstore.go
C1.source/privatevoice.src/internal/startup/startup_linux.go
C1.source/privatevoice.src/internal/startup/startup_windows.go
C1.source/privatevoice.src/internal/textproc/textproc.go
C1.source/privatevoice.src/internal/textproc/textproc_test.go
C1.source/privatevoice.src/internal/updater/updater.go
C1.source/privatevoice.src/internal/userdict/userdict.go
C1.source/privatevoice.src/internal/userdict/userdict_test.go
C1.source/privatevoice.src/locale_other.go
C1.source/privatevoice.src/locale_windows.go
C1.source/privatevoice.src/main.go
C1.source/privatevoice.src/scripts/QA-WINDOWS-PREVIEW.ps1
C1.source/privatevoice.src/scripts/RUN-WINDOWS-QA.cmd
C1.source/privatevoice.src/scripts/benchmark-recognition.sh
New file
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WAV_PATH="${1:-"$ROOT_DIR/testdata/perf/baseline.wav"}"
RUNS="${2:-3}"
if [[ ! -f "$WAV_PATH" ]]; then
  cat >&2 <<EOF
Missing WAV fixture: $WAV_PATH
Usage:
  scripts/benchmark-recognition.sh /absolute/path/to/baseline.wav 5
The WAV must be 16kHz mono PCM16. Keep the same fixture across versions for A/B performance comparison.
EOF
  exit 2
fi
cd "$ROOT_DIR"
go run ./cmd/benchmark-recognition --wav "$WAV_PATH" --runs "$RUNS"
C1.source/privatevoice.src/scripts/build-appstore-developerid-macos.sh
C1.source/privatevoice.src/scripts/build-appstore-macos.sh
C1.source/privatevoice.src/scripts/build-local-macos-monterey.sh
C1.source/privatevoice.src/scripts/build-local-macos-universal.sh
C1.source/privatevoice.src/scripts/build-local-macos.sh
C1.source/privatevoice.src/scripts/build-macappstore-pkg-macos.sh
C1.source/privatevoice.src/scripts/build-release-macos.sh
C1.source/privatevoice.src/scripts/build-windows-preview.sh
File was renamed from privatevoice.src/scripts/build-windows-preview.sh
@@ -4,8 +4,8 @@
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
APP_VERSION="2.2.0"
APP_BUILD="20260702.0303"
APP_VERSION="2.2.2"
APP_BUILD="20260703.0335"
ARCH="${1:-amd64}"
if [[ "$ARCH" != "amd64" ]]; then
C1.source/privatevoice.src/scripts/install-x-asr-zh-en-960ms.sh
C1.source/privatevoice.src/scripts/qa-settings-window-macos.sh
C1.source/privatevoice.src/scripts/verify-windows-qa-evidence.sh
C1.source/privatevoice.src/services/app_service.go
C1.source/privatevoice.src/services/audio_service.go
C1.source/privatevoice.src/services/config_service.go
C1.source/privatevoice.src/services/correction_csv_service.go
C1.source/privatevoice.src/services/diagnostics_service.go
New file
@@ -0,0 +1,196 @@
package services
import (
    "archive/zip"
    "encoding/json"
    "fmt"
    "io"
    "os"
    "path/filepath"
    "runtime"
    "time"
    "voicesnap/internal/config"
    "voicesnap/internal/model"
    "voicesnap/internal/modelselection"
    "voicesnap/internal/paths"
)
type DiagnosticsService struct {
    cfg     *config.Config
    version string
}
func NewDiagnosticsService(cfg *config.Config, version string) *DiagnosticsService {
    if cfg == nil {
        cfg = config.Default()
    }
    return &DiagnosticsService{cfg: cfg, version: version}
}
func (s *DiagnosticsService) GetRuntimeStats() map[string]interface{} {
    var mem runtime.MemStats
    runtime.ReadMemStats(&mem)
    return map[string]interface{}{
        "version":       s.version,
        "goos":          runtime.GOOS,
        "goarch":        runtime.GOARCH,
        "go_version":    runtime.Version(),
        "goroutines":    runtime.NumGoroutine(),
        "cpu_count":     runtime.NumCPU(),
        "heap_alloc_mb": bytesToMiB(mem.HeapAlloc),
        "heap_sys_mb":   bytesToMiB(mem.HeapSys),
        "stack_sys_mb":  bytesToMiB(mem.StackSys),
        "next_gc_mb":    bytesToMiB(mem.NextGC),
        "num_gc":        mem.NumGC,
        "data_dir":      paths.AppSupportDir(),
        "models_dir":    paths.ModelsRoot(),
    }
}
func (s *DiagnosticsService) ExportDiagnostics() (string, error) {
    dir := filepath.Join(paths.AppSupportDir(), "diagnostics")
    if err := os.MkdirAll(dir, 0755); err != nil {
        return "", err
    }
    outPath := filepath.Join(dir, "privatevoice-diagnostics-"+time.Now().Format("20060102-150405")+".zip")
    out, err := os.Create(outPath)
    if err != nil {
        return "", err
    }
    defer out.Close()
    zw := zip.NewWriter(out)
    if err := s.addJSON(zw, "runtime.json", s.GetRuntimeStats()); err != nil {
        zw.Close()
        return "", err
    }
    if err := s.addJSON(zw, "config.redacted.json", redactedConfig(s.cfg)); err != nil {
        zw.Close()
        return "", err
    }
    if err := s.addJSON(zw, "models.json", modelDiagnostics(s.cfg)); err != nil {
        zw.Close()
        return "", err
    }
    if err := addLogFiles(zw); err != nil {
        zw.Close()
        return "", err
    }
    if err := zw.Close(); err != nil {
        return "", err
    }
    return outPath, nil
}
func (s *DiagnosticsService) addJSON(zw *zip.Writer, name string, value interface{}) error {
    data, err := json.MarshalIndent(value, "", "  ")
    if err != nil {
        return err
    }
    w, err := zw.Create(name)
    if err != nil {
        return err
    }
    _, err = w.Write(append(data, '\n'))
    return err
}
func addLogFiles(zw *zip.Writer) error {
    for _, name := range []string{"app.log", "app.log.1", "app.log.2", "app.log.3"} {
        path := filepath.Join(paths.AppSupportDir(), name)
        if err := addFileIfExists(zw, path, filepath.Join("logs", name)); err != nil {
            return err
        }
    }
    return nil
}
func addFileIfExists(zw *zip.Writer, path, zipName string) error {
    in, err := os.Open(path)
    if err != nil {
        if os.IsNotExist(err) {
            return nil
        }
        return err
    }
    defer in.Close()
    info, err := in.Stat()
    if err != nil {
        return err
    }
    header, err := zip.FileInfoHeader(info)
    if err != nil {
        return err
    }
    header.Name = zipName
    header.Method = zip.Deflate
    w, err := zw.CreateHeader(header)
    if err != nil {
        return err
    }
    _, err = io.Copy(w, in)
    return err
}
func redactedConfig(cfg *config.Config) map[string]interface{} {
    if cfg == nil {
        cfg = config.Default()
    }
    return map[string]interface{}{
        "HotkeyVK":           cfg.HotkeyVK,
        "HotkeyMode":         cfg.HotkeyMode,
        "AutoHide":           cfg.AutoHide,
        "SoundFeedback":      cfg.SoundFeedback,
        "HideDockIcon":       cfg.HideDockIcon,
        "CopyToClipboard":    cfg.CopyToClipboard,
        "DeviceNameSet":      cfg.DeviceName != "",
        "SelectedModelID":    cfg.SelectedModelID,
        "ModelSelectionMode": cfg.ModelSelectionMode,
        "LanguageMode":       cfg.LanguageMode,
        "LanguageID":         cfg.LanguageID,
    }
}
func modelDiagnostics(cfg *config.Config) map[string]interface{} {
    current := modelselection.Resolve(cfg, nil)
    items := make([]map[string]interface{}, 0)
    for _, profile := range model.ListModelProfiles() {
        resolved, err := model.ResolveModel(profile.ID)
        item := map[string]interface{}{
            "model_id":      profile.ID,
            "display_name":  profile.DisplayName,
            "backend":       profile.BackendKind,
            "tier":          profile.Tier,
            "num_threads":   profile.NumThreads,
            "current":       profile.ID == current.ModelID,
            "supported":     model.IsModelSupportedInCurrentBuild(profile.ID),
            "download_size": profile.ApproxSize,
        }
        if err != nil {
            item["status"] = "resolve_error"
            item["error"] = err.Error()
        } else {
            item["status"] = string(resolved.Status)
            item["usable"] = resolved.IsUsable()
            item["source_dir_kind"] = resolved.SourceDirKind
            item["missing"] = resolved.Missing
            item["problems"] = resolved.Problems
        }
        items = append(items, item)
    }
    return map[string]interface{}{
        "current_model_id":      current.ModelID,
        "selection_mode":        current.SelectionMode,
        "effective_language_id": current.LanguageSettings.EffectiveLanguageID,
        "fallback_reason":       current.FallbackReason,
        "has_any_usable_model":  model.HasAnyUsableModel(),
        "supported_build_note":  fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
        "models":                items,
    }
}
func bytesToMiB(value uint64) float64 {
    return float64(value) / 1024.0 / 1024.0
}
C1.source/privatevoice.src/services/engine_service.go
C1.source/privatevoice.src/services/engine_service_qwen3_default_test.go
C1.source/privatevoice.src/services/engine_service_qwen3_monterey_test.go
C1.source/privatevoice.src/services/engine_service_test.go
C1.source/privatevoice.src/services/engine_service_windows_test.go
C1.source/privatevoice.src/services/history_service.go
C1.source/privatevoice.src/services/hotkey_service.go
C1.source/privatevoice.src/services/permission_service.go
C1.source/privatevoice.src/services/updater_service.go
C1.source/privatevoice.src/services/userdict_service.go
C1.source/privatevoice.src/system_load_darwin.go
C1.source/privatevoice.src/system_load_other.go
C1.source/privatevoice.src/testdata/round1/README.md
C1.source/privatevoice.src/testdata/round1/models-incomplete-missing-model/sensevoice-zh/tokens.txt
C1.source/privatevoice.src/testdata/round1/models-incomplete-missing-tokens/sensevoice-zh/model.int8.onnx
C1.source/privatevoice.src/testdata/round1/models-valid-legacy/sensevoice/model.int8.onnx
C1.source/privatevoice.src/testdata/round1/models-valid-legacy/sensevoice/tokens.txt
C1.source/privatevoice.src/testdata/round1/models-valid-new/sensevoice-zh/model.int8.onnx
C1.source/privatevoice.src/testdata/round1/models-valid-new/sensevoice-zh/tokens.txt
C1.source/privatevoice.src/testdata/round1/models-zero-byte/sensevoice-zh/tokens.txt
C1.source/privatevoice.src/testdata/round1/state-invalid-json/state.json
C1.source/privatevoice.src/testdata/round1/state-points-to-broken/broken/tokens.txt
C1.source/privatevoice.src/testdata/round1/state-points-to-broken/sensevoice/model.int8.onnx
C1.source/privatevoice.src/testdata/round1/state-points-to-broken/sensevoice/tokens.txt
C1.source/privatevoice.src/testdata/round1/state-points-to-broken/state.json
C1.source/privatevoice.src/testdata/round1/state-points-to-legacy/sensevoice/model.int8.onnx
C1.source/privatevoice.src/testdata/round1/state-points-to-legacy/sensevoice/tokens.txt
C1.source/privatevoice.src/testdata/round1/state-points-to-legacy/state.json
C1.source/privatevoice.src/voicesnap.rc
C1.source/privatevoice.src/wails.json
C2.builds/PrivateVoice-Dictation-2.2.0-build20260702.0303/RELEASE_MANIFEST.md
New file
@@ -0,0 +1,66 @@
# PrivateVoice Dictation 2.2.0 Build 20260702.0303
## Release Scope
- Type: real-user test candidate
- Version: 2.2.0
- Build: 20260702.0303
- Branch: codex/x-asr-experiment-2.1.29
- Commit: 0ffbf1935c9d091cce22a5583275ac0902f7a693
- Tag: v2.2.0-build20260702.0303-real-user-test
- Generated: 2026-07-02 03:09:18 CST
- Operator: Codex
This package set contains three macOS DMGs and one Windows preview ZIP for real-user testing. It is not an App Store package set. macOS DMGs are locally signed and not notarized. The Windows ZIP is an unsigned preview package.
## Artifacts
| Platform | Artifact | Size |
| --- | --- | --- |
| macOS Apple Silicon | `PrivateVoice-Dictation-2.2.0-build20260702.0303-macos-arm64.dmg` | 19M |
| macOS Intel | `PrivateVoice-Dictation-2.2.0-build20260702.0303-macos-x86_64.dmg` | 19M |
| macOS Universal | `PrivateVoice-Dictation-2.2.0-build20260702.0303-macos-universal.dmg` | 37M |
| Windows amd64 preview | `PrivateVoice-Dictation-2.2.0-build20260702.0303-windows-amd64-preview.zip` | 20M |
## SHA256
```text
db7b7a0c5ac14dd664c8430f9033ce7adb227ee1f30cc3cd38b6ce0bf44eb784  PrivateVoice-Dictation-2.2.0-build20260702.0303-macos-arm64.dmg
f72aca7f0c6a41ba15e8d618a924eda59c1238e6cd780844bf1bfad5ca75b539  PrivateVoice-Dictation-2.2.0-build20260702.0303-macos-universal.dmg
4ef0fc8afe40a9a2a843bc320a940b4f29d536da918dafaf5b0e0407c831703b  PrivateVoice-Dictation-2.2.0-build20260702.0303-macos-x86_64.dmg
82118105340a187bdc954eff01ef0ac2824e86037342e0606821dbf86d9a6b42  PrivateVoice-Dictation-2.2.0-build20260702.0303-windows-amd64-preview.zip
```
## Build Commands
```bash
cd /Users/ar/Projects/PrivateVoice/privatevoice.src
go test ./... -count=1
npm --prefix frontend run build
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go test -exec=/usr/bin/true ./...
./scripts/build-local-macos-universal.sh
./scripts/build-windows-preview.sh
```
## Validation
- `go test ./... -count=1`: passed
- `npm --prefix frontend run build`: passed with existing Svelte accessibility warnings
- Windows cross-compile smoke test: passed
- `git diff --check`: passed
- `hdiutil verify` for all three DMGs: passed
- `unzip -t` for Windows ZIP: passed
- `shasum -a 256 -c SHA256SUMS.txt`: passed
- macOS binary architectures:
  - arm64 DMG app: `arm64`
  - x86_64 DMG app: `x86_64`
  - universal DMG app: `x86_64 arm64`
- `codesign --verify --deep --strict --verbose=2` for all three local macOS apps: passed
- `com.apple.quarantine`: not present on release artifacts
## Notes
- This build includes Round 6 Cantonese language/model option work and prior Windows overlay/P1 download workflow changes from the current branch.
- Real Windows machine QA is still required before calling the Windows package stable.
- Monterey compatibility has not been revalidated in this package set.
- The release directory also contains `SHA256SUMS.txt` for independent checksum verification.
C2.builds/PrivateVoice-Dictation-2.2.0-build20260702.0303/SHA256SUMS.txt
New file
@@ -0,0 +1,4 @@
db7b7a0c5ac14dd664c8430f9033ce7adb227ee1f30cc3cd38b6ce0bf44eb784  PrivateVoice-Dictation-2.2.0-build20260702.0303-macos-arm64.dmg
f72aca7f0c6a41ba15e8d618a924eda59c1238e6cd780844bf1bfad5ca75b539  PrivateVoice-Dictation-2.2.0-build20260702.0303-macos-universal.dmg
4ef0fc8afe40a9a2a843bc320a940b4f29d536da918dafaf5b0e0407c831703b  PrivateVoice-Dictation-2.2.0-build20260702.0303-macos-x86_64.dmg
82118105340a187bdc954eff01ef0ac2824e86037342e0606821dbf86d9a6b42  PrivateVoice-Dictation-2.2.0-build20260702.0303-windows-amd64-preview.zip
C2.builds/PrivateVoice-Dictation-2.2.2-build20260703.0335/RELEASE_MANIFEST.md
New file
@@ -0,0 +1,106 @@
# PrivateVoice Dictation 2.2.2 Build 20260703.0335
## Release Scope
- Type: real-user test candidate
- Version: 2.2.2
- Build: 20260703.0335
- Branch: codex/x-asr-experiment-2.1.29
- Source freeze commit: 5fbe9789faf05ce99bbff6ec4cd0b905519acbd0
- Package build commit: 72b84399db9001a3b26ddad00c307438d940d8ee
- Release tag commit: a075aec123e1f7118f138738f196c2995aa96f98
- Tag: v2.2.2-build20260703.0335-real-user-test
- Generated: 2026-07-03 05:27:09 CST
- Operator: Codex, R221-4 operations / release
This package set contains three locally signed macOS DMGs and one unsigned Windows amd64 preview ZIP for real-user testing. It is not a notarized official website release. The Windows package is a preview ZIP, not a signed installer or stable Windows release.
## Artifacts
| Platform | Artifact | Size |
| --- | --- | --- |
| macOS Apple Silicon | `PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-arm64.dmg` | 20294112 bytes |
| macOS Universal | `PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-universal.dmg` | 39249190 bytes |
| macOS Intel | `PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-x86_64.dmg` | 19902000 bytes |
| Windows amd64 preview | `PrivateVoice-Dictation-2.2.2-build20260703.0335-windows-amd64-preview.zip` | 20585998 bytes |
## SHA256
```text
9dba061d838ea3c4448eecb09d276ed2143b6e73d66c9fb8e5be87d6244f8737  PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-arm64.dmg
117a6efa81be3fc9b79f12d0b03b1b778f8570b6207336b97180b28b96c39259  PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-universal.dmg
fbc915059f364bfc88323d6f14b607d82b3c88c1a14de0c8af9b500e726a7fea  PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-x86_64.dmg
da44563e83c8806d2e5a848a309dd627cb1abc040b7232d9aa098ac774486858  PrivateVoice-Dictation-2.2.2-build20260703.0335-windows-amd64-preview.zip
```
## Build Commands
Run from `/Users/ar/Projects/PrivateVoice/03-O/C1.source/privatevoice.src`:
```bash
PATH="/opt/homebrew/opt/openssl@3/bin:$PATH" ./scripts/build-local-macos.sh arm64
PATH="/opt/homebrew/opt/openssl@3/bin:$PATH" ./scripts/build-local-macos.sh x86_64
PATH="/opt/homebrew/opt/openssl@3/bin:$PATH" ./scripts/build-local-macos-universal.sh
./scripts/build-windows-preview.sh
```
Environment tools added during R221-4:
- `mingw-w64` via Homebrew, required for Windows preview ZIP generation.
- `openssl@3` via Homebrew, required by the local signing identity creation path in `build-local-macos.sh`.
## Validation
Run from `/Users/ar/Projects/PrivateVoice/03-O/C1.source/privatevoice.src` before packaging:
```bash
git diff --check
bash -n scripts/build-local-macos.sh && bash -n scripts/build-local-macos-universal.sh && bash -n scripts/build-windows-preview.sh && bash -n scripts/verify-windows-qa-evidence.sh
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go test -exec=/usr/bin/true ./...
go test ./...
npm --prefix frontend run build
```
Run from this release directory after packaging:
```bash
shasum -a 256 -c SHA256SUMS.txt
hdiutil verify 'PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-arm64.dmg'
hdiutil verify 'PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-x86_64.dmg'
hdiutil verify 'PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-universal.dmg'
unzip -t 'PrivateVoice-Dictation-2.2.2-build20260703.0335-windows-amd64-preview.zip'
```
Results:
- `git diff --check`: passed.
- Script syntax checks: passed.
- Windows cross smoke: passed after installing MinGW.
- `go test ./...`: passed with existing macOS linker warnings.
- `npm --prefix frontend run build`: passed with existing Svelte accessibility warnings.
- macOS DMG builds: passed; local codesign verification and `hdiutil verify` passed in scripts.
- Windows preview ZIP build: passed.
- `shasum -a 256 -c SHA256SUMS.txt`: passed for all four artifacts.
- Final `hdiutil verify`: passed for all three copied DMGs.
- Final `unzip -t`: passed for Windows preview ZIP.
- macOS binary architectures: arm64 DMG app `arm64`; x86_64 DMG app `x86_64`; universal DMG app `x86_64 arm64`.
- `com.apple.quarantine`: not present on release artifacts.
## CODEGRAPH And Codebase Memory
- CODEGRAPH checked: `/Users/ar/Projects/PrivateVoice/03-O/CODEGRAPH.md`.
- CODEGRAPH result: updated for R221 `2.2.2 build20260703.0335`, codebase-memory project path, excluded recovery snapshot, and current release gaps.
- codebase-memory status: refreshed.
- Refresh command: `codebase-memory-mcp cli index_repository '{"repo_path":"/Users/ar/Projects/PrivateVoice/03-O"}'`.
- Artifact path: `/Users/ar/Projects/PrivateVoice/03-O/.codebase-memory/artifact.json` and `/Users/ar/Projects/PrivateVoice/03-O/.codebase-memory/graph.db.zst`.
- Indexed commit recorded by artifact: 72b84399db9001a3b26ddad00c307438d940d8ee.
- Release tag commit: a075aec123e1f7118f138738f196c2995aa96f98.
- Difference check: `git diff --name-only 72b84399db9001a3b26ddad00c307438d940d8ee..a075aec123e1f7118f138738f196c2995aa96f98 -- C1.source/privatevoice.src` produced no output, so the tag commit has no build-affecting source difference from the package build commit.
## Known Gaps
- macOS DMGs are locally signed and not notarized.
- Windows package is an unsigned preview ZIP, not a stable Windows release.
- Windows 10/11 real-machine QA evidence is still required before calling Windows stable.
- Monterey compatibility was not validated for this package set.
- Developer ID notarization, official download page, authorization / activation, update, support, and rollback flows remain separate release tasks.
C2.builds/PrivateVoice-Dictation-2.2.2-build20260703.0335/SHA256SUMS.txt
New file
@@ -0,0 +1,4 @@
9dba061d838ea3c4448eecb09d276ed2143b6e73d66c9fb8e5be87d6244f8737  PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-arm64.dmg
117a6efa81be3fc9b79f12d0b03b1b778f8570b6207336b97180b28b96c39259  PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-universal.dmg
fbc915059f364bfc88323d6f14b607d82b3c88c1a14de0c8af9b500e726a7fea  PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-x86_64.dmg
da44563e83c8806d2e5a848a309dd627cb1abc040b7232d9aa098ac774486858  PrivateVoice-Dictation-2.2.2-build20260703.0335-windows-amd64-preview.zip
C2.builds/README.md
New file
@@ -0,0 +1,13 @@
# C2.builds
本目录保存当前候选包和迁移时保留的历史候选包。
当前最新:
- `PrivateVoice-Dictation-2.2.2-build20260703.0335/`
历史保留:
- `PrivateVoice-Dictation-2.2.0-build20260702.0303/`
二进制 `.dmg` 和 `.zip` 默认不提交 Git;本地文件用于当前机器接手、校验和分发准备。长期事实以 `RELEASE_MANIFEST.md`、`SHA256SUMS.txt`、`K2.项目管理/发布地图.md` 和会话主题摘要为准。
C3.tools/README.md
New file
@@ -0,0 +1,19 @@
# C3.tools
工具入口索引。实际构建、打包和 QA 脚本仍跟随源码维护,位于:
```text
../C1.source/privatevoice.src/scripts/
```
常用脚本:
- `build-local-macos.sh`
- `build-local-macos-universal.sh`
- `build-local-macos-monterey.sh`
- `build-release-macos.sh`
- `build-windows-preview.sh`
- `verify-windows-qa-evidence.sh`
- `qa-settings-window-macos.sh`
不要在本目录复制一份脚本实现,避免脚本漂移。
CHANGELOG.md
@@ -1,5 +1,21 @@
# Changelog
## v2.2.2 (2026-07-03)
### 性能与稳定性恢复
- **恢复 2.2.1 本地体验包中的性能修复**:合入慢识别诊断、流水线耗时记录、识别 benchmark 工具和诊断服务,用于定位模型、录音、识别、粘贴和 UI 生命周期耗时。
- **强化引擎生命周期保护**:补入引擎重载串行化、释放保护和跨平台占位实现,降低频繁切换语言 / 模型或长时间运行时的状态竞争风险。
- **补入录音、热键、overlay 和事件订阅清理**:收敛 recorder、hotkey、overlay 与前端事件订阅的生命周期泄漏风险,改善长时间真实使用稳定性。
- **保留正式版本口径**:本版本恢复的是 `2.2.1 build 20260702.1737` 的修复内容,但正式候选版本升为 `2.2.2`,避免与旧 macOS arm64 local 体验包混淆。
### 构建
- build: `20260703.0335`
- 说明: 本版本为 2.2.1 性能 / 稳定性修复恢复后的正式候选源码目标;进入四包冻结前仍需代码审核、QA 和发布门禁。
---
## v2.2.0 (2026-07-02)
### 粤语语言版本
CODEGRAPH.md
@@ -3,20 +3,38 @@
## Project
- Product: Wails v3 / Go desktop dictation app.
- Current branch purpose: Round 6 Cantonese language version real-user test package based on `v2.2.0-build20260702.0303`.
- Protected macOS behavior: existing macOS App Store / local DMG build lines are not changed by this branch.
- Git root: `/Users/ar/Projects/PrivateVoice/03-O`.
- Source root: `/Users/ar/Projects/PrivateVoice/03-O/C1.source/privatevoice.src`.
- Current branch purpose: R221 recovery freeze for `2.2.2 build20260703.0335`, restoring reviewed `2.2.1` fixes into the current active source and producing four real-user test candidate packages.
- Protected behavior: existing macOS App Store, local DMG, Monterey compatibility, Windows preview, model download safety, and paste/clipboard protection must not be changed without a scoped task and QA plan.
## Automatic Codebase Memory
- Status: `enabled`.
- Tool: `codebase-memory-mcp` 0.8.1.
- Current project name after path unification: `Users-ar-Projects-PrivateVoice-03-O`.
- Current artifact note: `.codebase-memory/artifact.json` must record project `Users-ar-Projects-PrivateVoice-03-O` before each freeze / release package.
- Shared artifact: `.codebase-memory/graph.db.zst`; metadata: `.codebase-memory/artifact.json`.
- Scope: `/Users/ar/Projects/PrivateVoice/03-O`.
- Refresh command: `codebase-memory-mcp cli index_repository '{"repo_path":"/Users/ar/Projects/PrivateVoice/03-O"}'`.
- Current `indexed_at`, commit, node count, and edge count are recorded in `.codebase-memory/artifact.json`.
- Excluded by indexer: `.git`, `K3.运营与发布资料/legacy-运营资料/PrivateVoice-26.0604上架`, `K3.运营与发布资料/legacy-运营资料/运营文档`, `C1.source/privatevoice-2.2.1-recovered.src`, `C1.source/privatevoice.src/frontend/dist`, `C1.source/privatevoice.src/frontend/node_modules`.
- Query example after regenerating graph from Git root: `codebase-memory-mcp cli search_graph '{"project":"Users-ar-Projects-PrivateVoice-03-O","query":"model registry","limit":5}'`.
- This automatic graph is an exploration aid only; it does not replace source reading, tests, QA evidence, or this `CODEGRAPH.md`.
- Refresh rule: regenerate `.codebase-memory/` before each source freeze, version freeze, formal rollback-capable test package, or release package; daily Coder work-in-progress changes do not force a refresh.
- Freeze record requirement: record the refresh command, `.codebase-memory/artifact.json` path, indexed commit, freeze commit, and whether the two commits match or differ only by `.codebase-memory/` artifact updates.
## Core Entrypoints
- `privatevoice.src/main.go`: process startup, data migration, logger, single-instance guard.
- `privatevoice.src/app.go`: Wails app wiring, settings window, tray, recorder, hotkey loop, ASR init, paste orchestration.
- `privatevoice.src/embed.go`: embeds frontend assets and app/tray icons.
- `C1.source/privatevoice.src/main.go`: process startup, data migration, logger, single-instance guard.
- `C1.source/privatevoice.src/app.go`: Wails app wiring, settings window, tray, recorder, hotkey loop, ASR init, paste orchestration.
- `C1.source/privatevoice.src/embed.go`: embeds frontend assets and app/tray icons.
## Language And Model Registry
- `privatevoice.src/internal/model/profile.go`: model and language IDs, model profile shape, install status types.
- `privatevoice.src/internal/model/registry.go`: language profile order, model profiles, language profiles, and normalization.
- `privatevoice.src/internal/modelselection/selection.go`: resolves current language and model, including manual-model fallback.
- `C1.source/privatevoice.src/internal/model/profile.go`: model and language IDs, model profile shape, install status types.
- `C1.source/privatevoice.src/internal/model/registry.go`: language profile order, model profiles, language profiles, and normalization.
- `C1.source/privatevoice.src/internal/modelselection/selection.go`: resolves current language and model, including manual-model fallback.
- Round 6 Cantonese path:
  - Language ID: `yue-HK`, displayed as `粤语`.
  - Default model: `sensevoice-yue-2025-09-09`.
@@ -25,23 +43,20 @@
## Platform Modules
- Audio: `privatevoice.src/internal/audio/recorder.go`
  - Uses `github.com/gen2brain/malgo`.
  - Cross-platform capture target: 16 kHz, 16-bit, mono.
- Audio: `C1.source/privatevoice.src/internal/audio/recorder.go`
- Hotkey:
  - macOS: `privatevoice.src/internal/hotkey/hotkey_darwin.go`
  - Windows: `privatevoice.src/internal/hotkey/hotkey_windows.go`
  - macOS: `C1.source/privatevoice.src/internal/hotkey/hotkey_darwin.go`
  - Windows: `C1.source/privatevoice.src/internal/hotkey/hotkey_windows.go`
- Text input:
  - macOS: `privatevoice.src/internal/input/paste_darwin.go`
  - Windows: `privatevoice.src/internal/input/paste_windows.go`
  - macOS: `C1.source/privatevoice.src/internal/input/paste_darwin.go`
  - Windows: `C1.source/privatevoice.src/internal/input/paste_windows.go`
- ASR engine:
  - macOS: `privatevoice.src/internal/engine/engine_darwin.go`
  - Windows: `privatevoice.src/internal/engine/engine_windows.go`
  - Linux: `privatevoice.src/internal/engine/engine_linux.go`
  - SenseVoice profiles may set `Profile.LanguageParam`; Cantonese uses `yue`.
  - macOS: `C1.source/privatevoice.src/internal/engine/engine_darwin.go`
  - Windows: `C1.source/privatevoice.src/internal/engine/engine_windows.go`
  - Linux: `C1.source/privatevoice.src/internal/engine/engine_linux.go`
- Overlay:
  - macOS: `privatevoice.src/internal/overlay/overlay_darwin.go`
  - Windows: `privatevoice.src/internal/overlay/overlay_windows.go`
  - macOS: `C1.source/privatevoice.src/internal/overlay/overlay_darwin.go`
  - Windows: `C1.source/privatevoice.src/internal/overlay/overlay_windows.go`
## Windows Preview Scope
@@ -49,31 +64,21 @@
- Non-SenseVoice models remain visible in settings but are disabled through `model.IsModelSupportedInCurrentBuild`.
- Windows unsupported reason: `windows_preview_unsupported`.
- Windows overlay remains native Win32/GDI+, with a larger translucent spectrum-style capsule indicator for recording states.
- Build script: `privatevoice.src/scripts/build-windows-preview.sh`.
- Evidence verifier: `privatevoice.src/scripts/verify-windows-qa-evidence.sh`.
- Runtime package includes:
  - `PrivateVoice Dictation.exe`
  - `onnxruntime.dll`
  - `sherpa-onnx-c-api.dll`
  - `sherpa-onnx-cxx-api.dll`
  - `RUN-WINDOWS-QA.cmd`
  - `QA-WINDOWS-PREVIEW.ps1`
  - `README-WINDOWS-PREVIEW.txt`
- Build script: `C1.source/privatevoice.src/scripts/build-windows-preview.sh`.
- Evidence verifier: `C1.source/privatevoice.src/scripts/verify-windows-qa-evidence.sh`.
## macOS Local Package Scope
## macOS Package Scope
- Local DMG script: `privatevoice.src/scripts/build-local-macos.sh`.
- Universal DMG script: `privatevoice.src/scripts/build-local-macos-universal.sh`.
- Local macOS package set for this branch:
  - `arm64` DMG for Apple Silicon.
  - `x86_64` DMG for Intel Mac.
  - `universal` DMG for Intel + Apple Silicon.
- Mainline macOS local packages keep `LSMinimumSystemVersion=14.0` and the default Qwen3-capable dependency line.
- Monterey-compatible packages remain a separate build line through `privatevoice.src/scripts/build-local-macos-monterey.sh`.
- Local DMG script: `C1.source/privatevoice.src/scripts/build-local-macos.sh`.
- Universal DMG script: `C1.source/privatevoice.src/scripts/build-local-macos-universal.sh`.
- Monterey compatibility script: `C1.source/privatevoice.src/scripts/build-local-macos-monterey.sh`.
- Developer ID / notarization script: `C1.source/privatevoice.src/scripts/build-release-macos.sh`.
- Mainline macOS local packages keep `LSMinimumSystemVersion=14.0`.
- Monterey-compatible packages remain a separate build line.
## Standard Verification Commands
Run from `privatevoice.src`:
Run from `C1.source/privatevoice.src`:
```bash
npm --prefix frontend run build
@@ -88,8 +93,7 @@
## Known Gaps
- Windows runtime QA must still be performed on Windows 10/11 hardware.
- Run `RUN-WINDOWS-QA.cmd` from the extracted preview package to collect WebView2/audio preflight, startup, engine, recording, recognition, paste, and Notepad target text evidence.
- After evidence is returned, run `verify-windows-qa-evidence.sh` and require `WINDOWS_QA_EVIDENCE_PASS` before claiming the goal complete.
- The Windows preview zip is unsigned and not an installer.
- SmartScreen / Defender prompts are expected for local testing.
- Windows runtime QA must still be performed on Windows 10/11 hardware before calling Windows stable.
- macOS 2.2.2 real-user test DMGs are locally signed and not notarized.
- Monterey compatibility was not revalidated for the 2.2.2 package set.
- Independent-site sales still need Developer ID release packaging, download page, authorization/activation, update strategy, support path, release manifest, and rollback policy.
K1.业务知识/成果地图.md
New file
@@ -0,0 +1,41 @@
# 成果地图
## 代码成果
- 当前源码根:`03-O/C1.source/privatevoice.src`
- 工程地图:`03-O/CODEGRAPH.md`
- 自动代码知识图谱:`03-O/.codebase-memory/`
- 主要技术栈:Go、Wails v3、Svelte 5、Vite、sherpa-onnx、malgo。
## 产品成果
- 离线语音输入桌面 App。
- 正式语言:中文、English、French、German、Spanish、Italian、Portuguese、Japanese、Korean。
- 真实用户测试候选:粤语 `yue-HK`,默认 `SenseVoice Yue 2025-09-09`。
- Windows preview:当前只支持 SenseVoice。
## QA 成果
- Round 6 粤语实现:`go test ./... -count=1`、前端 build、Windows cross test、`git diff --check` 通过。
- 2.2.0 四包候选:DMG verify、ZIP test、SHA256、架构和 codesign 校验通过。
- 详见 `03-O/K2.项目管理/QA地图.md`。
## 发布成果
- 最新候选包:`03-O/C2.builds/PrivateVoice-Dictation-2.2.0-build20260702.0303/`
- 包类型:真实用户测试候选包。
- 不是官网正式发布包;未 notarize。
- 详见 `03-O/K2.项目管理/发布地图.md`。
## 不作为当前成果的材料
- `03-O/K2.项目管理/legacy/` 下旧 Round 文档只作历史证据。
- 旧原始 session 正文不迁入新项目正文。
- `03-O/K3.运营与发布资料/legacy-运营资料/` 是本地运营素材,不等同当前发布事实。
## 当前缺口
- 官网独立站销售闭环。
- Windows 正式签名安装器与完整 QA。
- Monterey 覆盖验证。
- 自动更新策略重定向到官网发布链路。
K2.项目管理/QA地图.md
New file
@@ -0,0 +1,48 @@
# QA 地图
## 当前 QA 结论
迁移接收验证与产品 QA 结论必须区分。
- 迁移接收验证:由 `接收验证记录.md` 记录。
- Round 6 产品 QA:已有会话和 release manifest 证明基础自动化/包校验通过。
- Windows 真机 QA:仍未由本迁移补跑。
- 官网正式发布 QA:仍未完成 notarization、下载页、授权/激活、更新和回滚链路验证。
## 标准源码验证
从 `03-O/C1.source/privatevoice.src` 执行:
```bash
npm --prefix frontend run build
go test ./...
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go test -exec=/usr/bin/true ./...
```
## 包验证
从 `03-O/C2.builds/PrivateVoice-Dictation-2.2.0-build20260702.0303` 执行:
```bash
shasum -a 256 -c SHA256SUMS.txt
hdiutil verify 'PrivateVoice-Dictation-2.2.0-build20260702.0303-macos-arm64.dmg'
hdiutil verify 'PrivateVoice-Dictation-2.2.0-build20260702.0303-macos-x86_64.dmg'
hdiutil verify 'PrivateVoice-Dictation-2.2.0-build20260702.0303-macos-universal.dmg'
unzip -t 'PrivateVoice-Dictation-2.2.0-build20260702.0303-windows-amd64-preview.zip'
```
## Windows QA 入口
在 Windows preview 包解压后运行:
```text
RUN-WINDOWS-QA.cmd
```
收回 evidence zip 后,在源码根执行:
```bash
./scripts/verify-windows-qa-evidence.sh /path/to/qa-evidence-YYYYMMDD-HHMMSS.zip
```
只有出现 `WINDOWS_QA_EVIDENCE_PASS`,才能声称 Windows preview evidence 通过。
K2.项目管理/R221-1-开发自查.md
New file
@@ -0,0 +1,101 @@
# R221-1 开发自查
## 任务
- 任务 ID:`R221-1`
- Review Gate ID:`R221-2-GATE-20260703.0335`
- 目标:把归档 `2.2.1 build 20260702.1737` 中的性能与稳定性修复合入当前活跃源码根,并按 Owner 已确认口径升为 `2.2.2` 新 build。
- 不做范围:不生成四包、不创建 release tag、不把归档 arm64 local DMG 改称当前正式候选、不跳过代码审核 / QA / 发布门禁。
## 起点
- 当前 Git 根:`/Users/ar/Projects/PrivateVoice/03-O`
- 当前活跃源码根:`03-O/C1.source/privatevoice.src`
- 恢复快照:`03-O/C1.source/privatevoice-2.2.1-recovered.src`
- 归档仓库:`/Users/ar/Downloads/0702codex-move/02-project2/26.0702-Archive/PrivateVoice`
- 归档修复 commit:`ada0f669729d7a30b0112fe0af34a65cdfde2a32`
- 对比基线:`0ffbf1935c9d091cce22a5583275ac0902f7a693`
## 已执行
- 将归档 commit 中 `privatevoice.src/` 下的变更文件复制到当前活跃源码根。
- 新增 / 恢复:
  - `C1.source/privatevoice.src/cmd/benchmark-recognition/main.go`
  - `C1.source/privatevoice.src/scripts/benchmark-recognition.sh`
  - `C1.source/privatevoice.src/services/diagnostics_service.go`
- 更新性能 / 稳定性相关代码:
  - App 识别流水线、慢识别日志、诊断服务、benchmark 入口。
  - Recorder、engine、hotkey、overlay、logger、前端事件订阅和设置页生命周期处理。
- 将版本元数据从归档体验包的 `2.2.1 / 20260702.1737` 改为正式候选目标 `2.2.2 / 20260703.0335`:
  - `C1.source/privatevoice.src/app.go`
  - `C1.source/privatevoice.src/build/config.yml`
  - `C1.source/privatevoice.src/build/darwin/Info.plist`
  - `C1.source/privatevoice.src/frontend/package.json`
  - `C1.source/privatevoice.src/frontend/package-lock.json`
  - `C1.source/privatevoice.src/scripts/build-windows-preview.sh`
- 更新 `CHANGELOG.md`,新增 `v2.2.2 (2026-07-03)` 记录。
## 已验证
通过:
```bash
git -C '03-O' diff --check
npm --prefix '03-O/C1.source/privatevoice.src/frontend' run build
gofmt -w <changed-go-files>
go test ./...
```
前端 build 通过,仍有既有 Svelte a11y warning:
- `src/components/update/UpdateDialog.svelte`
- `src/components/shared/ToggleSwitch.svelte`
Go 门禁已在 R221-1A 后续补跑通过;`go test ./...` 输出存在 macOS 链接 warning:当前 Go / CLT 在 macOS 26 环境生成的临时 object 标记高于 `macOS-11.0` 链接目标,且 `@rpath/libonnxruntime.1.24.4.dylib` 标记最低 macOS 14.0。该 warning 未导致测试失败,但代码审核 / 发布前需评估目标系统兼容口径。
## R221-1A 补修记录
- Review Gate ID:`R221-2-GATE-20260703.0335`
- 处理范围:只处理 R221-2 `CHANGES_REQUESTED` 阻断项;未生成安装包、未创建 tag、未进入 QA、未扩展官网 / Windows 正式版 / Monterey backlog。
已处理:
- 已回退默认 UI fallback 从中文改为英文的产品外变更:
  - `C1.source/privatevoice.src/frontend/src/App.svelte`
  - `C1.source/privatevoice.src/frontend/src/components/settings/LanguagePage.svelte`
  - `C1.source/privatevoice.src/frontend/src/lib/i18n/index.ts`
- 已将新增文件纳入待审 diff / 后续提交范围:使用 `git add -N -- ...` 标记 intent-to-add,复核状态为 ` A`:
  - `C1.source/privatevoice.src/cmd/benchmark-recognition/main.go`
  - `C1.source/privatevoice.src/scripts/benchmark-recognition.sh`
  - `C1.source/privatevoice.src/services/diagnostics_service.go`
本轮验证:
```bash
git diff --check
gofmt -w <changed-go-files>
go test ./...
```
结果:
- `git diff --check`:PASS。
- `gofmt -w <changed-go-files>`:PASS,已覆盖当前待审 diff 中的 17 个 `.go` 变更文件。
- `go test ./...`:PASS。
- 环境:`go version go1.26.4 darwin/arm64`;Apple clang `17.0.0`,Command Line Tools 路径 `/Library/Developer/CommandLineTools`。
剩余风险:
- `go test ./...` 输出 macOS 链接 warning:当前 Go / CLT 生成物标记高于 `macOS-11.0` 链接目标,且 `@rpath/libonnxruntime.1.24.4.dylib` 标记最低 macOS 14.0;未导致测试失败,但需代码审核 / 发布流程评估对 Monterey 或更低版本兼容口径的影响。
## 风险点
- 本轮恢复的是历史修复 commit 的源码内容,尚未完成代码审核。
- Go 编译 / 测试已在本环境补跑通过,但存在 macOS 链接 warning,需在代码审核 / 发布前判断是否影响目标系统兼容承诺。
- `2.2.2` 四包、manifest、SHA256、tag、codebase-memory 刷新均未执行,必须在 R221-2 / R221-3 PASS 后由发布流程完成。
## 交接
- 当前 R221-1A 结论:新增文件纳入 diff、UI fallback 回退、`gofmt` 与 `go test ./...` 均已完成;可针对同一 Review Gate ID `R221-2-GATE-20260703.0335` 请求复审。
- 下一步:PMO 重新派发 R221-2 复审;R221-2 PASS 前不得进入 QA / 发布冻结。
- 代码审核重点:恢复 diff 是否完整、版本口径是否正确、性能诊断和生命周期修复是否引入回归、Windows preview 构建脚本是否仍符合规则。
K2.项目管理/R221-3-QA-evidence.md
New file
@@ -0,0 +1,96 @@
# R221-3 QA Evidence
## 结论
- 任务 ID:`R221-3`
- Review Gate ID:`R221-2-GATE-20260703.0335`
- QA 结论:`PASS`
- 时间:`2026-07-03 05:10:25 +0800`
- 范围:验证恢复后的当前源码与 `2.2.2 build 20260703.0335` 候选构建路径。
- 不做范围:不修改源码、不创建 tag、不生成正式发布包、不把本地 QA evidence 当作发布候选。
QA 允许 PMO 解锁 `R221-4` 发布冻结流程。该结论不等于 Windows 正式版通过、不等于 Windows 真机 QA 通过、不等于 Monterey 兼容通过、不等于 notarized 官网正式发布通过。
## 环境
- 项目根:`/Users/ar/Projects/PrivateVoice`
- Git 根:`/Users/ar/Projects/PrivateVoice/03-O`
- 源码根:`/Users/ar/Projects/PrivateVoice/03-O/C1.source/privatevoice.src`
- 分支:`codex/x-asr-experiment-2.1.29`
- HEAD:`abb469859662c285fac758c042812e0da943728f`
- macOS:`26.1 (25B78)`
- Go:`go version go1.26.4 darwin/arm64`
- Node:`v23.11.1`
- npm:`10.9.2`
## 工作区核对
- `git status --short --branch`:当前分支 ahead 1,存在 R221 待审源码 / 文档 diff。
- `git diff --cached --name-status`:无输出,当前无 staged diff。
- `git diff --stat`:43 files changed,包含 R221 恢复源码、版本元数据、前端生命周期清理、Windows preview 脚本、CODEGRAPH / 管理文档等。
- 新增源码文件已在 diff 范围:`cmd/benchmark-recognition/main.go`、`scripts/benchmark-recognition.sh`、`services/diagnostics_service.go`。
## 标准源码门禁
| 项 | 命令 | 结果 | 备注 |
| --- | --- | --- | --- |
| 空白检查 | `git diff --check` | PASS | 无输出 |
| gofmt 状态 | `git diff --name-only -- C1.source/privatevoice.src | grep -E '\.go$' | xargs gofmt -l` | PASS | 无输出,表示当前变更 Go 文件格式干净 |
| Go 测试 | `go test ./...` | PASS with warning | 复现 macOS 链接 warning,未导致测试失败 |
| 前端构建 | `npm --prefix frontend run build` | PASS with warning | 复现既有 Svelte a11y warning:`UpdateDialog.svelte`、`ToggleSwitch.svelte` |
## 核心路径核对
| 路径 | Evidence | 结论 |
| --- | --- | --- |
| 核心语音输入 / 录音 / 识别流水线 | `go test ./...`;`go test ./internal/model ./internal/modelselection ./internal/language ./internal/engine ./internal/audio ./services`;`go test . -run 'TestSlowRecognitionReason|TestRecognize|TestHotkey|TestLiveCaption|TestWaitForHotkeyRelease|TestReload|TestOverlay|TestVersion'` | 自动化层 PASS;未在本机做真实麦克风、全局热键、粘贴上屏手动测试 |
| 设置 / 热键配置 | 前端 build PASS;diff 核对 `GeneralPage.svelte`、`HotkeysPage.svelte` 增加 `onDestroy` 和 `clearHotkeyCapture()` | 代码级生命周期清理路径通过;未做真实窗口交互测试 |
| 语言 / 模型选择 | `go test ./internal/model ./internal/modelselection ./internal/language ./internal/engine ./internal/audio ./services` PASS;`go test ./internal/model -run 'Test.*Yue|Test.*Cantonese|Test.*Language'` PASS | 自动化层覆盖语言 / 模型解析 |
| 粤语 `yue-HK` | `rg` 核对 `CantoneseLanguageID = "yue-HK"`、`SenseVoiceYueModelID = "sensevoice-yue-2025-09-09"`、`LanguageParam: "yue"`;`internal/engine` 测试 PASS | 粤语注册与 SenseVoice `yue` 参数路径通过 |
| 前端生命周期风险 | diff 核对 `App.svelte` 释放 Wails event off hooks 和 `hashchange`;`LanguagePage.svelte` 释放模型下载事件;`PermissionsPage.svelte` 释放 interval / timeout;`SettingsWindow.svelte` 释放 store subscription | 生命周期清理路径通过构建与代码核对;未做长时间打开 / 反复开关窗口的视觉 smoke |
## Windows Preview 适配回归
| 项 | 命令 / 证据 | 结果 |
| --- | --- | --- |
| Windows cross smoke | `GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go test -exec=/usr/bin/true ./...` | NOT COVERED:本机缺 `x86_64-w64-mingw32-gcc`,命令失败于工具链,不是源码测试失败 |
| 编译器检查 | `command -v x86_64-w64-mingw32-gcc` | 未找到 |
| Windows build 脚本语法 | `bash -n scripts/build-windows-preview.sh` | PASS |
| Windows evidence verifier 语法 | `bash -n scripts/verify-windows-qa-evidence.sh` | PASS |
| 版本 / build | `scripts/build-windows-preview.sh` 内 `APP_VERSION="2.2.2"`、`APP_BUILD="20260703.0335"` | PASS |
| preview 口径 | `README-WINDOWS-PREVIEW.txt` 模板声明 unsigned preview、不是安装器、需 Windows 10/11 QA | PASS |
| evidence 要求 | `verify-windows-qa-evidence.sh` 要求 `qa-results.tsv`、`app.log`、`qa-target-dictation-text.txt`、`system-info.txt`,成功时输出 `WINDOWS_QA_EVIDENCE_PASS` | PASS |
QA 判断:Windows preview 构建路径与 evidence 规则可进入 `R221-4` 发布冻结环节,但发布 / 环境窗口必须补齐 MinGW 工具链并实际生成 / 验证 Windows preview ZIP。不能把 Windows preview 说成正式 Windows 版。
## 版本与构建路径
- `app.go`:`appVersion = "2.2.2"`,`appBuild = "20260703.0335"`。
- `build/config.yml`:`version: "2.2.2"`。
- `build/darwin/Info.plist`:`CFBundleShortVersionString = 2.2.2`,`CFBundleVersion = 20260703.0335`,`LSMinimumSystemVersion = 14.0`。
- `frontend/package.json` / `package-lock.json`:`2.2.2`。
- `scripts/build-windows-preview.sh`:`2.2.2 / 20260703.0335`。
- `CHANGELOG.md`:已有 `v2.2.2 (2026-07-03)`,明确恢复 `2.2.1` 内容但正式候选升为 `2.2.2`。
- `bash -n scripts/build-local-macos.sh`、`bash -n scripts/build-local-macos-universal.sh`、`bash -n scripts/build-release-macos.sh`:PASS。
## macOS Warning / Monterey 判断
`go test ./...` 输出 macOS 链接 warning:
- Go / CLT 临时 object 标记为 macOS 26.0,高于链接目标 macOS 11.0。
- `@rpath/libonnxruntime.1.24.4.dylib` 最低 macOS 14.0。
QA 判断:
- 不阻塞当前主线 `2.2.2`,因为当前 mainline macOS 包口径为 `LSMinimumSystemVersion = 14.0`,且 Go 测试已通过。
- 不得据此声称 Monterey 兼容。若 Owner 需要覆盖 macOS 12 / Monterey,必须单独升级 Monterey QA / 构建任务,至少验证 `build-local-macos-monterey.sh`、对应依赖最低系统和真机安装启动。
## 未覆盖风险
- 未做真实麦克风录音、全局热键、Notepad / 输入框粘贴、overlay 视觉稳定性、菜单栏 / Dock 图标策略的手动 smoke。
- 未做 Windows 10/11 真机 QA,未收回 `WINDOWS_QA_EVIDENCE_PASS` evidence zip。
- 未跑 Windows cross smoke,原因是本机缺 `x86_64-w64-mingw32-gcc`。
- 未做 Monterey 验证。
- 未做 notarization、Developer ID 官网发布链路、下载页 / 授权 / 更新 / 回滚链路验证。
这些风险不阻塞 `R221-4` 进入发布冻结流程,但必须在对应发布 / 平台任务中继续关闭;其中 Windows ZIP 生成必须先补齐 MinGW 工具链。
K2.项目管理/handoffs/2026-07-02-迁移交接.md
New file
@@ -0,0 +1,48 @@
# 2026-07-02 迁移交接
## 来源
- 源项目:`/Users/ar/Projects/PrivateVoice`
- 当前项目:`/Users/ar/Projects/PrivateVoice`
- 历史命名:`PrivateVoice2` 是迁移期间旧命名,不再作为当前路径。
- 源分支:`codex/x-asr-experiment-2.1.29`
- 固定点:`0ffbf1935c9d091cce22a5583275ac0902f7a693`
- Tag:`v2.2.0-build20260702.0303-real-user-test`
## 当前目标
让新 Codex 窗口可以只读 `/Users/ar/Projects/PrivateVoice` 文件继续工作,不依赖旧项目长会话。
## 已完成
- 新项目结构 `01-I / 02-P / 03-O` 已建立。
- Git 历史 clone 到 `03-O`,源码迁入 `03-O/C1.source/privatevoice.src`。
- 最新 2.2.0 候选包迁入 `03-O/C2.builds`。
- 旧过程文档降级到 `03-O/K2.项目管理/legacy/`。
- 原始 session 正文未迁入;已整理主题摘要。
- 入口文档、台账、事件地图、成果地图、QA/发布地图和迁移清单已建立。
## 已知错误或未跑检查
- 本迁移不补跑 Windows 真机 QA。
- 本迁移不补跑 notarization。
- 本迁移不重新构建 2.2.0 包。
- Monterey 兼容未在迁移中重新验证。
## 不可触碰区域
- 不修改 `/Users/ar/Projects/PrivateVoice` 旧项目。
- 不删除源项目旧 release、证书、worktree 或 session。
- 不把 `legacy` 旧任务清单当作当前活跃任务。
## 下一步
1. 完成迁移接收验证并更新 `接收验证记录.md`。
2. 如要继续产品工作,先在 `02-P/进度地图.md` 领取或新增活跃任务。
3. 若目标是官网销售,先补独立站 P0 发布计划。
4. 若目标是 Windows 正式版,先补签名、安装器和真机 QA evidence。
5. 若目标是粤语真实用户测试,先确认测试人、平台、反馈渠道和包分发口径。
## Reactivation Prompt
我们从这份 handoff 继续。请先读取本文件,再检查 `/Users/ar/Projects/PrivateVoice/AGENTS.md`、`PROJECT.md`、`INDEX.md`、`02-P/进度地图.md`、`03-O/CODEGRAPH.md` 和当前 Git 状态,核对哪些结论仍然成立,然后从“下一步”继续推进;不要假设旧 session 上下文仍然可用。
K2.项目管理/legacy/02-P-NBL/engineering-plan.md
K2.项目管理/legacy/02-P-NBL/freeze/20260603-directory-consolidation.md
K2.项目管理/legacy/02-P-NBL/freeze/20260603-independent-repo-and-archive.md
K2.项目管理/legacy/02-P-NBL/freeze/20260604-mac-app-store-category-hotfix.md
K2.项目管理/legacy/02-P-NBL/freeze/20260604-mac-app-store-release-freeze.md
K2.项目管理/legacy/02-P-NBL/freeze/20260605-mac-app-store-application-identifier-hotfix.md
K2.项目管理/legacy/02-P-NBL/freeze/20260605-mac-app-store-clean-rebuild.md
K2.项目管理/legacy/02-P-NBL/freeze/20260605-mac-app-store-quarantine-hotfix.md
K2.项目管理/legacy/02-P-NBL/freeze/20260605-mac-app-store-upload-preflight.md
K2.项目管理/legacy/02-P-NBL/freeze/20260607-x-asr-experiment-branch.md
K2.项目管理/legacy/02-P-NBL/freeze/20260607-x-asr-experimental-build.md
K2.项目管理/legacy/02-P-NBL/plan.md
K2.项目管理/legacy/02-P-NBL/product-plan.md
K2.项目管理/legacy/02-P-NBL/round-1/README.md
K2.项目管理/legacy/02-P-NBL/round-1/dev-checklist-current.md
K2.项目管理/legacy/02-P-NBL/round-1/dev-checklist.md
K2.项目管理/legacy/02-P-NBL/round-1/engineering-tasks.md
K2.项目管理/legacy/02-P-NBL/round-1/logs/go-test-all-20260602.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/go-test-internal-model-20260602.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/npm-run-build-20260602.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/app-log-tail-20260602.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/network-sample-20260602.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-clean-install-20260602.231851.launch.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-clean-install-20260602.231851.recognition.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-clean-install-20260602.231851.restore.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-clean-install-20260602.231851.setup.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-recognition-20260603.005122.launch.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-recognition-20260603.005122.recognition.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-recognition-20260603.005122.restore.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/new-user-recognition-20260603.005122.setup.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/offline-restart-app-log-20260602.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/offline-restart-postcheck-20260602.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/offline-restart-test-20260602.223742.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/offline-restart-test-20260602.225709.log
K2.项目管理/legacy/02-P-NBL/round-1/logs/qa-runtime/offline-restart-test-20260602.225709.pid
K2.项目管理/legacy/02-P-NBL/round-1/prd.md
K2.项目管理/legacy/02-P-NBL/round-1/qa-handoff-current.md
K2.项目管理/legacy/02-P-NBL/round-1/qa-precheck-current.md
K2.项目管理/legacy/02-P-NBL/round-1/qa-report-20260602-2.1.19.md
K2.项目管理/legacy/02-P-NBL/round-1/qa-report-template.md
K2.项目管理/legacy/02-P-NBL/round-1/qa-test-plan.md
K2.项目管理/legacy/02-P-NBL/round-1/todo-list.md
K2.项目管理/legacy/02-P-NBL/round-2/README.md
K2.项目管理/legacy/02-P-NBL/round-2/dev-checklist-current.md
K2.项目管理/legacy/02-P-NBL/round-2/engineering-tasks.md
K2.项目管理/legacy/02-P-NBL/round-2/logs/go-test-all-20260603-2.1.20.log
K2.项目管理/legacy/02-P-NBL/round-2/logs/go-test-all-20260603.log
K2.项目管理/legacy/02-P-NBL/round-2/logs/npm-run-build-20260603-2.1.20.log
K2.项目管理/legacy/02-P-NBL/round-2/logs/npm-run-build-20260603.log
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/build-local-round2-20260603.014931.log
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/current-run-id.txt
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/new-user-run-20260603.014931.log
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/new-user-run-20260603.014931.pid
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/offline-start-check-20260603.014931.log
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/offline-voice-final-20260603.014931.log
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/offline-voice-safety-20260603.014931.log
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/offline-voice-safety-20260603.014931.pid
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/offline-voice-start-20260603.014931.log
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-local-run-20260603.014931.log
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-local-run-20260603.014931.pid
K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-activated.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-after-restart.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-chinese.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-cursor-1095-523.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-cursor-1260-565.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-cursor-693-310.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-english.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-english2.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-english3.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-english4.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-english5.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-english6.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-initial.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-new-user.png

K2.项目管理/legacy/02-P-NBL/round-2/logs/qa-runtime/round2-screen-20260603.014931-zh-manual.png

K2.项目管理/legacy/02-P-NBL/round-2/prd.md
K2.项目管理/legacy/02-P-NBL/round-2/qa-handoff-current.md
K2.项目管理/legacy/02-P-NBL/round-2/qa-precheck-current.md
K2.项目管理/legacy/02-P-NBL/round-2/qa-report-current.md
K2.项目管理/legacy/02-P-NBL/round-2/qa-test-plan.md
K2.项目管理/legacy/02-P-NBL/round-2/todo-list.md
K2.项目管理/legacy/02-P-NBL/round-3/README.md
K2.项目管理/legacy/02-P-NBL/round-3/code-review-current.md
K2.项目管理/legacy/02-P-NBL/round-3/dev-checklist-current.md
K2.项目管理/legacy/02-P-NBL/round-3/engineering-tasks.md
K2.项目管理/legacy/02-P-NBL/round-3/logs/download-failure-protection-20260603.log
K2.项目管理/legacy/02-P-NBL/round-3/logs/git-diff-check-20260603-2.1.21.log
K2.项目管理/legacy/02-P-NBL/round-3/logs/git-diff-check-20260603.log
K2.项目管理/legacy/02-P-NBL/round-3/logs/go-test-all-20260603-2.1.21.log
K2.项目管理/legacy/02-P-NBL/round-3/logs/go-test-all-20260603.log
K2.项目管理/legacy/02-P-NBL/round-3/logs/npm-run-build-20260603-2.1.21.log
K2.项目管理/legacy/02-P-NBL/round-3/logs/npm-run-build-20260603.log
K2.项目管理/legacy/02-P-NBL/round-3/prd.md
K2.项目管理/legacy/02-P-NBL/round-3/qa-handoff-current.md
K2.项目管理/legacy/02-P-NBL/round-3/qa-precheck-current.md
K2.项目管理/legacy/02-P-NBL/round-3/qa-report-current.md
K2.项目管理/legacy/02-P-NBL/round-3/qa-test-plan.md
K2.项目管理/legacy/02-P-NBL/round-3/todo-list.md
K2.项目管理/legacy/02-P-NBL/round-4/README.md
K2.项目管理/legacy/02-P-NBL/round-4/code-review.md
K2.项目管理/legacy/02-P-NBL/round-4/dev-self-check.md
K2.项目管理/legacy/02-P-NBL/round-4/engineering-tasks.md
K2.项目管理/legacy/02-P-NBL/round-4/logs/git-diff-check-20260603-2.1.22.log
K2.项目管理/legacy/02-P-NBL/round-4/logs/git-diff-check-20260603-round4-after-qa-fix.log
K2.项目管理/legacy/02-P-NBL/round-4/logs/git-diff-check-20260603-round4.log
K2.项目管理/legacy/02-P-NBL/round-4/logs/go-test-all-20260603-2.1.22.log
K2.项目管理/legacy/02-P-NBL/round-4/logs/go-test-all-20260603-round4-after-qa-fix.log
K2.项目管理/legacy/02-P-NBL/round-4/logs/go-test-all-20260603-round4.log
K2.项目管理/legacy/02-P-NBL/round-4/logs/local-build-install-20260603-round4-after-qa-fix.log
K2.项目管理/legacy/02-P-NBL/round-4/logs/local-build-install-20260603-round4.log
K2.项目管理/legacy/02-P-NBL/round-4/logs/npm-ci-20260603-round4.log
K2.项目管理/legacy/02-P-NBL/round-4/logs/npm-run-build-20260603-2.1.22.log
K2.项目管理/legacy/02-P-NBL/round-4/logs/npm-run-build-20260603-round4-after-qa-fix.log
K2.项目管理/legacy/02-P-NBL/round-4/logs/npm-run-build-20260603-round4.log
K2.项目管理/legacy/02-P-NBL/round-4/prd.md
K2.项目管理/legacy/02-P-NBL/round-4/qa-handoff.md
K2.项目管理/legacy/02-P-NBL/round-4/qa-report.md
K2.项目管理/legacy/02-P-NBL/round-4/qa-test-plan.md
K2.项目管理/legacy/02-P-NBL/round-4/todo-list.md
K2.项目管理/legacy/02-P-NBL/round-5/README.md
K2.项目管理/legacy/02-P-NBL/round-5/dev-check-report.md
K2.项目管理/legacy/02-P-NBL/round-5/engineering-tasks.md
K2.项目管理/legacy/02-P-NBL/round-5/prd.md
K2.项目管理/legacy/02-P-NBL/round-5/qa-handoff.md
K2.项目管理/legacy/02-P-NBL/round-5/qa-report.md
K2.项目管理/legacy/02-P-NBL/round-5/qa-test-plan.md
K2.项目管理/legacy/02-P-NBL/round-5/todo-list.md
K2.项目管理/legacy/02-P-NBL/round-6/README.md
K2.项目管理/legacy/02-P-NBL/round-6/requirements-dev.md
K2.项目管理/legacy/02-P-NBL/round-6/todo-dev.md
K2.项目管理/legacy/02-P-NBL/x-asr-realtime-insertion-report.md
K2.项目管理/legacy/Docs/KM2.项目管理-非业务直接相关/AppStore审核拒绝-20260610.md
K2.项目管理/legacy/Docs/KM2.项目管理-非业务直接相关/AppStore打包连续返工经验教训-20260611.md
K2.项目管理/legacy/Docs/KM2.项目管理-非业务直接相关/QA工作-经验与教训.md
K2.项目管理/legacy/TODO-旧任务清单.md
K2.项目管理/moved-assets.jsonl
New file
@@ -0,0 +1,5 @@
{"time":"2026-07-02","scope":"new-project","source":"/Users/ar/Projects/PrivateVoice2/03-O/privatevoice.src","target":"/Users/ar/Projects/PrivateVoice2/03-O/C1.source/privatevoice.src","reason":"set source root to SOP C1.source path; source project was not modified"}
{"time":"2026-07-02","scope":"new-project","source":"/Users/ar/Projects/PrivateVoice2/03-O/02-P-NBL","target":"/Users/ar/Projects/PrivateVoice2/03-O/K2.项目管理/legacy/02-P-NBL","reason":"downgrade old process documents to historical evidence; source project was not modified"}
{"time":"2026-07-02","scope":"new-project","source":"/Users/ar/Projects/PrivateVoice2/03-O/Docs","target":"/Users/ar/Projects/PrivateVoice2/03-O/K2.项目管理/legacy/Docs","reason":"place old project management docs under legacy evidence; source project was not modified"}
{"time":"2026-07-02","scope":"new-project","source":"/Users/ar/Projects/PrivateVoice2/03-O/TODO.md","target":"/Users/ar/Projects/PrivateVoice2/03-O/K2.项目管理/legacy/TODO-旧任务清单.md","reason":"old TODO is not the active ledger; source project was not modified"}
{"time":"2026-07-02","scope":"new-project","source":"/Users/ar/Projects/PrivateVoice2/03-O/screenshot.png","target":"/Users/ar/Projects/PrivateVoice2/03-O/K3.运营与发布资料/screenshot.png","reason":"place product screenshot under operation/release materials; source project was not modified"}
K2.项目管理/restore-notes.md
New file
@@ -0,0 +1,22 @@
# Restore Notes
## 源项目恢复
本次迁移不再使用 `PrivateVoice2` 作为当前路径。当前项目根为 `/Users/ar/Projects/PrivateVoice`;旧项目材料已作为历史证据保留在本项目结构和 legacy 区内。
## 新项目结构性移动
新项目内发生的结构性移动记录在 `moved-assets.jsonl`。如果要恢复旧仓库布局,可在 `03-O` 中用 Git 历史查看迁移前路径:
```bash
cd /Users/ar/Projects/PrivateVoice/03-O
git log --oneline --follow -- C1.source/privatevoice.src/main.go
```
## 本地大文件
`C2.builds` 和 `K3.运营与发布资料/legacy-运营资料` 中的本地二进制/素材文件默认不提交 Git。若丢失,可从源项目重新复制:
```bash
rsync -a /Users/ar/Projects/PrivateVoice/Release/PrivateVoice-Dictation-2.2.0-build20260702.0303/ /Users/ar/Projects/PrivateVoice/03-O/C2.builds/PrivateVoice-Dictation-2.2.0-build20260702.0303/
```
K2.项目管理/session-topics/00-Topics.md
New file
@@ -0,0 +1,15 @@
# 会话主题索引
本目录是旧项目瘦身迁移时生成的压缩主题摘要 / 接手知识包,不是 PrivateVoice 的常规会话主题汇编目录。
旧项目原始 session 正文不迁入新项目正文。本目录只保留对接手仍有价值的主题摘要和来源索引。PrivateVoice 未来常规会话主题汇编必须写入 `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/00-Topics.md` 和 `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/_topics/`。
| 主题 | 文档 | 时间范围 | 说明 |
| --- | --- | --- | --- |
| 多语种与粤语 | `多语种与粤语.md` | 2026-05-29 至 2026-07-02 | 模型路线、Round 6 粤语、语言切换安全 |
| Windows 预览 | `Windows预览.md` | 2026-06-30 至 2026-07-02 | Windows preview 合并、限制、包和 QA 缺口 |
| 发布与打包 | `发布与打包.md` | 2026-06-04 至 2026-07-02 | App Store、本地包、2.2.0 四包候选 |
| App Store 与独立站 | `AppStore与独立站.md` | 2026-06-10 至 2026-07-02 | 渠道转向和独立站缺口 |
| 性能与稳定性 | `性能与稳定性.md` | 2026-06-28 至 2026-07-01 | 慢识别、尾音、下载/取消/首尾字修复 |
原始记录仍保留在源项目 `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/`。
K2.项目管理/session-topics/AppStore与独立站.md
New file
@@ -0,0 +1,28 @@
# App Store 与独立站
## 当前状态
项目曾围绕 Mac App Store 审核、pkg、合规和启动问题推进。2026-07-02 的结论是:主发布出口转为独立站销售。
## 当前结论
- App Store 特有链路不再是当前主线门禁。
- 建议采用“功能冻结,不做二进制绝对冻结”的发布策略。
- 首发建议先做 macOS 官网直发,不同步追求完整账号体系和复杂自动更新。
## P0 缺口
1. 固定官网发布基线。
2. Developer ID signed + notarized DMG。
3. 下载页、版本页、SHA256 和 manifest。
4. 最小授权 / 激活 / 交付闭环。
5. 更新策略:首发是否关闭自动更新,或跳官网。
6. 支持、诊断导出和退款/订单查询路径。
## 来源索引
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-06-10-审核拒绝排查.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-06-10-审核处理方案.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-06-13-AppStore三审驳回.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-06-23-AppStore四审会诊.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-07-02-独立站发布清单.md`
K2.项目管理/session-topics/Windows预览.md
New file
@@ -0,0 +1,24 @@
# Windows 预览
## 当前状态
Windows preview 已合入当前分支,并包含在 2.2.0 四包候选中。它仍是 unsigned preview ZIP,不是正式 Windows 安装器。
## 核心事实
- Windows preview 当前只支持 SenseVoice。
- 非 SenseVoice 模型在设置中可见但禁用。
- Windows overlay 使用原生 Win32/GDI+ 胶囊指示器。
- QA 脚本在包内:`RUN-WINDOWS-QA.cmd`、`QA-WINDOWS-PREVIEW.ps1`。
- 收回 evidence 后用 `scripts/verify-windows-qa-evidence.sh` 验证。
## 缺口
- 需要真实 Windows 10/11 硬件 QA。
- 需要签名、安装器和 SmartScreen 体验处理,才能推进正式 PC 版本。
## 来源索引
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-06-30-Windows预览版.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-07-01-Windows合并.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-07-01-Windows动效打包.md`
K2.项目管理/session-topics/发布与打包.md
New file
@@ -0,0 +1,32 @@
# 发布与打包
## 当前最新包
- Version:`2.2.0`
- Build:`20260702.0303`
- Commit:`0ffbf1935c9d091cce22a5583275ac0902f7a693`
- Tag:`v2.2.0-build20260702.0303-real-user-test`
- 类型:真实用户测试候选包。
## 包组成
- macOS arm64 DMG
- macOS x86_64 DMG
- macOS universal DMG
- Windows amd64 preview ZIP
## 验证摘要
源会话记录表明以下检查通过:`go test ./... -count=1`、`npm --prefix frontend run build`、Windows cross smoke、`git diff --check`、DMG verify、ZIP test、SHA256、架构和 codesign。
## 边界
- macOS 包未 notarize。
- Windows 包未签名。
- Monterey 兼容未在 2.2.0 四包中重新验证。
## 来源索引
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-07-02-多平台打包整理.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-07-02-粤语四包交付.md`
- `/Users/ar/Projects/PrivateVoice/Release/PrivateVoice-Dictation-2.2.0-build20260702.0303/RELEASE_MANIFEST.md`
K2.项目管理/session-topics/多语种与粤语.md
New file
@@ -0,0 +1,23 @@
# 多语种与粤语
## 当前状态
PrivateVoice 已从中文/English 双语言扩展到多语种 profile。Round 6 已新增独立粤语语言版本。
## 最近结论
- `yue-HK` 是独立语言 profile,显示为 `粤语`。
- 默认模型为 `sensevoice-yue-2025-09-09` / `SenseVoice Yue`。
- SenseVoice Yue engine 参数固定为 `LanguageParam="yue"`。
- 粤语 profile 不默认展示 Qwen3-ASR / X-ASR 作为粤语专项模型。
- `zh-HK` 不自动映射到粤语,避免香港用户被误切到粤语。
- 未安装目标语言只做 UI 预览;下载成功后才持久化语言并 reload engine。
## 来源索引
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-05-29-关于FunASR等模型的技术选型.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-06-22-多语种扩展.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-06-22-README语言表.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-07-02-粤语版本需求.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-07-02-粤语开发实现.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-07-02-粤语四包交付.md`
K2.项目管理/session-topics/性能与稳定性.md
New file
@@ -0,0 +1,23 @@
# 性能与稳定性
## 当前状态
2026-07-01 慢识别复盘显示,用户感知慢主要来自 `recognize_ms` 抖动和系统负载峰值,不是上屏、历史写入或 engine lock。
## 已落地
- 新增 `PERF slow_recognition` 日志。
- SenseVoice 尾音保护为 100ms。
- X-ASR 尾音保护为 300ms。
- 其他 sherpa 离线模型不再默认加尾音保护。
## 后续观察
- 系统负载异常时需要区分 Codex/Spotlight/WindowServer 等外部负载影响。
- 如继续优化,先补更细日志和真实用户样本,不直接盲调模型线程或 provider。
## 来源索引
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-06-28-首尾字修复.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-06-29-性能日志.md`
- `/Users/ar/Projects/PrivateVoice/.codex-sessionhistory/2026-07-01-识别变慢复盘.md`
K2.项目管理/发布地图.md
New file
@@ -0,0 +1,43 @@
# 发布地图
## 当前最新候选包
- Version:`2.2.2`
- Build:`20260703.0335`
- Source freeze commit:`5fbe9789faf05ce99bbff6ec4cd0b905519acbd0`
- Package build commit:`72b84399db9001a3b26ddad00c307438d940d8ee`
- Release tag commit:`a075aec123e1f7118f138738f196c2995aa96f98`
- Tag:`v2.2.2-build20260703.0335-real-user-test`
- 类型:真实用户测试候选包
- 路径:`03-O/C2.builds/PrivateVoice-Dictation-2.2.2-build20260703.0335/`
## 包列表
- `PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-arm64.dmg`
- `PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-x86_64.dmg`
- `PrivateVoice-Dictation-2.2.2-build20260703.0335-macos-universal.dmg`
- `PrivateVoice-Dictation-2.2.2-build20260703.0335-windows-amd64-preview.zip`
- `RELEASE_MANIFEST.md`
- `SHA256SUMS.txt`
## 当前边界
- macOS 包是本地签名 DMG,未 notarize。
- Windows 包是 unsigned preview ZIP,不是安装器,不是稳定 Windows 正式版。
- 该包可用于真实用户测试,不等同正式官网发布。
- Monterey 兼容未在本包集验证;如需覆盖 macOS 12,需单独任务。
## 本轮验证记录
- R221-2 代码审核:`R221-2-GATE-20260703.0335` PASS。
- R221-3 QA:`03-O/K2.项目管理/R221-3-QA-evidence.md` PASS。
- R221-4 发布冻结:四包生成、SHA256、DMG 完整性、Windows ZIP 解压测试、CODEGRAPH 检查、codebase-memory 刷新和 tag 创建已完成;详见当前包目录 `RELEASE_MANIFEST.md`。
## 官网独立站 P0 缺口
1. 明确官网首发固定 commit/build。
2. 生成 Developer ID signed + notarized DMG。
3. 建立下载页、版本页、SHA256、release manifest。
4. 决定授权/试用/激活/换机/重装最小方案。
5. 决定首发是否关闭应用内自动更新;如关闭,检查更新应跳官网。
6. 定义支持、诊断导出、退款/订单查询最小流程。
K2.项目管理/接收验证记录.md
New file
@@ -0,0 +1,28 @@
# 接收验证记录
## 验证目标
验证当前 `PrivateVoice` 迁移包是否可接手。该结论不等同产品 QA PASS、正式发布 PASS 或官网销售链路 PASS。
## 结果
| 项 | 命令或检查 | 结果 | 备注 |
| --- | --- | --- | --- |
| Git 状态 | `git status --short --branch` | PASS | 显示预期迁移变更;提交后需再看最终 clean 状态 |
| Diff 空白检查 | `git diff --check` | PASS | 无输出 |
| 依赖安装 | `npm --prefix frontend ci` | PASS with warning | 安装 48 packages;仍有既有 `6 vulnerabilities (3 moderate, 3 high)` |
| 前端 build | `npm --prefix frontend run build` | PASS with warning | 生成 `frontend/dist`;仍有既有 Svelte a11y warning |
| Go 测试 | `go test ./...` | PASS with warning | 通过;仍有既有 macOS linker warning |
| Windows cross smoke | `GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go test -exec=/usr/bin/true ./...` | PASS | 通过 |
| SHA256 | `shasum -a 256 -c SHA256SUMS.txt` | PASS | 四个候选包均 OK |
| macOS DMG 完整性 | `hdiutil verify ...arm64.dmg` | PASS | VALID |
| macOS DMG 完整性 | `hdiutil verify ...x86_64.dmg` | PASS | VALID |
| macOS DMG 完整性 | `hdiutil verify ...universal.dmg` | PASS | VALID |
| Windows ZIP 完整性 | `unzip -t ...windows-amd64-preview.zip` | PASS | No errors detected |
## 未跑项默认解释
- Windows 真机 QA 不属于本次迁移接收验证。
- Notarization 不属于本次迁移接收验证。
- 重新打包不属于本次迁移接收验证。
- 本次迁移验证不处理 `npm audit` 中既有漏洞提示;如要处理,需单独进入依赖安全任务。
K2.项目管理/新窗口接手指南.md
New file
@@ -0,0 +1,52 @@
# 新窗口接手指南
## 接手目标
从 `/Users/ar/Projects/PrivateVoice` 文件系统接手项目,不依赖旧长会话上下文。`PrivateVoice2` 只是迁移期间旧命名,不再作为当前路径。
## 必读顺序
1. `/Users/ar/Projects/PrivateVoice/AGENTS.md`
2. `/Users/ar/Projects/PrivateVoice/PROJECT.md`
3. `/Users/ar/Projects/PrivateVoice/INDEX.md`
4. `/Users/ar/Projects/PrivateVoice/02-P/进度地图.md`
5. `/Users/ar/Projects/PrivateVoice/02-P/事件地图.md`
6. `/Users/ar/Projects/PrivateVoice/03-O/CODEGRAPH.md`
7. `/Users/ar/Projects/PrivateVoice/03-O/K2.项目管理/迁移资产清单.md`
## 当前事实快照
- 当前工作不是继续旧会话,而是从新项目文件接手。
- 源码根:`03-O/C1.source/privatevoice.src`
- 当前基线:`v2.2.0-build20260702.0303-real-user-test`
- Round 6 粤语实现和四包候选已完成。
- Windows preview 与官网正式发布仍有缺口。
## 接手自检命令
```bash
cd /Users/ar/Projects/PrivateVoice/03-O
git status --short --branch
git diff --check
cd C1.source/privatevoice.src
npm --prefix frontend run build
go test ./...
```
## 不迁移内容
- 原始长会话正文。
- 旧 worktree。
- 旧构建包全集。
- 证书目录。
- node_modules、dist、构建缓存和用户级缓存。
## 成功判定
新窗口只读新项目文件后,应能说明:
- 项目是什么。
- 当前做到哪里。
- Git 根、源码根、QA 入口和发布入口在哪里。
- 下一步需要谁决策。
- 哪些材料是 legacy,不可直接作为当前任务来源。
K2.项目管理/迁移资产清单.md
New file
@@ -0,0 +1,45 @@
# 迁移资产清单
## 来源与目标
- 来源项目:旧 PrivateVoice 项目材料
- 当前项目:`/Users/ar/Projects/PrivateVoice`
- 历史命名:`PrivateVoice2` 是迁移期间旧命名,不再作为当前路径。
- 迁移时间:2026-07-02
- 基线:`0ffbf1935c9d091cce22a5583275ac0902f7a693` / `v2.2.0-build20260702.0303-real-user-test`
## 已迁入
| 来源 | 目标 | 说明 |
| --- | --- | --- |
| 旧 Git 仓库 | `03-O` | 使用 `git clone --no-hardlinks` 保留历史 |
| `privatevoice.src/` | `03-O/C1.source/privatevoice.src/` | 源码根迁入固定 C1.source |
| `02-P-NBL/` | `03-O/K2.项目管理/legacy/02-P-NBL/` | 旧 Round/计划/QA 证据,降级为历史 |
| `Docs/` | `03-O/K2.项目管理/legacy/Docs/` | 旧项目管理经验文档 |
| `TODO.md` | `03-O/K2.项目管理/legacy/TODO-旧任务清单.md` | 旧 TODO,只作历史 |
| `Release/PrivateVoice-Dictation-2.2.0-build20260702.0303/` | `03-O/C2.builds/PrivateVoice-Dictation-2.2.0-build20260702.0303/` | 最新真实用户测试候选包 |
| `X-运营资料/PrivateVoice-26.0604上架` 等 | `03-O/K3.运营与发布资料/legacy-运营资料/` | 本地运营素材,默认不提交 Git |
| `.codex-sessionhistory/` 关键信息 | `03-O/K2.项目管理/session-topics/`、`handoffs/` | 只迁移压缩摘要和交接,不迁原始正文 |
## 明确排除
- 旧项目 `.git/worktrees/` 与 `.worktrees/`。
- 旧项目 `.codex/` 与原始长会话正文。
- `privatevoice.src/frontend/node_modules/`。
- `privatevoice.src/frontend/dist/`。
- `privatevoice.src/build/local/`、`build/macappstore*`、`build/windows-preview/` 等本地构建输出。
- `Release/X-旧版build成果/` 历史包。
- `Release/01-证书/` 证书目录。
- 用户级缓存、系统噪声和 `.DS_Store`。
## Git 策略
- `03-O` 是新项目 Git 根。
- 最新包和运营素材已在本地目录保留,但大二进制文件默认由 `.gitignore` 排除,不进入迁移提交。
- `RELEASE_MANIFEST.md`、`SHA256SUMS.txt`、地图和清单可以提交。
## 风险说明
- 新路径把源码移动到 `C1.source/privatevoice.src`;已有脚本通常使用相对路径,后续执行命令需从新源码根进入。
- 历史文档里的旧路径保留为证据,不机械替换。
- 本次迁移不生成正式 release,不补跑 Windows 真机 QA,不补 notarization。
K3.运营与发布资料/README.md
New file
@@ -0,0 +1,12 @@
# 运营与发布资料
## 当前资料
- `screenshot.png`:产品截图。
- `legacy-运营资料/`:从旧项目选择性复制的运营素材和历史上架资料,本地保留,默认不提交 Git。
## 规则
- 运营素材不能覆盖 `PROJECT.md`、`发布地图.md` 或 release manifest 的当前发布事实。
- 名称带 `X-`、`旧`、`归档` 的资料只作历史参考。
- 如要对外发布官网内容,必须先在 `02-P/进度地图.md` 建立活跃任务并明确发布基线。
K3.运营与发布资料/legacy-运营资料/README.md
New file
@@ -0,0 +1,10 @@
# legacy 运营资料
本目录为旧项目运营资料的本地副本,来源:
- `/Users/ar/Projects/PrivateVoice/X-运营资料/PrivateVoice-26.0604上架`
- `/Users/ar/Projects/PrivateVoice/X-运营资料/运营文档`
- `/Users/ar/Projects/PrivateVoice/X-运营资料/OP资料-Privatevoice.MD`
- `/Users/ar/Projects/PrivateVoice/X-运营资料/26.0602-备忘.markdown`
这些材料用于追溯历史上架、截图、文案和运营准备,不是当前发布事实源。
K3.运营与发布资料/screenshot.png

README.md
old mode 100755 new mode 100644
@@ -1,165 +1,58 @@
# PrivateVoice
> 长按说话,松手即输 —— 离线 · 极速 · 跨平台
PrivateVoice 是一款离线语音转文字桌面 App:按住快捷键说话,松开后本地识别并输入到当前应用。当前源码位于 `C1.source/privatevoice.src`。
![PrivateVoice](screenshot.png)
![PrivateVoice](K3.运营与发布资料/screenshot.png)
PrivateVoice 是一款离线语音转文字工具。按住快捷键说话,松开即识别并输入文字到任意应用。无需联网,无需注册,开箱即用。
## 当前迁移基线
## 核心特性
- 分支:`codex/x-asr-experiment-2.1.29`
- Commit:`0ffbf1935c9d091cce22a5583275ac0902f7a693`
- Tag:`v2.2.0-build20260702.0303-real-user-test`
- 包类型:真实用户测试候选包,不是 notarized 官网正式发布包。
- 迁移后源码根:`C1.source/privatevoice.src`
- 最新候选包本地路径:`C2.builds/PrivateVoice-Dictation-2.2.0-build20260702.0303/`
- **完全离线** — SenseVoice 模型本地推理,无数据上传
- **DirectML GPU 加速** — 自动检测显卡,CPU 自动回退
- **两种输入模式** — 长按说话 + 短按自由说话
- **自定义快捷键** — 支持 Ctrl / Alt / Shift / 功能键 / 字母键
- **麦克风选择** — 下拉切换输入设备
- **录音音效** — 马林巴风格提示音(可关闭)
- **录音计时** — 指示器实时显示录音时长(0:00 → 0:03 → ...)
- **Escape 取消** — 录音中按 Esc 立即取消
- **剪贴板保护** — 粘贴后自动恢复原剪贴板内容
- **识别历史** — 自动保存最近 50 条,可复制、删除、设置保留时长
- **语气词过滤** — 自动去除"嗯、啊、呃"及 um/uh/hmm 等填充词
- **静音自动停止** — 自由说话模式下连续 3 秒无声自动结束并识别
- **中英混合优化** — 自动在中英文之间加空格,句首大写
- **DPI 自适应** — 指示器根据系统缩放自动调整大小
- **指示器可拖拽** — 拖动浮动指示器到任意位置,自动记忆
- **中英双语界面** — 自动跟随系统语言
- **应用内更新** — 自动检查新版本并提示下载
- **系统托盘常驻** — 关闭窗口不退出
- **开机自启** — 可选
## 核心能力
## 支持的输入语言
- 完全离线识别,模型本地推理。
- 长按说话与短按自由说话两种输入模式。
- 自定义快捷键、麦克风选择、录音音效、录音计时、Esc 取消。
- 剪贴板保护、识别历史、用户词库、语气词过滤和中英混合文本后处理。
- macOS 本地签名 DMG、Windows amd64 preview ZIP。
- 当前 Round 6 已新增独立 `粤语` 语言选项,默认模型为 `SenseVoice Yue 2025-09-09`。
当前 PrivateVoice 的产品语言版本为 **中文**、**English**、**French**、**German**、**Spanish**、**Italian**、**Portuguese**、**Japanese** 和 **Korean**。识别模型已经扩展到 SenseVoice、Moonshine English、Parakeet English、Parakeet TDT v3、日语 Zipformer、韩语 Zipformer、Qwen3-ASR 和 X-ASR zh-en 960ms;不同模型覆盖的输入语言和成熟度不同。
## 语言与模型状态
| 输入语言 / 场景 | 当前支持状态 | 默认 / 可选模型 | 说明 |
|---|---|---|---|
| 中文(普通话、简体 / 繁体) | 正式支持 | 默认:SenseVoice;可选高级:Qwen3-ASR;实验:X-ASR zh-en 960ms | 中文语言版本默认使用 SenseVoice,适合中文听写和日常输入。 |
| English | 正式支持 | 默认:Moonshine English;可选高级:Parakeet English;实验:X-ASR zh-en 960ms | English 语言版本默认使用 Moonshine English,Parakeet 适合更高准确率的英文输入。 |
| French / German / Spanish / Italian / Portuguese | 正式支持 | 默认:Parakeet TDT v3 | 欧洲五语统一使用 NVIDIA / sherpa-onnx 的 Parakeet TDT v3 多语种离线模型。Portuguese 覆盖 `pt-PT` 和 `pt-BR` 的基础识别路径。 |
| 中英混输 | 正式支持场景 | SenseVoice、Qwen3-ASR、X-ASR zh-en 960ms | 适合中文句子中夹杂英文术语、产品名、代码词等场景。 |
| 粤语(Cantonese) | 模型支持,尚非独立语言版本 | SenseVoice、Qwen3-ASR、X-ASR zh-en 960ms | 底层模型包含粤语能力;当前建议在中文语言版本下使用。 |
| 日语(日本語) | 正式支持 | 默认:Zipformer Japanese | 日语语言版本默认使用 ReazonSpeech 日语专项 Zipformer 离线模型。 |
| 韩语(한국어) | 正式支持 | 默认:Zipformer Korean | 韩语语言版本默认使用韩语专项 Zipformer 离线模型。 |
| 欧洲其他语种 | 模型支持,尚非独立语言版本 | Parakeet TDT v3 | Parakeet TDT v3 支持 25 种欧洲语言;当前只把 French、German、Spanish、Italian、Portuguese 作为正式产品语言版本。 |
| 其他语言 | 暂未正式支持 | 无独立默认模型 | 系统语言不在当前正式语言范围内时,当前产品会回落到 English 语言版本。 |
| --- | --- | --- | --- |
| 中文 | 正式支持 | 默认 SenseVoice;可选 Qwen3-ASR;实验 X-ASR zh-en 960ms | 中文听写和中英混输主路径。 |
| 粤语 | 真实用户测试候选 | 默认 SenseVoice Yue 2025-09-09 | 独立语言 profile:`yue-HK`,engine 参数固定 `yue`。 |
| English | 正式支持 | 默认 Moonshine English;可选 Parakeet English;实验 X-ASR zh-en 960ms | 英文听写主路径。 |
| French / German / Spanish / Italian / Portuguese | 正式支持 | Parakeet TDT v3 | 欧洲五语离线识别。 |
| Japanese | 正式支持 | Zipformer Japanese | 日语专项模型。 |
| Korean | 正式支持 | Zipformer Korean | 韩语专项模型。 |
| Windows preview | 技术预览 | SenseVoice only | 非 SenseVoice 模型可见但禁用;Windows 包未签名。 |
| 识别模型 | 模型状态 | 覆盖的输入语言 / 场景 | 所属语言版本 | 说明 |
|---|---|---|---|---|
| SenseVoice | 默认轻量模型 | 中文、粤语、English、日语、韩语 | 中文默认 | 轻量模型,当前推荐用于中文和中英混输,也提供日语、韩语等底层识别能力。 |
| Qwen3-ASR | 高级模型 | 中文、简繁中文、粤语、中英混输 | 中文可选 | 体积更大,适合中文长句、技术词和更高准确率需求。 |
| Moonshine English | 默认轻量模型 | English | English 默认 | 轻量英文离线模型,适合英文日常听写。 |
| Parakeet English | 高级模型 | English | English 可选 | 当前接入的是 `parakeet-unified-en-0.6b` 英文模型,适合长句、技术词和更高准确率需求。 |
| Parakeet TDT v3 | 默认多语种模型 | French、German、Spanish、Italian、Portuguese | 欧洲五语默认 | 一个模型包覆盖欧洲五语,适合离线听写;发布前基准对比 Canary-1B-v2 和 Whisper large-v3-turbo。 |
| Zipformer Japanese | 默认专项模型 | Japanese | 日语默认 | 基于 ReazonSpeech 的日语专项离线模型。 |
| Zipformer Korean | 默认专项模型 | Korean | 韩语默认 | 韩语专项离线模型。 |
| X-ASR zh-en 960ms | 实验模型 | 中文、English、粤语、中英混输 | 中文 / English 可选实验项 | 实验性中英流式模型,用于评估更低延迟和未来“边说边出字”体验。 |
## 快速开始
### 用户
1. 从项目 Releases 页面下载最新版本
2. **Windows**:解压到任意目录,双击 `privatevoice.exe`
3. **macOS**:打开 `.dmg`,拖入 Applications,首次启动需授予辅助功能权限
4. 首次启动自动下载语音模型(~200 MB)
5. 就绪后,在任意输入框中按住 **右 Ctrl**(macOS: **右 Command**)说话,松开即输入
### 开发者
## 开发入口
```bash
# 环境要求
# - Go 1.22+
# - Node.js 20+
# - CGO 编译器 (Windows: LLVM MinGW UCRT, macOS: Xcode Command Line Tools)
# 克隆
git clone <PrivateVoice 仓库地址>
cd PrivateVoice/privatevoice.src
# 安装前端依赖
cd frontend && npm install && cd ..
# 开发模式
wails3 dev
# 生产构建 (Windows)
windres privatevoice.rc -o privatevoice.syso
CGO_ENABLED=1 go build -ldflags "-H windowsgui -s -w" -o privatevoice.exe .
# 生产构建 (macOS)
CGO_ENABLED=1 go build -o privatevoice .
cd /Users/ar/Projects/PrivateVoice/03-O/C1.source/privatevoice.src
npm --prefix frontend run build
go test ./...
```
## 使用方法
常用打包和 QA 命令见 `CODEGRAPH.md`、`K2.项目管理/QA地图.md` 和 `K2.项目管理/发布地图.md`。
1. 系统托盘出现 PrivateVoice 图标,引擎加载完成后就绪
2. **长按模式**:按住快捷键说话,松开即识别并粘贴
3. **自由说话**:短按一下开始,连续说话,再短按一下停止
4. **取消录音**:录音中按 Esc
5. 右键托盘图标可打开设置或退出
## 目录
## 运行文件
**Windows:**
| 文件 | 大小 | 说明 |
|---|---|---|
| `privatevoice.exe` | ~15 MB | 主程序 |
| `onnxruntime.dll` | ~15 MB | ONNX Runtime |
| `sherpa-onnx-c-api.dll` | ~4 MB | sherpa-onnx C API |
| `sherpa-onnx-cxx-api.dll` | ~248 KB | sherpa-onnx C++ API |
| `models/sensevoice/` | ~200 MB | 语音模型(首次自动下载) |
**macOS:**
| 文件 | 大小 | 说明 |
|---|---|---|
| `PrivateVoice Dictation.app` | ~15 MB | 应用包(含 dylib) |
| `models/sensevoice/` | ~200 MB | 语音模型(首次自动下载) |
## 项目结构
```text
03-O/
├── C1.source/privatevoice.src/   源码根
├── C2.builds/                    本地迁移的最新候选包,二进制包不纳入 Git 提交
├── C3.tools/                     工具索引
├── K1.业务知识/                  成果地图和长期业务/技术知识
├── K2.项目管理/                  QA、发布、迁移、handoff、legacy 资料
├── K3.运营与发布资料/            当前运营资料索引和本地素材
└── CODEGRAPH.md                  人工工程地图
```
privatevoice.src/
├── main.go               # 入口:单实例 + Wails 启动
├── app.go                # 编排:热键 → 录音 → 识别 → 粘贴
├── internal/
│   ├── audio/            # malgo 录音 + 设备枚举
│   ├── engine/           # sherpa-onnx 推理
│   ├── hotkey/           # 全局热键轮询
│   ├── input/            # 剪贴板 + 粘贴 + 剪贴板保护
│   ├── config/           # JSON 配置
│   ├── history/          # 识别历史(JSON 持久化)
│   ├── textproc/         # 中英混合文本后处理
│   ├── sound/            # 录音音效(合成 WAV)
│   ├── overlay/          # 浮动指示器(Win32 GDI+ / macOS AppKit)
│   └── ...
├── services/             # Wails 服务(前端绑定)
└── frontend/             # Svelte + Vite
    ├── src/components/   # 设置窗口 + 指示器
    └── src/lib/          # stores + i18n
```
## 技术栈
| 组件 | 技术 |
|---|---|
| 桌面框架 | [Wails v3](https://wails.io/) |
| 前端 | [Svelte 5](https://svelte.dev/) + Vite + TypeScript |
| 音频录制 | [malgo](https://github.com/gen2brain/malgo) (miniaudio) |
| 语音识别 | [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) (SenseVoice) |
| 全局热键 | Windows: GetAsyncKeyState 轮询 / macOS: NSEvent + CGEventSource |
| 文字输入 | Windows: 剪贴板 + SendInput / macOS: 剪贴板 + CGEvent |
| 浮动指示器 | Windows: GDI+ Layered Window / macOS: AppKit + CoreGraphics |
| 音效 | 程序合成 WAV + 平台原生播放 |
## 许可证
MIT License
## 致谢
- [SherpaOnnx](https://github.com/k2-fsa/sherpa-onnx) — 语音识别引擎
- [Wails](https://wails.io/) — Go 桌面应用框架
- [SenseVoice](https://github.com/FunAudioLLM/SenseVoice) — 语音模型
privatevoice.src/internal/logger/logger.go
File was deleted