From 642b5d3260e020c2fc6f34a9569d10ddd7672963 Mon Sep 17 00:00:00 2001 From: KITAITI Makoto Date: Fri, 4 Sep 2026 14:33:37 +0900 Subject: [PATCH] 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 --------- Co-authored-by: Daniel Bevenius --- bindings/ruby/CHANGELOG.md | 8 +++ bindings/ruby/README.md | 2 +- bindings/ruby/ext/ruby_whisper.h | 6 +- bindings/ruby/ext/ruby_whisper_context.c | 71 ++++++++++++++++--- .../ruby/ext/ruby_whisper_parakeet_context.c | 12 ++++ bindings/ruby/ext/ruby_whisper_vad_context.c | 12 ++++ bindings/ruby/sig/whisper.rbs | 6 ++ bindings/ruby/test/jfk_reader/jfk_reader.c | 2 +- bindings/ruby/test/test_parakeet_context.rb | 7 ++ bindings/ruby/test/test_vad_context.rb | 8 +++ bindings/ruby/test/test_whisper.rb | 9 +++ bindings/ruby/whispercpp.gemspec | 2 +- 12 files changed, 128 insertions(+), 17 deletions(-) diff --git a/bindings/ruby/CHANGELOG.md b/bindings/ruby/CHANGELOG.md index ee0cec1df..8f7d239ed 100644 --- a/bindings/ruby/CHANGELOG.md +++ b/bindings/ruby/CHANGELOG.md @@ -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` diff --git a/bindings/ruby/README.md b/bindings/ruby/README.md index d5ac2336a..67a9ec372 100644 --- a/bindings/ruby/README.md +++ b/bindings/ruby/README.md @@ -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 ### diff --git a/bindings/ruby/ext/ruby_whisper.h b/bindings/ruby/ext/ruby_whisper.h index b977703ee..85ac63e73 100644 --- a/bindings/ruby/ext/ruby_whisper.h +++ b/bindings/ruby/ext/ruby_whisper.h @@ -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) diff --git a/bindings/ruby/ext/ruby_whisper_context.c b/bindings/ruby/ext/ruby_whisper_context.c index 0210ac8a9..07879023f 100644 --- a/bindings/ruby/ext/ruby_whisper_context.c +++ b/bindings/ruby/ext/ruby_whisper_context.c @@ -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); diff --git a/bindings/ruby/ext/ruby_whisper_parakeet_context.c b/bindings/ruby/ext/ruby_whisper_parakeet_context.c index b4a2fc5c4..8ac551d4f 100644 --- a/bindings/ruby/ext/ruby_whisper_parakeet_context.c +++ b/bindings/ruby/ext/ruby_whisper_parakeet_context.c @@ -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); diff --git a/bindings/ruby/ext/ruby_whisper_vad_context.c b/bindings/ruby/ext/ruby_whisper_vad_context.c index 323200924..b98ed9301 100644 --- a/bindings/ruby/ext/ruby_whisper_vad_context.c +++ b/bindings/ruby/ext/ruby_whisper_vad_context.c @@ -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); } diff --git a/bindings/ruby/sig/whisper.rbs b/bindings/ruby/sig/whisper.rbs index f2ebf6185..6000641cf 100644 --- a/bindings/ruby/sig/whisper.rbs +++ b/bindings/ruby/sig/whisper.rbs @@ -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 diff --git a/bindings/ruby/test/jfk_reader/jfk_reader.c b/bindings/ruby/test/jfk_reader/jfk_reader.c index 62207aaa4..7c95f72fe 100644 --- a/bindings/ruby/test/jfk_reader/jfk_reader.c +++ b/bindings/ruby/test/jfk_reader/jfk_reader.c @@ -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; diff --git a/bindings/ruby/test/test_parakeet_context.rb b/bindings/ruby/test/test_parakeet_context.rb index 2d039ce75..cef6b14d4 100644 --- a/bindings/ruby/test/test_parakeet_context.rb +++ b/bindings/ruby/test/test_parakeet_context.rb @@ -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 diff --git a/bindings/ruby/test/test_vad_context.rb b/bindings/ruby/test/test_vad_context.rb index 777724675..9cc5016a0 100644 --- a/bindings/ruby/test/test_vad_context.rb +++ b/bindings/ruby/test/test_vad_context.rb @@ -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) diff --git a/bindings/ruby/test/test_whisper.rb b/bindings/ruby/test/test_whisper.rb index 082547e7c..ba5dbe7b1 100644 --- a/bindings/ruby/test/test_whisper.rb +++ b/bindings/ruby/test/test_whisper.rb @@ -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 diff --git a/bindings/ruby/whispercpp.gemspec b/bindings/ruby/whispercpp.gemspec index 20068aa16..e10521c49 100644 --- a/bindings/ruby/whispercpp.gemspec +++ b/bindings/ruby/whispercpp.gemspec @@ -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']