From 01f5c6b708121e5fd1381ea642e28f246afdf1ee Mon Sep 17 00:00:00 2001 From: ciricc Date: Sun, 14 Sep 2025 01:47:57 +0300 Subject: [PATCH] refactor(go bindings): remove public method accessing unsafe whisper --- bindings/go/pkg/whisper/context.go | 31 ++++--- bindings/go/pkg/whisper/interface.go | 73 +++++++++++----- bindings/go/pkg/whisper/model.go | 16 ++-- bindings/go/pkg/whisper/model_test.go | 8 +- bindings/go/pkg/whisper/params_wrap.go | 6 +- bindings/go/pkg/whisper/token_identifier.go | 16 ++-- bindings/go/pkg/whisper/whisper_ctx.go | 5 +- bindings/go/pkg/whisper/whisper_ctx_test.go | 85 +++++++++++++++++++ bindings/go/pkg/whisper/whisper_state_test.go | 53 ++++++++++++ 9 files changed, 233 insertions(+), 60 deletions(-) create mode 100644 bindings/go/pkg/whisper/whisper_ctx_test.go create mode 100644 bindings/go/pkg/whisper/whisper_state_test.go diff --git a/bindings/go/pkg/whisper/context.go b/bindings/go/pkg/whisper/context.go index 7e2094815..e48617114 100644 --- a/bindings/go/pkg/whisper/context.go +++ b/bindings/go/pkg/whisper/context.go @@ -3,7 +3,6 @@ package whisper import ( "fmt" "io" - "log" "runtime" "strings" "time" @@ -14,13 +13,13 @@ import ( type context struct { n int - model Model + model *model st WhisperState - params Parameters + params *parameters Parameters } -func newContext(model Model, params Parameters) (Context, error) { +func newContext(model *model, params *parameters) (Context, error) { c := new(context) c.model = model @@ -28,7 +27,7 @@ func newContext(model Model, params Parameters) (Context, error) { c.Parameters = c.params // allocate isolated state per context - ctx, err := model.WhisperContext().UnsafeContext() + ctx, err := model.whisperContext().unsafeContext() if err != nil { return nil, err } @@ -46,7 +45,7 @@ func newContext(model Model, params Parameters) (Context, error) { // DetectedLanguage returns the detected language for the current context data func (context *context) DetectedLanguage() string { - ctx, err := context.model.WhisperContext().UnsafeContext() + ctx, err := context.model.whisperContext().unsafeContext() if err != nil { return "" } @@ -98,7 +97,7 @@ func (context *context) SystemInfo() string { // Make sure to call whisper_pcm_to_mel() or whisper_set_mel() first. // Returns the probabilities of all languages for this context's state. func (context *context) WhisperLangAutoDetect(offset_ms int, n_threads int) ([]float32, error) { - ctx, err := context.model.WhisperContext().UnsafeContext() + ctx, err := context.model.whisperContext().unsafeContext() if err != nil { return nil, err } @@ -123,7 +122,7 @@ func (context *context) Process( callNewSegment SegmentCallback, callProgress ProgressCallback, ) error { - ctx, err := context.model.WhisperContext().UnsafeContext() + ctx, err := context.model.whisperContext().unsafeContext() if err != nil { return err } @@ -133,13 +132,11 @@ func (context *context) Process( context.params.SetSingleSegment(true) } - lowLevelParams := context.params.UnsafeParams() - if lowLevelParams == nil { - return fmt.Errorf("lowLevelParams is nil: %w", ErrInternalAppError) + lowLevelParams, err := context.params.unsafeParams() + if err != nil { + return err } - log.Println("lowLevelParams", lowLevelParams) - st, err := context.st.UnsafeState() if err != nil { return err @@ -168,7 +165,7 @@ func (context *context) Process( // NextSegment returns the next segment from the context buffer func (context *context) NextSegment() (Segment, error) { - ctx, err := context.model.WhisperContext().UnsafeContext() + ctx, err := context.model.whisperContext().unsafeContext() if err != nil { return Segment{}, err } @@ -277,3 +274,9 @@ func toTokensFromState(ctx *whisper.Context, st *whisper.State, n int) []Token { return result } + +func (context *context) Model() Model { + return context.model +} + +var _ Context = (*context)(nil) diff --git a/bindings/go/pkg/whisper/interface.go b/bindings/go/pkg/whisper/interface.go index 9040718e0..56974769c 100644 --- a/bindings/go/pkg/whisper/interface.go +++ b/bindings/go/pkg/whisper/interface.go @@ -3,8 +3,6 @@ package whisper import ( "io" "time" - - whisper "github.com/ggerganov/whisper.cpp/bindings/go" ) /////////////////////////////////////////////////////////////////////////////// @@ -56,8 +54,13 @@ type Model interface { io.Closer // Return a new speech-to-text context. + // It may return an error is the model is not loaded or closed NewContext() (Context, error) + // Return a new parameters wrapper + // sampling is the sampling strategy to use + // configure is the function to configure the parameters + // It may return an error is the model is not loaded or closed NewParams( sampling SamplingStrategy, configure ParamsConfigure, @@ -65,9 +68,14 @@ type Model interface { // Return a new speech-to-text context configured via the provided function // and sampling strategy. The context is backed by an isolated whisper_state. - NewContextWithParams(sampling SamplingStrategy, configure ParamsConfigure) (Context, error) + // It may return an error is the model is not loaded or closed + NewContextWithParams( + sampling SamplingStrategy, + configure ParamsConfigure, + ) (Context, error) // Return true if the model is multilingual. + // It returns false if the model is not loaded or closed IsMultilingual() bool // Return all languages supported. @@ -81,6 +89,8 @@ type Model interface { ResetTimings() // WhisperContext returns the memory-safe whisper context wrapper of the raw whisper context + // You may need to use this to get the raw whisper context + // Ot check that the model's context is not closed WhisperContext() WhisperContext // Token identifier @@ -113,7 +123,6 @@ type Parameters interface { // Enable extra debug info (e.g., dump log_mel) SetDebugMode(bool) - // Diarization (tinydiarize) SetDiarize(bool) @@ -133,6 +142,9 @@ type Parameters interface { // Set the fallback temperature incrementation // Pass -1.0 to disable this feature SetTemperatureFallback(t float32) + + // Set the language + // If the model is not multilingual, this will return an error SetLanguage(string) error // Set single segment mode @@ -141,34 +153,39 @@ type Parameters interface { // Getter methods Language() string Threads() int - - UnsafeParams() *whisper.Params } // Context is the speech recognition context. type Context interface { io.Closer + // Deprecated: Use Params().SetLanguage() instead SetLanguage(string) error // Deprecated: Use Params().SetTranslate() instead SetTranslate(bool) + // Deprecated: Use Params().SetSplitOnWord() instead SetSplitOnWord(bool) + // Deprecated: Use Params().SetThreads() instead SetThreads(uint) // Deprecated: Use Params().SetOffset() instead SetOffset(time.Duration) + // Deprecated: Use Params().SetDuration() instead SetDuration(time.Duration) + // Deprecated: Use Params().SetTokenThreshold() instead SetTokenThreshold(float32) // Deprecated: Use Params().SetTokenSumThreshold() instead SetTokenSumThreshold(float32) // Deprecated: Use Params().SetMaxSegmentLength() instead + SetMaxSegmentLength(uint) + // Deprecated: Use Params().SetTokenTimestamps() instead SetTokenTimestamps(bool) @@ -200,7 +217,10 @@ type Context interface { // Deprecated: Use Params().Language() instead Language() string - // Return true if the model is multilingual. + // Return the model that the context is backed by + Model() Model + + // Deprecated: Use Model().IsMultilingual() instead IsMultilingual() bool // Get detected language @@ -215,37 +235,39 @@ type Context interface { // is reached, when io.EOF is returned. NextSegment() (Segment, error) - // Deprecated token methods - use Model.IsBEG(), Model.IsSOT(), etc. instead - // Deprecated: Use Model.IsBEG() instead + // Deprecated: Use Model().TokenIdentifier().IsBEG() instead IsBEG(Token) bool - // Deprecated: Use Model.IsSOT() instead + // Deprecated: Use Model().TokenIdentifier().IsSOT() instead IsSOT(Token) bool - // Deprecated: Use Model.IsEOT() instead + // Deprecated: Use Model().TokenIdentifier().IsEOT() instead IsEOT(Token) bool - // Deprecated: Use Model.IsPREV() instead + // Deprecated: Use Model().TokenIdentifier().IsPREV() instead IsPREV(Token) bool - // Deprecated: Use Model.IsSOLM() instead + // Deprecated: Use Model().TokenIdentifier().IsSOLM() instead IsSOLM(Token) bool - // Deprecated: Use Model.IsNOT() instead + // Deprecated: Use Model().TokenIdentifier().IsNOT() instead IsNOT(Token) bool - // Deprecated: Use Model.IsLANG() instead + // Deprecated: Use Model().TokenIdentifier().IsLANG() instead IsLANG(Token, string) bool - // Deprecated: Use Model.IsText() instead + // Deprecated: Use Model().TokenIdentifier().IsText() instead IsText(Token) bool - // Deprecated: Use Model.PrintTimings() instead - these are model-level performance metrics + // Deprecated: Use Model().PrintTimings() instead + // these are model-level performance metrics PrintTimings() - // Deprecated: Use Model.ResetTimings() instead - these are model-level performance metrics + // Deprecated: Use Model().ResetTimings() instead + // these are model-level performance metrics ResetTimings() + // SystemInfo returns the system information SystemInfo() string // Params returns a high-level parameters wrapper - preferred method @@ -267,13 +289,22 @@ type Segment struct { Tokens []Token // True if the next segment is predicted as a speaker turn (tinydiarize) + // It works only with the diarization supporting models (like small.en-tdrz.bin) with the diarization enabled + // using Parameters.SetDiarize(true) SpeakerTurnNext bool } // Token is a text or special token type Token struct { - Id int - Text string - P float32 + // ID of the token + Id int + + // Text of the token + Text string + + // Probability of the token + P float32 + + // Timestamp of the token Start, End time.Duration } diff --git a/bindings/go/pkg/whisper/model.go b/bindings/go/pkg/whisper/model.go index 203ec7b20..988ae1522 100644 --- a/bindings/go/pkg/whisper/model.go +++ b/bindings/go/pkg/whisper/model.go @@ -41,6 +41,10 @@ func (model *model) WhisperContext() WhisperContext { return model.ctx } +func (model *model) whisperContext() *whisperCtx { + return model.ctx +} + /////////////////////////////////////////////////////////////////////////////// // STRINGIFY @@ -58,7 +62,7 @@ func (model *model) String() string { // Return true if model is multilingual (language and translation options are supported) func (model *model) IsMultilingual() bool { - ctx, err := model.ctx.UnsafeContext() + ctx, err := model.ctx.unsafeContext() if err != nil { return false } @@ -68,7 +72,7 @@ func (model *model) IsMultilingual() bool { // Return all recognized languages. Initially it is set to auto-detect func (model *model) Languages() []string { - ctx, err := model.ctx.UnsafeContext() + ctx, err := model.ctx.unsafeContext() if err != nil { return nil } @@ -137,8 +141,8 @@ func defaultParamsConfigure(params Parameters) { func (m *model) newParams( sampling SamplingStrategy, configure ParamsConfigure, -) (Parameters, error) { - ctx, err := m.ctx.UnsafeContext() +) (*parameters, error) { + ctx, err := m.ctx.unsafeContext() if err != nil { return nil, ErrModelClosed } @@ -157,7 +161,7 @@ func (m *model) newParams( // PrintTimings prints the model performance timings to stdout. func (model *model) PrintTimings() { - ctx, err := model.ctx.UnsafeContext() + ctx, err := model.ctx.unsafeContext() if err != nil { return } @@ -167,7 +171,7 @@ func (model *model) PrintTimings() { // ResetTimings resets the model performance timing counters. func (model *model) ResetTimings() { - ctx, err := model.ctx.UnsafeContext() + ctx, err := model.ctx.unsafeContext() if err != nil { return } diff --git a/bindings/go/pkg/whisper/model_test.go b/bindings/go/pkg/whisper/model_test.go index 8797f0d0f..d080d8fe3 100644 --- a/bindings/go/pkg/whisper/model_test.go +++ b/bindings/go/pkg/whisper/model_test.go @@ -13,7 +13,7 @@ func TestNew(t *testing.T) { model, err := whisper.New(ModelPath) assert.NoError(err) assert.NotNil(model) - defer model.Close() + defer func() { _ = model.Close() }() }) @@ -42,7 +42,7 @@ func TestNewContext(t *testing.T) { model, err := whisper.New(ModelPath) assert.NoError(err) assert.NotNil(model) - defer model.Close() + defer func() { _ = model.Close() }() context, err := model.NewContext() assert.NoError(err) @@ -55,7 +55,7 @@ func TestIsMultilingual(t *testing.T) { model, err := whisper.New(ModelPath) assert.NoError(err) assert.NotNil(model) - defer model.Close() + defer func() { _ = model.Close() }() isMultilingual := model.IsMultilingual() @@ -71,7 +71,7 @@ func TestLanguages(t *testing.T) { model, err := whisper.New(ModelPath) assert.NoError(err) assert.NotNil(model) - defer model.Close() + defer func() { _ = model.Close() }() expectedLanguages := []string{ "en", "zh", "de", "es", "ru", "ko", "fr", "ja", "pt", "tr", "pl", diff --git a/bindings/go/pkg/whisper/params_wrap.go b/bindings/go/pkg/whisper/params_wrap.go index 17b70b19e..ad772d88b 100644 --- a/bindings/go/pkg/whisper/params_wrap.go +++ b/bindings/go/pkg/whisper/params_wrap.go @@ -13,7 +13,7 @@ type parameters struct { p *whisper.Params } -func newParameters(whisperParams *whisper.Params) Parameters { +func newParameters(whisperParams *whisper.Params) *parameters { return ¶meters{ p: whisperParams, } @@ -85,8 +85,8 @@ func (w *parameters) Threads() int { return w.p.Threads() } -func (w *parameters) UnsafeParams() *whisper.Params { - return w.p +func (w *parameters) unsafeParams() (*whisper.Params, error) { + return w.p, nil } var _ Parameters = ¶meters{} diff --git a/bindings/go/pkg/whisper/token_identifier.go b/bindings/go/pkg/whisper/token_identifier.go index 059386c1e..13867273d 100644 --- a/bindings/go/pkg/whisper/token_identifier.go +++ b/bindings/go/pkg/whisper/token_identifier.go @@ -14,7 +14,7 @@ func newTokenIdentifier(whisperContext *whisperCtx) *tokenIdentifier { // Token type checking methods (model-specific vocabulary) func (ti *tokenIdentifier) IsBEG(t Token) (bool, error) { - ctx, err := ti.ctx.UnsafeContext() + ctx, err := ti.ctx.unsafeContext() if err != nil { return false, err } @@ -23,7 +23,7 @@ func (ti *tokenIdentifier) IsBEG(t Token) (bool, error) { } func (ti *tokenIdentifier) IsEOT(t Token) (bool, error) { - ctx, err := ti.ctx.UnsafeContext() + ctx, err := ti.ctx.unsafeContext() if err != nil { return false, err } @@ -32,7 +32,7 @@ func (ti *tokenIdentifier) IsEOT(t Token) (bool, error) { } func (ti *tokenIdentifier) IsSOT(t Token) (bool, error) { - ctx, err := ti.ctx.UnsafeContext() + ctx, err := ti.ctx.unsafeContext() if err != nil { return false, err } @@ -41,7 +41,7 @@ func (ti *tokenIdentifier) IsSOT(t Token) (bool, error) { } func (ti *tokenIdentifier) IsPREV(t Token) (bool, error) { - ctx, err := ti.ctx.UnsafeContext() + ctx, err := ti.ctx.unsafeContext() if err != nil { return false, err } @@ -50,7 +50,7 @@ func (ti *tokenIdentifier) IsPREV(t Token) (bool, error) { } func (ti *tokenIdentifier) IsSOLM(t Token) (bool, error) { - ctx, err := ti.ctx.UnsafeContext() + ctx, err := ti.ctx.unsafeContext() if err != nil { return false, err } @@ -59,7 +59,7 @@ func (ti *tokenIdentifier) IsSOLM(t Token) (bool, error) { } func (ti *tokenIdentifier) IsNOT(t Token) (bool, error) { - ctx, err := ti.ctx.UnsafeContext() + ctx, err := ti.ctx.unsafeContext() if err != nil { return false, err } @@ -68,7 +68,7 @@ func (ti *tokenIdentifier) IsNOT(t Token) (bool, error) { } func (ti *tokenIdentifier) IsLANG(t Token, lang string) (bool, error) { - ctx, err := ti.ctx.UnsafeContext() + ctx, err := ti.ctx.unsafeContext() if err != nil { return false, err } @@ -90,7 +90,7 @@ func (ti *tokenIdentifier) IsText(t Token) (bool, error) { return false, nil } - ctx, err := ti.ctx.UnsafeContext() + ctx, err := ti.ctx.unsafeContext() if err != nil { return false, err } diff --git a/bindings/go/pkg/whisper/whisper_ctx.go b/bindings/go/pkg/whisper/whisper_ctx.go index 7e570bc3d..bdc166cf8 100644 --- a/bindings/go/pkg/whisper/whisper_ctx.go +++ b/bindings/go/pkg/whisper/whisper_ctx.go @@ -8,9 +8,6 @@ type WhisperContext interface { // IsClosed returns true if the whisper context is closed IsClosed() bool - - // UnsafeContext returns the raw whisper context - UnsafeContext() (*whisper.Context, error) } type whisperCtx struct { @@ -38,7 +35,7 @@ func (ctx *whisperCtx) IsClosed() bool { return ctx.ctx == nil } -func (ctx *whisperCtx) UnsafeContext() (*whisper.Context, error) { +func (ctx *whisperCtx) unsafeContext() (*whisper.Context, error) { if ctx.IsClosed() { return nil, ErrModelClosed } diff --git a/bindings/go/pkg/whisper/whisper_ctx_test.go b/bindings/go/pkg/whisper/whisper_ctx_test.go new file mode 100644 index 000000000..68a094bd2 --- /dev/null +++ b/bindings/go/pkg/whisper/whisper_ctx_test.go @@ -0,0 +1,85 @@ +package whisper + +import ( + "os" + "testing" + + w "github.com/ggerganov/whisper.cpp/bindings/go" + assert "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testModelPathCtx = "../../models/ggml-small.en.bin" + +func TestWhisperCtx_NilWrapper(t *testing.T) { + wctx := newWhisperCtx(nil) + + assert.True(t, wctx.IsClosed()) + + raw, err := wctx.unsafeContext() + assert.Nil(t, raw) + require.ErrorIs(t, err, ErrModelClosed) + + require.NoError(t, wctx.Close()) + // idempotent + require.NoError(t, wctx.Close()) +} + +func TestWhisperCtx_Lifecycle(t *testing.T) { + if _, err := os.Stat(testModelPathCtx); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", testModelPathCtx) + } + + raw := w.Whisper_init(testModelPathCtx) + require.NotNil(t, raw) + + wctx := newWhisperCtx(raw) + assert.False(t, wctx.IsClosed()) + + got, err := wctx.unsafeContext() + require.NoError(t, err) + require.NotNil(t, got) + + // close frees underlying ctx and marks closed + require.NoError(t, wctx.Close()) + assert.True(t, wctx.IsClosed()) + + got, err = wctx.unsafeContext() + assert.Nil(t, got) + require.ErrorIs(t, err, ErrModelClosed) + + // idempotent + require.NoError(t, wctx.Close()) + // no further free; raw already freed by wctx.Close() +} + +func TestWhisperCtx_FromModelLifecycle(t *testing.T) { + if _, err := os.Stat(testModelPathCtx); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", testModelPathCtx) + } + + modelNew, err := New(testModelPathCtx) + require.NoError(t, err) + require.NotNil(t, modelNew) + + model := modelNew.(*model) + + wc := model.whisperContext() + require.NotNil(t, wc) + + // Should be usable before model.Close + raw, err := wc.unsafeContext() + require.NoError(t, err) + require.NotNil(t, raw) + + // Close model should close underlying context + require.NoError(t, model.Close()) + + assert.True(t, wc.IsClosed()) + raw, err = wc.unsafeContext() + assert.Nil(t, raw) + require.ErrorIs(t, err, ErrModelClosed) + + // Idempotent close on wrapper + require.NoError(t, wc.Close()) +} diff --git a/bindings/go/pkg/whisper/whisper_state_test.go b/bindings/go/pkg/whisper/whisper_state_test.go new file mode 100644 index 000000000..37d828acb --- /dev/null +++ b/bindings/go/pkg/whisper/whisper_state_test.go @@ -0,0 +1,53 @@ +package whisper + +import ( + "os" + "testing" + + w "github.com/ggerganov/whisper.cpp/bindings/go" + assert "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testModelPathState = "../../models/ggml-small.en.bin" + +func TestWhisperState_NilWrapper(t *testing.T) { + ws := newWhisperState(nil) + + state, err := ws.UnsafeState() + assert.Nil(t, state) + require.ErrorIs(t, err, ErrModelClosed) + + require.NoError(t, ws.Close()) + // idempotent + require.NoError(t, ws.Close()) +} + +func TestWhisperState_Lifecycle(t *testing.T) { + if _, err := os.Stat(testModelPathState); os.IsNotExist(err) { + t.Skip("Skipping test, model not found:", testModelPathState) + } + + ctx := w.Whisper_init(testModelPathState) + require.NotNil(t, ctx) + defer ctx.Whisper_free() + + state := ctx.Whisper_init_state() + require.NotNil(t, state) + + ws := newWhisperState(state) + + got, err := ws.UnsafeState() + require.NoError(t, err) + require.NotNil(t, got) + + // close frees underlying state and marks closed + require.NoError(t, ws.Close()) + + got, err = ws.UnsafeState() + assert.Nil(t, got) + require.ErrorIs(t, err, ErrModelClosed) + + // idempotent + require.NoError(t, ws.Close()) +}