cohere-transcribe 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +9 -7
  4. data/ext/cohere_transcribe_native/.gitignore +6 -0
  5. data/ext/cohere_transcribe_native/CMakeLists.txt +8 -0
  6. data/ext/cohere_transcribe_native/audio_abi.cpp +20 -0
  7. data/ext/cohere_transcribe_native/audio_exports.macos +1 -0
  8. data/ext/cohere_transcribe_native/audio_exports.map +1 -0
  9. data/ext/cohere_transcribe_native/test/abi_smoke.rb +26 -0
  10. data/lib/cohere/transcribe/alignment/aligner.rb +40 -7
  11. data/lib/cohere/transcribe/asr/native.rb +43 -97
  12. data/lib/cohere/transcribe/audio/decoder.rb +184 -75
  13. data/lib/cohere/transcribe/audio/ffmpeg_native.rb +84 -32
  14. data/lib/cohere/transcribe/audio/segmentation.rb +1 -0
  15. data/lib/cohere/transcribe/cli.rb +30 -22
  16. data/lib/cohere/transcribe/configuration.rb +13 -0
  17. data/lib/cohere/transcribe/constants.rb +1 -1
  18. data/lib/cohere/transcribe/doctor.rb +2 -1
  19. data/lib/cohere/transcribe/hub.rb +446 -58
  20. data/lib/cohere/transcribe/input.rb +18 -3
  21. data/lib/cohere/transcribe/internal/interruptible_native_call.rb +70 -0
  22. data/lib/cohere/transcribe/internal/session_ownership.rb +61 -0
  23. data/lib/cohere/transcribe/internal/utf8.rb +19 -0
  24. data/lib/cohere/transcribe/model_identity.rb +10 -1
  25. data/lib/cohere/transcribe/output/publication.rb +95 -86
  26. data/lib/cohere/transcribe/pytorch_checkpoint.rb +38 -22
  27. data/lib/cohere/transcribe/runtime/engine.rb +37 -16
  28. data/lib/cohere/transcribe/runtime/preparation.rb +212 -35
  29. data/lib/cohere/transcribe/runtime/resources.rb +28 -62
  30. data/lib/cohere/transcribe/state/checkpoint.rb +3 -2
  31. data/lib/cohere/transcribe/state/io.rb +217 -117
  32. data/lib/cohere/transcribe/state/locking.rb +383 -64
  33. data/lib/cohere/transcribe/state/manifest.rb +21 -22
  34. data/lib/cohere/transcribe/types.rb +25 -11
  35. data/lib/cohere/transcribe/version.rb +1 -1
  36. data/sig/cohere/transcribe.rbs +1 -0
  37. metadata +5 -1
@@ -12,10 +12,11 @@ module Cohere
12
12
  STATE_SUFFIX = ".cohere-transcribe.manifest.json"
13
13
  CHECKPOINT_SUFFIX = ".cohere-transcribe.asr.json"
14
14
  MAX_STATE_BYTES = 64 * 1024 * 1024
15
- DEFERRED_PUBLICATION_EXCEPTIONS = {
16
- SignalException => :never,
17
- SystemExit => :never
18
- }.freeze
15
+ DEFERRED_PUBLICATION_EXCEPTIONS = { Object => :never }.freeze
16
+
17
+ class PublicationDirectoryChangedError < TranscriptionRuntimeError; end
18
+ class PublicationEntryError < TranscriptionRuntimeError; end
19
+ private_constant :PublicationDirectoryChangedError, :PublicationEntryError
19
20
 
20
21
  DirectoryBinding = Data.define(
21
22
  :access_path,
@@ -49,10 +50,10 @@ module Cohere
49
50
  stat.dev == device && stat.ino == inode
50
51
  return self if valid
51
52
 
52
- raise TranscriptionRuntimeError,
53
+ raise PublicationDirectoryChangedError,
53
54
  "Publication parent changed after planning: #{access_path}"
54
55
  rescue SystemCallError, ArgumentError => e
55
- raise TranscriptionRuntimeError,
56
+ raise PublicationDirectoryChangedError,
56
57
  "Publication parent changed after planning: #{access_path} (#{e.message})"
57
58
  end
58
59
  end
@@ -88,7 +89,7 @@ module Cohere
88
89
  handle.close_on_exec = true
89
90
  stat = handle.stat
90
91
  unless stat.directory? && stat.dev == binding.device && stat.ino == binding.inode
91
- raise TranscriptionRuntimeError,
92
+ raise PublicationDirectoryChangedError,
92
93
  "Publication parent changed while it was being opened: #{binding.access_path}"
93
94
  end
94
95
  guards.each(&:verify!)
@@ -98,6 +99,7 @@ module Cohere
98
99
  rescue Interrupt, SystemExit
99
100
  raise
100
101
  rescue SystemCallError, ArgumentError => e
102
+ guards&.each(&:verify!)
101
103
  raise TranscriptionRuntimeError,
102
104
  "Cannot open planned publication parent #{binding.access_path}: #{e.message}"
103
105
  ensure
@@ -169,12 +171,12 @@ module Cohere
169
171
  handle = open_entry(name, flags, 0, mode: writable ? "r+b" : "rb")
170
172
  unless handle.stat.file?
171
173
  handle.close
172
- raise TranscriptionRuntimeError,
174
+ raise PublicationEntryError,
173
175
  "Publication entry is not a regular file: #{display_path(name)}"
174
176
  end
175
177
  handle
176
178
  rescue Errno::ELOOP, Errno::EISDIR, Errno::ENXIO => e
177
- raise TranscriptionRuntimeError,
179
+ raise PublicationEntryError,
178
180
  "Publication entry is not a regular file: #{display_path(name)}",
179
181
  cause: e
180
182
  end
@@ -198,7 +200,7 @@ module Cohere
198
200
  end
199
201
  current = handle.stat
200
202
  current.dev == expected_stat.dev && current.ino == expected_stat.ino
201
- rescue Errno::ENOENT
203
+ rescue Errno::ENOENT, PublicationEntryError
202
204
  false
203
205
  ensure
204
206
  handle&.close
@@ -263,7 +265,7 @@ module Cohere
263
265
  def verify!
264
266
  stat = @handle.stat
265
267
  unless stat.directory? && stat.dev == binding.device && stat.ino == binding.inode
266
- raise TranscriptionRuntimeError,
268
+ raise PublicationDirectoryChangedError,
267
269
  "Retained publication parent changed identity: #{binding.access_path}"
268
270
  end
269
271
  @guards.each(&:verify!)
@@ -278,10 +280,12 @@ module Cohere
278
280
  end
279
281
 
280
282
  def close
281
- return if @closed
283
+ Thread.handle_interrupt(Object => :never) do
284
+ return if @closed
282
285
 
283
- @handle.close
284
- @closed = true
286
+ @handle.close
287
+ @closed = true
288
+ end
285
289
  nil
286
290
  end
287
291
 
@@ -367,7 +371,47 @@ module Cohere
367
371
 
368
372
  module_function
369
373
 
370
- def with_bound_parent(path, directory_binding: nil, guard_bindings: nil)
374
+ def finish_atomic_recovery!(subject:, completed:, primary_error:, recovery_errors:,
375
+ retained_backups:)
376
+ return if recovery_errors.empty?
377
+ # Raising from an ensure during Thread#kill replaces termination with a
378
+ # catchable exception, so cleanup remains best effort while aborting.
379
+ return if Thread.current.status == "aborting"
380
+
381
+ outcome = if completed
382
+ "completed but cleanup was incomplete"
383
+ elsif primary_error
384
+ "failed and recovery was incomplete"
385
+ else
386
+ "did not complete and recovery was incomplete"
387
+ end
388
+ detail = recovery_errors.join("; ")
389
+ retained = retained_backups.compact.map(&:to_s).uniq.sort
390
+ retained_detail = if retained.empty?
391
+ ""
392
+ elsif retained.one?
393
+ "; preserved backup: #{retained.first}"
394
+ else
395
+ "; preserved backups: #{retained}"
396
+ end
397
+ raise TranscriptionRuntimeError,
398
+ "#{subject} #{outcome} (#{detail})#{retained_detail}",
399
+ cause: primary_error
400
+ end
401
+
402
+ def close_atomic_resources(resources, recovery_errors, label:)
403
+ failures = []
404
+ Array(resources).compact.uniq.each do |resource|
405
+ resource.close unless resource.respond_to?(:closed?) && resource.closed?
406
+ rescue SystemCallError, IOError, TranscriptionRuntimeError => e
407
+ recovery_errors << "close #{label.call(resource)}: #{e.message}"
408
+ failures << e
409
+ end
410
+ failures
411
+ end
412
+
413
+ def with_bound_parent(path, directory_binding: nil, guard_bindings: nil,
414
+ recovery_errors: nil)
371
415
  path = Pathname(path).expand_path
372
416
  directory_binding ||= DirectoryBinding.capture(path.dirname)
373
417
  unless [directory_binding.access_path, directory_binding.canonical_path].include?(path.dirname)
@@ -380,9 +424,28 @@ module Cohere
380
424
  Thread.handle_interrupt(DEFERRED_PUBLICATION_EXCEPTIONS) do
381
425
  bound = BoundDirectory.open(directory_binding, guards: guards.uniq)
382
426
  end
383
- yield bound, path.basename.to_s
427
+ completed = false
428
+ primary_error = nil
429
+ begin
430
+ result = yield bound, path.basename.to_s
431
+ completed = true
432
+ result
433
+ rescue Exception => e # rubocop:disable Lint/RescueException -- preserve the active outcome through close
434
+ primary_error = e
435
+ raise
436
+ end
384
437
  ensure
385
- bound&.close
438
+ close_errors = recovery_errors || []
439
+ close_atomic_resources([bound], close_errors, label: ->(resource) { resource.binding.canonical_path })
440
+ unless recovery_errors
441
+ finish_atomic_recovery!(
442
+ subject: "Publication directory operation",
443
+ completed: completed,
444
+ primary_error: primary_error,
445
+ recovery_errors: close_errors,
446
+ retained_backups: []
447
+ )
448
+ end
386
449
  end
387
450
 
388
451
  def ensure_bound_directory(path, root_binding: nil, operation_hook: nil)
@@ -511,121 +574,158 @@ module Cohere
511
574
  end
512
575
 
513
576
  def write_state_atomic(path, payload, source_snapshot:, directory_binding: nil,
514
- guard_bindings: nil, operation_hook: nil, rename: nil)
577
+ guard_bindings: nil, operation_hook: nil, rename: nil,
578
+ commit_guard: nil)
515
579
  path = Pathname(path).expand_path
516
580
  directory_binding ||= ensure_bound_directory(path.dirname)
517
- with_bound_parent(
518
- path,
519
- directory_binding: directory_binding,
520
- guard_bindings: guard_bindings
521
- ) do |bound, basename|
522
- operation_hook&.call(:directory_opened, path)
523
- temporary_name = nil
524
- temporary = nil
525
- backup_name = nil
526
- backup = nil
527
- source = nil
528
- committed = false
529
- preserved_backup = false
530
- primary_failed = false
531
- begin
532
- mode = bound_state_mode(bound, basename)
533
- Thread.handle_interrupt(DEFERRED_PUBLICATION_EXCEPTIONS) do
534
- temporary_name, temporary = bound.create_temporary(basename, ".tmp")
535
- end
536
- temporary.chmod(mode)
537
- operation_hook&.call(:before_stage_write, path)
538
- temporary.write(JSON.pretty_generate(envelope(payload)))
539
- temporary.write("\n")
540
- temporary.flush
541
- temporary.fsync
542
- temporary.close
581
+ recovery_errors = []
582
+ retained_backups = []
583
+ completed = false
584
+ primary_error = nil
585
+ begin
586
+ with_bound_parent(
587
+ path,
588
+ directory_binding: directory_binding,
589
+ guard_bindings: guard_bindings,
590
+ recovery_errors: recovery_errors
591
+ ) do |bound, basename|
592
+ operation_hook&.call(:directory_opened, path)
593
+ temporary_name = nil
543
594
  temporary = nil
544
- bound.verify!
545
-
546
- operation_hook&.call(:before_backup_open, path)
547
- Thread.handle_interrupt(DEFERRED_PUBLICATION_EXCEPTIONS) do
548
- source = begin
549
- bound.open_regular(basename)
550
- rescue Errno::ENOENT
551
- nil
595
+ backup_name = nil
596
+ backup = nil
597
+ source = nil
598
+ committed = false
599
+ begin
600
+ mode = bound_state_mode(bound, basename)
601
+ Thread.handle_interrupt(DEFERRED_PUBLICATION_EXCEPTIONS) do
602
+ temporary_name, temporary = bound.create_temporary(basename, ".tmp")
552
603
  end
553
- end
554
- if source
604
+ temporary.chmod(mode)
605
+ operation_hook&.call(:before_stage_write, path)
606
+ temporary.write(JSON.pretty_generate(envelope(payload)))
607
+ temporary.write("\n")
608
+ temporary.flush
609
+ temporary.fsync
610
+ temporary.close
611
+ temporary = nil
612
+ bound.verify!
613
+
614
+ operation_hook&.call(:before_backup_open, path)
555
615
  Thread.handle_interrupt(DEFERRED_PUBLICATION_EXCEPTIONS) do
556
- backup_name, backup = bound.create_temporary(basename, ".bak")
616
+ source = begin
617
+ bound.open_regular(basename)
618
+ rescue Errno::ENOENT
619
+ nil
620
+ end
557
621
  end
558
- begin
559
- backup.chmod(source.stat.mode & 0o7777)
560
- operation_hook&.call(:before_backup_copy, path)
561
- IO.copy_stream(source, backup)
562
- backup.flush
563
- backup.fsync
564
- ensure
565
- source.close
566
- backup.close
622
+ if source
623
+ Thread.handle_interrupt(DEFERRED_PUBLICATION_EXCEPTIONS) do
624
+ backup_name, backup = bound.create_temporary(basename, ".bak")
625
+ end
626
+ backup_copy_error = nil
627
+ begin
628
+ backup.chmod(source.stat.mode & 0o7777)
629
+ operation_hook&.call(:before_backup_copy, path)
630
+ IO.copy_stream(source, backup)
631
+ backup.flush
632
+ backup.fsync
633
+ rescue Exception => e # rubocop:disable Lint/RescueException -- retain the copy failure through close
634
+ backup_copy_error = e
635
+ raise
636
+ ensure
637
+ close_failures = close_atomic_resources(
638
+ [source, backup],
639
+ recovery_errors,
640
+ label: ->(handle) { handle.respond_to?(:path) ? handle.path : path }
641
+ )
642
+ raise close_failures.first if close_failures.any? && backup_copy_error.nil?
643
+ end
567
644
  end
568
- end
569
645
 
570
- ensure_source_unchanged!(source_snapshot)
571
- bound.verify!
572
- operation_hook&.call(:before_rename, path)
573
- Thread.handle_interrupt(DEFERRED_PUBLICATION_EXCEPTIONS) do
574
- if rename
575
- committed = true
576
- rename.call(bound, temporary_name, basename, :commit)
577
- else
578
- bound.rename(temporary_name, basename)
579
- committed = true
646
+ ensure_source_unchanged!(source_snapshot)
647
+ bound.verify!
648
+ operation_hook&.call(:before_rename, path)
649
+ Thread.handle_interrupt(DEFERRED_PUBLICATION_EXCEPTIONS) do
650
+ commit_guard&.call
651
+ if rename
652
+ committed = true
653
+ rename.call(bound, temporary_name, basename, :commit)
654
+ else
655
+ bound.rename(temporary_name, basename)
656
+ committed = true
657
+ end
658
+ operation_hook&.call(:after_rename, path)
659
+ bound.verify!
660
+ bound.fsync
661
+ commit_guard&.call
662
+ completed = true
580
663
  end
581
- end
582
- operation_hook&.call(:after_rename, path)
583
- bound.verify!
584
- bound.fsync
585
- path
586
- rescue Exception => e # rubocop:disable Lint/RescueException -- rollback includes interrupts
587
- primary_failed = true
588
- if committed
589
- begin
590
- Thread.handle_interrupt(DEFERRED_PUBLICATION_EXCEPTIONS) do
591
- bound.unlink(basename, missing_ok: true)
592
- if backup_name
593
- if rename
594
- rename.call(bound, backup_name, basename, :rollback)
595
- else
596
- bound.rename(backup_name, basename)
664
+ path
665
+ rescue Exception => e # rubocop:disable Lint/RescueException -- rollback includes interrupts
666
+ primary_error = e
667
+ raise
668
+ ensure
669
+ Thread.handle_interrupt(DEFERRED_PUBLICATION_EXCEPTIONS) do
670
+ if !completed && committed
671
+ begin
672
+ bound.unlink(basename, missing_ok: true)
673
+ if backup_name
674
+ if rename
675
+ rename.call(bound, backup_name, basename, :rollback)
676
+ else
677
+ bound.rename(backup_name, basename)
678
+ end
679
+ backup_name = nil
597
680
  end
681
+ rescue StandardError => e
682
+ retained_backups << bound.display_path(backup_name) if backup_name
683
+ recovery_errors << "#{path}: #{e.message}"
684
+ end
685
+ begin
686
+ bound.fsync
687
+ rescue SystemCallError, TranscriptionRuntimeError => e
688
+ recovery_errors << "directory sync: #{e.message}"
689
+ end
690
+ end
691
+
692
+ close_atomic_resources(
693
+ [source, backup, temporary],
694
+ recovery_errors,
695
+ label: ->(handle) { handle.respond_to?(:path) ? handle.path : path }
696
+ )
697
+ if temporary_name
698
+ begin
699
+ bound.unlink(temporary_name, missing_ok: true)
700
+ rescue SystemCallError, TranscriptionRuntimeError => e
701
+ temporary_path = bound.display_path(temporary_name)
702
+ recovery_errors << "cleanup #{temporary_path}: #{e.message}"
703
+ end
704
+ end
705
+ if backup_name && !retained_backups.include?(bound.display_path(backup_name))
706
+ begin
707
+ bound.unlink(backup_name, missing_ok: true)
708
+ backup_name = nil
709
+ rescue SystemCallError, TranscriptionRuntimeError => e
710
+ backup_path = bound.display_path(backup_name)
711
+ retained_backups << backup_path
712
+ recovery_errors << "cleanup #{backup_path}: #{e.message}"
598
713
  end
599
- backup_name = nil
600
714
  end
601
- bound.fsync
602
- rescue SystemCallError, TranscriptionRuntimeError => rollback_error
603
- retained = backup_name ? bound.display_path(backup_name) : nil
604
- preserved_backup = !backup_name.nil?
605
- raise TranscriptionRuntimeError,
606
- "State commit failed and rollback was incomplete (#{rollback_error.message}); " \
607
- "preserved backup: #{retained}",
608
- cause: e
609
- end
610
- end
611
- raise e
612
- ensure
613
- cleanup_failure = nil
614
- cleanup_operations = [
615
- -> { temporary&.close unless temporary&.closed? },
616
- -> { backup&.close unless backup&.closed? },
617
- -> { bound.unlink(temporary_name, missing_ok: true) if temporary_name },
618
- lambda do
619
- bound.unlink(backup_name, missing_ok: true) if backup_name && !preserved_backup
620
715
  end
621
- ]
622
- cleanup_operations.each do |operation|
623
- operation.call
624
- rescue SystemCallError, IOError => e
625
- cleanup_failure ||= e
626
716
  end
627
- raise cleanup_failure if cleanup_failure && !primary_failed
628
717
  end
718
+ rescue Exception => e # rubocop:disable Lint/RescueException -- retain failures outside the bound block
719
+ primary_error ||= e
720
+ raise
721
+ ensure
722
+ finish_atomic_recovery!(
723
+ subject: "State commit",
724
+ completed: completed,
725
+ primary_error: primary_error,
726
+ recovery_errors: recovery_errors,
727
+ retained_backups: retained_backups
728
+ )
629
729
  end
630
730
  end
631
731