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
|
@@ -63,6 +63,7 @@ module Cohere
|
|
|
63
63
|
])
|
|
64
64
|
extern "void* sf_open(const char*, int, void*)"
|
|
65
65
|
extern "long long sf_readf_float(void*, void*, long long)"
|
|
66
|
+
extern "int sf_command(void*, int, void*, int)"
|
|
66
67
|
extern "int sf_close(void*)"
|
|
67
68
|
extern "const char* sf_strerror(void*)"
|
|
68
69
|
AVAILABLE = true
|
|
@@ -115,8 +116,70 @@ module Cohere
|
|
|
115
116
|
module_function
|
|
116
117
|
|
|
117
118
|
SFM_READ = 0x10
|
|
119
|
+
SFC_GET_CHANNEL_MAP_INFO = 0x1100
|
|
118
120
|
SRC_SINC_FASTEST = 2
|
|
119
121
|
BACKENDS = %w[auto ffmpeg torchcodec librosa libsndfile].freeze
|
|
122
|
+
SQRT_HALF = Math.sqrt(0.5)
|
|
123
|
+
DEFAULT_MONO_MIXES = {
|
|
124
|
+
1 => [1.0],
|
|
125
|
+
2 => [SQRT_HALF, SQRT_HALF],
|
|
126
|
+
3 => [SQRT_HALF, SQRT_HALF, 0.0], # FFmpeg's unspecified 3-channel default is 2.1, not 3.0
|
|
127
|
+
4 => [SQRT_HALF, SQRT_HALF, 1.0, 0.5],
|
|
128
|
+
5 => [SQRT_HALF, SQRT_HALF, 1.0, 0.5, 0.5],
|
|
129
|
+
6 => [SQRT_HALF, SQRT_HALF, 1.0, 0.0, 0.5, 0.5],
|
|
130
|
+
7 => [SQRT_HALF, SQRT_HALF, 1.0, 0.0, 0.5, 0.5, 0.5],
|
|
131
|
+
8 => [SQRT_HALF, SQRT_HALF, 1.0, 0.0, 0.5, 0.5, 0.5, 0.5],
|
|
132
|
+
10 => [SQRT_HALF, SQRT_HALF, 1.0, 0.0, 0.5, 0.5, SQRT_HALF, SQRT_HALF, 0.0, 0.0],
|
|
133
|
+
12 => [
|
|
134
|
+
SQRT_HALF, SQRT_HALF, 1.0, 0.0, 0.5, 0.5, 0.5, 0.5,
|
|
135
|
+
SQRT_HALF, SQRT_HALF, 0.0, 0.0
|
|
136
|
+
],
|
|
137
|
+
16 => [
|
|
138
|
+
SQRT_HALF, SQRT_HALF, 1.0, 0.5, 0.5, 0.5, 0.5, 0.5,
|
|
139
|
+
SQRT_HALF, 0.0, SQRT_HALF, 0.0, 0.0, 0.0, 0.0, 0.0
|
|
140
|
+
],
|
|
141
|
+
24 => [
|
|
142
|
+
SQRT_HALF, SQRT_HALF, 1.0, 0.0, 0.5, 0.5, SQRT_HALF, SQRT_HALF,
|
|
143
|
+
0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
|
144
|
+
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
|
|
145
|
+
]
|
|
146
|
+
}.transform_values(&:freeze).freeze
|
|
147
|
+
FFMPEG_7_DEFAULT_MONO_MIXES = {
|
|
148
|
+
14 => [
|
|
149
|
+
SQRT_HALF, SQRT_HALF, 1.0, 0.0, 0.5, 0.5, SQRT_HALF, SQRT_HALF,
|
|
150
|
+
0.5, 0.5, SQRT_HALF, SQRT_HALF, 0.0, 0.0
|
|
151
|
+
]
|
|
152
|
+
}.transform_values(&:freeze).freeze
|
|
153
|
+
FFMPEG_8_DEFAULT_MONO_MIXES = {
|
|
154
|
+
16 => [
|
|
155
|
+
SQRT_HALF, SQRT_HALF, 1.0, 0.0, 0.5, 0.5, SQRT_HALF, SQRT_HALF,
|
|
156
|
+
0.5, 0.5, SQRT_HALF, SQRT_HALF, 0.0, 0.0, 0.0, 0.0
|
|
157
|
+
]
|
|
158
|
+
}.transform_values(&:freeze).freeze
|
|
159
|
+
CHANNEL_POSITION_MONO_MIXES = {
|
|
160
|
+
1 => 1.0, # mono
|
|
161
|
+
2 => SQRT_HALF, # left
|
|
162
|
+
3 => SQRT_HALF, # right
|
|
163
|
+
4 => 1.0, # center
|
|
164
|
+
5 => SQRT_HALF, # front left
|
|
165
|
+
6 => SQRT_HALF, # front right
|
|
166
|
+
7 => 1.0, # front center
|
|
167
|
+
8 => 0.5, # rear center
|
|
168
|
+
9 => 0.5, # rear left
|
|
169
|
+
10 => 0.5, # rear right
|
|
170
|
+
11 => 0.0, # low-frequency effects
|
|
171
|
+
12 => SQRT_HALF, # front left of center
|
|
172
|
+
13 => SQRT_HALF, # front right of center
|
|
173
|
+
14 => 0.5, # side left
|
|
174
|
+
15 => 0.5, # side right
|
|
175
|
+
16 => 0.0, # top center
|
|
176
|
+
17 => SQRT_HALF, # top front left
|
|
177
|
+
18 => SQRT_HALF, # top front right
|
|
178
|
+
19 => 0.0, # top front center
|
|
179
|
+
20 => 0.0, # top rear left
|
|
180
|
+
21 => 0.0, # top rear right
|
|
181
|
+
22 => 0.0 # top rear center
|
|
182
|
+
}.freeze
|
|
120
183
|
|
|
121
184
|
# Best-effort metadata probe used for public skipped-result parity. It
|
|
122
185
|
# may inspect container headers and demuxer probe packets but never
|
|
@@ -131,8 +194,7 @@ module Cohere
|
|
|
131
194
|
end
|
|
132
195
|
return nil unless SoundFileABI::AVAILABLE
|
|
133
196
|
|
|
134
|
-
|
|
135
|
-
handle = SoundFileABI.sf_open(source.to_s, SFM_READ, info.to_ptr)
|
|
197
|
+
with_sound_file(source) do |handle, info|
|
|
136
198
|
return nil if handle.null?
|
|
137
199
|
|
|
138
200
|
frames = Integer(info.frames)
|
|
@@ -146,8 +208,6 @@ module Cohere
|
|
|
146
208
|
end
|
|
147
209
|
rescue Fiddle::DLError, SystemCallError, TranscriptionRuntimeError
|
|
148
210
|
nil
|
|
149
|
-
ensure
|
|
150
|
-
SoundFileABI.sf_close(handle) if defined?(handle) && handle && !handle.null?
|
|
151
211
|
end
|
|
152
212
|
|
|
153
213
|
# Best-effort upper bound for the buffers governed by max_decoded_bytes.
|
|
@@ -161,10 +221,7 @@ module Cohere
|
|
|
161
221
|
source = Pathname(path).expand_path
|
|
162
222
|
return unless source.file?
|
|
163
223
|
|
|
164
|
-
|
|
165
|
-
use_ffmpeg = requested == "ffmpeg" || requested == "torchcodec" ||
|
|
166
|
-
(%w[auto librosa].include?(requested) && native_ffmpeg_available)
|
|
167
|
-
if use_ffmpeg
|
|
224
|
+
if ffmpeg_backend?(requested)
|
|
168
225
|
duration = FFmpegNative.duration(source)
|
|
169
226
|
return unless duration&.finite? && duration >= 0.0
|
|
170
227
|
|
|
@@ -172,8 +229,7 @@ module Cohere
|
|
|
172
229
|
end
|
|
173
230
|
return unless SoundFileABI::AVAILABLE
|
|
174
231
|
|
|
175
|
-
|
|
176
|
-
handle = SoundFileABI.sf_open(source.to_s, SFM_READ, info.to_ptr)
|
|
232
|
+
with_sound_file(source) do |handle, info|
|
|
177
233
|
next if handle.null?
|
|
178
234
|
|
|
179
235
|
frames = Integer(info.frames)
|
|
@@ -181,14 +237,10 @@ module Cohere
|
|
|
181
237
|
source_rate = Integer(info.samplerate)
|
|
182
238
|
next unless frames >= 0 && channels.positive? && source_rate.positive?
|
|
183
239
|
|
|
184
|
-
|
|
185
|
-
output_frames = (frames * sample_rate.fdiv(source_rate)).ceil + 64
|
|
186
|
-
[input_bytes, output_frames * Fiddle::SIZEOF_FLOAT].max
|
|
240
|
+
projected_decoded_bytes(frames, channels, source_rate, sample_rate)
|
|
187
241
|
end
|
|
188
242
|
rescue Fiddle::DLError, SystemCallError, TranscriptionRuntimeError
|
|
189
243
|
nil
|
|
190
|
-
ensure
|
|
191
|
-
SoundFileABI.sf_close(handle) if defined?(handle) && handle && !handle.null?
|
|
192
244
|
end
|
|
193
245
|
|
|
194
246
|
def decode(path, backend: "auto", sample_rate: SAMPLE_RATE, max_decoded_bytes: 4 * (1024**3))
|
|
@@ -205,10 +257,7 @@ module Cohere
|
|
|
205
257
|
raise TranscriptionInputError, "Input does not exist: #{source}" unless source.exist?
|
|
206
258
|
raise TranscriptionInputError, "Input is not a regular file: #{source}" unless source.file?
|
|
207
259
|
|
|
208
|
-
|
|
209
|
-
use_ffmpeg = requested == "ffmpeg" || requested == "torchcodec" ||
|
|
210
|
-
(%w[auto librosa].include?(requested) && native_ffmpeg_available)
|
|
211
|
-
if use_ffmpeg
|
|
260
|
+
if ffmpeg_backend?(requested)
|
|
212
261
|
samples = FFmpegNative.decode(
|
|
213
262
|
source,
|
|
214
263
|
sample_rate: sample_rate,
|
|
@@ -239,67 +288,54 @@ module Cohere
|
|
|
239
288
|
raise TranscriptionRuntimeError, "libsndfile is required for native audio decoding: #{sound_file_error}"
|
|
240
289
|
end
|
|
241
290
|
|
|
242
|
-
|
|
243
|
-
handle = SoundFileABI.sf_open(source.to_s, SFM_READ, info.to_ptr)
|
|
291
|
+
with_sound_file(source) do |handle, info|
|
|
244
292
|
raise TranscriptionRuntimeError, "Cannot decode #{source}: #{SoundFileABI.sf_strerror(handle)}" if handle.null?
|
|
245
293
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
end
|
|
294
|
+
frames = Integer(info.frames)
|
|
295
|
+
channels = Integer(info.channels)
|
|
296
|
+
source_rate = Integer(info.samplerate)
|
|
297
|
+
unless frames >= 0 && channels.positive? && source_rate.positive?
|
|
298
|
+
raise TranscriptionRuntimeError, "Decoder returned invalid audio metadata for #{source}"
|
|
299
|
+
end
|
|
253
300
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
end
|
|
301
|
+
input_bytes = frames * channels * Fiddle::SIZEOF_FLOAT
|
|
302
|
+
projected_bytes = projected_decoded_bytes(frames, channels, source_rate, sample_rate)
|
|
303
|
+
if max_decoded_bytes && projected_bytes > max_decoded_bytes
|
|
304
|
+
raise DecodedAudioLimitError,
|
|
305
|
+
"Decoded audio exceeds the configured memory limit for #{source} " \
|
|
306
|
+
"(#{projected_bytes} > #{max_decoded_bytes} bytes)"
|
|
307
|
+
end
|
|
262
308
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
309
|
+
raw = Fiddle::Pointer.malloc([input_bytes, 1].max, Fiddle::RUBY_FREE)
|
|
310
|
+
channel_map = sound_file_channel_map(handle, channels)
|
|
311
|
+
read_frames = SoundFileABI.sf_readf_float(handle, raw, frames)
|
|
312
|
+
raise TranscriptionRuntimeError, "Cannot decode #{source}: #{SoundFileABI.sf_strerror(handle)}" if read_frames.negative?
|
|
313
|
+
raise TranscriptionRuntimeError, "Decoder returned more frames than allocated for #{source}" if read_frames > frames
|
|
267
314
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
end
|
|
274
|
-
interleaved = if frames.zero?
|
|
275
|
-
nil
|
|
276
|
-
else
|
|
277
|
-
Numo::SFloat.from_binary(raw[0, frames * channels * Fiddle::SIZEOF_FLOAT])
|
|
278
|
-
end
|
|
279
|
-
mono = if frames.zero?
|
|
280
|
-
Numo::SFloat.zeros(0)
|
|
281
|
-
elsif channels == 1
|
|
282
|
-
interleaved
|
|
283
|
-
elsif channels == 2
|
|
284
|
-
(interleaved.reshape(frames, channels).sum(1) * Math.sqrt(0.5)).cast_to(Numo::SFloat)
|
|
285
|
-
else
|
|
286
|
-
interleaved.reshape(frames, channels).mean(1).cast_to(Numo::SFloat)
|
|
287
|
-
end
|
|
288
|
-
samples = source_rate == sample_rate ? mono : resample(mono, source_rate, sample_rate, max_decoded_bytes)
|
|
289
|
-
validate_finite!(samples)
|
|
290
|
-
Decoded.new(
|
|
291
|
-
samples: samples.freeze,
|
|
292
|
-
sample_rate: sample_rate,
|
|
293
|
-
backend: "libsndfile",
|
|
294
|
-
fallback_reason: if %w[auto libsndfile].include?(requested)
|
|
295
|
-
nil
|
|
296
|
-
else
|
|
297
|
-
"Ruby #{requested} compatibility mode uses the native libsndfile ABI"
|
|
298
|
-
end
|
|
299
|
-
)
|
|
300
|
-
ensure
|
|
301
|
-
SoundFileABI.sf_close(handle)
|
|
315
|
+
frames = read_frames
|
|
316
|
+
begin
|
|
317
|
+
require "numo/narray"
|
|
318
|
+
rescue LoadError => e
|
|
319
|
+
raise TranscriptionRuntimeError, "numo-narray is required for decoded audio: #{e.message}"
|
|
302
320
|
end
|
|
321
|
+
interleaved = if frames.zero?
|
|
322
|
+
nil
|
|
323
|
+
else
|
|
324
|
+
Numo::SFloat.from_binary(raw[0, frames * channels * Fiddle::SIZEOF_FLOAT])
|
|
325
|
+
end
|
|
326
|
+
mono = downmix(interleaved, frames, channels, channel_map)
|
|
327
|
+
samples = source_rate == sample_rate ? mono : resample(mono, source_rate, sample_rate, max_decoded_bytes)
|
|
328
|
+
validate_finite!(samples)
|
|
329
|
+
Decoded.new(
|
|
330
|
+
samples: samples.freeze,
|
|
331
|
+
sample_rate: sample_rate,
|
|
332
|
+
backend: "libsndfile",
|
|
333
|
+
fallback_reason: if %w[auto libsndfile].include?(requested)
|
|
334
|
+
nil
|
|
335
|
+
else
|
|
336
|
+
"Ruby #{requested} compatibility mode uses the native libsndfile ABI"
|
|
337
|
+
end
|
|
338
|
+
)
|
|
303
339
|
end
|
|
304
340
|
end
|
|
305
341
|
|
|
@@ -318,7 +354,7 @@ module Cohere
|
|
|
318
354
|
output_capacity = (samples.length * ratio).ceil + 64
|
|
319
355
|
bytes = output_capacity * Fiddle::SIZEOF_FLOAT
|
|
320
356
|
if max_decoded_bytes && bytes > max_decoded_bytes
|
|
321
|
-
raise
|
|
357
|
+
raise DecodedAudioLimitError, "Resampled audio exceeds the configured memory limit"
|
|
322
358
|
end
|
|
323
359
|
|
|
324
360
|
input_string = samples.to_binary
|
|
@@ -357,6 +393,79 @@ module Cohere
|
|
|
357
393
|
samples
|
|
358
394
|
end
|
|
359
395
|
private_class_method :validate_finite!
|
|
396
|
+
|
|
397
|
+
def ffmpeg_backend?(requested)
|
|
398
|
+
return true if %w[ffmpeg torchcodec].include?(requested)
|
|
399
|
+
|
|
400
|
+
%w[auto librosa].include?(requested) && FFmpegNative.available?
|
|
401
|
+
end
|
|
402
|
+
private_class_method :ffmpeg_backend?
|
|
403
|
+
|
|
404
|
+
def projected_decoded_bytes(frames, channels, source_rate, target_rate)
|
|
405
|
+
input_bytes = frames * channels * Fiddle::SIZEOF_FLOAT
|
|
406
|
+
output_frames = (frames * target_rate.fdiv(source_rate)).ceil + 64
|
|
407
|
+
[input_bytes, output_frames * Fiddle::SIZEOF_FLOAT].max
|
|
408
|
+
end
|
|
409
|
+
private_class_method :projected_decoded_bytes
|
|
410
|
+
|
|
411
|
+
def with_sound_file(source)
|
|
412
|
+
SoundFileABI::SFInfo.malloc(Fiddle::RUBY_FREE) do |info|
|
|
413
|
+
Thread.handle_interrupt(Object => :never) do
|
|
414
|
+
handle = SoundFileABI.sf_open(source.to_s, SFM_READ, info.to_ptr)
|
|
415
|
+
begin
|
|
416
|
+
Thread.handle_interrupt(Object => :immediate) { yield handle, info }
|
|
417
|
+
ensure
|
|
418
|
+
SoundFileABI.sf_close(handle) unless handle.null?
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
private_class_method :with_sound_file
|
|
424
|
+
|
|
425
|
+
# libswresample's default mono matrix. Explicit libsndfile positions
|
|
426
|
+
# preserve height-channel layouts, while the numeric fallbacks mirror
|
|
427
|
+
# the first standard layout selected by av_channel_layout_default.
|
|
428
|
+
def downmix(interleaved, frames, channels, channel_map)
|
|
429
|
+
return Numo::SFloat.zeros(0) if frames.zero?
|
|
430
|
+
return interleaved if channels == 1
|
|
431
|
+
|
|
432
|
+
matrix = interleaved.reshape(frames, channels)
|
|
433
|
+
weights = channel_map&.map { |position| CHANNEL_POSITION_MONO_MIXES[position] }
|
|
434
|
+
weights = nil if weights&.any?(&:nil?)
|
|
435
|
+
weights ||= default_mono_mix(channels)
|
|
436
|
+
return matrix.mean(1).cast_to(Numo::SFloat) unless weights
|
|
437
|
+
|
|
438
|
+
matrix.dot(Numo::SFloat[*weights]).cast_to(Numo::SFloat)
|
|
439
|
+
end
|
|
440
|
+
private_class_method :downmix
|
|
441
|
+
|
|
442
|
+
def default_mono_mix(channels)
|
|
443
|
+
case channels
|
|
444
|
+
when 14, 16
|
|
445
|
+
avutil_major = FFmpegNative.avutil_major
|
|
446
|
+
if avutil_major && avutil_major >= 60 && (weights = FFMPEG_8_DEFAULT_MONO_MIXES[channels])
|
|
447
|
+
return weights
|
|
448
|
+
end
|
|
449
|
+
if avutil_major && avutil_major >= 59 && (weights = FFMPEG_7_DEFAULT_MONO_MIXES[channels])
|
|
450
|
+
return weights
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
DEFAULT_MONO_MIXES[channels]
|
|
455
|
+
end
|
|
456
|
+
private_class_method :default_mono_mix
|
|
457
|
+
|
|
458
|
+
def sound_file_channel_map(handle, channels)
|
|
459
|
+
buffer = Array.new(channels, 0).pack("i!*")
|
|
460
|
+
available = SoundFileABI.sf_command(
|
|
461
|
+
handle,
|
|
462
|
+
SFC_GET_CHANNEL_MAP_INFO,
|
|
463
|
+
Fiddle::Pointer[buffer],
|
|
464
|
+
buffer.bytesize
|
|
465
|
+
)
|
|
466
|
+
available == 1 ? buffer.unpack("i!*") : nil
|
|
467
|
+
end
|
|
468
|
+
private_class_method :sound_file_channel_map
|
|
360
469
|
end
|
|
361
470
|
end
|
|
362
471
|
end
|
|
@@ -4,16 +4,21 @@ require "fiddle"
|
|
|
4
4
|
require "rbconfig"
|
|
5
5
|
|
|
6
6
|
require_relative "../errors"
|
|
7
|
+
require_relative "../internal/interruptible_native_call"
|
|
7
8
|
|
|
8
9
|
module Cohere
|
|
9
10
|
module Transcribe
|
|
10
11
|
module Audio
|
|
12
|
+
class DecodedAudioLimitError < TranscriptionRuntimeError; end
|
|
13
|
+
|
|
11
14
|
# Lazy binding for the gem's subprocess-free libav decoder adapter. The
|
|
12
15
|
# adapter itself dynamically selects a compatible FFmpeg 4-8 runtime;
|
|
13
16
|
# Ruby never launches ffmpeg and does not retain a Python codec runtime.
|
|
14
17
|
module FFmpegNative
|
|
15
18
|
ERROR_CAPACITY = 1_024
|
|
16
19
|
CANCELLED_STATUS = 6
|
|
20
|
+
DECODED_AUDIO_LIMIT_STATUS = 4
|
|
21
|
+
CANCELLATION_JOIN_INTERVAL = 0.01
|
|
17
22
|
|
|
18
23
|
class Library
|
|
19
24
|
FUNCTIONS = {
|
|
@@ -24,6 +29,10 @@ module Cohere
|
|
|
24
29
|
free: [[:voidp], :void]
|
|
25
30
|
}.freeze
|
|
26
31
|
|
|
32
|
+
OPTIONAL_FUNCTIONS = {
|
|
33
|
+
versions: [%i[voidp size_t], :int]
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
27
36
|
TYPE_MAP = {
|
|
28
37
|
void: Fiddle::TYPE_VOID,
|
|
29
38
|
voidp: Fiddle::TYPE_VOIDP,
|
|
@@ -35,6 +44,9 @@ module Cohere
|
|
|
35
44
|
class << self
|
|
36
45
|
def load
|
|
37
46
|
@mutex ||= Mutex.new
|
|
47
|
+
# A failed load is deliberately retryable. Embedders may set an
|
|
48
|
+
# adapter override or make a packaged/system library available
|
|
49
|
+
# after an earlier best-effort audio probe.
|
|
38
50
|
@mutex.synchronize { @instance ||= load_uncached }
|
|
39
51
|
end
|
|
40
52
|
|
|
@@ -84,20 +96,20 @@ module Cohere
|
|
|
84
96
|
end
|
|
85
97
|
end
|
|
86
98
|
|
|
87
|
-
attr_reader :path, :diagnostic
|
|
99
|
+
attr_reader :path, :diagnostic, :ffmpeg_versions, :avutil_major
|
|
88
100
|
|
|
89
101
|
def initialize(path)
|
|
90
102
|
@path = path
|
|
91
103
|
@handle = Fiddle::Handle.new(path, Fiddle::RTLD_NOW)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
functions = FUNCTIONS.to_h do |name, signature|
|
|
105
|
+
[name, bind_function(name, signature)]
|
|
106
|
+
end
|
|
107
|
+
OPTIONAL_FUNCTIONS.each do |name, signature|
|
|
108
|
+
functions[name] = bind_function(name, signature)
|
|
109
|
+
rescue Fiddle::DLError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
@functions = functions.freeze
|
|
101
113
|
end
|
|
102
114
|
|
|
103
115
|
def probe!
|
|
@@ -105,31 +117,41 @@ module Cohere
|
|
|
105
117
|
message[0, ERROR_CAPACITY] = "\0" * ERROR_CAPACITY
|
|
106
118
|
status = @functions.fetch(:probe).call(message, ERROR_CAPACITY)
|
|
107
119
|
@diagnostic = message.to_s.force_encoding(Encoding::UTF_8).scrub.freeze
|
|
108
|
-
|
|
120
|
+
raise TranscriptionRuntimeError, "Native FFmpeg libraries are unavailable: #{@diagnostic}" unless status.zero?
|
|
109
121
|
|
|
110
|
-
|
|
122
|
+
versions = @functions[:versions]
|
|
123
|
+
return clear_ffmpeg_versions unless versions
|
|
124
|
+
|
|
125
|
+
tuple_bytes = 4 * Fiddle::SIZEOF_INT
|
|
126
|
+
tuple = Fiddle::Pointer.malloc(tuple_bytes, Fiddle::RUBY_FREE)
|
|
127
|
+
tuple[0, tuple_bytes] = [0, 0, 0, 0].pack("i!*")
|
|
128
|
+
version_status = versions.call(tuple, 4)
|
|
129
|
+
unless version_status.zero?
|
|
130
|
+
raise TranscriptionRuntimeError,
|
|
131
|
+
"Native FFmpeg libraries did not report their version tuple: #{@diagnostic}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@ffmpeg_versions = tuple[0, tuple_bytes].unpack("i!4").freeze
|
|
135
|
+
unless @ffmpeg_versions.all?(&:positive?)
|
|
136
|
+
raise TranscriptionRuntimeError,
|
|
137
|
+
"Native FFmpeg libraries reported an invalid version tuple: #{@ffmpeg_versions.inspect}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
@avutil_major = @ffmpeg_versions.fetch(2)
|
|
141
|
+
self
|
|
111
142
|
end
|
|
112
143
|
|
|
113
144
|
def decode(path, sample_rate:, max_decoded_bytes:)
|
|
114
|
-
|
|
145
|
+
Internal::InterruptibleNativeCall.run(
|
|
146
|
+
cancel: -> { @functions.fetch(:cancel).call },
|
|
147
|
+
join_interval: CANCELLATION_JOIN_INTERVAL,
|
|
148
|
+
missing_outcome: TranscriptionRuntimeError.new(
|
|
149
|
+
"Native FFmpeg decode worker exited without reporting an outcome"
|
|
150
|
+
),
|
|
151
|
+
thread_name: "cohere-ffmpeg-decode"
|
|
152
|
+
) do
|
|
115
153
|
decode_owned(path, sample_rate: sample_rate, max_decoded_bytes: max_decoded_bytes)
|
|
116
154
|
end
|
|
117
|
-
worker.name = "cohere-ffmpeg-decode" if worker.respond_to?(:name=)
|
|
118
|
-
worker.report_on_exception = false
|
|
119
|
-
completed = false
|
|
120
|
-
result = worker.value
|
|
121
|
-
completed = true
|
|
122
|
-
result
|
|
123
|
-
ensure
|
|
124
|
-
if defined?(worker) && worker && !completed
|
|
125
|
-
Thread.handle_interrupt(Exception => :never) do
|
|
126
|
-
@functions.fetch(:cancel).call if worker.alive?
|
|
127
|
-
rescue StandardError
|
|
128
|
-
nil
|
|
129
|
-
ensure
|
|
130
|
-
worker.join
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
155
|
end
|
|
134
156
|
|
|
135
157
|
def decode_owned(path, sample_rate:, max_decoded_bytes:)
|
|
@@ -142,7 +164,7 @@ module Cohere
|
|
|
142
164
|
maximum = max_decoded_bytes || 0
|
|
143
165
|
|
|
144
166
|
address = 0
|
|
145
|
-
Thread.handle_interrupt(
|
|
167
|
+
Thread.handle_interrupt(Object => :never) do
|
|
146
168
|
status = @functions.fetch(:decode).call(
|
|
147
169
|
c_string(path.to_s),
|
|
148
170
|
sample_rate,
|
|
@@ -154,11 +176,14 @@ module Cohere
|
|
|
154
176
|
)
|
|
155
177
|
address = output_slot[0, Fiddle::SIZEOF_VOIDP].unpack1("J")
|
|
156
178
|
begin
|
|
157
|
-
Thread.handle_interrupt(
|
|
179
|
+
Thread.handle_interrupt(Object => :immediate) do
|
|
158
180
|
count = count_slot[0, Fiddle::SIZEOF_INT64_T].unpack1("q")
|
|
159
181
|
detail = message.to_s.force_encoding(Encoding::UTF_8).scrub
|
|
160
182
|
unless status.zero?
|
|
161
183
|
raise Interrupt, "Native FFmpeg decode was cancelled for #{path}: #{detail}" if status == CANCELLED_STATUS
|
|
184
|
+
if status == DECODED_AUDIO_LIMIT_STATUS
|
|
185
|
+
raise DecodedAudioLimitError, "Cannot decode #{path} through native FFmpeg: #{detail}"
|
|
186
|
+
end
|
|
162
187
|
|
|
163
188
|
raise TranscriptionRuntimeError, "Cannot decode #{path} through native FFmpeg: #{detail}"
|
|
164
189
|
end
|
|
@@ -168,7 +193,7 @@ module Cohere
|
|
|
168
193
|
|
|
169
194
|
bytes = count * Fiddle::SIZEOF_FLOAT
|
|
170
195
|
if max_decoded_bytes && bytes > max_decoded_bytes
|
|
171
|
-
raise
|
|
196
|
+
raise DecodedAudioLimitError,
|
|
172
197
|
"Native FFmpeg exceeded the configured decoded-audio memory limit for #{path}"
|
|
173
198
|
end
|
|
174
199
|
|
|
@@ -223,6 +248,21 @@ module Cohere
|
|
|
223
248
|
|
|
224
249
|
private :decode_owned
|
|
225
250
|
|
|
251
|
+
def bind_function(name, (arguments, result))
|
|
252
|
+
address = @handle["cohere_audio_ffmpeg_#{name}"]
|
|
253
|
+
Fiddle::Function.new(
|
|
254
|
+
address,
|
|
255
|
+
arguments.map { |type| TYPE_MAP.fetch(type) },
|
|
256
|
+
TYPE_MAP.fetch(result)
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def clear_ffmpeg_versions
|
|
261
|
+
@ffmpeg_versions = nil
|
|
262
|
+
@avutil_major = nil
|
|
263
|
+
self
|
|
264
|
+
end
|
|
265
|
+
|
|
226
266
|
def c_string(value)
|
|
227
267
|
Fiddle::Pointer["#{value}\0"]
|
|
228
268
|
end
|
|
@@ -247,6 +287,18 @@ module Cohere
|
|
|
247
287
|
e.message
|
|
248
288
|
end
|
|
249
289
|
|
|
290
|
+
def avutil_major
|
|
291
|
+
library.avutil_major
|
|
292
|
+
rescue TranscriptionRuntimeError
|
|
293
|
+
nil
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def ffmpeg_versions
|
|
297
|
+
library.ffmpeg_versions
|
|
298
|
+
rescue TranscriptionRuntimeError
|
|
299
|
+
nil
|
|
300
|
+
end
|
|
301
|
+
|
|
250
302
|
def decode(path, sample_rate:, max_decoded_bytes:)
|
|
251
303
|
library.decode(path, sample_rate: sample_rate, max_decoded_bytes: max_decoded_bytes)
|
|
252
304
|
end
|
|
@@ -33,6 +33,7 @@ module Cohere
|
|
|
33
33
|
start_time = Float(raw_start)
|
|
34
34
|
end_time = Float(raw_end)
|
|
35
35
|
raise ArgumentError, "Segment #{index} has non-finite bounds" unless start_time.finite? && end_time.finite?
|
|
36
|
+
raise ArgumentError, "Segment #{index} has inverted bounds" if end_time < start_time
|
|
36
37
|
if start_time < -tolerance || end_time > duration + tolerance
|
|
37
38
|
raise ArgumentError,
|
|
38
39
|
format("Segment %<index>d lies outside the audio: %<start>.6f..%<end>.6f for %<duration>.6fs",
|