Module: Cohere::Transcribe::Configuration

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

Constant Summary collapse

CHOICES =
{
  language: %w[ar en],
  device: %w[auto mps cuda cpu],
  dtype: %w[auto bf16 fp16 fp32],
  audio_backend: %w[auto torchcodec ffmpeg librosa],
  vad: %w[silero auditok none],
  vad_engine: %w[auto torch onnx jit],
  truncation_policy: %w[retry warn],
  alignment: %w[word segment none],
  align_dtype: %w[fp32 fp16]
}.freeze
INTEGER_OPTIONS =
%i[
  preprocess_workers vad_batch_size vad_block_frames vad_threads
  min_silence_ms speech_pad_ms batch_size batch_max_size max_new_tokens
  max_retry_tokens align_batch_size max_chars
].freeze
REAL_OPTIONS =
%i[
  audio_memory_gb min_dur max_dur max_silence energy_threshold vad_threshold
  batch_audio_seconds batch_vram_target max_cue_dur max_gap
].freeze
BOOLEAN_OPTIONS =
%i[
  text_only recursive pipeline_preparation vad_merge adaptive_batch pin_memory
  stop_repetition_loops
].freeze
MAX_TORCH_VAD_PADDED_FRAMES =
32_768
ASR_FIXED_MIN_SECONDS =
1.0

Class Method Summary collapse

Class Method Details

.finite?(value) ⇒ Boolean

Returns:

  • (Boolean)


272
273
274
# File 'lib/cohere/transcribe/configuration.rb', line 272

def finite?(value)
  value.is_a?(Numeric) && !value.is_a?(Complex) && value.respond_to?(:finite?) && value.finite?
end

.invalid!(message) ⇒ Object

Raises:

  • (error_class)


276
277
278
279
# File 'lib/cohere/transcribe/configuration.rb', line 276

def invalid!(message)
  error_class = defined?(TranscriptionConfigurationError) ? TranscriptionConfigurationError : ArgumentError
  raise error_class, message
end

.local_reference?(value, description:) ⇒ Boolean

Returns:

  • (Boolean)


214
215
216
217
218
219
220
221
222
223
# File 'lib/cohere/transcribe/configuration.rb', line 214

def local_reference?(value, description:)
  text = value.is_a?(String) ? value : value.to_path
  unless value.is_a?(String)
    local = ModelIdentity.resolve_local_directory(text, description: description)
    raise ArgumentError, "#{description} directory #{text.inspect} does not exist" unless local

    return true
  end
  !ModelIdentity.resolve_local_directory(text, description: description).nil?
end

.positive_finite!(value, option) ⇒ Object



268
269
270
# File 'lib/cohere/transcribe/configuration.rb', line 268

def positive_finite!(value, option)
  invalid!("#{option} must be finite and positive") unless finite?(value) && value.positive?
end

.positive_finite_if_set!(value, option) ⇒ Object



262
263
264
265
266
# File 'lib/cohere/transcribe/configuration.rb', line 262

def positive_finite_if_set!(value, option)
  return if value.nil?

  positive_finite!(value, option)
end

.positive_integer_if_set!(value, option) ⇒ Object



251
252
253
254
255
# File 'lib/cohere/transcribe/configuration.rb', line 251

def positive_integer_if_set!(value, option)
  return if value.nil? || value.positive?

  invalid!("#{option} must be positive")
end

.resolved(options, model_identity: nil) ⇒ Object



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
# File 'lib/cohere/transcribe/configuration.rb', line 168

def resolved(options, model_identity: nil)
  validate!(options)
  publication = options.publication
  text_mode = options.text_only || options.alignment == "none"
  if publication
    formats = publication.formats || (text_mode ? ["txt"] : %w[txt srt vtt])
    publication = publication.with(formats: formats.map(&:to_s).uniq.freeze)
  end
  changes = {
    alignment: text_mode ? "none" : options.alignment,
    # ggml consumes host float buffers through its native session ABI;
    # there is no PyTorch CPU tensor to page-lock or asynchronous H2D
    # transfer for this compatibility option to affect.
    pin_memory: false,
    publication: publication
  }
  if model_identity
    changes.merge!(
      model: model_identity.model_id,
      model_revision: model_identity.model_revision,
      adapter: model_identity.adapter_id,
      adapter_revision: model_identity.adapter_revision
    )
  end
  options.with(**changes)
end

.validate!(options) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
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
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
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
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/cohere/transcribe/configuration.rb', line 41

def validate!(options)
  unless defined?(TranscriptionOptions) && options.is_a?(TranscriptionOptions)
    raise TypeError, "options must be a TranscriptionOptions instance"
  end

  validate_model_reference!(options.model, option: "--model", description: "Model")
  validate_revision!(options.model_revision, option: "--model-revision")
  if local_reference?(options.model, description: "Model") && options.model_revision
    invalid!("--model-revision cannot be used with a local model directory")
  end
  validate_model_reference!(options.adapter, option: "--adapter", description: "Adapter") unless options.adapter.nil?
  invalid!("--adapter-revision requires --adapter") if options.adapter_revision && options.adapter.nil?
  validate_revision!(options.adapter_revision, option: "--adapter-revision")
  if !options.adapter.nil? &&
     local_reference?(options.adapter, description: "Adapter") && options.adapter_revision
    invalid!("--adapter-revision cannot be used with a local adapter directory")
  end

  BOOLEAN_OPTIONS.each do |name|
    value = options.public_send(name)
    invalid!("--#{name.to_s.tr("_", "-")} must be a boolean") unless [true, false].include?(value)
  end
  INTEGER_OPTIONS.each do |name|
    value = options.public_send(name)
    next if value.nil? || value.is_a?(Integer)

    invalid!("--#{name.to_s.tr("_", "-")} must be an integer")
  end
  REAL_OPTIONS.each do |name|
    value = options.public_send(name)
    next if value.nil? || (value.is_a?(Numeric) && !value.is_a?(Complex))

    invalid!("--#{name.to_s.tr("_", "-")} must be a real number")
  end
  CHOICES.each do |name, choices|
    value = options.public_send(name)
    next if value.is_a?(String) && choices.include?(value)

    invalid!("--#{name.to_s.tr("_", "-")} must be one of: #{choices.sort.join(", ")}")
  end

  invalid!("--text-only conflicts with --alignment word") if options.text_only && options.alignment == "word"
  publication = options.publication
  if !publication.nil? && (!defined?(PublicationOptions) || !publication.is_a?(PublicationOptions))
    invalid!("publication must be a PublicationOptions instance or nil")
  end
  formats = publication&.formats&.map(&:to_s)
  text_mode = options.text_only || options.alignment == "none"
  invalid!("Plain-text mode supports only --formats txt") if text_mode && formats && formats.uniq != ["txt"]

  positive_finite!(options.audio_memory_gb, "--audio-memory-gb")
  positive_integer_if_set!(options.preprocess_workers, "--preprocess-workers")

  packed_vad = options.vad == "silero" && %w[auto torch].include?(options.vad_engine)
  if packed_vad
    unless positive_integer?(options.vad_batch_size) && positive_integer?(options.vad_block_frames)
      invalid!("--vad-batch-size and --vad-block-frames must be positive")
    end
    if options.vad_batch_size * options.vad_block_frames > MAX_TORCH_VAD_PADDED_FRAMES
      invalid!(
        "--vad-batch-size * --vad-block-frames must not exceed " \
        "#{format("%<frames>d", frames: MAX_TORCH_VAD_PADDED_FRAMES)} frames"
      )
    end
  end
  positive_integer_if_set!(options.vad_threads, "--vad-threads")
  invalid!("--vad-threads applies only to packed Torch Silero VAD") if options.vad_threads && !packed_vad

  positive_integer_if_set!(options.batch_size, "--batch-size")
  positive_integer_if_set!(options.batch_max_size, "--batch-max-size")
  if options.batch_size && options.batch_max_size && options.batch_max_size < options.batch_size
    invalid!("--batch-max-size must be at least --batch-size")
  end
  invalid!("--batch-max-size requires --adaptive-batch") if options.batch_max_size && !options.adaptive_batch
  positive_finite_if_set!(options.batch_audio_seconds, "--batch-audio-seconds")
  unless finite?(options.batch_vram_target) && options.batch_vram_target.between?(0.50, 0.98)
    invalid!("--batch-vram-target must be between 0.50 and 0.98")
  end

  invalid!("--align-batch-size must be positive") if (options.alignment == "word") && !positive_integer?(options.align_batch_size)
  positive_finite!(options.max_dur, "--max-dur")
  invalid!("--min-dur must be finite") unless finite?(options.min_dur)
  invalid!("Require 0 <= --min-dur <= --max-dur") if options.vad != "none" && !options.min_dur.between?(0, options.max_dur)
  if options.vad == "none" && options.max_dur < ASR_FIXED_MIN_SECONDS
    invalid!(
      "--vad none requires --max-dur >= #{ASR_FIXED_MIN_SECONDS.to_i} second " \
      "to bound segment count and memory use"
    )
  end

  case options.vad
  when "silero"
    invalid!("--vad-threshold must be between 0 and 1") unless finite?(options.vad_threshold) && options.vad_threshold.between?(0, 1)
    unless options.min_silence_ms.is_a?(Integer) && options.speech_pad_ms.is_a?(Integer)
      invalid!("--min-silence-ms and --speech-pad-ms must be integers")
    end
    if options.min_silence_ms.negative? || options.speech_pad_ms.negative?
      invalid!("--min-silence-ms and --speech-pad-ms must be non-negative")
    end
  when "auditok"
    if (options.max_silence.is_a?(Numeric) && options.max_silence.negative?) ||
       (options.energy_threshold.is_a?(Numeric) && options.energy_threshold.negative?)
      invalid!("Auditok silence and energy thresholds must be finite and non-negative")
    end
    invalid!("--vad auditok requires --min-dur > 0") unless options.min_dur.positive?
    if options.max_silence.is_a?(Numeric) && options.max_silence >= options.max_dur
      invalid!("--vad auditok requires --max-silence < --max-dur")
    end
  end
  invalid!("--vad-merge is supported only with --vad silero") if options.vad_merge && options.vad != "silero"

  invalid!("--max-new-tokens must be positive") unless positive_integer?(options.max_new_tokens)
  invalid!("--max-retry-tokens must be an integer") unless options.max_retry_tokens.is_a?(Integer)
  invalid!("--max-retry-tokens must be at least --max-new-tokens") if options.max_retry_tokens < options.max_new_tokens
  thresholds = [
    options.vad_threshold, options.max_silence, options.energy_threshold,
    options.max_cue_dur, options.max_gap
  ]
  invalid!("All numeric thresholds and cue limits must be finite") unless thresholds.all? { |v| finite?(v) }
  if options.alignment != "none" &&
     (!positive_integer?(options.max_chars) || options.max_cue_dur <= 0 || options.max_gap.negative?)
    invalid!("Subtitle cue limits must be positive (and --max-gap non-negative)")
  end

  options
end

.validate_model_reference!(value, option:, description:) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/cohere/transcribe/configuration.rb', line 195

def validate_model_reference!(value, option:, description:)
  unless value.is_a?(String) || value.respond_to?(:to_path)
    raise TypeError, "#{description} must be a string or a path-like local directory"
  end

  text = value.is_a?(String) ? value : value.to_path
  raise TypeError, "#{description} path must resolve to text" unless text.is_a?(String)

  stripped = PythonText.strip(text)
  invalid!("#{option} must be a non-empty Hugging Face repository ID or local directory") if stripped.empty?
  invalid!("#{option} must not have leading or trailing whitespace") unless text == stripped
  return if local_reference?(value, description: description)

  valid = valid_repository_id?(text)
  invalid!("#{option} must be a valid Hugging Face repository ID or local directory") unless valid
rescue ArgumentError => e
  invalid!(e.message)
end

.validate_revision!(value, option:) ⇒ Object



225
226
227
228
229
230
231
232
233
234
# File 'lib/cohere/transcribe/configuration.rb', line 225

def validate_revision!(value, option:)
  return if value.nil?

  if value.is_a?(String)
    stripped = PythonText.strip(value)
    return if !stripped.empty? && value == stripped
  end

  invalid!("#{option} must be a non-empty revision without surrounding whitespace")
end