Ariver
2026-07-02 0ffbf1935c9d091cce22a5583275ac0902f7a693
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package engine
 
import (
    "fmt"
    "time"
    "voicesnap/internal/config"
    "voicesnap/internal/language"
    "voicesnap/internal/logger"
    "voicesnap/internal/model"
    "voicesnap/internal/modelselection"
    "voicesnap/internal/paths"
)
 
// Engine is the interface for ASR engines.
type Engine interface {
    // Recognize takes float32 PCM samples (16kHz mono) and returns the recognized text.
    Recognize(samples []float32) (string, error)
    // 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()
}
 
// ModelDir returns the path to the sensevoice model directory.
func ModelDir() string {
    resolved, err := resolveCurrentModel()
    if err == nil && resolved.IsUsable() {
        return resolved.RootDir
    }
    return paths.ModelDir()
}
 
// ModelPath returns the path to the ONNX model file (prefers int8).
func ModelPath() string {
    resolved, err := resolveCurrentModel()
    if err == nil && resolved.IsUsable() {
        return resolved.Files["model"]
    }
    return ""
}
 
// TokensPath returns the path to the tokens.txt file.
func TokensPath() string {
    resolved, err := resolveCurrentModel()
    if err == nil && resolved.IsUsable() {
        return resolved.Files["tokens"]
    }
    return ""
}
 
// ModelExists checks if both the model and tokens files exist.
func ModelExists() bool {
    resolved, err := resolveCurrentModel()
    if err != nil {
        return false
    }
    return resolved.IsUsable()
}
 
// New creates a new platform-specific ASR engine.
// Returns an error if the model files are not found.
func New() (Engine, error) {
    cfg, err := config.Load()
    if err != nil {
        logger.Error("Failed to load config for model selection: %v", err)
        cfg = config.Default()
    }
    current := modelselection.Resolve(cfg, language.NewSystemDetector())
    return NewWithModelID(current.ModelID)
}
 
func NewWithModelID(modelID string) (Engine, error) {
    resolved, err := model.ResolveModel(model.NormalizeModelID(modelID))
    if err != nil {
        return nil, err
    }
    return NewWithResolvedModel(resolved)
}
 
func NewWithResolvedModel(resolved model.ResolvedModel) (Engine, error) {
    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 !isSupportedBackend(resolved.BackendKind) {
        return nil, fmt.Errorf("unsupported backend kind: %s", resolved.BackendKind)
    }
 
    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 {
        cfg = config.Default()
    }
    current := modelselection.Resolve(cfg, language.NewSystemDetector())
    return model.ResolveModel(current.ModelID)
}