Module: Cohere::Transcribe::Doctor

Defined in:
lib/cohere/transcribe/doctor.rb

Overview

Installation and model-metadata diagnostics that never load ASR weights.

Defined Under Namespace

Classes: EarlyExit, Options, Results

Constant Summary collapse

DEFAULT_MODEL_ID =
"CohereLabs/cohere-transcribe-arabic-07-2026"
EXPECTED_ONNX_SHA256 =
"914fd98ac0a73d69ba1e70c9b1d66acb740eff90500dfde08b89a961b168a6a9"
COHERE_PROCESSOR_CLASS =
"CohereAsrProcessor"
COHERE_FEATURE_EXTRACTOR_CLASS =
"CohereAsrFeatureExtractor"
DEFAULT_MAX_AUDIO_CLIP_SECONDS =
35.0
REQUIRED_PROMPT_TOKENS =
%w[
   <|startofcontext|> <|startoftranscript|> <|emo:undefined|>
  <|ar|> <|en|> <|pnc|> <|noitn|> <|notimestamp|> <|nodiarize|>
  <|endoftext|>
].freeze

Class Method Summary collapse

Class Method Details

.import_required(results, library, feature) ⇒ Object



427
428
429
430
431
432
433
434
435
436
437
# File 'lib/cohere/transcribe/doctor.rb', line 427

def import_required(results, library, feature)
  require library
  gem_name = { "numo/narray" => "numo-narray" }.fetch(library, library.split("/").first)
  version = Gem.loaded_specs[gem_name]&.version
  suffix = version ? " #{version}" : ""
  results.ok("#{feature}: #{library}#{suffix}")
  true
rescue LoadError => e
  results.fail("#{feature}: cannot require #{library.inspect}: #{e.message}")
  false
end

.main(argv = ARGV, out: $stdout, err: $stderr, checks: nil) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/cohere/transcribe/doctor.rb', line 89

def main(argv = ARGV, out: $stdout, err: $stderr, checks: nil)
  options = parse_args(argv, out: out)
  checker = checks || self
  results = Results.new(out: out)
  checker.validate_files(results)
  checker.validate_common_runtime(results)
  checker.validate_silero(results)
  checker.validate_word_alignment(results) if options.mode == "word"
  checker.report_optional_runtime(results, options.audio_backend)

  selected_model = options.model != DEFAULT_MODEL_ID ||
                   !options.model_revision.nil? ||
                   !options.adapter.nil? ||
                   !options.adapter_revision.nil?
  if options.model_access || selected_model
    checker.validate_model_access(
      results,
      include_aligner: options.mode == "word",
      model_id: options.model,
      model_revision: options.model_revision,
      adapter_id: options.adapter,
      adapter_revision: options.adapter_revision
    )
  end

  out.puts
  if results.failures.positive?
    out.puts("Validation failed: #{results.failures} failure(s), #{results.warnings} warning(s).")
    1
  else
    out.puts("Validation passed for #{options.mode} mode with #{results.warnings} warning(s).")
    0
  end
rescue EarlyExit => e
  e.status
rescue OptionParser::ParseError => e
  err.puts(option_parser({}, out: out))
  err.puts("cohere-transcribe-doctor: error: #{e.message}")
  2
end

.option_parser(values, out:) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/cohere/transcribe/doctor.rb', line 130

def option_parser(values, out:)
  OptionParser.new do |parser|
    # OptionParser installs an implicit --version switch that aborts when
    # no program version is configured.  The reference doctor does not
    # expose that switch, so remove it and let normal unknown-option
    # handling report the command-line error.
    parser.base.long.delete("version")
    parser.banner = "Usage: cohere-transcribe-doctor [options]"
    parser.separator("")
    parser.separator("Validate the transcription package without loading model weights.")
    parser.separator("")
    parser.on("--mode MODE", String, %w[word segment], "Output mode to validate (default: segment).") do |value|
      values[:mode] = value
    end
    parser.on("--model-access", "Resolve and validate selected model metadata.") do
      values[:model_access] = true
    end
    parser.on("--model MODEL", "Hub repository or local model directory; implies model access.") do |value|
      values[:model] = value
    end
    parser.on("--model-revision REVISION", "Optional Hub model commit, tag, or branch.") do |value|
      values[:model_revision] = value
    end
    parser.on("--adapter ADAPTER", "Optional Hub repository or local LoRA adapter directory.") do |value|
      values[:adapter] = value
    end
    parser.on("--adapter-revision REVISION", "Optional Hub adapter commit, tag, or branch.") do |value|
      values[:adapter_revision] = value
    end
    parser.on(
      "--audio-backend BACKEND",
      String,
      %w[auto torchcodec ffmpeg librosa],
      "Decoder configuration to validate (default: auto)."
    ) { |value| values[:audio_backend] = value }
    parser.on_tail("-h", "--help", "Show this help and exit.") do
      out.puts(parser)
      raise EarlyExit, 0
    end
  end
end

.parse_args(argv = ARGV, out: $stdout) ⇒ Object

Raises:

  • (OptionParser::InvalidArgument)


69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/cohere/transcribe/doctor.rb', line 69

def parse_args(argv = ARGV, out: $stdout)
  values = {
    mode: "segment",
    model_access: false,
    model: DEFAULT_MODEL_ID,
    model_revision: nil,
    adapter: nil,
    adapter_revision: nil,
    audio_backend: "auto"
  }
  raw_arguments = Array(argv).dup
  preflight_ambiguous_long_options!(raw_arguments)
  arguments = defer_unknown_options_before_early_exit(raw_arguments)
  parser = option_parser(values, out: out)
  parser.parse!(arguments)
  raise OptionParser::InvalidArgument, "unexpected positional argument: #{arguments.first}" unless arguments.empty?

  Options.new(**values)
end

.report_native_device_capabilities(results, library) ⇒ Object



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/cohere/transcribe/doctor.rb', line 289

def report_native_device_capabilities(results, library)
  available = ["cpu"]
  %w[cuda mps].each do |device|
    available << device if library.resolve_device(device) == device
  rescue TranscriptionRuntimeError
    next
  end
  resolved = library.resolve_device("auto")
  results.ok("native inference devices: #{available.join(", ")}; auto resolves to #{resolved}")
  case resolved
  when "cuda"
    bf16 = library.supports_bf16?(resolved) ? "supported" : "not supported"
    results.ok("accelerator: CUDA available through native runtime; BF16 operations #{bf16}")
  when "mps"
    bf16 = library.supports_bf16?(resolved) ? "supported" : "not supported"
    results.ok("accelerator: Apple Metal available through native runtime; BF16 operations #{bf16}")
  when "cpu"
    results.warn("accelerator: CPU only; the 2B model will be substantially slower")
  else
    results.fail("native runtime returned an unsupported automatic device: #{resolved.inspect}")
  end
end

.report_optional_runtime(results, audio_backend) ⇒ Object



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/cohere/transcribe/doctor.rb', line 354

def report_optional_runtime(results, audio_backend)
  case audio_backend
  when "auto", "ffmpeg", "torchcodec", "librosa"
    require_relative "audio/decoder"
    if Audio::FFmpegNative.available?
      results.ok("native FFmpeg decoder: #{Audio::FFmpegNative.diagnostic}")
      if %w[torchcodec librosa].include?(audio_backend)
        results.warn("#{audio_backend} compatibility mode uses FFmpeg through the native C ABI")
      end
    else
      sound_file = Audio.const_get(:SoundFileABI, false)
      if %w[auto librosa].include?(audio_backend) && sound_file.const_get(:AVAILABLE)
        label = audio_backend == "librosa" ? "librosa compatibility fallback" : "native audio decoder fallback"
        results.ok("#{label}: libsndfile ABI")
        results.warn("native FFmpeg decoder unavailable: #{Audio::FFmpegNative.diagnostic}")
      else
        results.fail("native FFmpeg decoder unavailable: #{Audio::FFmpegNative.diagnostic}")
      end
    end
  end
rescue LoadError, StandardError => e
  results.fail("audio decoder configuration: #{e.class}: #{e.message}")
end

.validate_common_runtime(results, native_library: nil) ⇒ Object



275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/cohere/transcribe/doctor.rb', line 275

def validate_common_runtime(results, native_library: nil)
  results.ok("Ruby #{RUBY_VERSION}")
  import_required(results, "numo/narray", "numeric runtime")
  import_required(results, "onnxruntime", "ONNX inference runtime")
  require_relative "constants"
  require_relative "errors"
  require_relative "asr/native"
  library = native_library || ASR::NativeLibrary.load
  results.ok("native Cohere ASR runtime: #{library.path}")
  report_native_device_capabilities(results, library)
rescue StandardError => e
  results.fail("native Cohere ASR runtime: #{e.class}: #{e.message}")
end

.validate_files(results) ⇒ Object



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/cohere/transcribe/doctor.rb', line 246

def validate_files(results)
  require "digest"

  specification = Gem.loaded_specs["cohere-transcribe"]
  if specification.nil?
    results.warn("package metadata is unavailable; running from a source checkout")
  elsif specification.version.to_s == VERSION
    results.ok("package metadata version: #{VERSION}")
  else
    results.fail("package metadata is #{specification.version}, runtime is #{VERSION}")
  end

  asset = File.expand_path("vad/silero_vad_v6.onnx", __dir__)
  unless File.file?(asset)
    results.fail("missing Silero ONNX asset: #{asset}")
    return
  end
  digest = Digest::SHA256.file(asset).hexdigest
  if digest == EXPECTED_ONNX_SHA256
    results.ok("Silero ONNX asset integrity: #{digest}")
  else
    results.fail(
      "Silero ONNX checksum mismatch: expected #{EXPECTED_ONNX_SHA256}, found #{digest}"
    )
  end
rescue SystemCallError => e
  results.fail("cannot validate packaged assets: #{e.class}: #{e.message}")
end

.validate_model_access(results, include_aligner:, model_id: DEFAULT_MODEL_ID, model_revision: nil, adapter_id: nil, adapter_revision: nil, hub: nil) ⇒ Object



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/cohere/transcribe/doctor.rb', line 378

def validate_model_access(
  results,
  include_aligner:,
  model_id: DEFAULT_MODEL_ID,
  model_revision: nil,
  adapter_id: nil,
  adapter_revision: nil,
  hub: nil
)
  require_relative "constants"
  require_relative "hub"
  require_relative "model_identity"

  hub ||= Hub.new
  identity = ModelIdentity.resolve(
    model_id,
    model_revision,
    adapter_id,
    adapter_revision,
    hub: hub
  )
  maximum = validate_asr_processor_metadata!(identity, hub)
  validate_aligner_metadata!(hub) if include_aligner
  model_reference = reference(identity.model_id, identity.model_revision)
  adapter = if identity.adapter_id
              ", adapter #{reference(identity.adapter_id, identity.adapter_revision)}"
            else
              ""
            end
  aligner = include_aligner ? " and pinned MMS ONNX aligner metadata" : ""
  results.ok(
    "ASR configuration #{model_reference} (#{identity.model_format})#{adapter}#{aligner} accessible; " \
    "#{COHERE_PROCESSOR_CLASS} one-row limit is #{maximum}s"
  )
  if identity.model_format.to_s != "dense"
    results.warn(
      "saved #{identity.model_format} checkpoint detected; quantized inference is not part of the core Ruby path"
    )
  end
  results.warn("PEFT/LoRA adapter inference is not part of the core Ruby Dense path") if identity.adapter_id
rescue Hub::AuthenticationError => e
  results.fail(
    "Cannot access the gated ASR model #{model_id}. Accept its terms, then set HF_TOKEN. " \
    "(#{e.class}: #{e.message})"
  )
rescue StandardError => e
  results.fail("ASR model or adapter validation: #{e.class}: #{e.message}")
end

.validate_silero(results) ⇒ Object



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/cohere/transcribe/doctor.rb', line 312

def validate_silero(results)
  require_relative "vad/silero"

  probabilities = VAD::Silero.new.speech_probabilities(Array.new(1024, 0.0))
  valid = probabilities.length == 2 && probabilities.all? do |probability|
    probability.is_a?(Numeric) && probability.finite? && probability.between?(0.0, 1.0)
  end
  if valid
    results.ok("Silero ONNX smoke: 2 finite probability frames")
  else
    results.fail("Silero ONNX smoke returned invalid probabilities: #{probabilities.inspect}")
  end
rescue LoadError, StandardError => e
  results.fail("Silero ONNX smoke: #{e.class}: #{e.message}")
end

.validate_word_alignment(results) ⇒ Object



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/cohere/transcribe/doctor.rb', line 328

def validate_word_alignment(results)
  require_relative "alignment/aligner"

  path = Alignment::CTC.forced_align(
    Numo::SFloat.cast([[4.0, 0.0], [0.0, 4.0]]),
    [1],
    blank: 0
  )
  raise "unexpected CTC path #{path.inspect}" unless path == [0, 1]

  tokens, = Alignment::Text.preprocess("مرحبا بكم في العالم", "ara")
  expected = ["<star>", "m r h b a", "<star>", "b k m", "<star>", "f y", "<star>", "a l ' a l m"]
  raise "unexpected Arabic romanization #{tokens.inspect}" unless tokens == expected

  fp32 = Alignment::ModelProvider::ARTIFACTS.fetch("fp32")
  results.ok(
    "word alignment: pure-Ruby MMS CTC Viterbi and Arabic romanization smokes execute"
  )
  results.ok(
    "word aligner: #{Alignment::ModelProvider::REPOSITORY}@#{Alignment::ModelProvider::REVISION}, " \
    "FP32 SHA-256 #{fp32.sha256}, bounded per-segment uniform fallback"
  )
rescue StandardError => e
  results.fail("word alignment: #{e.class}: #{e.message}")
end