ruby : Add #free method, check MemoryView strictly (#4032)

* Bump version to 1.3.9

* Fix JFKReader's format

* Add Whisper::Context#free

* Add test for Whisper::Context#free

* Add Whisper::Context#free to RBS

* Add Whisper::VAD::Context#free

* Add test for Whisper::VAD::Context#free

* Add Whisper::VAD::Context#free to RBS

* Add Parakeet::Context#free

* Add test for Parakeet::Context#free

* Add Parakeet::Context#free to RBS

* Check MemoryView format more strictly

* Fallback MemoryView flags to SIMPLE when MemoryView not available

* Add changelog entries

* Update document on callbacks

* Remove whitespace

Co-authored-by: Daniel Bevenius <daniel.bevenius@gmail.com>

---------

Co-authored-by: Daniel Bevenius <daniel.bevenius@gmail.com>
This commit is contained in:
KITAITI Makoto
2026-09-04 14:33:37 +09:00
committed by GitHub
co-authored by Daniel Bevenius
parent eacbd8234c
commit 642b5d3260
12 changed files with 128 additions and 17 deletions
+8
View File
@@ -1,5 +1,13 @@
# Changelog
## [1.3.9] - Unreleased
Bundled whisper.cpp: `1.9.3`
- Fixed test fixture JFKReader's format string.
- Add #free method to `Whisper::Context`, `Whisper::VAD::Context` and `Whisper::Parakeet::Context`.
- Checked MemoryView format more strictly.
## [1.3.8] - 2026-07-28
Bundled whisper.cpp: `1.9.1`
+1 -1
View File
@@ -202,7 +202,7 @@ whisper.transcribe("path/to/audio.wav", params, n_processors: Etc.nprocessors)
Note that transcription occasionally might be low accuracy when it works in parallel.
If n_processors is greater than 1, you cannot set any callbacks including new_segment_callback, progress_callback, encoder_begin_callback, abort_callback, and log_callback set by Whisper.log_set.
If n_processors is greater than 1, you cannot set some callbacks including encoder_begin_callback, abort_callback. Also, progress_callback is ignored even if it's set.
### Segments ###
+3 -3
View File
@@ -169,7 +169,7 @@ extern VALUE ruby_whisper_log_queue_drain(ruby_whisper_log_queue *log_queue);
#define GetContext(obj, rw) do { \
TypedData_Get_Struct((obj), ruby_whisper, &ruby_whisper_type, (rw)); \
if ((rw)->context == NULL) { \
rb_raise(rb_eRuntimeError, "Not initialized"); \
rb_raise(rb_eRuntimeError, "Already freed or not initialized"); \
} \
} while (0)
@@ -187,7 +187,7 @@ extern VALUE ruby_whisper_log_queue_drain(ruby_whisper_log_queue *log_queue);
#define GetVADContext(obj, rwvc) do { \
TypedData_Get_Struct((obj), ruby_whisper_vad_context, &ruby_whisper_vad_context_type, (rwvc)); \
if ((rwvc)->context == NULL) { \
rb_raise(rb_eRuntimeError, "Not initialized"); \
rb_raise(rb_eRuntimeError, "Already freed or not initialized"); \
} \
} while (0)
@@ -209,7 +209,7 @@ extern VALUE ruby_whisper_log_queue_drain(ruby_whisper_log_queue *log_queue);
#define GetParakeetContext(obj, rwpc) do { \
TypedData_Get_Struct((obj), ruby_whisper_parakeet_context, &ruby_whisper_parakeet_context_type, (rwpc)); \
if ((rwpc)->context == NULL) { \
rb_raise(rb_eRuntimeError, "Not initialized"); \
rb_raise(rb_eRuntimeError, "Already freed or not initialized"); \
} \
} while (0)
+60 -11
View File
@@ -316,23 +316,49 @@ VALUE ruby_whisper_model_type(VALUE self)
static bool
check_memory_view(rb_memory_view_t *memview)
{
rb_memory_view_prepare_item_desc(memview);
if (!memview->format) {
rb_warn("currently format is required");
rb_warn("format is required");
return false;
}
if (strcmp(memview->format, "f") == 0) {
// accept
} else if (strcmp(memview->format, "e") == 0) {
if (IS_BIGENDIAN) {
rb_warn("currently format \"e\" is only supported on little-endian environment");
return false;
}
} else {
rb_warn("currently only format \"f\" and \"e\" on little-endian environment is supported for MemoryView, but given: %s", memview->format);
if (memview->item_desc.length != 1) {
rb_warn("format must be exact one character");
return false;
}
rb_memory_view_item_component_t component = memview->item_desc.components[0];
if (component.offset) {
rb_warn("format has offset");
return false;
}
if (component.repeat != 1) {
rb_warn("format repeated");
return false;
}
switch (component.format) {
case 'f':
// accept
break;
case 'e':
if (IS_BIGENDIAN) {
rb_warn("currently format \"e\" is only supported on little-endian environment");
return false;
}
break;
case 'g':
if (!IS_BIGENDIAN) {
rb_warn("currently format \"g\" is only supported on big-endian environment");
return false;
}
break;
default:
rb_warn("currently only format \"f\", \"e\" on little-endian environment and \"g\" on big-endian environment are supported for MemoryView, but given: %c", component.format);
return false;
}
if (memview->ndim != 1 && !(memview->ndim == 2 && memview->shape[1] == 1)) {
// TODO: Accept ndim == 2 with shape [n_samples, channels] and channels > 1 by averaging the samples in different channels or just taking the first channel
rb_warn("currently only 1 dimensional MemoryView is supported, but given: %zd", memview->ndim);
@@ -394,13 +420,24 @@ parse_samples(VALUE *samples, VALUE *n_samples)
}
parsed.n_samples = (int)RARRAY_LEN(*samples);
} else if (memview_available) {
bool memview_got = rb_memory_view_get(*samples, &parsed.memview, RUBY_MEMORY_VIEW_SIMPLE);
bool memview_got = rb_memory_view_get(*samples, &parsed.memview, RUBY_MEMORY_VIEW_FORMAT | RUBY_MEMORY_VIEW_ROW_MAJOR);
if (memview_got) {
parsed.memview_exported = check_memory_view(&parsed.memview);
if (!parsed.memview_exported) {
rb_memory_view_release(&parsed.memview);
parsed.memview = (rb_memory_view_t){0};
}
} else {
// Sometimes MemoryView producers accept only SIMPLE flag even when they provide suitable MemoryView
parsed.memview = (rb_memory_view_t){0};
bool simple_memview_got = rb_memory_view_get(*samples, &parsed.memview, RUBY_MEMORY_VIEW_SIMPLE);
if (simple_memview_got) {
parsed.memview_exported = check_memory_view(&parsed.memview);
if (!parsed.memview_exported) {
rb_memory_view_release(&parsed.memview);
parsed.memview = (rb_memory_view_t){0};
}
}
}
if (parsed.memview_exported) {
ssize_t n_samples_size = parsed.memview.byte_size / parsed.memview.item_size;
@@ -791,6 +828,17 @@ ruby_whisper_full_get_vad_segment_t1(VALUE self, VALUE i_segment)
return LONG2NUM(whisper_full_get_vad_segment_t1(rw->context, c_i_segment));
}
static VALUE
ruby_whisper_context_free(VALUE self)
{
ruby_whisper *rw;
GetContext(self, rw);
whisper_free(rw->context);
rw->context = NULL;
return Qnil;
}
// High level API
static VALUE
@@ -881,6 +929,7 @@ init_ruby_whisper_context(VALUE *mWhisper)
rb_define_method(cContext, "full_get_vad_segment_t1", ruby_whisper_full_get_vad_segment_t1, 1);
rb_define_method(cContext, "full", ruby_whisper_full, -1);
rb_define_method(cContext, "full_parallel", ruby_whisper_full_parallel, -1);
rb_define_method(cContext, "free", ruby_whisper_context_free, 0);
// High level
rb_define_method(cContext, "full_get_segment", ruby_whisper_full_get_segment, 1);
@@ -269,6 +269,17 @@ ruby_whisper_parakeet_context_full(int argc, VALUE *argv, VALUE self)
}
}
static VALUE
ruby_whisper_parakeet_free(VALUE self)
{
ruby_whisper_parakeet_context *rwpc;
GetParakeetContext(self, rwpc);
parakeet_free(rwpc->context);
rwpc->context = NULL;
return Qnil;
}
static VALUE
ruby_whisper_parakeet_context_get_model(VALUE self)
{
@@ -289,6 +300,7 @@ init_ruby_whisper_parakeet_context(VALUE *mParakeet)
rb_define_method(cParakeetContext, "model", ruby_whisper_parakeet_context_get_model, 0);
rb_define_method(cParakeetContext, "each_segment", ruby_whisper_parakeet_context_each_segment, 0);
rb_define_method(cParakeetContext, "full", ruby_whisper_parakeet_context_full, -1);
rb_define_method(cParakeetContext, "free", ruby_whisper_parakeet_free, 0);
#define REGISTER_SEGMENT_ATTR(name, type) \
rb_define_method(cParakeetContext, "full_" #name, ruby_whisper_parakeet_context_full_##name, 1);
@@ -132,6 +132,17 @@ ruby_whisper_vad_segments_from_samples(int argc, VALUE *argv, VALUE self)
return segments;
}
static VALUE
ruby_whisper_vad_free(VALUE self)
{
ruby_whisper_vad_context *rwvc;
GetVADContext(self, rwvc);
whisper_vad_free(rwvc->context);
rwvc->context = NULL;
return Qnil;
}
void init_ruby_whisper_vad_context(VALUE *mVAD)
{
cVADContext = rb_define_class_under(*mVAD, "Context", rb_cObject);
@@ -139,4 +150,5 @@ void init_ruby_whisper_vad_context(VALUE *mVAD)
rb_define_method(cVADContext, "initialize", ruby_whisper_vad_context_initialize, 1);
rb_define_method(cVADContext, "segments_from_samples", ruby_whisper_vad_segments_from_samples, -1);
rb_define_method(cVADContext, "detect", ruby_whisper_vad_detect, 2);
rb_define_method(cVADContext, "free", ruby_whisper_vad_free, 0);
}
+6
View File
@@ -159,6 +159,9 @@ module Whisper
| (Whisper::Params, _Samples, ?Integer n_samples) -> self
| (Whisper::Params, _Samples, ?Integer? n_samples, Integer n_processors) -> self
# Free context and unload the model.
def free: () -> void
class Params
def self.new: (
?use_gpu: boolish,
@@ -708,6 +711,8 @@ module Whisper
def each_segment: { (Segment) -> void } -> void
| () -> Enumerator[Segment]
def free: () -> void
class Params
def self.new: (?use_gpu: boolish, ?gpu_device: Integer) -> instance
def use_gpu: () -> boolish
@@ -1000,6 +1005,7 @@ module Whisper
def segments_from_samples: (Params, Array[Float] samples, ?Integer n_samples) -> Segments
| (Params, _Samples, ?Integer n_samples) -> Segments
def detect: (path wav_file_path, Params) -> Segments
def free: () -> void
end
class Segments
+1 -1
View File
@@ -69,7 +69,7 @@ jfk_reader_get_memory_view(const VALUE obj, rb_memory_view_t *view, int flags)
view->data = (void *)args.data;
view->byte_size = sizeof(float) * n_samples;
view->readonly = true;
view->format = "f";
view->format = "e";
view->item_size = sizeof(float);
view->item_desc.components = NULL;
view->item_desc.length = 0;
@@ -113,4 +113,11 @@ class TestParakeetContext < TestBase
@parakeet.transcribe "nothing", @params
end
end
def test_free
@parakeet.free
assert_raise RuntimeError do
@parakeet.transcribe AUDIO, @params
end
end
end
+8
View File
@@ -25,6 +25,14 @@ class TestVADContext < TestBase
end
end
def test_free
vad = Whisper::VAD::Context.new("silero-v6.2.0")
vad.free
assert_raise RuntimeError do
vad.detect(AUDIO, Whisper::VAD::Params.new)
end
end
private
def assert_segments(segments)
+9
View File
@@ -50,6 +50,15 @@ class TestWhisper < TestBase
end
end
def test_free
whisper = Whisper::Context.new("base.en")
whisper.free
assert_raise RuntimeError do
whisper.model_type
end
end
private
def without_log_callback
+1 -1
View File
@@ -3,7 +3,7 @@ require_relative "extsources"
Gem::Specification.new do |s|
s.name = "whispercpp"
s.authors = ["Georgi Gerganov", "Todd A. Fisher"]
s.version = '1.3.8'
s.version = '1.3.9'
s.description = %q{High-performance inference of OpenAI's Whisper automatic speech recognition (ASR) model via Ruby}
s.email = 'todd.fisher@gmail.com'
s.extra_rdoc_files = ['LICENSE', 'README.md']