ruby : transcribe without GVL, accept more MemoryViews, Windows support, fix memory size report, improve document (#3775)

* Change MemoryView example using NDAV

* Add note on audio attributes for #full and #full_parallel

* Support more variants of MemoryView

* Use IO.popen instead of Kernel.` for Windows compatibility

* Use cmake's -C option instead of multiple -D options

* Fix memsize calculation

* Remove unused argument

* Add is_interrupted field to abort callback container

* Fix RBS syntax

* Address document comment for RDoc

* Add .document for RDoc

* Add .rdoc_options

* Run #full without GVL

* Initialize callbacks with nil

* Specify implicity Whisper::Params to distinguish from Whisper::Context::Params

* Run callbacks without GVL

* Call log callback with GVL

* Run full_parallel without GVL

* Run transcribe without GVL

* Fix ruby_whisper_lock_gvl and ruby_whisper_unlock_gvl

* Fix return value of encoder_begin_callback

* Report GVL unlocking from transcribe

* Remove unused interface

* Restore overload of full_parallel

* Close process

* Fix struct name

* Make is_without_gvl thread local

* Use rb_thread_call_with_gvl instead of global variable

* Retrieve instance variable in GVL

* Narrow acceptable MemoryView format

* Fix option cache path

* Reduce files in package

* Use append_cflags

* Add ext/*.rb to task dependencies

* Use copy instead of cp

* Make TestPackage more portable

* Patch for lower version Ruby

* Make build scripts more portable

* Add Windows support

* Don't raise exceptions
This commit is contained in:
KITAITI Makoto
2026-05-07 06:28:18 +02:00
committed by GitHub
parent 4bf733672b
commit c81b2dabbc
17 changed files with 653 additions and 166 deletions
+10 -4
View File
@@ -22,13 +22,17 @@ class Dependencies
else
nil
end
}.reverse.collect {|lib| "lib#{lib}.a"}
}.reverse.collect {|lib| "#{prefix(lib)}#{lib}.#{RbConfig::CONFIG['LIBEXT']}"}
end
def to_s
libs.join(" ")
end
def local_libs
to_s
end
private
def dot_path
@@ -36,9 +40,7 @@ class Dependencies
end
def generate_dot
args = ["-S", "sources", "-B", "build", "--graphviz", dot_path, "-D", "BUILD_SHARED_LIBS=OFF"]
args << @options.to_s unless @options.to_s.empty?
system @cmake, *args, exception: true
system @cmake, "-S", "sources", "-B", "build", *@options.graphviz_cmake_args, "--graphviz", dot_path, *@options, exception: true
end
def parse_dot
@@ -59,6 +61,10 @@ class Dependencies
end
end
def prefix(lib)
"lib"
end
def tsort_each_node
@nodes.each_key do |node|
yield node
@@ -0,0 +1,17 @@
require_relative "dependencies"
class DependenciesForWindows < Dependencies
def local_libs
libs.collect {|lib| %|"#{lib_path(lib)}"|}.join(" ")
end
private
def prefix(lib)
lib.start_with?("ggml") ? "" : "lib"
end
def lib_path(lib)
File.join(__dir__, lib).tr("\\", "/")
end
end
+20 -8
View File
@@ -1,15 +1,27 @@
require "mkmf"
require_relative "options"
require_relative "dependencies"
if RUBY_PLATFORM.match? /mswin|mingw|ucrt/
require_relative "options_for_windows"
require_relative "dependencies_for_windows"
Opts = OptionsForWindows
Deps = DependenciesForWindows
else
require_relative "options"
require_relative "dependencies"
Opts = Options
Deps = Dependencies
end
cmake = find_executable("cmake") || abort
options = Options.new(cmake).to_s
options = Opts.new(cmake)
have_library("gomp") rescue nil
libs = Dependencies.new(cmake, options).to_s
libs = Deps.new(cmake, options)
$CFLAGS << " -O3 -march=native"
append_cflags ["-O3", "-march=native"]
$INCFLAGS << " -Isources/include -Isources/ggml/include -Isources/examples"
$LOCAL_LIBS << " #{libs}"
$LOCAL_LIBS << " #{libs.local_libs}"
$cleanfiles << " build #{libs}"
create_makefile "whisper" do |conf|
@@ -17,7 +29,7 @@ create_makefile "whisper" do |conf|
$(TARGET_SO): #{libs}
#{libs}: cmake-targets
cmake-targets:
#{"\t"}#{cmake} -S sources -B build -D BUILD_SHARED_LIBS=OFF -D CMAKE_ARCHIVE_OUTPUT_DIRECTORY=#{__dir__} -D CMAKE_POSITION_INDEPENDENT_CODE=ON #{options}
#{"\t"}#{cmake} --build build --config Release --target common whisper
#{"\t"}"#{cmake}" -S sources -B build #{options}
#{"\t"}"#{cmake}" --build build --config Release --target common whisper
EOF
end
+54 -14
View File
@@ -1,26 +1,36 @@
require "fileutils"
class Options
def initialize(cmake="cmake")
@cmake = cmake
@options = {}
configure
write_cache_file
end
def to_a
[
"-D", "BUILD_SHARED_LIBS=OFF",
"-D", "WHISPER_BUILD_TESTS=OFF",
"-D", "CMAKE_ARCHIVE_OUTPUT_DIRECTORY=#{__dir__}",
"-D", "CMAKE_POSITION_INDEPENDENT_CODE=ON",
"-C", cache_path
]
end
def to_s
@options
.reject {|name, (type, value)| value.nil?}
.collect {|name, (type, value)| "-D #{name}=#{value == true ? "ON" : value == false ? "OFF" : value.shellescape}"}
.join(" ")
command_line(*to_a)
end
def cmake_options
return @cmake_options if @cmake_options
def graphviz_cmake_args
[]
end
output = nil
Dir.chdir __dir__ do
output = `#{@cmake.shellescape} -S sources -B build -L`
end
@cmake_options = output.lines.drop_while {|line| line.chomp != "-- Cache values"}.drop(1)
private
def cmake_options
@cmake_options ||= cmake_options_output.lines.drop_while {|line| line.chomp != "-- Cache values"}.drop(1)
.filter_map {|line|
option, value = line.chomp.split("=", 2)
name, type = option.split(":", 2)
@@ -34,7 +44,11 @@ class Options
}.to_h
end
private
def cmake_options_output
Dir.chdir(__dir__) do
IO.popen([@cmake, "-S", "sources", "-B", "build", "-L"]) {|io| io.read}
end
end
def configure
cmake_options.each_pair do |name, (type, default_value)|
@@ -74,12 +88,38 @@ class Options
def enabled?(option)
op = @options[option]
raise "Option not exist: #{option}" unless op
raise "Option not boolean: #{option}(#{op[0]})" unless op[0] == "BOOL"
return false unless op
return false unless op[0] == "BOOL"
if op[1].nil?
cmake_options[option][1]
else
op[1]
end
end
def cache_path
File.join(__dir__, "sources", "Options.cmake")
end
def write_cache_file
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)" % {
name:,
type:,
value: value == true ? "ON" : value == false ? "OFF" : escape_cmake(value)
}
file.puts line
end
end
end
def escape_cmake(str)
str.gsub(/[\\"]/, '\\\\\&')
end
def command_line(*args)
args.collect {|arg| %|"#{arg.to_s.gsub(/[\\"]/, '\\\\\&')}"|}.join(" ")
end
end
+51
View File
@@ -0,0 +1,51 @@
require_relative "options"
class OptionsForWindows < Options
def to_s
command_line(*generator_args, *to_a)
end
def graphviz_cmake_args
generator_args
end
private
def arm?
RbConfig::CONFIG["host_cpu"].to_s.downcase.match?(/\A(?:arm64|aarch64)\z/)
end
def cmake_options_output
Dir.chdir(__dir__) do
IO.popen([@cmake, "-S", "sources", "-B", "build", *generator_args, "-L"]) {|io| io.read}
end
end
def generator_args
generator = cmake_generator
["-G", generator] if generator && !generator.empty?
end
def cmake_generator
return @cmake_generator if defined?(@cmake_generator)
generator = ENV["CMAKE_GENERATOR"]
abort "CMAKE_GENERATOR=#{generator} is unsupported for mingw/ucrt Ruby" if visual_studio_generator_name?(generator)
return @cmake_generator = generator unless generator.nil? || generator.empty?
ninja = find_executable("ninja")
return @cmake_generator = "Ninja" if ninja
make = find_executable("make")
return @cmake_generator = "MSYS Makefiles" if make
mingw32_make = find_executable("mingw32-make")
return @cmake_generator = "MinGW Makefiles" if mingw32_make
@cmake_generator = nil
end
def visual_studio_generator_name?(generator)
generator && generator.start_with?("Visual Studio")
end
end
+32 -4
View File
@@ -29,6 +29,7 @@ ID id_cache;
ID id_n_processors;
static bool is_log_callback_finalized = false;
static bool is_ruby_log_callback_present = false;
// High level API
extern VALUE ruby_whisper_segment_allocate(VALUE klass);
@@ -106,18 +107,43 @@ static VALUE ruby_whisper_s_finalize_log_callback(VALUE self, VALUE id) {
return Qnil;
}
typedef struct {
int level;
const char * buffer;
} call_log_callbacks_args;
static void*
call_log_callbacks(void *v_args) {
VALUE log_callback = rb_iv_get(mWhisper, "log_callback");
if (NIL_P(log_callback)) {
return NULL;
}
call_log_callbacks_args *args = (call_log_callbacks_args *)v_args;
VALUE user_data = rb_iv_get(mWhisper, "user_data");
rb_funcall(log_callback, id_call, 3, INT2NUM(args->level), rb_str_new2(args->buffer), user_data);
return NULL;
}
static void
ruby_whisper_log_callback(enum ggml_log_level level, const char * buffer, void * user_data) {
if (is_log_callback_finalized) {
return;
}
VALUE log_callback = rb_iv_get(mWhisper, "log_callback");
if (NIL_P(log_callback)) {
if (!is_ruby_log_callback_present) {
return;
}
VALUE udata = rb_iv_get(mWhisper, "user_data");
rb_funcall(log_callback, id_call, 3, INT2NUM(level), rb_str_new2(buffer), udata);
call_log_callbacks_args args = {
level,
buffer,
};
if (ruby_thread_has_gvl_p()) {
call_log_callbacks((void *)&args);
} else {
rb_thread_call_with_gvl(call_log_callbacks, (void *)&args);
}
}
/*
@@ -140,8 +166,10 @@ static VALUE ruby_whisper_s_log_set(VALUE self, VALUE log_callback, VALUE user_d
if (NIL_P(log_callback)) {
whisper_log_set(NULL, NULL);
is_ruby_log_callback_present = false;
} else {
whisper_log_set(ruby_whisper_log_callback, NULL);
is_ruby_log_callback_present = true;
}
return Qnil;
+16 -1
View File
@@ -2,10 +2,17 @@
#define RUBY_WHISPER_H
#include <ruby.h>
#include <ruby/version.h>
#include <ruby/util.h>
#include <ruby/thread.h>
#include <ruby/memory_view.h>
#include "whisper.h"
#if RUBY_API_VERSION_MAJOR < 4
// Exists but not declared as public API
int ruby_thread_has_gvl_p(void);
#endif
typedef struct {
VALUE *context;
VALUE user_data;
@@ -13,6 +20,14 @@ typedef struct {
VALUE callbacks;
} ruby_whisper_callback_container;
typedef struct {
VALUE *context;
VALUE user_data;
VALUE callback;
VALUE callbacks;
bool is_interrupted;
} ruby_whisper_abort_callback_container;
typedef struct {
struct whisper_context *context;
} ruby_whisper;
@@ -27,7 +42,7 @@ typedef struct {
ruby_whisper_callback_container *new_segment_callback_container;
ruby_whisper_callback_container *progress_callback_container;
ruby_whisper_callback_container *encoder_begin_callback_container;
ruby_whisper_callback_container *abort_callback_container;
ruby_whisper_abort_callback_container *abort_callback_container;
VALUE vad_params;
} ruby_whisper_params;
+94 -8
View File
@@ -1,5 +1,11 @@
#include "ruby_whisper.h"
#ifdef WORDS_BIGENDIAN
#define IS_BIGENDIAN true
#else
#define IS_BIGENDIAN false
#endif
extern ID id_to_s;
extern ID id___method__;
extern ID id_to_enum;
@@ -47,6 +53,27 @@ typedef struct full_parallel_args {
int n_processors;
} full_parallel_args;
typedef struct full_without_gvl_args {
struct whisper_context *context;
struct whisper_full_params *params;
float *samples;
int n_samples;
int result;
} full_without_gvl_args;
typedef struct full_parallel_without_gvl_args {
struct whisper_context *context;
struct whisper_full_params *params;
float *samples;
int n_samples;
int n_processors;
int result;
} full_parallel_without_gvl_args;
typedef struct full_ubf_args {
ruby_whisper_abort_callback_container *abort_callback_container;
} full_ubf_args;
static void
ruby_whisper_free(ruby_whisper *rw)
{
@@ -74,7 +101,7 @@ static size_t
ruby_whisper_memsize(const void *p)
{
const ruby_whisper *rw = (const ruby_whisper *)p;
size_t size = sizeof(rw);
size_t size = sizeof(*rw);
if (!rw) {
return 0;
}
@@ -304,11 +331,25 @@ VALUE ruby_whisper_model_type(VALUE self)
static bool
check_memory_view(rb_memory_view_t *memview)
{
if (memview->format != NULL && strcmp(memview->format, "f") != 0) {
rb_warn("currently only format \"f\" is supported for MemoryView, but given: %s", memview->format);
if (!memview->format) {
rb_warn("currently format is required");
return false;
}
if (memview->format != NULL && memview->ndim != 1) {
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);
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);
return false;
}
@@ -426,6 +467,22 @@ release_samples(VALUE rb_parsed_args)
return Qnil;
}
static void*
full_without_gvl(void *rb_args)
{
full_without_gvl_args *args = (full_without_gvl_args *)rb_args;
args->result = whisper_full(args->context, *args->params, args->samples, args->n_samples);
return NULL;
}
static void
full_ubf(void *rb_args)
{
full_ubf_args *args = (full_ubf_args *)rb_args;
args->abort_callback_container->is_interrupted = true;
}
static VALUE
full_body(VALUE rb_args)
{
@@ -437,9 +494,19 @@ full_body(VALUE rb_args)
TypedData_Get_Struct(*args->params, ruby_whisper_params, &ruby_whisper_params_type, rwp);
prepare_transcription(rwp, args->context, 1);
int result = whisper_full(rw->context, rwp->params, args->samples, args->n_samples);
return INT2NUM(result);
struct full_without_gvl_args full_without_gvl_args = {
rw->context,
&rwp->params,
args->samples,
args->n_samples,
0,
};
full_ubf_args full_ubf_args = {
rwp->abort_callback_container,
};
rb_thread_call_without_gvl(full_without_gvl, (void *)&full_without_gvl_args, full_ubf, (void *)&full_ubf_args);
return INT2NUM(full_without_gvl_args.result);
}
/*
@@ -477,6 +544,14 @@ VALUE ruby_whisper_full(int argc, VALUE *argv, VALUE self)
}
}
static void*
full_parallel_without_gvl(void *rb_args)
{
full_parallel_without_gvl_args *args = (full_parallel_without_gvl_args *)rb_args;
args->result = whisper_full_parallel(args->context, *args->params, args->samples, args->n_samples, args->n_processors);
return NULL;
}
static VALUE
full_parallel_body(VALUE rb_args)
{
@@ -488,9 +563,20 @@ full_parallel_body(VALUE rb_args)
TypedData_Get_Struct(*args->params, ruby_whisper_params, &ruby_whisper_params_type, rwp);
prepare_transcription(rwp, args->context, args->n_processors);
int result = whisper_full_parallel(rw->context, rwp->params, args->samples, args->n_samples, args->n_processors);
return INT2NUM(result);
struct full_parallel_without_gvl_args full_parallel_without_gvl_args = {
rw->context,
&rwp->params,
args->samples,
args->n_samples,
args->n_processors,
0,
};
full_ubf_args full_ubf_args = {
rwp->abort_callback_container,
};
rb_thread_call_without_gvl(full_parallel_without_gvl, (void *)&full_parallel_without_gvl_args, full_ubf, (void *)&full_ubf_args);
return INT2NUM(full_parallel_without_gvl_args.result);
}
/*
+221 -52
View File
@@ -93,21 +93,66 @@ rb_whisper_callback_container_allocate() {
container->context = NULL;
container->user_data = Qnil;
container->callback = Qnil;
container->callbacks = rb_ary_new();
container->callbacks = Qnil;
return container;
}
static void new_segment_callback(struct whisper_context *ctx, struct whisper_state *state, int n_new, void *user_data) {
const ruby_whisper_callback_container *container = (ruby_whisper_callback_container *)user_data;
static void
rb_whisper_abort_callback_container_mark(ruby_whisper_abort_callback_container *rwc)
{
if (rwc == NULL) return;
rb_gc_mark(rwc->user_data);
rb_gc_mark(rwc->callback);
rb_gc_mark(rwc->callbacks);
}
static ruby_whisper_abort_callback_container*
rb_whisper_abort_callback_container_allocate() {
ruby_whisper_abort_callback_container *container;
container = ALLOC(ruby_whisper_abort_callback_container);
container->context = NULL;
container->user_data = Qnil;
container->callback = Qnil;
container->callbacks = Qnil;
container->is_interrupted = false;
return container;
}
static bool
ruby_whisper_callback_container_is_present(const ruby_whisper_callback_container *container) {
return !NIL_P(container->callback) || !NIL_P(container->callbacks);
}
static bool
ruby_whisper_abort_callback_container_is_present(const ruby_whisper_abort_callback_container *container) {
return !NIL_P(container->callback) || !NIL_P(container->callbacks);
}
typedef struct {
const ruby_whisper_callback_container *container;
struct whisper_state *state;
int n_new;
} call_new_segment_callbacks_args;
static void*
call_new_segment_callbacks(void *v_args) {
call_new_segment_callbacks_args *args = (call_new_segment_callbacks_args *)v_args;
const ruby_whisper_callback_container *container = args->container;
struct whisper_state *state = args->state;
int n_new = args->n_new;
// Currently, doesn't support state because
// those require to resolve GC-related problems.
if (!NIL_P(container->callback)) {
rb_funcall(container->callback, id_call, 4, *container->context, Qnil, INT2NUM(n_new), container->user_data);
}
if (NIL_P(container->callbacks)) {
return NULL;
}
const long callbacks_len = RARRAY_LEN(container->callbacks);
if (0 == callbacks_len) {
return;
return NULL;
}
const int n_segments = whisper_full_n_segments_from_state(state);
for (int i = n_new; i > 0; i--) {
@@ -118,95 +163,208 @@ static void new_segment_callback(struct whisper_context *ctx, struct whisper_sta
rb_funcall(cb, id_call, 1, segment);
}
}
return NULL;
}
static void new_segment_callback(struct whisper_context *ctx, struct whisper_state *state, int n_new, void *user_data) {
const ruby_whisper_callback_container *container = (ruby_whisper_callback_container *)user_data;
if (!ruby_whisper_callback_container_is_present(container)) {
return;
}
call_new_segment_callbacks_args args = {
container,
state,
n_new
};
rb_thread_call_with_gvl(call_new_segment_callbacks, (void *)&args);
}
typedef struct {
const ruby_whisper_callback_container *container;
struct whisper_state *state;
int progress_cur;
} call_progress_callbacks_args;
static void*
call_progress_callbacks(void *v_args) {
call_progress_callbacks_args *args = (call_progress_callbacks_args *)v_args;
const ruby_whisper_callback_container *container = args->container;
int progress_cur = args->progress_cur;
// Currently, doesn't support state because
// those require to resolve GC-related problems.
if (!NIL_P(args->container->callback)) {
rb_funcall(container->callback, id_call, 4, *container->context, Qnil, INT2NUM(progress_cur), container->user_data);
}
if (NIL_P(container->callbacks)) {
return NULL;
}
const long callbacks_len = RARRAY_LEN(container->callbacks);
if (0 == callbacks_len) {
return NULL;
}
for (int j = 0; j < callbacks_len; j++) {
VALUE cb = rb_ary_entry(container->callbacks, j);
rb_funcall(cb, id_call, 1, INT2NUM(progress_cur));
}
return NULL;
}
static void progress_callback(struct whisper_context *ctx, struct whisper_state *state, int progress_cur, void *user_data) {
const ruby_whisper_callback_container *container = (ruby_whisper_callback_container *)user_data;
const VALUE progress = INT2NUM(progress_cur);
// Currently, doesn't support state because
// those require to resolve GC-related problems.
if (!NIL_P(container->callback)) {
rb_funcall(container->callback, id_call, 4, *container->context, Qnil, progress, container->user_data);
}
const long callbacks_len = RARRAY_LEN(container->callbacks);
if (0 == callbacks_len) {
if (!ruby_whisper_callback_container_is_present(container)) {
return;
}
for (int j = 0; j < callbacks_len; j++) {
VALUE cb = rb_ary_entry(container->callbacks, j);
rb_funcall(cb, id_call, 1, progress);
}
call_progress_callbacks_args args = {
container,
state,
progress_cur
};
rb_thread_call_with_gvl(call_progress_callbacks, (void *)&args);
}
static bool encoder_begin_callback(struct whisper_context *ctx, struct whisper_state *state, void *user_data) {
const ruby_whisper_callback_container *container = (ruby_whisper_callback_container *)user_data;
bool is_aborted = false;
VALUE result;
typedef struct {
const ruby_whisper_callback_container *container;
struct whisper_state *state;
bool is_continued;
} call_encoder_begin_callbacks_args;
static void*
call_encoder_begin_callbacks(void *v_args) {
call_encoder_begin_callbacks_args *args = (call_encoder_begin_callbacks_args *)v_args;
const ruby_whisper_callback_container *container = args->container;
VALUE result = Qnil;
// Currently, doesn't support state because
// those require to resolve GC-related problems.
if (!NIL_P(container->callback)) {
result = rb_funcall(container->callback, id_call, 3, *container->context, Qnil, container->user_data);
if (result == Qfalse) {
is_aborted = true;
args->is_continued = false;
return NULL;
}
}
const long callbacks_len = RARRAY_LEN(container->callbacks);
if (0 == callbacks_len) {
return !is_aborted;
}
for (int j = 0; j < callbacks_len; j++) {
VALUE cb = rb_ary_entry(container->callbacks, j);
result = rb_funcall(cb, id_call, 0);
if (result == Qfalse) {
is_aborted = true;
if (!NIL_P(container->callbacks)) {
const long callbacks_len = RARRAY_LEN(container->callbacks);
if (0 == callbacks_len) {
return NULL;
}
for (int j = 0; j < callbacks_len; j++) {
VALUE cb = rb_ary_entry(container->callbacks, j);
result = rb_funcall(cb, id_call, 0);
if (result == Qfalse) {
args->is_continued = false;
return NULL;
}
}
}
return !is_aborted;
return NULL;
}
static bool abort_callback(void * user_data) {
static bool encoder_begin_callback(struct whisper_context *ctx, struct whisper_state *state, void *user_data) {
const ruby_whisper_callback_container *container = (ruby_whisper_callback_container *)user_data;
if (!ruby_whisper_callback_container_is_present(container)) {
return true;
}
call_encoder_begin_callbacks_args args = {
container,
state,
true
};
rb_thread_call_with_gvl(call_encoder_begin_callbacks, (void *)&args);
return args.is_continued;
}
typedef struct {
const ruby_whisper_abort_callback_container *container;
struct whisper_state *state;
bool is_interrupted;
} call_abort_callbacks_args;
static void*
call_abort_callbacks(void *v_args) {
call_abort_callbacks_args *args = (call_abort_callbacks_args *)v_args;
const ruby_whisper_abort_callback_container *container = args->container;
if (container->is_interrupted) {
args->is_interrupted = true;
return NULL;
}
if (!NIL_P(container->callback)) {
VALUE result = rb_funcall(container->callback, id_call, 1, container->user_data);
if (!NIL_P(result) && Qfalse != result) {
return true;
args->is_interrupted = true;
return NULL;
}
}
if (NIL_P(container->callbacks)) {
return NULL;
}
const long callbacks_len = RARRAY_LEN(container->callbacks);
if (0 == callbacks_len) {
return false;
return NULL;
}
for (int j = 0; j < callbacks_len; j++) {
VALUE cb = rb_ary_entry(container->callbacks, j);
VALUE result = rb_funcall(cb, id_call, 1, container->user_data);
if (!NIL_P(result) && Qfalse != result) {
return true;
args->is_interrupted = true;
return NULL;
}
}
return false;
return NULL;
}
static bool abort_callback(void * user_data) {
const ruby_whisper_abort_callback_container *container = (ruby_whisper_abort_callback_container *)user_data;
if (container->is_interrupted) {
return true;
}
if (!ruby_whisper_abort_callback_container_is_present(container)) {
return false;
}
call_abort_callbacks_args args = {
container,
NULL,
false
};
rb_thread_call_with_gvl(call_abort_callbacks, (void *)&args);
return args.is_interrupted;
}
static void
check_thread_safety(ruby_whisper_params *rwp, VALUE *context, int n_processors)
check_thread_safety(ruby_whisper_params *rwp, int n_processors)
{
if (n_processors == 1) {
return;
}
if (!NIL_P(rwp->new_segment_callback_container->callback) || 0 != RARRAY_LEN(rwp->new_segment_callback_container->callbacks)) {
if (ruby_whisper_callback_container_is_present(rwp->new_segment_callback_container)) {
rb_raise(rb_eRuntimeError, "new segment callback not supported on parallel transcription");
}
if (!NIL_P(rwp->progress_callback_container->callback) || 0 != RARRAY_LEN(rwp->progress_callback_container->callbacks)) {
if (ruby_whisper_callback_container_is_present(rwp->progress_callback_container)) {
rb_raise(rb_eRuntimeError, "progress callback not supported on parallel transcription");
}
if (!NIL_P(rwp->encoder_begin_callback_container->callback) || 0 != RARRAY_LEN(rwp->encoder_begin_callback_container->callbacks)) {
if (ruby_whisper_callback_container_is_present(rwp->encoder_begin_callback_container)) {
rb_raise(rb_eRuntimeError, "encoder begin callback not supported on parallel transcription");
}
if (!NIL_P(rwp->abort_callback_container->callback) || 0 != RARRAY_LEN(rwp->abort_callback_container->callbacks)) {
if (ruby_whisper_abort_callback_container_is_present(rwp->abort_callback_container)) {
rb_raise(rb_eRuntimeError, "abort callback not supported on parallel transcription");
}
@@ -217,29 +375,28 @@ check_thread_safety(ruby_whisper_params *rwp, VALUE *context, int n_processors)
}
static void register_callbacks(ruby_whisper_params * rwp, VALUE * context) {
if (!NIL_P(rwp->new_segment_callback_container->callback) || 0 != RARRAY_LEN(rwp->new_segment_callback_container->callbacks)) {
if (ruby_whisper_callback_container_is_present(rwp->new_segment_callback_container)) {
rwp->new_segment_callback_container->context = context;
rwp->params.new_segment_callback = new_segment_callback;
rwp->params.new_segment_callback_user_data = rwp->new_segment_callback_container;
}
if (!NIL_P(rwp->progress_callback_container->callback) || 0 != RARRAY_LEN(rwp->progress_callback_container->callbacks)) {
if (ruby_whisper_callback_container_is_present(rwp->progress_callback_container)) {
rwp->progress_callback_container->context = context;
rwp->params.progress_callback = progress_callback;
rwp->params.progress_callback_user_data = rwp->progress_callback_container;
}
if (!NIL_P(rwp->encoder_begin_callback_container->callback) || 0 != RARRAY_LEN(rwp->encoder_begin_callback_container->callbacks)) {
if (ruby_whisper_callback_container_is_present(rwp->encoder_begin_callback_container)) {
rwp->encoder_begin_callback_container->context = context;
rwp->params.encoder_begin_callback = encoder_begin_callback;
rwp->params.encoder_begin_callback_user_data = rwp->encoder_begin_callback_container;
}
if (!NIL_P(rwp->abort_callback_container->callback) || 0 != RARRAY_LEN(rwp->abort_callback_container->callbacks)) {
rwp->abort_callback_container->context = context;
rwp->params.abort_callback = abort_callback;
rwp->params.abort_callback_user_data = rwp->abort_callback_container;
}
rwp->abort_callback_container->context = context;
rwp->params.abort_callback = abort_callback;
rwp->abort_callback_container->is_interrupted = false;
rwp->params.abort_callback_user_data = rwp->abort_callback_container;
}
static void set_vad_params(ruby_whisper_params *rwp)
@@ -255,7 +412,7 @@ static void set_vad_params(ruby_whisper_params *rwp)
void
prepare_transcription(ruby_whisper_params *rwp, VALUE *context, int n_processors)
{
check_thread_safety(rwp, context, n_processors);
check_thread_safety(rwp, n_processors);
register_callbacks(rwp, context);
set_vad_params(rwp);
}
@@ -267,7 +424,7 @@ rb_whisper_params_mark(void *p)
rb_whisper_callbcack_container_mark(rwp->new_segment_callback_container);
rb_whisper_callbcack_container_mark(rwp->progress_callback_container);
rb_whisper_callbcack_container_mark(rwp->encoder_begin_callback_container);
rb_whisper_callbcack_container_mark(rwp->abort_callback_container);
rb_whisper_abort_callback_container_mark(rwp->abort_callback_container);
rb_gc_mark(rwp->vad_params);
}
@@ -338,7 +495,7 @@ ruby_whisper_params_allocate(VALUE klass)
rwp->new_segment_callback_container = rb_whisper_callback_container_allocate();
rwp->progress_callback_container = rb_whisper_callback_container_allocate();
rwp->encoder_begin_callback_container = rb_whisper_callback_container_allocate();
rwp->abort_callback_container = rb_whisper_callback_container_allocate();
rwp->abort_callback_container = rb_whisper_abort_callback_container_allocate();
return obj;
}
@@ -1302,6 +1459,9 @@ ruby_whisper_params_on_new_segment(VALUE self)
ruby_whisper_params *rwp;
TypedData_Get_Struct(self, ruby_whisper_params, &ruby_whisper_params_type, rwp);
const VALUE blk = rb_block_proc();
if (NIL_P(rwp->new_segment_callback_container->callbacks)) {
rwp->new_segment_callback_container->callbacks = rb_ary_new();
}
rb_ary_push(rwp->new_segment_callback_container->callbacks, blk);
return Qnil;
}
@@ -1322,6 +1482,9 @@ ruby_whisper_params_on_progress(VALUE self)
ruby_whisper_params *rwp;
TypedData_Get_Struct(self, ruby_whisper_params, &ruby_whisper_params_type, rwp);
const VALUE blk = rb_block_proc();
if (NIL_P(rwp->progress_callback_container->callbacks)) {
rwp->progress_callback_container->callbacks = rb_ary_new();
}
rb_ary_push(rwp->progress_callback_container->callbacks, blk);
return Qnil;
}
@@ -1342,6 +1505,9 @@ ruby_whisper_params_on_encoder_begin(VALUE self)
ruby_whisper_params *rwp;
TypedData_Get_Struct(self, ruby_whisper_params, &ruby_whisper_params_type, rwp);
const VALUE blk = rb_block_proc();
if (NIL_P(rwp->encoder_begin_callback_container->callbacks)) {
rwp->encoder_begin_callback_container->callbacks = rb_ary_new();
}
rb_ary_push(rwp->encoder_begin_callback_container->callbacks, blk);
return Qnil;
}
@@ -1366,6 +1532,9 @@ ruby_whisper_params_abort_on(VALUE self)
ruby_whisper_params *rwp;
TypedData_Get_Struct(self, ruby_whisper_params, &ruby_whisper_params_type, rwp);
const VALUE blk = rb_block_proc();
if (NIL_P(rwp->abort_callback_container->callbacks)) {
rwp->abort_callback_container->callbacks = rb_ary_new();
}
rb_ary_push(rwp->abort_callback_container->callbacks, blk);
return Qnil;
}
+44 -3
View File
@@ -15,8 +15,37 @@ 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 void prepare_transcription(ruby_whisper_params * rwp, VALUE * self, int n_processors);
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;
}
typedef struct {
ruby_whisper_abort_callback_container *abort_callback_container;
} transcribe_ubf_args;
static void
transcribe_ubf(void *rb_args)
{
transcribe_ubf_args *args = (transcribe_ubf_args *)rb_args;
args->abort_callback_container->is_interrupted = true;
}
/*
* transcribe a single file
@@ -75,7 +104,19 @@ ruby_whisper_transcribe(int argc, VALUE *argv, VALUE self) {
prepare_transcription(rwp, &self, n_processors);
if (whisper_full_parallel(rw->context, rwp->params, pcmf32.data(), pcmf32.size(), n_processors) != 0) {
transcribe_without_gvl_args args = {
rw->context,
&rwp->params,
pcmf32.data(),
pcmf32.size(),
n_processors,
0,
};
transcribe_ubf_args ubf_args = {
rwp->abort_callback_container,
};
rb_thread_call_without_gvl(transcribe_without_gvl, (void *)&args, transcribe_ubf, (void *)&ubf_args);
if (args.result != 0) {
fprintf(stderr, "failed to process audio\n");
return self;
}