174 lines
6.6 KiB
Ruby
174 lines
6.6 KiB
Ruby
# 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
|