refactor(go bindings): remove public method accessing unsafe whisper

This commit is contained in:
ciricc 2025-09-14 01:47:57 +03:00
parent 2f16f8039d
commit 01f5c6b708
9 changed files with 233 additions and 60 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ type parameters struct {
p *whisper.Params
}
func newParameters(whisperParams *whisper.Params) Parameters {
func newParameters(whisperParams *whisper.Params) *parameters {
return &parameters{
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 = &parameters{}

View File

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

View File

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

View File

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

View File

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