Add Windows preview build
9 files modified
4 files added
| | |
| | | - build: `20260629.2014` |
| | | - 说明: 本版本为性能回退排查诊断候选包,用于真实使用几天后分析慢在识别、尾部等待还是粘贴链路。 |
| | | |
| | | ### Windows 技术预览 |
| | | |
| | | - **新增 Windows 预览构建脚本**:`privatevoice.src/scripts/build-windows-preview.sh` 可生成 Windows x64 预览目录和 zip。 |
| | | - **预览版只开放 SenseVoice**:Windows build 中其他模型仍可见但置灰禁用,避免用户选择尚未验证的模型导致启动或识别失败。 |
| | | - **打包 sherpa/onnxruntime 运行时 DLL**:预览包包含 `onnxruntime.dll`、`sherpa-onnx-c-api.dll`、`sherpa-onnx-cxx-api.dll`。 |
| | | - **交付边界**:该产物是 Windows 技术预览,不是已签名安装器,也未完成 Windows 10/11 真机 QA。 |
| | | |
| | | --- |
| | | |
| | | ## v2.1.37 (2026-06-29) |
| New file |
| | |
| | | # CODEGRAPH |
| | | |
| | | ## Project |
| | | |
| | | - Product: Wails v3 / Go desktop dictation app. |
| | | - Current branch purpose: Windows technical preview based on `v2.1.38-build20260629.2014-diagnostic`. |
| | | - Protected macOS behavior: existing macOS App Store / local DMG build lines are not changed by this branch. |
| | | |
| | | ## 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. |
| | | |
| | | ## Platform Modules |
| | | |
| | | - Audio: `privatevoice.src/internal/audio/recorder.go` |
| | | - Uses `github.com/gen2brain/malgo`. |
| | | - Cross-platform capture target: 16 kHz, 16-bit, mono. |
| | | - Hotkey: |
| | | - macOS: `privatevoice.src/internal/hotkey/hotkey_darwin.go` |
| | | - Windows: `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` |
| | | - ASR engine: |
| | | - macOS: `privatevoice.src/internal/engine/engine_darwin.go` |
| | | - Windows: `privatevoice.src/internal/engine/engine_windows.go` |
| | | - Overlay: |
| | | - macOS: `privatevoice.src/internal/overlay/overlay_darwin.go` |
| | | - Windows: `privatevoice.src/internal/overlay/overlay_windows.go` |
| | | |
| | | ## Windows Preview Scope |
| | | |
| | | - Windows preview supports `SenseVoice` only. |
| | | - Non-SenseVoice models remain visible in settings but are disabled through `model.IsModelSupportedInCurrentBuild`. |
| | | - Windows unsupported reason: `windows_preview_unsupported`. |
| | | - Build script: `privatevoice.src/scripts/build-windows-preview.sh`. |
| | | - Runtime package includes: |
| | | - `PrivateVoice Dictation.exe` |
| | | - `onnxruntime.dll` |
| | | - `sherpa-onnx-c-api.dll` |
| | | - `sherpa-onnx-cxx-api.dll` |
| | | - `README-WINDOWS-PREVIEW.txt` |
| | | |
| | | ## Standard Verification Commands |
| | | |
| | | Run from `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 ./... |
| | | ./scripts/build-windows-preview.sh |
| | | ``` |
| | | |
| | | ## Known Gaps |
| | | |
| | | - Windows runtime QA must still be performed on Windows 10/11 hardware. |
| | | - The Windows preview zip is unsigned and not an installer. |
| | | - SmartScreen / Defender prompts are expected for local testing. |
| | |
| | | if (option.unsupportedReason === 'requires_macos_14') { |
| | | return t('models.qwen3RequiresMacOS14') |
| | | } |
| | | if (option.unsupportedReason === 'windows_preview_unsupported') { |
| | | return t('models.windowsPreviewUnsupported') |
| | | } |
| | | return t('settings.modelUnavailable') |
| | | } |
| | | |
| | |
| | | "japaneseDesc": "Dedicated Japanese offline model based on ReazonSpeech.", |
| | | "koreanDesc": "Dedicated Korean offline model for Korean dictation.", |
| | | "qwen3Desc": "Higher quality Chinese offline model for long Chinese dictation, technical terms, and mixed Chinese-English input.", |
| | | "qwen3RequiresMacOS14": "Qwen3-ASR requires macOS 14 or later. This compatibility build does not support downloading it." |
| | | "qwen3RequiresMacOS14": "Qwen3-ASR requires macOS 14 or later. This compatibility build does not support downloading it.", |
| | | "windowsPreviewUnsupported": "The Windows preview currently supports SenseVoice only. This model is not available yet." |
| | | }, |
| | | "permissions": { |
| | | "title": "Permission Check", |
| | |
| | | "japaneseDesc": "基于 ReazonSpeech 的日语专项离线模型。", |
| | | "koreanDesc": "韩语专项离线模型,适合韩语听写。", |
| | | "qwen3Desc": "更高质量的中文离线模型,适合中文长句、技术词和中英混输。", |
| | | "qwen3RequiresMacOS14": "Qwen3-ASR 需要 macOS 14 或更高版本,本兼容包暂不支持下载。" |
| | | "qwen3RequiresMacOS14": "Qwen3-ASR 需要 macOS 14 或更高版本,本兼容包暂不支持下载。", |
| | | "windowsPreviewUnsupported": "Windows 预览版当前仅支持 SenseVoice,暂不支持此模型。" |
| | | }, |
| | | "permissions": { |
| | | "title": "权限检查", |
| | |
| | | package model |
| | | |
| | | const UnsupportedReasonRequiresMacOS14 = "requires_macos_14" |
| | | const ( |
| | | UnsupportedReasonRequiresMacOS14 = "requires_macos_14" |
| | | UnsupportedReasonWindowsPreview = "windows_preview_unsupported" |
| | | ) |
| | | |
| | | func IsModelSupportedInCurrentBuild(modelID string) bool { |
| | | if modelID == Qwen3ASRModelID { |
| | | return qwen3ASRSupportedInCurrentBuild |
| | | } |
| | | return true |
| | | return ModelUnsupportedReason(modelID) == "" |
| | | } |
| | | |
| | | func ModelUnsupportedReason(modelID string) string { |
| | | if modelID == Qwen3ASRModelID && !IsModelSupportedInCurrentBuild(modelID) { |
| | | return UnsupportedReasonRequiresMacOS14 |
| | | } |
| | | return "" |
| | | return modelUnsupportedReasonInCurrentBuild(modelID) |
| | | } |
| | |
| | | //go:build !monterey_compat |
| | | //go:build !monterey_compat && !windows |
| | | |
| | | package model |
| | | |
| | | const qwen3ASRSupportedInCurrentBuild = true |
| | | func modelUnsupportedReasonInCurrentBuild(modelID string) string { |
| | | return "" |
| | | } |
| | |
| | | |
| | | package model |
| | | |
| | | const qwen3ASRSupportedInCurrentBuild = false |
| | | func modelUnsupportedReasonInCurrentBuild(modelID string) string { |
| | | if modelID == Qwen3ASRModelID { |
| | | return UnsupportedReasonRequiresMacOS14 |
| | | } |
| | | return "" |
| | | } |
| New file |
| | |
| | | //go:build windows |
| | | |
| | | package model |
| | | |
| | | func modelUnsupportedReasonInCurrentBuild(modelID string) string { |
| | | if modelID == DefaultModelID { |
| | | return "" |
| | | } |
| | | return UnsupportedReasonWindowsPreview |
| | | } |
| New file |
| | |
| | | #!/usr/bin/env bash |
| | | set -euo pipefail |
| | | |
| | | ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| | | cd "$ROOT_DIR" |
| | | |
| | | APP_VERSION="2.1.38" |
| | | APP_BUILD="20260629.2014" |
| | | ARCH="${1:-amd64}" |
| | | |
| | | if [[ "$ARCH" != "amd64" ]]; then |
| | | echo "Only amd64 is supported for the Windows preview build." >&2 |
| | | exit 2 |
| | | fi |
| | | |
| | | for tool in go npm x86_64-w64-mingw32-gcc x86_64-w64-mingw32-g++; do |
| | | if ! command -v "$tool" >/dev/null 2>&1; then |
| | | echo "Missing required tool: $tool" >&2 |
| | | exit 127 |
| | | fi |
| | | done |
| | | |
| | | OUT_ROOT="$ROOT_DIR/build/windows-preview" |
| | | OUT_DIR="$OUT_ROOT/PrivateVoice-Dictation-${APP_VERSION}-build${APP_BUILD}-windows-${ARCH}-preview" |
| | | ZIP_PATH="$OUT_ROOT/PrivateVoice-Dictation-${APP_VERSION}-build${APP_BUILD}-windows-${ARCH}-preview.zip" |
| | | |
| | | rm -rf "$OUT_DIR" "$ZIP_PATH" |
| | | mkdir -p "$OUT_DIR" |
| | | |
| | | echo "Building frontend..." |
| | | npm --prefix frontend install |
| | | npm --prefix frontend run build |
| | | |
| | | SHERPA_WINDOWS_DIR="$(go list -m -f '{{.Dir}}' github.com/k2-fsa/sherpa-onnx-go-windows)" |
| | | SHERPA_DLL_DIR="$SHERPA_WINDOWS_DIR/lib/x86_64-pc-windows-gnu" |
| | | for dll in onnxruntime.dll sherpa-onnx-c-api.dll sherpa-onnx-cxx-api.dll; do |
| | | if [[ ! -f "$SHERPA_DLL_DIR/$dll" ]]; then |
| | | echo "Missing sherpa runtime DLL: $SHERPA_DLL_DIR/$dll" >&2 |
| | | exit 3 |
| | | fi |
| | | done |
| | | |
| | | echo "Building Windows executable..." |
| | | GOOS=windows \ |
| | | GOARCH=amd64 \ |
| | | CGO_ENABLED=1 \ |
| | | CC=x86_64-w64-mingw32-gcc \ |
| | | CXX=x86_64-w64-mingw32-g++ \ |
| | | go build \ |
| | | -buildvcs=false \ |
| | | -ldflags="-H windowsgui" \ |
| | | -o "$OUT_DIR/PrivateVoice Dictation.exe" \ |
| | | . |
| | | |
| | | cp "$SHERPA_DLL_DIR/onnxruntime.dll" "$OUT_DIR/" |
| | | cp "$SHERPA_DLL_DIR/sherpa-onnx-c-api.dll" "$OUT_DIR/" |
| | | cp "$SHERPA_DLL_DIR/sherpa-onnx-cxx-api.dll" "$OUT_DIR/" |
| | | |
| | | cat >"$OUT_DIR/README-WINDOWS-PREVIEW.txt" <<EOF |
| | | PrivateVoice Dictation Windows Preview |
| | | Version: ${APP_VERSION} |
| | | Build: ${APP_BUILD} |
| | | Architecture: Windows x64 |
| | | |
| | | Scope: |
| | | - Technical preview based on PrivateVoice Dictation ${APP_VERSION}. |
| | | - The first Windows preview supports SenseVoice Chinese recognition only. |
| | | - Other models are visible but disabled in this preview build. |
| | | |
| | | How to run: |
| | | 1. Extract the whole folder. |
| | | 2. Keep the EXE and DLL files in the same directory. |
| | | 3. Run "PrivateVoice Dictation.exe". |
| | | 4. Download/install the SenseVoice model if prompted. |
| | | 5. Use the configured hotkey and verify text input in Notepad or a browser text field. |
| | | |
| | | Known limitations: |
| | | - This is not a signed Windows installer. |
| | | - Real Windows audio, hotkey, paste, tray, and WebView2 behavior still need QA on Windows 10/11. |
| | | - If Windows Defender or SmartScreen blocks it, allow it only for local testing. |
| | | EOF |
| | | |
| | | (cd "$OUT_ROOT" && zip -qr "$(basename "$ZIP_PATH")" "$(basename "$OUT_DIR")") |
| | | |
| | | echo "Built: $OUT_DIR" |
| | | echo "Zip: $ZIP_PATH" |
| | |
| | | if err != nil { |
| | | return model.ModelProfile{}, err |
| | | } |
| | | if !model.IsModelSupportedInCurrentBuild(profile.ID) { |
| | | switch model.ModelUnsupportedReason(profile.ID) { |
| | | case "": |
| | | case model.UnsupportedReasonRequiresMacOS14: |
| | | return model.ModelProfile{}, fmt.Errorf("model %s requires macOS 14 or later", profile.ID) |
| | | case model.UnsupportedReasonWindowsPreview: |
| | | return model.ModelProfile{}, fmt.Errorf("model %s is not supported in the Windows preview build; use SenseVoice", profile.ID) |
| | | default: |
| | | return model.ModelProfile{}, fmt.Errorf("model %s is not supported in this build", profile.ID) |
| | | } |
| | | |
| | | current := s.currentModel() |
| | |
| | | //go:build !monterey_compat |
| | | //go:build !monterey_compat && !windows |
| | | |
| | | package services |
| | | |
| New file |
| | |
| | | //go:build windows |
| | | |
| | | package services |
| | | |
| | | import ( |
| | | "strings" |
| | | "testing" |
| | | |
| | | "voicesnap/internal/config" |
| | | "voicesnap/internal/language" |
| | | "voicesnap/internal/model" |
| | | "voicesnap/internal/modelselection" |
| | | ) |
| | | |
| | | func TestWindowsPreviewShowsButDisablesNonSenseVoiceModels(t *testing.T) { |
| | | cfg := config.Default() |
| | | cfg.LanguageMode = config.LanguageModeManual |
| | | cfg.LanguageID = model.DefaultLanguageID |
| | | service := NewEngineService(cfg) |
| | | profile, err := model.GetModelProfile(model.Qwen3ASRModelID) |
| | | if err != nil { |
| | | t.Fatal(err) |
| | | } |
| | | |
| | | status := service.modelStatusMap(profile, modelselection.CurrentModel{ |
| | | ModelID: model.DefaultModelID, |
| | | Profile: model.DefaultModelProfile(), |
| | | SelectionMode: config.ModelSelectionModeAuto, |
| | | LanguageSettings: language.Resolve(config.LanguageModeManual, model.DefaultLanguageID, nil), |
| | | }) |
| | | |
| | | if got := status["supportedInBuild"]; got != false { |
| | | t.Fatalf("supportedInBuild = %v, want false", got) |
| | | } |
| | | if got := status["unsupportedReason"]; got != model.UnsupportedReasonWindowsPreview { |
| | | t.Fatalf("unsupportedReason = %v, want %s", got, model.UnsupportedReasonWindowsPreview) |
| | | } |
| | | if _, err := service.allowedModelProfile(model.Qwen3ASRModelID); err == nil { |
| | | t.Fatal("expected qwen3-asr to be rejected in Windows preview build") |
| | | } else if !strings.Contains(err.Error(), "Windows preview") { |
| | | t.Fatalf("error = %q, want Windows preview reason", err.Error()) |
| | | } |
| | | } |