From e533a8cd34995af9aaeeb321b41969a260fef882 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 24 Jun 2026 02:37:59 +0800
Subject: [PATCH] Fix about page support link wrapping
---
privatevoice.src/internal/engine/engine_darwin.go | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 211 insertions(+), 1 deletions(-)
diff --git a/privatevoice.src/internal/engine/engine_darwin.go b/privatevoice.src/internal/engine/engine_darwin.go
index be5b525..5dd090d 100755
--- a/privatevoice.src/internal/engine/engine_darwin.go
+++ b/privatevoice.src/internal/engine/engine_darwin.go
@@ -4,6 +4,9 @@
import (
"fmt"
+ "strings"
+ "sync"
+ "time"
"voicesnap/internal/logger"
"voicesnap/internal/model"
@@ -11,14 +14,43 @@
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 {
recognizer *sherpa.OfflineRecognizer
hwInfo string
}
+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) {
if !isSupportedBackend(resolved.BackendKind) {
return nil, fmt.Errorf("unsupported backend kind: %s", resolved.BackendKind)
+ }
+ if resolved.BackendKind == model.BackendXASRStreaming {
+ return newXASRStreamingEngine(resolved)
}
providers := darwinProviders(resolved.ProviderOrder)
@@ -44,6 +76,30 @@
return nil, fmt.Errorf("failed to initialize sherpa-onnx with any provider")
}
+func newXASRStreamingEngine(resolved model.ResolvedModel) (Engine, error) {
+ providers := darwinProviders(resolved.ProviderOrder)
+
+ for _, p := range providers {
+ config, err := onlineConfigForResolvedModel(resolved, p.provider)
+ if err != nil {
+ return nil, err
+ }
+
+ recognizer := sherpa.NewOnlineRecognizer(&config)
+ if recognizer != nil {
+ info := fmt.Sprintf("%s ยท %s", resolved.Profile.DisplayName, p.name)
+ logger.Info("Engine initialized: %s", info)
+ return &xasrStreamingEngine{
+ recognizer: recognizer,
+ hwInfo: info,
+ }, nil
+ }
+ logger.Info("Failed to init X-ASR with %s, trying next provider", p.name)
+ }
+
+ return nil, fmt.Errorf("failed to initialize X-ASR streaming recognizer with any provider")
+}
+
func offlineConfigForResolvedModel(resolved model.ResolvedModel, provider string) (sherpa.OfflineRecognizerConfig, error) {
config := sherpa.OfflineRecognizerConfig{}
config.FeatConfig.SampleRate = 16000
@@ -62,6 +118,10 @@
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"]
@@ -79,6 +139,29 @@
config.ModelConfig.Qwen3ASR.Seed = 0
default:
return sherpa.OfflineRecognizerConfig{}, fmt.Errorf("unsupported backend kind: %s", resolved.BackendKind)
+ }
+
+ return config, nil
+}
+
+func onlineConfigForResolvedModel(resolved model.ResolvedModel, provider string) (sherpa.OnlineRecognizerConfig, error) {
+ config := sherpa.OnlineRecognizerConfig{}
+ config.FeatConfig.SampleRate = 16000
+ config.FeatConfig.FeatureDim = 80
+ config.ModelConfig.Tokens = resolved.Files["tokens"]
+ config.ModelConfig.NumThreads = resolved.Profile.NumThreads
+ config.ModelConfig.Provider = provider
+ config.ModelConfig.ModelType = "zipformer2"
+ config.DecodingMethod = "greedy_search"
+ config.EnableEndpoint = 0
+
+ switch resolved.BackendKind {
+ case model.BackendXASRStreaming:
+ config.ModelConfig.Transducer.Encoder = resolved.Files["encoder"]
+ config.ModelConfig.Transducer.Decoder = resolved.Files["decoder"]
+ config.ModelConfig.Transducer.Joiner = resolved.Files["joiner"]
+ default:
+ return sherpa.OnlineRecognizerConfig{}, fmt.Errorf("unsupported online backend kind: %s", resolved.BackendKind)
}
return config, nil
@@ -119,7 +202,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()
@@ -127,8 +210,125 @@
return result.Text, nil
}
+func (e *xasrStreamingEngine) Recognize(samples []float32) (string, error) {
+ session, err := e.NewStreamingSession()
+ if err != nil {
+ return "", err
+ }
+ 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))
+}
+
+func onlineResultText(recognizer *sherpa.OnlineRecognizer, stream *sherpa.OnlineStream) string {
+ result := recognizer.GetResult(stream)
+ if result == nil {
+ return ""
+ }
+ return result.Text
+}
+
+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 {
return e.hwInfo
+}
+
+func (e *xasrStreamingEngine) HardwareInfo() string {
+ return e.hwInfo
+}
+
+func (e *xasrStreamingEngine) ReleaseTailCaptureDelay() time.Duration {
+ return xasrReleaseTailDelay
+}
+
+func (e *xasrStreamingEngine) HoldPreCaptureEnabled() bool {
+ return true
}
func (e *sherpaEngine) Close() {
@@ -137,3 +337,13 @@
e.recognizer = nil
}
}
+
+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