Skip to content

Commit 9adbd76

Browse files
uvlad7nshmyrev
andauthored
Add Ruby gem (#2022)
* Init gem * WIP * WIP * WIP: Gemify * WIP * Try ffi_gen * Revert "Try ffi_gen" This reverts commit a54e56b. * Vibecode 1 * Vibecode 2 * rework progressbar * Some ref + test_captcha * Return deleted by Claude * Draft precompiled packaging * Review and refactor C bindings * ref OwnedString * Rename methods and add notes * rubocop * Fix progressbar, add examples * Ref spec * Ref spec - final * Final ver * Add srt, other fixes * Move to ruby dir --------- Co-authored-by: Nickolay V. Shmyrev <nshmyrev@gmail.com>
1 parent 625e44c commit 9adbd76

23 files changed

Lines changed: 1499 additions & 14 deletions

ruby/.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/.bundle/
2+
/.yardoc
3+
/_yardoc/
4+
/coverage/
5+
/doc/
6+
/pkg/
7+
/spec/reports/
8+
/tmp/
9+
10+
# rspec failure tracking
11+
.rspec_status
12+
.idea

ruby/.rspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
--format documentation
2+
--color
3+
--require spec_helper

ruby/.rubocop.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
plugins:
2+
- rubocop-rake
3+
- rubocop-rspec
4+
5+
AllCops:
6+
TargetRubyVersion: 2.5
7+
NewCops: enable
8+
9+
Style/StringLiterals:
10+
EnforcedStyle: double_quotes
11+
12+
Style/StringLiteralsInInterpolation:
13+
EnforcedStyle: double_quotes
14+
15+
Style/TrailingCommaInArguments:
16+
EnforcedStyleForMultiline: consistent_comma
17+
18+
Style/TrailingCommaInArrayLiteral:
19+
EnforcedStyleForMultiline: consistent_comma
20+
21+
Style/TrailingCommaInHashLiteral:
22+
EnforcedStyleForMultiline: consistent_comma
23+
24+
Style/NumericPredicate:
25+
EnforcedStyle: comparison
26+
27+
Layout/CaseIndentation:
28+
EnforcedStyle: end
29+
30+
Metrics/AbcSize:
31+
Max: 27
32+
33+
Metrics/CyclomaticComplexity:
34+
Max: 10
35+
36+
Metrics/PerceivedComplexity:
37+
Max: 10
38+
39+
Metrics/MethodLength:
40+
Max: 20
41+
42+
Style/SymbolArray:
43+
Exclude:
44+
- 'lib/vosk/ffi.rb'
45+
46+
RSpec/MultipleMemoizedHelpers:
47+
Max: 10
48+
49+
RSpec/ExampleLength:
50+
Max: 10
51+
52+
RSpec/MultipleExpectations:
53+
Max: 3

ruby/Gemfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
source "https://rubygems.org"
4+
5+
# Specify your gem's dependencies in vosk.gemspec
6+
gemspec
7+
8+
gem "rake", "~> 13.0"
9+
10+
gem "rspec", "~> 3.0"
11+
12+
gem "rubocop", "~> 1.21"
13+
gem "rubocop-rake", "~> 0.7.1"
14+
gem "rubocop-rspec", "~> 3.9"
15+
16+
gem "wavefile", "~> 1.1"
17+
18+
gem "webmock", "~> 3.26"

ruby/Gemfile.lock

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
PATH
2+
remote: .
3+
specs:
4+
vosk (0.3.45)
5+
bytesize (~> 0.1)
6+
ffi (~> 1.6)
7+
fileutils (~> 1.7)
8+
httparty (~> 0.21)
9+
progressbar (~> 1.13)
10+
rubyzip (~> 2.4)
11+
srt (~> 0.1.5)
12+
13+
GEM
14+
remote: https://rubygems.org/
15+
specs:
16+
addressable (2.8.8)
17+
public_suffix (>= 2.0.2, < 8.0)
18+
ast (2.4.2)
19+
bigdecimal (4.0.1)
20+
bytesize (0.1.2)
21+
crack (1.0.1)
22+
bigdecimal
23+
rexml
24+
csv (3.2.6)
25+
diff-lcs (1.5.1)
26+
ffi (1.17.0-x86_64-linux-gnu)
27+
fileutils (1.7.3)
28+
hashdiff (1.2.1)
29+
httparty (0.22.0)
30+
csv
31+
mini_mime (>= 1.0.0)
32+
multi_xml (>= 0.5.2)
33+
json (2.9.0)
34+
language_server-protocol (3.17.0.3)
35+
lint_roller (1.1.0)
36+
mini_mime (1.1.5)
37+
multi_xml (0.6.0)
38+
parallel (1.26.3)
39+
parser (3.3.10.2)
40+
ast (~> 2.4.1)
41+
racc
42+
prism (1.9.0)
43+
progressbar (1.13.0)
44+
public_suffix (7.0.2)
45+
racc (1.8.1)
46+
rainbow (3.1.1)
47+
rake (13.2.1)
48+
regexp_parser (2.9.3)
49+
rexml (3.4.4)
50+
rspec (3.13.0)
51+
rspec-core (~> 3.13.0)
52+
rspec-expectations (~> 3.13.0)
53+
rspec-mocks (~> 3.13.0)
54+
rspec-core (3.13.2)
55+
rspec-support (~> 3.13.0)
56+
rspec-expectations (3.13.3)
57+
diff-lcs (>= 1.2.0, < 2.0)
58+
rspec-support (~> 3.13.0)
59+
rspec-mocks (3.13.2)
60+
diff-lcs (>= 1.2.0, < 2.0)
61+
rspec-support (~> 3.13.0)
62+
rspec-support (3.13.2)
63+
rubocop (1.84.2)
64+
json (~> 2.3)
65+
language_server-protocol (~> 3.17.0.2)
66+
lint_roller (~> 1.1.0)
67+
parallel (~> 1.10)
68+
parser (>= 3.3.0.2)
69+
rainbow (>= 2.2.2, < 4.0)
70+
regexp_parser (>= 2.9.3, < 3.0)
71+
rubocop-ast (>= 1.49.0, < 2.0)
72+
ruby-progressbar (~> 1.7)
73+
unicode-display_width (>= 2.4.0, < 4.0)
74+
rubocop-ast (1.49.0)
75+
parser (>= 3.3.7.2)
76+
prism (~> 1.7)
77+
rubocop-rake (0.7.1)
78+
lint_roller (~> 1.1)
79+
rubocop (>= 1.72.1)
80+
rubocop-rspec (3.9.0)
81+
lint_roller (~> 1.1)
82+
rubocop (~> 1.81)
83+
ruby-progressbar (1.13.0)
84+
rubyzip (2.4.1)
85+
srt (0.1.5)
86+
unicode-display_width (3.1.2)
87+
unicode-emoji (~> 4.0, >= 4.0.4)
88+
unicode-emoji (4.0.4)
89+
wavefile (1.1.2)
90+
webmock (3.26.1)
91+
addressable (>= 2.8.0)
92+
crack (>= 0.3.2)
93+
hashdiff (>= 0.4.0, < 2.0.0)
94+
95+
PLATFORMS
96+
x86_64-linux
97+
98+
DEPENDENCIES
99+
rake (~> 13.0)
100+
rspec (~> 3.0)
101+
rubocop (~> 1.21)
102+
rubocop-rake (~> 0.7.1)
103+
rubocop-rspec (~> 3.9)
104+
vosk!
105+
wavefile (~> 1.1)
106+
webmock (~> 3.26)
107+
108+
BUNDLED WITH
109+
2.4.22

ruby/README.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Vosk Ruby
2+
3+
Ruby bindings for [Vosk](https://alphacephei.com/vosk/) — an offline speech recognition toolkit supporting 20+ languages.
4+
5+
## Installation
6+
7+
Add to your Gemfile:
8+
9+
```ruby
10+
gem "vosk"
11+
```
12+
13+
Or install directly:
14+
15+
```bash
16+
gem install vosk
17+
```
18+
19+
The gem ships with a precompiled `libvosk` for supported platforms. On other platforms it will attempt to load a system-installed `libvosk`.
20+
21+
## Usage
22+
23+
### Basic transcription
24+
25+
```ruby
26+
require "vosk"
27+
require "wavefile"
28+
29+
# Load a model by language (downloaded automatically if not cached)
30+
model = Vosk::Model.new(lang: "en-us")
31+
32+
# Or by name, or by local path:
33+
# model = Vosk::Model.new(model_name: "vosk-model-small-en-us-0.4")
34+
# model = Vosk::Model.new(model_path: "/path/to/model")
35+
36+
WaveFile::Reader.new("audio.wav") do |reader|
37+
rec = Vosk::KaldiRecognizer.new(model, reader.format.sample_rate)
38+
39+
reader.each_buffer(4000) do |buffer|
40+
data = buffer.samples.pack(WaveFile::PACK_CODES.dig(:pcm, 16))
41+
if rec.accept_waveform(data).nonzero?
42+
puts rec.result # JSON: {"text": "..."}
43+
else
44+
puts rec.partial_result # JSON: {"partial": "..."}
45+
end
46+
end
47+
puts rec.final_result
48+
end
49+
```
50+
51+
Audio must be mono, 16-bit PCM WAV. Use [wavefile](https://github.com/jstrait/wavefile) to read it.
52+
53+
### Grammar / keyword recognition
54+
55+
Pass a JSON array of phrases as the third argument:
56+
57+
```ruby
58+
rec = Vosk::KaldiRecognizer.new(model, sample_rate, '["one two three", "[unk]"]')
59+
```
60+
61+
### Recognizer options
62+
63+
```ruby
64+
rec.words = true # include per-word timing in results
65+
rec.partial_words = true # include per-word timing in partial results
66+
rec.max_alternatives = 5 # return n-best list instead of single result
67+
rec.nlsml = true # return NLSML instead of JSON
68+
```
69+
70+
### SRT subtitle generation
71+
72+
`srt_result` reads raw PCM from any IO stream and returns an SRT-formatted string. Use ffmpeg to decode any audio format on the fly:
73+
74+
```ruby
75+
require "vosk"
76+
77+
SAMPLE_RATE = 16_000
78+
79+
model = Vosk::Model.new(lang: "en-us")
80+
rec = Vosk::KaldiRecognizer.new(model, SAMPLE_RATE)
81+
rec.words = true # required for word-level timestamps
82+
83+
IO.popen(["ffmpeg", "-loglevel", "quiet", "-i", "audio.mp4",
84+
"-ar", SAMPLE_RATE.to_s, "-ac", "1", "-f", "s16le", "-",]) do |stream|
85+
puts rec.srt_result(stream)
86+
end
87+
```
88+
89+
The `words_per_line:` keyword controls how many words appear per subtitle line (default: 7):
90+
91+
```ruby
92+
rec.srt_result(stream, words_per_line: 5)
93+
```
94+
95+
### Speaker identification
96+
97+
```ruby
98+
spk_model = Vosk::SpkModel.new("/path/to/spk-model")
99+
rec = Vosk::KaldiRecognizer.new(model, sample_rate)
100+
rec.spk_model = spk_model
101+
```
102+
103+
### Listing available models
104+
105+
```ruby
106+
puts Vosk.models # all model names
107+
puts Vosk.languages # all supported language codes
108+
```
109+
110+
### Logging
111+
112+
```ruby
113+
Vosk.log_level = -1 # suppress all output
114+
Vosk.log_level = 0 # default
115+
```
116+
117+
### Transcriber CLI
118+
119+
The gem includes a `vosk-transcriber` executable:
120+
121+
```bash
122+
vosk-transcriber audio.wav
123+
```
124+
125+
## Model storage
126+
127+
Models are cached in `~/.cache/vosk/` by default, or in the directory set by `$VOSK_MODEL_PATH`.
128+
129+
## Development
130+
131+
```bash
132+
bundle install
133+
bundle exec rake spec
134+
```
135+
136+
## License
137+
138+
Apache-2.0

ruby/Rakefile

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# frozen_string_literal: true
2+
3+
require "bundler/gem_tasks"
4+
require "rspec/core/rake_task"
5+
require "rubygems/package_task"
6+
7+
RSpec::Core::RakeTask.new(:spec)
8+
9+
require "rubocop/rake_task"
10+
require "fileutils"
11+
12+
RuboCop::RakeTask.new
13+
14+
# TODO: modify 'build' task somehow to include pre-compiled binary
15+
# (or add this logic into gemspec)
16+
17+
spec = Gem::Specification.load("vosk.gemspec")
18+
# Tweak spec when building pre-compiled gem:
19+
# (see example in https://github.com/oxidize-rb/actions/blob/main/cross-gem/action.yml and https://github.com/oxidize-rb/rb-sys/blob/main/gem/lib/rb_sys/extensiontask.rb)
20+
# See also https://github.com/rake-compiler/rake-compiler/blob/master/lib/rake/extensiontask.rb and https://github.com/rake-compiler/rake-compiler-dock
21+
# def cross_compile?
22+
# # TODO: impl
23+
# false
24+
# end
25+
26+
# TODO: maybe it should be a separate task that extends Gem::PackageTask
27+
# if cross_compile?
28+
# # Can't use 'file' task bc we don't know file names exactly
29+
# task :libs do
30+
# # TODO
31+
# # vosk_source = os.getenv("VOSK_SOURCE", os.path.abspath(os.path.join(os.path.dirname(__file__),
32+
# # "..")))
33+
# FileUtils.cp Dir[File.join(vosk_source, "src/lib*.*")], "lib/vosk"
34+
# # TODO: check it works properly (spec is modified after `gem` task is already created)
35+
# # Matches package_data = {'vosk': ['*.so', '*.dll', '*.dyld']},
36+
# spec.files += Dir["lib/vosk/*.{so,dll,dyld}"]
37+
# end
38+
# task gem: :libs
39+
# # TODO: figure out platform
40+
# # system = os.environ.get('VOSK_SYSTEM', platform.system())
41+
# # architecture = os.environ.get('VOSK_ARCHITECTURE', platform.architecture()[0])
42+
# # machine = os.environ.get('VOSK_MACHINE', platform.machine())
43+
# spec.platform = Gem::Platform.new(platf)
44+
# end
45+
46+
# add your default gem packing task
47+
# Run with `rake build`
48+
Gem::PackageTask.new(spec)
49+
50+
task default: %i[spec rubocop]

0 commit comments

Comments
 (0)