From 2305a6142a8af348bce94ce2a264e31820fd2d4e Mon Sep 17 00:00:00 2001 From: ciricc Date: Sat, 20 Sep 2025 15:56:48 +0300 Subject: [PATCH] feat: split context into stateful/stateless; add concurrency gate, added model context params, benchmarks, silence mode for the ggml --- bindings/go/.gitignore | 1 + bindings/go/Makefile | 7 + bindings/go/go.mod | 2 +- bindings/go/pkg/whisper/concurrency_gate.go | 58 ++ bindings/go/pkg/whisper/consts.go | 1 + .../go/pkg/whisper/context_benchmark_test.go | 239 +++++++ bindings/go/pkg/whisper/context_test.go | 635 +++++++++++------- bindings/go/pkg/whisper/log.go | 9 + bindings/go/pkg/whisper/model.go | 78 ++- .../go/pkg/whisper/model_context_params.go | 27 + .../{context.go => stateful_context.go} | 2 + .../go/pkg/whisper/stateful_context_test.go | 81 +++ bindings/go/pkg/whisper/stateless_context.go | 377 +++++++++++ .../go/pkg/whisper/stateless_context_test.go | 52 ++ bindings/go/pkg/whisper/test_helpers_test.go | 129 ++++ bindings/go/pkg/whisper/util_test.go | 10 + bindings/go/whisper.go | 81 +++ bindings/go/whisper_test.go | 5 + .../download/golang.org/x/tools/gopls/@v/list | 1 + .../golang.org/x/tools/gopls/@v/v0.20.0.info | 1 + .../golang.org/x/tools/gopls/@v/v0.20.0.mod | 32 + .../lookup/golang.org/x/tools/gopls@v0.20.0 | 9 + .../sumdb/sum.golang.org/tile/8/0/x161/441 | Bin 0 -> 8192 bytes .../sum.golang.org/tile/8/0/x170/113.p/24 | Bin 0 -> 768 bytes .../sumdb/sum.golang.org/tile/8/1/630 | Bin 0 -> 8192 bytes .../sumdb/sum.golang.org/tile/8/1/664.p/129 | Bin 0 -> 4128 bytes .../sumdb/sum.golang.org/tile/8/2/002.p/152 | Bin 0 -> 4864 bytes .../sumdb/sum.golang.org/tile/8/3/000.p/2 | 3 + pkg/sumdb/sum.golang.org/latest | 5 + 29 files changed, 1578 insertions(+), 267 deletions(-) create mode 100644 bindings/go/pkg/whisper/concurrency_gate.go create mode 100644 bindings/go/pkg/whisper/context_benchmark_test.go create mode 100644 bindings/go/pkg/whisper/log.go create mode 100644 bindings/go/pkg/whisper/model_context_params.go rename bindings/go/pkg/whisper/{context.go => stateful_context.go} (98%) create mode 100644 bindings/go/pkg/whisper/stateful_context_test.go create mode 100644 bindings/go/pkg/whisper/stateless_context.go create mode 100644 bindings/go/pkg/whisper/stateless_context_test.go create mode 100644 bindings/go/pkg/whisper/test_helpers_test.go create mode 100644 pkg/mod/cache/download/golang.org/x/tools/gopls/@v/list create mode 100644 pkg/mod/cache/download/golang.org/x/tools/gopls/@v/v0.20.0.info create mode 100644 pkg/mod/cache/download/golang.org/x/tools/gopls/@v/v0.20.0.mod create mode 100644 pkg/mod/cache/download/sumdb/sum.golang.org/lookup/golang.org/x/tools/gopls@v0.20.0 create mode 100644 pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x161/441 create mode 100644 pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x170/113.p/24 create mode 100644 pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/630 create mode 100644 pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/664.p/129 create mode 100644 pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/2/002.p/152 create mode 100644 pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/3/000.p/2 create mode 100644 pkg/sumdb/sum.golang.org/latest diff --git a/bindings/go/.gitignore b/bindings/go/.gitignore index 20a1b6e48..bfdd3e3c7 100644 --- a/bindings/go/.gitignore +++ b/bindings/go/.gitignore @@ -1,3 +1,4 @@ build models samples/a13.wav +samples/benchmark_out.wav diff --git a/bindings/go/Makefile b/bindings/go/Makefile index c9ab66255..fb57d0fc9 100644 --- a/bindings/go/Makefile +++ b/bindings/go/Makefile @@ -46,6 +46,13 @@ endif examples: $(EXAMPLES_DIR) +benchmark: model-small whisper modtidy +ifeq ($(UNAME_S),Darwin) + @C_INCLUDE_PATH=${INCLUDE_PATH} LIBRARY_PATH=${LIBRARY_PATH} GGML_METAL_PATH_RESOURCES=${GGML_METAL_PATH_RESOURCES} go test -ldflags "-extldflags '$(EXT_LDFLAGS)'" -bench=BenchmarkContextProcess -benchmem -run '^$$' ./pkg/whisper/... +else + @C_INCLUDE_PATH=${INCLUDE_PATH} LIBRARY_PATH=${LIBRARY_PATH} go test -benchmem -run '^$$' ./pkg/whisper/... +endif + model-small: mkdir examples/go-model-download @${BUILD_DIR}/go-model-download -out models ggml-small.en.bin diff --git a/bindings/go/go.mod b/bindings/go/go.mod index 7c92c7b48..5cfd3268a 100644 --- a/bindings/go/go.mod +++ b/bindings/go/go.mod @@ -3,13 +3,13 @@ module github.com/ggerganov/whisper.cpp/bindings/go go 1.23 require ( + github.com/go-audio/audio v1.0.0 github.com/go-audio/wav v1.1.0 github.com/stretchr/testify v1.9.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-audio/audio v1.0.0 // indirect github.com/go-audio/riff v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/bindings/go/pkg/whisper/concurrency_gate.go b/bindings/go/pkg/whisper/concurrency_gate.go new file mode 100644 index 000000000..03469a29c --- /dev/null +++ b/bindings/go/pkg/whisper/concurrency_gate.go @@ -0,0 +1,58 @@ +package whisper + +import ( + "sync" + "sync/atomic" + + // Bindings + whisper "github.com/ggerganov/whisper.cpp/bindings/go" +) + +// Gate provides a simple acquire/release contract per key. +// The default implementation is a single-entry lock per key (limit=1). +type Gate interface { + // Acquire returns true if the key was acquired; false if already held + Acquire(key any) bool + // Release releases the key if currently held + Release(key any) +} + +// singleFlightGate is a minimal lock with limit=1 per key +type singleFlightGate struct { + m sync.Map // key -> *int32 (0 available, 1 held) +} + +func (g *singleFlightGate) Acquire(key any) bool { + ptr, _ := g.m.LoadOrStore(key, new(int32)) + busy := ptr.(*int32) + return atomic.CompareAndSwapInt32(busy, 0, 1) +} + +func (g *singleFlightGate) Release(key any) { + if v, ok := g.m.Load(key); ok { + atomic.StoreInt32(v.(*int32), 0) + } +} + +var defaultGate Gate = &singleFlightGate{} + +// SetGate allows applications to override the default gate (e.g., for custom policies) +// Passing nil resets to the default singleFlightGate. +func SetGate(g Gate) { + if g == nil { + defaultGate = &singleFlightGate{} + return + } + defaultGate = g +} + +func gate() Gate { return defaultGate } + +// modelKey derives a stable key per underlying model context for guarding stateless ops +func modelKey(model *ModelContext) *whisper.Context { + if model == nil || model.ctxAccessor() == nil { + return nil + } + ctx, _ := model.ctxAccessor().context() + return ctx +} diff --git a/bindings/go/pkg/whisper/consts.go b/bindings/go/pkg/whisper/consts.go index eab223ce5..fbdd9c310 100644 --- a/bindings/go/pkg/whisper/consts.go +++ b/bindings/go/pkg/whisper/consts.go @@ -20,6 +20,7 @@ var ( ErrUnsupportedLanguage = errors.New("unsupported language") ErrModelNotMultilingual = errors.New("model is not multilingual") ErrModelClosed = errors.Join(errors.New("model has been closed"), ErrInternalAppError) + ErrStatelessBusy = errors.New("stateless context is busy; concurrent processing not supported") // Private errors errParametersRequired = errors.New("parameters are required") diff --git a/bindings/go/pkg/whisper/context_benchmark_test.go b/bindings/go/pkg/whisper/context_benchmark_test.go new file mode 100644 index 000000000..04cf5c897 --- /dev/null +++ b/bindings/go/pkg/whisper/context_benchmark_test.go @@ -0,0 +1,239 @@ +package whisper_test + +import ( + "fmt" + "math" + "os" + "runtime" + "testing" + "time" + + whisper "github.com/ggerganov/whisper.cpp/bindings/go/pkg/whisper" + "github.com/go-audio/audio" + wav "github.com/go-audio/wav" +) + +// benchProcessVariants runs the common benchmark matrix across context kinds, +// thread sets, and callback modes, for given samples. If singleIteration is true +// it runs only one iteration regardless of b.N. If printTimings is true, +// model timings and custom ms_process metric are reported for NoCallback runs. +func benchProcessVariants( + b *testing.B, + samples []float32, + singleIteration bool, + printTimings bool, + useGPU bool, +) { + threadSets := []uint{1, 2, 4, uint(runtime.NumCPU())} + + device := "cpu" + if useGPU { + device = "gpu" + } + + // Initialize model per device mode + mp := whisper.NewModelContextParams() + mp.SetUseGPU(useGPU) + model, err := whisper.NewModelContextWithParams(ModelPath, mp) + if err != nil { + b.Fatalf("load model (%s): %v", device, err) + } + defer func() { _ = model.Close() }() + + // Context kinds: stateless and stateful + ctxKinds := []struct { + name string + new func() (whisper.Context, error) + }{ + { + name: "stateless", + new: func() (whisper.Context, error) { + params, err := whisper.NewParameters(model, whisper.SAMPLING_GREEDY, func(p *whisper.Parameters) {}) + if err != nil { + return nil, err + } + return whisper.NewStatelessContext(model, params) + }, + }, + { + name: "stateful", + new: func() (whisper.Context, error) { + params, err := whisper.NewParameters(model, whisper.SAMPLING_GREEDY, nil) + if err != nil { + return nil, err + } + return whisper.NewStatefulContext(model, params) + }, + }, + } + + for _, kind := range ctxKinds { + b.Run(device+"/"+kind.name, func(b *testing.B) { + for _, threads := range threadSets { + b.Run(fmt.Sprintf("threads=%d/NoCallback", threads), func(b *testing.B) { + b.ReportAllocs() + b.SetBytes(int64(len(samples) * 4)) + ctx, err := kind.new() + if err != nil { + b.Fatalf("new %s context: %v", kind.name, err) + } + defer func() { _ = ctx.Close() }() + ctx.SetThreads(threads) + + iters := b.N + if singleIteration { + iters = 1 + } + + b.ResetTimer() + for i := 0; i < iters; i++ { + if printTimings { + model.ResetTimings() + } + start := time.Now() + if err := ctx.Process(samples, nil, nil, nil); err != nil { + b.Fatalf("process: %v", err) + } + if printTimings { + elapsed := time.Since(start) + model.PrintTimings() + b.ReportMetric(float64(elapsed.Milliseconds()), "ms_process") + } + } + }) + + b.Run(fmt.Sprintf("threads=%d/WithSegmentCallback", threads), func(b *testing.B) { + b.ReportAllocs() + b.SetBytes(int64(len(samples) * 4)) + ctx, err := kind.new() + if err != nil { + b.Fatalf("new %s context: %v", kind.name, err) + } + defer func() { _ = ctx.Close() }() + ctx.SetThreads(threads) + + iters := b.N + if singleIteration { + iters = 1 + } + + b.ResetTimer() + for i := 0; i < iters; i++ { + start := time.Now() + // Passing a segment callback forces single-segment mode and exercises token extraction + if err := ctx.Process(samples, nil, func(seg whisper.Segment) {}, nil); err != nil { + b.Fatalf("process with callback: %v", err) + } + if printTimings { + elapsed := time.Since(start) + b.ReportMetric(float64(elapsed.Milliseconds()), "ms_process") + } + } + }) + } + }) + } +} + +// BenchmarkContextProcess runs the high-level Context.Process across +// different thread counts, with and without segment callbacks. +func BenchmarkContextProcessCPU(b *testing.B) { + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + b.Skipf("model not found: %s", ModelPath) + } + if _, err := os.Stat(SamplePath); os.IsNotExist(err) { + b.Skipf("sample not found: %s", SamplePath) + } + + // Load audio once (reuse helper) + data := helperLoadSample(b, SamplePath) + + benchProcessVariants(b, data, false, true, false) +} + +// BenchmarkContextProcessBig runs one single iteration over a big input +// (the short sample concatenated 10x) to simulate long audio processing. +// This is complementary to BenchmarkContextProcess which runs many iterations +// over the short sample. +func BenchmarkContextProcessBigCPU(b *testing.B) { + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + b.Skipf("model not found: %s", ModelPath) + } + if _, err := os.Stat(SamplePath); os.IsNotExist(err) { + b.Skipf("sample not found: %s", SamplePath) + } + + // Load audio once (reuse helper with meta) + data, sampleRate, numChans := helperLoadSampleWithMeta(b, SamplePath) + + // Build big dataset: input concatenated 10x + bigData := make([]float32, len(data)*10) + for i := 0; i < 10; i++ { + copy(bigData[i*len(data):(i+1)*len(data)], data) + } + + // Write the big dataset to a wav file for inspection + outPath := "../../samples/benchmark_out.wav" + fout, err := os.Create(outPath) + if err != nil { + b.Fatalf("create output wav: %v", err) + } + enc := wav.NewEncoder(fout, sampleRate, 16, numChans, 1) + intBuf := &audio.IntBuffer{ + Format: &audio.Format{NumChannels: numChans, SampleRate: sampleRate}, + SourceBitDepth: 16, + Data: make([]int, len(bigData)), + } + for i, s := range bigData { + v := int(math.Round(float64(s) * 32767.0)) + if v > 32767 { + v = 32767 + } else if v < -32768 { + v = -32768 + } + intBuf.Data[i] = v + } + if err := enc.Write(intBuf); err != nil { + _ = fout.Close() + b.Fatalf("encode wav: %v", err) + } + if err := enc.Close(); err != nil { + _ = fout.Close() + b.Fatalf("close encoder: %v", err) + } + _ = fout.Close() + + benchProcessVariants(b, bigData, true, true, false) +} + +// GPU variants reuse model-level GPU enablement via model params +func BenchmarkContextProcessGPU(b *testing.B) { + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + b.Skipf("model not found: %s", ModelPath) + } + if _, err := os.Stat(SamplePath); os.IsNotExist(err) { + b.Skipf("sample not found: %s", SamplePath) + } + + data := helperLoadSample(b, SamplePath) + + benchProcessVariants(b, data, false, true, true) +} + +func BenchmarkContextProcessBigGPU(b *testing.B) { + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + b.Skipf("model not found: %s", ModelPath) + } + if _, err := os.Stat(SamplePath); os.IsNotExist(err) { + b.Skipf("sample not found: %s", SamplePath) + } + + data, _, _ := helperLoadSampleWithMeta(b, SamplePath) + + bigData := make([]float32, len(data)*10) + for i := 0; i < 10; i++ { + copy(bigData[i*len(data):(i+1)*len(data)], data) + } + + benchProcessVariants(b, bigData, true, true, true) +} diff --git a/bindings/go/pkg/whisper/context_test.go b/bindings/go/pkg/whisper/context_test.go index a56d355ae..f18238ba5 100644 --- a/bindings/go/pkg/whisper/context_test.go +++ b/bindings/go/pkg/whisper/context_test.go @@ -3,11 +3,9 @@ package whisper_test import ( "io" "os" - "sync" "testing" "github.com/ggerganov/whisper.cpp/bindings/go/pkg/whisper" - "github.com/go-audio/wav" assert "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -15,115 +13,163 @@ import ( func TestSetLanguage(t *testing.T) { assert := assert.New(t) - model, err := whisper.New(ModelPath) - assert.NoError(err) - assert.NotNil(model) - defer func() { _ = model.Close() }() + cases := []struct { + name string + new func(t *testing.T) (whisper.Context, func()) + }{ + {name: "stateless", new: helperNewStatelessContext}, + {name: "stateful", new: helperNewStatefulContext}, + } - context, err := model.NewContext() - assert.NoError(err) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx, cleanup := tc.new(t) + defer cleanup() - // This returns an error since - // the model 'models/ggml-small.en.bin' - // that is loaded is not multilingual - err = context.SetLanguage("en") - assert.Error(err) + // This returns an error since the small.en model is not multilingual + err := ctx.SetLanguage("en") + assert.Error(err) + }) + } } func TestContextModelIsMultilingual(t *testing.T) { assert := assert.New(t) - model, err := whisper.New(ModelPath) - assert.NoError(err) - assert.NotNil(model) - defer func() { _ = model.Close() }() + cases := []struct { + name string + new func(t *testing.T) (whisper.Context, func()) + }{ + {name: "stateless", new: helperNewStatelessContext}, + {name: "stateful", new: helperNewStatefulContext}, + } - context, err := model.NewContext() - assert.NoError(err) - - isMultilingual := context.IsMultilingual() - - // This returns false since - // the model 'models/ggml-small.en.bin' - // that is loaded is not multilingual - assert.False(isMultilingual) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx, cleanup := tc.new(t) + defer cleanup() + assert.False(ctx.IsMultilingual()) + }) + } } func TestLanguage(t *testing.T) { assert := assert.New(t) - model, err := whisper.New(ModelPath) - assert.NoError(err) - assert.NotNil(model) - defer func() { _ = model.Close() }() + cases := []struct { + name string + new func(t *testing.T) (whisper.Context, func()) + }{ + {name: "stateless", new: helperNewStatelessContext}, + {name: "stateful", new: helperNewStatefulContext}, + } - context, err := model.NewContext() - assert.NoError(err) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx, cleanup := tc.new(t) + defer cleanup() + expectedLanguage := "en" + actualLanguage := ctx.Language() + assert.Equal(expectedLanguage, actualLanguage) + }) + } +} - // This always returns en since - // the model 'models/ggml-small.en.bin' - // that is loaded is not multilingual - expectedLanguage := "en" - actualLanguage := context.Language() - assert.Equal(expectedLanguage, actualLanguage) +// Generic behavior: Language() and DetectedLanguage() match for both context types +func TestContext_Generic_LanguageAndDetectedLanguage(t *testing.T) { + assert := assert.New(t) + + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", ModelPath) + } + if _, err := os.Stat(SamplePath); os.IsNotExist(err) { + t.Skip("Skipping test, sample not found:", SamplePath) + } + + data := helperLoadSample(t, SamplePath) + + cases := []struct { + name string + new func(t *testing.T) (whisper.Context, func()) + }{ + {name: "stateless", new: helperNewStatelessContext}, + {name: "stateful", new: helperNewStatefulContext}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx, cleanup := tc.new(t) + defer cleanup() + + langBefore := ctx.Language() + assert.NoError(ctx.Process(data, nil, nil, nil)) + detected := ctx.DetectedLanguage() + assert.Equal(langBefore, detected) + }) + } } func TestProcess(t *testing.T) { assert := assert.New(t) - fh, err := os.Open(SamplePath) - assert.NoError(err) - defer func() { _ = fh.Close() }() + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", ModelPath) + } + if _, err := os.Stat(SamplePath); os.IsNotExist(err) { + t.Skip("Skipping test, sample not found:", SamplePath) + } - // Decode the WAV file - load the full buffer - dec := wav.NewDecoder(fh) - buf, err := dec.FullPCMBuffer() - assert.NoError(err) - assert.Equal(uint16(1), dec.NumChans) + data := helperLoadSample(t, SamplePath) - data := buf.AsFloat32Buffer().Data + cases := []struct { + name string + new func(t *testing.T) (whisper.Context, func()) + }{ + {name: "stateless", new: helperNewStatelessContext}, + {name: "stateful", new: helperNewStatefulContext}, + } - model, err := whisper.New(ModelPath) - assert.NoError(err) - assert.NotNil(model) - defer func() { _ = model.Close() }() - - context, err := model.NewContext() - assert.NoError(err) - - err = context.Process(data, nil, nil, nil) - assert.NoError(err) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx, cleanup := tc.new(t) + defer cleanup() + err := ctx.Process(data, nil, nil, nil) + assert.NoError(err) + }) + } } func TestDetectedLanguage(t *testing.T) { assert := assert.New(t) - fh, err := os.Open(SamplePath) - assert.NoError(err) - defer func() { _ = fh.Close() }() + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", ModelPath) + } + if _, err := os.Stat(SamplePath); os.IsNotExist(err) { + t.Skip("Skipping test, sample not found:", SamplePath) + } - // Decode the WAV file - load the full buffer - dec := wav.NewDecoder(fh) - buf, err := dec.FullPCMBuffer() - assert.NoError(err) - assert.Equal(uint16(1), dec.NumChans) + data := helperLoadSample(t, SamplePath) - data := buf.AsFloat32Buffer().Data + cases := []struct { + name string + new func(t *testing.T) (whisper.Context, func()) + }{ + {name: "stateless", new: helperNewStatelessContext}, + {name: "stateful", new: helperNewStatefulContext}, + } - model, err := whisper.New(ModelPath) - assert.NoError(err) - assert.NotNil(model) - defer func() { _ = model.Close() }() - - context, err := model.NewContext() - assert.NoError(err) - - err = context.Process(data, nil, nil, nil) - assert.NoError(err) - - expectedLanguage := "en" - actualLanguage := context.DetectedLanguage() - assert.Equal(expectedLanguage, actualLanguage) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx, cleanup := tc.new(t) + defer cleanup() + err := ctx.Process(data, nil, nil, nil) + assert.NoError(err) + expectedLanguage := "en" + actualLanguage := ctx.DetectedLanguage() + assert.Equal(expectedLanguage, actualLanguage) + }) + } } // TestContext_ConcurrentProcessing tests that multiple contexts can process concurrently @@ -138,113 +184,29 @@ func TestContext_ConcurrentProcessing(t *testing.T) { t.Skip("Skipping test, sample not found:", SamplePath) } - fh, err := os.Open(SamplePath) - assert.NoError(err) - defer func() { _ = fh.Close() }() + data := helperLoadSample(t, SamplePath) - dec := wav.NewDecoder(fh) - buf, err := dec.FullPCMBuffer() - assert.NoError(err) - assert.Equal(uint16(1), dec.NumChans) - data := buf.AsFloat32Buffer().Data - - model, err := whisper.New(ModelPath) - assert.NoError(err) - assert.NotNil(model) - defer func() { _ = model.Close() }() - - ctx, err := model.NewContext() - assert.NoError(err) - assert.NotNil(ctx) - defer func() { _ = ctx.Close() }() - - err = ctx.Process(data, nil, nil, nil) - assert.NoError(err) - - seg, err := ctx.NextSegment() - assert.NoError(err) - assert.NotEmpty(seg.Text) -} - -// TestContext_Parallel_DifferentInputs tests concurrent processing with different inputs -// This validates that each context maintains isolated state for concurrent processing -func TestContext_Parallel_DifferentInputs(t *testing.T) { - assert := assert.New(t) - - if _, err := os.Stat(ModelPath); os.IsNotExist(err) { - t.Skip("Skipping test, model not found:", ModelPath) - } - if _, err := os.Stat(SamplePath); os.IsNotExist(err) { - t.Skip("Skipping test, sample not found:", SamplePath) + cases := []struct { + name string + new func(t *testing.T) (whisper.Context, func()) + }{ + {name: "stateless", new: helperNewStatelessContext}, + {name: "stateful", new: helperNewStatefulContext}, } - fh, err := os.Open(SamplePath) - assert.NoError(err) - defer func() { _ = fh.Close() }() + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx, cleanup := tc.new(t) + defer cleanup() - dec := wav.NewDecoder(fh) - buf, err := dec.FullPCMBuffer() - assert.NoError(err) - assert.Equal(uint16(1), dec.NumChans) - data := buf.AsFloat32Buffer().Data - assert.Greater(len(data), 10) + err := ctx.Process(data, nil, nil, nil) + assert.NoError(err) - // Create half-sample (second half) - half := make([]float32, len(data)/2) - copy(half, data[len(data)/2:]) - - model, err := whisper.New(ModelPath) - assert.NoError(err) - assert.NotNil(model) - defer func() { _ = model.Close() }() - - ctx1, err := model.NewContext() - assert.NoError(err) - defer func() { _ = ctx1.Close() }() - ctx2, err := model.NewContext() - assert.NoError(err) - defer func() { _ = ctx2.Close() }() - - // Run in parallel - each context has isolated whisper_state - var wg sync.WaitGroup - var first1, first2 string - var e1, e2 error - - wg.Add(2) - - // No mutex needed because each context is isolated by whisper_state - go func() { - defer wg.Done() - e1 = ctx1.Process(data, nil, nil, nil) - if e1 == nil { - seg, err := ctx1.NextSegment() - if err == nil { - first1 = seg.Text - } else { - e1 = err - } - } - }() - - go func() { - defer wg.Done() - e2 = ctx2.Process(half, nil, nil, nil) - if e2 == nil { - seg, err := ctx2.NextSegment() - if err == nil { - first2 = seg.Text - } else { - e2 = err - } - } - }() - - wg.Wait() - assert.NoError(e1) - assert.NoError(e2) - assert.NotEmpty(first1) - assert.NotEmpty(first2) - assert.NotEqual(first1, first2, "first segments should differ for different inputs") + seg, err := ctx.NextSegment() + assert.NoError(err) + assert.NotEmpty(seg.Text) + }) + } } // TestContext_Close tests that Context.Close() properly frees resources @@ -256,53 +218,72 @@ func TestContext_Close(t *testing.T) { t.Skip("Skipping test, model not found:", ModelPath) } - model, err := whisper.New(ModelPath) - assert.NoError(err) - assert.NotNil(model) - defer func() { _ = model.Close() }() + cases := []struct { + name string + new func(t *testing.T) (whisper.Context, func()) + }{ + {name: "stateless", new: helperNewStatelessContext}, + {name: "stateful", new: helperNewStatefulContext}, + } - ctx, err := model.NewContext() - assert.NoError(err) - assert.NotNil(ctx) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx, cleanup := tc.new(t) + defer cleanup() - // Close the context - err = ctx.Close() - require.NoError(t, err) + // Close the context + err := ctx.Close() + require.NoError(t, err) - // Try to use closed context - should return errors - err = ctx.Process([]float32{0.1, 0.2, 0.3}, nil, nil, nil) - require.ErrorIs(t, err, whisper.ErrModelClosed) + // Try to use closed context - should return errors + err = ctx.Process([]float32{0.1, 0.2, 0.3}, nil, nil, nil) + require.ErrorIs(t, err, whisper.ErrModelClosed) + // TODO: remove this logic after deprecating the ErrInternalAppError + require.ErrorIs(t, err, whisper.ErrInternalAppError) - // TODO: remove this logic after deprecating the ErrInternalAppError - require.ErrorIs(t, err, whisper.ErrInternalAppError) + lang := ctx.DetectedLanguage() + require.Empty(t, lang) - lang := ctx.DetectedLanguage() - require.Empty(t, lang) + _, err = ctx.NextSegment() + assert.ErrorIs(err, whisper.ErrModelClosed) + // TODO: remove this logic after deprecating the ErrInternalAppError + assert.ErrorIs(err, whisper.ErrInternalAppError) - _, err = ctx.NextSegment() - assert.ErrorIs(err, whisper.ErrModelClosed) - - // TODO: remove this logic after deprecating the ErrInternalAppError - assert.ErrorIs(err, whisper.ErrInternalAppError) - - // Multiple closes should be safe - err = ctx.Close() - require.NoError(t, err) + // Multiple closes should be safe + err = ctx.Close() + require.NoError(t, err) + }) + } } func Test_Close_Context_of_Closed_Model(t *testing.T) { assert := assert.New(t) - model, err := whisper.New(ModelPath) - assert.NoError(err) - assert.NotNil(model) + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", ModelPath) + } - ctx, err := model.NewContext() - assert.NoError(err) - assert.NotNil(ctx) + t.Run("stateless", func(t *testing.T) { + model, err := whisper.NewModelContext(ModelPath) + assert.NoError(err) + defer func() { _ = model.Close() }() + params := helperNewParams(t, model, nil) + ctx, err := whisper.NewStatelessContext(model, params) + assert.NoError(err) + require.NoError(t, model.Close()) + require.NoError(t, ctx.Close()) + }) - require.NoError(t, model.Close()) - require.NoError(t, ctx.Close()) + t.Run("stateful", func(t *testing.T) { + model, err := whisper.NewModelContext(ModelPath) + assert.NoError(err) + defer func() { _ = model.Close() }() + params := helperNewParams(t, model, nil) + ctx, err := whisper.NewStatefulContext(model, params) + assert.NoError(err) + require.NoError(t, model.Close()) + require.NoError(t, ctx.Close()) + }) } func TestContext_VAD_And_Diarization_Params_DoNotPanic(t *testing.T) { @@ -315,15 +296,7 @@ func TestContext_VAD_And_Diarization_Params_DoNotPanic(t *testing.T) { t.Skip("Skipping test, sample not found:", SamplePath) } - fh, err := os.Open(SamplePath) - assert.NoError(err) - defer func() { _ = fh.Close() }() - - dec := wav.NewDecoder(fh) - buf, err := dec.FullPCMBuffer() - assert.NoError(err) - assert.Equal(uint16(1), dec.NumChans) - data := buf.AsFloat32Buffer().Data + data := helperLoadSample(t, SamplePath) model, err := whisper.NewModelContext(ModelPath) assert.NoError(err) @@ -352,15 +325,7 @@ func TestContext_VAD_And_Diarization_Params_DoNotPanic(t *testing.T) { } func TestDiarization_TwoSpeakers_Boundaries(t *testing.T) { - fh, err := os.Open(MultiSpeakerSamplePath) - require.NoError(t, err) - defer func() { _ = fh.Close() }() - - dec := wav.NewDecoder(fh) - buf, err := dec.FullPCMBuffer() - assert.Equal(t, uint16(1), dec.NumChans) - require.NoError(t, err) - data := buf.AsFloat32Buffer().Data + data := helperLoadSample(t, MultiSpeakerSamplePath) model, err := whisper.NewModelContext(ModelTinydiarizePath) require.NoError(t, err) @@ -426,29 +391,193 @@ func TestContext_SpeakerTurnNext_Field_Present(t *testing.T) { t.Skip("Skipping test, sample not found:", SamplePath) } - fh, err := os.Open(SamplePath) - assert.NoError(err) - defer func() { _ = fh.Close() }() + data := helperLoadSample(t, SamplePath) - dec := wav.NewDecoder(fh) - buf, err := dec.FullPCMBuffer() - assert.NoError(err) - assert.Equal(uint16(1), dec.NumChans) - data := buf.AsFloat32Buffer().Data + cases := []struct { + name string + new func(t *testing.T) (whisper.Context, func()) + }{ + {name: "stateless", new: helperNewStatelessContext}, + {name: "stateful", new: helperNewStatefulContext}, + } - model, err := whisper.New(ModelPath) - assert.NoError(err) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctx, cleanup := tc.new(t) + defer cleanup() + + err := ctx.Process(data, nil, nil, nil) + assert.NoError(err) + + seg, err := ctx.NextSegment() + assert.NoError(err) + t.Logf("SpeakerTurnNext: %v", seg.SpeakerTurnNext) + _ = seg.SpeakerTurnNext + }) + } +} + +// Ensure Process produces at least one segment for both stateless and stateful contexts +func TestContext_Process_ProducesSegments_BothKinds(t *testing.T) { + assert := assert.New(t) + + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", ModelPath) + } + if _, err := os.Stat(SamplePath); os.IsNotExist(err) { + t.Skip("Skipping test, sample not found:", SamplePath) + } + + data := helperLoadSample(t, SamplePath) + + // Stateless + stateless, cleanupS := helperNewStatelessContext(t) + defer cleanupS() + require.NoError(t, stateless.Process(data, nil, nil, nil)) + var statelessCount int + for { + _, err := stateless.NextSegment() + if err == io.EOF { + break + } + require.NoError(t, err) + statelessCount++ + } + assert.Greater(statelessCount, 0, "stateless should produce at least one segment") + + // Stateful + stateful, cleanupSt := helperNewStatefulContext(t) + defer cleanupSt() + require.NoError(t, stateful.Process(data, nil, nil, nil)) + var statefulCount int + for { + _, err := stateful.NextSegment() + if err == io.EOF { + break + } + require.NoError(t, err) + statefulCount++ + } + assert.Greater(statefulCount, 0, "stateful should produce at least one segment") +} + +// With temperature=0 (greedy), stateless and stateful should produce identical segments +func TestContext_Process_SameResults_TemperatureZero(t *testing.T) { + assert := assert.New(t) + + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", ModelPath) + } + if _, err := os.Stat(SamplePath); os.IsNotExist(err) { + t.Skip("Skipping test, sample not found:", SamplePath) + } + + data := helperLoadSample(t, SamplePath) + + // Use a single model to avoid environment differences + model, err := whisper.NewModelContext(ModelPath) + require.NoError(t, err) defer func() { _ = model.Close() }() - ctx, err := model.NewContext() - assert.NoError(err) - defer func() { _ = ctx.Close() }() + // Independent params with temperature=0 for determinism + p := helperNewParams(t, model, func(p *whisper.Parameters) { + p.SetTemperature(0) + p.SetThreads(1) + }) - err = ctx.Process(data, nil, nil, nil) - assert.NoError(err) + stateless, err := whisper.NewStatelessContext(model, p) + require.NoError(t, err) + defer func() { _ = stateless.Close() }() - seg, err := ctx.NextSegment() - assert.NoError(err) - t.Logf("SpeakerTurnNext: %v", seg.SpeakerTurnNext) - _ = seg.SpeakerTurnNext // ensure field exists and is readable + stateful, err := whisper.NewStatefulContext(model, p) + require.NoError(t, err) + defer func() { _ = stateful.Close() }() + + require.NoError(t, stateless.Process(data, nil, nil, nil)) + require.NoError(t, stateful.Process(data, nil, nil, nil)) + + // Collect segment texts + var segsStateless, segsStateful []string + for { + seg, err := stateless.NextSegment() + if err == io.EOF { + break + } + require.NoError(t, err) + segsStateless = append(segsStateless, seg.Text) + } + for { + seg, err := stateful.NextSegment() + if err == io.EOF { + break + } + require.NoError(t, err) + segsStateful = append(segsStateful, seg.Text) + } + + // Both should have at least one segment and be identical + require.Greater(t, len(segsStateless), 0) + require.Greater(t, len(segsStateful), 0) + assert.Equal(len(segsStateful), len(segsStateless)) + for i := range segsStateless { + assert.Equal(segsStateless[i], segsStateful[i], "segment %d text differs", i) + } +} + +// Model.GetTimings: stateless processing updates model timings (non-zero), +// stateful processing does not (zero timings) +func TestModel_GetTimings_Stateless_NonZero_Stateful_Zero(t *testing.T) { + assert := assert.New(t) + + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", ModelPath) + } + if _, err := os.Stat(SamplePath); os.IsNotExist(err) { + t.Skip("Skipping test, sample not found:", SamplePath) + } + + data := helperLoadSample(t, SamplePath) + + model, err := whisper.NewModelContext(ModelPath) + require.NoError(t, err) + defer func() { _ = model.Close() }() + + // Stateless should produce non-zero timings + t.Run("stateless", func(t *testing.T) { + model.ResetTimings() + params := helperNewParams(t, model, nil) + ctx, err := whisper.NewStatelessContext(model, params) + require.NoError(t, err) + defer func() { _ = ctx.Close() }() + + require.NoError(t, ctx.Process(data, nil, nil, nil)) + + timings, ok := model.GetTimings() + require.True(t, ok, "expected timings to be available after stateless processing") + nonZero := timings.SampleMS > 0 || timings.EncodeMS > 0 || timings.DecodeMS > 0 || timings.BatchdMS > 0 || timings.PromptMS > 0 + assert.True(nonZero, "expected at least one non-zero timing after stateless processing: %#v", timings) + }) + + // Stateful should keep model-level timings at zero + t.Run("stateful", func(t *testing.T) { + model.ResetTimings() + params := helperNewParams(t, model, nil) + ctx, err := whisper.NewStatefulContext(model, params) + require.NoError(t, err) + defer func() { _ = ctx.Close() }() + + require.NoError(t, ctx.Process(data, nil, nil, nil)) + + timings, ok := model.GetTimings() + // Expect timings present but all zero; if not present at all, treat as zero-equivalent + if ok { + assert.Equal(float32(0), timings.SampleMS) + assert.Equal(float32(0), timings.EncodeMS) + assert.Equal(float32(0), timings.DecodeMS) + assert.Equal(float32(0), timings.BatchdMS) + assert.Equal(float32(0), timings.PromptMS) + } else { + t.Log("timings not available for stateful processing; treating as zero") + } + }) } diff --git a/bindings/go/pkg/whisper/log.go b/bindings/go/pkg/whisper/log.go new file mode 100644 index 000000000..66eb0d5c7 --- /dev/null +++ b/bindings/go/pkg/whisper/log.go @@ -0,0 +1,9 @@ +package whisper + +import low "github.com/ggerganov/whisper.cpp/bindings/go" + +// DisableLogs disables all C-side logging from whisper.cpp and ggml. +// Call once early in your program before creating models/contexts. +func DisableLogs() { + low.DisableLogs() +} diff --git a/bindings/go/pkg/whisper/model.go b/bindings/go/pkg/whisper/model.go index a16d6a134..c22490fb8 100644 --- a/bindings/go/pkg/whisper/model.go +++ b/bindings/go/pkg/whisper/model.go @@ -5,7 +5,7 @@ import ( "os" // Bindings - whisper "github.com/ggerganov/whisper.cpp/bindings/go" + low "github.com/ggerganov/whisper.cpp/bindings/go" ) type ModelContext struct { @@ -17,27 +17,50 @@ type ModelContext struct { // Make sure model adheres to the interface var _ Model = (*ModelContext)(nil) +// Timings is a compact, high-level timing snapshot in milliseconds +type Timings struct { + SampleMS float32 + EncodeMS float32 + DecodeMS float32 + BatchdMS float32 + PromptMS float32 +} + // Deprecated: Use NewModelContext instead func New(path string) (Model, error) { return NewModelContext(path) } // NewModelContext creates a new model context + func NewModelContext( path string, +) (*ModelContext, error) { + return NewModelContextWithParams( + path, + NewModelContextParams(), + ) +} + +// NewModelContextWithParams creates a new model context with custom initialization params +func NewModelContextWithParams( + path string, + params ModelContextParams, ) (*ModelContext, error) { model := new(ModelContext) if _, err := os.Stat(path); err != nil { return nil, err - } else if ctx := whisper.Whisper_init(path); ctx == nil { - return nil, ErrUnableToLoadModel - } else { - model.ca = newCtxAccessor(ctx) - model.tokId = newTokenIdentifier(model.ca) - model.path = path } - // Return success + ctx := low.Whisper_init_with_params(path, params.toLow()) + if ctx == nil { + return nil, ErrUnableToLoadModel + } + + model.ca = newCtxAccessor(ctx) + model.tokId = newTokenIdentifier(model.ca) + model.path = path + return model, nil } @@ -75,9 +98,9 @@ func (model *ModelContext) Languages() []string { return nil } - result := make([]string, 0, whisper.Whisper_lang_max_id()) - for i := 0; i < whisper.Whisper_lang_max_id(); i++ { - str := whisper.Whisper_lang_str(i) + result := make([]string, 0, low.Whisper_lang_max_id()) + for i := 0; i < low.Whisper_lang_max_id(); i++ { + str := low.Whisper_lang_str(i) if ctx.Whisper_lang_id(str) >= 0 { result = append(result, str) } @@ -95,8 +118,8 @@ func (model *ModelContext) NewContext() (Context, error) { return nil, err } - // Return new context (now state-backed) - return NewStatefulContext( + // Return new context (stateless for backward compatibility with timings) + return NewStatelessContext( model, params, ) @@ -122,6 +145,35 @@ func (model *ModelContext) ResetTimings() { ctx.Whisper_reset_timings() } +// GetTimings returns a compact snapshot of model-level processing timings. +// +// Behavior notes: +// - Stateless contexts (created via ModelContext.NewContext or NewStatelessContext) +// update model-level timings during Process. After a stateless Process call, +// the returned timings are expected to be non-zero (ok == true). +// - Stateful contexts (created via NewStatefulContext) use a per-state backend +// and do not affect model-level timings. After a stateful Process call, +// the returned timings are expected to be zero values (fields equal 0) or +// the call may return ok == false depending on the underlying implementation. +// +// Use ResetTimings before measurement to clear previous values. +func (model *ModelContext) GetTimings() (Timings, bool) { + ctx, err := model.ca.context() + if err != nil { + return Timings{}, false + } + if t, ok := ctx.Whisper_get_timings_go(); ok { + return Timings{ + SampleMS: t.SampleMS, + EncodeMS: t.EncodeMS, + DecodeMS: t.DecodeMS, + BatchdMS: t.BatchdMS, + PromptMS: t.PromptMS, + }, true + } + return Timings{}, false +} + func (model *ModelContext) tokenIdentifier() *tokenIdentifier { return model.tokId } diff --git a/bindings/go/pkg/whisper/model_context_params.go b/bindings/go/pkg/whisper/model_context_params.go new file mode 100644 index 000000000..62733b9bb --- /dev/null +++ b/bindings/go/pkg/whisper/model_context_params.go @@ -0,0 +1,27 @@ +package whisper + +import ( + low "github.com/ggerganov/whisper.cpp/bindings/go" +) + +type ModelContextParams struct { + p low.ContextParams +} + +func NewModelContextParams() ModelContextParams { + return ModelContextParams{ + p: low.Whisper_context_default_params(), + } +} + +func (p *ModelContextParams) SetUseGPU(v bool) { + p.p.SetUseGPU(v) +} + +func (p *ModelContextParams) SetGPUDevice(n int) { + p.p.SetGPUDevice(n) +} + +func (p *ModelContextParams) toLow() low.ContextParams { + return p.p +} diff --git a/bindings/go/pkg/whisper/context.go b/bindings/go/pkg/whisper/stateful_context.go similarity index 98% rename from bindings/go/pkg/whisper/context.go rename to bindings/go/pkg/whisper/stateful_context.go index 4b178f964..08e04094c 100644 --- a/bindings/go/pkg/whisper/context.go +++ b/bindings/go/pkg/whisper/stateful_context.go @@ -392,4 +392,6 @@ func (context *StatefulContext) SetTranslate(v bool) { context.params.SetTranslate(v) } +// Make stateful context compatible with the old deprecated interface for +// the simple migration into multi-threaded processing. var _ Context = (*StatefulContext)(nil) diff --git a/bindings/go/pkg/whisper/stateful_context_test.go b/bindings/go/pkg/whisper/stateful_context_test.go new file mode 100644 index 000000000..0062aed10 --- /dev/null +++ b/bindings/go/pkg/whisper/stateful_context_test.go @@ -0,0 +1,81 @@ +package whisper_test + +import ( + "os" + "sync" + "testing" + + whisper "github.com/ggerganov/whisper.cpp/bindings/go/pkg/whisper" + assert "github.com/stretchr/testify/assert" +) + +// Stateful-specific: parallel processing supported +func TestContext_Parallel_DifferentInputs_Stateful(t *testing.T) { + assert := assert.New(t) + + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", ModelPath) + } + if _, err := os.Stat(SamplePath); os.IsNotExist(err) { + t.Skip("Skipping test, sample not found:", SamplePath) + } + + data := helperLoadSample(t, SamplePath) + assert.Greater(len(data), 10) + + // Create half-sample (second half) + half := make([]float32, len(data)/2) + copy(half, data[len(data)/2:]) + + model, err := whisper.NewModelContext(ModelPath) + assert.NoError(err) + defer func() { _ = model.Close() }() + + params1 := helperNewParams(t, model, nil) + params2 := helperNewParams(t, model, nil) + + ctx1, err := whisper.NewStatefulContext(model, params1) + assert.NoError(err) + defer func() { _ = ctx1.Close() }() + ctx2, err := whisper.NewStatefulContext(model, params2) + assert.NoError(err) + defer func() { _ = ctx2.Close() }() + + var wg sync.WaitGroup + var first1, first2 string + var e1, e2 error + wg.Add(2) + + go func() { + defer wg.Done() + e1 = ctx1.Process(data, nil, nil, nil) + if e1 == nil { + seg, err := ctx1.NextSegment() + if err == nil { + first1 = seg.Text + } else { + e1 = err + } + } + }() + + go func() { + defer wg.Done() + e2 = ctx2.Process(half, nil, nil, nil) + if e2 == nil { + seg, err := ctx2.NextSegment() + if err == nil { + first2 = seg.Text + } else { + e2 = err + } + } + }() + + wg.Wait() + assert.NoError(e1) + assert.NoError(e2) + assert.NotEmpty(first1) + assert.NotEmpty(first2) + assert.NotEqual(first1, first2, "first segments should differ for different inputs") +} diff --git a/bindings/go/pkg/whisper/stateless_context.go b/bindings/go/pkg/whisper/stateless_context.go new file mode 100644 index 000000000..7dbe8be29 --- /dev/null +++ b/bindings/go/pkg/whisper/stateless_context.go @@ -0,0 +1,377 @@ +package whisper + +import ( + "fmt" + "io" + "runtime" + "strings" + "time" + + // Bindings + whisper "github.com/ggerganov/whisper.cpp/bindings/go" +) + +type StatelessContext struct { + n int + model *ModelContext + params *Parameters + closed bool +} + +// NewStatelessContext creates a new stateless context backed by the model's context +func NewStatelessContext(model *ModelContext, params *Parameters) (*StatelessContext, error) { + if model == nil { + return nil, errModelRequired + } + + if params == nil { + return nil, errParametersRequired + } + + // Ensure model context is available + if _, err := model.ctxAccessor().context(); err != nil { + return nil, err + } + + c := new(StatelessContext) + c.model = model + c.params = params + + return c, nil +} + +// DetectedLanguage returns the detected language for the current context data +func (context *StatelessContext) DetectedLanguage() string { + if context.closed { + return "" + } + ctx, err := context.model.ctxAccessor().context() + if err != nil { + return "" + } + return whisper.Whisper_lang_str(ctx.Whisper_full_lang_id()) +} + +// Close marks the context as closed. +func (context *StatelessContext) Close() error { + context.closed = true + return nil +} + +// Params returns a high-level parameters wrapper +func (context *StatelessContext) Params() *Parameters { + return context.params +} + +// ResetTimings resets the model performance timing counters. +// Deprecated: Use Model.ResetTimings() instead - these are model-level performance metrics. +func (context *StatelessContext) ResetTimings() { + context.model.ResetTimings() +} + +// PrintTimings prints the model performance timings to stdout. +// Deprecated: Use Model.PrintTimings() instead - these are model-level performance metrics. +func (context *StatelessContext) PrintTimings() { + context.model.PrintTimings() +} + +// SystemInfo returns the system information +func (context *StatelessContext) SystemInfo() string { + return fmt.Sprintf("system_info: n_threads = %d / %d | %s\n", + context.params.Threads(), + runtime.NumCPU(), + whisper.Whisper_print_system_info(), + ) +} + +// Use mel data at offset_ms to try and auto-detect the spoken language +// Make sure to call whisper_pcm_to_mel() or whisper_set_mel() first. +// Returns the probabilities of all languages for this context. +func (context *StatelessContext) WhisperLangAutoDetect(offset_ms int, n_threads int) ([]float32, error) { + if context.closed { + return nil, ErrModelClosed + } + ctx, err := context.model.ctxAccessor().context() + if err != nil { + return nil, err + } + langProbs, err := ctx.Whisper_lang_auto_detect(offset_ms, n_threads) + if err != nil { + return nil, err + } + return langProbs, nil +} + +// Process new sample data and return any errors +func (context *StatelessContext) Process( + data []float32, + callEncoderBegin EncoderBeginCallback, + callNewSegment SegmentCallback, + callProgress ProgressCallback, +) error { + if context.closed { + return ErrModelClosed + } + ctx, err := context.model.ctxAccessor().context() + if err != nil { + return err + } + // Concurrency guard: prevent concurrent stateless processing on shared model ctx + k := modelKey(context.model) + if !gate().Acquire(k) { + return ErrStatelessBusy + } + defer gate().Release(k) + + // If the callback is defined then we force on single_segment mode + if callNewSegment != nil { + context.params.SetSingleSegment(true) + } + + lowLevelParams, err := context.params.unsafeParams() + if err != nil { + return err + } + + if err := ctx.Whisper_full(*lowLevelParams, data, callEncoderBegin, + func(new int) { + if callNewSegment != nil { + num_segments := ctx.Whisper_full_n_segments() + s0 := num_segments - new + for i := s0; i < num_segments; i++ { + callNewSegment(toSegmentFromContext(ctx, i)) + } + } + }, func(progress int) { + if callProgress != nil { + callProgress(progress) + } + }); err != nil { + return err + } + + // Return success + return nil +} + +// NextSegment returns the next segment from the context buffer +func (context *StatelessContext) NextSegment() (Segment, error) { + if context.closed { + return Segment{}, ErrModelClosed + } + ctx, err := context.model.ctxAccessor().context() + if err != nil { + return Segment{}, err + } + + if context.n >= ctx.Whisper_full_n_segments() { + return Segment{}, io.EOF + } + + result := toSegmentFromContext(ctx, context.n) + context.n++ + + return result, nil +} + +func (context *StatelessContext) IsMultilingual() bool { + return context.model.IsMultilingual() +} + +// Token helpers +// Deprecated: Use Model.IsText() instead - token checking is model-specific. +func (context *StatelessContext) IsText(t Token) bool { + result, _ := context.model.tokenIdentifier().IsText(t) + return result +} + +// Deprecated: Use Model.IsBEG() instead - token checking is model-specific. +func (context *StatelessContext) IsBEG(t Token) bool { + result, _ := context.model.tokenIdentifier().IsBEG(t) + return result +} + +// Deprecated: Use Model.IsSOT() instead - token checking is model-specific. +func (context *StatelessContext) IsSOT(t Token) bool { + result, _ := context.model.tokenIdentifier().IsSOT(t) + return result +} + +// Deprecated: Use Model.IsEOT() instead - token checking is model-specific. +func (context *StatelessContext) IsEOT(t Token) bool { + result, _ := context.model.tokenIdentifier().IsEOT(t) + return result +} + +// Deprecated: Use Model.IsPREV() instead - token checking is model-specific. +func (context *StatelessContext) IsPREV(t Token) bool { + result, _ := context.model.tokenIdentifier().IsPREV(t) + return result +} + +// Deprecated: Use Model.IsSOLM() instead - token checking is model-specific. +func (context *StatelessContext) IsSOLM(t Token) bool { + result, _ := context.model.tokenIdentifier().IsSOLM(t) + return result +} + +// Deprecated: Use Model.IsNOT() instead - token checking is model-specific. +func (context *StatelessContext) IsNOT(t Token) bool { + result, _ := context.model.tokenIdentifier().IsNOT(t) + return result +} + +func (context *StatelessContext) SetLanguage(lang string) error { + if context.closed || context.model.ctxAccessor().isClosed() { + return ErrModelClosed + } + + if !context.model.IsMultilingual() { + return ErrModelNotMultilingual + } + + return context.params.SetLanguage(lang) +} + +// Deprecated: Use Model.IsLANG() instead - token checking is model-specific. +func (context *StatelessContext) IsLANG(t Token, lang string) bool { + result, _ := context.model.tokenIdentifier().IsLANG(t, lang) + return result +} + +// Context-backed helper functions +func toSegmentFromContext(ctx *whisper.Context, n int) Segment { + return Segment{ + Num: n, + Text: strings.TrimSpace(ctx.Whisper_full_get_segment_text(n)), + Start: time.Duration(ctx.Whisper_full_get_segment_t0(n)) * time.Millisecond * 10, + End: time.Duration(ctx.Whisper_full_get_segment_t1(n)) * time.Millisecond * 10, + Tokens: toTokensFromContext(ctx, n), + SpeakerTurnNext: false, // speaker turn available only with state-backed accessors + } +} + +func toTokensFromContext(ctx *whisper.Context, n int) []Token { + result := make([]Token, ctx.Whisper_full_n_tokens(n)) + + for i := 0; i < len(result); i++ { + data := ctx.Whisper_full_get_token_data(n, i) + result[i] = Token{ + Id: int(ctx.Whisper_full_get_token_id(n, i)), + Text: ctx.Whisper_full_get_token_text(n, i), + P: ctx.Whisper_full_get_token_p(n, i), + Start: time.Duration(data.T0()) * time.Millisecond * 10, + End: time.Duration(data.T1()) * time.Millisecond * 10, + } + } + + return result +} + +// Deprecated: Use Params().Language() instead +func (context *StatelessContext) Language() string { + return context.params.Language() +} + +// Deprecated: Use Params().SetAudioCtx() instead +func (context *StatelessContext) SetAudioCtx(n uint) { + context.params.SetAudioCtx(n) +} + +// SetBeamSize implements Context. +// Deprecated: Use Params().SetBeamSize() instead +func (context *StatelessContext) SetBeamSize(v int) { + context.params.SetBeamSize(v) +} + +// SetDuration implements Context. +// Deprecated: Use Params().SetDuration() instead +func (context *StatelessContext) SetDuration(v time.Duration) { + context.params.SetDuration(v) +} + +// SetEntropyThold implements Context. +// Deprecated: Use Params().SetEntropyThold() instead +func (context *StatelessContext) SetEntropyThold(v float32) { + context.params.SetEntropyThold(v) +} + +// SetInitialPrompt implements Context. +// Deprecated: Use Params().SetInitialPrompt() instead +func (context *StatelessContext) SetInitialPrompt(v string) { + context.params.SetInitialPrompt(v) +} + +// SetMaxContext implements Context. +// Deprecated: Use Params().SetMaxContext() instead +func (context *StatelessContext) SetMaxContext(v int) { + context.params.SetMaxContext(v) +} + +// SetMaxSegmentLength implements Context. +// Deprecated: Use Params().SetMaxSegmentLength() instead +func (context *StatelessContext) SetMaxSegmentLength(v uint) { + context.params.SetMaxSegmentLength(v) +} + +// SetMaxTokensPerSegment implements Context. +// Deprecated: Use Params().SetMaxTokensPerSegment() instead +func (context *StatelessContext) SetMaxTokensPerSegment(v uint) { + context.params.SetMaxTokensPerSegment(v) +} + +// SetOffset implements Context. +// Deprecated: Use Params().SetOffset() instead +func (context *StatelessContext) SetOffset(v time.Duration) { + context.params.SetOffset(v) +} + +// SetSplitOnWord implements Context. +// Deprecated: Use Params().SetSplitOnWord() instead +func (context *StatelessContext) SetSplitOnWord(v bool) { + context.params.SetSplitOnWord(v) +} + +// SetTemperature implements Context. +// Deprecated: Use Params().SetTemperature() instead +func (context *StatelessContext) SetTemperature(v float32) { + context.params.SetTemperature(v) +} + +// SetTemperatureFallback implements Context. +// Deprecated: Use Params().SetTemperatureFallback() instead +func (context *StatelessContext) SetTemperatureFallback(v float32) { + context.params.SetTemperatureFallback(v) +} + +// SetThreads implements Context. +// Deprecated: Use Params().SetThreads() instead +func (context *StatelessContext) SetThreads(v uint) { + context.params.SetThreads(v) +} + +// SetTokenSumThreshold implements Context. +// Deprecated: Use Params().SetTokenSumThreshold() instead +func (context *StatelessContext) SetTokenSumThreshold(v float32) { + context.params.SetTokenSumThreshold(v) +} + +// SetTokenThreshold implements Context. +// Deprecated: Use Params().SetTokenThreshold() instead +func (context *StatelessContext) SetTokenThreshold(v float32) { + context.params.SetTokenThreshold(v) +} + +// SetTokenTimestamps implements Context. +// Deprecated: Use Params().SetTokenTimestamps() instead +func (context *StatelessContext) SetTokenTimestamps(v bool) { + context.params.SetTokenTimestamps(v) +} + +// SetTranslate implements Context. +// Deprecated: Use Params().SetTranslate() instead +func (context *StatelessContext) SetTranslate(v bool) { + context.params.SetTranslate(v) +} + +var _ Context = (*StatelessContext)(nil) diff --git a/bindings/go/pkg/whisper/stateless_context_test.go b/bindings/go/pkg/whisper/stateless_context_test.go new file mode 100644 index 000000000..0eb867d19 --- /dev/null +++ b/bindings/go/pkg/whisper/stateless_context_test.go @@ -0,0 +1,52 @@ +package whisper_test + +import ( + "sync" + "testing" + + whisper "github.com/ggerganov/whisper.cpp/bindings/go/pkg/whisper" + assert "github.com/stretchr/testify/assert" +) + +// Ensure stateless contexts cannot process in parallel without isolation +func TestStatelessContext_NotParallelSafe(t *testing.T) { + data := helperLoadSample(t, SamplePath) + + model, closeModel := helperNewModelContext(t) + defer closeModel() + + params := helperNewParams(t, model, nil) + + // Create two stateless contexts sharing the same underlying model context + ctx1, err := whisper.NewStatelessContext(model, params) + assert.NoError(t, err) + defer func() { _ = ctx1.Close() }() + + ctx2, err := whisper.NewStatelessContext(model, params) + assert.NoError(t, err) + defer func() { _ = ctx2.Close() }() + + // Run both in parallel - expect a panic or error from underlying whisper_full + // We capture panics to assert the behavior. + var wg sync.WaitGroup + wg.Add(2) + + var err1, err2 error + + go func() { + defer wg.Done() + err1 = ctx1.Process(data, nil, nil, nil) + }() + + go func() { + defer wg.Done() + err2 = ctx2.Process(data, nil, nil, nil) + }() + + wg.Wait() + + // At least one should return ErrStatelessBusy + if err1 != whisper.ErrStatelessBusy && err2 != whisper.ErrStatelessBusy { + t.Fatalf("expected ErrStatelessBusy when processing in parallel with StatelessContext, got err1=%v err2=%v", err1, err2) + } +} diff --git a/bindings/go/pkg/whisper/test_helpers_test.go b/bindings/go/pkg/whisper/test_helpers_test.go new file mode 100644 index 000000000..15fedc961 --- /dev/null +++ b/bindings/go/pkg/whisper/test_helpers_test.go @@ -0,0 +1,129 @@ +package whisper_test + +import ( + "os" + "testing" + + whisper "github.com/ggerganov/whisper.cpp/bindings/go/pkg/whisper" + wav "github.com/go-audio/wav" +) + +func helperLoadSample(tb testing.TB, path string) []float32 { + tb.Helper() + fh, err := os.Open(path) + if err != nil { + tb.Fatalf("open sample: %v", err) + } + defer func() { _ = fh.Close() }() + + dec := wav.NewDecoder(fh) + buf, err := dec.FullPCMBuffer() + if err != nil { + tb.Fatalf("decode wav: %v", err) + } + if dec.NumChans != 1 { + tb.Fatalf("expected mono wav, got channels=%d", dec.NumChans) + } + return buf.AsFloat32Buffer().Data +} + +// helperLoadSampleWithMeta loads wav and returns samples with sample rate and channels +func helperLoadSampleWithMeta(tb testing.TB, path string) ([]float32, int, int) { + tb.Helper() + fh, err := os.Open(path) + if err != nil { + tb.Fatalf("open sample: %v", err) + } + defer func() { _ = fh.Close() }() + + dec := wav.NewDecoder(fh) + buf, err := dec.FullPCMBuffer() + if err != nil { + tb.Fatalf("decode wav: %v", err) + } + if dec.NumChans != 1 { + tb.Fatalf("expected mono wav, got channels=%d", dec.NumChans) + } + return buf.AsFloat32Buffer().Data, int(dec.SampleRate), int(dec.NumChans) +} + +func helperNewModel(t *testing.T) (whisper.Model, func()) { + t.Helper() + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", ModelPath) + } + model, err := whisper.New(ModelPath) + if err != nil { + t.Fatalf("load model: %v", err) + } + return model, func() { _ = model.Close() } +} + +func helperNewModelContext(t *testing.T) (*whisper.ModelContext, func()) { + t.Helper() + if _, err := os.Stat(ModelPath); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", ModelPath) + } + model, err := whisper.NewModelContext(ModelPath) + if err != nil { + t.Fatalf("load model ctx: %v", err) + } + return model, func() { _ = model.Close() } +} + +func helperNewParams(t *testing.T, model *whisper.ModelContext, configure whisper.ParamsConfigure) *whisper.Parameters { + t.Helper() + params, err := whisper.NewParameters(model, whisper.SAMPLING_GREEDY, configure) + if err != nil { + t.Fatalf("new params: %v", err) + } + return params +} + +func helperProcessOnce(t *testing.T, ctx whisper.Context, data []float32) { + t.Helper() + if err := ctx.Process(data, nil, nil, nil); err != nil { + t.Fatalf("process: %v", err) + } +} + +func helperFirstSegmentText(t *testing.T, ctx whisper.Context) string { + t.Helper() + seg, err := ctx.NextSegment() + if err != nil { + t.Fatalf("next segment: %v", err) + } + return seg.Text +} + +// helperNewStatelessContext creates a fresh stateless context and returns a cleanup func +func helperNewStatelessContext(t *testing.T) (whisper.Context, func()) { + t.Helper() + model, closeModel := helperNewModelContext(t) + params := helperNewParams(t, model, nil) + ctx, err := whisper.NewStatelessContext(model, params) + if err != nil { + t.Fatalf("new stateless context: %v", err) + } + cleanup := func() { + _ = ctx.Close() + closeModel() + } + return ctx, cleanup +} + +// helperNewStatefulContext creates a fresh stateful context and returns a cleanup func +func helperNewStatefulContext(t *testing.T) (whisper.Context, func()) { + t.Helper() + model, closeModel := helperNewModelContext(t) + params := helperNewParams(t, model, nil) + ctx, err := whisper.NewStatefulContext(model, params) + if err != nil { + t.Fatalf("new stateful context: %v", err) + } + cleanup := func() { + _ = ctx.Close() + closeModel() + } + return ctx, cleanup +} diff --git a/bindings/go/pkg/whisper/util_test.go b/bindings/go/pkg/whisper/util_test.go index 1b27255ae..a2fadca58 100644 --- a/bindings/go/pkg/whisper/util_test.go +++ b/bindings/go/pkg/whisper/util_test.go @@ -1,8 +1,18 @@ package whisper_test +import ( + "os" + "testing" +) + const ( ModelPath = "../../models/ggml-small.en.bin" ModelTinydiarizePath = "../../models/ggml-small.en-tdrz.bin" SamplePath = "../../samples/jfk.wav" MultiSpeakerSamplePath = "../../samples/a13.wav" ) + +func TestMain(m *testing.M) { + // whisper.DisableLogs() + os.Exit(m.Run()) +} diff --git a/bindings/go/whisper.go b/bindings/go/whisper.go index 83089a26f..023a33d26 100644 --- a/bindings/go/whisper.go +++ b/bindings/go/whisper.go @@ -15,6 +15,7 @@ import ( #cgo darwin LDFLAGS: -lggml-metal -lggml-blas #cgo darwin LDFLAGS: -framework Accelerate -framework Metal -framework Foundation -framework CoreGraphics #include +#include #include extern void callNewSegment(void* user_data, int new); @@ -60,6 +61,22 @@ static struct whisper_full_params whisper_full_default_params_cb(struct whisper_ params.progress_callback_user_data = (void*)(ctx); return params; } + +// Disable all C-side logging (whisper.cpp and ggml) +static void go_cb_log_disable(enum ggml_log_level level, const char * text, void * user_data) { + (void) level; (void) text; (void) user_data; +} + +static void whisper_log_disable_all(void) { + ggml_log_set(go_cb_log_disable, NULL); + whisper_log_set(go_cb_log_disable, NULL); +} + +// Enable default logging (stdout) for whisper.cpp and ggml +static void whisper_log_enable_default(void) { + ggml_log_set(NULL, NULL); + whisper_log_set(NULL, NULL); +} */ import "C" @@ -73,6 +90,8 @@ type ( TokenData C.struct_whisper_token_data SamplingStrategy C.enum_whisper_sampling_strategy Params C.struct_whisper_full_params + Timings C.struct_whisper_timings + ContextParams C.struct_whisper_context_params ) /////////////////////////////////////////////////////////////////////////////// @@ -98,6 +117,12 @@ var ( ErrInvalidLanguage = errors.New("invalid language") ) +// DisableLogs disables all logging coming from the C libraries (whisper.cpp and ggml). +// Call once early in program startup if you want to silence device/backend prints. +func DisableLogs() { + C.whisper_log_disable_all() +} + /////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS @@ -113,6 +138,36 @@ func Whisper_init(path string) *Context { } } +// Whisper_context_default_params returns default model context params +func Whisper_context_default_params() ContextParams { + return ContextParams(C.whisper_context_default_params()) +} + +// SetUseGPU enables or disables GPU acceleration on the model context (if available) +func (p *ContextParams) SetUseGPU(v bool) { + if v { + p.use_gpu = C.bool(true) + } else { + p.use_gpu = C.bool(false) + } +} + +// SetGPUDevice selects the GPU device index for the model context (CUDA) +func (p *ContextParams) SetGPUDevice(n int) { + p.gpu_device = C.int(n) +} + +// Whisper_init_with_params allocates and initializes a model using custom context params +func Whisper_init_with_params(path string, params ContextParams) *Context { + cPath := C.CString(path) + defer C.free(unsafe.Pointer(cPath)) + if ctx := C.whisper_init_from_file_with_params(cPath, (C.struct_whisper_context_params)(params)); ctx != nil { + return (*Context)(ctx) + } else { + return nil + } +} + // Frees all memory allocated by the model. func (ctx *Context) Whisper_free() { C.whisper_free((*C.struct_whisper_context)(ctx)) @@ -355,6 +410,32 @@ func (ctx *Context) Whisper_reset_timings() { C.whisper_reset_timings((*C.struct_whisper_context)(ctx)) } +// TimingsGo is a Go-friendly copy of whisper_timings +type TimingsGo struct { + SampleMS float32 + EncodeMS float32 + DecodeMS float32 + BatchdMS float32 + PromptMS float32 +} + +// Whisper_get_timings_go retrieves timing counters and converts them to TimingsGo +func (ctx *Context) Whisper_get_timings_go() (TimingsGo, bool) { + t := C.whisper_get_timings((*C.struct_whisper_context)(ctx)) + if t == nil { + return TimingsGo{}, false + } + // The C struct is 5 consecutive floats; reinterpret and copy + arr := (*[5]C.float)(unsafe.Pointer(t)) + return TimingsGo{ + SampleMS: float32(arr[0]), + EncodeMS: float32(arr[1]), + DecodeMS: float32(arr[2]), + BatchdMS: float32(arr[3]), + PromptMS: float32(arr[4]), + }, true +} + // Print system information func Whisper_print_system_info() string { return C.GoString(C.whisper_print_system_info()) diff --git a/bindings/go/whisper_test.go b/bindings/go/whisper_test.go index 23bbfbff0..330981fb4 100644 --- a/bindings/go/whisper_test.go +++ b/bindings/go/whisper_test.go @@ -19,6 +19,11 @@ const ( SamplePath = "samples/jfk.wav" ) +func TestMain(m *testing.M) { + whisper.DisableLogs() + os.Exit(m.Run()) +} + func Test_Whisper_000(t *testing.T) { assert := assert.New(t) if _, err := os.Stat(ModelPath); os.IsNotExist(err) { diff --git a/pkg/mod/cache/download/golang.org/x/tools/gopls/@v/list b/pkg/mod/cache/download/golang.org/x/tools/gopls/@v/list new file mode 100644 index 000000000..1847373e9 --- /dev/null +++ b/pkg/mod/cache/download/golang.org/x/tools/gopls/@v/list @@ -0,0 +1 @@ +v0.20.0 diff --git a/pkg/mod/cache/download/golang.org/x/tools/gopls/@v/v0.20.0.info b/pkg/mod/cache/download/golang.org/x/tools/gopls/@v/v0.20.0.info new file mode 100644 index 000000000..08057762f --- /dev/null +++ b/pkg/mod/cache/download/golang.org/x/tools/gopls/@v/v0.20.0.info @@ -0,0 +1 @@ +{"Version":"v0.20.0","Time":"2025-07-28T18:28:48Z","Origin":{"VCS":"git","URL":"https://go.googlesource.com/tools","Subdir":"gopls","Hash":"2e31135b736b96cd609904370c71563ce5447826","Ref":"refs/tags/gopls/v0.20.0"}} \ No newline at end of file diff --git a/pkg/mod/cache/download/golang.org/x/tools/gopls/@v/v0.20.0.mod b/pkg/mod/cache/download/golang.org/x/tools/gopls/@v/v0.20.0.mod new file mode 100644 index 000000000..47caff989 --- /dev/null +++ b/pkg/mod/cache/download/golang.org/x/tools/gopls/@v/v0.20.0.mod @@ -0,0 +1,32 @@ +module golang.org/x/tools/gopls + +go 1.24.2 + +require ( + github.com/fatih/gomodifytags v1.17.1-0.20250423142747-f3939df9aa3c + github.com/fsnotify/fsnotify v1.9.0 + github.com/google/go-cmp v0.7.0 + github.com/jba/templatecheck v0.7.1 + golang.org/x/mod v0.26.0 + golang.org/x/sync v0.16.0 + golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b + golang.org/x/text v0.27.0 + golang.org/x/tools v0.35.1-0.20250728180453-01a3475a31bc + golang.org/x/vuln v1.1.4 + gopkg.in/yaml.v3 v3.0.1 + honnef.co/go/tools v0.7.0-0.dev.0.20250523013057-bbc2f4dd71ea + mvdan.cc/gofumpt v0.8.0 + mvdan.cc/xurls/v2 v2.6.0 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/fatih/camelcase v1.0.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/google/safehtml v0.1.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/tools/go/expect v0.1.1-deprecated // indirect + golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect +) diff --git a/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/golang.org/x/tools/gopls@v0.20.0 b/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/golang.org/x/tools/gopls@v0.20.0 new file mode 100644 index 000000000..fd63ef446 --- /dev/null +++ b/pkg/mod/cache/download/sumdb/sum.golang.org/lookup/golang.org/x/tools/gopls@v0.20.0 @@ -0,0 +1,9 @@ +41328958 +golang.org/x/tools/gopls v0.20.0 h1:fxOYZXKl6IsOTKIh6IgjDbIDHlr5btOtOUkrGOgFDB4= +golang.org/x/tools/gopls v0.20.0/go.mod h1:vxYUZ8l4swjbvTQJJONmVfbHsd1ovixCwB7sodBbTYI= + +go.sum database tree +43548952 +nX6jrsdthQ8kDPrwxKP2h/3CAC+o/Tzl00DK+QUiDxE= + +— sum.golang.org Az3grtVCRqi+V2+TLDpRvXhgZDzixz81eDxCTse8HVQFKkxvm3+CBHWwrkincl2+LzuJetgKkMzjLg5M1SI/XmJT7AQ= diff --git a/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x161/441 b/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x161/441 new file mode 100644 index 0000000000000000000000000000000000000000..20eff0ee6d9284c82c0e98d3804f9791854c6d3b GIT binary patch literal 8192 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 literal 0 HcmV?d00001 diff --git a/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x170/113.p/24 b/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/0/x170/113.p/24 new file mode 100644 index 0000000000000000000000000000000000000000..01d9b6eed1150f85bb8aec986eb6f27e09f803b3 GIT binary patch literal 768 zcmV+b1ONPTHX67i26i+QwVq9z#G{ZL&Y=NJsJWeIxr`p7Jkmv9YARXIRR@>k=?2I9z7e(f@QAuAruFR%^g>%H_?M(GGMQ4ClZv>sy<>#~^R^iMO3G-)3cVf$PPO zR3F5=DM{+X$1}7W-m^ zN@1H0?Uq@N-fXU4j-pGrcCI7J*k$Tm$Wb9^EkH6P&?4! z?47Ax+)ns*f8(jY{*eH+ygL2n#$&~o_U0$P@$ zE=OA=sZNI7fr(rE?LOusJkB~@N~RP1H9_EmNiy?&*=8 zh<|3g%J)9IVV7_8d+dX*62Gy>Ry2rX$w|cI6qPH4P05^7pvgNoRjtRUQOD1-mCQ%T zJgt>NmPH^Yal;dJ=h@rFTdrTwo;5p2@+sxtN(XyX%s>lB>663S$ofhm=<{`>D29-p yD52Bvmf7!(-@21ssKXvOBT;YrJFy+WAIx;VTu4cMVz6`Lw1AG)sN3PgL+}rYn1)pV literal 0 HcmV?d00001 diff --git a/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/630 b/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/630 new file mode 100644 index 0000000000000000000000000000000000000000..e2c434fd07f5707f4fc4fed134f94062f8bf9970 GIT binary patch literal 8192 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 literal 0 HcmV?d00001 diff --git a/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/664.p/129 b/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/1/664.p/129 new file mode 100644 index 0000000000000000000000000000000000000000..ff565c04c66b068c0a3239c483c93f32814b69ce GIT binary patch literal 4128 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 literal 0 HcmV?d00001 diff --git a/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/2/002.p/152 b/pkg/mod/cache/download/sumdb/sum.golang.org/tile/8/2/002.p/152 new file mode 100644 index 0000000000000000000000000000000000000000..fbfb0682ee5866c7c077d19ed4e75f1de6de8864 GIT binary patch literal 4864 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