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
|
build
|
||||||
models
|
models
|
||||||
samples/a13.wav
|
samples/a13.wav
|
||||||
|
samples/benchmark_out.wav
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,13 @@ endif
|
||||||
|
|
||||||
examples: $(EXAMPLES_DIR)
|
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
|
model-small: mkdir examples/go-model-download
|
||||||
@${BUILD_DIR}/go-model-download -out models ggml-small.en.bin
|
@${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
|
go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/go-audio/audio v1.0.0
|
||||||
github.com/go-audio/wav v1.1.0
|
github.com/go-audio/wav v1.1.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
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/go-audio/riff v1.0.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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")
|
ErrUnsupportedLanguage = errors.New("unsupported language")
|
||||||
ErrModelNotMultilingual = errors.New("model is not multilingual")
|
ErrModelNotMultilingual = errors.New("model is not multilingual")
|
||||||
ErrModelClosed = errors.Join(errors.New("model has been closed"), ErrInternalAppError)
|
ErrModelClosed = errors.Join(errors.New("model has been closed"), ErrInternalAppError)
|
||||||
|
ErrStatelessBusy = errors.New("stateless context is busy; concurrent processing not supported")
|
||||||
|
|
||||||
// Private errors
|
// Private errors
|
||||||
errParametersRequired = errors.New("parameters are required")
|
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 (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ggerganov/whisper.cpp/bindings/go/pkg/whisper"
|
"github.com/ggerganov/whisper.cpp/bindings/go/pkg/whisper"
|
||||||
"github.com/go-audio/wav"
|
|
||||||
assert "github.com/stretchr/testify/assert"
|
assert "github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
@ -15,115 +13,163 @@ import (
|
||||||
func TestSetLanguage(t *testing.T) {
|
func TestSetLanguage(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
model, err := whisper.New(ModelPath)
|
cases := []struct {
|
||||||
assert.NoError(err)
|
name string
|
||||||
assert.NotNil(model)
|
new func(t *testing.T) (whisper.Context, func())
|
||||||
defer func() { _ = model.Close() }()
|
}{
|
||||||
|
{name: "stateless", new: helperNewStatelessContext},
|
||||||
|
{name: "stateful", new: helperNewStatefulContext},
|
||||||
|
}
|
||||||
|
|
||||||
context, err := model.NewContext()
|
for _, tc := range cases {
|
||||||
assert.NoError(err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ctx, cleanup := tc.new(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
// This returns an error since
|
// This returns an error since the small.en model is not multilingual
|
||||||
// the model 'models/ggml-small.en.bin'
|
err := ctx.SetLanguage("en")
|
||||||
// that is loaded is not multilingual
|
assert.Error(err)
|
||||||
err = context.SetLanguage("en")
|
})
|
||||||
assert.Error(err)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContextModelIsMultilingual(t *testing.T) {
|
func TestContextModelIsMultilingual(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
model, err := whisper.New(ModelPath)
|
cases := []struct {
|
||||||
assert.NoError(err)
|
name string
|
||||||
assert.NotNil(model)
|
new func(t *testing.T) (whisper.Context, func())
|
||||||
defer func() { _ = model.Close() }()
|
}{
|
||||||
|
{name: "stateless", new: helperNewStatelessContext},
|
||||||
|
{name: "stateful", new: helperNewStatefulContext},
|
||||||
|
}
|
||||||
|
|
||||||
context, err := model.NewContext()
|
for _, tc := range cases {
|
||||||
assert.NoError(err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ctx, cleanup := tc.new(t)
|
||||||
isMultilingual := context.IsMultilingual()
|
defer cleanup()
|
||||||
|
assert.False(ctx.IsMultilingual())
|
||||||
// This returns false since
|
})
|
||||||
// the model 'models/ggml-small.en.bin'
|
}
|
||||||
// that is loaded is not multilingual
|
|
||||||
assert.False(isMultilingual)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLanguage(t *testing.T) {
|
func TestLanguage(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
model, err := whisper.New(ModelPath)
|
cases := []struct {
|
||||||
assert.NoError(err)
|
name string
|
||||||
assert.NotNil(model)
|
new func(t *testing.T) (whisper.Context, func())
|
||||||
defer func() { _ = model.Close() }()
|
}{
|
||||||
|
{name: "stateless", new: helperNewStatelessContext},
|
||||||
|
{name: "stateful", new: helperNewStatefulContext},
|
||||||
|
}
|
||||||
|
|
||||||
context, err := model.NewContext()
|
for _, tc := range cases {
|
||||||
assert.NoError(err)
|
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
|
// Generic behavior: Language() and DetectedLanguage() match for both context types
|
||||||
// the model 'models/ggml-small.en.bin'
|
func TestContext_Generic_LanguageAndDetectedLanguage(t *testing.T) {
|
||||||
// that is loaded is not multilingual
|
assert := assert.New(t)
|
||||||
expectedLanguage := "en"
|
|
||||||
actualLanguage := context.Language()
|
if _, err := os.Stat(ModelPath); os.IsNotExist(err) {
|
||||||
assert.Equal(expectedLanguage, actualLanguage)
|
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) {
|
func TestProcess(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
fh, err := os.Open(SamplePath)
|
if _, err := os.Stat(ModelPath); os.IsNotExist(err) {
|
||||||
assert.NoError(err)
|
t.Skip("Skipping test, model not found:", ModelPath)
|
||||||
defer func() { _ = fh.Close() }()
|
}
|
||||||
|
if _, err := os.Stat(SamplePath); os.IsNotExist(err) {
|
||||||
|
t.Skip("Skipping test, sample not found:", SamplePath)
|
||||||
|
}
|
||||||
|
|
||||||
// Decode the WAV file - load the full buffer
|
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)
|
for _, tc := range cases {
|
||||||
assert.NoError(err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
assert.NotNil(model)
|
ctx, cleanup := tc.new(t)
|
||||||
defer func() { _ = model.Close() }()
|
defer cleanup()
|
||||||
|
err := ctx.Process(data, nil, nil, nil)
|
||||||
context, err := model.NewContext()
|
assert.NoError(err)
|
||||||
assert.NoError(err)
|
})
|
||||||
|
}
|
||||||
err = context.Process(data, nil, nil, nil)
|
|
||||||
assert.NoError(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetectedLanguage(t *testing.T) {
|
func TestDetectedLanguage(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
fh, err := os.Open(SamplePath)
|
if _, err := os.Stat(ModelPath); os.IsNotExist(err) {
|
||||||
assert.NoError(err)
|
t.Skip("Skipping test, model not found:", ModelPath)
|
||||||
defer func() { _ = fh.Close() }()
|
}
|
||||||
|
if _, err := os.Stat(SamplePath); os.IsNotExist(err) {
|
||||||
|
t.Skip("Skipping test, sample not found:", SamplePath)
|
||||||
|
}
|
||||||
|
|
||||||
// Decode the WAV file - load the full buffer
|
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)
|
for _, tc := range cases {
|
||||||
assert.NoError(err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
assert.NotNil(model)
|
ctx, cleanup := tc.new(t)
|
||||||
defer func() { _ = model.Close() }()
|
defer cleanup()
|
||||||
|
err := ctx.Process(data, nil, nil, nil)
|
||||||
context, err := model.NewContext()
|
assert.NoError(err)
|
||||||
assert.NoError(err)
|
expectedLanguage := "en"
|
||||||
|
actualLanguage := ctx.DetectedLanguage()
|
||||||
err = context.Process(data, nil, nil, nil)
|
assert.Equal(expectedLanguage, actualLanguage)
|
||||||
assert.NoError(err)
|
})
|
||||||
|
}
|
||||||
expectedLanguage := "en"
|
|
||||||
actualLanguage := context.DetectedLanguage()
|
|
||||||
assert.Equal(expectedLanguage, actualLanguage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestContext_ConcurrentProcessing tests that multiple contexts can process concurrently
|
// 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)
|
t.Skip("Skipping test, sample not found:", SamplePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
fh, err := os.Open(SamplePath)
|
data := helperLoadSample(t, SamplePath)
|
||||||
assert.NoError(err)
|
|
||||||
defer func() { _ = fh.Close() }()
|
|
||||||
|
|
||||||
dec := wav.NewDecoder(fh)
|
cases := []struct {
|
||||||
buf, err := dec.FullPCMBuffer()
|
name string
|
||||||
assert.NoError(err)
|
new func(t *testing.T) (whisper.Context, func())
|
||||||
assert.Equal(uint16(1), dec.NumChans)
|
}{
|
||||||
data := buf.AsFloat32Buffer().Data
|
{name: "stateless", new: helperNewStatelessContext},
|
||||||
|
{name: "stateful", new: helperNewStatefulContext},
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fh, err := os.Open(SamplePath)
|
for _, tc := range cases {
|
||||||
assert.NoError(err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
defer func() { _ = fh.Close() }()
|
ctx, cleanup := tc.new(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
dec := wav.NewDecoder(fh)
|
err := ctx.Process(data, nil, nil, nil)
|
||||||
buf, err := dec.FullPCMBuffer()
|
assert.NoError(err)
|
||||||
assert.NoError(err)
|
|
||||||
assert.Equal(uint16(1), dec.NumChans)
|
|
||||||
data := buf.AsFloat32Buffer().Data
|
|
||||||
assert.Greater(len(data), 10)
|
|
||||||
|
|
||||||
// Create half-sample (second half)
|
seg, err := ctx.NextSegment()
|
||||||
half := make([]float32, len(data)/2)
|
assert.NoError(err)
|
||||||
copy(half, data[len(data)/2:])
|
assert.NotEmpty(seg.Text)
|
||||||
|
})
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestContext_Close tests that Context.Close() properly frees resources
|
// 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)
|
t.Skip("Skipping test, model not found:", ModelPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
model, err := whisper.New(ModelPath)
|
cases := []struct {
|
||||||
assert.NoError(err)
|
name string
|
||||||
assert.NotNil(model)
|
new func(t *testing.T) (whisper.Context, func())
|
||||||
defer func() { _ = model.Close() }()
|
}{
|
||||||
|
{name: "stateless", new: helperNewStatelessContext},
|
||||||
|
{name: "stateful", new: helperNewStatefulContext},
|
||||||
|
}
|
||||||
|
|
||||||
ctx, err := model.NewContext()
|
for _, tc := range cases {
|
||||||
assert.NoError(err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
assert.NotNil(ctx)
|
ctx, cleanup := tc.new(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
// Close the context
|
// Close the context
|
||||||
err = ctx.Close()
|
err := ctx.Close()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Try to use closed context - should return errors
|
// Try to use closed context - should return errors
|
||||||
err = ctx.Process([]float32{0.1, 0.2, 0.3}, nil, nil, nil)
|
err = ctx.Process([]float32{0.1, 0.2, 0.3}, nil, nil, nil)
|
||||||
require.ErrorIs(t, err, whisper.ErrModelClosed)
|
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
|
lang := ctx.DetectedLanguage()
|
||||||
require.ErrorIs(t, err, whisper.ErrInternalAppError)
|
require.Empty(t, lang)
|
||||||
|
|
||||||
lang := ctx.DetectedLanguage()
|
_, err = ctx.NextSegment()
|
||||||
require.Empty(t, lang)
|
assert.ErrorIs(err, whisper.ErrModelClosed)
|
||||||
|
// TODO: remove this logic after deprecating the ErrInternalAppError
|
||||||
|
assert.ErrorIs(err, whisper.ErrInternalAppError)
|
||||||
|
|
||||||
_, err = ctx.NextSegment()
|
// Multiple closes should be safe
|
||||||
assert.ErrorIs(err, whisper.ErrModelClosed)
|
err = ctx.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_Close_Context_of_Closed_Model(t *testing.T) {
|
func Test_Close_Context_of_Closed_Model(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
model, err := whisper.New(ModelPath)
|
if _, err := os.Stat(ModelPath); os.IsNotExist(err) {
|
||||||
assert.NoError(err)
|
t.Skip("Skipping test, model not found:", ModelPath)
|
||||||
assert.NotNil(model)
|
}
|
||||||
|
|
||||||
ctx, err := model.NewContext()
|
t.Run("stateless", func(t *testing.T) {
|
||||||
assert.NoError(err)
|
model, err := whisper.NewModelContext(ModelPath)
|
||||||
assert.NotNil(ctx)
|
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())
|
t.Run("stateful", func(t *testing.T) {
|
||||||
require.NoError(t, ctx.Close())
|
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) {
|
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)
|
t.Skip("Skipping test, sample not found:", SamplePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
fh, err := os.Open(SamplePath)
|
data := helperLoadSample(t, 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
|
|
||||||
|
|
||||||
model, err := whisper.NewModelContext(ModelPath)
|
model, err := whisper.NewModelContext(ModelPath)
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
|
|
@ -352,15 +325,7 @@ func TestContext_VAD_And_Diarization_Params_DoNotPanic(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDiarization_TwoSpeakers_Boundaries(t *testing.T) {
|
func TestDiarization_TwoSpeakers_Boundaries(t *testing.T) {
|
||||||
fh, err := os.Open(MultiSpeakerSamplePath)
|
data := helperLoadSample(t, 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
|
|
||||||
|
|
||||||
model, err := whisper.NewModelContext(ModelTinydiarizePath)
|
model, err := whisper.NewModelContext(ModelTinydiarizePath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
@ -426,29 +391,193 @@ func TestContext_SpeakerTurnNext_Field_Present(t *testing.T) {
|
||||||
t.Skip("Skipping test, sample not found:", SamplePath)
|
t.Skip("Skipping test, sample not found:", SamplePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
fh, err := os.Open(SamplePath)
|
data := helperLoadSample(t, SamplePath)
|
||||||
assert.NoError(err)
|
|
||||||
defer func() { _ = fh.Close() }()
|
|
||||||
|
|
||||||
dec := wav.NewDecoder(fh)
|
cases := []struct {
|
||||||
buf, err := dec.FullPCMBuffer()
|
name string
|
||||||
assert.NoError(err)
|
new func(t *testing.T) (whisper.Context, func())
|
||||||
assert.Equal(uint16(1), dec.NumChans)
|
}{
|
||||||
data := buf.AsFloat32Buffer().Data
|
{name: "stateless", new: helperNewStatelessContext},
|
||||||
|
{name: "stateful", new: helperNewStatefulContext},
|
||||||
|
}
|
||||||
|
|
||||||
model, err := whisper.New(ModelPath)
|
for _, tc := range cases {
|
||||||
assert.NoError(err)
|
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() }()
|
defer func() { _ = model.Close() }()
|
||||||
|
|
||||||
ctx, err := model.NewContext()
|
// Independent params with temperature=0 for determinism
|
||||||
assert.NoError(err)
|
p := helperNewParams(t, model, func(p *whisper.Parameters) {
|
||||||
defer func() { _ = ctx.Close() }()
|
p.SetTemperature(0)
|
||||||
|
p.SetThreads(1)
|
||||||
|
})
|
||||||
|
|
||||||
err = ctx.Process(data, nil, nil, nil)
|
stateless, err := whisper.NewStatelessContext(model, p)
|
||||||
assert.NoError(err)
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = stateless.Close() }()
|
||||||
|
|
||||||
seg, err := ctx.NextSegment()
|
stateful, err := whisper.NewStatefulContext(model, p)
|
||||||
assert.NoError(err)
|
require.NoError(t, err)
|
||||||
t.Logf("SpeakerTurnNext: %v", seg.SpeakerTurnNext)
|
defer func() { _ = stateful.Close() }()
|
||||||
_ = seg.SpeakerTurnNext // ensure field exists and is readable
|
|
||||||
|
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"
|
"os"
|
||||||
|
|
||||||
// Bindings
|
// Bindings
|
||||||
whisper "github.com/ggerganov/whisper.cpp/bindings/go"
|
low "github.com/ggerganov/whisper.cpp/bindings/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelContext struct {
|
type ModelContext struct {
|
||||||
|
|
@ -17,27 +17,50 @@ type ModelContext struct {
|
||||||
// Make sure model adheres to the interface
|
// Make sure model adheres to the interface
|
||||||
var _ Model = (*ModelContext)(nil)
|
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
|
// Deprecated: Use NewModelContext instead
|
||||||
func New(path string) (Model, error) {
|
func New(path string) (Model, error) {
|
||||||
return NewModelContext(path)
|
return NewModelContext(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewModelContext creates a new model context
|
// NewModelContext creates a new model context
|
||||||
|
|
||||||
func NewModelContext(
|
func NewModelContext(
|
||||||
path string,
|
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) {
|
) (*ModelContext, error) {
|
||||||
model := new(ModelContext)
|
model := new(ModelContext)
|
||||||
if _, err := os.Stat(path); err != nil {
|
if _, err := os.Stat(path); err != nil {
|
||||||
return nil, err
|
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
|
return model, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,9 +98,9 @@ func (model *ModelContext) Languages() []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]string, 0, whisper.Whisper_lang_max_id())
|
result := make([]string, 0, low.Whisper_lang_max_id())
|
||||||
for i := 0; i < whisper.Whisper_lang_max_id(); i++ {
|
for i := 0; i < low.Whisper_lang_max_id(); i++ {
|
||||||
str := whisper.Whisper_lang_str(i)
|
str := low.Whisper_lang_str(i)
|
||||||
if ctx.Whisper_lang_id(str) >= 0 {
|
if ctx.Whisper_lang_id(str) >= 0 {
|
||||||
result = append(result, str)
|
result = append(result, str)
|
||||||
}
|
}
|
||||||
|
|
@ -95,8 +118,8 @@ func (model *ModelContext) NewContext() (Context, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return new context (now state-backed)
|
// Return new context (stateless for backward compatibility with timings)
|
||||||
return NewStatefulContext(
|
return NewStatelessContext(
|
||||||
model,
|
model,
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
|
|
@ -122,6 +145,35 @@ func (model *ModelContext) ResetTimings() {
|
||||||
ctx.Whisper_reset_timings()
|
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 {
|
func (model *ModelContext) tokenIdentifier() *tokenIdentifier {
|
||||||
return model.tokId
|
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)
|
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)
|
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
|
package whisper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ModelPath = "../../models/ggml-small.en.bin"
|
ModelPath = "../../models/ggml-small.en.bin"
|
||||||
ModelTinydiarizePath = "../../models/ggml-small.en-tdrz.bin"
|
ModelTinydiarizePath = "../../models/ggml-small.en-tdrz.bin"
|
||||||
SamplePath = "../../samples/jfk.wav"
|
SamplePath = "../../samples/jfk.wav"
|
||||||
MultiSpeakerSamplePath = "../../samples/a13.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: -lggml-metal -lggml-blas
|
||||||
#cgo darwin LDFLAGS: -framework Accelerate -framework Metal -framework Foundation -framework CoreGraphics
|
#cgo darwin LDFLAGS: -framework Accelerate -framework Metal -framework Foundation -framework CoreGraphics
|
||||||
#include <whisper.h>
|
#include <whisper.h>
|
||||||
|
#include <ggml.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
extern void callNewSegment(void* user_data, int new);
|
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);
|
params.progress_callback_user_data = (void*)(ctx);
|
||||||
return params;
|
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"
|
import "C"
|
||||||
|
|
||||||
|
|
@ -73,6 +90,8 @@ type (
|
||||||
TokenData C.struct_whisper_token_data
|
TokenData C.struct_whisper_token_data
|
||||||
SamplingStrategy C.enum_whisper_sampling_strategy
|
SamplingStrategy C.enum_whisper_sampling_strategy
|
||||||
Params C.struct_whisper_full_params
|
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")
|
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
|
// 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.
|
// Frees all memory allocated by the model.
|
||||||
func (ctx *Context) Whisper_free() {
|
func (ctx *Context) Whisper_free() {
|
||||||
C.whisper_free((*C.struct_whisper_context)(ctx))
|
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))
|
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
|
// Print system information
|
||||||
func Whisper_print_system_info() string {
|
func Whisper_print_system_info() string {
|
||||||
return C.GoString(C.whisper_print_system_info())
|
return C.GoString(C.whisper_print_system_info())
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ const (
|
||||||
SamplePath = "samples/jfk.wav"
|
SamplePath = "samples/jfk.wav"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
whisper.DisableLogs()
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
func Test_Whisper_000(t *testing.T) {
|
func Test_Whisper_000(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
if _, err := os.Stat(ModelPath); os.IsNotExist(err) {
|
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