From 58c6b963b81e07765d29cda9410399e9644c886f Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Tue, 30 Jun 2026 03:23:24 +0800
Subject: [PATCH] Add Windows preview QA collector
---
privatevoice.src/internal/engine/engine_darwin.go | 155 +++++++++++++++++++++++++++++++++++++++++++++------
1 files changed, 136 insertions(+), 19 deletions(-)
diff --git a/privatevoice.src/internal/engine/engine_darwin.go b/privatevoice.src/internal/engine/engine_darwin.go
index d18dd48..b45349d 100755
--- a/privatevoice.src/internal/engine/engine_darwin.go
+++ b/privatevoice.src/internal/engine/engine_darwin.go
@@ -4,11 +4,26 @@
import (
"fmt"
+ "strings"
+ "sync"
+ "time"
"voicesnap/internal/logger"
"voicesnap/internal/model"
_ "github.com/k2-fsa/sherpa-onnx-go-macos"
sherpa "github.com/k2-fsa/sherpa-onnx-go/sherpa_onnx"
+)
+
+const (
+ asrSampleRate = 16000
+ xasrHeadPaddingSamples = asrSampleRate / 4
+ xasrTailPaddingSamples = asrSampleRate + asrSampleRate/2
+ xasrReleaseTailDelay = 300 * time.Millisecond
+)
+
+var (
+ xasrHeadPadding = make([]float32, xasrHeadPaddingSamples)
+ xasrTailPadding = make([]float32, xasrTailPaddingSamples)
)
type sherpaEngine struct {
@@ -19,6 +34,15 @@
type xasrStreamingEngine struct {
recognizer *sherpa.OnlineRecognizer
hwInfo string
+ mu sync.Mutex
+}
+
+type xasrStreamingSession struct {
+ engine *xasrStreamingEngine
+ stream *sherpa.OnlineStream
+ lastText string
+ hasAcceptedSamples bool
+ finished bool
}
func newPlatformEngine(resolved model.ResolvedModel) (Engine, error) {
@@ -94,21 +118,19 @@
config.ModelConfig.Moonshine.Encoder = resolved.Files["encoder"]
config.ModelConfig.Moonshine.UncachedDecoder = resolved.Files["uncached_decoder"]
config.ModelConfig.Moonshine.CachedDecoder = resolved.Files["cached_decoder"]
+ case model.BackendTransducer:
+ config.ModelConfig.Transducer.Encoder = resolved.Files["encoder"]
+ config.ModelConfig.Transducer.Decoder = resolved.Files["decoder"]
+ config.ModelConfig.Transducer.Joiner = resolved.Files["joiner"]
case model.BackendNemoTransducer:
config.ModelConfig.Transducer.Encoder = resolved.Files["encoder"]
config.ModelConfig.Transducer.Decoder = resolved.Files["decoder"]
config.ModelConfig.Transducer.Joiner = resolved.Files["joiner"]
config.ModelConfig.ModelType = model.BackendNemoTransducer
case model.BackendQwen3ASR:
- config.ModelConfig.Qwen3ASR.ConvFrontend = resolved.Files["conv_frontend"]
- config.ModelConfig.Qwen3ASR.Encoder = resolved.Files["encoder"]
- config.ModelConfig.Qwen3ASR.Decoder = resolved.Files["decoder"]
- config.ModelConfig.Qwen3ASR.Tokenizer = resolved.Files["tokenizer"]
- config.ModelConfig.Qwen3ASR.MaxTotalLen = 1024
- config.ModelConfig.Qwen3ASR.MaxNewTokens = 256
- config.ModelConfig.Qwen3ASR.Temperature = 0.0
- config.ModelConfig.Qwen3ASR.TopP = 0.9
- config.ModelConfig.Qwen3ASR.Seed = 0
+ if err := applyQwen3ASRConfig(&config, resolved); err != nil {
+ return sherpa.OfflineRecognizerConfig{}, err
+ }
default:
return sherpa.OfflineRecognizerConfig{}, fmt.Errorf("unsupported backend kind: %s", resolved.BackendKind)
}
@@ -174,7 +196,7 @@
stream := sherpa.NewOfflineStream(e.recognizer)
defer sherpa.DeleteOfflineStream(stream)
- stream.AcceptWaveform(16000, samples)
+ stream.AcceptWaveform(asrSampleRate, samples)
e.recognizer.Decode(stream)
result := stream.GetResult()
@@ -183,24 +205,108 @@
}
func (e *xasrStreamingEngine) Recognize(samples []float32) (string, error) {
- stream := sherpa.NewOnlineStream(e.recognizer)
- defer sherpa.DeleteOnlineStream(stream)
-
- if len(samples) > 0 {
- stream.AcceptWaveform(16000, samples)
+ session, err := e.NewStreamingSession()
+ if err != nil {
+ return "", err
}
- stream.InputFinished()
+ defer session.Close()
+ if _, err := session.Accept(samples); err != nil {
+ return "", err
+ }
+ return session.Finish()
+}
+
+func (e *xasrStreamingEngine) NewStreamingSession() (StreamingSession, error) {
+ if e.recognizer == nil {
+ return nil, fmt.Errorf("X-ASR recognizer is not initialized")
+ }
+ stream := sherpa.NewOnlineStream(e.recognizer)
+ if stream == nil {
+ return nil, fmt.Errorf("failed to create X-ASR streaming session")
+ }
+ return &xasrStreamingSession{
+ engine: e,
+ stream: stream,
+ }, nil
+}
+
+func (s *xasrStreamingSession) Accept(samples []float32) (string, error) {
+ if len(samples) == 0 {
+ return s.lastText, nil
+ }
+ if s.stream == nil || s.finished {
+ return s.lastText, nil
+ }
+ if !s.hasAcceptedSamples {
+ samples = xasrSamplesWithHeadPadding(samples)
+ s.hasAcceptedSamples = true
+ }
+
+ s.engine.mu.Lock()
+ defer s.engine.mu.Unlock()
+
+ s.stream.AcceptWaveform(asrSampleRate, samples)
+ s.lastText = s.engine.decodeReadyLocked(s.stream, s.lastText)
+ return s.lastText, nil
+}
+
+func (s *xasrStreamingSession) Finish() (string, error) {
+ if s.stream == nil || s.finished {
+ return s.lastText, nil
+ }
+
+ s.engine.mu.Lock()
+ defer s.engine.mu.Unlock()
+
+ s.stream.AcceptWaveform(asrSampleRate, xasrTailPadding)
+ s.lastText = s.engine.decodeReadyLocked(s.stream, s.lastText)
+ s.stream.InputFinished()
+ s.lastText = s.engine.decodeReadyLocked(s.stream, s.lastText)
+ s.finished = true
+ return s.lastText, nil
+}
+
+func (s *xasrStreamingSession) Close() {
+ if s.stream == nil {
+ return
+ }
+
+ s.engine.mu.Lock()
+ defer s.engine.mu.Unlock()
+
+ sherpa.DeleteOnlineStream(s.stream)
+ s.stream = nil
+}
+
+func (e *xasrStreamingEngine) decodeReadyLocked(stream *sherpa.OnlineStream, lastText string) string {
for e.recognizer.IsReady(stream) {
e.recognizer.Decode(stream)
+ lastText = rememberNonEmptyText(lastText, onlineResultText(e.recognizer, stream))
}
+ return rememberNonEmptyText(lastText, onlineResultText(e.recognizer, stream))
+}
- result := e.recognizer.GetResult(stream)
+func onlineResultText(recognizer *sherpa.OnlineRecognizer, stream *sherpa.OnlineStream) string {
+ result := recognizer.GetResult(stream)
if result == nil {
- return "", nil
+ return ""
}
+ return result.Text
+}
- return result.Text, nil
+func rememberNonEmptyText(lastText, nextText string) string {
+ if strings.TrimSpace(nextText) == "" {
+ return lastText
+ }
+ return nextText
+}
+
+func xasrSamplesWithHeadPadding(samples []float32) []float32 {
+ padded := make([]float32, 0, len(xasrHeadPadding)+len(samples))
+ padded = append(padded, xasrHeadPadding...)
+ padded = append(padded, samples...)
+ return padded
}
func (e *sherpaEngine) HardwareInfo() string {
@@ -211,6 +317,14 @@
return e.hwInfo
}
+func (e *xasrStreamingEngine) ReleaseTailCaptureDelay() time.Duration {
+ return xasrReleaseTailDelay
+}
+
+func (e *xasrStreamingEngine) HoldPreCaptureEnabled() bool {
+ return true
+}
+
func (e *sherpaEngine) Close() {
if e.recognizer != nil {
sherpa.DeleteOfflineRecognizer(e.recognizer)
@@ -219,6 +333,9 @@
}
func (e *xasrStreamingEngine) Close() {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+
if e.recognizer != nil {
sherpa.DeleteOnlineRecognizer(e.recognizer)
e.recognizer = nil
--
Gitblit v1.9.3