Module: Cohere::Transcribe::CLI

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

Overview

Command-line adapter for the dependency-light public API.

Defined Under Namespace

Classes: EarlyExit, ParsedCommand

Constant Summary collapse

DEFAULT_MODEL_ID =
"CohereLabs/cohere-transcribe-arabic-07-2026"
OUTPUT_FORMATS =
%w[txt srt vtt json].freeze
OUTPUT_PATH_DISPLAY_LIMIT =
20

Class Method Summary collapse

Class Method Details

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

Console-entry alias matching the reference package's outer entry point.



158
159
160
# File 'lib/cohere/transcribe/cli.rb', line 158

def cli(argv = ARGV, out: $stdout, err: $stderr, transcriber: nil)
  main(argv, out: out, err: err, transcriber: transcriber)
end

.default_valuesObject



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/cohere/transcribe/cli.rb', line 350

def default_values
  {
    model: DEFAULT_MODEL_ID,
    model_revision: nil,
    adapter: nil,
    adapter_revision: nil,
    language: "ar",
    formats: nil,
    output_dir: nil,
    recursive: true,
    existing: "error",
    device: "auto",
    dtype: "auto",
    audio_backend: "auto",
    audio_memory_gb: 4.0,
    preprocess_workers: nil,
    pipeline_preparation: true,
    vad: "silero",
    vad_engine: "auto",
    vad_batch_size: 16,
    vad_block_frames: 512,
    vad_threads: nil,
    vad_merge: false,
    min_dur: 0.5,
    max_dur: 30.0,
    max_silence: 0.6,
    energy_threshold: 50.0,
    vad_threshold: 0.5,
    min_silence_ms: 300,
    speech_pad_ms: 60,
    batch_size: nil,
    batch_max_size: nil,
    batch_audio_seconds: nil,
    batch_vram_target: 0.9,
    adaptive_batch: false,
    pin_memory: false,
    max_new_tokens: 445,
    max_retry_tokens: 896,
    truncation_policy: "retry",
    stop_repetition_loops: true,
    alignment: "segment",
    text_only: false,
    align_batch_size: 4,
    align_dtype: "fp32",
    max_chars: 80,
    max_cue_dur: 6.0,
    max_gap: 0.6,
    profile_json: nil
  }
end

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



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
129
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
# File 'lib/cohere/transcribe/cli.rb', line 91

def main(argv = ARGV, out: $stdout, err: $stderr, transcriber: nil)
  command = parse_args(argv, out: out)
  load_public_api unless transcriber
  unless transcriber || Cohere::Transcribe.respond_to?(:transcribe)
    raise TranscriptionRuntimeError, "The public transcription runtime is unavailable"
  end

  callable = transcriber || Cohere::Transcribe.method(:transcribe)

  out.puts("\n[1/4] Validating inputs and outputs")
  stage_two_printed = false
  print_stage_two = lambda do
    next if stage_two_printed

    out.puts("\n[2/4] Loading ASR + preparing audio")
    stage_two_printed = true
  end
  run = callable.call(
    command.audio,
    options: command.options,
    progress: progress_reporter(out, before_first: print_stage_two),
    raise_on_error: false
  )
  if all_skipped?(run)
    run.skipped.each do |result|
      out.puts("    skipping #{result.path}: verified output generation is complete")
    end
    out.puts("    All inputs were skipped; no model was loaded.")
    run.errors.each { |error| out.puts("    [error] #{error}") }
    command.options.publication.profile_json&.then { |path| out.puts("    #{path}") }
    return run.ok? ? 0 : 1
  end

  print_stage_two.call
  out.puts(
    command.options.alignment == "word" ? "\n[3/4] Forced alignment + transactional outputs" : "\n[3/4] Transactional outputs"
  )
  out.puts("\n[4/4] Summary")
  print_summary(run, out: out)
  command.options.publication.profile_json&.then { |path| out.puts("    #{path}") }
  run.ok? ? 0 : 1
rescue EarlyExit => e
  e.status
rescue OptionParser::ParseError => e
  err.puts(option_parser(default_values, {}, out: out))
  err.puts("cohere-transcribe: error: #{e.message}")
  2
rescue TranscriptionError => e
  err.puts(e.message)
  1
rescue Interrupt
  out.puts(
    "\n    Interrupted; the active output commit was rolled back. " \
    "Files completed earlier remain published."
  )
  130
rescue SignalException => e
  raise unless e.signm == "SIGTERM"

  out.puts(
    "\n    Termination requested; active work was cancelled and " \
    "completed files remain published."
  )
  143
end

.option_parser(values, state, out:) ⇒ Object



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
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
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/cohere/transcribe/cli.rb', line 162

def option_parser(values, state, out:)
  OptionParser.new do |parser|
    parser.banner = "Usage: cohere-transcribe [options] audio [audio ...]"
    parser.separator("")
    parser.separator("Batch Arabic/English transcription with optional timestamp alignment.")
    parser.separator("")

    parser.on("--model MODEL", "Hugging Face repository or local native Cohere ASR directory.") do |value|
      values[:model] = value
    end
    parser.on("--model-revision REVISION", "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", "Hub adapter commit, tag, or branch.") do |value|
      values[:adapter_revision] = value
    end
    parser.on("--language LANGUAGE", String, %w[ar en], "Spoken language tag (ar or en).") do |value|
      values[:language] = value
    end
    parser.on("--formats FORMAT [FORMAT ...]", String, "Outputs to write: txt, srt, vtt, json.") do |encoded|
      value = encoded.split(FORMAT_ARGUMENT_SEPARATOR, -1)
      unsupported = value - OUTPUT_FORMATS
      unless unsupported.empty?
        raise OptionParser::InvalidArgument,
              "unsupported output format(s): #{unsupported.sort.join(", ")}"
      end
      values[:formats] = value
    end
    parser.on("--output-dir DIRECTORY", "Output root; preserve directory-relative structure.") do |value|
      values[:output_dir] = value
    end
    parser.on("--recursive", "Recurse into input directories (default).") { values[:recursive] = true }
    parser.on("--no-recursive", "Do not recurse into input directories.") { values[:recursive] = false }
    parser.on("--existing POLICY", String, %w[error overwrite skip], "Existing outputs policy.") do |value|
      values[:existing] = value
    end
    parser.on("--device DEVICE", String, %w[auto mps cuda cpu], "Inference device.") do |value|
      values[:device] = value
    end
    parser.on("--dtype DTYPE", String, %w[auto bf16 fp16 fp32], "ASR model precision.") do |value|
      values[:dtype] = value
    end
    parser.on(
      "--audio-backend BACKEND",
      String,
      %w[auto torchcodec ffmpeg librosa],
      "Audio decoder configuration."
    ) { |value| values[:audio_backend] = value }
    parser.on("--audio-memory-gb GIB", String, "Decoded-PCM memory limit per file/group.") do |value|
      values[:audio_memory_gb] = parse_reference_float(value)
    end
    parser.on("--preprocess-workers COUNT", String, "Concurrent audio decode workers.") do |value|
      values[:preprocess_workers] = parse_reference_integer(value)
    end
    parser.on("--pipeline-preparation", "Overlap bounded preparation with ASR (default).") do
      values[:pipeline_preparation] = true
    end
    parser.on("--no-pipeline-preparation", "Disable preparation/ASR overlap.") do
      values[:pipeline_preparation] = false
    end

    parser.separator("")
    parser.separator("Segmentation (VAD):")
    parser.on("--vad MODE", String, %w[silero auditok none], "Segmentation policy.") do |value|
      values[:vad] = value
    end
    parser.on("--vad-engine ENGINE", String, %w[auto torch onnx jit], "Silero runtime.") do |value|
      values[:vad_engine] = value
    end
    parser.on("--vad-batch-size COUNT", String, "Maximum files per packed VAD call.") do |value|
      values[:vad_batch_size] = parse_reference_integer(value)
    end
    parser.on("--vad-block-frames COUNT", String, "Maximum frames per packed VAD file.") do |value|
      values[:vad_block_frames] = parse_reference_integer(value)
    end
    parser.on("--vad-threads COUNT", String, "Packed VAD CPU thread count.") do |value|
      values[:vad_threads] = parse_reference_integer(value)
    end
    parser.on("--vad-merge", "Merge adjacent Silero speech spans.") { values[:vad_merge] = true }
    parser.on("--no-vad-merge", "Do not merge adjacent Silero spans (default).") do
      values[:vad_merge] = false
    end
    parser.on("--min-dur SECONDS", String, "Minimum speech duration.") do |value|
      values[:min_dur] = parse_reference_float(value)
    end
    parser.on("--max-dur SECONDS", String, "Maximum segment/window duration.") do |value|
      values[:max_dur] = parse_reference_float(value)
    end
    parser.on("--max-silence SECONDS", String, "Maximum Auditok internal silence.") do |value|
      values[:max_silence] = parse_reference_float(value)
    end
    parser.on("--energy-threshold DB", String, "Auditok energy threshold.") do |value|
      values[:energy_threshold] = parse_reference_float(value)
    end
    parser.on("--vad-threshold PROBABILITY", String, "Silero speech threshold.") do |value|
      values[:vad_threshold] = parse_reference_float(value)
    end
    parser.on("--min-silence-ms MILLISECONDS", String, "Silero split silence.") do |value|
      values[:min_silence_ms] = parse_reference_integer(value)
    end
    parser.on("--speech-pad-ms MILLISECONDS", String, "Silero speech padding.") do |value|
      values[:speech_pad_ms] = parse_reference_integer(value)
    end

    parser.separator("")
    parser.separator("Transcription:")
    parser.on("--batch-size COUNT", String, "Initial ASR segment count.") do |value|
      values[:batch_size] = parse_reference_integer(value)
    end
    parser.on("--batch-max-size COUNT", String, "Adaptive batch upper row cap.") do |value|
      values[:batch_max_size] = parse_reference_integer(value)
    end
    parser.on("--batch-audio-seconds SECONDS", String, "Maximum padded audio per batch.") do |value|
      values[:batch_audio_seconds] = parse_reference_float(value)
    end
    parser.on("--batch-vram-target FRACTION", String, "Adaptive batching VRAM target.") do |value|
      values[:batch_vram_target] = parse_reference_float(value)
    end
    parser.on("--adaptive-batch", "Enable experimental adaptive batch growth.") do
      values[:adaptive_batch] = true
    end
    parser.on("--no-adaptive-batch", "Disable adaptive growth (default).") do
      values[:adaptive_batch] = false
    end
    parser.on("--pin-memory", "Compatibility flag; not applicable to native ggml.") do
      values[:pin_memory] = true
    end
    parser.on("--no-pin-memory", "Leave native ggml host buffers unpinned (default).") do
      values[:pin_memory] = false
    end
    parser.on("--max-new-tokens COUNT", String, "Initial decoder token limit.") do |value|
      values[:max_new_tokens] = parse_reference_integer(value)
    end
    parser.on("--max-retry-tokens COUNT", String, "Automatic retry token limit.") do |value|
      values[:max_retry_tokens] = parse_reference_integer(value)
    end
    parser.on("--truncation-policy POLICY", String, %w[retry warn], "Token-limit behavior.") do |value|
      values[:truncation_policy] = value
    end
    parser.on("--stop-repetition-loops", "Stop conservative decoder repetition loops (default).") do
      values[:stop_repetition_loops] = true
    end
    parser.on("--no-stop-repetition-loops", "Disable the repetition-loop guard.") do
      values[:stop_repetition_loops] = false
    end

    parser.separator("")
    parser.separator("Alignment and subtitle cues:")
    parser.on("--alignment MODE", String, %w[word segment none], "Timestamp mode.") do |value|
      state[:alignment] = true
      values[:alignment] = value
    end
    parser.on("--text-only", "Alias for --alignment none.") do
      state[:text_only] = true
      values[:text_only] = true
    end
    parser.on("--align-batch-size COUNT", String, "Maximum alignment windows per batch.") do |value|
      values[:align_batch_size] = parse_reference_integer(value)
    end
    parser.on("--align-dtype DTYPE", String, %w[fp32 fp16], "Alignment precision.") do |value|
      values[:align_dtype] = value
    end
    parser.on("--max-chars COUNT", String, "Target subtitle cue length.") do |value|
      values[:max_chars] = parse_reference_integer(value)
    end
    parser.on("--max-cue-dur SECONDS", String, "Target subtitle cue duration.") do |value|
      values[:max_cue_dur] = parse_reference_float(value)
    end
    parser.on("--max-gap SECONDS", String, "Maximum inter-word cue gap.") do |value|
      values[:max_gap] = parse_reference_float(value)
    end
    parser.on("--profile-json PATH", "Write performance telemetry as JSON.") do |value|
      values[:profile_json] = value
    end
    parser.on("--version", "Show version and exit.") do
      out.puts("cohere-transcribe #{VERSION}")
      raise EarlyExit, 0
    end
    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



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/cohere/transcribe/cli.rb', line 53

def parse_args(argv = ARGV, out: $stdout)
  values = default_values
  state = { alignment: false, text_only: false }
  raw_arguments = Array(argv).dup
  preflight_mutually_exclusive_options!(raw_arguments)
  preflight_reference_numeric_tokens!(raw_arguments)
  preflight_ambiguous_long_options!(raw_arguments)
  parser_arguments = defer_unknown_options_before_early_exit(raw_arguments)
  arguments = normalize_formats_arguments(parser_arguments)
  parser = option_parser(values, state, out: out)
  parser.parse!(arguments)
  enforce_reference_positional_order!(raw_arguments)
  raise OptionParser::MissingArgument, "audio" if arguments.empty?
  raise OptionParser::InvalidOption, "--text-only conflicts with --alignment" if state.values_at(:alignment, :text_only).all?

  load_configuration_contract
  values[:alignment] = "none" if values[:text_only]
  text_mode = values[:alignment] == "none"
  formats = values.delete(:formats)
  formats = text_mode ? ["txt"] : %w[txt srt vtt] if formats.nil?
  formats = formats.uniq
  raise TranscriptionConfigurationError, "Plain-text mode supports only --formats txt" if text_mode && formats != ["txt"]

  publication = PublicationOptions.new(
    formats: formats,
    output_dir: values.delete(:output_dir),
    existing: values.delete(:existing),
    profile_json: values.delete(:profile_json)
  )
  options = TranscriptionOptions.new(**values, publication: publication)
  Configuration.validate!(options)
  ParsedCommand.new(audio: arguments.map { |value| value.dup.freeze }.freeze, options: options)
rescue ArgumentError => e
  raise if defined?(TranscriptionError) && e.is_a?(TranscriptionError)

  raise OptionParser::InvalidArgument, e.message
end