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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +9 -7
- data/ext/cohere_transcribe_native/.gitignore +6 -0
- data/ext/cohere_transcribe_native/CMakeLists.txt +8 -0
- data/ext/cohere_transcribe_native/audio_abi.cpp +20 -0
- data/ext/cohere_transcribe_native/audio_exports.macos +1 -0
- data/ext/cohere_transcribe_native/audio_exports.map +1 -0
- data/ext/cohere_transcribe_native/test/abi_smoke.rb +26 -0
- data/lib/cohere/transcribe/alignment/aligner.rb +40 -7
- data/lib/cohere/transcribe/asr/native.rb +43 -97
- data/lib/cohere/transcribe/audio/decoder.rb +184 -75
- data/lib/cohere/transcribe/audio/ffmpeg_native.rb +84 -32
- data/lib/cohere/transcribe/audio/segmentation.rb +1 -0
- data/lib/cohere/transcribe/cli.rb +30 -22
- data/lib/cohere/transcribe/configuration.rb +13 -0
- data/lib/cohere/transcribe/constants.rb +1 -1
- data/lib/cohere/transcribe/doctor.rb +2 -1
- data/lib/cohere/transcribe/hub.rb +446 -58
- data/lib/cohere/transcribe/input.rb +18 -3
- data/lib/cohere/transcribe/internal/interruptible_native_call.rb +70 -0
- data/lib/cohere/transcribe/internal/session_ownership.rb +61 -0
- data/lib/cohere/transcribe/internal/utf8.rb +19 -0
- data/lib/cohere/transcribe/model_identity.rb +10 -1
- data/lib/cohere/transcribe/output/publication.rb +95 -86
- data/lib/cohere/transcribe/pytorch_checkpoint.rb +38 -22
- data/lib/cohere/transcribe/runtime/engine.rb +37 -16
- data/lib/cohere/transcribe/runtime/preparation.rb +212 -35
- data/lib/cohere/transcribe/runtime/resources.rb +28 -62
- data/lib/cohere/transcribe/state/checkpoint.rb +3 -2
- data/lib/cohere/transcribe/state/io.rb +217 -117
- data/lib/cohere/transcribe/state/locking.rb +383 -64
- data/lib/cohere/transcribe/state/manifest.rb +21 -22
- data/lib/cohere/transcribe/types.rb +25 -11
- data/lib/cohere/transcribe/version.rb +1 -1
- data/sig/cohere/transcribe.rbs +1 -0
- metadata +5 -1
|
@@ -264,7 +264,13 @@ module Cohere
|
|
|
264
264
|
requested_workers: resolved_options.preprocess_workers,
|
|
265
265
|
enabled: resolved_options.pipeline_preparation,
|
|
266
266
|
worker_limit: vad_file_concurrency_limit(resolved_options),
|
|
267
|
-
estimate_bytes: estimate_bytes
|
|
267
|
+
estimate_bytes: estimate_bytes,
|
|
268
|
+
exclusive_retry: lambda do |prepared|
|
|
269
|
+
prepared.error.is_a?(Audio::DecodedAudioLimitError)
|
|
270
|
+
end,
|
|
271
|
+
retained_bytes: lambda do |prepared|
|
|
272
|
+
prepared.decoded ? prepared.decoded.samples.byte_size : 0
|
|
273
|
+
end
|
|
268
274
|
) do |item, decoded_byte_limit, worker_slot|
|
|
269
275
|
prepare_entry(
|
|
270
276
|
item,
|
|
@@ -371,7 +377,12 @@ module Cohere
|
|
|
371
377
|
end
|
|
372
378
|
|
|
373
379
|
def preflight_outcome(index, entry, plan, resolved_options, measurements)
|
|
374
|
-
|
|
380
|
+
if resolved_options.publication&.existing == "skip" && plan.skipped
|
|
381
|
+
optimistic = Output::Publication.revalidate(plan, resolved_options)
|
|
382
|
+
return skipped_result(entry, plan) if optimistic.action == :skip
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
Output::Publication.with_plan_lock(plan) do |lock|
|
|
375
386
|
decision = Output::Publication.revalidate(plan, resolved_options)
|
|
376
387
|
case decision.action
|
|
377
388
|
when :skip
|
|
@@ -383,7 +394,8 @@ module Cohere
|
|
|
383
394
|
plan,
|
|
384
395
|
decision,
|
|
385
396
|
resolved_options,
|
|
386
|
-
measurements
|
|
397
|
+
measurements,
|
|
398
|
+
lock
|
|
387
399
|
)
|
|
388
400
|
else
|
|
389
401
|
PreparationItem.new(index: index, entry: entry, plan: plan)
|
|
@@ -391,7 +403,7 @@ module Cohere
|
|
|
391
403
|
end
|
|
392
404
|
end
|
|
393
405
|
|
|
394
|
-
def preflight_resumed_entry(index, entry, plan, decision, resolved_options, measurements)
|
|
406
|
+
def preflight_resumed_entry(index, entry, plan, decision, resolved_options, measurements, lock)
|
|
395
407
|
prepared = Preparation::PreparedEntry.new(
|
|
396
408
|
item: PreparationItem.new(index: index, entry: entry, plan: plan),
|
|
397
409
|
snapshot: source_snapshot(entry.path),
|
|
@@ -410,6 +422,7 @@ module Cohere
|
|
|
410
422
|
decision.generation_id,
|
|
411
423
|
resolved_options,
|
|
412
424
|
measurements,
|
|
425
|
+
lock: lock,
|
|
413
426
|
defer_word_alignment: resolved_options.alignment == "word"
|
|
414
427
|
)
|
|
415
428
|
rescue FatalRuntimeError, ProgressCallbackError
|
|
@@ -569,7 +582,7 @@ module Cohere
|
|
|
569
582
|
measurements.decode_seconds += prepared.decode_seconds
|
|
570
583
|
measurements.vad_seconds += prepared.vad_seconds
|
|
571
584
|
record_vad_measurements(measurements, prepared.vad_details)
|
|
572
|
-
Output::Publication.with_plan_lock(plan) do
|
|
585
|
+
Output::Publication.with_plan_lock(plan) do |lock|
|
|
573
586
|
decision = Output::Publication.revalidate(plan, resolved_options)
|
|
574
587
|
return skipped_result(entry, plan) if decision.action == :skip
|
|
575
588
|
|
|
@@ -587,11 +600,12 @@ module Cohere
|
|
|
587
600
|
decision.checkpoint,
|
|
588
601
|
decision.generation_id,
|
|
589
602
|
resolved_options,
|
|
590
|
-
measurements
|
|
603
|
+
measurements,
|
|
604
|
+
lock: lock
|
|
591
605
|
)
|
|
592
606
|
end
|
|
593
607
|
|
|
594
|
-
process_fresh_entry(prepared, resolved_options, measurements)
|
|
608
|
+
process_fresh_entry(prepared, resolved_options, measurements, lock: lock)
|
|
595
609
|
end
|
|
596
610
|
rescue FatalRuntimeError, ProgressCallbackError
|
|
597
611
|
raise
|
|
@@ -609,7 +623,7 @@ module Cohere
|
|
|
609
623
|
measurements.decode_seconds += prepared.decode_seconds
|
|
610
624
|
measurements.vad_seconds += prepared.vad_seconds
|
|
611
625
|
record_vad_measurements(measurements, prepared.vad_details)
|
|
612
|
-
outcome = Output::Publication.with_plan_lock(plan) do
|
|
626
|
+
outcome = Output::Publication.with_plan_lock(plan) do |lock|
|
|
613
627
|
decision = Output::Publication.revalidate(plan, resolved_options)
|
|
614
628
|
if decision.action == :skip
|
|
615
629
|
WordPipeline::Final.new(index: item.index, result: skipped_result(entry, plan))
|
|
@@ -635,6 +649,7 @@ module Cohere
|
|
|
635
649
|
decision.generation_id,
|
|
636
650
|
resolved_options,
|
|
637
651
|
measurements,
|
|
652
|
+
lock: lock,
|
|
638
653
|
defer_word_alignment: true
|
|
639
654
|
)
|
|
640
655
|
else
|
|
@@ -642,6 +657,7 @@ module Cohere
|
|
|
642
657
|
prepared,
|
|
643
658
|
resolved_options,
|
|
644
659
|
measurements,
|
|
660
|
+
lock: lock,
|
|
645
661
|
defer_word_alignment: true
|
|
646
662
|
)
|
|
647
663
|
end
|
|
@@ -661,7 +677,7 @@ module Cohere
|
|
|
661
677
|
def process_word_alignment(work, reload_result, resolved_options, measurements, alignment_state)
|
|
662
678
|
entry = work.entry
|
|
663
679
|
plan = work.plan
|
|
664
|
-
Output::Publication.with_plan_lock(plan) do
|
|
680
|
+
Output::Publication.with_plan_lock(plan) do |lock|
|
|
665
681
|
decision = Output::Publication.revalidate(plan, resolved_options)
|
|
666
682
|
return skipped_result(entry, plan) if decision.action == :skip
|
|
667
683
|
if (alignment_error = alignment_state.fetch(:error))
|
|
@@ -679,6 +695,7 @@ module Cohere
|
|
|
679
695
|
resolved_options,
|
|
680
696
|
measurements,
|
|
681
697
|
work.generation_id,
|
|
698
|
+
lock: lock,
|
|
682
699
|
asr_evicted: true
|
|
683
700
|
)
|
|
684
701
|
ensure_source_unchanged!(entry.path, work.source_snapshot)
|
|
@@ -710,7 +727,7 @@ module Cohere
|
|
|
710
727
|
record_alignment_measurements(measurements, before, aligner) if before && aligner
|
|
711
728
|
end
|
|
712
729
|
|
|
713
|
-
def process_fresh_entry(prepared, resolved_options, measurements, defer_word_alignment: false)
|
|
730
|
+
def process_fresh_entry(prepared, resolved_options, measurements, lock:, defer_word_alignment: false)
|
|
714
731
|
item = prepared.item
|
|
715
732
|
entry = item.entry
|
|
716
733
|
plan = item.plan
|
|
@@ -787,7 +804,8 @@ module Cohere
|
|
|
787
804
|
speech_spans: speech_spans,
|
|
788
805
|
vad_provider_options: vad_provider_options(vad_details),
|
|
789
806
|
directory_binding: plan.directory_bindings.last,
|
|
790
|
-
guard_bindings: plan.directory_bindings
|
|
807
|
+
guard_bindings: plan.directory_bindings,
|
|
808
|
+
lock: lock
|
|
791
809
|
)
|
|
792
810
|
ensure
|
|
793
811
|
measurements.checkpoint_seconds += monotonic - checkpoint_started
|
|
@@ -812,12 +830,13 @@ module Cohere
|
|
|
812
830
|
plan,
|
|
813
831
|
resolved_options,
|
|
814
832
|
measurements,
|
|
815
|
-
generation_id
|
|
833
|
+
generation_id,
|
|
834
|
+
lock: lock
|
|
816
835
|
)
|
|
817
836
|
end
|
|
818
837
|
|
|
819
838
|
def process_resumed_entry(prepared, checkpoint, generation_id, resolved_options, measurements,
|
|
820
|
-
defer_word_alignment: false)
|
|
839
|
+
lock:, defer_word_alignment: false)
|
|
821
840
|
item = prepared.item
|
|
822
841
|
entry = item.entry
|
|
823
842
|
plan = item.plan
|
|
@@ -866,7 +885,8 @@ module Cohere
|
|
|
866
885
|
plan,
|
|
867
886
|
resolved_options,
|
|
868
887
|
measurements,
|
|
869
|
-
generation_id
|
|
888
|
+
generation_id,
|
|
889
|
+
lock: lock
|
|
870
890
|
)
|
|
871
891
|
end
|
|
872
892
|
|
|
@@ -888,7 +908,7 @@ module Cohere
|
|
|
888
908
|
end
|
|
889
909
|
|
|
890
910
|
def render_and_publish(result, samples, segment_times, speech_spans, plan, resolved_options,
|
|
891
|
-
measurements, generation_id, asr_evicted: false)
|
|
911
|
+
measurements, generation_id, lock:, asr_evicted: false)
|
|
892
912
|
progressive_started = monotonic unless resolved_options.alignment == "word"
|
|
893
913
|
words, fallback_alignment_segments = words_for_segments(
|
|
894
914
|
samples,
|
|
@@ -922,7 +942,8 @@ module Cohere
|
|
|
922
942
|
result,
|
|
923
943
|
resolved_options,
|
|
924
944
|
generation_id: generation_id,
|
|
925
|
-
speech_spans: speech_spans
|
|
945
|
+
speech_spans: speech_spans,
|
|
946
|
+
lock: lock
|
|
926
947
|
)
|
|
927
948
|
if outputs.any?
|
|
928
949
|
result = result.with(
|
|
@@ -31,15 +31,85 @@ module Cohere
|
|
|
31
31
|
#
|
|
32
32
|
# A pipelined group receives at most half of the configured decoded-PCM
|
|
33
33
|
# budget (and never more than 512 MiB). Estimated decode sizes determine
|
|
34
|
-
# group membership and per-file ceilings.
|
|
35
|
-
#
|
|
34
|
+
# group membership and per-file ceilings. Unknown sizes receive an equal
|
|
35
|
+
# share initially. A ceiling failure first retries within the remaining
|
|
36
|
+
# configured PCM budget while successful group entries stay retained. If
|
|
37
|
+
# that is insufficient, later retained audio is released and only those
|
|
38
|
+
# entries are prepared again after the full-ceiling retry. A known file
|
|
39
|
+
# that cannot fit the group cap starts with that exclusive path.
|
|
36
40
|
# Native decoder implementation transients are outside this retained-PCM
|
|
37
41
|
# accounting, as they are in the Python path.
|
|
38
42
|
class Pipeline
|
|
39
43
|
include Enumerable
|
|
40
44
|
|
|
41
|
-
Group = Data.define(:items, :limits, :exclusive)
|
|
42
|
-
|
|
45
|
+
Group = Data.define(:items, :limits, :exclusive) do
|
|
46
|
+
def self.exclusive(item, limit)
|
|
47
|
+
new(items: [item].freeze, limits: [limit].freeze, exclusive: true)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Explicit cursor keeps metadata probes on the preparation caller's
|
|
52
|
+
# native stack while preserving lazy current/next-group discovery.
|
|
53
|
+
class GroupCursor
|
|
54
|
+
def initialize(items:, workers:, group_byte_limit:, memory_byte_limit:,
|
|
55
|
+
estimated_reservation:, unknown_reservation:, bounded_group:)
|
|
56
|
+
@items = items
|
|
57
|
+
@workers = workers
|
|
58
|
+
@group_byte_limit = group_byte_limit
|
|
59
|
+
@memory_byte_limit = memory_byte_limit
|
|
60
|
+
@estimated_reservation = estimated_reservation
|
|
61
|
+
@unknown_reservation = unknown_reservation
|
|
62
|
+
@bounded_group = bounded_group
|
|
63
|
+
@index = 0
|
|
64
|
+
@pending = nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def next
|
|
68
|
+
items = []
|
|
69
|
+
estimates = []
|
|
70
|
+
total = 0
|
|
71
|
+
while items.length < @workers
|
|
72
|
+
pair = take
|
|
73
|
+
break unless pair
|
|
74
|
+
|
|
75
|
+
item, estimate = pair
|
|
76
|
+
if estimate > @group_byte_limit
|
|
77
|
+
return Group.exclusive(item, @memory_byte_limit) if items.empty?
|
|
78
|
+
|
|
79
|
+
@pending = pair
|
|
80
|
+
break
|
|
81
|
+
end
|
|
82
|
+
if !items.empty? && total + estimate > @group_byte_limit
|
|
83
|
+
@pending = pair
|
|
84
|
+
break
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
items << item
|
|
88
|
+
estimates << estimate
|
|
89
|
+
total += estimate
|
|
90
|
+
end
|
|
91
|
+
raise StopIteration if items.empty?
|
|
92
|
+
|
|
93
|
+
@bounded_group.call(items, estimates)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def take
|
|
99
|
+
if @pending
|
|
100
|
+
pair = @pending
|
|
101
|
+
@pending = nil
|
|
102
|
+
return pair
|
|
103
|
+
end
|
|
104
|
+
return if @index >= @items.length
|
|
105
|
+
|
|
106
|
+
item = @items.fetch(@index)
|
|
107
|
+
@index += 1
|
|
108
|
+
estimate = @estimated_reservation.call(item) || @unknown_reservation
|
|
109
|
+
[item, estimate]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
private_constant :Group, :GroupCursor
|
|
43
113
|
|
|
44
114
|
class WorkerPool
|
|
45
115
|
STOP = Object.new.freeze
|
|
@@ -94,7 +164,7 @@ module Cohere
|
|
|
94
164
|
attr_reader :effective_workers, :group_byte_limit, :memory_byte_limit, :wait_seconds
|
|
95
165
|
|
|
96
166
|
def initialize(items, memory_byte_limit:, requested_workers:, enabled:, worker_limit: nil,
|
|
97
|
-
estimate_bytes: nil, &prepare)
|
|
167
|
+
estimate_bytes: nil, exclusive_retry: nil, retained_bytes: nil, &prepare)
|
|
98
168
|
raise ArgumentError, "prepare block is required" unless prepare
|
|
99
169
|
|
|
100
170
|
@items = items.to_a.freeze
|
|
@@ -103,6 +173,8 @@ module Cohere
|
|
|
103
173
|
|
|
104
174
|
@prepare = prepare
|
|
105
175
|
@estimate_bytes = estimate_bytes
|
|
176
|
+
@exclusive_retry = exclusive_retry
|
|
177
|
+
@retained_bytes = retained_bytes
|
|
106
178
|
@wait_seconds = 0.0
|
|
107
179
|
@enabled = enabled && @items.length > 1
|
|
108
180
|
@worker_limit = worker_limit.nil? ? nil : Integer(worker_limit)
|
|
@@ -131,14 +203,24 @@ module Cohere
|
|
|
131
203
|
|
|
132
204
|
groups = build_groups
|
|
133
205
|
pool = WorkerPool.new(@effective_workers, &@prepare)
|
|
134
|
-
|
|
135
|
-
|
|
206
|
+
group = groups.next
|
|
207
|
+
group_index = 0
|
|
208
|
+
pending = submit(group, group_index, pool)
|
|
209
|
+
loop do
|
|
136
210
|
prepared = resolve(pending)
|
|
137
211
|
pending = nil
|
|
138
|
-
next_group = groups
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
212
|
+
next_group = next_group(groups)
|
|
213
|
+
if retry_exclusively?(group, prepared)
|
|
214
|
+
consume_with_exclusive_retry(group, prepared, pool, &block)
|
|
215
|
+
else
|
|
216
|
+
pending = submit(next_group, group_index + 1, pool) if next_group && !group.exclusive && !next_group.exclusive
|
|
217
|
+
consume(prepared, &block)
|
|
218
|
+
end
|
|
219
|
+
break unless next_group
|
|
220
|
+
|
|
221
|
+
pending ||= submit(next_group, group_index + 1, pool)
|
|
222
|
+
group = next_group
|
|
223
|
+
group_index += 1
|
|
142
224
|
end
|
|
143
225
|
completed = true
|
|
144
226
|
ensure
|
|
@@ -176,7 +258,7 @@ module Cohere
|
|
|
176
258
|
def submit(group, group_index, pool)
|
|
177
259
|
Thread.new do
|
|
178
260
|
Thread.current.name = "cohere-audio-group-#{group_index}" if Thread.current.respond_to?(:name=)
|
|
179
|
-
|
|
261
|
+
pool.prepare(group)
|
|
180
262
|
end.tap { |thread| thread.report_on_exception = false }
|
|
181
263
|
end
|
|
182
264
|
|
|
@@ -187,36 +269,119 @@ module Cohere
|
|
|
187
269
|
@wait_seconds += monotonic - started if started
|
|
188
270
|
end
|
|
189
271
|
|
|
190
|
-
def
|
|
191
|
-
|
|
272
|
+
def next_group(groups)
|
|
273
|
+
groups.next
|
|
274
|
+
rescue StopIteration
|
|
275
|
+
nil
|
|
192
276
|
end
|
|
193
277
|
|
|
194
|
-
def
|
|
195
|
-
return
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
items = []
|
|
206
|
-
estimates = []
|
|
278
|
+
def retry_exclusively?(group, prepared)
|
|
279
|
+
return false if group.exclusive || !@exclusive_retry
|
|
280
|
+
|
|
281
|
+
prepared.any? { |entry| @exclusive_retry.call(entry) }
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def consume_with_exclusive_retry(group, prepared, pool, &block)
|
|
285
|
+
prepared.each_index do |index|
|
|
286
|
+
entry = prepared.fetch(index)
|
|
287
|
+
if entry.nil?
|
|
288
|
+
consume_single_retry(group.items.fetch(index), @memory_byte_limit, pool, &block)
|
|
207
289
|
next
|
|
208
290
|
end
|
|
291
|
+
unless @exclusive_retry.call(entry)
|
|
292
|
+
begin
|
|
293
|
+
block.call(entry)
|
|
294
|
+
ensure
|
|
295
|
+
prepared[index] = nil
|
|
296
|
+
end
|
|
297
|
+
next
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
prepared[index] = nil
|
|
301
|
+
failed_limit = group.limits.fetch(index)
|
|
302
|
+
if failed_limit >= @memory_byte_limit
|
|
303
|
+
block.call(entry)
|
|
304
|
+
next
|
|
305
|
+
end
|
|
306
|
+
unless @retained_bytes
|
|
307
|
+
release_retained_entries(prepared)
|
|
308
|
+
collect_released_audio
|
|
309
|
+
end
|
|
310
|
+
limit = available_retry_limit(prepared)
|
|
311
|
+
retried = prepare_single(group.items.fetch(index), limit, pool) if limit > failed_limit
|
|
312
|
+
full_retry = retried.nil? ||
|
|
313
|
+
(limit < @memory_byte_limit && @exclusive_retry.call(retried.fetch(0)))
|
|
314
|
+
if full_retry
|
|
315
|
+
retried&.clear
|
|
316
|
+
release_retained_entries(prepared)
|
|
317
|
+
collect_released_audio
|
|
318
|
+
retried = prepare_single(group.items.fetch(index), @memory_byte_limit, pool)
|
|
319
|
+
end
|
|
320
|
+
consume_single_result(retried, &block)
|
|
321
|
+
end
|
|
322
|
+
ensure
|
|
323
|
+
release(prepared)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def consume_single_retry(item, limit, pool, &)
|
|
327
|
+
consume_single_result(prepare_single(item, limit, pool), &)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def prepare_single(item, limit, pool)
|
|
331
|
+
pool.prepare(Group.exclusive(item, limit))
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def consume_single_result(retained)
|
|
335
|
+
yield retained.fetch(0)
|
|
336
|
+
ensure
|
|
337
|
+
retained&.clear
|
|
338
|
+
collect_released_audio
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def available_retry_limit(prepared)
|
|
342
|
+
return @memory_byte_limit unless @retained_bytes
|
|
343
|
+
|
|
344
|
+
retained = prepared.compact.sum { |entry| retained_bytes(entry) }
|
|
345
|
+
[@memory_byte_limit - retained, 0].max
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def release_retained_entries(prepared)
|
|
349
|
+
unless @retained_bytes
|
|
350
|
+
prepared.each_index do |index|
|
|
351
|
+
next unless prepared[index]
|
|
209
352
|
|
|
210
|
-
|
|
211
|
-
groups << bounded_group(items, estimates)
|
|
212
|
-
items = []
|
|
213
|
-
estimates = []
|
|
353
|
+
prepared[index] = nil
|
|
214
354
|
end
|
|
215
|
-
|
|
216
|
-
estimates << estimate
|
|
355
|
+
return
|
|
217
356
|
end
|
|
218
|
-
|
|
219
|
-
|
|
357
|
+
|
|
358
|
+
prepared.each_index do |index|
|
|
359
|
+
entry = prepared[index]
|
|
360
|
+
next unless entry && retained_bytes(entry).positive?
|
|
361
|
+
|
|
362
|
+
prepared[index] = nil
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def retained_bytes(entry)
|
|
367
|
+
bytes = Integer(@retained_bytes.call(entry))
|
|
368
|
+
raise ArgumentError, "retained_bytes must return a non-negative integer" if bytes.negative?
|
|
369
|
+
|
|
370
|
+
bytes
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def build_groups
|
|
374
|
+
return equal_groups.each unless @estimate_bytes
|
|
375
|
+
|
|
376
|
+
GroupCursor.new(
|
|
377
|
+
items: @items,
|
|
378
|
+
workers: @effective_workers,
|
|
379
|
+
group_byte_limit: @group_byte_limit,
|
|
380
|
+
memory_byte_limit: @memory_byte_limit,
|
|
381
|
+
estimated_reservation: method(:estimated_reservation),
|
|
382
|
+
unknown_reservation: unknown_reservation,
|
|
383
|
+
bounded_group: method(:bounded_group)
|
|
384
|
+
)
|
|
220
385
|
end
|
|
221
386
|
|
|
222
387
|
def equal_groups
|
|
@@ -238,6 +403,10 @@ module Cohere
|
|
|
238
403
|
nil
|
|
239
404
|
end
|
|
240
405
|
|
|
406
|
+
def unknown_reservation
|
|
407
|
+
[@group_byte_limit / @effective_workers, 1].max
|
|
408
|
+
end
|
|
409
|
+
|
|
241
410
|
def bounded_group(items, estimates)
|
|
242
411
|
limits = estimates.dup
|
|
243
412
|
remaining = @group_byte_limit - limits.sum
|
|
@@ -261,7 +430,15 @@ module Cohere
|
|
|
261
430
|
end
|
|
262
431
|
end
|
|
263
432
|
ensure
|
|
433
|
+
release(prepared)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def release(prepared)
|
|
264
437
|
prepared&.clear
|
|
438
|
+
collect_released_audio
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def collect_released_audio
|
|
265
442
|
GC.start(full_mark: false, immediate_mark: true, immediate_sweep: true)
|
|
266
443
|
end
|
|
267
444
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "monitor"
|
|
4
4
|
require "weakref"
|
|
5
5
|
|
|
6
|
+
require_relative "../internal/session_ownership"
|
|
7
|
+
|
|
6
8
|
module Cohere
|
|
7
9
|
module Transcribe
|
|
8
10
|
module Runtime
|
|
@@ -12,47 +14,6 @@ module Cohere
|
|
|
12
14
|
class ModelResources
|
|
13
15
|
OWNER_GUARD = Monitor.new
|
|
14
16
|
|
|
15
|
-
# The global owner record and the ObjectSpace finalizer retain this
|
|
16
|
-
# state, never the ModelResources instance itself. It also lets a new
|
|
17
|
-
# owner synchronously evict a collected predecessor before loading.
|
|
18
|
-
class SessionOwnership
|
|
19
|
-
def initialize
|
|
20
|
-
@mutex = Mutex.new
|
|
21
|
-
@session = nil
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def session
|
|
25
|
-
@mutex.synchronize { @session }
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def install(session)
|
|
29
|
-
@mutex.synchronize do
|
|
30
|
-
raise TranscriptionRuntimeError, "ASR session ownership is already installed" if @session
|
|
31
|
-
|
|
32
|
-
@session = session
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def close
|
|
37
|
-
session = @mutex.synchronize do
|
|
38
|
-
current = @session
|
|
39
|
-
@session = nil
|
|
40
|
-
current
|
|
41
|
-
end
|
|
42
|
-
return unless session
|
|
43
|
-
|
|
44
|
-
session.close
|
|
45
|
-
nil
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def finalize
|
|
49
|
-
close
|
|
50
|
-
rescue Exception # rubocop:disable Lint/RescueException -- finalizers must not escape during GC or shutdown
|
|
51
|
-
nil
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
private_constant :SessionOwnership
|
|
55
|
-
|
|
56
17
|
class << self
|
|
57
18
|
def evict_current_asr_owner
|
|
58
19
|
OWNER_GUARD.synchronize do
|
|
@@ -118,7 +79,9 @@ module Cohere
|
|
|
118
79
|
|
|
119
80
|
def initialize
|
|
120
81
|
@asr_key = nil
|
|
121
|
-
@asr_ownership = SessionOwnership.new
|
|
82
|
+
@asr_ownership = Internal::SessionOwnership.new(
|
|
83
|
+
installed_error: "ASR session ownership is already installed"
|
|
84
|
+
)
|
|
122
85
|
@batch_controller = nil
|
|
123
86
|
@closed = false
|
|
124
87
|
ObjectSpace.define_finalizer(
|
|
@@ -129,8 +92,8 @@ module Cohere
|
|
|
129
92
|
|
|
130
93
|
# Returns [session, loaded]. A key change evicts this instance's prior
|
|
131
94
|
# session, while acquisition also evicts a different process owner.
|
|
132
|
-
def acquire_asr(key)
|
|
133
|
-
raise ArgumentError, "an ASR loader block is required" unless
|
|
95
|
+
def acquire_asr(key, &loader)
|
|
96
|
+
raise ArgumentError, "an ASR loader block is required" unless loader
|
|
134
97
|
|
|
135
98
|
self.class::OWNER_GUARD.synchronize do
|
|
136
99
|
ensure_open!
|
|
@@ -139,36 +102,39 @@ module Cohere
|
|
|
139
102
|
|
|
140
103
|
owner = self.class.send(:current_owner_locked)
|
|
141
104
|
owner.send(:evict_asr_locked) if owner && !owner.equal?(self)
|
|
142
|
-
self.class.send(:claim_locked, self)
|
|
143
105
|
|
|
144
106
|
loaded = false
|
|
145
|
-
|
|
146
|
-
|
|
107
|
+
if @asr_ownership.session
|
|
108
|
+
self.class.send(:claim_locked, self)
|
|
109
|
+
else
|
|
110
|
+
completed = false
|
|
147
111
|
session = nil
|
|
148
112
|
begin
|
|
149
|
-
|
|
150
|
-
|
|
113
|
+
Thread.handle_interrupt(Object => :never) do
|
|
114
|
+
self.class.send(:claim_locked, self)
|
|
115
|
+
session = Thread.handle_interrupt(Object => :on_blocking, &loader)
|
|
116
|
+
raise TranscriptionRuntimeError, "ASR loader returned no native session" if session.nil?
|
|
151
117
|
|
|
152
|
-
Thread.handle_interrupt(Exception => :never) do
|
|
153
118
|
@asr_ownership.install(session)
|
|
154
119
|
@asr_key = owned_key
|
|
155
120
|
@batch_controller = nil
|
|
156
121
|
loaded = true
|
|
157
|
-
installed = true
|
|
158
122
|
end
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
123
|
+
completed = true
|
|
124
|
+
ensure
|
|
125
|
+
unless completed
|
|
126
|
+
Thread.handle_interrupt(Object => :never) do
|
|
127
|
+
if @asr_ownership.session.equal?(session)
|
|
128
|
+
evict_asr_locked
|
|
129
|
+
else
|
|
130
|
+
session&.close
|
|
131
|
+
end
|
|
132
|
+
rescue Exception # rubocop:disable Lint/RescueException -- preserve loader termination
|
|
133
|
+
nil
|
|
134
|
+
ensure
|
|
135
|
+
self.class.send(:release_locked, self)
|
|
165
136
|
end
|
|
166
|
-
rescue Exception # rubocop:disable Lint/RescueException -- preserve the loader failure
|
|
167
|
-
nil
|
|
168
137
|
end
|
|
169
|
-
raise
|
|
170
|
-
ensure
|
|
171
|
-
self.class.send(:release_locked, self) unless installed
|
|
172
138
|
end
|
|
173
139
|
end
|
|
174
140
|
[@asr_ownership.session, loaded].freeze
|
|
@@ -32,7 +32,7 @@ module Cohere
|
|
|
32
32
|
|
|
33
33
|
def write_asr_checkpoint(path:, result:, source_snapshot:, asr_contract_key:,
|
|
34
34
|
speech_spans:, vad_provider_options: nil, generation_id: nil,
|
|
35
|
-
directory_binding: nil, guard_bindings: nil)
|
|
35
|
+
directory_binding: nil, guard_bindings: nil, lock: nil)
|
|
36
36
|
generation_id = generation_id.to_s
|
|
37
37
|
generation_id = SecureRandom.hex(16) if generation_id.empty?
|
|
38
38
|
payload = asr_checkpoint_payload(
|
|
@@ -48,7 +48,8 @@ module Cohere
|
|
|
48
48
|
payload,
|
|
49
49
|
source_snapshot: source_snapshot,
|
|
50
50
|
directory_binding: directory_binding,
|
|
51
|
-
guard_bindings: guard_bindings
|
|
51
|
+
guard_bindings: guard_bindings,
|
|
52
|
+
commit_guard: lock&.method(:verify!)
|
|
52
53
|
)
|
|
53
54
|
generation_id.freeze
|
|
54
55
|
end
|