package engine
|
|
import (
|
"fmt"
|
"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()
|
}
|
|
// 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.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)
|
}
|