coremlit 0.1.2

Safe, synchronous CoreML runtime for macOS (CPU/GPU/Neural Engine) with opt-in on-device multimodal pipelines: speech (Whisper STT, forced alignment, speaker diarization, Silero VAD), AudioSet sound-event tagging, and audio/text/image embeddings (CLAP, granite, SigLIP)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
[package]
name = "coremlit"
description = "Safe, synchronous CoreML runtime for macOS (CPU/GPU/Neural Engine) with opt-in on-device multimodal pipelines: speech (Whisper STT, forced alignment, speaker diarization, Silero VAD), AudioSet sound-event tagging, and audio/text/image embeddings (CLAP, granite, SigLIP)"
version.workspace = true
edition.workspace = true
publish = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
readme = "../README.md"
keywords = ["coreml", "macos", "speech", "embeddings", "multimodal"]
categories = ["multimedia::audio", "os::macos-apis", "artificial-intelligence", "computer-vision", "api-bindings"]
exclude = [
  "conversion/",
  "tests/align/fixtures/",
  "tests/ced/fixtures/",
  "tests/clap/fixtures/",
  "tests/face/fixtures/",
  "tests/identity/fixtures/",
  "tests/lid/fixtures/",
  "tests/siglip/fixtures/",
  "tests/speaker/fixtures/",
  "tests/vad/fixtures/",
  "tests/whisper/fixtures/",
  "tests/whisper_swift_probes/",
]

[package.metadata.docs.rs]
all-features = true
default-target = "aarch64-apple-darwin"
targets = ["aarch64-apple-darwin"]

[features]
default = []
# `windit?/serde` is a WEAK dep-feature: it turns on windit's own serde impls
# only when windit is already in the graph (via `clap`/`granite`/`ced`/`lid`),
# never pulling windit in on its own. It is what lets
# `embeddings::granite::LongTextOptions` derive — it composes windit's
# `WindowOptions`, so it cannot be serialized without it — and it is the
# step that module's doc named as the prerequisite.
serde = ["dep:serde", "windit?/serde"]
tracing = ["dep:tracing"]

whisper = [
  "dep:libc",
  "dep:mach2",
  "dep:rand",
  "dep:serde_json",
  "dep:tokenizers",
  "dep:unicode_categories",
]
# Former whisperkit `nl-recognizer` (Apple `NLLanguageRecognizer` redetection).
# Whisper-only code, so it implies `whisper`.
nl-recognizer = ["whisper", "dep:objc2-natural-language"]

# `dep:humantime` is `AlignerOptions`'s canonical `Display` rendering
# `max_intra_silent_run` as humantime text ("80ms"), the SAME grammar
# `humantime::format_duration` already writes elsewhere in this workspace
# (ingraph's own `[registry]` seat; asry/diaric/zuoer instead use
# `humantime-serde`, a document-only bridge that does not implement `Display`
# itself, so it cannot fill this seam).
#
# `dep:serde_json` reads a model's own `{token: id}` vocabulary
# (`audio::align::vocab::Vocabulary`) and writes, for the model's contract, the
# tokenizer document asry's seam builder parses, its specials declared special
# added tokens; `dep:serde` is the entry-by-entry visitor that reader
# refuses a repeated token with. `dep:tokenizers` parses that document again at
# load, as asry's builder does, so the aligner reads back the columns the seam
# reserves: asry keeps its tokenizer off its public API. asry's `emissions`
# feature already pulls `tokenizers` 0.23, which depends on serde and
# serde_json, so these pull no new version into the lockfile, and the parse is
# asry's own crate (and `dep:serde` does not turn on this crate's own `serde`
# feature). The workspace row's `unstable_wasm` adds `fancy-regex` (with
# `bit-set` and `bit-vec`, all three already locked) to an `align`-only build,
# compiled and unused: the `onig` backend asry enables takes precedence.
align = ["dep:asry", "dep:humantime", "dep:serde_json", "dep:serde", "dep:tokenizers"]
# asry's ONNX aligner (pulls ort + whisper.cpp)
# as the word-timing parity oracle. DEV/TEST only — adds nothing to the library.
align-oracle = ["align", "asry/alignment"]

speaker = ["dep:diaric"]
vad = ["dep:zuoer"]
# `dep:serde_json` is the shared `embeddings::tokenizer_guard`: the tokenizers
# crate depends on serde_json itself, so this pulls nothing new into the graph.
clap = ["dep:rustfft", "dep:tokenizers", "dep:windit", "dep:serde_json"]

# `dep:serde_json` reads the BPE merge table back out of the loaded model for
# `embeddings::granite::token_index`'s separatorless fast lane (#72); tokenizers
# itself depends on serde_json, so this pulls nothing new into the graph.
granite = ["dep:tokenizers", "dep:windit", "windit/text", "dep:sha2", "dep:serde_json"]

# LICENSE PREREQUISITE (owner-gated, NOT resolved by this change): `pixon` is
# currently licensed GPL-3.0-or-later; the owner will relicense it permissive
# (MIT OR Apache-2.0) before this branch merges. It is a flagged merge blocker
# tracked outside this change, not something wired here.
siglip = ["dep:tokenizers", "dep:pixon", "dep:sha2", "dep:serde_json"]

ced = ["dep:rustfft", "dep:soundevents-dataset", "dep:windit"]

lid = ["dep:rustfft", "dep:windit"]

# `audio::identity` — one fixed 6 s window in, one raw 192-d speaker embedding
# out. Only the mel front end needs a dependency; the windowing and scoring of
# several windows are the caller's, so there is no `windit` here.
identity = ["dep:rustfft"]

# The face-embedding surface (`embeddings::face`): the ArcFace 5-point
# similarity alignment plus a manifest-driven CoreML embedder. Its ONE
# dependency is `sha2`, already an optional workspace dep that `granite` and
# `siglip` pull: `FaceEmbedder::load` hashes the artifact directory it loads so
# an embedding carries the identity of the weights that produced it, not just
# their schema (issue #138 §3). The alignment is `f64` arithmetic and the
# embedder builds a `MultiArray` the always-compiled runtime core already owns.
#
# PLAIN `face`, and that is deliberate rather than a rename left undone. Two
# thirds of this feature is encumbered by nothing: the alignment is `f64`
# arithmetic over a synthetic golden, and the embedder loads a CALLER-SUPPLIED
# path — including an artifact whose licence permits a product. The ONE staged
# artifact (InsightFace's `w600k_r50`, research-only at both the weights and the
# corpus layer) rides `commercial-face-arcface` below, which gates its manifest
# module and nothing else. Gating this feature instead would make the register
# say "requires a commercial licence" about code that requires nothing.
face = ["dep:sha2"]

# Requires a commercial licence for the ArcFace `w600k_r50` weights and their WebFace600K
# corpus. InsightFace publishes both for non-commercial research only, and offers no
# commercial grant over them, so what this feature adds is the manifest for an artifact that
# may be used to develop, evaluate and test — never to ship. It is never in `default`, no
# other feature may pull it in, and this repository still redistributes no weight bytes: the
# artifact lives in a PRIVATE Hugging Face repository that CI fetches with a read token, the
# same posture `identity` already has (see NOTICE, "CI DOWNLOADS; IT DOES NOT
# REDISTRIBUTE"). The `commercial-` prefix reads backwards on purpose — it looks like
# "cleared for commercial use" — which is why the sentence you just read has to be the first
# one.
#
# WHAT ENABLING IT DOES, exactly: it WIRES coremlit's own registered copy of that artifact.
# It compiles the `embeddings::face::arcface` manifest module (four constants naming the
# bundle, the path `MODELS_LOCK` stages it to, its I/O contract and its measured compute
# placement), it turns on the `arcface` `MODELS_LOCK` table that stages those bytes for CI,
# and it builds the four gated suites in `tests/face/`. Manifested, staged, tested — that
# wiring, and only that wiring, is what the licence register in `tests/model_licences.rs`
# governs.
#
# WHAT IT DOES NOT DO: it does not stop a caller loading their own copy of these — or of any
# other — weights through the plain `face` feature. `FaceEmbedder::load` takes a
# caller-supplied path and a caller-written `FaceModel`, so a product with a commercially
# licensed ArcFace-shaped model of its own must be able to write that manifest and load it
# under `face`, and nothing but a digest would tell those bytes from InsightFace's. The
# licence of bytes a caller supplies is between them and whoever published them — for these
# weights, InsightFace. That residual is issue #138 §8's, stated in the module doc of
# `tests/model_licences.rs`; what this feature's NAME and first sentence are for is to tell
# you that coremlit's registered artifact needs a licence, and coremlit hands nobody the
# bytes.
commercial-face-arcface = ["face"]

[dependencies]
objc2.workspace = true
objc2-core-ml.workspace = true
objc2-foundation.workspace = true
objc2-core-video.workspace = true
objc2-core-foundation.workspace = true
block2.workspace = true
half.workspace = true
thiserror.workspace = true
derive_more.workspace = true

# --- cross-cutting optionals ---
serde = { workspace = true, optional = true }
tracing = { workspace = true, optional = true }

# --- audio::whisper optionals ---
libc = { workspace = true, optional = true }
mach2 = { workspace = true, optional = true }
rand = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
tokenizers = { workspace = true, optional = true }
unicode_categories = { workspace = true, optional = true }
objc2-natural-language = { workspace = true, optional = true }

# --- audio::align optional dep (the `align` feature) ---
asry = { version = "0.3", default-features = false, features = ["emissions"], optional = true }
humantime = { version = "2", optional = true }

# --- audio::speaker optional dep (the `speaker` feature) ---
diaric = { version = "0.2", optional = true }

# --- audio::vad optional dep (the `vad` feature) ---
zuoer = { version = "0.3", optional = true }

# --- embeddings::clap optionals (the `clap` feature) ---
rustfft = { workspace = true, optional = true }

# audio::ced's 527-class rated AudioSet label set (ort-free data crate; see the
# workspace table for the feature pins).
soundevents-dataset = { workspace = true, optional = true }

# --- shared windowing engine (the `clap`, `granite` + `lid` features) ---
windit = { version = "0.5", optional = true }

# --- embeddings::granite runtime tokenizer-identity digest (the `granite` feature) ---
sha2 = { workspace = true, optional = true }

# --- embeddings::siglip image resampler (the `siglip` feature) ---
# LICENSE: pixon is GPL-3.0-or-later today; its owner-gated relicense to
# MIT OR Apache-2.0 is a merge blocker for this branch (see the `siglip` feature
# comment above).
pixon = { version = "0.6", default-features = false, features = ["std", "rgb"], optional = true }

[dev-dependencies]
half.workspace = true
cpal.workspace = true
criterion.workspace = true
hound.workspace = true
rubato.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
postcard.workspace = true
sha2.workspace = true
tempfile.workspace = true
toml.workspace = true
syn.workspace = true
proc-macro2 = { workspace = true, features = ["span-locations"] }
tokenizers.workspace = true
npyz.workspace = true
png.workspace = true
libc.workspace = true
mach2.workspace = true
asry = { version = "0.3", default-features = false, features = ["emissions"] }

[[test]]
name = "face_align_golden"
path = "tests/face/align_golden.rs"
required-features = ["face"]

# The two-sided gate assertion. `face`, NOT `commercial-face-arcface`: half of
# what it pins is what a plain-`face` caller can still do, and a suite that
# needed the gate could not say that.
[[test]]
name = "face_gate"
path = "tests/face/gate.rs"
required-features = ["face"]

# The four `commercial-face-arcface` gate suites. Their model gates need the
# staged w600k_r50 bundle and are `#[ignore]`d; the eight hermetic tests beside
# them read only committed bytes — the fixtures, the ONNX reference, and the
# vendored silero bundle the manifest refuses — so the modelless `features` job
# runs those on every PR.
[[test]]
name = "face_model_io"
path = "tests/face/model_io.rs"
required-features = ["commercial-face-arcface"]

[[test]]
name = "face_parity"
path = "tests/face/parity.rs"
required-features = ["commercial-face-arcface"]

[[test]]
name = "face_known_pairs"
path = "tests/face/known_pairs.rs"
required-features = ["commercial-face-arcface"]

[[test]]
name = "face_placement"
path = "tests/face/placement.rs"
required-features = ["commercial-face-arcface"]

[[test]]
name = "whisper_golden_provenance"
path = "tests/whisper/golden_provenance.rs"
required-features = ["whisper"]

[[test]]
name = "whisper_longform"
path = "tests/whisper/longform.rs"
required-features = ["whisper"]

[[test]]
name = "whisper_model_io"
path = "tests/whisper/model_io.rs"
required-features = ["whisper"]

[[test]]
name = "whisper_models_lock"
path = "tests/whisper/models_lock.rs"
required-features = ["whisper"]

[[test]]
name = "whisper_parity_es"
path = "tests/whisper/parity_es.rs"
required-features = ["whisper"]

[[test]]
name = "whisper_parity_jfk"
path = "tests/whisper/parity_jfk.rs"
required-features = ["whisper"]

[[test]]
name = "whisper_pipeline"
path = "tests/whisper/pipeline.rs"
required-features = ["whisper"]

[[test]]
name = "whisper_streaming"
path = "tests/whisper/streaming.rs"
required-features = ["whisper"]

[[test]]
name = "whisper_vad_source"
path = "tests/whisper/vad_source.rs"
required-features = ["whisper", "vad"]

[[test]]
name = "whisper_rtf_gate"
path = "benches/whisper/rtf_gate.rs"
required-features = ["whisper"]

[[bench]]
name = "whisper_stages"
path = "benches/whisper/stages.rs"
harness = false
required-features = ["whisper"]

[[bench]]
name = "whisper_rtf"
path = "benches/whisper/rtf.rs"
harness = false
required-features = ["whisper"]

[[example]]
name = "whisper_mic_stream"
path = "examples/whisper/mic_stream.rs"
test = true
required-features = ["whisper"]

[[example]]
name = "whisper_transcribe_wav"
path = "examples/whisper/transcribe_wav.rs"
required-features = ["whisper"]

# --- audio::align ---
[[test]]
name = "align_align_chunk"
path = "tests/align/align_chunk.rs"
required-features = ["align"]

[[test]]
name = "align_model_io"
path = "tests/align/model_io.rs"
required-features = ["align"]

[[test]]
name = "align_registry_handle"
path = "tests/align/registry_handle.rs"
required-features = ["align"]

# The word-timing parity gate needs asry's ONNX aligner oracle (ort +
# whisper.cpp) — the former alignkit `parity-oracle`, now `align-oracle`.
[[test]]
name = "align_parity_words"
path = "tests/align/parity_words.rs"
required-features = ["align-oracle"]

[[bench]]
name = "align_align"
path = "benches/align/align.rs"
harness = false
required-features = ["align"]

# --- audio::speaker ---
[[test]]
name = "speaker_argmax_model_io"
path = "tests/speaker/argmax_model_io.rs"
required-features = ["speaker"]

[[test]]
name = "speaker_golden_metadata"
path = "tests/speaker/golden_metadata.rs"
required-features = ["speaker"]

[[test]]
name = "speaker_model_io"
path = "tests/speaker/model_io.rs"
required-features = ["speaker"]

[[test]]
name = "speaker_parity_argmax_accuracy"
path = "tests/speaker/parity_argmax_accuracy.rs"
required-features = ["speaker"]

[[test]]
name = "speaker_parity_argmax_swift"
path = "tests/speaker/parity_argmax_swift.rs"
required-features = ["speaker"]

[[test]]
name = "speaker_parity_embed"
path = "tests/speaker/parity_embed.rs"
required-features = ["speaker"]

[[test]]
name = "speaker_parity_seg"
path = "tests/speaker/parity_seg.rs"
required-features = ["speaker"]

[[test]]
name = "speaker_parity_online_swift"
path = "tests/speaker/parity_online_swift.rs"
required-features = ["speaker"]

[[test]]
name = "speaker_parity_diarize_wiring"
path = "tests/speaker/parity_diarize_wiring.rs"
required-features = ["speaker"]

# --- audio::vad ---
[[test]]
name = "vad_model_io"
path = "tests/vad/model_io.rs"
required-features = ["vad"]

[[test]]
name = "vad_model_state"
path = "tests/vad/model_state.rs"
required-features = ["vad"]

[[test]]
name = "vad_parity_swift"
path = "tests/vad/parity_swift.rs"
required-features = ["vad"]

[[test]]
name = "vad_reexport"
path = "tests/vad/reexport.rs"
required-features = ["vad"]

[[test]]
name = "clap_e2e"
path = "tests/clap/e2e.rs"
required-features = ["clap"]

[[test]]
name = "clap_model_io"
path = "tests/clap/model_io.rs"
required-features = ["clap"]

[[test]]
name = "clap_text_model_io"
path = "tests/clap/text_model_io.rs"
required-features = ["clap"]

[[test]]
name = "clap_placement"
path = "tests/clap/placement.rs"
required-features = ["clap"]

[[test]]
name = "clap_tokenizer_identity"
path = "tests/clap/tokenizer_identity.rs"
required-features = ["clap"]

[[test]]
name = "granite_tokenizer_identity"
path = "tests/granite/tokenizer_identity.rs"
required-features = ["granite"]

[[test]]
name = "granite_driver_crosscheck"
path = "tests/granite/driver_crosscheck.rs"
required-features = ["granite"]

[[test]]
name = "granite_model_io"
path = "tests/granite/model_io.rs"
required-features = ["granite"]

[[test]]
name = "granite_parity_embed"
path = "tests/granite/parity_embed.rs"
required-features = ["granite"]

[[test]]
name = "granite_placement"
path = "tests/granite/placement.rs"
required-features = ["granite"]

[[test]]
name = "granite_embed_long"
path = "tests/granite/embed_long.rs"
required-features = ["granite"]

[[test]]
name = "granite_embed_windows"
path = "tests/granite/embed_windows.rs"
required-features = ["granite"]

[[test]]
name = "siglip_tokenizer_identity"
path = "tests/siglip/tokenizer_identity.rs"
required-features = ["siglip"]

[[test]]
name = "siglip_preprocess"
path = "tests/siglip/preprocess.rs"
required-features = ["siglip"]

[[test]]
name = "siglip_model_io"
path = "tests/siglip/model_io.rs"
required-features = ["siglip"]

[[test]]
name = "siglip_text_model_io"
path = "tests/siglip/text_model_io.rs"
required-features = ["siglip"]

[[test]]
name = "siglip_parity_embed"
path = "tests/siglip/parity_embed.rs"
required-features = ["siglip"]

[[test]]
name = "siglip_placement"
path = "tests/siglip/placement.rs"
required-features = ["siglip"]

[[test]]
name = "siglip_e2e"
path = "tests/siglip/e2e.rs"
required-features = ["siglip"]

[[test]]
name = "siglip_band_provenance"
path = "tests/siglip/band_provenance.rs"
required-features = ["siglip"]

[[test]]
name = "ced_model_io"
path = "tests/ced/model_io.rs"
required-features = ["ced"]

[[test]]
name = "ced_parity_logits"
path = "tests/ced/parity_logits.rs"
required-features = ["ced"]

[[test]]
name = "ced_placement"
path = "tests/ced/placement.rs"
required-features = ["ced"]

[[test]]
name = "ced_e2e"
path = "tests/ced/e2e.rs"
required-features = ["ced"]

[[test]]
name = "lid_model_io"
path = "tests/lid/model_io.rs"
required-features = ["lid"]

[[test]]
name = "lid_e2e"
path = "tests/lid/e2e.rs"
required-features = ["lid"]

[[test]]
name = "lid_long_clip"
path = "tests/lid/long_clip.rs"
required-features = ["lid"]

[[test]]
name = "identity_model_io"
path = "tests/identity/model_io.rs"
required-features = ["identity"]

[[test]]
name = "identity_parity_embed"
path = "tests/identity/parity_embed.rs"
required-features = ["identity"]

[[bench]]
name = "clap_encode"
path = "benches/clap/encode.rs"
harness = false
required-features = ["clap"]

[lints]
workspace = true