CHANGELOG.md
@@ -13,6 +13,16 @@ - build: `20260629.2014` - 说明: 本版本为性能回退排查诊断候选包,用于真实使用几天后分析慢在识别、尾部等待还是粘贴链路。 ### Windows 技术预览 - **新增 Windows 预览构建脚本**:`privatevoice.src/scripts/build-windows-preview.sh` 可生成 Windows x64 预览目录和 zip。 - **内置 Windows 真机 QA 采集脚本**:预览包包含 `QA-WINDOWS-PREVIEW.ps1`,用于采集 WebView2 / 音频端点预检信息、启动 App、打开 Notepad 目标文件、提示完成一次听写,并收集日志和目标文本证据。 - **新增双击 QA 启动器**:预览包包含 `RUN-WINDOWS-QA.cmd`,便于 Windows 测试者直接启动 QA 流程。 - **新增 Windows QA 证据验证脚本**:`privatevoice.src/scripts/verify-windows-qa-evidence.sh` 可检查返回的 `qa-evidence-*.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) CODEGRAPH.md
New file @@ -0,0 +1,67 @@ # 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`. - 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` ## 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 ./scripts/verify-windows-qa-evidence.sh /path/to/qa-evidence-YYYYMMDD-HHMMSS.zip ``` ## 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. privatevoice.src/frontend/src/components/settings/LanguagePage.svelte
@@ -184,6 +184,9 @@ if (option.unsupportedReason === 'requires_macos_14') { return t('models.qwen3RequiresMacOS14') } if (option.unsupportedReason === 'windows_preview_unsupported') { return t('models.windowsPreviewUnsupported') } return t('settings.modelUnavailable') } privatevoice.src/frontend/src/lib/i18n/en.json
@@ -92,7 +92,8 @@ "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", privatevoice.src/frontend/src/lib/i18n/zh.json
@@ -92,7 +92,8 @@ "japaneseDesc": "基于 ReazonSpeech 的日语专项离线模型。", "koreanDesc": "韩语专项离线模型,适合韩语听写。", "qwen3Desc": "更高质量的中文离线模型,适合中文长句、技术词和中英混输。", "qwen3RequiresMacOS14": "Qwen3-ASR 需要 macOS 14 或更高版本,本兼容包暂不支持下载。" "qwen3RequiresMacOS14": "Qwen3-ASR 需要 macOS 14 或更高版本,本兼容包暂不支持下载。", "windowsPreviewUnsupported": "Windows 预览版当前仅支持 SenseVoice,暂不支持此模型。" }, "permissions": { "title": "权限检查", privatevoice.src/internal/model/build_capabilities.go
@@ -1,17 +1,14 @@ 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) } privatevoice.src/internal/model/build_capabilities_default.go
@@ -1,5 +1,7 @@ //go:build !monterey_compat //go:build !monterey_compat && !windows package model const qwen3ASRSupportedInCurrentBuild = true func modelUnsupportedReasonInCurrentBuild(modelID string) string { return "" } privatevoice.src/internal/model/build_capabilities_monterey.go
@@ -2,4 +2,9 @@ package model const qwen3ASRSupportedInCurrentBuild = false func modelUnsupportedReasonInCurrentBuild(modelID string) string { if modelID == Qwen3ASRModelID { return UnsupportedReasonRequiresMacOS14 } return "" } privatevoice.src/internal/model/build_capabilities_windows.go
New file @@ -0,0 +1,10 @@ //go:build windows package model func modelUnsupportedReasonInCurrentBuild(modelID string) string { if modelID == DefaultModelID { return "" } return UnsupportedReasonWindowsPreview } privatevoice.src/scripts/QA-WINDOWS-PREVIEW.ps1
New file @@ -0,0 +1,190 @@ param( [switch]$CollectOnly, [switch]$NoLaunch, [int]$WaitSeconds = 45 ) $ErrorActionPreference = "Stop" function Write-Step { param([string]$Message) Write-Host "" Write-Host "== $Message ==" -ForegroundColor Cyan } function Add-Result { param( [System.Collections.Generic.List[string]]$Results, [string]$Name, [bool]$Passed, [string]$Detail = "" ) $state = if ($Passed) { "PASS" } else { "NEEDS_EVIDENCE" } $line = if ($Detail) { "$state`t$Name`t$Detail" } else { "$state`t$Name" } $Results.Add($line) | Out-Null $color = if ($Passed) { "Green" } else { "Yellow" } Write-Host "[$state] $Name $Detail" -ForegroundColor $color } $PackageDir = Split-Path -Parent $MyInvocation.MyCommand.Path $ExePath = Join-Path $PackageDir "PrivateVoice Dictation.exe" $RequiredFiles = @( "PrivateVoice Dictation.exe", "onnxruntime.dll", "sherpa-onnx-c-api.dll", "sherpa-onnx-cxx-api.dll" ) $AppDataDir = Join-Path $env:APPDATA "PrivateVoice Input" $LogPath = Join-Path $AppDataDir "app.log" $Timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $EvidenceDir = Join-Path $PackageDir "qa-evidence-$Timestamp" $TargetPath = Join-Path $EvidenceDir "qa-target.txt" $Results = [System.Collections.Generic.List[string]]::new() New-Item -ItemType Directory -Path $EvidenceDir -Force | Out-Null Write-Step "PrivateVoice Windows preview QA" "GeneratedAt: $(Get-Date -Format o)" | Set-Content -Encoding UTF8 (Join-Path $EvidenceDir "qa-summary.txt") "PackageDir: $PackageDir" | Add-Content -Encoding UTF8 (Join-Path $EvidenceDir "qa-summary.txt") "AppDataDir: $AppDataDir" | Add-Content -Encoding UTF8 (Join-Path $EvidenceDir "qa-summary.txt") Write-Step "Checking package files" foreach ($file in $RequiredFiles) { $path = Join-Path $PackageDir $file Add-Result $Results "file exists: $file" (Test-Path $path) $path } Write-Step "Collecting system information" try { Get-ComputerInfo | Select-Object WindowsProductName, WindowsVersion, OsHardwareAbstractionLayer, CsSystemType, CsProcessors, CsTotalPhysicalMemory | Format-List | Out-String | Set-Content -Encoding UTF8 (Join-Path $EvidenceDir "system-info.txt") Add-Result $Results "system info collected" $true } catch { Add-Result $Results "system info collected" $false $_.Exception.Message } Write-Step "Collecting Windows runtime preflight" try { $webview2Entries = @() $uninstallRoots = @( "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" ) foreach ($root in $uninstallRoots) { $webview2Entries += Get-ItemProperty $root -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match "WebView2" } | Select-Object DisplayName, DisplayVersion, Publisher, InstallLocation } if ($webview2Entries.Count -gt 0) { $webview2Entries | Format-List | Out-String | Set-Content -Encoding UTF8 (Join-Path $EvidenceDir "webview2-runtime.txt") } else { "No WebView2 Runtime entry found in standard uninstall registry paths. App startup logs remain authoritative." | Set-Content -Encoding UTF8 (Join-Path $EvidenceDir "webview2-runtime.txt") } Add-Result $Results "WebView2 registry inventory collected" $true "entries=$($webview2Entries.Count)" } catch { Add-Result $Results "WebView2 registry inventory collected" $false $_.Exception.Message } try { if (Get-Command Get-PnpDevice -ErrorAction SilentlyContinue) { Get-PnpDevice -Class AudioEndpoint -ErrorAction SilentlyContinue | Select-Object Status, Class, FriendlyName, InstanceId | Format-Table -AutoSize | Out-String | Set-Content -Encoding UTF8 (Join-Path $EvidenceDir "audio-endpoints.txt") } else { Get-CimInstance Win32_SoundDevice -ErrorAction SilentlyContinue | Select-Object Status, Name, Manufacturer, DeviceID | Format-Table -AutoSize | Out-String | Set-Content -Encoding UTF8 (Join-Path $EvidenceDir "audio-endpoints.txt") } Add-Result $Results "audio endpoint inventory collected" $true } catch { Add-Result $Results "audio endpoint inventory collected" $false $_.Exception.Message } if (-not $CollectOnly -and -not $NoLaunch) { Write-Step "Launching app" if (-not (Test-Path $ExePath)) { throw "Missing executable: $ExePath" } Start-Process -FilePath $ExePath -WorkingDirectory $PackageDir | Out-Null } Write-Step "Waiting for app log" $deadline = (Get-Date).AddSeconds($WaitSeconds) while ((Get-Date) -lt $deadline -and -not (Test-Path $LogPath)) { Start-Sleep -Milliseconds 500 } Add-Result $Results "app log exists" (Test-Path $LogPath) $LogPath Write-Step "Checking process" $processes = Get-Process | Where-Object { $_.ProcessName -like "PrivateVoice*" -or $_.Path -eq $ExePath } | Select-Object Id, ProcessName, Path $processes | Format-Table -AutoSize | Out-String | Set-Content -Encoding UTF8 (Join-Path $EvidenceDir "processes.txt") Add-Result $Results "PrivateVoice process visible" (($processes | Measure-Object).Count -gt 0) if (-not $CollectOnly) { Write-Step "Manual runtime check" @( "PrivateVoice Windows Preview QA Target", "Click after the marker below, dictate one short Chinese sentence, then press Ctrl+S in Notepad.", "DICTATION_RESULT:" ) | Set-Content -Encoding UTF8 $TargetPath try { Start-Process -FilePath "notepad.exe" -ArgumentList "`"$TargetPath`"" | Out-Null Add-Result $Results "Notepad target launched" $true $TargetPath } catch { Add-Result $Results "Notepad target launched" $false $_.Exception.Message } Write-Host "1. Keep this PowerShell window open." -ForegroundColor White Write-Host "2. In PrivateVoice, make sure the SenseVoice model is installed and the engine is ready." -ForegroundColor White Write-Host "3. Use the Notepad window opened by this script as the target input field." -ForegroundColor White Write-Host "4. Click after DICTATION_RESULT:, use the configured hotkey, and speak one short Chinese sentence." -ForegroundColor White Write-Host "5. Confirm the recognized text appears in Notepad, press Ctrl+S in Notepad, then return here." -ForegroundColor White Read-Host "Press Enter after you save the Notepad target file" } if (Test-Path $TargetPath) { Copy-Item $TargetPath (Join-Path $EvidenceDir "qa-target-captured.txt") -Force $targetText = Get-Content $TargetPath -Raw -ErrorAction SilentlyContinue $marker = "DICTATION_RESULT:" $markerIndex = $targetText.IndexOf($marker) $dictationText = "" if ($markerIndex -ge 0) { $dictationText = $targetText.Substring($markerIndex + $marker.Length).Trim() } $dictationText | Set-Content -Encoding UTF8 (Join-Path $EvidenceDir "qa-target-dictation-text.txt") Add-Result $Results "target file saved with dictation text" ($dictationText.Length -gt 0) "chars=$($dictationText.Length)" } Write-Step "Collecting logs" if (Test-Path $LogPath) { Copy-Item $LogPath (Join-Path $EvidenceDir "app.log") -Force $logText = Get-Content $LogPath -Raw -ErrorAction SilentlyContinue Add-Result $Results "startup log found" ($logText -match "PrivateVoice Dictation starting") Add-Result $Results "engine ready log found" ($logText -match "ASR engine ready|Engine initialized") Add-Result $Results "recording log found" ($logText -match "Recording started") Add-Result $Results "recognized text log found" ($logText -match "Recognized:") Add-Result $Results "paste success log found" ($logText -match "PERF paste_done.*result=success|PERF fallback_type_done.*result=success") } $ConfigPath = Join-Path $AppDataDir "config.json" if (Test-Path $ConfigPath) { Copy-Item $ConfigPath (Join-Path $EvidenceDir "config.json") -Force } $Results | Set-Content -Encoding UTF8 (Join-Path $EvidenceDir "qa-results.tsv") $Results | Add-Content -Encoding UTF8 (Join-Path $EvidenceDir "qa-summary.txt") Write-Step "Evidence package" $ZipPath = "$EvidenceDir.zip" if (Test-Path $ZipPath) { Remove-Item $ZipPath -Force } Compress-Archive -Path (Join-Path $EvidenceDir "*") -DestinationPath $ZipPath Write-Host "Evidence directory: $EvidenceDir" -ForegroundColor Green Write-Host "Evidence zip: $ZipPath" -ForegroundColor Green Write-Host "" Write-Host "Send back the evidence zip if any check is NEEDS_EVIDENCE or the app does not behave correctly." -ForegroundColor Yellow privatevoice.src/scripts/RUN-WINDOWS-QA.cmd
New file @@ -0,0 +1,18 @@ @echo off setlocal cd /d "%~dp0" echo PrivateVoice Dictation Windows Preview QA echo. echo This will run QA-WINDOWS-PREVIEW.ps1 and collect evidence files. echo. powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0QA-WINDOWS-PREVIEW.ps1" set EXITCODE=%ERRORLEVEL% echo. if not "%EXITCODE%"=="0" ( echo QA script exited with code %EXITCODE%. ) else ( echo QA script finished. ) echo. pause exit /b %EXITCODE% privatevoice.src/scripts/build-windows-preview.sh
New file @@ -0,0 +1,91 @@ #!/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/" cp "$ROOT_DIR/scripts/QA-WINDOWS-PREVIEW.ps1" "$OUT_DIR/" cp "$ROOT_DIR/scripts/RUN-WINDOWS-QA.cmd" "$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. Double-click "RUN-WINDOWS-QA.cmd". 4. If Windows blocks the command file, right-click "QA-WINDOWS-PREVIEW.ps1" and choose "Run with PowerShell", or run this command from the extracted folder: powershell -ExecutionPolicy Bypass -File .\\QA-WINDOWS-PREVIEW.ps1 5. Follow the on-screen checklist. It will collect WebView2/audio preflight data, launch the app, open a Notepad target file, ask you to dictate once, and collect logs plus the saved target text. 6. You can also run "PrivateVoice Dictation.exe" directly after QA. 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. - The QA evidence zip may contain app logs and the saved Notepad target text. Review before sharing if needed. EOF (cd "$OUT_ROOT" && zip -qr "$(basename "$ZIP_PATH")" "$(basename "$OUT_DIR")") echo "Built: $OUT_DIR" echo "Zip: $ZIP_PATH" privatevoice.src/scripts/verify-windows-qa-evidence.sh
New file @@ -0,0 +1,96 @@ #!/usr/bin/env bash set -euo pipefail if [[ $# -ne 1 ]]; then echo "Usage: $0 /path/to/qa-evidence-YYYYMMDD-HHMMSS.zip" >&2 exit 2 fi evidence_zip="$1" if [[ ! -f "$evidence_zip" ]]; then echo "Evidence zip not found: $evidence_zip" >&2 exit 2 fi workdir="$(mktemp -d)" cleanup() { rm -rf "$workdir" } trap cleanup EXIT unzip -q "$evidence_zip" -d "$workdir" find_one() { local name="$1" find "$workdir" -type f -name "$name" -print -quit } require_file() { local name="$1" local path path="$(find_one "$name")" if [[ -z "$path" ]]; then echo "FAIL missing $name" >&2 return 1 fi echo "$path" } check_pattern() { local label="$1" local file="$2" local pattern="$3" if rg -q "$pattern" "$file"; then echo "PASS $label" return 0 fi echo "FAIL $label" return 1 } failures=0 qa_results="$(require_file "qa-results.tsv")" || failures=$((failures + 1)) app_log="$(require_file "app.log")" || failures=$((failures + 1)) target_text="$(require_file "qa-target-dictation-text.txt")" || failures=$((failures + 1)) system_info="$(require_file "system-info.txt")" || failures=$((failures + 1)) if [[ -n "${qa_results:-}" ]]; then if rg -q '^NEEDS_EVIDENCE' "$qa_results"; then echo "FAIL qa-results.tsv contains NEEDS_EVIDENCE" rg '^NEEDS_EVIDENCE' "$qa_results" || true failures=$((failures + 1)) else echo "PASS qa-results.tsv has no NEEDS_EVIDENCE" fi fi if [[ -n "${app_log:-}" ]]; then check_pattern "startup log" "$app_log" 'PrivateVoice Dictation starting' || failures=$((failures + 1)) check_pattern "engine ready log" "$app_log" 'ASR engine ready|Engine initialized' || failures=$((failures + 1)) check_pattern "recording log" "$app_log" 'Recording started' || failures=$((failures + 1)) check_pattern "recognized text log" "$app_log" 'Recognized:' || failures=$((failures + 1)) check_pattern "paste success log" "$app_log" 'PERF paste_done.*result=success|PERF fallback_type_done.*result=success' || failures=$((failures + 1)) fi if [[ -n "${target_text:-}" ]]; then if [[ -s "$target_text" ]] && [[ -n "$(tr -d '[:space:]' < "$target_text")" ]]; then echo "PASS Notepad target text captured" else echo "FAIL Notepad target text is empty" failures=$((failures + 1)) fi fi if [[ -n "${system_info:-}" ]]; then check_pattern "Windows system info" "$system_info" 'Windows|Microsoft|CsSystemType|WindowsProductName' || failures=$((failures + 1)) fi echo if [[ "$failures" -eq 0 ]]; then echo "WINDOWS_QA_EVIDENCE_PASS" exit 0 fi echo "WINDOWS_QA_EVIDENCE_FAIL failures=$failures" exit 1 privatevoice.src/services/engine_service.go
@@ -373,8 +373,14 @@ 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() privatevoice.src/services/engine_service_qwen3_default_test.go
@@ -1,4 +1,4 @@ //go:build !monterey_compat //go:build !monterey_compat && !windows package services privatevoice.src/services/engine_service_windows_test.go
New file @@ -0,0 +1,43 @@ //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()) } }