| | |
| | | |
| | | import ( |
| | | "fmt" |
| | | "syscall" |
| | | "time" |
| | | "unsafe" |
| | | |
| | | "voicesnap/internal/logger" |
| | | "voicesnap/internal/model" |
| | | |
| | |
| | | |
| | | 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 |
| | |
| | | config := sherpa.OfflineRecognizerConfig{} |
| | | config.FeatConfig.SampleRate = 16000 |
| | | config.FeatConfig.FeatureDim = 80 |
| | | config.ModelConfig.SenseVoice.Model = resolved.Files["model"] |
| | | config.ModelConfig.SenseVoice.Model = modelPath |
| | | config.ModelConfig.SenseVoice.Language = resolved.Profile.LanguageParam |
| | | config.ModelConfig.SenseVoice.UseInverseTextNormalization = 1 |
| | | config.ModelConfig.Tokens = resolved.Files["tokens"] |
| | | config.ModelConfig.Tokens = tokensPath |
| | | config.ModelConfig.NumThreads = resolved.Profile.NumThreads |
| | | config.ModelConfig.Provider = p.provider |
| | | config.DecodingMethod = "greedy_search" |
| | |
| | | logger.Info("Engine initialized: %s", info) |
| | | return &sherpaEngine{ |
| | | recognizer: recognizer, |
| | | provider: p.provider, |
| | | hwInfo: info, |
| | | }, nil |
| | | } |
| | |
| | | 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 |
| | | } |
| | |
| | | return e.hwInfo |
| | | } |
| | | |
| | | func (e *sherpaEngine) Provider() string { |
| | | return e.provider |
| | | } |
| | | |
| | | func (e *sherpaEngine) ReleaseTailCaptureDelay() time.Duration { |
| | | return senseVoiceReleaseTailDelay |
| | | } |