Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ruby : Sync whisper.cpp and model download feature #2617

Merged
merged 10 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bindings/ruby/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
LICENSE
pkg/
lib/whisper.*
lib/whisper.so
lib/whisper.bundle
lib/whisper.dll
65 changes: 55 additions & 10 deletions bindings/ruby/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Usage
```ruby
require "whisper"

whisper = Whisper::Context.new("path/to/model.bin")
whisper = Whisper::Context.new(Whisper::Model["base"])

params = Whisper::Params.new
params.language = "en"
Expand All @@ -41,21 +41,60 @@ end

### Preparing model ###

Use script to download model file(s):
Some models are prepared up-front:

```bash
git clone https://github.com/ggerganov/whisper.cpp.git
cd whisper.cpp
sh ./models/download-ggml-model.sh base.en
```ruby
base_en = Whisper::Model["base.en"]
whisper = Whisper::Context.new(base_en)
```

At first time you use a model, it is downloaded automatically. After that, downloaded cached file is used. To clear cache, call `#clear_cache`:

```ruby
Whisper::Model["base"].clear_cache
```

There are some types of models. See [models][] page for details.
You can see the list of prepared model names by `Whisper::Model.preconverted_model_names`:

```ruby
puts Whisper::Model.preconverted_model_names
# tiny
# tiny.en
# tiny-q5_1
# tiny.en-q5_1
# tiny-q8_0
# base
# base.en
# base-q5_1
# base.en-q5_1
# base-q8_0
# :
# :
```

You can also use local model files you prepared:

```ruby
whisper = Whisper::Context.new("path/to/your/model.bin")
```

Or, you can download model files:

```ruby
model_uri = Whisper::Model::URI.new("http://example.net/uri/of/your/model.bin")
whisper = Whisper::Context.new(model_uri)
```

See [models][] page for details.

### Preparing audio file ###

Currently, whisper.cpp accepts only 16-bit WAV files.

### API ###
API
---

### Segments ###

Once `Whisper::Context#transcribe` called, you can retrieve segments by `#each_segment`:

Expand Down Expand Up @@ -107,10 +146,12 @@ whisper.transcribe("path/to/audio.wav", params)

```

### Models ###

You can see model information:

```ruby
whisper = Whisper::Context.new("path/to/model.bin")
whisper = Whisper::Context.new(Whisper::Model["base"])
model = whisper.model

model.n_vocab # => 51864
Expand All @@ -128,6 +169,8 @@ model.type # => "base"

```

### Logging ###

You can set log callback:

```ruby
Expand Down Expand Up @@ -160,6 +203,8 @@ Whisper.log_set ->(level, buffer, user_data) {
Whisper::Context.new(MODEL)
```

### 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.

```ruby
Expand All @@ -169,7 +214,7 @@ require "wavefile"
reader = WaveFile::Reader.new("path/to/audio.wav", WaveFile::Format.new(:mono, :float, 16000))
samples = reader.enum_for(:each_buffer).map(&:samples).flatten

whisper = Whisper::Context.new("path/to/model.bin")
whisper = Whisper::Context.new(Whisper::Model["base"])
whisper.full(Whisper::Params.new, samples)
whisper.each_segment do |segment|
puts segment.text
Expand Down
14 changes: 2 additions & 12 deletions bindings/ruby/Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,9 @@ EXTSOURCES.each do |src|
end

CLEAN.include SOURCES
CLEAN.include FileList[
"ext/*.o",
"ext/*.metal",
"ext/whisper.{so,bundle,dll}",
"ext/depend"
]
CLEAN.include FileList["ext/*.o", "ext/*.metal", "ext/whisper.{so,bundle,dll}"]

task build: FileList[
"ext/Makefile",
"ext/ruby_whisper.h",
"ext/ruby_whisper.cpp",
"whispercpp.gemspec",
]
task build: ["ext/Makefile", "ext/ruby_whisper.h", "ext/ruby_whisper.cpp", "whispercpp.gemspec"]

directory "pkg"
CLOBBER.include "pkg"
Expand Down
1 change: 0 additions & 1 deletion bindings/ruby/ext/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ Makefile
whisper.so
whisper.bundle
whisper.dll
depend
scripts/get-flags.mk
*.o
*.c
Expand Down
8 changes: 4 additions & 4 deletions bindings/ruby/ext/extconf.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require 'mkmf'

# need to use c++ compiler flags
$CXXFLAGS << ' -std=c++11'
$CXXFLAGS << ' -std=c++17'

$LDFLAGS << ' -lstdc++'

Expand Down Expand Up @@ -35,10 +35,10 @@
$GGML_METAL_EMBED_LIBRARY = true
end

$MK_CPPFLAGS = '-Iggml/include -Iggml/src -Iinclude -Isrc -Iexamples'
$MK_CPPFLAGS = '-Iggml/include -Iggml/src -Iggml/src/ggml-cpu -Iinclude -Isrc -Iexamples'
$MK_CFLAGS = '-std=c11 -fPIC'
$MK_CXXFLAGS = '-std=c++11 -fPIC'
$MK_NVCCFLAGS = '-std=c++11'
$MK_CXXFLAGS = '-std=c++17 -fPIC'
$MK_NVCCFLAGS = '-std=c++17'
$MK_LDFLAGS = ''

$OBJ_GGML = []
Expand Down
7 changes: 7 additions & 0 deletions bindings/ruby/ext/ruby_whisper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ static ID id_to_enum;
static ID id_length;
static ID id_next;
static ID id_new;
static ID id_to_path;

static bool is_log_callback_finalized = false;

Expand Down Expand Up @@ -194,7 +195,9 @@ static VALUE ruby_whisper_params_allocate(VALUE klass) {

/*
* call-seq:
* new(Whisper::Model["base.en"]) -> Whisper::Context
* new("path/to/model.bin") -> Whisper::Context
* new(Whisper::Model::URI.new("https://example.net/uri/of/model.bin")) -> Whisper::Context
*/
static VALUE ruby_whisper_initialize(int argc, VALUE *argv, VALUE self) {
ruby_whisper *rw;
Expand All @@ -204,6 +207,9 @@ static VALUE ruby_whisper_initialize(int argc, VALUE *argv, VALUE self) {
rb_scan_args(argc, argv, "01", &whisper_model_file_path);
Data_Get_Struct(self, ruby_whisper, rw);

if (rb_respond_to(whisper_model_file_path, id_to_path)) {
whisper_model_file_path = rb_funcall(whisper_model_file_path, id_to_path, 0);
}
if (!rb_respond_to(whisper_model_file_path, id_to_s)) {
rb_raise(rb_eRuntimeError, "Expected file path to model to initialize Whisper::Context");
}
Expand Down Expand Up @@ -1733,6 +1739,7 @@ void Init_whisper() {
id_length = rb_intern("length");
id_next = rb_intern("next");
id_new = rb_intern("new");
id_to_path = rb_intern("to_path");

mWhisper = rb_define_module("Whisper");
cContext = rb_define_class_under(mWhisper, "Context", rb_cObject);
Expand Down
2 changes: 2 additions & 0 deletions bindings/ruby/lib/whisper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require "whisper.so"
require "whisper/model"
159 changes: 159 additions & 0 deletions bindings/ruby/lib/whisper/model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
require "whisper.so"
require "uri"
require "net/http"
require "pathname"
require "io/console/size"

class Whisper::Model
class URI
def initialize(uri)
@uri = URI(uri)
end

def to_path
cache
cache_path.to_path
end

def clear_cache
path = cache_path
path.delete if path.exist?
end

private

def cache_path
base_cache_dir/@uri.host/@uri.path[1..]
end

def base_cache_dir
base = case RUBY_PLATFORM
when /mswin|mingw/
ENV.key?("LOCALAPPDATA") ? Pathname(ENV["LOCALAPPDATA"]) : Pathname(Dir.home)/"AppData/Local"
when /darwin/
Pathname(Dir.home)/"Library/Caches"
else
ENV.key?("XDG_CACHE_HOME") ? ENV["XDG_CACHE_HOME"] : Pathname(Dir.home)/".cache"
end
base/"whisper.cpp"
end

def cache
path = cache_path
headers = {}
headers["if-modified-since"] = path.mtime.httpdate if path.exist?
request @uri, headers
path
end

def request(uri, headers)
Net::HTTP.start uri.host, uri.port, use_ssl: uri.scheme == "https" do |http|
request = Net::HTTP::Get.new(uri, headers)
http.request request do |response|
case response
when Net::HTTPNotModified
# noop
when Net::HTTPOK
download response
when Net::HTTPRedirection
request URI(response["location"])
else
raise response
end
end
end
end

def download(response)
path = cache_path
path.dirname.mkpath unless path.dirname.exist?
downloading_path = Pathname("#{path}.downloading")
size = response.content_length
downloading_path.open "wb" do |file|
downloaded = 0
response.read_body do |chunk|
file << chunk
downloaded += chunk.bytesize
show_progress downloaded, size
end
end
downloading_path.rename path
end

def show_progress(current, size)
return unless size

unless @prev
@prev = Time.now
$stderr.puts "Downloading #{@uri}"
end

now = Time.now
return if now - @prev < 1 && current < size

progress_width = 20
progress = current.to_f / size
arrow_length = progress * progress_width
arrow = "=" * (arrow_length - 1) + ">" + " " * (progress_width - arrow_length)
line = "[#{arrow}] (#{format_bytesize(current)} / #{format_bytesize(size)})"
padding = ' ' * ($stderr.winsize[1] - line.size)
$stderr.print "\r#{line}#{padding}"
$stderr.puts if current >= size
@prev = now
end

def format_bytesize(bytesize)
return "0.0 B" if bytesize.zero?

units = %w[B KiB MiB GiB TiB]
exp = (Math.log(bytesize) / Math.log(1024)).to_i
format("%.1f %s", bytesize.to_f / 1024 ** exp, units[exp])
end
end

@names = {}
%w[
tiny
tiny.en
tiny-q5_1
tiny.en-q5_1
tiny-q8_0
base
base.en
base-q5_1
base.en-q5_1
base-q8_0
small
small.en
small.en-tdrz
small-q5_1
small.en-q5_1
small-q8_0
medium
medium.en
medium-q5_0
medium.en-q5_0
medium-q8_0
large-v1
large-v2
large-v2-q5_0
large-v2-8_0
large-v3
large-v3-q5_0
large-v3-turbo
large-v3-turbo-q5_0
large-v3-turbo-q8_0
].each do |name|
@names[name] = URI.new("https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-#{name}.bin")
end

class << self
def [](name)
@names[name]
end

def preconverted_model_names
@names.keys
end
end
end
Loading
Loading