chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:45:58 +08:00
commit 2dd9ea9aee
261 changed files with 32719 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
# frozen_string_literal: true
require "vosk"
require "webmock/rspec"
RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
# Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching!
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
Vosk.log_level = -1
+173
View File
@@ -0,0 +1,173 @@
# frozen_string_literal: true
RSpec.describe Vosk::Model do
let(:tmpdir) { Dir.mktmpdir }
let(:en_model_path) { File.join(Dir.home, ".cache/vosk/vosk-model-small-en-us-0.4") }
let(:model_name) { "vosk-model-small-en-us-0.4" }
let(:model_list) do
[{ "name" => model_name, "lang" => "en-us", "type" => "small", "obsolete" => "false" }]
end
# Allocate an instance without calling initialize — no C library needed.
let(:stub_model) { described_class.allocate }
after { FileUtils.rm_rf(tmpdir) }
it "raises Vosk::Error on a bad path" do
expect do
described_class.new(model_path: "/nonexistent/path")
end.to raise_error(Vosk::Error, "Failed to create a model")
end
context "when loaded from a path" do
subject(:model) { described_class.new(model_path: en_model_path) }
it "constructs successfully" do
expect(model).to be_a(described_class)
end
it "finds a known word" do
expect(model.vosk_model_find_word("one")).to be >= 0
end
it "returns -1 for an unknown word" do
expect(model.vosk_model_find_word("xyzzy")).to eq(-1)
end
end
describe "#get_model_by_name" do
context "when the model directory already exists in MODEL_DIRS" do
before do
FileUtils.makedirs(File.join(tmpdir, model_name))
stub_const("Vosk::MODEL_DIRS", [tmpdir])
end
it "returns its path without hitting the network" do
expect(stub_model.send(:get_model_by_name, model_name)).to eq(File.join(tmpdir, model_name))
end
end
context "when the model is not in any MODEL_DIR" do
before do
stub_const("Vosk::MODEL_DIRS", [tmpdir])
stub_request(:get, Vosk::MODEL_LIST_URL)
.to_return(status: 200, body: model_list.to_json, headers: { "Content-Type" => "application/json" })
allow(stub_model).to receive(:download_model)
end
it "downloads the model to the last MODEL_DIR" do
stub_model.send(:get_model_by_name, model_name)
expect(stub_model).to have_received(:download_model).with(File.join(tmpdir, model_name))
end
it "returns the expected path after downloading" do
expect(stub_model.send(:get_model_by_name, model_name)).to eq(File.join(tmpdir, model_name))
end
it "exits when the model name is not in the remote list" do
expect { stub_model.send(:get_model_by_name, "vosk-model-unknown") }.to raise_error(SystemExit)
end
end
end
describe "#get_model_by_lang" do
context "when a matching model directory already exists" do
before do
FileUtils.makedirs(File.join(tmpdir, model_name))
stub_const("Vosk::MODEL_DIRS", [tmpdir])
end
it "returns its path without hitting the network" do
expect(stub_model.send(:get_model_by_lang, "en-us")).to eq(File.join(tmpdir, model_name))
end
it "also matches vosk-model-<lang> (without -small-)" do
other = "vosk-model-en-us-0.22"
FileUtils.makedirs(File.join(tmpdir, other))
stub_const("Vosk::MODEL_DIRS", [tmpdir])
expect(stub_model.send(:get_model_by_lang, "en-us")).not_to be_nil
end
end
context "when no local model exists for the language" do
before do
stub_const("Vosk::MODEL_DIRS", [tmpdir])
stub_request(:get, Vosk::MODEL_LIST_URL)
.to_return(status: 200, body: model_list.to_json, headers: { "Content-Type" => "application/json" })
allow(stub_model).to receive(:download_model)
end
it "downloads a small model for the language" do
stub_model.send(:get_model_by_lang, "en-us")
expect(stub_model).to have_received(:download_model).with(File.join(tmpdir, model_name))
end
it "exits when no model is available for the language" do
expect { stub_model.send(:get_model_by_lang, "xx-unknown") }.to raise_error(SystemExit)
end
end
end
describe "#download_model" do
let(:model_path) { File.join(tmpdir, model_name) }
let(:zip_content) do
Zip::OutputStream.write_buffer do |zip|
zip.put_next_entry("#{model_name}/conf/model.conf")
zip.write("# model config\n" * 1_000)
zip.put_next_entry("#{model_name}/README")
zip.write("test model\n")
end.string
end
let(:progress_bar_content) { StringIO.new }
let(:progress_bar_stream) do
dbl = double
allow(dbl).to receive(:tty?).with(no_args).and_return(true)
allow(dbl).to receive(:print) do |*args|
progress_bar_content.print(*args)
end
allow(dbl).to receive(:flush).with(no_args).and_return(dbl)
allow(dbl).to receive(:winsize).with(no_args).and_return([40, 180])
dbl
end
before do
stub_const("ProgressBar::Output::DEFAULT_OUTPUT_STREAM", progress_bar_stream)
stub_request(:get, "#{Vosk::MODEL_PRE_URL}#{model_name}.zip")
.to_return(status: 200, body: zip_content, headers: { "Content-Length" => zip_content.bytesize.to_s })
end
it "extracts the archive into the parent directory" do
stub_model.send(:download_model, model_path)
expect(File.exist?(File.join(model_path, "conf/model.conf"))).to be(true)
end
it "deletes the zip file after extraction" do
stub_model.send(:download_model, model_path)
expect(File.exist?("#{model_path}.zip")).to be(false)
end
it "creates the parent directory when it does not yet exist" do
nested_path = File.join(tmpdir, "new_subdir", model_name)
stub_model.send(:download_model, nested_path)
expect(Dir.exist?(File.dirname(nested_path))).to be(true)
end
it "uses the correct download URL" do
stub_model.send(:download_model, model_path)
expect(WebMock).to have_requested(:get, "#{Vosk::MODEL_PRE_URL}#{model_name}.zip")
end
it "displays a progress bar during download" do
stub_model.send(:download_model, model_path)
# TODO: test callback when multiple fragments are received
expect(progress_bar_content.string).to eq(
(" " * 180).concat(
"\r" \
"vosk-model-small-en-us-0.4.zip: 0%|=---=---=---=---=---=---=---=---=---=---=---=---=---=---=---=---=--" \
"-=---=---=---=---=---=---=---=---=---=---=| 0 bytes/?? [00:00<??:??:??, 0/s]\r" \
"vosk-model-small-en-us-0.4.zip: 100%|███████████████████████████████████████████████████████████████████" \
"████████████████████████████████████| 402 bytes/402 bytes [00:00<00:00, 0/s]\n",
),
)
end
end
end
+182
View File
@@ -0,0 +1,182 @@
# frozen_string_literal: true
require "json"
require "srt"
require "wavefile"
RSpec.describe Vosk do
let(:en_model_path) { File.join(Dir.home, ".cache/vosk/vosk-model-small-en-us-0.4") }
let(:test_wav_path) { File.expand_path("../example/test.wav", __dir__) }
let(:wave_reader) { WaveFile::Reader.new(test_wav_path) }
let(:wave_chunks) do
chunks = []
wave_reader.each_buffer(4000) { |buffer| chunks.push(buffer.samples.pack(WaveFile::PACK_CODES.dig(:pcm, 16))) }
chunks
end
after { wave_reader.close }
it "has a version number" do
expect(Vosk::VERSION).not_to be_nil
end
describe "EndpointerMode" do
it "defines the correct integer constants" do
expect([
Vosk::EndpointerMode::DEFAULT,
Vosk::EndpointerMode::SHORT,
Vosk::EndpointerMode::LONG,
Vosk::EndpointerMode::VERY_LONG,
]).to eq([0, 1, 2, 3])
end
end
describe Vosk::SpkModel do
it "raises Vosk::Error on a bad path" do
expect do
described_class.new("/nonexistent/path")
end.to raise_error(Vosk::Error, "Failed to create a speaker model")
end
end
describe Vosk::KaldiRecognizer do
let(:model) { Vosk::Model.new(model_path: en_model_path) }
let(:wave_sample_rate) { wave_reader.format.sample_rate }
describe "initialization" do
it "accepts (model, sample_rate)" do
expect(described_class.new(model, wave_sample_rate)).to be_a(described_class)
end
it "accepts (model, sample_rate, grammar_string)" do
rec = described_class.new(model, wave_sample_rate, '["one two three", "[unk]"]')
expect(rec).to be_a(described_class)
end
it "raises TypeError for an unknown third argument type" do
expect { described_class.new(model, wave_sample_rate, 42) }.to raise_error(TypeError)
end
end
context "when processing audio" do
subject(:rec) { described_class.new(model, wave_sample_rate) }
let(:wave_stream) do
stream = wave_chunks.each_with_object(StringIO.new) { |chunk, stream| stream.write(chunk) }
stream.rewind
stream
end
let(:expected_srt) do
<<~SRT
1
00:00:00,870 --> 00:00:02,610
what zero zero zero one
2
00:00:03,930 --> 00:00:04,950
no no to uno
3
00:00:06,240 --> 00:00:08,010
cyril one eight zero three
SRT
end
it "accept_waveform returns 0 or 1" do
results = wave_chunks.map { |chunk| rec.accept_waveform(chunk) }.uniq
expect(results).to contain_exactly(0, 1)
end
it "result returns valid JSON" do
rec.accept_waveform(wave_chunks.first)
expect { JSON.parse(rec.result) }.not_to raise_error
end
it "partial_result returns valid JSON" do
rec.accept_waveform(wave_chunks.first)
expect { JSON.parse(rec.partial_result) }.not_to raise_error
end
it "final_result returns valid JSON" do
expect { JSON.parse(rec.final_result) }.not_to raise_error
end
it "transcribes the test file to non-empty text" do
wave_chunks.each { |chunk| rec.accept_waveform(chunk) }
text = JSON.parse(rec.final_result)["text"]
expect(text).not_to be_empty
end
it "reset clears the partial result" do
rec.accept_waveform(wave_chunks.first)
rec.reset
expect(JSON.parse(rec.partial_result)["partial"]).to be_nil.or(eq(""))
end
it "set_words does not raise" do
expect { rec.words = true }.not_to raise_error
end
it "set_words includes per-word timing in results" do
rec.words = true
wave_chunks.each { |chunk| rec.accept_waveform(chunk) }
result = JSON.parse(rec.final_result)
expect(result).to have_key("text")
expect(result["result"]).to be_an(Array)
end
it "set_partial_words does not raise" do
expect { rec.partial_words = true }.not_to raise_error
end
it "set_max_alternatives produces an alternatives array" do
rec.max_alternatives = 5
wave_chunks.each { |chunk| rec.accept_waveform(chunk) }
result = JSON.parse(rec.final_result)
expect(result).to have_key("alternatives")
expect(result["alternatives"]).to be_an(Array)
end
it "set_endpointer_mode accepts EndpointerMode constants",
skip: Gem::Version.new(Vosk::VERSION) < Gem::Version.new("0.3.46") && "requires libvosk >= 0.3.46" do
expect { rec.endpointer_mode = :short }.not_to raise_error
end
it "set_endpointer_delays accepts float values",
skip: Gem::Version.new(Vosk::VERSION) < Gem::Version.new("0.3.46") && "requires libvosk >= 0.3.46" do
expect { rec.set_endpointer_delays(0.5, 1.0, 30.0) }.not_to raise_error
end
it "set_grammar changes the active grammar" do
expect { rec.grammar = '["one two three", "[unk]"]' }.not_to raise_error
end
it "srt_result produces a valid SRT" do
rec.words = true
expect(rec.srt_result(wave_stream)).to eq(expected_srt)
end
end
context "with a grammar recognizer" do
subject(:rec) do
described_class.new(
model, wave_sample_rate,
'["one two three four five six seven eight nine zero", "[unk]"]',
)
end
it "produces a result constrained to the grammar vocabulary" do
wave_chunks.each { |chunk| rec.accept_waveform(chunk) }
expect(JSON.parse(rec.final_result)).to have_key("text")
end
end
end
describe Vosk::Processor,
skip: Gem::Version.new(Vosk::VERSION) < Gem::Version.new("0.3.48") && "requires libvosk >= 0.3.48" do
it "raises Vosk::Error on a bad lang/type" do
expect { described_class.new("xx_invalid", "itn") }.to raise_error(Vosk::Error, "Failed to create processor")
end
end
end