feat: split context into stateful/stateless; add concurrency gate, added model context params, benchmarks, silence mode for the ggml

This commit is contained in:
ciricc 2025-09-20 15:56:48 +03:00
parent 8f9ad60fca
commit 2305a6142a
29 changed files with 1578 additions and 267 deletions

View File

@ -1,3 +1,4 @@
build
models
samples/a13.wav
samples/benchmark_out.wav

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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")

View File

@ -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)
}

View File

@ -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")
}
})
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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")
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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())
}

View File

@ -15,6 +15,7 @@ import (
#cgo darwin LDFLAGS: -lggml-metal -lggml-blas
#cgo darwin LDFLAGS: -framework Accelerate -framework Metal -framework Foundation -framework CoreGraphics
#include <whisper.h>
#include <ggml.h>
#include <stdlib.h>
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())

View File

@ -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) {

View File

@ -0,0 +1 @@
v0.20.0

View File

@ -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"}}

View File

@ -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
)

View File

@ -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=

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
o
”@m°%q±£T `…ow!^ñ¾Z—{§<>Ë<EFBFBD>éÐvÍ빜5Ñ1 "sJ®YFØñ Ç
iÂéè¶9Z•Æ9Æ

View File

@ -0,0 +1,5 @@
go.sum database tree
43548952
nX6jrsdthQ8kDPrwxKP2h/3CAC+o/Tzl00DK+QUiDxE=
— sum.golang.org Az3grtVCRqi+V2+TLDpRvXhgZDzixz81eDxCTse8HVQFKkxvm3+CBHWwrkincl2+LzuJetgKkMzjLg5M1SI/XmJT7AQ=