From 605a6b2b280c0b2d555f0e2fad56b289361ce29c Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 07 Jun 2026 11:28:52 +0800
Subject: [PATCH] Add X-ASR live captions
---
privatevoice.src/internal/engine/engine_darwin.go | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 103 insertions(+), 10 deletions(-)
diff --git a/privatevoice.src/internal/engine/engine_darwin.go b/privatevoice.src/internal/engine/engine_darwin.go
index d18dd48..3fc59e1 100755
--- a/privatevoice.src/internal/engine/engine_darwin.go
+++ b/privatevoice.src/internal/engine/engine_darwin.go
@@ -4,12 +4,21 @@
import (
"fmt"
+ "strings"
+ "sync"
"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
+ xasrTailPaddingSamples = asrSampleRate + asrSampleRate/2
+)
+
+var xasrTailPadding = make([]float32, xasrTailPaddingSamples)
type sherpaEngine struct {
recognizer *sherpa.OfflineRecognizer
@@ -19,6 +28,14 @@
type xasrStreamingEngine struct {
recognizer *sherpa.OnlineRecognizer
hwInfo string
+ mu sync.Mutex
+}
+
+type xasrStreamingSession struct {
+ engine *xasrStreamingEngine
+ stream *sherpa.OnlineStream
+ lastText string
+ finished bool
}
func newPlatformEngine(resolved model.ResolvedModel) (Engine, error) {
@@ -174,7 +191,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 +200,97 @@
}
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
+ }
+
+ 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 (e *sherpaEngine) HardwareInfo() string {
@@ -219,6 +309,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