| | |
| | | "os" |
| | | "path/filepath" |
| | | "testing" |
| | | "time" |
| | | |
| | | "voicesnap/internal/model" |
| | | ) |
| | |
| | | } |
| | | } |
| | | |
| | | func TestOfflineConfigForQwen3ASRUsesQwen3Config(t *testing.T) { |
| | | func TestOfflineConfigForZipformerUsesPlainTransducer(t *testing.T) { |
| | | root := t.TempDir() |
| | | files := map[string]string{ |
| | | "conv_frontend": filepath.Join(root, "conv_frontend.onnx"), |
| | | "encoder": filepath.Join(root, "encoder.int8.onnx"), |
| | | "decoder": filepath.Join(root, "decoder.int8.onnx"), |
| | | "tokenizer": filepath.Join(root, "tokenizer"), |
| | | "encoder": filepath.Join(root, "encoder-epoch-99-avg-1.int8.onnx"), |
| | | "decoder": filepath.Join(root, "decoder-epoch-99-avg-1.onnx"), |
| | | "joiner": filepath.Join(root, "joiner-epoch-99-avg-1.int8.onnx"), |
| | | "tokens": filepath.Join(root, "tokens.txt"), |
| | | } |
| | | resolved := model.ResolvedModel{ |
| | | ModelID: model.Qwen3ASRModelID, |
| | | BackendKind: model.BackendQwen3ASR, |
| | | ModelID: model.JapaneseModelID, |
| | | BackendKind: model.BackendTransducer, |
| | | Profile: model.ModelProfile{ |
| | | DisplayName: "Qwen3-ASR", |
| | | BackendKind: model.BackendQwen3ASR, |
| | | DisplayName: "Zipformer Japanese", |
| | | BackendKind: model.BackendTransducer, |
| | | ProviderOrder: []string{"cpu"}, |
| | | NumThreads: 4, |
| | | NumThreads: 1, |
| | | }, |
| | | Files: files, |
| | | } |
| | |
| | | if err != nil { |
| | | t.Fatal(err) |
| | | } |
| | | if config.ModelConfig.Qwen3ASR.ConvFrontend != files["conv_frontend"] { |
| | | t.Fatalf("conv frontend = %q, want %q", config.ModelConfig.Qwen3ASR.ConvFrontend, files["conv_frontend"]) |
| | | if config.ModelConfig.Transducer.Encoder != files["encoder"] { |
| | | t.Fatalf("encoder = %q, want %q", config.ModelConfig.Transducer.Encoder, files["encoder"]) |
| | | } |
| | | if config.ModelConfig.Qwen3ASR.Encoder != files["encoder"] { |
| | | t.Fatalf("encoder = %q, want %q", config.ModelConfig.Qwen3ASR.Encoder, files["encoder"]) |
| | | if config.ModelConfig.Transducer.Decoder != files["decoder"] { |
| | | t.Fatalf("decoder = %q, want %q", config.ModelConfig.Transducer.Decoder, files["decoder"]) |
| | | } |
| | | if config.ModelConfig.Qwen3ASR.Decoder != files["decoder"] { |
| | | t.Fatalf("decoder = %q, want %q", config.ModelConfig.Qwen3ASR.Decoder, files["decoder"]) |
| | | if config.ModelConfig.Transducer.Joiner != files["joiner"] { |
| | | t.Fatalf("joiner = %q, want %q", config.ModelConfig.Transducer.Joiner, files["joiner"]) |
| | | } |
| | | if config.ModelConfig.Qwen3ASR.Tokenizer != files["tokenizer"] { |
| | | t.Fatalf("tokenizer = %q, want %q", config.ModelConfig.Qwen3ASR.Tokenizer, files["tokenizer"]) |
| | | if config.ModelConfig.Tokens != files["tokens"] { |
| | | t.Fatalf("tokens = %q, want %q", config.ModelConfig.Tokens, files["tokens"]) |
| | | } |
| | | if config.ModelConfig.Qwen3ASR.MaxTotalLen != 1024 { |
| | | t.Fatalf("max total len = %d, want 1024", config.ModelConfig.Qwen3ASR.MaxTotalLen) |
| | | } |
| | | if config.ModelConfig.Qwen3ASR.MaxNewTokens != 256 { |
| | | t.Fatalf("max new tokens = %d, want 256", config.ModelConfig.Qwen3ASR.MaxNewTokens) |
| | | } |
| | | if config.ModelConfig.Tokens != "" { |
| | | t.Fatalf("tokens = %q, want empty for qwen3-asr", config.ModelConfig.Tokens) |
| | | if config.ModelConfig.ModelType != "" { |
| | | t.Fatalf("model type = %q, want empty for plain transducer", config.ModelConfig.ModelType) |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | func TestRememberNonEmptyTextKeepsLastPartial(t *testing.T) { |
| | | got := rememberNonEmptyText("最后一句", " ") |
| | | if got != "最后一句" { |
| | | t.Fatalf("rememberNonEmptyText() = %q, want last non-empty text", got) |
| | | } |
| | | |
| | | got = rememberNonEmptyText("旧内容", "新内容") |
| | | if got != "新内容" { |
| | | t.Fatalf("rememberNonEmptyText() = %q, want new non-empty text", got) |
| | | } |
| | | } |
| | | |
| | | func TestXASRTailPaddingMatchesFlushWindow(t *testing.T) { |
| | | if xasrTailPaddingSamples != 24000 { |
| | | t.Fatalf("tail padding samples = %d, want 24000", xasrTailPaddingSamples) |
| | | } |
| | | if len(xasrTailPadding) != xasrTailPaddingSamples { |
| | | t.Fatalf("tail padding len = %d, want %d", len(xasrTailPadding), xasrTailPaddingSamples) |
| | | } |
| | | } |
| | | |
| | | func TestXASRHeadPaddingPreparesInitialContext(t *testing.T) { |
| | | if xasrHeadPaddingSamples != 4000 { |
| | | t.Fatalf("head padding samples = %d, want 4000", xasrHeadPaddingSamples) |
| | | } |
| | | input := []float32{0.1, -0.2, 0.3} |
| | | got := xasrSamplesWithHeadPadding(input) |
| | | |
| | | if len(got) != xasrHeadPaddingSamples+len(input) { |
| | | t.Fatalf("padded len = %d, want %d", len(got), xasrHeadPaddingSamples+len(input)) |
| | | } |
| | | for i := 0; i < xasrHeadPaddingSamples; i++ { |
| | | if got[i] != 0 { |
| | | t.Fatalf("head padding sample %d = %v, want 0", i, got[i]) |
| | | } |
| | | } |
| | | for i, want := range input { |
| | | if got[xasrHeadPaddingSamples+i] != want { |
| | | t.Fatalf("payload sample %d = %v, want %v", i, got[xasrHeadPaddingSamples+i], want) |
| | | } |
| | | } |
| | | } |
| | | |
| | | func TestXASRRequestsReleaseTailCaptureDelay(t *testing.T) { |
| | | got := (&xasrStreamingEngine{}).ReleaseTailCaptureDelay() |
| | | if got != 300*time.Millisecond { |
| | | t.Fatalf("ReleaseTailCaptureDelay() = %v, want 300ms", got) |
| | | } |
| | | } |
| | | |
| | | func TestSenseVoiceRequestsReleaseTailCaptureDelay(t *testing.T) { |
| | | got := (&sherpaEngine{backendKind: model.BackendSenseVoice}).ReleaseTailCaptureDelay() |
| | | if got != 100*time.Millisecond { |
| | | t.Fatalf("ReleaseTailCaptureDelay() = %v, want 100ms", got) |
| | | } |
| | | } |
| | | |
| | | func TestOtherSherpaModelsDoNotRequestReleaseTailCaptureDelay(t *testing.T) { |
| | | for _, backendKind := range []string{ |
| | | model.BackendMoonshine, |
| | | model.BackendTransducer, |
| | | model.BackendNemoTransducer, |
| | | model.BackendQwen3ASR, |
| | | } { |
| | | t.Run(backendKind, func(t *testing.T) { |
| | | got := (&sherpaEngine{backendKind: backendKind}).ReleaseTailCaptureDelay() |
| | | if got != 0 { |
| | | t.Fatalf("ReleaseTailCaptureDelay() = %v, want 0", got) |
| | | } |
| | | }) |
| | | } |
| | | } |
| | | |
| | | func TestXASREnablesHoldPreCapture(t *testing.T) { |
| | | if !(&xasrStreamingEngine{}).HoldPreCaptureEnabled() { |
| | | t.Fatal("X-ASR should enable hold pre-capture") |
| | | } |
| | | } |
| | | |
| | | func TestOfflineSherpaDoesNotEnableHoldPreCapture(t *testing.T) { |
| | | if _, ok := any(&sherpaEngine{}).(HoldPreCaptureEngine); ok { |
| | | t.Fatal("offline sherpa engine should not enable hold pre-capture") |
| | | } |
| | | } |
| | | |
| | | func TestXASRRealModelSmoke(t *testing.T) { |
| | | if os.Getenv("PRIVATEVOICE_XASR_SMOKE") != "1" { |
| | | t.Skip("set PRIVATEVOICE_XASR_SMOKE=1 to run the real X-ASR model smoke test") |