--- /dev/null
+README.md
+LICENSE
+sig
--- /dev/null
+title: whispercpp
+main_page: README.md
### Low-level API to transcribe ###
-You can also call `Whisper::Context#full` and `#full_parallel` with a Ruby array as samples. Although `#transcribe` with audio file path is recommended because it extracts PCM samples in C++ and is fast, `#full` and `#full_parallel` give you flexibility.
+You can also call `Whisper::Context#full` and `#full_parallel` with a Ruby array as samples. Although `#transcribe` with audio file path is recommended because it extracts PCM samples in C++ and is fast, `#full` and `#full_parallel` give you flexibility. Unlike `#transcribe`, these methods requires 16,000 Hz, 32-bit float audio.
```ruby
require "whisper"
```ruby
require "torchaudio"
-require "arrow-numo-narray"
+require "ndav/torch/tensor"
require "whisper"
waveform, sample_rate = TorchAudio.load("test/fixtures/jfk.wav")
-# Convert Torch::Tensor to Arrow::Array via Numo::NArray
-samples = waveform.squeeze.numo.to_arrow.to_arrow_array
+# Convert Torch::Tensor to NDAV
+samples = waveform.squeeze.to_ndav
whisper = Whisper::Context.new("base")
whisper
- # Arrow::Array exports MemoryView
+ # NDAV exports MemoryView
.full(Whisper::Params.new, samples)
```
file src
directory dir
file dest => [src, dir] do |t|
- cp t.source, t.name
+ copy t.source, t.name
end
SOURCES.include dest
end
SO_FILE = File.join("ext", LIB_NAME)
LIB_FILE = File.join("lib", LIB_NAME)
-file "ext/Makefile" => SRC + ["ext/extconf.rb"] + SOURCES do |t|
+file "ext/Makefile" => SRC + SOURCES + FileList["ext/*.rb"] do |t|
chdir "ext" do
ruby "extconf.rb"
end
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
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
end
end
+ def prefix(lib)
+ "lib"
+ end
+
def tsort_each_node
@nodes.each_key do |node|
yield node
--- /dev/null
+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
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|
$(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
+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)
}.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)|
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
--- /dev/null
+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
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);
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);
+ }
}
/*
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;
#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;
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;
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;
#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;
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)
{
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;
}
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 (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->format != NULL && memview->ndim != 1) {
+
+ 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;
}
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)
{
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);
}
/*
}
}
+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)
{
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);
}
/*
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--) {
rb_funcall(cb, id_call, 1, segment);
}
}
+
+ return NULL;
}
-static void progress_callback(struct whisper_context *ctx, struct whisper_state *state, int progress_cur, void *user_data) {
+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;
- const VALUE progress = INT2NUM(progress_cur);
- // Currently, doesn't support state because
+ 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(container->callback)) {
- rb_funcall(container->callback, id_call, 4, *container->context, Qnil, progress, container->user_data);
+ 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;
+ return NULL;
}
for (int j = 0; j < callbacks_len; j++) {
VALUE cb = rb_ary_entry(container->callbacks, j);
- rb_funcall(cb, id_call, 1, progress);
+ rb_funcall(cb, id_call, 1, INT2NUM(progress_cur));
}
+
+ return NULL;
}
-static bool encoder_begin_callback(struct whisper_context *ctx, struct whisper_state *state, void *user_data) {
+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;
- bool is_aborted = false;
- VALUE result;
+ if (!ruby_whisper_callback_container_is_present(container)) {
+ return;
+ }
+
+ call_progress_callbacks_args args = {
+ container,
+ state,
+ progress_cur
+ };
+ rb_thread_call_with_gvl(call_progress_callbacks, (void *)&args);
+}
+
+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");
}
}
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)
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);
}
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);
}
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;
}
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;
}
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;
}
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;
}
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;
}
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
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;
}
.devops
.github
ci
- examples/wchess/wchess.wasm
+ examples/addon.node
+ examples/bench.wasm
+ examples/command
+ examples/command.wasm
+ examples/lsp
+ examples/main
+ examples/python
+ examples/stream
+ examples/stream.wasm
+ examples/sycl
+ examples/talk-llama
+ examples/wchess
examples/whisper.android
examples/whisper.android.java
+ examples/whisper.nvim
examples/whisper.objc
examples/whisper.swiftui
+ examples/whisper.wasm
grammars
models
samples
scripts
+ tests
].collect {|dir| root/dir}
ignored_files = %w[
AUTHORS
Makefile
- README.md
- README_sycl.md
.gitignore
.gitmodules
.dockerignore
- whisper.nvim
- twitch.sh
- yt-wsp.sh
- close-issue.yml
- build-xcframework.sh
+]
+ignored_exts = %w[
+ .yml
+ .sh
+ .md
+ .py
+ .js
+ .nvim
]
EXTSOURCES =
`git ls-files -z #{root}`.split("\x0")
.collect {|file| Pathname(file)}
.reject {|file|
- ignored_dirs.any? {|dir| file.descend.any? {|desc| desc == dir}} ||
+ ignored_exts.include?(file.extname) ||
ignored_files.include?(file.basename.to_path) ||
- (file.descend.to_a[1] != root && file.descend.to_a[1] != Pathname("..")/"javascript")
+ ignored_dirs.any? {|dir| file.descend.any? {|desc| desc == dir}} ||
+ (file.descend.to_a[1] != root && file != Pathname("..")/"javascript"/"package-tmpl.json")
}
.collect(&:to_path)
end
type log_callback = ^(Integer level, String message, Object user_data) -> void
- type new_segment_callback = ^(Whisper::Context, void, Integer n_new, Object user_data) -> void
- type progress_callback = ^(Whisper::Context, void, Integer progress, Object user_data) -> void
- type encoder_begin_callback = ^(Whisper::Context, void, Object user_data) -> void
- type abort_callback = ^(Whisper::Context, void, Object user_data) -> boolish
+ type new_segment_callback = ^(Whisper::Context, untyped, Integer n_new, Object user_data) -> void
+ type progress_callback = ^(Whisper::Context, untyped, Integer progress, Object user_data) -> void
+ type encoder_begin_callback = ^(Whisper::Context, untyped, Object user_data) -> void
+ type abort_callback = ^(Whisper::Context, untyped, Object user_data) -> boolish
VERSION: String
LOG_LEVEL_NONE: Integer
# puts text
# end
#
- # If n_processors is greater than 1, you cannot set any callbacks including
+ # 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
- def transcribe: (path, Params, ?n_processors: Integer) -> self
- | (path, Params, ?n_processors: Integer) { (String) -> void } -> self
+ def transcribe: (path, Whisper::Params, ?n_processors: Integer) -> self
+ | (path, Whisper::Params, ?n_processors: Integer) { (String) -> void } -> self
def model_n_vocab: () -> Integer
def model_n_audio_ctx: () -> Integer
# puts segment.text
# end
#
- # Returns an Enumerator if no block given:
+ # Returns an `Enumerator` if no block given:
#
# whisper.transcribe("path/to/audio.wav", params)
# enum = whisper.each_segment
#
def full_lang_id: () -> Integer
- # Start time of a segment indexed by +segment_index+ in centiseconds (10 times milliseconds).
+ # Start time of a segment indexed by `segment_index` in centiseconds (10 times milliseconds).
#
# full_get_segment_t0(3) # => 1668 (16680 ms)
#
def full_get_segment_t0: (Integer) -> Integer
- # End time of a segment indexed by +segment_index+ in centiseconds (10 times milliseconds).
+ # End time of a segment indexed by `segment_index` in centiseconds (10 times milliseconds).
#
# full_get_segment_t1(3) # => 1668 (16680 ms)
#
def full_get_segment_t1: (Integer) -> Integer
- # Whether the next segment indexed by +segment_index+ is predicated as a speaker turn.
+ # Whether the next segment indexed by `segment_index` is predicated as a speaker turn.
#
# full_get_segment_speacker_turn_next(3) # => true
#
def full_get_segment_speaker_turn_next: (Integer) -> (true | false)
- # Text of a segment indexed by +segment_index+.
+ # Text of a segment indexed by `segment_index`.
#
# full_get_segment_text(3) # => "ask not what your country can do for you, ..."
#
def full_get_segment_no_speech_prob: (Integer) -> Float
- # Run the entire model: PCM -> log mel spectrogram -> encoder -> decoder -> text
- # Not thread safe for same context
+ # 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.
#
- # The second argument +samples+ must be an array of samples, respond to :length, or be a MemoryView of an array of float. It must be 32 bit float PCM audio data.
+ # The second argument `samples` must be an array of samples, respond to `:length`, or be a MemoryView of an array of float. It must be 32 bit float PCM audio data.
#
- def full: (Params, Array[Float] samples, ?Integer n_samples) -> self
- | (Params, _Samples, ?Integer n_samples) -> self
+ def full: (Whisper::Params, Array[Float] samples, ?Integer n_samples) -> self
+ | (Whisper::Params, _Samples, ?Integer n_samples) -> self
- # Split the input audio in chunks and process each chunk separately using whisper_full_with_state()
- # Result is stored in the default state of the context
- # Not thread safe if executed in parallel on the same context.
- # It seems this approach can offer some speedup in some cases.
+ # Split the input audio in chunks and process each chunk separately using `whisper_full_with_state()`
+ # Result is stored in the default state of the context
+ # Not thread safe if executed in parallel on the same context.
+ # It seems this approach can offer some speedup in some cases.
# However, the transcription accuracy can be worse at the beginning and end of each chunk.
#
- # If n_processors is greater than 1, you cannot set any callbacks including
+ # 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
- def full_parallel: (Params, Array[Float], ?Integer n_samples) -> self
- | (Params, _Samples, ?Integer n_samples) -> self
- | (Params, _Samples, ?Integer? n_samples, Integer n_processors) -> self
+ def full_parallel: (Whisper::Params, Array[Float], ?Integer n_samples) -> self
+ | (Whisper::Params, _Samples, ?Integer n_samples) -> self
+ | (Whisper::Params, _Samples, ?Integer? n_samples, Integer n_processors) -> self
def to_srt: () -> String
def to_webvtt: () -> String
def translate: () -> (true | false)
def no_context=: (boolish) -> boolish
- # If true, does not use past transcription (if any) as initial prompt for the decoder.
+ # If `true`, does not use past transcription (if any) as initial prompt for the decoder.
#
def no_context: () -> (true | false)
def single_segment=: (boolish) -> boolish
- # If true, forces single segment output (useful for streaming).
+ # If `true`, forces single segment output (useful for streaming).
#
def single_segment: () -> (true | false)
def print_special=: (boolish) -> boolish
- # If true, prints special tokens (e.g. <SOT>, <EOT>, <BEG>, etc.).
+ # If `true`, prints special tokens (e.g. <SOT>, <EOT>, <BEG>, etc.).
#
def print_special: () -> (true | false)
def print_progress=: (boolish) -> boolish
- # If true, prints progress information.
+ # If `true`, prints progress information.
#
def print_progress: () -> (true | false)
def print_realtime=: (boolish) -> boolish
- # If true, prints results from within whisper.cpp. (avoid it, use callback instead)
+ # If `true`, prints results from within whisper.cpp. (avoid it, use callback instead)
#
def print_realtime: () -> (true | false)
- # If true, prints timestamps for each text segment when printing realtime.
+ # If `true`, prints timestamps for each text segment when printing realtime.
#
def print_timestamps=: (boolish) -> boolish
def suppress_blank=: (boolish) -> boolish
- # If true, suppresses blank outputs.
+ # If `true`, suppresses blank outputs.
#
def suppress_blank: () -> (true | false)
def suppress_nst=: (boolish) -> boolish
- # If true, suppresses non-speech-tokens.
+ # If `true`, suppresses non-speech-tokens.
#
def suppress_nst: () -> (true | false)
def token_timestamps=: (boolish) -> boolish
- # If true, enables token-level timestamps.
+ # If `true`, enables token-level timestamps.
#
def token_timestamps: () -> (true | false)
def split_on_word=: (boolish) -> boolish
- # If true, split on word rather than on token (when used with max_len).
+ # If `true`, split on word rather than on token (when used with max_len).
#
def split_on_word: () -> (true | false)
def initial_prompt=: (_ToS) -> _ToS
def carry_initial_prompt=: (boolish) -> boolish
- # Tokens to provide to the whisper decoder as initial prompt
- # these are prepended to any existing text context from a previous call
- # use whisper_tokenize() to convert text to tokens.
+ # Tokens to provide to the whisper decoder as initial prompt
+ # these are prepended to any existing text context from a previous call
+ # use whisper_tokenize() to convert text to tokens.
# Maximum of whisper_n_text_ctx()/2 tokens are used (typically 224).
#
def initial_prompt: () -> (String | nil)
def diarize=: (boolish) -> boolish
- # If true, enables diarization.
+ # If `true`, enables diarization.
#
def diarize: () -> (true | false)
#
def on_new_segment: { (Segment) -> void } -> void
- # Hook called on progress update. Yields each progress Integer between 0 and 100.
+ # Hook called on progress update. Yields each progress `Integer` between 0 and 100.
#
def on_progress: { (Integer progress) -> void } -> void
#
def on_encoder_begin: { () -> void } -> void
- # Call block to determine whether abort or not. Return +true+ when you want to abort.
+ # Call block to determine whether abort or not. Return `true` when you want to abort.
#
# params.abort_on do
# if some_condition
# Yields each Whisper::Token:
#
- # whisper.each_segment.first.each_token do |token|
- # p token
- # end
+ # whisper.each_segment.first.each_token do |token|
+ # p token
+ # end
#
- # Returns an Enumerator if no block is given:
+ # Returns an `Enumerator` if no block is given:
#
- # whisper.each_segment.first.each_token.to_a # => [#<Whisper::Token>, ...]
+ # whisper.each_segment.first.each_token.to_a # => [#<Whisper::Token>, ...]
#
def each_token: { (Token) -> void } -> void
| () -> Enumerator[Token]
def to_webvtt_cue: () -> String
- # Possible keys: :start_time, :end_time, :text, :no_speech_prob, :speaker_turn_next
+ # Possible keys: `:start_time`, `:end_time`, `:text`, `:no_speech_prob`, `:speaker_turn_next`
#
# whisper.each_segment do |segment|
# segment => {start_time:, end_time:, text:, no_speech_prob:, speaker_turn_next:}
# [EXPERIMENTAL] Token-level timestamps with DTW
#
- # Do not use if you haven't computed token-level timestamps with dtw.
+ # Do not use if you haven't computed token-level timestamps with dtw.
# Roughly corresponds to the moment in audio in which the token was output.
#
def t_dtw: () -> Integer
# Start time of the token.
#
- # Token-level timestamp data.
+ # Token-level timestamp data.
# Do not use if you haven't computed token-level timestamps.
#
def start_time: () -> Integer
# End time of the token.
#
- # Token-level timestamp data.
+ # Token-level timestamp data.
# Do not use if you haven't computed token-level timestamps.
#
def end_time: () -> Integer
require_relative "helper"
require 'tempfile'
require 'tmpdir'
-require 'shellwords'
+require 'open3'
class TestPackage < TestBase
def test_build
Tempfile.create do |file|
- assert system("gem", "build", "whispercpp.gemspec", "--output", file.to_path.shellescape, exception: true)
+ assert system("gem", "build", "whispercpp.gemspec", "--output", file.to_path, exception: true)
assert file.size > 0
assert_path_exist file.to_path
end
def test_install
gemspec = Gem::Specification.load("whispercpp.gemspec")
Dir.mktmpdir do |dir|
- system "gem", "install", "--install-dir", dir.shellescape, "--no-document", "pkg/#{gemspec.file_name.shellescape}", exception: true
+ system "gem", "install", "--install-dir", dir, "--no-document", File.join("pkg", gemspec.file_name), exception: true
assert_installed dir, gemspec.version
end
end
omit_unless RUBY_PLATFORM.match?(/darwin/) do
gemspec = Gem::Specification.load("whispercpp.gemspec")
Dir.mktmpdir do |dir|
- system "gem", "install", "--install-dir", dir.shellescape, "--no-document", "pkg/#{gemspec.file_name.shellescape}", "--", "--enable-whisper-coreml", exception: true
+ system "gem", "install", "--install-dir", dir, "--no-document", File.join("pkg", gemspec.file_name), "--", "--enable-whisper-coreml", exception: true
assert_installed dir, gemspec.version
libdir = File.join(dir, "gems", "#{gemspec.name}-#{gemspec.version}", "lib")
assert_nothing_raised do
system "ruby", "-I", libdir, "-r", "whisper", "-e", "Whisper::Context.new('tiny')", exception: true
end
- assert_match(/COREML = 1/, `ruby -I #{libdir.shellescape} -r whisper -e 'puts Whisper.system_info_str'`)
+ output, status = Open3.capture2("ruby", "-I", libdir, "-r", "whisper", "-e", "puts Whisper.system_info_str")
+ assert_match /COREML = 1/, output
end
end
end