feat: split context into stateful/stateless; add concurrency gate, added model context params, benchmarks, silence mode for the ggml
This commit is contained in:
parent
8f9ad60fca
commit
2305a6142a
|
|
@ -1,3 +1,4 @@
|
|||
build
|
||||
models
|
||||
samples/a13.wav
|
||||
samples/benchmark_out.wav
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
v0.20.0
|
||||
|
|
@ -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"}}
|
||||
|
|
@ -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
|
||||
)
|
||||
9
pkg/mod/cache/download/sumdb/sum.golang.org/lookup/golang.org/x/tools/gopls@v0.20.0
vendored
Normal file
9
pkg/mod/cache/download/sumdb/sum.golang.org/lookup/golang.org/x/tools/gopls@v0.20.0
vendored
Normal 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.
|
|
@ -0,0 +1,3 @@
|
|||
o
|
||||
”@m°%q±£T`…ow!^ñ¾Z—{§<>Ë<EFBFBD>éÐvÍ빜5Ñ1 "sJ®YFØñ Ç
|
||||
iÂéè¶9Z•Æ9Æ
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
go.sum database tree
|
||||
43548952
|
||||
nX6jrsdthQ8kDPrwxKP2h/3CAC+o/Tzl00DK+QUiDxE=
|
||||
|
||||
— sum.golang.org Az3grtVCRqi+V2+TLDpRvXhgZDzixz81eDxCTse8HVQFKkxvm3+CBHWwrkincl2+LzuJetgKkMzjLg5M1SI/XmJT7AQ=
|
||||
Loading…
Reference in New Issue