fix(go bindings): unit tests and exported interfaces

This commit is contained in:
ciricc 2025-09-14 02:41:28 +03:00
parent 221e93a5d7
commit b751ec1f55
9 changed files with 64 additions and 157 deletions

View File

@ -19,8 +19,12 @@ var (
ErrProcessingFailed = errors.New("processing failed")
ErrUnsupportedLanguage = errors.New("unsupported language")
ErrModelNotMultilingual = errors.New("model is not multilingual")
ErrUnableToCreateState = errors.New("unable to create state")
ErrModelClosed = errors.Join(errors.New("model has been closed"), ErrInternalAppError)
// Private errors
errParametersRequired = errors.New("parameters are required")
errModelRequired = errors.New("model is required")
errUnableToCreateState = errors.New("unable to create state")
)
///////////////////////////////////////////////////////////////////////////////

View File

@ -14,11 +14,19 @@ import (
type context struct {
n int
model *model
st WhisperState
st *whisperState
*Parameters
}
func NewContext(model *model, params *Parameters) (*context, error) {
if model == nil {
return nil, errModelRequired
}
if params == nil {
return nil, errParametersRequired
}
c := new(context)
c.model = model
@ -32,7 +40,7 @@ func NewContext(model *model, params *Parameters) (*context, error) {
st := ctx.Whisper_init_state()
if st == nil {
return nil, ErrUnableToCreateState
return nil, errUnableToCreateState
}
c.st = newWhisperState(st)
@ -48,7 +56,7 @@ func (context *context) DetectedLanguage() string {
return ""
}
st, err := context.st.UnsafeState()
st, err := context.st.unsafeState()
if err != nil {
return ""
}
@ -62,7 +70,7 @@ func (context *context) DetectedLanguage() string {
// Close frees the whisper state and marks the context as closed.
func (context *context) Close() error {
return context.st.Close()
return context.st.close()
}
// Params returns a high-level parameters wrapper
@ -100,7 +108,7 @@ func (context *context) WhisperLangAutoDetect(offset_ms int, n_threads int) ([]f
return nil, err
}
st, err := context.st.UnsafeState()
st, err := context.st.unsafeState()
if err != nil {
return nil, err
}
@ -135,7 +143,7 @@ func (context *context) Process(
return err
}
st, err := context.st.UnsafeState()
st, err := context.st.unsafeState()
if err != nil {
return err
}
@ -168,7 +176,7 @@ func (context *context) NextSegment() (Segment, error) {
return Segment{}, err
}
st, err := context.st.UnsafeState()
st, err := context.st.unsafeState()
if err != nil {
return Segment{}, err
}
@ -190,48 +198,48 @@ func (context *context) IsMultilingual() bool {
// Token helpers
// Deprecated: Use Model.IsText() instead - token checking is model-specific.
func (context *context) IsText(t Token) bool {
result, _ := context.model.TokenIdentifier().IsText(t)
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 {
result, _ := context.model.TokenIdentifier().IsBEG(t)
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 {
result, _ := context.model.TokenIdentifier().IsSOT(t)
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 {
result, _ := context.model.TokenIdentifier().IsEOT(t)
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 {
result, _ := context.model.TokenIdentifier().IsPREV(t)
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 {
result, _ := context.model.TokenIdentifier().IsSOLM(t)
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 {
result, _ := context.model.TokenIdentifier().IsNOT(t)
result, _ := context.model.tokenIdentifier().IsNOT(t)
return result
}
func (context *context) SetLanguage(lang string) error {
if context.model.whisperContext().IsClosed() {
if context.model.whisperContext().isClosed() {
// TODO: remove this logic after deprecating the ErrInternalAppError
return ErrModelClosed
}
@ -245,7 +253,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 {
result, _ := context.model.TokenIdentifier().IsLANG(t, lang)
result, _ := context.model.tokenIdentifier().IsLANG(t, lang)
return result
}

View File

@ -329,7 +329,11 @@ func TestContext_VAD_And_Diarization_Params_DoNotPanic(t *testing.T) {
assert.NoError(err)
defer func() { _ = model.Close() }()
ctx, err := whisper.NewContext(model, nil)
params, err := whisper.NewParameters(model, whisper.SAMPLING_GREEDY, nil)
assert.NoError(err)
assert.NotNil(params)
ctx, err := whisper.NewContext(model, params)
assert.NoError(err)
defer func() { _ = ctx.Close() }()

View File

@ -20,32 +20,6 @@ type ProgressCallback func(int)
// continue processing. It is called during the Process function
type EncoderBeginCallback func() bool
type TokenIdentifier interface {
// Test for "begin" token
IsBEG(Token) (bool, error)
// Test for "start of transcription" token
IsSOT(Token) (bool, error)
// Test for "end of transcription" token
IsEOT(Token) (bool, error)
// Test for "start of prev" token
IsPREV(Token) (bool, error)
// Test for "start of lm" token
IsSOLM(Token) (bool, error)
// Test for "no timestamps" token
IsNOT(Token) (bool, error)
// Test for token associated with a specific language
IsLANG(Token, string) (bool, error)
// Test for text token
IsText(Token) (bool, error)
}
type ParamsConfigure func(*Parameters)
// Model is the interface to a whisper model. Create a new model with the
@ -74,65 +48,8 @@ type Model interface {
ResetTimings()
}
// // Parameters configures decode / processing behavior
// type Parameters interface {
// SetTranslate(bool)
// SetSplitOnWord(bool)
// SetThreads(uint)
// SetOffset(time.Duration)
// SetDuration(time.Duration)
// SetTokenThreshold(float32)
// SetTokenSumThreshold(float32)
// SetMaxSegmentLength(uint)
// SetTokenTimestamps(bool)
// SetMaxTokensPerSegment(uint)
// SetAudioCtx(uint)
// SetMaxContext(n int)
// SetBeamSize(n int)
// SetEntropyThold(t float32)
// SetInitialPrompt(prompt string)
// SetNoContext(bool)
// SetPrintSpecial(bool)
// SetPrintProgress(bool)
// SetPrintRealtime(bool)
// SetPrintTimestamps(bool)
// // Enable extra debug info (e.g., dump log_mel)
// SetDebugMode(bool)
// // Diarization (tinydiarize)
// SetDiarize(bool)
// // Voice Activity Detection (VAD)
// SetVAD(bool)
// SetVADModelPath(string)
// SetVADThreshold(float32)
// SetVADMinSpeechMs(int)
// SetVADMinSilenceMs(int)
// SetVADMaxSpeechSec(float32)
// SetVADSpeechPadMs(int)
// SetVADSamplesOverlap(float32)
// // Set the temperature
// SetTemperature(t float32)
// // 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
// SetSingleSegment(bool)
// // Getter methods
// Language() string
// Threads() int
// }
// Context is the speech recognition context.
// Deprecated: Use NewContext implementation struct instead of relying on this interface
type Context interface {
io.Closer

View File

@ -9,9 +9,9 @@ import (
)
type model struct {
path string
ctx *whisperCtx
tokenIdentifier *tokenIdentifier
path string
ctx *whisperCtx
tokId *tokenIdentifier
}
// Make sure model adheres to the interface
@ -33,7 +33,7 @@ func NewModel(
return nil, ErrUnableToLoadModel
} else {
model.ctx = newWhisperCtx(ctx)
model.tokenIdentifier = newTokenIdentifier(model.ctx)
model.tokId = newTokenIdentifier(model.ctx)
model.path = path
}
@ -42,20 +42,13 @@ func NewModel(
}
func (model *model) Close() error {
return model.ctx.Close()
}
func (model *model) WhisperContext() WhisperContext {
return model.ctx
return model.ctx.close()
}
func (model *model) whisperContext() *whisperCtx {
return model.ctx
}
///////////////////////////////////////////////////////////////////////////////
// STRINGIFY
func (model *model) String() string {
str := "<whisper.model"
if model.ctx != nil {
@ -65,9 +58,6 @@ func (model *model) String() string {
return str + ">"
}
///////////////////////////////////////////////////////////////////////////////
// PUBLIC METHODS
// Return true if model is multilingual (language and translation options are supported)
func (model *model) IsMultilingual() bool {
ctx, err := model.ctx.unsafeContext()
@ -132,7 +122,6 @@ func (model *model) ResetTimings() {
ctx.Whisper_reset_timings()
}
// WhisperContext returns the low-level whisper context, or error if the model is closed.
func (model *model) TokenIdentifier() TokenIdentifier {
return model.tokenIdentifier
func (model *model) tokenIdentifier() *tokenIdentifier {
return model.tokId
}

View File

@ -2,14 +2,6 @@ package whisper
import whisper "github.com/ggerganov/whisper.cpp/bindings/go"
type WhisperContext interface {
// Close closes the whisper context
Close() error
// IsClosed returns true if the whisper context is closed
IsClosed() bool
}
type whisperCtx struct {
ctx *whisper.Context
}
@ -20,7 +12,7 @@ func newWhisperCtx(ctx *whisper.Context) *whisperCtx {
}
}
func (ctx *whisperCtx) Close() error {
func (ctx *whisperCtx) close() error {
if ctx.ctx == nil {
return nil
}
@ -31,16 +23,14 @@ func (ctx *whisperCtx) Close() error {
return nil
}
func (ctx *whisperCtx) IsClosed() bool {
func (ctx *whisperCtx) isClosed() bool {
return ctx.ctx == nil
}
func (ctx *whisperCtx) unsafeContext() (*whisper.Context, error) {
if ctx.IsClosed() {
if ctx.isClosed() {
return nil, ErrModelClosed
}
return ctx.ctx, nil
}
var _ WhisperContext = (*whisperCtx)(nil)

View File

@ -14,15 +14,15 @@ const testModelPathCtx = "../../models/ggml-small.en.bin"
func TestWhisperCtx_NilWrapper(t *testing.T) {
wctx := newWhisperCtx(nil)
assert.True(t, wctx.IsClosed())
assert.True(t, wctx.isClosed())
raw, err := wctx.unsafeContext()
assert.Nil(t, raw)
require.ErrorIs(t, err, ErrModelClosed)
require.NoError(t, wctx.Close())
require.NoError(t, wctx.close())
// idempotent
require.NoError(t, wctx.Close())
require.NoError(t, wctx.close())
}
func TestWhisperCtx_Lifecycle(t *testing.T) {
@ -34,22 +34,22 @@ func TestWhisperCtx_Lifecycle(t *testing.T) {
require.NotNil(t, raw)
wctx := newWhisperCtx(raw)
assert.False(t, wctx.IsClosed())
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())
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())
require.NoError(t, wctx.close())
// no further free; raw already freed by wctx.Close()
}
@ -75,11 +75,11 @@ func TestWhisperCtx_FromModelLifecycle(t *testing.T) {
// Close model should close underlying context
require.NoError(t, model.Close())
assert.True(t, wc.IsClosed())
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())
require.NoError(t, wc.close())
}

View File

@ -2,22 +2,17 @@ package whisper
import whisper "github.com/ggerganov/whisper.cpp/bindings/go"
type WhisperState interface {
Close() error
UnsafeState() (*whisper.State, error)
}
type whisperState struct {
state *whisper.State
}
func newWhisperState(state *whisper.State) WhisperState {
func newWhisperState(state *whisper.State) *whisperState {
return &whisperState{
state: state,
}
}
func (s *whisperState) Close() error {
func (s *whisperState) close() error {
if s.state == nil {
return nil
}
@ -28,7 +23,7 @@ func (s *whisperState) Close() error {
return nil
}
func (s *whisperState) UnsafeState() (*whisper.State, error) {
func (s *whisperState) unsafeState() (*whisper.State, error) {
if s.state == nil {
return nil, ErrModelClosed
}

View File

@ -14,13 +14,13 @@ const testModelPathState = "../../models/ggml-small.en.bin"
func TestWhisperState_NilWrapper(t *testing.T) {
ws := newWhisperState(nil)
state, err := ws.UnsafeState()
state, err := ws.unsafeState()
assert.Nil(t, state)
require.ErrorIs(t, err, ErrModelClosed)
require.NoError(t, ws.Close())
require.NoError(t, ws.close())
// idempotent
require.NoError(t, ws.Close())
require.NoError(t, ws.close())
}
func TestWhisperState_Lifecycle(t *testing.T) {
@ -37,17 +37,17 @@ func TestWhisperState_Lifecycle(t *testing.T) {
ws := newWhisperState(state)
got, err := ws.UnsafeState()
got, err := ws.unsafeState()
require.NoError(t, err)
require.NotNil(t, got)
// close frees underlying state and marks closed
require.NoError(t, ws.Close())
require.NoError(t, ws.close())
got, err = ws.UnsafeState()
got, err = ws.unsafeState()
assert.Nil(t, got)
require.ErrorIs(t, err, ErrModelClosed)
// idempotent
require.NoError(t, ws.Close())
require.NoError(t, ws.close())
}