| | |
| | | |
| | | import ( |
| | | "fmt" |
| | | "time" |
| | | "voicesnap/internal/config" |
| | | "voicesnap/internal/language" |
| | | "voicesnap/internal/logger" |
| | | "voicesnap/internal/model" |
| | | "voicesnap/internal/modelselection" |
| | | "voicesnap/internal/paths" |
| | | ) |
| | | |
| | |
| | | // 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() |
| | | } |
| | | |
| | |
| | | cfg, err := config.Load() |
| | | if err != nil { |
| | | logger.Error("Failed to load config for model selection: %v", err) |
| | | return NewWithModelID(model.DefaultModelID) |
| | | cfg = config.Default() |
| | | } |
| | | return NewWithModelID(cfg.SelectedModelID) |
| | | current := modelselection.Resolve(cfg, language.NewSystemDetector()) |
| | | return NewWithModelID(current.ModelID) |
| | | } |
| | | |
| | | func NewWithModelID(modelID string) (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 resolved.BackendKind != model.BackendSenseVoice { |
| | | if !isSupportedBackend(resolved.BackendKind) { |
| | | return nil, fmt.Errorf("unsupported backend kind: %s", resolved.BackendKind) |
| | | } |
| | | |
| | | logger.Info("Loading model %s from %s", resolved.ModelID, resolved.RootDir) |
| | | 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 { |
| | | return model.ResolveModel(model.DefaultModelID) |
| | | cfg = config.Default() |
| | | } |
| | | return model.ResolveModel(model.NormalizeModelID(cfg.SelectedModelID)) |
| | | current := modelselection.Resolve(cfg, language.NewSystemDetector()) |
| | | return model.ResolveModel(current.ModelID) |
| | | } |