Class: Cohere::Transcribe::Alignment::Aligner
- Inherits:
-
Object
- Object
- Cohere::Transcribe::Alignment::Aligner
- Defined in:
- lib/cohere/transcribe/alignment/aligner.rb
Overview
Full-file MMS emissions plus per-ASR-segment CTC Viterbi alignment. The 30 s windows, 2 s context, 20 ms stride, wildcard column, and uniform per-segment recovery are the same as the Python reference.
Constant Summary collapse
- SAMPLE_RATE =
16_000- INPUTS_TO_LOGITS_RATIO =
320- WINDOW_SECONDS =
30- CONTEXT_SECONDS =
2- WINDOW_SAMPLES =
WINDOW_SECONDS * SAMPLE_RATE
- CONTEXT_SAMPLES =
CONTEXT_SECONDS * SAMPLE_RATE
- INPUT_SAMPLES =
WINDOW_SAMPLES + (2 * CONTEXT_SAMPLES)
- WINDOW_FRAMES =
WINDOW_SAMPLES / INPUTS_TO_LOGITS_RATIO
- CONTEXT_FRAMES =
CONTEXT_SAMPLES / INPUTS_TO_LOGITS_RATIO
- STRIDE_MS =
INPUTS_TO_LOGITS_RATIO * 1_000.0 / SAMPLE_RATE
- ISO3 =
{ "ar" => "ara", "en" => "eng" }.freeze
- VOCABULARY =
{ "<blank>" => 0, "<pad>" => 1, "</s>" => 2, "<unk>" => 3, "a" => 4, "i" => 5, "e" => 6, "n" => 7, "o" => 8, "u" => 9, "t" => 10, "s" => 11, "r" => 12, "m" => 13, "k" => 14, "l" => 15, "d" => 16, "g" => 17, "h" => 18, "y" => 19, "b" => 20, "p" => 21, "w" => 22, "c" => 23, "v" => 24, "j" => 25, "z" => 26, "f" => 27, "'" => 28, "q" => 29, "x" => 30 }.freeze
- BLANK_ID =
VOCABULARY.fetch("<blank>")
Instance Attribute Summary collapse
-
#batch_size ⇒ Object
readonly
Returns the value of attribute batch_size.
-
#emissions_seconds ⇒ Object
readonly
Returns the value of attribute emissions_seconds.
-
#session ⇒ Object
readonly
Returns the value of attribute session.
-
#viterbi_seconds ⇒ Object
readonly
Returns the value of attribute viterbi_seconds.
Instance Method Summary collapse
- #align(audio, segment_times, segment_texts, language:) ⇒ Object
- #align_emissions(emissions, stride_ms, segment_times, segment_texts, language:) ⇒ Object
- #close ⇒ Object
- #compute_emissions(audio) ⇒ Object
-
#initialize(dtype: "fp32", device: "cpu", batch_size: 4, session: nil, **session_options) ⇒ Aligner
constructor
A new instance of Aligner.
- #load! ⇒ Object
- #load_seconds ⇒ Object
- #provider ⇒ Object
Constructor Details
#initialize(dtype: "fp32", device: "cpu", batch_size: 4, session: nil, **session_options) ⇒ Aligner
Returns a new instance of Aligner.
251 252 253 254 255 256 257 258 259 |
# File 'lib/cohere/transcribe/alignment/aligner.rb', line 251 def initialize(dtype: "fp32", device: "cpu", batch_size: 4, session: nil, **) raise ArgumentError, "aligner batch_size must be positive" unless batch_size.is_a?(Integer) && batch_size.positive? @session = session || Session.new(dtype: dtype, device: device, **) @batch_size = batch_size @learned_batch_size = batch_size @emissions_seconds = 0.0 @viterbi_seconds = 0.0 end |
Instance Attribute Details
#batch_size ⇒ Object (readonly)
Returns the value of attribute batch_size.
249 250 251 |
# File 'lib/cohere/transcribe/alignment/aligner.rb', line 249 def batch_size @batch_size end |
#emissions_seconds ⇒ Object (readonly)
Returns the value of attribute emissions_seconds.
249 250 251 |
# File 'lib/cohere/transcribe/alignment/aligner.rb', line 249 def emissions_seconds @emissions_seconds end |
#session ⇒ Object (readonly)
Returns the value of attribute session.
249 250 251 |
# File 'lib/cohere/transcribe/alignment/aligner.rb', line 249 def session @session end |
#viterbi_seconds ⇒ Object (readonly)
Returns the value of attribute viterbi_seconds.
249 250 251 |
# File 'lib/cohere/transcribe/alignment/aligner.rb', line 249 def viterbi_seconds @viterbi_seconds end |
Instance Method Details
#align(audio, segment_times, segment_texts, language:) ⇒ Object
278 279 280 281 282 283 284 285 286 |
# File 'lib/cohere/transcribe/alignment/aligner.rb', line 278 def align(audio, segment_times, segment_texts, language:) raise ArgumentError, "segment_times and segment_texts must have equal lengths" unless segment_times.length == segment_texts.length emissions, stride_ms = compute_emissions(audio) started = monotonic align_emissions(emissions, stride_ms, segment_times, segment_texts, language: language) ensure @viterbi_seconds += monotonic - started if started end |
#align_emissions(emissions, stride_ms, segment_times, segment_texts, language:) ⇒ Object
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 400 401 402 403 404 405 406 407 408 |
# File 'lib/cohere/transcribe/alignment/aligner.rb', line 350 def align_emissions(emissions, stride_ms, segment_times, segment_texts, language:) frame_count, emission_classes = emissions.shape star_id = emission_classes - 1 raise TranscriptionRuntimeError, "MMS wildcard column collides with its tokenizer vocabulary" if VOCABULARY.value?(star_id) dictionary = VOCABULARY.merge("<star>" => star_id).freeze index_to_token = dictionary.invert.freeze iso_language = ISO3.fetch(language, language) words = [] fallback_count = 0 segment_times.zip(segment_texts).each_with_index do |((start_time, end_time), text), segment_index| text = PythonText.strip(text.to_s) next if text.empty? first_frame = [(start_time * 1_000 / stride_ms).round(half: :even), 0].max last_frame = [(end_time * 1_000 / stride_ms).round(half: :even), frame_count].min if last_frame - first_frame < 2 fallback_count += 1 words.concat(uniform_fallback(text, start_time, end_time, segment_index)) next end begin tokens_starred, text_starred = Text.preprocess(text, iso_language) targets = tokens_starred.join(" ").split.filter_map { |token| dictionary[token] } raise ArgumentError, "Transcript produced no aligner vocabulary tokens" if targets.empty? path = CTC.forced_align(emissions[first_frame...last_frame, true], targets, blank: BLANK_ID) segments = CTC.merge_repeats(path, index_to_token) spans = CTC.spans(tokens_starred, segments, index_to_token.fetch(BLANK_ID)) results = Text.postprocess(text_starred, spans, stride_ms) expected_tokens = PythonText.split(text) unless results.map { |word| word.fetch(:text) } == expected_tokens raise ArgumentError, "forced alignment did not preserve the complete ASR transcript " \ "(#{results.length}/#{expected_tokens.length} words)" end rescue StandardError fallback_count += 1 words.concat(uniform_fallback(text, start_time, end_time, segment_index)) next end results.each_with_index do |word, word_index| absolute_start = (start_time + word.fetch(:start)).clamp(start_time, end_time) absolute_end = (start_time + word.fetch(:end)).clamp(absolute_start, end_time) words << TranscriptionWord.new( start: absolute_start, end: absolute_end, text: word.fetch(:text), segment_index: segment_index, segment_word_index: word_index, timing_source: "ctc" ) end end [words.freeze, fallback_count] end |
#close ⇒ Object
274 275 276 |
# File 'lib/cohere/transcribe/alignment/aligner.rb', line 274 def close session.close end |
#compute_emissions(audio) ⇒ Object
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/alignment/aligner.rb', line 288 def compute_emissions(audio) started = monotonic samples = mono_float32(audio) raise ArgumentError, "Cannot compute CTC emissions for empty audio" if samples.empty? total_windows = (samples.length + WINDOW_SAMPLES - 1) / WINDOW_SAMPLES extension_samples = (total_windows * WINDOW_SAMPLES) - samples.length extension_frames = extension_samples / INPUTS_TO_LOGITS_RATIO frame_count = (total_windows * WINDOW_FRAMES) - extension_frames raise TranscriptionRuntimeError, "MMS aligner produced no usable CTC frames" unless frame_count.positive? emissions = nil write_offset = 0 first_window = 0 current_batch_size = [batch_size, @learned_batch_size].min while first_window < total_windows window_count = [current_batch_size, total_windows - first_window].min begin input = build_window_batch(samples, first_window, window_count) logits = session.run(input) batch_log_probs = crop_and_normalize(logits, window_count) rescue NoMemoryError, StandardError => e raise unless out_of_memory?(e) && current_batch_size > 1 current_batch_size = [1, current_batch_size / 2].max @learned_batch_size = current_batch_size next end expected_frames = window_count * WINDOW_FRAMES unless batch_log_probs.shape[0] == expected_frames raise TranscriptionRuntimeError, "MMS aligner returned an unexpected frame count" end class_count = batch_log_probs.shape[1] unless class_count == VOCABULARY.length raise TranscriptionRuntimeError, "MMS aligner returned #{class_count} classes; expected #{VOCABULARY.length}" end emissions ||= Numo::SFloat.zeros(frame_count, class_count + 1) if first_window + window_count == total_windows && extension_frames.positive? kept = batch_log_probs.shape[0] - extension_frames batch_log_probs = batch_log_probs[0...kept, true] end next_offset = write_offset + batch_log_probs.shape[0] raise TranscriptionRuntimeError, "MMS aligner produced too many CTC frames" if next_offset > emissions.shape[0] emissions[write_offset...next_offset, 0...class_count] = batch_log_probs write_offset = next_offset first_window += window_count end unless emissions && write_offset == emissions.shape[0] raise TranscriptionRuntimeError, "MMS emission assembly mismatch: wrote #{write_offset}, expected #{frame_count}" end [emissions, STRIDE_MS] ensure @emissions_seconds += monotonic - started if started end |
#load! ⇒ Object
269 270 271 272 |
# File 'lib/cohere/transcribe/alignment/aligner.rb', line 269 def load! session.load! if session.respond_to?(:load!) self end |
#load_seconds ⇒ Object
265 266 267 |
# File 'lib/cohere/transcribe/alignment/aligner.rb', line 265 def load_seconds session.load_seconds end |
#provider ⇒ Object
261 262 263 |
# File 'lib/cohere/transcribe/alignment/aligner.rb', line 261 def provider session.provider end |