From ac31349b32d89b2798cd20224fcdede91d6bfe05 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Tue, 30 Jun 2026 03:30:51 +0800
Subject: [PATCH] Capture Windows QA target text

---
 privatevoice.src/internal/engine/engine.go |   53 +++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 47 insertions(+), 6 deletions(-)

diff --git a/privatevoice.src/internal/engine/engine.go b/privatevoice.src/internal/engine/engine.go
index 8e2c23c..aed8e7f 100755
--- a/privatevoice.src/internal/engine/engine.go
+++ b/privatevoice.src/internal/engine/engine.go
@@ -2,9 +2,12 @@
 
 import (
 	"fmt"
+	"time"
 	"voicesnap/internal/config"
+	"voicesnap/internal/language"
 	"voicesnap/internal/logger"
 	"voicesnap/internal/model"
+	"voicesnap/internal/modelselection"
 	"voicesnap/internal/paths"
 )
 
@@ -15,6 +18,33 @@
 	// HardwareInfo returns a human-readable description of the hardware backend being used.
 	HardwareInfo() string
 	// Close releases engine resources.
+	Close()
+}
+
+// StreamingEngine is implemented by engines that can expose partial results
+// while audio is still being captured.
+type StreamingEngine interface {
+	NewStreamingSession() (StreamingSession, error)
+}
+
+// ReleaseTailCaptureEngine is implemented by engines that need a short audio
+// capture grace period after the user releases the recording hotkey.
+type ReleaseTailCaptureEngine interface {
+	ReleaseTailCaptureDelay() time.Duration
+}
+
+// HoldPreCaptureEngine is implemented by engines that should start capturing
+// audio immediately on hold-to-talk key down, before the activation delay has
+// confirmed that the hotkey was not part of a key combination.
+type HoldPreCaptureEngine interface {
+	HoldPreCaptureEnabled() bool
+}
+
+// StreamingSession receives incremental 16kHz mono PCM and returns the current
+// best transcript for the active utterance.
+type StreamingSession interface {
+	Accept(samples []float32) (string, error)
+	Finish() (string, error)
 	Close()
 }
 
@@ -60,9 +90,10 @@
 	cfg, err := config.Load()
 	if err != nil {
 		logger.Error("Failed to load config for model selection: %v", err)
-		return NewWithModelID(model.DefaultModelID)
+		cfg = config.Default()
 	}
-	return NewWithModelID(cfg.SelectedModelID)
+	current := modelselection.Resolve(cfg, language.NewSystemDetector())
+	return NewWithModelID(current.ModelID)
 }
 
 func NewWithModelID(modelID string) (Engine, error) {
@@ -77,18 +108,28 @@
 	if !resolved.IsUsable() {
 		return nil, fmt.Errorf("model %s is not usable: %s missing=%v problems=%v", resolved.ModelID, resolved.Status, resolved.Missing, resolved.Problems)
 	}
-	if resolved.BackendKind != model.BackendSenseVoice {
+	if !isSupportedBackend(resolved.BackendKind) {
 		return nil, fmt.Errorf("unsupported backend kind: %s", resolved.BackendKind)
 	}
 
-	logger.Info("Loading model %s from %s", resolved.ModelID, resolved.RootDir)
+	logger.Info("Loading model %s backend=%s from %s", resolved.ModelID, resolved.BackendKind, resolved.RootDir)
 	return newPlatformEngine(resolved)
+}
+
+func isSupportedBackend(backend string) bool {
+	switch backend {
+	case model.BackendSenseVoice, model.BackendMoonshine, model.BackendTransducer, model.BackendNemoTransducer, model.BackendQwen3ASR, model.BackendXASRStreaming:
+		return true
+	default:
+		return false
+	}
 }
 
 func resolveCurrentModel() (model.ResolvedModel, error) {
 	cfg, err := config.Load()
 	if err != nil {
-		return model.ResolveModel(model.DefaultModelID)
+		cfg = config.Default()
 	}
-	return model.ResolveModel(model.NormalizeModelID(cfg.SelectedModelID))
+	current := modelselection.Resolve(cfg, language.NewSystemDetector())
+	return model.ResolveModel(current.ModelID)
 }

--
Gitblit v1.9.3