refactor(go bindings): make ModelContext and StatefulContext public

This commit is contained in:
ciricc 2025-09-14 04:39:26 +03:00
parent 125ea6122b
commit 8f9ad60fca
7 changed files with 109 additions and 108 deletions

View File

@ -11,14 +11,15 @@ import (
whisper "github.com/ggerganov/whisper.cpp/bindings/go"
)
type context struct {
type StatefulContext struct {
n int
model *model
model *ModelContext
st *whisperState
params *Parameters
}
func NewContext(model *model, params *Parameters) (*context, error) {
// NewStatefulContext creates a new stateful context
func NewStatefulContext(model *ModelContext, params *Parameters) (*StatefulContext, error) {
if model == nil {
return nil, errModelRequired
}
@ -27,12 +28,12 @@ func NewContext(model *model, params *Parameters) (*context, error) {
return nil, errParametersRequired
}
c := new(context)
c := new(StatefulContext)
c.model = model
c.params = params
// allocate isolated state per context
ctx, err := model.whisperContext().unsafeContext()
ctx, err := model.ctxAccessor().context()
if err != nil {
return nil, err
}
@ -49,8 +50,8 @@ 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()
func (context *StatefulContext) DetectedLanguage() string {
ctx, err := context.model.ctxAccessor().context()
if err != nil {
return ""
}
@ -68,29 +69,29 @@ func (context *context) DetectedLanguage() string {
}
// Close frees the whisper state and marks the context as closed.
func (context *context) Close() error {
func (context *StatefulContext) Close() error {
return context.st.close()
}
// Params returns a high-level parameters wrapper
func (context *context) Params() *Parameters {
func (context *StatefulContext) 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 *context) ResetTimings() {
func (context *StatefulContext) 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 *context) PrintTimings() {
func (context *StatefulContext) PrintTimings() {
context.model.PrintTimings()
}
// SystemInfo returns the system information
func (context *context) SystemInfo() string {
func (context *StatefulContext) SystemInfo() string {
return fmt.Sprintf("system_info: n_threads = %d / %d | %s\n",
context.params.Threads(),
runtime.NumCPU(),
@ -101,8 +102,8 @@ func (context *context) SystemInfo() string {
// 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's state.
func (context *context) WhisperLangAutoDetect(offset_ms int, n_threads int) ([]float32, error) {
ctx, err := context.model.whisperContext().unsafeContext()
func (context *StatefulContext) WhisperLangAutoDetect(offset_ms int, n_threads int) ([]float32, error) {
ctx, err := context.model.ctxAccessor().context()
if err != nil {
return nil, err
}
@ -121,13 +122,13 @@ func (context *context) WhisperLangAutoDetect(offset_ms int, n_threads int) ([]f
}
// Process new sample data and return any errors
func (context *context) Process(
func (context *StatefulContext) Process(
data []float32,
callEncoderBegin EncoderBeginCallback,
callNewSegment SegmentCallback,
callProgress ProgressCallback,
) error {
ctx, err := context.model.whisperContext().unsafeContext()
ctx, err := context.model.ctxAccessor().context()
if err != nil {
return err
}
@ -169,8 +170,8 @@ 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()
func (context *StatefulContext) NextSegment() (Segment, error) {
ctx, err := context.model.ctxAccessor().context()
if err != nil {
return Segment{}, err
}
@ -190,55 +191,55 @@ func (context *context) NextSegment() (Segment, error) {
return result, nil
}
func (context *context) IsMultilingual() bool {
func (context *StatefulContext) IsMultilingual() bool {
return context.model.IsMultilingual()
}
// Token helpers
// Deprecated: Use Model.IsText() instead - token checking is model-specific.
func (context *context) IsText(t Token) bool {
func (context *StatefulContext) IsText(t Token) bool {
result, _ := context.model.tokenIdentifier().IsText(t)
return result
}
// Deprecated: Use Model.IsBEG() instead - token checking is model-specific.
func (context *context) IsBEG(t Token) bool {
func (context *StatefulContext) IsBEG(t Token) bool {
result, _ := context.model.tokenIdentifier().IsBEG(t)
return result
}
// Deprecated: Use Model.IsSOT() instead - token checking is model-specific.
func (context *context) IsSOT(t Token) bool {
func (context *StatefulContext) IsSOT(t Token) bool {
result, _ := context.model.tokenIdentifier().IsSOT(t)
return result
}
// Deprecated: Use Model.IsEOT() instead - token checking is model-specific.
func (context *context) IsEOT(t Token) bool {
func (context *StatefulContext) IsEOT(t Token) bool {
result, _ := context.model.tokenIdentifier().IsEOT(t)
return result
}
// Deprecated: Use Model.IsPREV() instead - token checking is model-specific.
func (context *context) IsPREV(t Token) bool {
func (context *StatefulContext) IsPREV(t Token) bool {
result, _ := context.model.tokenIdentifier().IsPREV(t)
return result
}
// Deprecated: Use Model.IsSOLM() instead - token checking is model-specific.
func (context *context) IsSOLM(t Token) bool {
func (context *StatefulContext) IsSOLM(t Token) bool {
result, _ := context.model.tokenIdentifier().IsSOLM(t)
return result
}
// Deprecated: Use Model.IsNOT() instead - token checking is model-specific.
func (context *context) IsNOT(t Token) bool {
func (context *StatefulContext) IsNOT(t Token) bool {
result, _ := context.model.tokenIdentifier().IsNOT(t)
return result
}
func (context *context) SetLanguage(lang string) error {
if context.model.whisperContext().isClosed() {
func (context *StatefulContext) SetLanguage(lang string) error {
if context.model.ctxAccessor().isClosed() {
// TODO: remove this logic after deprecating the ErrInternalAppError
return ErrModelClosed
}
@ -251,7 +252,7 @@ func (context *context) SetLanguage(lang string) error {
}
// Deprecated: Use Model.IsLANG() instead - token checking is model-specific.
func (context *context) IsLANG(t Token, lang string) bool {
func (context *StatefulContext) IsLANG(t Token, lang string) bool {
result, _ := context.model.tokenIdentifier().IsLANG(t, lang)
return result
}
@ -286,109 +287,109 @@ func toTokensFromState(ctx *whisper.Context, st *whisper.State, n int) []Token {
}
// Deprecated: Use Params().Language() instead
func (context *context) Language() string {
func (context *StatefulContext) Language() string {
return context.params.Language()
}
// Deprecated: Use Params().SetAudioCtx() instead
func (context *context) SetAudioCtx(n uint) {
func (context *StatefulContext) SetAudioCtx(n uint) {
context.params.SetAudioCtx(n)
}
// SetBeamSize implements Context.
// Deprecated: Use Params().SetBeamSize() instead
func (context *context) SetBeamSize(v int) {
func (context *StatefulContext) SetBeamSize(v int) {
context.params.SetBeamSize(v)
}
// SetDuration implements Context.
// Deprecated: Use Params().SetDuration() instead
func (context *context) SetDuration(v time.Duration) {
func (context *StatefulContext) SetDuration(v time.Duration) {
context.params.SetDuration(v)
}
// SetEntropyThold implements Context.
// Deprecated: Use Params().SetEntropyThold() instead
func (context *context) SetEntropyThold(v float32) {
func (context *StatefulContext) SetEntropyThold(v float32) {
context.params.SetEntropyThold(v)
}
// SetInitialPrompt implements Context.
// Deprecated: Use Params().SetInitialPrompt() instead
func (context *context) SetInitialPrompt(v string) {
func (context *StatefulContext) SetInitialPrompt(v string) {
context.params.SetInitialPrompt(v)
}
// SetMaxContext implements Context.
// Deprecated: Use Params().SetMaxContext() instead
func (context *context) SetMaxContext(v int) {
func (context *StatefulContext) SetMaxContext(v int) {
context.params.SetMaxContext(v)
}
// SetMaxSegmentLength implements Context.
// Deprecated: Use Params().SetMaxSegmentLength() instead
func (context *context) SetMaxSegmentLength(v uint) {
func (context *StatefulContext) SetMaxSegmentLength(v uint) {
context.params.SetMaxSegmentLength(v)
}
// SetMaxTokensPerSegment implements Context.
// Deprecated: Use Params().SetMaxTokensPerSegment() instead
func (context *context) SetMaxTokensPerSegment(v uint) {
func (context *StatefulContext) SetMaxTokensPerSegment(v uint) {
context.params.SetMaxTokensPerSegment(v)
}
// SetOffset implements Context.
// Deprecated: Use Params().SetOffset() instead
func (context *context) SetOffset(v time.Duration) {
func (context *StatefulContext) SetOffset(v time.Duration) {
context.params.SetOffset(v)
}
// SetSplitOnWord implements Context.
// Deprecated: Use Params().SetSplitOnWord() instead
func (context *context) SetSplitOnWord(v bool) {
func (context *StatefulContext) SetSplitOnWord(v bool) {
context.params.SetSplitOnWord(v)
}
// SetTemperature implements Context.
// Deprecated: Use Params().SetTemperature() instead
func (context *context) SetTemperature(v float32) {
func (context *StatefulContext) SetTemperature(v float32) {
context.params.SetTemperature(v)
}
// SetTemperatureFallback implements Context.
// Deprecated: Use Params().SetTemperatureFallback() instead
func (context *context) SetTemperatureFallback(v float32) {
func (context *StatefulContext) SetTemperatureFallback(v float32) {
context.params.SetTemperatureFallback(v)
}
// SetThreads implements Context.
// Deprecated: Use Params().SetThreads() instead
func (context *context) SetThreads(v uint) {
func (context *StatefulContext) SetThreads(v uint) {
context.params.SetThreads(v)
}
// SetTokenSumThreshold implements Context.
// Deprecated: Use Params().SetTokenSumThreshold() instead
func (context *context) SetTokenSumThreshold(v float32) {
func (context *StatefulContext) SetTokenSumThreshold(v float32) {
context.params.SetTokenSumThreshold(v)
}
// SetTokenThreshold implements Context.
// Deprecated: Use Params().SetTokenThreshold() instead
func (context *context) SetTokenThreshold(v float32) {
func (context *StatefulContext) SetTokenThreshold(v float32) {
context.params.SetTokenThreshold(v)
}
// SetTokenTimestamps implements Context.
// Deprecated: Use Params().SetTokenTimestamps() instead
func (context *context) SetTokenTimestamps(v bool) {
func (context *StatefulContext) SetTokenTimestamps(v bool) {
context.params.SetTokenTimestamps(v)
}
// SetTranslate implements Context.
// Deprecated: Use Params().SetTranslate() instead
func (context *context) SetTranslate(v bool) {
func (context *StatefulContext) SetTranslate(v bool) {
context.params.SetTranslate(v)
}
var _ Context = (*context)(nil)
var _ Context = (*StatefulContext)(nil)

View File

@ -325,7 +325,7 @@ func TestContext_VAD_And_Diarization_Params_DoNotPanic(t *testing.T) {
assert.Equal(uint16(1), dec.NumChans)
data := buf.AsFloat32Buffer().Data
model, err := whisper.NewModel(ModelPath)
model, err := whisper.NewModelContext(ModelPath)
assert.NoError(err)
defer func() { _ = model.Close() }()
@ -333,7 +333,7 @@ func TestContext_VAD_And_Diarization_Params_DoNotPanic(t *testing.T) {
assert.NoError(err)
assert.NotNil(params)
ctx, err := whisper.NewContext(model, params)
ctx, err := whisper.NewStatefulContext(model, params)
assert.NoError(err)
defer func() { _ = ctx.Close() }()
@ -362,7 +362,7 @@ func TestDiarization_TwoSpeakers_Boundaries(t *testing.T) {
require.NoError(t, err)
data := buf.AsFloat32Buffer().Data
model, err := whisper.NewModel(ModelTinydiarizePath)
model, err := whisper.NewModelContext(ModelTinydiarizePath)
require.NoError(t, err)
defer func() { _ = model.Close() }()
@ -377,7 +377,7 @@ func TestDiarization_TwoSpeakers_Boundaries(t *testing.T) {
require.NoError(t, err)
// diarize ON with beam search and tighter segmentation
ctxOn, err := whisper.NewContext(model, params)
ctxOn, err := whisper.NewStatefulContext(model, params)
require.NoError(t, err)
defer func() { _ = ctxOn.Close() }()
@ -396,7 +396,7 @@ func TestDiarization_TwoSpeakers_Boundaries(t *testing.T) {
require.Greater(t, turnsOn, 0, "expected speaker turn boundaries with diarization enabled")
// diarize OFF baseline with same segmentation and beam
ctxOff, err := whisper.NewContext(model, params)
ctxOff, err := whisper.NewStatefulContext(model, params)
require.NoError(t, err)
defer func() { _ = ctxOff.Close() }()

View File

@ -8,32 +8,32 @@ import (
whisper "github.com/ggerganov/whisper.cpp/bindings/go"
)
type model struct {
type ModelContext struct {
path string
ctx *whisperCtx
ca *ctxAccessor
tokId *tokenIdentifier
}
// Make sure model adheres to the interface
var _ Model = (*model)(nil)
var _ Model = (*ModelContext)(nil)
// Deprecated: Use NewModel instead
// Deprecated: Use NewModelContext instead
func New(path string) (Model, error) {
return NewModel(path)
return NewModelContext(path)
}
// NewModel creates a new model without initializing the context
func NewModel(
// NewModelContext creates a new model context
func NewModelContext(
path string,
) (*model, error) {
model := new(model)
) (*ModelContext, error) {
model := new(ModelContext)
if _, err := os.Stat(path); err != nil {
return nil, err
} else if ctx := whisper.Whisper_init(path); ctx == nil {
return nil, ErrUnableToLoadModel
} else {
model.ctx = newWhisperCtx(ctx)
model.tokId = newTokenIdentifier(model.ctx)
model.ca = newCtxAccessor(ctx)
model.tokId = newTokenIdentifier(model.ca)
model.path = path
}
@ -41,17 +41,17 @@ func NewModel(
return model, nil
}
func (model *model) Close() error {
return model.ctx.close()
func (model *ModelContext) Close() error {
return model.ca.close()
}
func (model *model) whisperContext() *whisperCtx {
return model.ctx
func (model *ModelContext) ctxAccessor() *ctxAccessor {
return model.ca
}
func (model *model) String() string {
func (model *ModelContext) String() string {
str := "<whisper.model"
if model.ctx != nil {
if model.ca != nil {
str += fmt.Sprintf(" model=%q", model.path)
}
@ -59,8 +59,8 @@ 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()
func (model *ModelContext) IsMultilingual() bool {
ctx, err := model.ca.context()
if err != nil {
return false
}
@ -69,8 +69,8 @@ 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()
func (model *ModelContext) Languages() []string {
ctx, err := model.ca.context()
if err != nil {
return nil
}
@ -88,7 +88,7 @@ func (model *model) Languages() []string {
// NewContext creates a new speech-to-text context.
// Each context is backed by an isolated whisper_state for safe concurrent processing.
func (model *model) NewContext() (Context, error) {
func (model *ModelContext) NewContext() (Context, error) {
// Create new context with default params
params, err := NewParameters(model, SAMPLING_GREEDY, nil)
if err != nil {
@ -96,15 +96,15 @@ func (model *model) NewContext() (Context, error) {
}
// Return new context (now state-backed)
return NewContext(
return NewStatefulContext(
model,
params,
)
}
// PrintTimings prints the model performance timings to stdout.
func (model *model) PrintTimings() {
ctx, err := model.ctx.unsafeContext()
func (model *ModelContext) PrintTimings() {
ctx, err := model.ca.context()
if err != nil {
return
}
@ -113,8 +113,8 @@ func (model *model) PrintTimings() {
}
// ResetTimings resets the model performance timing counters.
func (model *model) ResetTimings() {
ctx, err := model.ctx.unsafeContext()
func (model *ModelContext) ResetTimings() {
ctx, err := model.ca.context()
if err != nil {
return
}
@ -122,6 +122,6 @@ func (model *model) ResetTimings() {
ctx.Whisper_reset_timings()
}
func (model *model) tokenIdentifier() *tokenIdentifier {
func (model *ModelContext) tokenIdentifier() *tokenIdentifier {
return model.tokId
}

View File

@ -26,11 +26,11 @@ func defaultParamsConfigure(params *Parameters) {
}
func NewParameters(
model *model,
model *ModelContext,
sampling SamplingStrategy,
configure ParamsConfigure,
) (*Parameters, error) {
ctx, err := model.ctx.unsafeContext()
ctx, err := model.ca.context()
if err != nil {
return nil, ErrModelClosed
}

View File

@ -3,10 +3,10 @@ package whisper
import whisper "github.com/ggerganov/whisper.cpp/bindings/go"
type tokenIdentifier struct {
ctx *whisperCtx
ctx *ctxAccessor
}
func newTokenIdentifier(whisperContext *whisperCtx) *tokenIdentifier {
func newTokenIdentifier(whisperContext *ctxAccessor) *tokenIdentifier {
return &tokenIdentifier{
ctx: whisperContext,
}
@ -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.context()
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.context()
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.context()
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.context()
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.context()
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.context()
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.context()
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.context()
if err != nil {
return false, err
}

View File

@ -2,17 +2,17 @@ package whisper
import whisper "github.com/ggerganov/whisper.cpp/bindings/go"
type whisperCtx struct {
type ctxAccessor struct {
ctx *whisper.Context
}
func newWhisperCtx(ctx *whisper.Context) *whisperCtx {
return &whisperCtx{
func newCtxAccessor(ctx *whisper.Context) *ctxAccessor {
return &ctxAccessor{
ctx: ctx,
}
}
func (ctx *whisperCtx) close() error {
func (ctx *ctxAccessor) close() error {
if ctx.ctx == nil {
return nil
}
@ -23,11 +23,11 @@ func (ctx *whisperCtx) close() error {
return nil
}
func (ctx *whisperCtx) isClosed() bool {
func (ctx *ctxAccessor) isClosed() bool {
return ctx.ctx == nil
}
func (ctx *whisperCtx) unsafeContext() (*whisper.Context, error) {
func (ctx *ctxAccessor) context() (*whisper.Context, error) {
if ctx.isClosed() {
return nil, ErrModelClosed
}

View File

@ -12,11 +12,11 @@ import (
const testModelPathCtx = "../../models/ggml-small.en.bin"
func TestWhisperCtx_NilWrapper(t *testing.T) {
wctx := newWhisperCtx(nil)
wctx := newCtxAccessor(nil)
assert.True(t, wctx.isClosed())
raw, err := wctx.unsafeContext()
raw, err := wctx.context()
assert.Nil(t, raw)
require.ErrorIs(t, err, ErrModelClosed)
@ -33,10 +33,10 @@ func TestWhisperCtx_Lifecycle(t *testing.T) {
raw := w.Whisper_init(testModelPathCtx)
require.NotNil(t, raw)
wctx := newWhisperCtx(raw)
wctx := newCtxAccessor(raw)
assert.False(t, wctx.isClosed())
got, err := wctx.unsafeContext()
got, err := wctx.context()
require.NoError(t, err)
require.NotNil(t, got)
@ -44,7 +44,7 @@ func TestWhisperCtx_Lifecycle(t *testing.T) {
require.NoError(t, wctx.close())
assert.True(t, wctx.isClosed())
got, err = wctx.unsafeContext()
got, err = wctx.context()
assert.Nil(t, got)
require.ErrorIs(t, err, ErrModelClosed)
@ -62,13 +62,13 @@ func TestWhisperCtx_FromModelLifecycle(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, modelNew)
model := modelNew.(*model)
model := modelNew.(*ModelContext)
wc := model.whisperContext()
wc := model.ctxAccessor()
require.NotNil(t, wc)
// Should be usable before model.Close
raw, err := wc.unsafeContext()
raw, err := wc.context()
require.NoError(t, err)
require.NotNil(t, raw)
@ -76,7 +76,7 @@ func TestWhisperCtx_FromModelLifecycle(t *testing.T) {
require.NoError(t, model.Close())
assert.True(t, wc.isClosed())
raw, err = wc.unsafeContext()
raw, err = wc.context()
assert.Nil(t, raw)
require.ErrorIs(t, err, ErrModelClosed)