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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edcc763ef9c3c66e41fc508fb3c45fca037623e3fc3be03529c553b78f488a29
|
|
4
|
+
data.tar.gz: 675bf23b2dd0c1966a434e9e078ca45bcdc86154ae04c03fce72312801eb8f52
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 627b836b339dde2265e72f8adc73605052eada1a030b24f97b48720b1088b8139905b857a7a796da7e6b608903cc42c05bb50daa3db4b42537057820fc810e8d
|
|
7
|
+
data.tar.gz: 3f310c45da7aecfa19952b329b6b52bf878e4772491d234029b4a822b2ed79c3efefb6c403b6474f42038c9516e84312c46b59286fa1598bde8b0a4363515654
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.3] - 2026-07-18
|
|
4
|
+
|
|
5
|
+
- Close every libsndfile handle, directory descriptor, decoded buffer, and native session exactly once, defer thread termination through ownership handoffs and durable commit boundaries, cancel and join dedicated native workers before their callers terminate, reject empty native inference rows locally, and consolidate detached session ownership without retaining wrapper objects.
|
|
6
|
+
- Match native FFmpeg mono mixing for standard multichannel and height-channel layouts across supported FFmpeg generations, obtain the FFmpeg version tuple through an optional native ABI symbol while retaining compatible older audio adapters, classify decoded-audio ceiling failures explicitly, prepare unknown sizes concurrently, retain successful grouped decodes across useful ceiling retries, release retained PCM before a full-ceiling retry, and keep lazy metadata probes on the caller's native stack.
|
|
7
|
+
- Restore warm-cache Hub operation for temporary connection, rate-limit, server, malformed-JSON, and missing-or-invalid-commit responses while retaining online branch revalidation, bounded short-lived memoization, independent concurrent refreshes, bounded standard bare-SHA refs published atomically as best-effort hints, role-specific shared-cache modes for directories, immutable downloads, replaceable refs, and download locks, readable-lock fallback with clear remediation for inaccessible older locks, tolerant mode updates, repository-file listings that reject an empty or wholly malformed result, consistent public transient errors, and definitive authentication or missing-repository errors.
|
|
8
|
+
- Reject invalid byte paths before transcription or publication, share byte-to-UTF-8 normalization across public surfaces, treat publication-parent changes during read-only verification as reprocessing decisions, reject inverted segment bounds, and coordinate output publication through output-adjacent locks that remain held across rescue contexts, propagate release failures, support shared directories and verified read-only skips, reject unowned multi-user-writable registries without a sticky bit, revalidate the authoritative lock at checkpoint and output commit boundaries, unify state and output recovery across exceptions, nonlocal exits, and thread termination, report incomplete recovery for ordinary failures and nonlocal exits with retained-backup paths and preserved causes, let pending thread termination remain terminal at every recovery stage, tolerate disappearance of the released 0.1.2 compatibility lock only while the canonical lock remains held, reject replacement even inside rescue contexts, and remove that compatibility path at the final 0.2.0 release while retaining it through prereleases.
|
|
9
|
+
- Combine full-storage tensor checksum validation with conversion, simplify ZIP64 locator and checksum bookkeeping, preserve the discovered CUDA toolkit runtime path, and exclude untracked native build artifacts from locally built gems.
|
|
10
|
+
- Match the pinned Python word-aligner geometry for sub-30-second audio, require exact command-line enum values, and use the public `OUTPUT_FORMATS` constant as the single accepted-format source for types and the CLI.
|
|
11
|
+
|
|
3
12
|
## [0.1.2] - 2026-07-16
|
|
4
13
|
|
|
5
14
|
- Correct macOS creation-mode delivery for descriptor-relative staged files and turn concurrent output replacement during preflight into a normal reprocessing decision.
|
data/README.md
CHANGED
|
@@ -18,7 +18,7 @@ The native extension builds a portable CPU backend by default. Accelerator avail
|
|
|
18
18
|
|
|
19
19
|
Dense conversion and inference support F16, BF16, and F32 GGUF weights for the pinned checkpoint and compatible Dense fine-tunes. CPU execution resolves to FP32. CUDA `auto` selects BF16 when the loaded runtime reports hardware support and otherwise selects FP16; MPS `auto` selects FP16. An explicit unsupported BF16 accelerator request fails instead of silently changing precision.
|
|
20
20
|
|
|
21
|
-
Audio decoding never launches the `ffmpeg` executable. The packaged `libcohere_audio` adapter dynamically binds a compatible libavformat/libavcodec/libavutil/libswresample tuple and decodes the first audio stream directly to mono 16 kHz float PCM. This path covers AAC, AIFF, ALAC, FLAC, M4A, MP3, MP4, Ogg/Vorbis, Opus, WAV, WebM, and WMA. Explicit `torchcodec` and `librosa` compatibility modes use the same FFmpeg codec runtime through that C ABI, preserving the complete accepted-format set while reporting the concrete decoder in result provenance. If the adapter is absent, `auto` and `librosa` can fall back to libsndfile/libsamplerate for the formats those libraries support.
|
|
21
|
+
Audio decoding never launches the `ffmpeg` executable. The packaged `libcohere_audio` adapter dynamically binds a compatible libavformat/libavcodec/libavutil/libswresample tuple and decodes the first audio stream directly to mono 16 kHz float PCM. This path covers AAC, AIFF, ALAC, FLAC, M4A, MP3, MP4, Ogg/Vorbis, Opus, WAV, WebM, and WMA. Explicit `torchcodec` and `librosa` compatibility modes use the same FFmpeg codec runtime through that C ABI, preserving the complete accepted-format set while reporting the concrete decoder in result provenance. If the adapter is absent, `auto` and `librosa` can fall back to libsndfile/libsamplerate for the formats those libraries support. Audio adapters built before version metadata was added remain loadable; without that metadata the Ruby wrapper uses the older channel-layout table, which is identical for common inputs with eight or fewer channels but can differ for unusual 14- or 16-channel layouts.
|
|
22
22
|
|
|
23
23
|
The FFmpeg adapter gives duration probes and full decodes monotonic deadlines (30 seconds and one hour, respectively) and shares a generation-based cooperative cancellation hook. It supplies FFmpeg's public `AVIOInterruptCB` to input I/O and also checks cancellation between codec and resampler calls. Cancellation therefore takes effect when FFmpeg invokes that callback or returns control; it does not forcibly unwind an uninterruptible codec or system call.
|
|
24
24
|
|
|
@@ -126,7 +126,7 @@ ensure
|
|
|
126
126
|
end
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
Inputs may be a string, a path-like object, or an ordered array of files and directories. Directory expansion is deterministic, recursive by default, and deduplicates canonical paths. A run preserves that expanded order and provides `successful`, `failed`, `skipped`, `single`, and `ok?` helpers.
|
|
129
|
+
Inputs may be a string, a path-like object, or an ordered array of files and directories. Public path and model-reference strings must contain valid UTF-8 bytes; binary-encoded strings containing valid UTF-8 are normalized without changing their bytes, while undecodable byte names are rejected before work begins. Directory expansion is deterministic, recursive by default, and deduplicates canonical paths. A run preserves that expanded order and provides `successful`, `failed`, `skipped`, `single`, and `ok?` helpers.
|
|
130
130
|
|
|
131
131
|
Per-file media failures are returned as failed results so the rest of a batch can finish. Pass `raise_on_error: true` to raise `BatchTranscriptionError`; its `run` still contains every completed result.
|
|
132
132
|
|
|
@@ -162,7 +162,9 @@ run = Cohere::Transcribe.transcribe("recordings", options: options)
|
|
|
162
162
|
|
|
163
163
|
All formats for one source are staged before commit. Existing outputs are preserved if staging fails, and directory-relative structure is retained beneath `output_dir`.
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
Concurrent publishers coordinate through a persistent `.cohere-transcribe-locks` directory beside the outputs. Its access mode follows the output directory so collaborators who can publish there can use the same locks. New or current-user-owned shared writable registries receive the sticky bit where the filesystem supports it; an unowned multi-user-writable registry without that bit is rejected with remediation guidance. The authoritative output-adjacent lock is revalidated at checkpoint and output commit boundaries, so replacing its path aborts the commit even on filesystems that cannot retain the sticky bit. A fully verified `existing: "skip"` run does not need to create or modify the lock directory. The 0.1 release series also acquires the released 0.1.2 temporary lock for mixed-version coordination; disappearance of that temporary path is tolerated only while the canonical lock remains held, replacement is rejected even when verification runs inside a rescue handler, compatibility remains enabled through 0.2.0 prereleases, and the path is removed at the final 0.2.0 release. A filesystem that rejects an exclusive lock on an otherwise read-only lock file returns a typed runtime error; it never substitutes a non-exclusive lock.
|
|
166
|
+
|
|
167
|
+
Durable publication binds the planned output root and parent directory inodes, then performs staging, backup, commit, rollback, and cleanup relative to a retained directory descriptor. A concurrent directory rename or symlink replacement therefore cannot redirect transcript, checkpoint, manifest, or profile bytes. If an exception or nonlocal exit leaves recovery incomplete before commit completion, the typed failure names every retained backup or temporary and preserves the original exception as its cause. Thread termination still attempts rollback and cleanup but remains terminal even when that recovery is incomplete. Ordinary post-commit cleanup failures are also typed; once a commit is durable, post-commit cleanup remains best effort during termination. Publication returns a typed error if the planned path identity changes or if the platform lacks `O_NOFOLLOW` and the POSIX `openat`, `renameat`, and `unlinkat` primitives; the supported Linux and macOS targets provide them.
|
|
166
168
|
|
|
167
169
|
Publication state records the source's canonical path, device, inode, size, nanosecond mtime, and nanosecond ctime, plus checksums for the state payload and published artifacts. With `existing: "skip"`, a verified manifest is resolved before PCM decode, VAD, or a Dense model session is opened; a best-effort metadata duration probe supplies the skipped result's duration without materializing PCM.
|
|
168
170
|
|
|
@@ -187,7 +189,7 @@ The segment runtime uses CrispASR's padded Cohere encoder and ragged greedy deco
|
|
|
187
189
|
|
|
188
190
|
Native inference failures carry a thread-local error kind and diagnostic message across the C ABI. Ruby maps invalid arguments and invariant violations to fatal failures, allocator failures to OOM, and ordinary runtime failures to isolatable errors. Only typed OOM failures teach a smaller adaptive batch cap; fatal failures open the retained session's circuit breaker.
|
|
189
191
|
|
|
190
|
-
For multi-file runs, `preprocess_workers` concurrently decodes and segments one ordered preparation group while `pipeline_preparation` permits exactly one next group to overlap current ASR. Each ordinary pipelined group is capped at the smaller of half `audio_memory_gb` and 512 MiB of retained mono float PCM, with metadata-based size estimates assigning per-file ceilings instead of dividing the cap equally.
|
|
192
|
+
For multi-file runs, `preprocess_workers` concurrently decodes and segments one ordered preparation group while `pipeline_preparation` permits exactly one next group to overlap current ASR. Each ordinary pipelined group is capped at the smaller of half `audio_memory_gb` and 512 MiB of retained mono float PCM, with metadata-based size estimates assigning per-file ceilings instead of dividing the cap equally. Files without usable estimates initially share the group cap equally. A file whose known estimate exceeds the group cap is prepared alone with the full `audio_memory_gb` ceiling and no adjacent-group overlap. If metadata is missing or underestimates one grouped decode, only that file is retried while successful decoded audio remains retained whenever the remaining configured PCM budget is sufficient. If the retry still needs the complete ceiling, retained audio after that file is released and only those successful entries are prepared again in order; unrelated failures are not repeated. Native codec transients can add short-lived overhead. Disable `pipeline_preparation` for a fully sequential path. Automatic worker selection uses one worker for one file and at most two otherwise; explicit counts are capped by available processors and the group size. Decode/VAD failures remain isolated per file, and results, progress events, and publication always follow input order. Word alignment does not retain those prepared waveforms across its phase barrier. It re-decodes one file at a time through the concrete backend recorded during ASR (with one look-ahead only when the adjacent PCM pair fits `audio_memory_gb`) and rejects backend or sample-count drift. Resumable ASR checkpoints enter the alignment phase directly without reopening Dense or repeating decode/VAD preparation.
|
|
191
193
|
|
|
192
194
|
`pin_memory` remains accepted for Python API/CLI compatibility, but resolves to `false` in this ggml runtime. Ruby passes float PCM directly through the native session ABI, so there is no PyTorch host tensor to pin and no nonblocking tensor transfer for the option to accelerate.
|
|
193
195
|
|
|
@@ -202,7 +204,7 @@ The packed-Torch tuning options remain effective without Python. For requested `
|
|
|
202
204
|
The packaged graph has temporal input `[seq_len, 576]` but recurrent h/c inputs fixed at `[1, 1, 128]`; it has no file-batch axis, lengths, or mask. Concatenating files would therefore leak recurrent state between recordings. Ruby instead uses one thread-confined session per active file and makes `vad_batch_size` an upper bound on that independent-file concurrency for requested `auto`/`torch`. The effective count is also bounded by `preprocess_workers`, CPU availability, group size, and the sequential preparation mode. Profiles consequently report `max_files_per_call: 1`, exact temporal model-call/frame counts, and provider options matching ONNX Runtime introspection (`CPUExecutionProvider: {}`), while the configured batch/block values and effective temporal block remain visible.
|
|
203
205
|
- `vad: "auditok"` uses a native Ruby implementation of Auditok's 50 ms PCM16 log-RMS tokenizer.
|
|
204
206
|
- `vad: "none"` creates bounded fixed windows using `max_dur`.
|
|
205
|
-
- `alignment: "word"` computes full-file emissions with the exact pinned MMS-300M forced-aligner ONNX export, then runs a pure-Ruby float32 CTC Viterbi kernel. An unalignable segment alone falls back to bounded uniform timing without dropping transcript words.
|
|
207
|
+
- `alignment: "word"` computes full-file emissions with the exact pinned MMS-300M forced-aligner ONNX export, then runs a pure-Ruby float32 CTC Viterbi kernel. Audio shorter than 30 seconds follows the pinned aligner's direct waveform geometry; inputs shorter than its 400-sample receptive field receive only the minimum right padding. An unalignable segment alone falls back to bounded uniform timing without dropping transcript words.
|
|
206
208
|
- `alignment: "segment"` distributes words uniformly over the segment's speech spans.
|
|
207
209
|
- `alignment: "none"` returns plain text without words or cues.
|
|
208
210
|
|
|
@@ -215,7 +217,7 @@ CohereLabs/cohere-transcribe-arabic-07-2026
|
|
|
215
217
|
0a8193caa4f3f92131471ab08824e488141cb392
|
|
216
218
|
```
|
|
217
219
|
|
|
218
|
-
Hub artifacts reuse the standard Hugging Face cache. Symbolic branches and tags are revalidated while online;
|
|
220
|
+
Hub artifacts reuse the standard Hugging Face cache. Symbolic branches and tags are revalidated while online with a bounded per-client memo of at most five seconds; concurrent misses refresh independently and each caller uses its own fetched commit. Standard bare-SHA ref reads are capped at 41 bytes, and ref files are atomically replaced as best-effort cache hints without invalidating a valid online result when the cache is unreadable or read-only. Shared-cache directories preserve setgid and execute-gated access; snapshot directories receive a sticky bit where supported, ref directories remain replaceable for atomic collaborator updates, immutable payloads and refs inherit collaborator read access without write access, and download locks inherit the read/write access needed for coordination. Mode updates that the filesystem cannot represent are tolerated, current-user-owned older cache entries are upgraded when possible, readable non-writable locks can still coordinate where the filesystem permits an exclusive lock, and an inaccessible older lock returns a typed error with remediation instead of being replaced while it may still be held. Temporary connection, rate-limit, server, malformed-JSON, or missing-or-invalid-commit responses can reuse a complete cached file snapshot, while authentication and missing-repository responses remain definitive errors. Malformed individual repository-file entries are ignored when valid siblings remain, but an empty or wholly malformed listing is reported directly. Set `HF_HUB_OFFLINE=1` to resolve symbolic revisions only from an already complete cached snapshot; an offline symbolic revision remains at its last successfully cached commit until a later online refresh succeeds. Dense Safetensors and `pytorch_model.bin` weights are streamed into a GGUF conversion once and reused from `~/.cache/cohere-transcribe`. PyTorch metadata is decoded by a restricted, allowlist-only Ruby reader; it never imports Python or executes pickle globals. Current `torch.save` ZIP files and the preceding raw-storage stream format are supported, including sharded indexes and strided tensors. Ancient tar-format checkpoints are rejected because they cannot be interpreted with the same restricted weights-only contract; re-save those as Safetensors or a current state dict. The retained-session identity includes the resolved device and dtype as well as the model identity. Each converted artifact has an independent cache key for its output dtype and a SHA-256 source fingerprint over the model ID/revision, relative source paths, sizes, mtimes, and ctimes, so even a same-size, mtime-preserving rewrite invalidates a local fine-tune through its changed ctime. A sidecar completion marker binds that source fingerprint, output dtype, and cache layout to the converted GGUF's device, inode, size, mtime, and ctime. This marker is stored beside the model as `*.complete.json`. The cache accepts only regular, non-symlink GGUF/marker files; conversion locks use no-follow opens where available and verify that the opened descriptor still matches the path's device and inode. Cross-process output locks use an output-adjacent `.cohere-transcribe-locks/` registry derived from the canonical output set and also acquire the released 0.1.2 temporary lock during this transition.
|
|
219
221
|
|
|
220
222
|
Word mode uses `onnx-community/mms-300m-1130-forced-aligner-ONNX@2100fb247d8e43962eef24491597fbeb8b469531`, an ONNX export of `MahmoudAshraf/mms-300m-1130-forced-aligner@49402e9577b1158620820667c218cd494cc44486`. The default `align_dtype: "fp32"` works with the CPU provider even when Dense ASR runs on CUDA. `align_dtype: "fp16"` requires a CUDA-enabled ONNX Runtime; point `COHERE_TRANSCRIBE_ONNXRUNTIME_LIBRARY` at that runtime when it is not the one supplied by the installed `onnxruntime` gem. The runtime verifies the complete model file against a pinned byte size and SHA-256 before loading it. These downloaded model weights are CC-BY-NC-4.0 and are not distributed inside the gem; see `lib/cohere/transcribe/alignment/ATTRIBUTION.md` for provenance and notices.
|
|
221
223
|
|
|
@@ -254,7 +256,7 @@ Installed-gem WER measurements use the standalone runner documented in [`benchma
|
|
|
254
256
|
|
|
255
257
|
### Releasing
|
|
256
258
|
|
|
257
|
-
Add a repository Actions secret named `RUBYGEMS_AUTH_TOKEN` containing a RubyGems API key with permission to push `cohere-transcribe`. Publishing a GitHub Release tagged `
|
|
259
|
+
Add a repository Actions secret named `RUBYGEMS_AUTH_TOKEN` containing a RubyGems API key with permission to push `cohere-transcribe`. Publishing a GitHub Release tagged `0.1.3` runs the release workflow, verifies that the tag matches `Cohere::Transcribe::VERSION`, builds the exact `cohere-transcribe-0.1.3.gem` artifact, and pushes it to RubyGems.org. A leading `v` is also accepted by the release check.
|
|
258
260
|
|
|
259
261
|
Normal CI runs the Ruby suite, style checks, signature validation, native CPU build and ABI smoke checks, and source-gem build on Linux and macOS. It does not run the installed-gem WER or performance benchmarks.
|
|
260
262
|
|
|
@@ -200,6 +200,14 @@ endif()
|
|
|
200
200
|
if(COHERE_TRANSCRIBE_CUDA)
|
|
201
201
|
target_compile_definitions(crispasr PRIVATE GGML_USE_CUDA)
|
|
202
202
|
target_link_libraries(crispasr PRIVATE ggml-cuda)
|
|
203
|
+
# Source gems are compiled on the destination machine. Preserve the exact
|
|
204
|
+
# toolkit library directory found by CMake so non-system CUDA installs
|
|
205
|
+
# (including tarball and environment-managed toolkits) remain loadable after
|
|
206
|
+
# RubyGems moves libcrispasr out of the build tree.
|
|
207
|
+
set_property(TARGET crispasr APPEND PROPERTY
|
|
208
|
+
BUILD_RPATH "${CUDAToolkit_LIBRARY_DIR}")
|
|
209
|
+
set_property(TARGET crispasr APPEND PROPERTY
|
|
210
|
+
INSTALL_RPATH "${CUDAToolkit_LIBRARY_DIR}")
|
|
203
211
|
endif()
|
|
204
212
|
|
|
205
213
|
if(MSVC)
|
|
@@ -1338,6 +1338,26 @@ COHERE_AUDIO_EXPORT int cohere_audio_ffmpeg_probe(char* diagnostic, std::size_t
|
|
|
1338
1338
|
}
|
|
1339
1339
|
}
|
|
1340
1340
|
|
|
1341
|
+
COHERE_AUDIO_EXPORT int cohere_audio_ffmpeg_versions(int* versions, std::size_t count) {
|
|
1342
|
+
if (versions && count > 0)
|
|
1343
|
+
std::fill_n(versions, std::min<std::size_t>(count, 4), 0);
|
|
1344
|
+
if (!versions || count < 4)
|
|
1345
|
+
return 2;
|
|
1346
|
+
|
|
1347
|
+
try {
|
|
1348
|
+
Runtime& api = runtime();
|
|
1349
|
+
if (!api.available)
|
|
1350
|
+
return 1;
|
|
1351
|
+
versions[0] = api.format_major;
|
|
1352
|
+
versions[1] = api.codec_major;
|
|
1353
|
+
versions[2] = api.util_major;
|
|
1354
|
+
versions[3] = api.resample_major;
|
|
1355
|
+
return 0;
|
|
1356
|
+
} catch (...) {
|
|
1357
|
+
return 1;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1341
1361
|
COHERE_AUDIO_EXPORT int cohere_audio_ffmpeg_decode(
|
|
1342
1362
|
const char* path,
|
|
1343
1363
|
int target_rate,
|
|
@@ -154,6 +154,7 @@ abort "no packaged libcohere_audio found in #{directory}" unless audio_library
|
|
|
154
154
|
audio_handle = Fiddle::Handle.new(audio_library, Fiddle::RTLD_NOW)
|
|
155
155
|
audio_symbols = %w[
|
|
156
156
|
cohere_audio_ffmpeg_probe
|
|
157
|
+
cohere_audio_ffmpeg_versions
|
|
157
158
|
cohere_audio_ffmpeg_decode
|
|
158
159
|
cohere_audio_ffmpeg_duration
|
|
159
160
|
cohere_audio_ffmpeg_cancel
|
|
@@ -171,7 +172,32 @@ probe_status = probe.call(diagnostic, 1_024)
|
|
|
171
172
|
raise "audio probe returned an invalid status" unless [0, 1].include?(probe_status)
|
|
172
173
|
raise "audio probe returned no diagnostic" if diagnostic.to_s.empty?
|
|
173
174
|
|
|
175
|
+
versions = Fiddle::Function.new(
|
|
176
|
+
audio_handle["cohere_audio_ffmpeg_versions"],
|
|
177
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T],
|
|
178
|
+
Fiddle::TYPE_INT
|
|
179
|
+
)
|
|
180
|
+
raise "audio versions accepted a null output" unless versions.call(0, 0) == 2
|
|
181
|
+
|
|
174
182
|
if probe_status.zero?
|
|
183
|
+
tuple_bytes = 4 * Fiddle::SIZEOF_INT
|
|
184
|
+
tuple = Fiddle::Pointer.malloc(tuple_bytes, Fiddle::RUBY_FREE)
|
|
185
|
+
tuple[0, tuple_bytes] = [0, 0, 0, 0].pack("i!*")
|
|
186
|
+
raise "audio versions call failed" unless versions.call(tuple, 4).zero?
|
|
187
|
+
|
|
188
|
+
format_major, codec_major, util_major, resample_major = tuple[0, tuple_bytes].unpack("i!4")
|
|
189
|
+
raise "audio versions returned an incompatible format/codec tuple" unless format_major == codec_major
|
|
190
|
+
raise "audio versions returned an incompatible avutil major" unless util_major == format_major - 2
|
|
191
|
+
|
|
192
|
+
expected_resample = if format_major == 58
|
|
193
|
+
3
|
|
194
|
+
elsif format_major <= 60
|
|
195
|
+
4
|
|
196
|
+
else
|
|
197
|
+
format_major - 56
|
|
198
|
+
end
|
|
199
|
+
raise "audio versions returned an incompatible swresample major" unless resample_major == expected_resample
|
|
200
|
+
|
|
175
201
|
duration = Fiddle::Function.new(
|
|
176
202
|
audio_handle["cohere_audio_ffmpeg_duration"],
|
|
177
203
|
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T],
|
|
@@ -231,6 +231,7 @@ module Cohere
|
|
|
231
231
|
WINDOW_SAMPLES = WINDOW_SECONDS * SAMPLE_RATE
|
|
232
232
|
CONTEXT_SAMPLES = CONTEXT_SECONDS * SAMPLE_RATE
|
|
233
233
|
INPUT_SAMPLES = WINDOW_SAMPLES + (2 * CONTEXT_SAMPLES)
|
|
234
|
+
MINIMUM_INPUT_SAMPLES = 400
|
|
234
235
|
WINDOW_FRAMES = WINDOW_SAMPLES / INPUTS_TO_LOGITS_RATIO
|
|
235
236
|
CONTEXT_FRAMES = CONTEXT_SAMPLES / INPUTS_TO_LOGITS_RATIO
|
|
236
237
|
STRIDE_MS = INPUTS_TO_LOGITS_RATIO * 1_000.0 / SAMPLE_RATE
|
|
@@ -289,6 +290,7 @@ module Cohere
|
|
|
289
290
|
started = monotonic
|
|
290
291
|
samples = mono_float32(audio)
|
|
291
292
|
raise ArgumentError, "Cannot compute CTC emissions for empty audio" if samples.empty?
|
|
293
|
+
return [compute_direct_emissions(samples), STRIDE_MS] if samples.length < WINDOW_SAMPLES
|
|
292
294
|
|
|
293
295
|
total_windows = (samples.length + WINDOW_SAMPLES - 1) / WINDOW_SAMPLES
|
|
294
296
|
extension_samples = (total_windows * WINDOW_SAMPLES) - samples.length
|
|
@@ -320,10 +322,7 @@ module Cohere
|
|
|
320
322
|
end
|
|
321
323
|
|
|
322
324
|
class_count = batch_log_probs.shape[1]
|
|
323
|
-
|
|
324
|
-
raise TranscriptionRuntimeError,
|
|
325
|
-
"MMS aligner returned #{class_count} classes; expected #{VOCABULARY.length}"
|
|
326
|
-
end
|
|
325
|
+
validate_class_count!(class_count)
|
|
327
326
|
emissions ||= Numo::SFloat.zeros(frame_count, class_count + 1)
|
|
328
327
|
if first_window + window_count == total_windows && extension_frames.positive?
|
|
329
328
|
kept = batch_log_probs.shape[0] - extension_frames
|
|
@@ -440,6 +439,29 @@ module Cohere
|
|
|
440
439
|
batch
|
|
441
440
|
end
|
|
442
441
|
|
|
442
|
+
# The pinned ctc-forced-aligner passes sub-30-second waveforms to MMS
|
|
443
|
+
# directly, without window context or tail padding. Its Wav2Vec2 feature
|
|
444
|
+
# extractor needs at least its 400-sample convolutional receptive field,
|
|
445
|
+
# so only shorter inputs receive the minimum right padding needed to run.
|
|
446
|
+
def compute_direct_emissions(audio)
|
|
447
|
+
input_samples = [audio.length, MINIMUM_INPUT_SAMPLES].max
|
|
448
|
+
input = Numo::SFloat.zeros(1, input_samples)
|
|
449
|
+
input[0, 0...audio.length] = audio
|
|
450
|
+
logits = session.run(input)
|
|
451
|
+
logits = Numo::SFloat.cast(logits) unless logits.is_a?(Numo::NArray)
|
|
452
|
+
unless logits.ndim == 3 && logits.shape[0] == 1 && logits.shape[1].positive?
|
|
453
|
+
raise TranscriptionRuntimeError,
|
|
454
|
+
"MMS aligner returned invalid direct logits shape #{logits.shape.inspect}"
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
class_count = logits.shape[2]
|
|
458
|
+
validate_class_count!(class_count)
|
|
459
|
+
log_probs = log_softmax(logits.reshape(logits.shape[1], class_count))
|
|
460
|
+
emissions = Numo::SFloat.zeros(log_probs.shape[0], class_count + 1)
|
|
461
|
+
emissions[true, 0...class_count] = log_probs
|
|
462
|
+
emissions
|
|
463
|
+
end
|
|
464
|
+
|
|
443
465
|
def crop_and_normalize(logits, expected_batch)
|
|
444
466
|
logits = Numo::SFloat.cast(logits) unless logits.is_a?(Numo::NArray)
|
|
445
467
|
unless logits.ndim == 3 && logits.shape[0] == expected_batch &&
|
|
@@ -451,12 +473,23 @@ module Cohere
|
|
|
451
473
|
classes = logits.shape[2]
|
|
452
474
|
cropped = logits[true, CONTEXT_FRAMES...(CONTEXT_FRAMES + WINDOW_FRAMES), true]
|
|
453
475
|
.reshape(expected_batch * WINDOW_FRAMES, classes)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
476
|
+
log_softmax(cropped)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def log_softmax(logits)
|
|
480
|
+
maxima = logits.max(1).reshape(logits.shape[0], 1)
|
|
481
|
+
shifted = logits - maxima
|
|
482
|
+
denominators = Numo::NMath.log(Numo::NMath.exp(shifted).sum(1)).reshape(logits.shape[0], 1)
|
|
457
483
|
(shifted - denominators).cast_to(Numo::SFloat)
|
|
458
484
|
end
|
|
459
485
|
|
|
486
|
+
def validate_class_count!(class_count)
|
|
487
|
+
return if class_count == VOCABULARY.length
|
|
488
|
+
|
|
489
|
+
raise TranscriptionRuntimeError,
|
|
490
|
+
"MMS aligner returned #{class_count} classes; expected #{VOCABULARY.length}"
|
|
491
|
+
end
|
|
492
|
+
|
|
460
493
|
def uniform_fallback(text, start_time, end_time, segment_index)
|
|
461
494
|
Output::Timing.uniform_words(
|
|
462
495
|
text, start_time, end_time, segment_index, "uniform_fallback"
|
|
@@ -4,6 +4,8 @@ require "etc"
|
|
|
4
4
|
require "fiddle"
|
|
5
5
|
require "rbconfig"
|
|
6
6
|
require_relative "../errors"
|
|
7
|
+
require_relative "../internal/interruptible_native_call"
|
|
8
|
+
require_relative "../internal/session_ownership"
|
|
7
9
|
require_relative "../python_text"
|
|
8
10
|
require_relative "failure_policy"
|
|
9
11
|
|
|
@@ -297,69 +299,38 @@ module Cohere
|
|
|
297
299
|
4 => :error
|
|
298
300
|
}.freeze
|
|
299
301
|
|
|
300
|
-
#
|
|
301
|
-
#
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def initialize(library)
|
|
306
|
-
@library = library
|
|
307
|
-
@mutex = Mutex.new
|
|
308
|
-
@session = nil
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
def install(session)
|
|
312
|
-
@mutex.synchronize do
|
|
313
|
-
raise TranscriptionRuntimeError, "Native session ownership is already installed" if @session
|
|
314
|
-
|
|
315
|
-
@session = session
|
|
316
|
-
end
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
def close
|
|
320
|
-
session = @mutex.synchronize do
|
|
321
|
-
current = @session
|
|
322
|
-
@session = nil
|
|
323
|
-
current
|
|
324
|
-
end
|
|
325
|
-
return unless session
|
|
326
|
-
|
|
327
|
-
@library.call(:session_close, session)
|
|
328
|
-
nil
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
def finalize
|
|
332
|
-
close
|
|
333
|
-
rescue Exception # rubocop:disable Lint/RescueException -- finalizers must not escape during GC or shutdown
|
|
334
|
-
nil
|
|
302
|
+
# A callable retained by detached ownership state. It keeps the native
|
|
303
|
+
# library alive, but never captures the NativeSession being finalized.
|
|
304
|
+
NativeSessionCloser = Data.define(:library) do
|
|
305
|
+
def call(session)
|
|
306
|
+
library.call(:session_close, session)
|
|
335
307
|
end
|
|
336
308
|
end
|
|
337
|
-
private_constant :
|
|
338
|
-
|
|
339
|
-
def self.finalizer_for(ownership)
|
|
340
|
-
proc { |_object_id| ownership.finalize }
|
|
341
|
-
end
|
|
342
|
-
private_class_method :finalizer_for
|
|
309
|
+
private_constant :NativeSessionCloser
|
|
343
310
|
|
|
344
311
|
attr_reader :backend, :batch_capacity, :compute_backend, :device, :last_batch_metrics, :model_path
|
|
345
312
|
|
|
346
313
|
def initialize(model_path, options, threads: nil, library: NativeLibrary.load)
|
|
314
|
+
initialized = false
|
|
347
315
|
@library = library
|
|
348
316
|
@model_path = Pathname.new(model_path).expand_path.freeze
|
|
349
317
|
raise TranscriptionRuntimeError, "Native Dense model does not exist: #{@model_path}" unless @model_path.file?
|
|
350
318
|
|
|
351
319
|
@mutex = Mutex.new
|
|
352
320
|
@closed = false
|
|
353
|
-
@session_ownership = SessionOwnership.new(
|
|
321
|
+
@session_ownership = Internal::SessionOwnership.new(
|
|
322
|
+
close: NativeSessionCloser.new(@library),
|
|
323
|
+
installed_error: "Native session ownership is already installed"
|
|
324
|
+
)
|
|
354
325
|
ObjectSpace.define_finalizer(
|
|
355
326
|
self,
|
|
356
|
-
|
|
327
|
+
Internal::SessionOwnership.finalizer_for(@session_ownership)
|
|
357
328
|
)
|
|
358
329
|
thread_count = normalize_threads(threads)
|
|
359
330
|
# An asynchronous exception may become deliverable immediately after
|
|
360
331
|
# the foreign open returns. Defer it until the pointer belongs to the
|
|
361
|
-
# independent cleanup state, then let
|
|
362
|
-
Thread.handle_interrupt(
|
|
332
|
+
# independent cleanup state, then let constructor teardown close it.
|
|
333
|
+
Thread.handle_interrupt(Object => :never) do
|
|
363
334
|
session = @library.open_session(
|
|
364
335
|
model_path: @model_path,
|
|
365
336
|
device: options.device.to_s,
|
|
@@ -397,25 +368,29 @@ module Cohere
|
|
|
397
368
|
close
|
|
398
369
|
raise TranscriptionRuntimeError, "Native runtime reported an invalid Cohere batch capacity"
|
|
399
370
|
end
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
371
|
+
initialized = true
|
|
372
|
+
ensure
|
|
373
|
+
unless initialized
|
|
374
|
+
Thread.handle_interrupt(Object => :never) do
|
|
375
|
+
close if defined?(@session_ownership)
|
|
376
|
+
rescue Exception # rubocop:disable Lint/RescueException -- preserve constructor termination
|
|
377
|
+
nil
|
|
378
|
+
end
|
|
405
379
|
end
|
|
406
|
-
raise
|
|
407
380
|
end
|
|
408
381
|
|
|
409
382
|
def transcribe(samples, language:, offset: 0.0, max_new_tokens: nil)
|
|
410
383
|
@mutex.synchronize do
|
|
411
384
|
ensure_open!
|
|
412
385
|
@last_batch_metrics = nil
|
|
413
|
-
generation_limit = max_new_tokens.nil? ? @default_max_new_tokens : Integer(max_new_tokens)
|
|
414
|
-
set_generation_limit!(generation_limit)
|
|
415
386
|
binary, sample_count = float_samples(samples)
|
|
387
|
+
raise ArgumentError, "Audio segment must not be empty" if sample_count.zero?
|
|
416
388
|
raise TranscriptionRuntimeError, "Audio segment is too large for the native ABI" \
|
|
417
389
|
if sample_count > MAX_NATIVE_SAMPLE_COUNT
|
|
418
390
|
|
|
391
|
+
generation_limit = max_new_tokens.nil? ? @default_max_new_tokens : Integer(max_new_tokens)
|
|
392
|
+
set_generation_limit!(generation_limit)
|
|
393
|
+
|
|
419
394
|
run_native_inference do
|
|
420
395
|
result = @library.call(
|
|
421
396
|
:session_transcribe_lang,
|
|
@@ -452,16 +427,19 @@ module Cohere
|
|
|
452
427
|
@mutex.synchronize do
|
|
453
428
|
ensure_open!
|
|
454
429
|
@last_batch_metrics = nil
|
|
455
|
-
generation_limit = max_new_tokens.nil? ? @default_max_new_tokens : Integer(max_new_tokens)
|
|
456
|
-
set_generation_limit!(generation_limit)
|
|
457
|
-
|
|
458
430
|
buffers_and_counts = sample_batches.map { |samples| float_samples(samples) }
|
|
431
|
+
empty_lane = buffers_and_counts.index { |_binary, count| count.zero? }
|
|
432
|
+
raise ArgumentError, "Audio batch row #{empty_lane} must not be empty" if empty_lane
|
|
433
|
+
|
|
459
434
|
oversized_lane = buffers_and_counts.index { |_binary, count| count > MAX_NATIVE_SAMPLE_COUNT }
|
|
460
435
|
if oversized_lane
|
|
461
436
|
raise TranscriptionRuntimeError,
|
|
462
437
|
"Audio batch row #{oversized_lane} is too large for the native ABI"
|
|
463
438
|
end
|
|
464
439
|
|
|
440
|
+
generation_limit = max_new_tokens.nil? ? @default_max_new_tokens : Integer(max_new_tokens)
|
|
441
|
+
set_generation_limit!(generation_limit)
|
|
442
|
+
|
|
465
443
|
run_native_inference do
|
|
466
444
|
pointers = buffers_and_counts.map { |binary, _count| Fiddle::Pointer[binary] }
|
|
467
445
|
pointer_table = pointers.map(&:to_i).pack("J*")
|
|
@@ -558,51 +536,19 @@ module Cohere
|
|
|
558
536
|
|
|
559
537
|
# Ruby cannot deliver Thread#raise (including SIGINT's Interrupt) while
|
|
560
538
|
# the receiving thread is blocked inside Fiddle. Keep the caller at an
|
|
561
|
-
# interruptible join while a
|
|
539
|
+
# interruptible join while a dedicated worker owns the foreign call. The
|
|
562
540
|
# caller still holds @mutex, so close and another inference cannot race
|
|
563
541
|
# the session; cancellation is the only concurrent C ABI operation.
|
|
564
|
-
def run_native_inference
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
outcome << [:returned, yield]
|
|
570
|
-
rescue Exception => e # rubocop:disable Lint/RescueException -- transfer native cancellation intact
|
|
571
|
-
outcome << [:raised, e]
|
|
572
|
-
end
|
|
573
|
-
worker.report_on_exception = false
|
|
574
|
-
|
|
575
|
-
begin
|
|
576
|
-
worker.join
|
|
577
|
-
rescue Exception => e # rubocop:disable Lint/RescueException -- caller cancellation must win
|
|
578
|
-
cancel_and_hard_join(worker)
|
|
579
|
-
raise e
|
|
580
|
-
end
|
|
581
|
-
end
|
|
582
|
-
|
|
583
|
-
status, value = begin
|
|
584
|
-
outcome.pop(true)
|
|
585
|
-
rescue ThreadError
|
|
586
|
-
raise ExecutionError.new(
|
|
542
|
+
def run_native_inference(&)
|
|
543
|
+
Internal::InterruptibleNativeCall.run(
|
|
544
|
+
cancel: -> { @library.call(:session_cancel, @session) },
|
|
545
|
+
join_interval: CANCELLATION_JOIN_INTERVAL,
|
|
546
|
+
missing_outcome: ExecutionError.new(
|
|
587
547
|
"Native Cohere inference worker exited without reporting an outcome",
|
|
588
548
|
failure_kind: :fatal
|
|
589
|
-
)
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
value
|
|
594
|
-
end
|
|
595
|
-
|
|
596
|
-
# A cancellation can arrive after Thread.new but before the worker has
|
|
597
|
-
# entered the C function. Retry the non-poisoning session cancel until
|
|
598
|
-
# the worker exits; never kill a thread that may still own ggml state.
|
|
599
|
-
def cancel_and_hard_join(worker)
|
|
600
|
-
loop do
|
|
601
|
-
@library.call(:session_cancel, @session)
|
|
602
|
-
return if worker.join(CANCELLATION_JOIN_INTERVAL)
|
|
603
|
-
rescue Exception # rubocop:disable Lint/RescueException -- preserve the first caller exception
|
|
604
|
-
next
|
|
605
|
-
end
|
|
549
|
+
),
|
|
550
|
+
&
|
|
551
|
+
)
|
|
606
552
|
end
|
|
607
553
|
|
|
608
554
|
def canonical_device(name)
|