chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "vosk/version"
|
||||
require "httparty"
|
||||
require_relative "vosk/progressbar"
|
||||
require_relative "vosk/ffi"
|
||||
require "zip"
|
||||
require "fileutils"
|
||||
require "json"
|
||||
require "srt"
|
||||
|
||||
# Vosk speech recognition system
|
||||
module Vosk
|
||||
class Error < StandardError; end
|
||||
|
||||
# Remote location of the models and local folders
|
||||
MODEL_PRE_URL = "https://alphacephei.com/vosk/models/"
|
||||
MODEL_LIST_URL = "#{MODEL_PRE_URL}model-list.json"
|
||||
# TODO: Test on Windows
|
||||
MODEL_DIRS = [
|
||||
ENV.fetch("VOSK_MODEL_PATH", nil), "/usr/share/vosk",
|
||||
File.join(Dir.home, "AppData/Local/vosk"), File.join(Dir.home, ".cache/vosk"),
|
||||
].compact.freeze
|
||||
|
||||
# Different from Python: no need to print inside the method, simply use +puts Vosk.models+
|
||||
def self.models
|
||||
response = HTTParty.get(MODEL_LIST_URL, timeout: 10)
|
||||
response.map { |model| model["name"] }
|
||||
end
|
||||
|
||||
# Different from Python: no need to print inside the method, simply use +puts Vosk.languages+
|
||||
def self.languages
|
||||
response = HTTParty.get(MODEL_LIST_URL, timeout: 10)
|
||||
response.map { |model| model["lang"] }.uniq
|
||||
end
|
||||
|
||||
# Model stores all the data required for recognition
|
||||
# it contains static data and can be shared across processing
|
||||
# threads.
|
||||
class Model
|
||||
attr_reader :handle
|
||||
|
||||
def initialize(model_path: nil, model_name: nil, lang: nil)
|
||||
model_path ||= get_model_path(model_name, lang)
|
||||
@handle = C.vosk_model_new(model_path)
|
||||
end
|
||||
|
||||
def vosk_model_find_word(word)
|
||||
C.vosk_model_find_word(@handle, word)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_model_path(model_name, lang)
|
||||
if model_name
|
||||
get_model_by_name(model_name)
|
||||
else
|
||||
get_model_by_lang(lang)
|
||||
end
|
||||
end
|
||||
|
||||
def get_model_by_name(model_name)
|
||||
MODEL_DIRS.each do |directory|
|
||||
next unless Dir.exist?(directory)
|
||||
|
||||
entry = Dir.entries(directory).find { |f| f == model_name }
|
||||
return File.join(directory, entry) if entry
|
||||
end
|
||||
response = HTTParty.get(MODEL_LIST_URL, timeout: 10)
|
||||
result_model = response.map { |m| m["name"] }.find { |n| n == model_name }
|
||||
unless result_model
|
||||
# It's not common for Ruby gems to exit the whole process, but I decided to match Python behavior
|
||||
puts "model name #{model_name} does not exist"
|
||||
exit(1)
|
||||
end
|
||||
# It always selects the last dir for downloads, ignoring env and windows-specific dir
|
||||
dest = File.join(MODEL_DIRS.last, result_model)
|
||||
download_model(dest)
|
||||
dest
|
||||
end
|
||||
|
||||
def get_model_by_lang(lang)
|
||||
MODEL_DIRS.each do |directory|
|
||||
next unless Dir.exist?(directory)
|
||||
|
||||
entry = Dir.entries(directory).find { |f| f.match?(/\Avosk-model(-small)?-#{Regexp.escape(lang)}/) }
|
||||
return File.join(directory, entry) if entry
|
||||
end
|
||||
response = HTTParty.get(MODEL_LIST_URL, timeout: 10)
|
||||
result_model = response.find do |m|
|
||||
m["lang"] == lang && m["type"] == "small" && m["obsolete"] == "false"
|
||||
end&.dig("name")
|
||||
unless result_model
|
||||
# It's not common for Ruby gems to exit the whole process, but I decided to match Python behavior
|
||||
puts "lang #{lang} does not exist"
|
||||
exit(1)
|
||||
end
|
||||
# It always selects the last dir for downloads, ignoring env and windows-specific dir
|
||||
dest = File.join(MODEL_DIRS.last, result_model)
|
||||
download_model(dest)
|
||||
dest
|
||||
end
|
||||
|
||||
# Python param "model_name" is, in fact, a full path
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def download_model(model_path)
|
||||
dir = File.dirname(model_path)
|
||||
# Python version won't try to create the directory if a file with the same name exists
|
||||
FileUtils.makedirs(dir)
|
||||
model_name = File.basename(model_path)
|
||||
zip_path = "#{model_path}.zip"
|
||||
url = "#{MODEL_PRE_URL}#{model_name}.zip"
|
||||
|
||||
progressbar = ProgressBar.create(
|
||||
# Why add MODEL_PRE_URL and then split it away?
|
||||
title: "#{model_name}.zip",
|
||||
total: nil,
|
||||
progress_mark: "█",
|
||||
format: "%t: %j%%|%B| %s/%z [%d<%o, %r/s]",
|
||||
rate_scale: ->(rate) { ByteSize.new(rate.to_i).to_s },
|
||||
)
|
||||
|
||||
begin
|
||||
download_file(url, zip_path) do |bsize, tsize|
|
||||
progressbar.total = tsize if tsize && tsize >= progressbar.progress
|
||||
progressbar.progress += bsize
|
||||
end
|
||||
progressbar.finish
|
||||
ensure
|
||||
progressbar&.stop
|
||||
end
|
||||
|
||||
Zip::File.open(zip_path) do |zip_file|
|
||||
zip_file.each do |entry|
|
||||
entry_path = File.join(dir, entry.name)
|
||||
FileUtils.makedirs(File.dirname(entry_path))
|
||||
entry.extract(entry_path) { true }
|
||||
end
|
||||
end
|
||||
|
||||
File.unlink(zip_path)
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
def download_file(url, dest, &callback)
|
||||
File.open(dest, File::CREAT | File::WRONLY | File::TRUNC | File::BINARY) do |file|
|
||||
response = HTTParty.get(url, stream_body: true) do |fragment|
|
||||
next unless fragment.http_response.is_a?(Net::HTTPSuccess)
|
||||
|
||||
file.write(fragment)
|
||||
callback&.call(fragment.bytesize, fragment.http_response["Content-Length"]&.to_i)
|
||||
end
|
||||
raise HTTParty::ResponseError.new(response), "Code #{response.code}" unless response.success?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Speaker model is the same as model but contains the data
|
||||
# for speaker identification.
|
||||
class SpkModel
|
||||
attr_reader :handle
|
||||
|
||||
def initialize(model_path)
|
||||
@handle = C.vosk_spk_model_new(model_path)
|
||||
end
|
||||
end
|
||||
|
||||
# Endpointer scaling factor
|
||||
class EndpointerMode
|
||||
C::VoskEndpointerMode.symbol_map.each do |name, value|
|
||||
const_set(name.upcase, value)
|
||||
end
|
||||
end
|
||||
|
||||
# Recognizer object is the main object which processes data.
|
||||
# Each recognizer usually runs in own thread and takes audio as input.
|
||||
# Once audio is processed recognizer returns JSON object as a string
|
||||
# which represent decoded information - words, confidences, times, n-best lists,
|
||||
# speaker information and so on
|
||||
class KaldiRecognizer
|
||||
# Python version accepts *args, so in case of a wrong number of arguments it'll raise TypeError,
|
||||
# while Ruby raises ArgumentError
|
||||
def initialize(model, sample_rate, grammar_or_spk_model = nil)
|
||||
@handle = case grammar_or_spk_model
|
||||
when nil
|
||||
C.vosk_recognizer_new(model.handle, sample_rate.to_f)
|
||||
when SpkModel
|
||||
C.vosk_recognizer_new_spk(model.handle, sample_rate.to_f, grammar_or_spk_model.handle)
|
||||
when String
|
||||
C.vosk_recognizer_new_grm(model.handle, sample_rate.to_f, grammar_or_spk_model)
|
||||
else
|
||||
raise TypeError, "Unknown arguments"
|
||||
end
|
||||
end
|
||||
|
||||
def max_alternatives=(max_alternatives)
|
||||
C.vosk_recognizer_set_max_alternatives(@handle, max_alternatives)
|
||||
end
|
||||
|
||||
def words=(enable_words)
|
||||
C.vosk_recognizer_set_words(@handle, enable_words ? 1 : 0)
|
||||
end
|
||||
|
||||
def partial_words=(enable_partial_words)
|
||||
C.vosk_recognizer_set_partial_words(@handle, enable_partial_words ? 1 : 0)
|
||||
end
|
||||
|
||||
def nlsml=(enable_nlsml)
|
||||
C.vosk_recognizer_set_nlsml(@handle, enable_nlsml ? 1 : 0)
|
||||
end
|
||||
|
||||
def endpointer_mode=(mode)
|
||||
C.vosk_recognizer_set_endpointer_mode(@handle, mode.to_i)
|
||||
end
|
||||
|
||||
def set_endpointer_delays(t_start_max, t_end, t_max)
|
||||
C.vosk_recognizer_set_endpointer_delays(@handle, t_start_max.to_f, t_end.to_f, t_max.to_f)
|
||||
end
|
||||
|
||||
def spk_model=(spk_model)
|
||||
C.vosk_recognizer_set_spk_model(@handle, spk_model.handle)
|
||||
end
|
||||
|
||||
def grammar=(grammar)
|
||||
C.vosk_recognizer_set_grm(@handle, grammar)
|
||||
end
|
||||
|
||||
def accept_waveform(data)
|
||||
res = C.vosk_recognizer_accept_waveform(@handle, data, data.bytesize)
|
||||
raise Error, "Failed to process waveform" if res < 0
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
def result
|
||||
C.vosk_recognizer_result(@handle)
|
||||
end
|
||||
|
||||
def partial_result
|
||||
C.vosk_recognizer_partial_result(@handle)
|
||||
end
|
||||
|
||||
def final_result
|
||||
C.vosk_recognizer_final_result(@handle)
|
||||
end
|
||||
|
||||
def reset
|
||||
C.vosk_recognizer_reset(@handle)
|
||||
end
|
||||
|
||||
def srt_result(stream, words_per_line: 7)
|
||||
results = []
|
||||
while (data = stream.read(4000))
|
||||
results.push(result) if accept_waveform(data).nonzero?
|
||||
end
|
||||
results.push(final_result)
|
||||
|
||||
create_srt(results, words_per_line)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_srt(results, words_per_line)
|
||||
srt = SRT::File.new
|
||||
results.each do |res|
|
||||
jres = JSON.parse(res)
|
||||
next unless jres.key?("result")
|
||||
|
||||
jres["result"].each_slice(words_per_line) do |line|
|
||||
sub = SRT::Line.new
|
||||
sub.sequence = srt.lines.length + 1
|
||||
sub.start_time = line.first["start"]
|
||||
sub.end_time = line.last["end"]
|
||||
sub.text = line.map { |w| w["word"] }.join(" ")
|
||||
srt.lines.push(sub)
|
||||
end
|
||||
end
|
||||
srt.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# Batch model object
|
||||
class BatchModel
|
||||
attr_reader :handle
|
||||
|
||||
# Python version accepts additional ignored args for some reason
|
||||
def initialize(model_path)
|
||||
@handle = C.vosk_batch_model_new(model_path)
|
||||
end
|
||||
|
||||
def wait
|
||||
C.vosk_batch_model_wait(@handle)
|
||||
end
|
||||
end
|
||||
|
||||
# Batch recognizer object
|
||||
class BatchRecognizer
|
||||
# Python version accepts *args, but I don't just use regular arguments
|
||||
def initialize(batch_model, sample_rate)
|
||||
@handle = C.vosk_batch_recognizer_new(batch_model.handle, sample_rate.to_f)
|
||||
end
|
||||
|
||||
def accept_waveform(data)
|
||||
C.vosk_batch_recognizer_accept_waveform(@handle, data, data.bytesize)
|
||||
end
|
||||
|
||||
def result
|
||||
res = C.vosk_batch_recognizer_front_result(@handle)
|
||||
C.vosk_batch_recognizer_pop(@handle)
|
||||
res
|
||||
end
|
||||
|
||||
def finish_stream
|
||||
C.vosk_batch_recognizer_finish_stream(@handle)
|
||||
end
|
||||
|
||||
def pending_chunks
|
||||
C.vosk_batch_recognizer_get_pending_chunks(@handle)
|
||||
end
|
||||
end
|
||||
|
||||
# Inverse text normalization
|
||||
class Processor
|
||||
# Python version accepts *args, but I don't just use regular arguments
|
||||
def initialize(lang, type)
|
||||
@handle = C.vosk_text_processor_new(lang, type)
|
||||
end
|
||||
|
||||
def process(text)
|
||||
C.vosk_text_processor_itn(@handle, text)
|
||||
end
|
||||
end
|
||||
|
||||
def self.log_level=(level)
|
||||
C.vosk_set_log_level(level)
|
||||
end
|
||||
|
||||
def self.gpu_init
|
||||
C.vosk_gpu_init
|
||||
end
|
||||
|
||||
def self.gpu_thread_init
|
||||
C.vosk_gpu_thread_init
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,180 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "ffi"
|
||||
|
||||
module Vosk
|
||||
module C # :nodoc: all
|
||||
extend FFI::Library
|
||||
|
||||
# FIXME: Load same way as in Python, test on Windows
|
||||
# This second option, 'vosk', allows system-wide installed library to be loaded.
|
||||
# I see you search /usr/share/vosk, so I guess it's supported somehow.
|
||||
# It'll allow the gem to be used on systems not supported in pre-compiled releases.
|
||||
# (in fact, we only need the first in a pre-compiled release and the second otherwise,
|
||||
# but not worth the effort to put more configuration in the build stage - not possible without hacks)
|
||||
# But when we load a lib not shipped with the gem itself, we (might) need to ensure it's a compatible version
|
||||
# Note: options in the array are alternatives, only the first found is loaded
|
||||
# Note: probably needs RubyInstaller::Runtime.add_dll_directory on Windows - if FFI::Platform.windows?
|
||||
ffi_lib [File.join(__dir__, FFI.map_library_name("vosk")), "vosk"]
|
||||
|
||||
class VoskModel < FFI::AutoPointer
|
||||
def self.from_native(ptr, _ctx)
|
||||
raise Error, "Failed to create a model" if ptr.null?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def self.release(ptr)
|
||||
C.vosk_model_free(ptr)
|
||||
end
|
||||
end
|
||||
|
||||
class VoskSpkModel < FFI::AutoPointer
|
||||
def self.from_native(ptr, _ctx)
|
||||
raise Error, "Failed to create a speaker model" if ptr.null?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def self.release(ptr)
|
||||
C.vosk_spk_model_free(ptr)
|
||||
end
|
||||
end
|
||||
|
||||
VoskEndpointerMode = enum(
|
||||
:VoskEndpointerMode,
|
||||
[
|
||||
:default, 0,
|
||||
:short, 1,
|
||||
:long, 2,
|
||||
:very_long, 3,
|
||||
],
|
||||
)
|
||||
|
||||
class VoskRecognizer < FFI::AutoPointer
|
||||
def self.from_native(ptr, _ctx)
|
||||
raise Error, "Failed to create a recognizer" if ptr.null?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def self.release(ptr)
|
||||
C.vosk_recognizer_free(ptr)
|
||||
end
|
||||
end
|
||||
|
||||
class VoskBatchModel < FFI::AutoPointer
|
||||
def self.from_native(ptr, _ctx)
|
||||
raise Error, "Failed to create a model" if ptr.null?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def self.release(ptr)
|
||||
C.vosk_batch_model_free(ptr)
|
||||
end
|
||||
end
|
||||
|
||||
class VoskBatchRecognizer < FFI::AutoPointer
|
||||
def self.from_native(ptr, _ctx)
|
||||
raise Error, "Failed to create a recognizer" if ptr.null?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def self.release(ptr)
|
||||
C.vosk_batch_recognizer_free(ptr)
|
||||
end
|
||||
end
|
||||
|
||||
class VoskTextProcessor < FFI::AutoPointer
|
||||
def self.from_native(ptr, _ctx)
|
||||
raise Error, "Failed to create processor" if ptr.null?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def self.release(ptr)
|
||||
C.vosk_text_processor_free(ptr)
|
||||
end
|
||||
end
|
||||
|
||||
attach_function :vosk_model_new, [:string], VoskModel
|
||||
attach_function :vosk_model_free, [VoskModel], :void
|
||||
attach_function :vosk_model_find_word, [VoskModel, :string], :int
|
||||
|
||||
attach_function :vosk_spk_model_new, [:string], VoskSpkModel
|
||||
attach_function :vosk_spk_model_free, [VoskSpkModel], :void
|
||||
|
||||
attach_function :vosk_recognizer_new, [VoskModel, :float], VoskRecognizer
|
||||
attach_function :vosk_recognizer_new_spk, [VoskModel, :float, VoskSpkModel], VoskRecognizer
|
||||
attach_function :vosk_recognizer_new_grm, [VoskModel, :float, :string], VoskRecognizer
|
||||
attach_function :vosk_recognizer_set_spk_model, [VoskRecognizer, VoskSpkModel], :void
|
||||
attach_function :vosk_recognizer_set_grm, [VoskRecognizer, :string], :void
|
||||
attach_function :vosk_recognizer_set_max_alternatives, [VoskRecognizer, :int], :void
|
||||
attach_function :vosk_recognizer_set_words, [VoskRecognizer, :int], :void
|
||||
attach_function :vosk_recognizer_set_partial_words, [VoskRecognizer, :int], :void
|
||||
attach_function :vosk_recognizer_set_nlsml, [VoskRecognizer, :int], :void
|
||||
# TODO: remove this, it was needed only because I used libvosk from Python package
|
||||
if Gem::Version.new(VERSION) >= Gem::Version.new("0.3.46")
|
||||
attach_function :vosk_recognizer_set_endpointer_mode, [VoskRecognizer, VoskEndpointerMode], :void
|
||||
attach_function :vosk_recognizer_set_endpointer_delays, [VoskRecognizer, :float, :float, :float], :void
|
||||
end
|
||||
attach_function :vosk_recognizer_accept_waveform, [VoskRecognizer, :buffer_in, :int], :int
|
||||
# vosk_recognizer_accept_waveform_s; vosk_recognizer_accept_waveform_f - skipped
|
||||
attach_function :vosk_recognizer_result, [VoskRecognizer], :string
|
||||
attach_function :vosk_recognizer_partial_result, [VoskRecognizer], :string
|
||||
attach_function :vosk_recognizer_final_result, [VoskRecognizer], :string
|
||||
attach_function :vosk_recognizer_reset, [VoskRecognizer], :void
|
||||
attach_function :vosk_recognizer_free, [VoskRecognizer], :void
|
||||
|
||||
attach_function :vosk_set_log_level, [:int], :void
|
||||
attach_function :vosk_gpu_init, [], :void
|
||||
attach_function :vosk_gpu_thread_init, [], :void
|
||||
|
||||
attach_function :vosk_batch_model_new, [:string], VoskBatchModel
|
||||
attach_function :vosk_batch_model_free, [VoskBatchModel], :void
|
||||
attach_function :vosk_batch_model_wait, [VoskBatchModel], :void
|
||||
|
||||
attach_function :vosk_batch_recognizer_new, [VoskBatchModel, :float], VoskBatchRecognizer
|
||||
attach_function :vosk_batch_recognizer_free, [VoskBatchRecognizer], :void
|
||||
attach_function :vosk_batch_recognizer_accept_waveform, [VoskBatchRecognizer, :buffer_in, :int], :void
|
||||
# vosk_batch_recognizer_set_nlsml - skipped
|
||||
attach_function :vosk_batch_recognizer_finish_stream, [VoskBatchRecognizer], :void
|
||||
attach_function :vosk_batch_recognizer_front_result, [VoskBatchRecognizer], :string
|
||||
attach_function :vosk_batch_recognizer_pop, [VoskBatchRecognizer], :void
|
||||
attach_function :vosk_batch_recognizer_get_pending_chunks, [VoskBatchRecognizer], :int
|
||||
|
||||
# https://github.com/ffi/ffi/issues/467
|
||||
class OwnedString
|
||||
extend FFI::DataConverter
|
||||
|
||||
native_type :strptr
|
||||
|
||||
def self.to_native(_value, _context)
|
||||
raise TypeError, "owned_string can't be used for input"
|
||||
end
|
||||
|
||||
def self.from_native((str, ptr), _context)
|
||||
C.free(ptr)
|
||||
str
|
||||
end
|
||||
end
|
||||
|
||||
typedef OwnedString, :owned_string
|
||||
attach_function :free, [:pointer], :void
|
||||
|
||||
# TODO: remove this, it was needed only because I used libvosk from Python package
|
||||
if Gem::Version.new(VERSION) >= Gem::Version.new("0.3.48")
|
||||
attach_function :vosk_text_processor_new, [:string, :string], VoskTextProcessor
|
||||
attach_function :vosk_text_processor_free, [VoskTextProcessor], :void
|
||||
# NOTE: you have a memory leak here in your python version, needs to be
|
||||
# ptr = _c.vosk_text_processor_itn(self._handle, text.encode('utf-8'))
|
||||
# str = _ffi.string(ptr).decode('utf-8')
|
||||
# _ffi.gc(ptr, _c.free) # or call libc free directly
|
||||
attach_function :vosk_text_processor_itn, [VoskTextProcessor, :string], :owned_string
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :C
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "progressbar"
|
||||
require "bytesize"
|
||||
|
||||
# Extends progressbar with tqdm-like output:
|
||||
# %s / %z → human-readable byte sizes: current size / total siZe
|
||||
# %d / %o → compact time: elapsed Duration / Outstanding remaining
|
||||
# %r → rate_scale lambda may return a ByteSize string
|
||||
|
||||
# Override rate_of_change so rate_scale can return a string.
|
||||
# The default format '%i' coerces the result to integer, breaking string values.
|
||||
ProgressBar::Components::Rate.prepend(Module.new do
|
||||
def rate_of_change(format_string = "%s")
|
||||
return "0" if elapsed_seconds <= 0
|
||||
|
||||
format_string % scaled_rate
|
||||
end
|
||||
end)
|
||||
|
||||
class ProgressBar # :nodoc: all
|
||||
# Add ByteSize-formatted progress methods to the Progress component.
|
||||
class Progress
|
||||
def progress_with_precision
|
||||
ByteSize.new(progress).to_s
|
||||
end
|
||||
|
||||
def total_with_unknown_indicator_with_precision
|
||||
total ? ByteSize.new(total).to_s : total_with_unknown_indicator
|
||||
end
|
||||
end
|
||||
|
||||
module Components
|
||||
# Add label-free time methods to the Time component.
|
||||
class Time
|
||||
def elapsed_no_label
|
||||
val = elapsed
|
||||
val.start_with?("00:") ? val[3..-1] : val
|
||||
end
|
||||
|
||||
def estimated_no_label
|
||||
val = estimated_with_friendly_oob.split(": ", 2).last
|
||||
val.start_with?("00:") ? val[3..-1] : val
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Extend MOLECULES with new non-overlapping keys — originals are untouched.
|
||||
# remove_const avoids the "already initialized constant" warning.
|
||||
molecules = ProgressBar::Format::Molecule::MOLECULES
|
||||
ProgressBar::Format::Molecule.send(:remove_const, :MOLECULES)
|
||||
ProgressBar::Format::Molecule::MOLECULES = molecules.merge(
|
||||
s: %i[progressable progress_with_precision],
|
||||
z: %i[progressable total_with_unknown_indicator_with_precision],
|
||||
d: %i[time_component elapsed_no_label],
|
||||
o: %i[time_component estimated_no_label],
|
||||
).freeze
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vosk
|
||||
VERSION = "0.3.45"
|
||||
end
|
||||
Reference in New Issue
Block a user