ruby : add VAD speech segments API (#3931)

* Bump version

* Add test for VAD segments API

* Implement VAD segments API

* Add signatures of VAD segments API

* Stop to dependent on mutex_m

* Check segment range for vad_segment_t0/t1

* Remove needless check

* Remove dead codes

* Fix index check for VAD segments

* Remove needless line

* Use NUM2DBL instead of RFLOAT_VALUE for safety

* Fix document

* Check type of samples

* Arrange CMake cache notation for older version

* Remove unnecessary line break

* Check return value of whisper_vad_segments_from_samples

* Fix type of VAD::Segment#start_time and #end_time

* Prevent dangling pointer from Params to VAD::Params

* Fix typos

* Update README

* Make test follow fix of impl

* Use segments_from_samples_body in ruby_whisper_vad_detect

* Run segments_from_samples without GVL

* Remove unnecessary codes

* [skip ci]Remove trailing 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-07-28 15:08:44 +09:00
committed by GitHub
co-authored by Daniel Bevenius
parent 080bbbe852
commit 97c56f1dc1
18 changed files with 140 additions and 80 deletions
+5 -4
View File
@@ -117,7 +117,7 @@ Whisper::Params.new(
threshold: 1.0, # defaults to 0.5
min_speech_duration_ms: 500, # defaults to 250
min_silence_duration_ms: 200, # defaults to 100
max_speech_duration_s: 30000, # default is FLT_MAX,
max_speech_duration_s: 30000.0, # default is FLT_MAX,
speech_pad_ms: 50, # defaults to 30
samples_overlap: 0.5 # defaults to 0.1
),
@@ -176,7 +176,7 @@ See whisper.cpp's [README](https://github.com/ggml-org/whisper.cpp/blob/master/R
Boolean options:
* `-DGGML_BLAS=1` -> `--enable-ggml-blas`
* `-DWHISER_COREML=OFF` -> `--disable-whisper-coreml`
* `-DWHISPER_COREML=OFF` -> `--disable-whisper-coreml`
Argument options:
@@ -184,7 +184,7 @@ Argument options:
Combination:
* `-DGGML_CUDA=1 -DCMAKE_CUDA_ARCHITECTURES="86"` -> `--enable-ggml-cuda --cmake_cuda-architectures="86"`
* `-DGGML_CUDA=1 -DCMAKE_CUDA_ARCHITECTURES="86"` -> `--enable-ggml-cuda --cmake-cuda-architectures="86"`
For boolean options like `GGML_CUDA`, the README says `-DGGML_CUDA=1`. You need strip `-D`, prepend `--enable-` for `1` or `ON` (`--disable-` for `0` or `OFF`) and make it kebab-case: `--enable-ggml-cuda`.
For options which require arguments like `CMAKE_CUDA_ARCHITECTURES`, the README says `-DCMAKE_CUDA_ARCHITECTURES="86"`. You need strip `-D`, prepend `--`, make it kebab-case, append `=` and append argument: `--cmake-cuda-architectures="86"`.
@@ -478,13 +478,14 @@ Development
% cd whisper.cpp/bindings/ruby
% rake test
First call of `rake test` builds an extension and downloads a model for testing. After that, you add tests in `tests` directory and modify `ext/ruby_whisper.cpp`.
First call of `rake test` builds an extension and downloads a model for testing. After that, you add tests in `test` directory and modify `ext/*.{h,c,cpp}`.
If something seems wrong on build, running `rake clean` solves some cases.
### Need help ###
* Windows support
* Check of compilation with various hardware
* Refinement of C/C++ code, especially memory management
License
+1 -1
View File
@@ -105,7 +105,7 @@ class Options
FileUtils.mkpath File.dirname(cache_path)
File.open cache_path, "w" do |file|
@options.reject {|name, (type, value)| value.nil?}.each do |name, (type, value)|
line = "set(CACHE{%<name>s} TYPE %<type>s FORCE VALUE %<value>s)" % {
line = 'set(%<name>s %<value>s CACHE %<type>s "" FORCE)' %{
name:,
type:,
value: value == true ? "ON" : value == false ? "OFF" : escape_cmake(value)
-1
View File
@@ -133,7 +133,6 @@ void Init_whisper() {
id_coreml_compiled_models = rb_intern("coreml_compiled_models");
id_cache = rb_intern("cache");
id_n_processors = rb_intern("n_processors");
id_extended = rb_intern("extended");
id_start_log_callback_thread = rb_intern("start_log_callback_thread");
id_log_callback_thread = rb_intern("@log_callback_thread");
id_alive_p = rb_intern("alive?");
+7 -1
View File
@@ -110,6 +110,13 @@ typedef struct {
int n_samples;
} ruby_whisper_full_args;
typedef struct segments_from_samples_args {
VALUE *context;
VALUE *params;
float *samples;
int n_samples;
} segments_from_samples_args;
typedef struct ruby_whisper_full_parallel_args {
VALUE *context;
VALUE *params;
@@ -149,7 +156,6 @@ typedef struct {
VALUE context;
} ruby_whisper_parakeet_model;
extern ID id_extended;
extern ID id_log_callback_thread;
extern ID id_start_log_callback_thread;
extern ID id_alive_p;
+50 -1
View File
@@ -349,7 +349,11 @@ fill_samples(VALUE rb_args)
if (RB_TYPE_P(*args->src, T_ARRAY)) {
for (int i = 0; i < args->n_samples; i++) {
args->dest[i] = RFLOAT_VALUE(rb_ary_entry(*args->src, i));
VALUE sample = rb_ary_entry(*args->src, i);
if (!RB_FLOAT_TYPE_P(sample)) {
sample = rb_to_float(sample);
}
args->dest[i] = RFLOAT_VALUE(sample);
}
} else {
// TODO: use rb_block_call
@@ -357,6 +361,9 @@ fill_samples(VALUE rb_args)
for (int i = 0; i < args->n_samples; i++) {
// TODO: check if iter is exhausted and raise ArgumentError appropriately
VALUE sample = rb_funcall(iter, id_next, 0);
if (!RB_FLOAT_TYPE_P(sample)) {
sample = rb_to_float(sample);
}
args->dest[i] = RFLOAT_VALUE(sample);
}
}
@@ -745,6 +752,45 @@ ruby_whisper_full_get_segment_no_speech_prob(VALUE self, VALUE i_segment)
return DBL2NUM(no_speech_prob);
}
static VALUE
ruby_whisper_full_n_vad_segments(VALUE self)
{
ruby_whisper *rw;
GetContext(self, rw);
return INT2NUM(whisper_full_n_vad_segments(rw->context));
}
static int
ruby_whisper_full_check_vad_segment_index(const ruby_whisper *rw, const VALUE i_segment)
{
const int c_i_segment = NUM2INT(i_segment);
if (c_i_segment < 0 || c_i_segment >= whisper_full_n_vad_segments(rw->context)) {
rb_raise(rb_eIndexError, "segment index %d out of range", c_i_segment);
}
return c_i_segment;
}
static VALUE
ruby_whisper_full_get_vad_segment_t0(VALUE self, VALUE i_segment)
{
ruby_whisper *rw;
GetContext(self, rw);
const int c_i_segment = ruby_whisper_full_check_vad_segment_index(rw, i_segment);
return LONG2NUM(whisper_full_get_vad_segment_t0(rw->context, c_i_segment));
}
static VALUE
ruby_whisper_full_get_vad_segment_t1(VALUE self, VALUE i_segment)
{
ruby_whisper *rw;
GetContext(self, rw);
const int c_i_segment = ruby_whisper_full_check_vad_segment_index(rw, i_segment);
return LONG2NUM(whisper_full_get_vad_segment_t1(rw->context, c_i_segment));
}
// High level API
static VALUE
@@ -830,6 +876,9 @@ init_ruby_whisper_context(VALUE *mWhisper)
rb_define_method(cContext, "full_get_segment_speaker_turn_next", ruby_whisper_full_get_segment_speaker_turn_next, 1);
rb_define_method(cContext, "full_get_segment_text", ruby_whisper_full_get_segment_text, 1);
rb_define_method(cContext, "full_get_segment_no_speech_prob", ruby_whisper_full_get_segment_no_speech_prob, 1);
rb_define_method(cContext, "full_n_vad_segments", ruby_whisper_full_n_vad_segments, 0);
rb_define_method(cContext, "full_get_vad_segment_t0", ruby_whisper_full_get_vad_segment_t0, 1);
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);
@@ -41,7 +41,6 @@
rb_define_singleton_method(mod, "drain_logs", ruby_whisper_##log_queue##_s_drain_logs, 0); \
rb_define_singleton_method(mod, "log_set", ruby_whisper_##log_queue##_s_log_set, 2); \
rb_set_end_proc(ruby_whisper_##log_queue##_end_proc, Qnil); \
rb_extend_object(mod, mLogSettable); \
rb_funcall(mLogSettable, id_extended, 1, mod);
rb_extend_object(mod, mLogSettable);
#endif
+9 -8
View File
@@ -446,7 +446,7 @@ ruby_whisper_params_allocate(VALUE klass)
rwp->params.vad_model_path = ruby_strdup(rwp->params.vad_model_path);
}
rwp->diarize = false;
rwp->vad_params = TypedData_Wrap_Struct(cVADParams, &ruby_whisper_vad_params_type, (void *)&rwp->params.vad_params);
rwp->vad_params = rb_class_new_instance(0, NULL, cVADParams);
rwp->new_segment_callback_container = ruby_whisper_callback_container_allocate();
rwp->progress_callback_container = ruby_whisper_callback_container_allocate();
rwp->encoder_begin_callback_container = ruby_whisper_callback_container_allocate();
@@ -918,7 +918,7 @@ ruby_whisper_params_set_temperature(VALUE self, VALUE value)
{
ruby_whisper_params *rwp;
TypedData_Get_Struct(self, ruby_whisper_params, &ruby_whisper_params_type, rwp);
rwp->params.temperature = RFLOAT_VALUE(value);
rwp->params.temperature = NUM2DBL(value);
return value;
}
/*
@@ -943,7 +943,7 @@ ruby_whisper_params_set_max_initial_ts(VALUE self, VALUE value)
{
ruby_whisper_params *rwp;
TypedData_Get_Struct(self, ruby_whisper_params, &ruby_whisper_params_type, rwp);
rwp->params.max_initial_ts = RFLOAT_VALUE(value);
rwp->params.max_initial_ts = NUM2DBL(value);
return value;
}
/*
@@ -966,7 +966,7 @@ ruby_whisper_params_set_length_penalty(VALUE self, VALUE value)
{
ruby_whisper_params *rwp;
TypedData_Get_Struct(self, ruby_whisper_params, &ruby_whisper_params_type, rwp);
rwp->params.length_penalty = RFLOAT_VALUE(value);
rwp->params.length_penalty = NUM2DBL(value);
return value;
}
/*
@@ -989,7 +989,7 @@ ruby_whisper_params_set_temperature_inc(VALUE self, VALUE value)
{
ruby_whisper_params *rwp;
TypedData_Get_Struct(self, ruby_whisper_params, &ruby_whisper_params_type, rwp);
rwp->params.temperature_inc = RFLOAT_VALUE(value);
rwp->params.temperature_inc = NUM2DBL(value);
return value;
}
/*
@@ -1014,7 +1014,7 @@ ruby_whisper_params_set_entropy_thold(VALUE self, VALUE value)
{
ruby_whisper_params *rwp;
TypedData_Get_Struct(self, ruby_whisper_params, &ruby_whisper_params_type, rwp);
rwp->params.entropy_thold = RFLOAT_VALUE(value);
rwp->params.entropy_thold = NUM2DBL(value);
return value;
}
/*
@@ -1037,7 +1037,7 @@ ruby_whisper_params_set_logprob_thold(VALUE self, VALUE value)
{
ruby_whisper_params *rwp;
TypedData_Get_Struct(self, ruby_whisper_params, &ruby_whisper_params_type, rwp);
rwp->params.logprob_thold = RFLOAT_VALUE(value);
rwp->params.logprob_thold = NUM2DBL(value);
return value;
}
/*
@@ -1060,7 +1060,7 @@ ruby_whisper_params_set_no_speech_thold(VALUE self, VALUE value)
{
ruby_whisper_params *rwp;
TypedData_Get_Struct(self, ruby_whisper_params, &ruby_whisper_params_type, rwp);
rwp->params.no_speech_thold = RFLOAT_VALUE(value);
rwp->params.no_speech_thold = NUM2DBL(value);
return value;
}
static VALUE
@@ -1307,6 +1307,7 @@ static VALUE
ruby_whisper_params_set_vad_params(VALUE self, VALUE value)
{
ruby_whisper_params *rwp;
rb_check_typeddata(value, &ruby_whisper_vad_params_type);
TypedData_Get_Struct(self, ruby_whisper_params, &ruby_whisper_params_type, rwp);
rwp->vad_params = value;
return value;
@@ -15,28 +15,9 @@ extern ID id_call;
extern ID id_to_path;
extern ID transcribe_option_names[1];
extern void prepare_transcription(ruby_whisper_params * rwp, VALUE * self, int n_processors);
extern VALUE full_body(VALUE rb_args);
extern VALUE full_parallel_body(VALUE rb_args);
typedef struct{
struct whisper_context *context;
struct whisper_full_params *params;
float *samples;
size_t n_samples;
int n_processors;
int result;
} transcribe_without_gvl_args;
static void*
transcribe_without_gvl(void *rb_args)
{
transcribe_without_gvl_args *args = (transcribe_without_gvl_args *)rb_args;
args->result = whisper_full_parallel(args->context, *args->params, args->samples, args->n_samples, args->n_processors);
return NULL;
}
/*
* transcribe a single file
* can emit to a block results
+30 -10
View File
@@ -12,13 +12,6 @@ extern VALUE release_samples(VALUE parsed);
extern VALUE ruby_whisper_vad_segments_s_init(struct whisper_vad_segments *segments);
typedef struct segments_from_samples_args {
VALUE *context;
VALUE *params;
float *samples;
int n_samples;
} segments_from_samples_args;
static size_t
ruby_whisper_vad_context_memsize(const void *p)
{
@@ -77,7 +70,24 @@ ruby_whisper_vad_context_initialize(VALUE self, VALUE model_path)
return Qnil;
}
static VALUE
typedef struct {
struct whisper_vad_context *context;
struct whisper_vad_params *params;
float *samples;
int n_samples;
struct whisper_vad_segments *result;
} segments_from_samples_without_gvl_args;
static void*
segments_from_samples_without_gvl(void *rb_args)
{
segments_from_samples_without_gvl_args *args = (segments_from_samples_without_gvl_args *)rb_args;
args->result = whisper_vad_segments_from_samples(args->context, *args->params, args->samples, args->n_samples);
return NULL;
}
VALUE
segments_from_samples_body(VALUE rb_args)
{
segments_from_samples_args *args = (segments_from_samples_args *)rb_args;
@@ -87,9 +97,19 @@ segments_from_samples_body(VALUE rb_args)
GetVADContext(*args->context, rwvc);
GetVADParams(*args->params, rwvp);
struct whisper_vad_segments *segments = whisper_vad_segments_from_samples(rwvc->context, rwvp->params, args->samples, args->n_samples);
segments_from_samples_without_gvl_args segments_from_samples_without_gvl_args = {
rwvc->context,
&rwvp->params,
args->samples,
args->n_samples,
NULL
};
rb_thread_call_without_gvl(segments_from_samples_without_gvl, (void *)&segments_from_samples_without_gvl_args, NULL, NULL);
if (!segments_from_samples_without_gvl_args.result) {
rb_raise(rb_eRuntimeError, "Failed to process audio");
}
return ruby_whisper_vad_segments_s_init(segments);
return ruby_whisper_vad_segments_s_init(segments_from_samples_without_gvl_args.result);
}
static VALUE
@@ -16,18 +16,16 @@ extern const rb_data_type_t ruby_whisper_vad_params_type;
extern const rb_data_type_t ruby_whisper_vad_segments_type;
extern VALUE ruby_whisper_vad_segments_s_init(struct whisper_vad_segments *segments);
extern VALUE segments_from_samples_body(VALUE rb_args);
VALUE
ruby_whisper_vad_detect(VALUE self, VALUE file_path, VALUE params) {
ruby_whisper_vad_context *rwvc;
ruby_whisper_vad_params *rwvp;
std::string cpp_file_path;
std::vector<float> pcmf32;
std::vector<std::vector<float>> pcmf32s;
whisper_vad_segments *segments;
GetVADContext(self, rwvc);
TypedData_Get_Struct(params, ruby_whisper_vad_params, &ruby_whisper_vad_params_type, rwvp);
if (rb_respond_to(file_path, id_to_path)) {
file_path = rb_funcall(file_path, id_to_path, 0);
@@ -38,12 +36,13 @@ ruby_whisper_vad_detect(VALUE self, VALUE file_path, VALUE params) {
rb_raise(rb_eRuntimeError, "Failed to open '%s' as WAV file\n", cpp_file_path.c_str());
}
segments = whisper_vad_segments_from_samples(rwvc->context, rwvp->params, pcmf32.data(), pcmf32.size());
if (segments == nullptr) {
rb_raise(rb_eRuntimeError, "Failed to process audio\n");
}
return ruby_whisper_vad_segments_s_init(segments);
segments_from_samples_args args = {
&self,
&params,
pcmf32.data(),
(int)pcmf32.size(),
};
return segments_from_samples_body((VALUE)&args);
}
#ifdef __cplusplus
+4 -4
View File
@@ -31,7 +31,7 @@ static ID id_samples_overlap;
const rb_data_type_t ruby_whisper_vad_params_type = {
"ruby_whisper_vad_params",
{0, 0, ruby_whisper_vad_params_memsize,},
{0, RUBY_DEFAULT_FREE, ruby_whisper_vad_params_memsize,},
0, 0,
0
};
@@ -56,7 +56,7 @@ ruby_whisper_vad_params_set_threshold(VALUE self, VALUE value)
{
ruby_whisper_vad_params *rwvp;
TypedData_Get_Struct(self, ruby_whisper_vad_params, &ruby_whisper_vad_params_type, rwvp);
rwvp->params.threshold = RFLOAT_VALUE(value);
rwvp->params.threshold = NUM2DBL(value);
return value;
}
@@ -125,7 +125,7 @@ ruby_whisper_vad_params_set_max_speech_duration_s(VALUE self, VALUE value)
{
ruby_whisper_vad_params *rwvp;
TypedData_Get_Struct(self, ruby_whisper_vad_params, &ruby_whisper_vad_params_type, rwvp);
rwvp->params.max_speech_duration_s = RFLOAT_VALUE(value);
rwvp->params.max_speech_duration_s = NUM2DBL(value);
return value;
}
@@ -171,7 +171,7 @@ ruby_whisper_vad_params_set_samples_overlap(VALUE self, VALUE value)
{
ruby_whisper_vad_params *rwvp;
TypedData_Get_Struct(self, ruby_whisper_vad_params, &ruby_whisper_vad_params_type, rwvp);
rwvp->params.samples_overlap = RFLOAT_VALUE(value);
rwvp->params.samples_overlap = NUM2DBL(value);
return value;
}
+2 -2
View File
@@ -68,7 +68,7 @@ ruby_whisper_vad_segment_get_start_time(VALUE self)
TypedData_Get_Struct(self, ruby_whisper_vad_segment, &ruby_whisper_vad_segment_type, rwvs);
TypedData_Get_Struct(rwvs->segments, ruby_whisper_vad_segments, &ruby_whisper_vad_segments_type, rwvss);
t0 = whisper_vad_segments_get_segment_t0(rwvss->segments, rwvs->index);
return DBL2NUM(t0 * 10);
return LONG2NUM(t0 * 10);
}
static VALUE
@@ -81,7 +81,7 @@ ruby_whisper_vad_segment_get_end_time(VALUE self)
TypedData_Get_Struct(self, ruby_whisper_vad_segment, &ruby_whisper_vad_segment_type, rwvs);
TypedData_Get_Struct(rwvs->segments, ruby_whisper_vad_segments, &ruby_whisper_vad_segments_type, rwvss);
t1 = whisper_vad_segments_get_segment_t1(rwvss->segments, rwvs->index);
return DBL2NUM(t1 * 10);
return LONG2NUM(t1 * 10);
}
static VALUE
+5 -8
View File
@@ -1,13 +1,5 @@
require "mutex_m"
module Whisper
module LogSettable
class << self
def extended(base)
base.extend Mutex_m
end
end
private
def start_log_callback_thread
@@ -32,5 +24,10 @@ module Whisper
end
}
end
def synchronize(&block)
@mutex ||= Thread::Mutex.new
@mutex.synchronize &block
end
end
end
-7
View File
@@ -44,7 +44,6 @@ module Whisper
return path if cache_path.exist?
headers = {}
headers["if-modified-since"] = path.mtime.httpdate if path.exist?
request @uri, headers
path
end
@@ -54,17 +53,11 @@ module Whisper
request = Net::HTTP::Get.new(uri, headers)
http.request request do |response|
case response
when Net::HTTPNotModified
# noop
when Net::HTTPOK
return if !response.key?("last-modified") && cache_path.exist?
download response
when Net::HTTPRedirection
request URI(response["location"]), headers
else
return if headers.key?("if-modified-since") # Use cache file
raise "#{response.code} #{response.message}\n#{response.body}"
end
end
+6
View File
@@ -131,6 +131,12 @@ module Whisper
def full_get_segment_no_speech_prob: (Integer) -> Float
def full_n_vad_segments: () -> Integer
def full_get_vad_segment_t0: (Integer) -> Integer
def full_get_vad_segment_t1: (Integer) -> Integer
# Run the entire model: PCM -> log mel spectrogram -> encoder -> decoder -> text
# Not thread safe for same context
# Uses the specified decoding strategy to obtain the text.
+9
View File
@@ -16,4 +16,13 @@ class TestVAD < TestBase
assert_match(/ask not what your country can do for you[,.] ask what you can do for your country/i, text)
end
end
def test_vad_segments_api
@whisper.transcribe TestBase::AUDIO, @params
assert_equal 4, @whisper.full_n_vad_segments
@whisper.full_n_vad_segments.times do |i|
assert @whisper.full_get_vad_segment_t0(i) < @whisper.full_get_vad_segment_t1(i)
end
end
end
+2 -2
View File
@@ -44,8 +44,8 @@ class TestVADContext < TestBase
assert_instance_of Enumerator, segments.each
segment = segments.each.first
assert_instance_of Float, segment.start_time
assert_instance_of Float, segment.end_time
assert_instance_of Integer, segment.start_time
assert_instance_of Integer, segment.end_time
segment => {start_time:, end_time:}
assert_equal segment.start_time, start_time
+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.7'
s.version = '1.3.8'
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']