//go:build windows package engine import ( "fmt" "syscall" "time" "unsafe" "voicesnap/internal/logger" "voicesnap/internal/model" _ "github.com/k2-fsa/sherpa-onnx-go-windows" sherpa "github.com/k2-fsa/sherpa-onnx-go/sherpa_onnx" ) type sherpaEngine struct { recognizer *sherpa.OfflineRecognizer provider string hwInfo string } var ( kernel32 = syscall.NewLazyDLL("kernel32.dll") procGetShortPathNameW = kernel32.NewProc("GetShortPathNameW") ) func newPlatformEngine(resolved model.ResolvedModel) (Engine, error) { modelPath := windowsRuntimePath(resolved.Files["model"]) tokensPath := windowsRuntimePath(resolved.Files["tokens"]) if modelPath != resolved.Files["model"] || tokensPath != resolved.Files["tokens"] { logger.Info("Using Windows short paths for sherpa runtime model files") } // Try DirectML first, then CPU fallback providers := []struct { name string provider string }{ {"DirectML (GPU)", "directml"}, {"CPU", "cpu"}, } for _, p := range providers { config := sherpa.OfflineRecognizerConfig{} config.FeatConfig.SampleRate = 16000 config.FeatConfig.FeatureDim = 80 config.ModelConfig.SenseVoice.Model = modelPath config.ModelConfig.SenseVoice.Language = resolved.Profile.LanguageParam config.ModelConfig.SenseVoice.UseInverseTextNormalization = 1 config.ModelConfig.Tokens = tokensPath config.ModelConfig.NumThreads = resolved.Profile.NumThreads config.ModelConfig.Provider = p.provider config.DecodingMethod = "greedy_search" recognizer := sherpa.NewOfflineRecognizer(&config) if recognizer != nil { info := fmt.Sprintf("SenseVoice ยท %s", p.name) logger.Info("Engine initialized: %s", info) return &sherpaEngine{ recognizer: recognizer, provider: p.provider, hwInfo: info, }, nil } logger.Info("Failed to init with %s, trying next provider", p.name) } return nil, fmt.Errorf("failed to initialize sherpa-onnx with any provider") } func windowsRuntimePath(path string) string { if path == "" { return path } if isASCII(path) { return path } short, ok := shortPathName(path) if !ok || short == "" { return path } return short } func isASCII(path string) bool { for _, r := range path { if r > 127 { return false } } return true } func shortPathName(path string) (string, bool) { pathPtr, err := syscall.UTF16PtrFromString(path) if err != nil { return "", false } buf := make([]uint16, 32768) ret, _, _ := procGetShortPathNameW.Call( uintptr(unsafe.Pointer(pathPtr)), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), ) if ret == 0 || ret >= uintptr(len(buf)) { return "", false } return syscall.UTF16ToString(buf[:int(ret)]), true } func (e *sherpaEngine) Recognize(samples []float32) (string, error) { totalStart := time.Now() streamStart := time.Now() stream := sherpa.NewOfflineStream(e.recognizer) streamMS := durationMS(time.Since(streamStart)) defer sherpa.DeleteOfflineStream(stream) acceptStart := time.Now() stream.AcceptWaveform(16000, samples) acceptMS := durationMS(time.Since(acceptStart)) decodeStart := time.Now() e.recognizer.Decode(stream) decodeMS := durationMS(time.Since(decodeStart)) resultStart := time.Now() result := stream.GetResult() resultMS := durationMS(time.Since(resultStart)) logger.Info( "PERF engine_recognize_detail backend=%q provider=%q engine=%q samples=%d audio_ms=%d create_stream_ms=%d accept_ms=%d decode_ms=%d result_ms=%d total_ms=%d", model.BackendSenseVoice, e.provider, e.hwInfo, len(samples), audioDurationMS(samples), streamMS, acceptMS, decodeMS, resultMS, durationMS(time.Since(totalStart)), ) return result.Text, nil } func (e *sherpaEngine) HardwareInfo() string { return e.hwInfo } func (e *sherpaEngine) Provider() string { return e.provider } func (e *sherpaEngine) ReleaseTailCaptureDelay() time.Duration { return senseVoiceReleaseTailDelay } func (e *sherpaEngine) Close() { if e.recognizer != nil { sherpa.DeleteOfflineRecognizer(e.recognizer) e.recognizer = nil } }