transcribe-cpp 0.1.3__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/.gitignore +4 -0
  2. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/PKG-INFO +19 -7
  3. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/README.md +15 -3
  4. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/pyproject.toml +3 -3
  5. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/src/transcribe_cpp/__init__.py +152 -36
  6. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/src/transcribe_cpp/_generated.py +58 -20
  7. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/src/transcribe_cpp/_library.py +2 -2
  8. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/tests/test_backends.py +16 -7
  9. transcribe_cpp-0.2.1/tests/test_device_select.py +38 -0
  10. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/tests/test_family_ext.py +15 -1
  11. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/tests/test_provider_discovery.py +1 -0
  12. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/uv.lock +3 -3
  13. transcribe_cpp-0.1.3/tests/test_device_select.py +0 -55
  14. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/LICENSE +0 -0
  15. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/_generate/README.md +0 -0
  16. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/_generate/check_version_sync.py +0 -0
  17. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/_generate/generate.py +0 -0
  18. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/examples/stream_wav.py +0 -0
  19. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/examples/transcribe_wav.py +0 -0
  20. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/src/transcribe_cpp/_abi.py +0 -0
  21. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/src/transcribe_cpp/errors.py +0 -0
  22. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/src/transcribe_cpp/py.typed +0 -0
  23. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/tests/conftest.py +0 -0
  24. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/tests/test_abi.py +0 -0
  25. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/tests/test_errors.py +0 -0
  26. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/tests/test_example.py +0 -0
  27. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/tests/test_lifetime.py +0 -0
  28. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/tests/test_pcm.py +0 -0
  29. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/tests/test_streaming.py +0 -0
  30. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/tests/test_transcribe.py +0 -0
  31. {transcribe_cpp-0.1.3 → transcribe_cpp-0.2.1}/tests/test_utf8_paths.py +0 -0
@@ -59,6 +59,10 @@ __pycache__/
59
59
 
60
60
  # WER evaluation data + generated working reports.
61
61
  /samples/wer/
62
+ # Diarization eval corpora (AMI audio + fetched RTTMs); regenerable via
63
+ # scripts/diar/ingest_ami.py + fetch_ami_forced_alignment.py. The committed
64
+ # oracle case lives at samples/sortformer-2spk-mix.wav (outside samples/diar/).
65
+ /samples/diar/
62
66
  /reports/*
63
67
 
64
68
  # Reviewable porting evidence (intake, porting log) is intentionally committed.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: transcribe-cpp
3
- Version: 0.1.3
3
+ Version: 0.2.1
4
4
  Summary: Python bindings for transcribe.cpp
5
5
  Project-URL: Homepage, https://github.com/handy-computer/transcribe.cpp
6
6
  Project-URL: Repository, https://github.com/handy-computer/transcribe.cpp
@@ -21,9 +21,9 @@ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
22
22
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
23
  Requires-Python: >=3.9
24
- Requires-Dist: transcribe-cpp-native==0.1.3.*
24
+ Requires-Dist: transcribe-cpp-native==0.2.1.*
25
25
  Provides-Extra: cu12
26
- Requires-Dist: transcribe-cpp-native-cu12==0.1.3.*; extra == 'cu12'
26
+ Requires-Dist: transcribe-cpp-native-cu12==0.2.1.*; extra == 'cu12'
27
27
  Provides-Extra: test
28
28
  Requires-Dist: numpy; extra == 'test'
29
29
  Requires-Dist: pytest>=7; extra == 'test'
@@ -37,6 +37,10 @@ a C/C++ speech-to-text library built on ggml.
37
37
  > **Status: in development.** Until wheels are published, use a locally built
38
38
  > `libtranscribe` through repo auto-discovery or `TRANSCRIBE_LIBRARY`.
39
39
 
40
+ Upgrading from 0.1? See the
41
+ [0.2 migration guide](https://github.com/handy-computer/transcribe.cpp/blob/main/docs/migrating-to-0.2.md),
42
+ including the replacement of `gpu_device=` with exact device objects.
43
+
40
44
  ```python
41
45
  import transcribe_cpp
42
46
 
@@ -75,9 +79,17 @@ Long transcriptions can be cancelled from another thread with
75
79
 
76
80
  ## Backends
77
81
 
78
- `Model(backend=...)` picks the compute device (`"auto"` uses the best
79
- available). `transcribe_cpp.backends()` lists registered backends and
80
- `backend_available(kind)` checks one kind.
82
+ `Model(backend=...)` applies a backend policy (`"auto"` uses the best
83
+ available). `transcribe_cpp.backends()` returns process-local device objects;
84
+ pass one as `Model(device=device)` for exact selection with no fallback. Persist
85
+ a device's `device_id`, not its runtime handle or index. `backend_available(kind)`
86
+ checks whether a backend policy can currently be satisfied.
87
+
88
+ ```python
89
+ device = next(d for d in transcribe_cpp.backends() if d.device_type == "cpu")
90
+ with transcribe_cpp.Model("model.gguf", device=device) as model:
91
+ print(model.device)
92
+ ```
81
93
 
82
94
  | Variable | Effect |
83
95
  |---|---|
@@ -6,6 +6,10 @@ a C/C++ speech-to-text library built on ggml.
6
6
  > **Status: in development.** Until wheels are published, use a locally built
7
7
  > `libtranscribe` through repo auto-discovery or `TRANSCRIBE_LIBRARY`.
8
8
 
9
+ Upgrading from 0.1? See the
10
+ [0.2 migration guide](https://github.com/handy-computer/transcribe.cpp/blob/main/docs/migrating-to-0.2.md),
11
+ including the replacement of `gpu_device=` with exact device objects.
12
+
9
13
  ```python
10
14
  import transcribe_cpp
11
15
 
@@ -44,9 +48,17 @@ Long transcriptions can be cancelled from another thread with
44
48
 
45
49
  ## Backends
46
50
 
47
- `Model(backend=...)` picks the compute device (`"auto"` uses the best
48
- available). `transcribe_cpp.backends()` lists registered backends and
49
- `backend_available(kind)` checks one kind.
51
+ `Model(backend=...)` applies a backend policy (`"auto"` uses the best
52
+ available). `transcribe_cpp.backends()` returns process-local device objects;
53
+ pass one as `Model(device=device)` for exact selection with no fallback. Persist
54
+ a device's `device_id`, not its runtime handle or index. `backend_available(kind)`
55
+ checks whether a backend policy can currently be satisfied.
56
+
57
+ ```python
58
+ device = next(d for d in transcribe_cpp.backends() if d.device_type == "cpu")
59
+ with transcribe_cpp.Model("model.gguf", device=device) as model:
60
+ print(model.device)
61
+ ```
50
62
 
51
63
  | Variable | Effect |
52
64
  |---|---|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "transcribe-cpp"
7
- version = "0.1.3"
7
+ version = "0.2.1"
8
8
  description = "Python bindings for transcribe.cpp"
9
9
  readme = "README.md"
10
10
  # 3.8 is EOL (2024-10); 3.9 is the floor. The binding is ctypes-only, so there
@@ -34,13 +34,13 @@ classifiers = [
34
34
  # packaging fix still resolves); the import-time version/header-hash check in
35
35
  # _library.py is the runtime backstop. check_version_sync.py gates this pin
36
36
  # against include/transcribe.h.
37
- dependencies = ["transcribe-cpp-native==0.1.3.*"]
37
+ dependencies = ["transcribe-cpp-native==0.2.1.*"]
38
38
 
39
39
  [project.optional-dependencies]
40
40
  # Opt-in accelerator providers — ADDITIVE: they install alongside the default
41
41
  # provider and the best one wins at runtime. Same base-version pin contract
42
42
  # as the hard dependency (gated by check_version_sync.py).
43
- cu12 = ["transcribe-cpp-native-cu12==0.1.3.*"]
43
+ cu12 = ["transcribe-cpp-native-cu12==0.2.1.*"]
44
44
  # Test-only deps. Run with: uv run --extra test pytest (from bindings/python).
45
45
  # numpy is here so the numpy PCM-input tests run in every lane instead of
46
46
  # silently skipping wherever numpy happens to be absent.
@@ -24,7 +24,7 @@ import ctypes
24
24
  import os
25
25
  import threading
26
26
  import weakref
27
- from dataclasses import dataclass
27
+ from dataclasses import dataclass, field
28
28
  from typing import Literal, Optional, Sequence, Union
29
29
 
30
30
  from . import _abi, _generated
@@ -46,17 +46,19 @@ from .errors import (
46
46
  raise_for_status,
47
47
  )
48
48
 
49
- __version__ = "0.1.3"
49
+ __version__ = "0.2.1"
50
50
 
51
51
  # String-enum types, exported so callers (and type checkers) can name them.
52
- Backend = Literal["auto", "cpu", "metal", "vulkan", "cpu_accel", "cuda"]
52
+ Backend = Literal["auto", "cpu", "metal", "vulkan", "cpu_accel", "cuda", "rocm"]
53
53
  KVType = Literal["auto", "f32", "f16"]
54
54
  Task = Literal["transcribe", "translate"]
55
55
  Timestamps = Literal["none", "auto", "segment", "word", "token"]
56
+ Diarize = Literal["default", "off", "on"]
57
+ SortformerPreset = Literal["default", "very_high_latency", "high_latency", "low_latency"]
56
58
  CommitPolicy = Literal["auto", "on_finalize", "stable_prefix"]
57
59
  Feature = Literal[
58
60
  "initial_prompt", "temperature_fallback", "long_form",
59
- "cancellation", "pnc", "itn",
61
+ "cancellation", "pnc", "itn", "diarization",
60
62
  ]
61
63
 
62
64
  __all__ = [
@@ -66,6 +68,7 @@ __all__ = [
66
68
  "Session",
67
69
  "Result",
68
70
  "Segment",
71
+ "SpeakerSegment",
69
72
  "Word",
70
73
  "Token",
71
74
  "Capabilities",
@@ -79,11 +82,14 @@ __all__ = [
79
82
  "MoonshineStreamingOptions",
80
83
  "ParakeetStreamOptions",
81
84
  "ParakeetBufferedStreamOptions",
85
+ "SortformerStreamOptions",
82
86
  "VoxtralRealtimeStreamOptions",
83
87
  "Backend",
88
+ "SortformerPreset",
84
89
  "KVType",
85
90
  "Task",
86
91
  "Timestamps",
92
+ "Diarize",
87
93
  "CommitPolicy",
88
94
  "Feature",
89
95
  "TranscribeError",
@@ -165,6 +171,7 @@ _RunParams = _generated.transcribe_run_params
165
171
  _Capabilities = _generated.transcribe_capabilities
166
172
  _Timings = _generated.transcribe_timings
167
173
  _Segment = _generated.transcribe_segment
174
+ _SpeakerSegment = _generated.transcribe_speaker_segment
168
175
  _Word = _generated.transcribe_word
169
176
  _Token = _generated.transcribe_token
170
177
  _StreamParams = _generated.transcribe_stream_params
@@ -180,6 +187,7 @@ _BACKENDS = {
180
187
  "vulkan": _generated.TRANSCRIBE_BACKEND_VULKAN,
181
188
  "cpu_accel": _generated.TRANSCRIBE_BACKEND_CPU_ACCEL,
182
189
  "cuda": _generated.TRANSCRIBE_BACKEND_CUDA,
190
+ "rocm": _generated.TRANSCRIBE_BACKEND_ROCM,
183
191
  }
184
192
  _KV_TYPES = {
185
193
  "auto": _generated.TRANSCRIBE_KV_TYPE_AUTO,
@@ -198,6 +206,11 @@ _TIMESTAMPS = {
198
206
  "token": _generated.TRANSCRIBE_TIMESTAMPS_TOKEN,
199
207
  }
200
208
  _TIMESTAMP_NAMES = {v: k for k, v in _TIMESTAMPS.items()}
209
+ _DIARIZE = {
210
+ "default": _generated.TRANSCRIBE_DIARIZE_MODE_DEFAULT,
211
+ "off": _generated.TRANSCRIBE_DIARIZE_MODE_OFF,
212
+ "on": _generated.TRANSCRIBE_DIARIZE_MODE_ON,
213
+ }
201
214
  _COMMIT_POLICIES = {
202
215
  "auto": _generated.TRANSCRIBE_STREAM_COMMIT_AUTO,
203
216
  "on_finalize": _generated.TRANSCRIBE_STREAM_COMMIT_ON_FINALIZE,
@@ -220,6 +233,7 @@ _FEATURES = {
220
233
  "cancellation": _generated.TRANSCRIBE_FEATURE_CANCELLATION,
221
234
  "pnc": _generated.TRANSCRIBE_FEATURE_PNC,
222
235
  "itn": _generated.TRANSCRIBE_FEATURE_ITN,
236
+ "diarization": _generated.TRANSCRIBE_FEATURE_DIARIZATION,
223
237
  }
224
238
 
225
239
 
@@ -252,13 +266,17 @@ _DEVICE_TYPE_NAMES = {
252
266
  }
253
267
 
254
268
 
255
- @dataclass(frozen=True)
269
+ @dataclass(frozen=True, eq=False)
256
270
  class BackendDevice:
257
- """One registered compute device (owned copies of the C strings)."""
271
+ """One registered compute device (owned copies of the C strings).
272
+
273
+ Equality compares opaque native identity; display index and live memory
274
+ snapshots do not affect whether two values name the same device.
275
+ """
258
276
 
259
277
  name: str
260
278
  description: str
261
- kind: str # "cpu" | "accel" | "metal" | "vulkan" | "cuda" | "sycl" | "gpu" | "unknown"
279
+ kind: str # "cpu" | "accel" | "metal" | "vulkan" | "cuda" | "rocm" | "sycl" | "gpu" | "unknown"
262
280
  # Vendor-agnostic class: "cpu" | "gpu" | "igpu" | "accel", or "unknown" for a
263
281
  # value reported by a runtime newer than this binding (tell such devices
264
282
  # apart by device_id / name, not by this axis).
@@ -269,18 +287,26 @@ class BackendDevice:
269
287
  # (via backends() or Model.device) to refresh; backend-defined and not
270
288
  # comparable across device kinds.
271
289
  memory_free: int
272
- # Registry index of this device the value to pass as ``Model(...,
273
- # gpu_device=index)`` to select it (0 means auto: discrete GPUs are
274
- # probed before integrated).
275
- # None when the device came from Model.device, since the underlying
276
- # transcribe_model_get_device() does not expose an index; correlate such a
277
- # device back to backends() by device_id / name instead. The index is
278
- # order-dependent and not stable across driver updates or hosts.
290
+ # Registry index for display. Exact model selection uses the BackendDevice
291
+ # itself; indices are process-local and not stable across driver updates.
279
292
  index: Optional[int] = None
293
+ # Opaque process-local native device handle. Applications persist device_id,
294
+ # never this value.
295
+ _handle: Optional[int] = field(default=None, repr=False, compare=False)
296
+
297
+ def __eq__(self, other: object) -> bool:
298
+ if self is other:
299
+ return True
300
+ if not isinstance(other, BackendDevice):
301
+ return NotImplemented
302
+ return self._handle is not None and self._handle == other._handle
303
+
304
+ def __hash__(self) -> int:
305
+ return hash(self._handle) if self._handle is not None else object.__hash__(self)
280
306
 
281
307
 
282
- def _backend_device_from_raw(dev, index: Optional[int] = None) -> BackendDevice:
283
- """Build a BackendDevice from a library-filled transcribe_backend_device."""
308
+ def _backend_device_from_raw(dev, handle: Optional[int], index: Optional[int] = None) -> BackendDevice:
309
+ """Build a BackendDevice from a library-filled transcribe_device_info."""
284
310
  return BackendDevice(
285
311
  name=_decode(dev.name),
286
312
  description=_decode(dev.description),
@@ -290,6 +316,7 @@ def _backend_device_from_raw(dev, index: Optional[int] = None) -> BackendDevice:
290
316
  memory_total=int(dev.memory_total),
291
317
  memory_free=int(dev.memory_free),
292
318
  index=index,
319
+ _handle=handle,
293
320
  )
294
321
 
295
322
 
@@ -301,12 +328,15 @@ def backends() -> list[BackendDevice]:
301
328
  Each device's ``memory_free`` is live as of the call; call again to poll
302
329
  a device's available memory over time."""
303
330
  devices = []
304
- for i in range(_lib.transcribe_backend_device_count()):
305
- dev = _generated.transcribe_backend_device()
306
- _lib.transcribe_backend_device_init(_byref(dev))
307
- _check(_lib.transcribe_get_backend_device(i, _byref(dev)),
331
+ for i in range(_lib.transcribe_device_count()):
332
+ handle = _lib.transcribe_device_get(i)
333
+ if not handle:
334
+ continue
335
+ dev = _generated.transcribe_device_info()
336
+ _lib.transcribe_device_info_init(_byref(dev))
337
+ _check(_lib.transcribe_device_get_info(handle, _byref(dev)),
308
338
  f"reading backend device {i}")
309
- devices.append(_backend_device_from_raw(dev, index=i))
339
+ devices.append(_backend_device_from_raw(dev, int(handle), index=i))
310
340
  return devices
311
341
 
312
342
 
@@ -434,6 +464,18 @@ class Segment:
434
464
  n_words: int
435
465
  first_token: int
436
466
  n_tokens: int
467
+ speaker_id: int
468
+
469
+
470
+ @dataclass(frozen=True)
471
+ class SpeakerSegment:
472
+ """One diarized turn. Times are zero when the model attributes text but
473
+ does not provide speaker timing; ``p`` is NaN when unavailable."""
474
+
475
+ t0_ms: int
476
+ t1_ms: int
477
+ speaker_id: int
478
+ p: float
437
479
 
438
480
 
439
481
  @dataclass(frozen=True)
@@ -499,9 +541,14 @@ class Result:
499
541
  returned, so it stays valid after later runs."""
500
542
 
501
543
  text: str
544
+ #: The model's decoded output before family post-processing (diarization
545
+ #: markers, timestamp/special tokens, tag filtering, whitespace trims).
546
+ #: Equal to ``text`` modulo whitespace for families that emit clean text.
547
+ raw_text: str
502
548
  language: str
503
549
  timestamp_kind: str
504
550
  segments: tuple[Segment, ...]
551
+ speaker_segments: tuple[SpeakerSegment, ...]
505
552
  words: tuple[Word, ...]
506
553
  tokens: tuple[Token, ...]
507
554
  timings: Timings
@@ -548,6 +595,14 @@ def _segment_from(s) -> Segment:
548
595
  text=_decode(s.text), t0_ms=s.t0_ms, t1_ms=s.t1_ms,
549
596
  first_word=s.first_word, n_words=s.n_words,
550
597
  first_token=s.first_token, n_tokens=s.n_tokens,
598
+ speaker_id=s.speaker_id,
599
+ )
600
+
601
+
602
+ def _speaker_segment_from(s) -> SpeakerSegment:
603
+ return SpeakerSegment(
604
+ t0_ms=s.t0_ms, t1_ms=s.t1_ms,
605
+ speaker_id=s.speaker_id, p=s.p,
551
606
  )
552
607
 
553
608
 
@@ -583,7 +638,7 @@ def _stream_update_from(u) -> StreamUpdate:
583
638
 
584
639
 
585
640
  def _build_run_params(task, language, target_language, timestamps,
586
- keep_special_tags, spec_k_drafts):
641
+ keep_special_tags, spec_k_drafts, diarize="default"):
587
642
  if not isinstance(spec_k_drafts, int) or spec_k_drafts < -1:
588
643
  raise InvalidArgument(
589
644
  f"spec_k_drafts must be -1 (family default), 0 (disabled), or a "
@@ -593,6 +648,7 @@ def _build_run_params(task, language, target_language, timestamps,
593
648
  _lib.transcribe_run_params_init(_byref(params))
594
649
  params.task = _enum(_TASKS, task, "task")
595
650
  params.timestamps = _enum(_TIMESTAMPS, timestamps, "timestamps")
651
+ params.diarize = _enum(_DIARIZE, diarize, "diarize")
596
652
  params.language = language.encode("utf-8") if language else None
597
653
  params.target_language = target_language.encode("utf-8") if target_language else None
598
654
  params.keep_special_tags = keep_special_tags
@@ -763,6 +819,40 @@ class VoxtralRealtimeStreamOptions(FamilyExtension):
763
819
  ext.min_decode_interval_ms = self.min_decode_interval_ms
764
820
 
765
821
 
822
+ class SortformerStreamOptions(FamilyExtension):
823
+ """Sortformer streaming operating-point options (run slot).
824
+
825
+ Sortformer is a diarizer: a run produces speaker segments, no text.
826
+ ``preset`` selects the latency / accuracy trade-off from the model's
827
+ published menu; ``"default"`` keeps the GGUF-shipped checkpoint
828
+ configuration. ``"very_high_latency"`` (~30 s lookahead) is the
829
+ offline-file operating point; ``"low_latency"`` (~1 s) is the
830
+ real-time point and costs substantially more compute per audio
831
+ second."""
832
+
833
+ _slot = "run"
834
+ _kind = _generated.TRANSCRIBE_EXT_KIND_SORTFORMER_STREAM
835
+ _struct = _generated.transcribe_sortformer_stream_ext
836
+ _init = "transcribe_sortformer_stream_ext_init"
837
+
838
+ _presets = {
839
+ "default": _generated.TRANSCRIBE_SORTFORMER_PRESET_DEFAULT,
840
+ "very_high_latency": _generated.TRANSCRIBE_SORTFORMER_PRESET_VERY_HIGH_LATENCY,
841
+ "high_latency": _generated.TRANSCRIBE_SORTFORMER_PRESET_HIGH_LATENCY,
842
+ "low_latency": _generated.TRANSCRIBE_SORTFORMER_PRESET_LOW_LATENCY,
843
+ }
844
+
845
+ def __init__(self, *, preset: SortformerPreset | None = None):
846
+ if preset is not None and preset not in self._presets:
847
+ raise ValueError(f"unknown sortformer preset {preset!r}; "
848
+ f"expected one of {sorted(self._presets)}")
849
+ self.preset = preset
850
+
851
+ def _apply(self, ext) -> None:
852
+ if self.preset is not None:
853
+ ext.preset = self._presets[self.preset]
854
+
855
+
766
856
  # --- high-level handles ---------------------------------------------------
767
857
 
768
858
 
@@ -783,7 +873,7 @@ class Model:
783
873
  """
784
874
 
785
875
  def __init__(self, path: str | os.PathLike, *,
786
- backend: Backend = "auto", gpu_device: int = 0):
876
+ backend: Backend = "auto", device: BackendDevice | None = None):
787
877
  # Live sessions, tracked weakly: close() must free them before the
788
878
  # model, because transcribe_model_free is only valid once every
789
879
  # derived session is gone (use-after-free otherwise). Created before
@@ -796,7 +886,10 @@ class Model:
796
886
  params = _ModelLoadParams()
797
887
  _lib.transcribe_model_load_params_init(_byref(params))
798
888
  params.backend = _enum(_BACKENDS, backend, backend_source)
799
- params.gpu_device = gpu_device
889
+ if device is not None:
890
+ if not isinstance(device, BackendDevice) or device._handle is None:
891
+ raise TypeError("device must be a BackendDevice returned by backends()")
892
+ params.device = device._handle
800
893
 
801
894
  handle = ctypes.c_void_p()
802
895
  status = _lib.transcribe_model_load_file(
@@ -831,11 +924,14 @@ class Model:
831
924
  live snapshot, so read this again to poll how much device memory is
832
925
  left after the model loaded. Raises if the model has no resolved
833
926
  compute device."""
834
- dev = _generated.transcribe_backend_device()
835
- _lib.transcribe_backend_device_init(_byref(dev))
836
- _check(_lib.transcribe_model_get_device(self._h, _byref(dev)),
837
- "model_get_device")
838
- return _backend_device_from_raw(dev)
927
+ handle = _lib.transcribe_model_device(self._h)
928
+ if not handle:
929
+ raise BackendError("model has no resolved compute device")
930
+ dev = _generated.transcribe_device_info()
931
+ _lib.transcribe_device_info_init(_byref(dev))
932
+ _check(_lib.transcribe_device_get_info(handle, _byref(dev)),
933
+ "device_get_info")
934
+ return _backend_device_from_raw(dev, int(handle))
839
935
 
840
936
  @property
841
937
  def capabilities(self) -> Capabilities:
@@ -983,6 +1079,7 @@ class Session:
983
1079
  language: str | None = None,
984
1080
  target_language: str | None = None,
985
1081
  timestamps: Timestamps = "auto",
1082
+ diarize: Diarize = "default",
986
1083
  keep_special_tags: bool = False,
987
1084
  spec_k_drafts: int = -1,
988
1085
  family: FamilyExtension | None = None) -> Result:
@@ -1000,7 +1097,7 @@ class Session:
1000
1097
  self._cancel.clear()
1001
1098
  array, n_samples = _pcm_to_carray(pcm)
1002
1099
  params = _build_run_params(task, language, target_language, timestamps,
1003
- keep_special_tags, spec_k_drafts)
1100
+ keep_special_tags, spec_k_drafts, diarize)
1004
1101
  ext = self._resolve_family(family, "run") if family is not None else None
1005
1102
  if ext is not None:
1006
1103
  params.family = ctypes.cast(
@@ -1019,6 +1116,7 @@ class Session:
1019
1116
  language: str | None = None,
1020
1117
  target_language: str | None = None,
1021
1118
  timestamps: Timestamps = "auto",
1119
+ diarize: Diarize = "default",
1022
1120
  keep_special_tags: bool = False,
1023
1121
  spec_k_drafts: int = -1,
1024
1122
  family: FamilyExtension | None = None,
@@ -1055,7 +1153,7 @@ class Session:
1055
1153
  counts[k] = n
1056
1154
 
1057
1155
  params = _build_run_params(task, language, target_language, timestamps,
1058
- keep_special_tags, spec_k_drafts)
1156
+ keep_special_tags, spec_k_drafts, diarize)
1059
1157
  ext = self._resolve_family(family, "run") if family is not None else None
1060
1158
  if ext is not None:
1061
1159
  params.family = ctypes.cast(
@@ -1106,6 +1204,7 @@ class Session:
1106
1204
 
1107
1205
  def stream(self, *, task: Task = "transcribe", language: str | None = None,
1108
1206
  target_language: str | None = None, timestamps: Timestamps = "none",
1207
+ diarize: Diarize = "default",
1109
1208
  keep_special_tags: bool = False, commit_policy: CommitPolicy = "auto",
1110
1209
  stable_prefix_agreement_n: int = 0,
1111
1210
  family: FamilyExtension | None = None) -> Stream:
@@ -1120,7 +1219,7 @@ class Session:
1120
1219
  # spec_k_drafts is an offline-decode knob; streaming always uses the
1121
1220
  # family default (-1).
1122
1221
  run_params = _build_run_params(task, language, target_language, timestamps,
1123
- keep_special_tags, -1)
1222
+ keep_special_tags, -1, diarize)
1124
1223
  sp = _StreamParams()
1125
1224
  _lib.transcribe_stream_params_init(_byref(sp))
1126
1225
  sp.commit_policy = _enum(_COMMIT_POLICIES, commit_policy, "commit_policy")
@@ -1152,7 +1251,10 @@ class Session:
1152
1251
  get_word = lambda j, out: _lib.transcribe_get_word(h, j, out)
1153
1252
  n_tok = lambda: _lib.transcribe_n_tokens(h)
1154
1253
  get_tok = lambda j, out: _lib.transcribe_get_token(h, j, out)
1254
+ n_speaker = lambda: _lib.transcribe_n_speaker_segments(h)
1255
+ get_speaker = lambda j, out: _lib.transcribe_get_speaker_segment(h, j, out)
1155
1256
  full_text = _lib.transcribe_full_text(h)
1257
+ raw_text = _lib.transcribe_raw_text(h)
1156
1258
  language = _lib.transcribe_detected_language(h)
1157
1259
  kind = _lib.transcribe_returned_timestamp_kind(h)
1158
1260
  get_tim = lambda out: _lib.transcribe_get_timings(h, out)
@@ -1163,7 +1265,10 @@ class Session:
1163
1265
  get_word = lambda j, out: _lib.transcribe_batch_get_word(h, utt, j, out)
1164
1266
  n_tok = lambda: _lib.transcribe_batch_n_tokens(h, utt)
1165
1267
  get_tok = lambda j, out: _lib.transcribe_batch_get_token(h, utt, j, out)
1268
+ n_speaker = lambda: _lib.transcribe_batch_n_speaker_segments(h, utt)
1269
+ get_speaker = lambda j, out: _lib.transcribe_batch_get_speaker_segment(h, utt, j, out)
1166
1270
  full_text = _lib.transcribe_batch_full_text(h, utt)
1271
+ raw_text = _lib.transcribe_batch_raw_text(h, utt)
1167
1272
  language = _lib.transcribe_batch_detected_language(h, utt)
1168
1273
  kind = _lib.transcribe_batch_returned_timestamp_kind(h, utt)
1169
1274
  get_tim = lambda out: _lib.transcribe_batch_get_timings(h, utt, out)
@@ -1189,15 +1294,24 @@ class Session:
1189
1294
  get_tok(j, _byref(tok))
1190
1295
  tokens.append(_token_from(tok))
1191
1296
 
1297
+ speaker_segments = []
1298
+ for j in range(n_speaker()):
1299
+ speaker = _SpeakerSegment()
1300
+ _lib.transcribe_speaker_segment_init(_byref(speaker))
1301
+ get_speaker(j, _byref(speaker))
1302
+ speaker_segments.append(_speaker_segment_from(speaker))
1303
+
1192
1304
  tm = _Timings()
1193
1305
  _lib.transcribe_timings_init(_byref(tm))
1194
1306
  get_tim(_byref(tm))
1195
1307
 
1196
1308
  return Result(
1197
1309
  text=_decode(full_text),
1310
+ raw_text=_decode(raw_text),
1198
1311
  language=_decode(language),
1199
1312
  timestamp_kind=_TIMESTAMP_NAMES.get(kind, "unknown"),
1200
1313
  segments=tuple(segments),
1314
+ speaker_segments=tuple(speaker_segments),
1201
1315
  words=tuple(words),
1202
1316
  tokens=tuple(tokens),
1203
1317
  timings=_timings_from(tm),
@@ -1339,7 +1453,7 @@ def transcribe(
1339
1453
  pcm: PCMLike,
1340
1454
  *,
1341
1455
  backend: Backend = "auto",
1342
- gpu_device: int = 0,
1456
+ device: BackendDevice | None = None,
1343
1457
  n_threads: int = 0,
1344
1458
  kv_type: KVType = "auto",
1345
1459
  n_ctx: int = 0,
@@ -1347,6 +1461,7 @@ def transcribe(
1347
1461
  language: str | None = None,
1348
1462
  target_language: str | None = None,
1349
1463
  timestamps: Timestamps = "auto",
1464
+ diarize: Diarize = "default",
1350
1465
  keep_special_tags: bool = False,
1351
1466
  spec_k_drafts: int = -1,
1352
1467
  family: FamilyExtension | None = None,
@@ -1356,19 +1471,20 @@ def transcribe(
1356
1471
  *model* may be a path (loaded and freed within this call) or an existing
1357
1472
  Model (reused and left open). Loading a model is not free, so to transcribe
1358
1473
  many clips keep a Model and call ``model.session().run(...)`` yourself; this
1359
- helper is for the one-shot case. ``backend`` / ``gpu_device`` apply only when
1474
+ helper is for the one-shot case. ``backend`` / ``device`` apply only when
1360
1475
  *model* is a path — they are ignored when an already-loaded Model is passed.
1361
1476
  ``family`` / ``spec_k_drafts`` pass through to :meth:`Session.run`.
1362
1477
  """
1363
1478
  session_opts = dict(n_threads=n_threads, kv_type=kv_type, n_ctx=n_ctx)
1364
1479
  run_opts = dict(task=task, language=language, target_language=target_language,
1365
- timestamps=timestamps, keep_special_tags=keep_special_tags,
1480
+ timestamps=timestamps, diarize=diarize,
1481
+ keep_special_tags=keep_special_tags,
1366
1482
  spec_k_drafts=spec_k_drafts, family=family)
1367
1483
 
1368
1484
  if isinstance(model, Model):
1369
1485
  with model.session(**session_opts) as session:
1370
1486
  return session.run(pcm, **run_opts)
1371
1487
 
1372
- with Model(model, backend=backend, gpu_device=gpu_device) as owned:
1488
+ with Model(model, backend=backend, device=device) as owned:
1373
1489
  with owned.session(**session_opts) as session:
1374
1490
  return session.run(pcm, **run_opts)
@@ -13,7 +13,7 @@ import ctypes as _c
13
13
  # Stable digest of the ABI surface below (structs, enums, macros, layout,
14
14
  # prototypes). A native provider package echoes this back so the API
15
15
  # package can reject an ABI-mismatched provider before dlopen.
16
- PUBLIC_HEADER_HASH = "86b16dd97ad1cb58"
16
+ PUBLIC_HEADER_HASH = "7df72bf9e667b8c2"
17
17
 
18
18
  # === enum constants ===
19
19
  TRANSCRIBE_OK = 0
@@ -48,7 +48,8 @@ TRANSCRIBE_ABI_STREAM_UPDATE = 9
48
48
  TRANSCRIBE_ABI_STREAM_TEXT = 10
49
49
  TRANSCRIBE_ABI_SESSION_LIMITS = 11
50
50
  TRANSCRIBE_ABI_EXT = 12
51
- TRANSCRIBE_ABI_BACKEND_DEVICE = 13
51
+ TRANSCRIBE_ABI_DEVICE_INFO = 13
52
+ TRANSCRIBE_ABI_SPEAKER_SEGMENT = 14
52
53
  TRANSCRIBE_LOG_LEVEL_NONE = 0
53
54
  TRANSCRIBE_LOG_LEVEL_INFO = 1
54
55
  TRANSCRIBE_LOG_LEVEL_WARN = 2
@@ -71,6 +72,9 @@ TRANSCRIBE_PNC_MODE_ON = 2
71
72
  TRANSCRIBE_ITN_MODE_DEFAULT = 0
72
73
  TRANSCRIBE_ITN_MODE_OFF = 1
73
74
  TRANSCRIBE_ITN_MODE_ON = 2
75
+ TRANSCRIBE_DIARIZE_MODE_DEFAULT = 0
76
+ TRANSCRIBE_DIARIZE_MODE_OFF = 1
77
+ TRANSCRIBE_DIARIZE_MODE_ON = 2
74
78
  TRANSCRIBE_EXT_SLOT_RUN = 0
75
79
  TRANSCRIBE_EXT_SLOT_STREAM = 1
76
80
  TRANSCRIBE_BACKEND_AUTO = 0
@@ -79,6 +83,7 @@ TRANSCRIBE_BACKEND_METAL = 2
79
83
  TRANSCRIBE_BACKEND_VULKAN = 3
80
84
  TRANSCRIBE_BACKEND_CPU_ACCEL = 4
81
85
  TRANSCRIBE_BACKEND_CUDA = 5
86
+ TRANSCRIBE_BACKEND_ROCM = 6
82
87
  TRANSCRIBE_DEVICE_TYPE_CPU = 0
83
88
  TRANSCRIBE_DEVICE_TYPE_GPU = 1
84
89
  TRANSCRIBE_DEVICE_TYPE_IGPU = 2
@@ -89,6 +94,7 @@ TRANSCRIBE_FEATURE_LONG_FORM = 2
89
94
  TRANSCRIBE_FEATURE_CANCELLATION = 3
90
95
  TRANSCRIBE_FEATURE_PNC = 4
91
96
  TRANSCRIBE_FEATURE_ITN = 5
97
+ TRANSCRIBE_FEATURE_DIARIZATION = 6
92
98
  TRANSCRIBE_STREAM_IDLE = 0
93
99
  TRANSCRIBE_STREAM_ACTIVE = 1
94
100
  TRANSCRIBE_STREAM_FINISHED = 2
@@ -96,6 +102,10 @@ TRANSCRIBE_STREAM_FAILED = 3
96
102
  TRANSCRIBE_STREAM_COMMIT_AUTO = 0
97
103
  TRANSCRIBE_STREAM_COMMIT_ON_FINALIZE = 1
98
104
  TRANSCRIBE_STREAM_COMMIT_STABLE_PREFIX = 2
105
+ TRANSCRIBE_SORTFORMER_PRESET_DEFAULT = 0
106
+ TRANSCRIBE_SORTFORMER_PRESET_VERY_HIGH_LATENCY = 1
107
+ TRANSCRIBE_SORTFORMER_PRESET_HIGH_LATENCY = 2
108
+ TRANSCRIBE_SORTFORMER_PRESET_LOW_LATENCY = 3
99
109
  TRANSCRIBE_WHISPER_PROMPT_FIRST_SEGMENT = 0
100
110
  TRANSCRIBE_WHISPER_PROMPT_ALL_SEGMENTS = 1
101
111
 
@@ -103,13 +113,14 @@ TRANSCRIBE_WHISPER_PROMPT_ALL_SEGMENTS = 1
103
113
  TRANSCRIBE_EXT_KIND_MOONSHINE_STREAMING_STREAM = 1414746957
104
114
  TRANSCRIBE_EXT_KIND_PARAKEET_BUFFERED_STREAM = 1396853584
105
115
  TRANSCRIBE_EXT_KIND_PARAKEET_STREAM = 1414744912
116
+ TRANSCRIBE_EXT_KIND_SORTFORMER_STREAM = 1414743635
106
117
  TRANSCRIBE_EXT_KIND_VOXTRAL_REALTIME_STREAM = 1414746710
107
118
  TRANSCRIBE_EXT_KIND_WHISPER_RUN = 1314015319
108
119
 
109
120
  # === structs ===
110
121
  class transcribe_ext(_c.Structure):
111
122
  pass
112
- class transcribe_backend_device(_c.Structure):
123
+ class transcribe_device_info(_c.Structure):
113
124
  pass
114
125
  class transcribe_model_load_params(_c.Structure):
115
126
  pass
@@ -135,12 +146,16 @@ class transcribe_word(_c.Structure):
135
146
  pass
136
147
  class transcribe_token(_c.Structure):
137
148
  pass
149
+ class transcribe_speaker_segment(_c.Structure):
150
+ pass
138
151
  class transcribe_moonshine_streaming_stream_ext(_c.Structure):
139
152
  pass
140
153
  class transcribe_parakeet_stream_ext(_c.Structure):
141
154
  pass
142
155
  class transcribe_parakeet_buffered_stream_ext(_c.Structure):
143
156
  pass
157
+ class transcribe_sortformer_stream_ext(_c.Structure):
158
+ pass
144
159
  class transcribe_voxtral_realtime_stream_ext(_c.Structure):
145
160
  pass
146
161
  class transcribe_whisper_run_ext(_c.Structure):
@@ -149,22 +164,24 @@ class transcribe_whisper_chunk_trace(_c.Structure):
149
164
  pass
150
165
 
151
166
  transcribe_ext._fields_ = [("size", _c.c_uint64), ("kind", _c.c_uint32)]
152
- transcribe_backend_device._fields_ = [("struct_size", _c.c_uint64), ("name", _c.c_char_p), ("description", _c.c_char_p), ("kind", _c.c_char_p), ("device_id", _c.c_char_p), ("memory_total", _c.c_uint64), ("memory_free", _c.c_uint64), ("device_type", _c.c_int)]
153
- transcribe_model_load_params._fields_ = [("struct_size", _c.c_uint64), ("backend", _c.c_int), ("gpu_device", _c.c_int)]
167
+ transcribe_device_info._fields_ = [("struct_size", _c.c_uint64), ("name", _c.c_char_p), ("description", _c.c_char_p), ("kind", _c.c_char_p), ("device_id", _c.c_char_p), ("memory_total", _c.c_uint64), ("memory_free", _c.c_uint64), ("device_type", _c.c_int)]
168
+ transcribe_model_load_params._fields_ = [("struct_size", _c.c_uint64), ("backend", _c.c_int), ("device", _c.c_void_p)]
154
169
  transcribe_session_params._fields_ = [("struct_size", _c.c_uint64), ("n_threads", _c.c_int), ("kv_type", _c.c_int), ("n_ctx", _c.c_int32)]
155
- transcribe_run_params._fields_ = [("struct_size", _c.c_uint64), ("task", _c.c_int), ("timestamps", _c.c_int), ("pnc", _c.c_int), ("itn", _c.c_int), ("language", _c.c_char_p), ("target_language", _c.c_char_p), ("keep_special_tags", _c.c_bool), ("family", _c.POINTER(transcribe_ext)), ("spec_k_drafts", _c.c_int32)]
170
+ transcribe_run_params._fields_ = [("struct_size", _c.c_uint64), ("task", _c.c_int), ("timestamps", _c.c_int), ("pnc", _c.c_int), ("itn", _c.c_int), ("diarize", _c.c_int), ("language", _c.c_char_p), ("target_language", _c.c_char_p), ("keep_special_tags", _c.c_bool), ("family", _c.POINTER(transcribe_ext)), ("spec_k_drafts", _c.c_int32)]
156
171
  transcribe_capabilities._fields_ = [("struct_size", _c.c_uint64), ("native_sample_rate", _c.c_int32), ("n_languages", _c.c_int), ("languages", _c.POINTER(_c.c_char_p)), ("max_timestamp_kind", _c.c_int), ("supports_language_detect", _c.c_bool), ("supports_translate", _c.c_bool), ("supports_streaming", _c.c_bool), ("supports_spec_decode", _c.c_bool), ("max_audio_ms", _c.c_int64), ("n_translate_target_languages", _c.c_int), ("translate_target_languages", _c.POINTER(_c.c_char_p))]
157
172
  transcribe_session_limits._fields_ = [("struct_size", _c.c_uint64), ("effective_n_ctx", _c.c_int32), ("effective_max_audio_ms", _c.c_int64), ("max_kv_bytes", _c.c_int64)]
158
173
  transcribe_stream_params._fields_ = [("struct_size", _c.c_uint64), ("family", _c.POINTER(transcribe_ext)), ("commit_policy", _c.c_int), ("stable_prefix_agreement_n", _c.c_uint32)]
159
174
  transcribe_stream_update._fields_ = [("struct_size", _c.c_uint64), ("result_changed", _c.c_bool), ("is_final", _c.c_bool), ("revision", _c.c_int32), ("input_received_ms", _c.c_int64), ("audio_committed_ms", _c.c_int64), ("buffered_ms", _c.c_int64), ("committed_changed", _c.c_bool), ("tentative_changed", _c.c_bool)]
160
175
  transcribe_stream_text._fields_ = [("struct_size", _c.c_uint64), ("full_text", _c.c_char_p), ("full_text_bytes", _c.c_uint64), ("committed_text", _c.c_char_p), ("committed_text_bytes", _c.c_uint64), ("tentative_text", _c.c_char_p), ("tentative_text_bytes", _c.c_uint64), ("raw_tentative_start_bytes", _c.c_uint64)]
161
176
  transcribe_timings._fields_ = [("struct_size", _c.c_uint64), ("load_ms", _c.c_float), ("mel_ms", _c.c_float), ("encode_ms", _c.c_float), ("decode_ms", _c.c_float)]
162
- transcribe_segment._fields_ = [("struct_size", _c.c_uint64), ("t0_ms", _c.c_int64), ("t1_ms", _c.c_int64), ("first_word", _c.c_int), ("n_words", _c.c_int), ("first_token", _c.c_int), ("n_tokens", _c.c_int), ("text", _c.c_char_p)]
177
+ transcribe_segment._fields_ = [("struct_size", _c.c_uint64), ("t0_ms", _c.c_int64), ("t1_ms", _c.c_int64), ("first_word", _c.c_int), ("n_words", _c.c_int), ("first_token", _c.c_int), ("n_tokens", _c.c_int), ("text", _c.c_char_p), ("speaker_id", _c.c_int32)]
163
178
  transcribe_word._fields_ = [("struct_size", _c.c_uint64), ("t0_ms", _c.c_int64), ("t1_ms", _c.c_int64), ("seg_index", _c.c_int), ("first_token", _c.c_int), ("n_tokens", _c.c_int), ("text", _c.c_char_p)]
164
179
  transcribe_token._fields_ = [("struct_size", _c.c_uint64), ("id", _c.c_int), ("p", _c.c_float), ("t0_ms", _c.c_int64), ("t1_ms", _c.c_int64), ("seg_index", _c.c_int), ("word_index", _c.c_int), ("text", _c.c_char_p)]
180
+ transcribe_speaker_segment._fields_ = [("struct_size", _c.c_uint64), ("t0_ms", _c.c_int64), ("t1_ms", _c.c_int64), ("speaker_id", _c.c_int32), ("p", _c.c_float)]
165
181
  transcribe_moonshine_streaming_stream_ext._fields_ = [("ext", transcribe_ext), ("min_decode_interval_ms", _c.c_int32)]
166
182
  transcribe_parakeet_stream_ext._fields_ = [("ext", transcribe_ext), ("att_context_right", _c.c_int32)]
167
183
  transcribe_parakeet_buffered_stream_ext._fields_ = [("ext", transcribe_ext), ("left_ms", _c.c_int32), ("chunk_ms", _c.c_int32), ("right_ms", _c.c_int32)]
184
+ transcribe_sortformer_stream_ext._fields_ = [("ext", transcribe_ext), ("preset", _c.c_int)]
168
185
  transcribe_voxtral_realtime_stream_ext._fields_ = [("ext", transcribe_ext), ("num_delay_tokens", _c.c_int32), ("min_decode_interval_ms", _c.c_int32)]
169
186
  transcribe_whisper_run_ext._fields_ = [("ext", transcribe_ext), ("initial_prompt", _c.c_char_p), ("prompt_tokens", _c.POINTER(_c.c_int32)), ("n_prompt_tokens", _c.c_size_t), ("prompt_condition", _c.c_int), ("condition_on_prev_tokens", _c.c_bool), ("max_prev_context_tokens", _c.c_int32), ("temperature", _c.c_float), ("temperature_inc", _c.c_float), ("compression_ratio_thold", _c.c_float), ("logprob_thold", _c.c_float), ("no_speech_thold", _c.c_float), ("seed", _c.c_uint32), ("max_initial_timestamp", _c.c_float)]
170
187
  transcribe_whisper_chunk_trace._fields_ = [("struct_size", _c.c_uint64), ("t0_ms", _c.c_int64), ("t1_ms", _c.c_int64), ("temperature_used", _c.c_float), ("compression_ratio", _c.c_float), ("avg_logprob", _c.c_float), ("no_speech_prob", _c.c_float), ("no_speech_triggered", _c.c_bool), ("n_fallbacks", _c.c_int32)]
@@ -173,7 +190,7 @@ transcribe_whisper_chunk_trace._fields_ = [("struct_size", _c.c_uint64), ("t0_ms
173
190
  # transcribe_abi_struct id per struct (for the native size/align check).
174
191
  ABI_STRUCT_IDS = {
175
192
  'transcribe_ext': 12,
176
- 'transcribe_backend_device': 13,
193
+ 'transcribe_device_info': 13,
177
194
  'transcribe_model_load_params': 0,
178
195
  'transcribe_session_params': 1,
179
196
  'transcribe_run_params': 2,
@@ -186,27 +203,30 @@ ABI_STRUCT_IDS = {
186
203
  'transcribe_segment': 6,
187
204
  'transcribe_word': 7,
188
205
  'transcribe_token': 8,
206
+ 'transcribe_speaker_segment': 14,
189
207
  }
190
208
 
191
209
  # C-compiler layout captured at generation (for offset self-check).
192
210
  STRUCT_LAYOUT = {
193
211
  'transcribe_ext': {'size': 16, 'align': 8, 'offsets': {'size': 0, 'kind': 8}},
194
- 'transcribe_backend_device': {'size': 64, 'align': 8, 'offsets': {'struct_size': 0, 'name': 8, 'description': 16, 'kind': 24, 'device_id': 32, 'memory_total': 40, 'memory_free': 48, 'device_type': 56}},
195
- 'transcribe_model_load_params': {'size': 16, 'align': 8, 'offsets': {'struct_size': 0, 'backend': 8, 'gpu_device': 12}},
212
+ 'transcribe_device_info': {'size': 64, 'align': 8, 'offsets': {'struct_size': 0, 'name': 8, 'description': 16, 'kind': 24, 'device_id': 32, 'memory_total': 40, 'memory_free': 48, 'device_type': 56}},
213
+ 'transcribe_model_load_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'backend': 8, 'device': 16}},
196
214
  'transcribe_session_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'n_threads': 8, 'kv_type': 12, 'n_ctx': 16}},
197
- 'transcribe_run_params': {'size': 64, 'align': 8, 'offsets': {'struct_size': 0, 'task': 8, 'timestamps': 12, 'pnc': 16, 'itn': 20, 'language': 24, 'target_language': 32, 'keep_special_tags': 40, 'family': 48, 'spec_k_drafts': 56}},
215
+ 'transcribe_run_params': {'size': 72, 'align': 8, 'offsets': {'struct_size': 0, 'task': 8, 'timestamps': 12, 'pnc': 16, 'itn': 20, 'diarize': 24, 'language': 32, 'target_language': 40, 'keep_special_tags': 48, 'family': 56, 'spec_k_drafts': 64}},
198
216
  'transcribe_capabilities': {'size': 56, 'align': 8, 'offsets': {'struct_size': 0, 'native_sample_rate': 8, 'n_languages': 12, 'languages': 16, 'max_timestamp_kind': 24, 'supports_language_detect': 28, 'supports_translate': 29, 'supports_streaming': 30, 'supports_spec_decode': 31, 'max_audio_ms': 32, 'n_translate_target_languages': 40, 'translate_target_languages': 48}},
199
217
  'transcribe_session_limits': {'size': 32, 'align': 8, 'offsets': {'struct_size': 0, 'effective_n_ctx': 8, 'effective_max_audio_ms': 16, 'max_kv_bytes': 24}},
200
218
  'transcribe_stream_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'family': 8, 'commit_policy': 16, 'stable_prefix_agreement_n': 20}},
201
219
  'transcribe_stream_update': {'size': 48, 'align': 8, 'offsets': {'struct_size': 0, 'result_changed': 8, 'is_final': 9, 'revision': 12, 'input_received_ms': 16, 'audio_committed_ms': 24, 'buffered_ms': 32, 'committed_changed': 40, 'tentative_changed': 41}},
202
220
  'transcribe_stream_text': {'size': 64, 'align': 8, 'offsets': {'struct_size': 0, 'full_text': 8, 'full_text_bytes': 16, 'committed_text': 24, 'committed_text_bytes': 32, 'tentative_text': 40, 'tentative_text_bytes': 48, 'raw_tentative_start_bytes': 56}},
203
221
  'transcribe_timings': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'load_ms': 8, 'mel_ms': 12, 'encode_ms': 16, 'decode_ms': 20}},
204
- 'transcribe_segment': {'size': 48, 'align': 8, 'offsets': {'struct_size': 0, 't0_ms': 8, 't1_ms': 16, 'first_word': 24, 'n_words': 28, 'first_token': 32, 'n_tokens': 36, 'text': 40}},
222
+ 'transcribe_segment': {'size': 56, 'align': 8, 'offsets': {'struct_size': 0, 't0_ms': 8, 't1_ms': 16, 'first_word': 24, 'n_words': 28, 'first_token': 32, 'n_tokens': 36, 'text': 40, 'speaker_id': 48}},
205
223
  'transcribe_word': {'size': 48, 'align': 8, 'offsets': {'struct_size': 0, 't0_ms': 8, 't1_ms': 16, 'seg_index': 24, 'first_token': 28, 'n_tokens': 32, 'text': 40}},
206
224
  'transcribe_token': {'size': 48, 'align': 8, 'offsets': {'struct_size': 0, 'id': 8, 'p': 12, 't0_ms': 16, 't1_ms': 24, 'seg_index': 32, 'word_index': 36, 'text': 40}},
225
+ 'transcribe_speaker_segment': {'size': 32, 'align': 8, 'offsets': {'struct_size': 0, 't0_ms': 8, 't1_ms': 16, 'speaker_id': 24, 'p': 28}},
207
226
  'transcribe_moonshine_streaming_stream_ext': {'size': 24, 'align': 8, 'offsets': {'ext': 0, 'min_decode_interval_ms': 16}},
208
227
  'transcribe_parakeet_stream_ext': {'size': 24, 'align': 8, 'offsets': {'ext': 0, 'att_context_right': 16}},
209
228
  'transcribe_parakeet_buffered_stream_ext': {'size': 32, 'align': 8, 'offsets': {'ext': 0, 'left_ms': 16, 'chunk_ms': 20, 'right_ms': 24}},
229
+ 'transcribe_sortformer_stream_ext': {'size': 24, 'align': 8, 'offsets': {'ext': 0, 'preset': 16}},
210
230
  'transcribe_voxtral_realtime_stream_ext': {'size': 24, 'align': 8, 'offsets': {'ext': 0, 'num_delay_tokens': 16, 'min_decode_interval_ms': 20}},
211
231
  'transcribe_whisper_run_ext': {'size': 80, 'align': 8, 'offsets': {'ext': 0, 'initial_prompt': 16, 'prompt_tokens': 24, 'n_prompt_tokens': 32, 'prompt_condition': 40, 'condition_on_prev_tokens': 44, 'max_prev_context_tokens': 48, 'temperature': 52, 'temperature_inc': 56, 'compression_ratio_thold': 60, 'logprob_thold': 64, 'no_speech_thold': 68, 'seed': 72, 'max_initial_timestamp': 76}},
212
232
  'transcribe_whisper_chunk_trace': {'size': 48, 'align': 8, 'offsets': {'struct_size': 0, 't0_ms': 8, 't1_ms': 16, 'temperature_used': 24, 'compression_ratio': 28, 'avg_logprob': 32, 'no_speech_prob': 36, 'no_speech_triggered': 40, 'n_fallbacks': 44}},
@@ -221,16 +241,14 @@ def configure(lib):
221
241
  lib.transcribe_abi_struct_size.argtypes = [_c.c_int]
222
242
  lib.transcribe_backend_available.restype = _c.c_bool
223
243
  lib.transcribe_backend_available.argtypes = [_c.c_int]
224
- lib.transcribe_backend_device_count.restype = _c.c_int
225
- lib.transcribe_backend_device_count.argtypes = []
226
- lib.transcribe_backend_device_init.restype = None
227
- lib.transcribe_backend_device_init.argtypes = [_c.POINTER(transcribe_backend_device)]
228
244
  lib.transcribe_batch_detected_language.restype = _c.c_char_p
229
245
  lib.transcribe_batch_detected_language.argtypes = [_c.c_void_p, _c.c_int]
230
246
  lib.transcribe_batch_full_text.restype = _c.c_char_p
231
247
  lib.transcribe_batch_full_text.argtypes = [_c.c_void_p, _c.c_int]
232
248
  lib.transcribe_batch_get_segment.restype = _c.c_int
233
249
  lib.transcribe_batch_get_segment.argtypes = [_c.c_void_p, _c.c_int, _c.c_int, _c.POINTER(transcribe_segment)]
250
+ lib.transcribe_batch_get_speaker_segment.restype = _c.c_int
251
+ lib.transcribe_batch_get_speaker_segment.argtypes = [_c.c_void_p, _c.c_int, _c.c_int, _c.POINTER(transcribe_speaker_segment)]
234
252
  lib.transcribe_batch_get_timings.restype = _c.c_int
235
253
  lib.transcribe_batch_get_timings.argtypes = [_c.c_void_p, _c.c_int, _c.POINTER(transcribe_timings)]
236
254
  lib.transcribe_batch_get_token.restype = _c.c_int
@@ -241,10 +259,14 @@ def configure(lib):
241
259
  lib.transcribe_batch_n_results.argtypes = [_c.c_void_p]
242
260
  lib.transcribe_batch_n_segments.restype = _c.c_int
243
261
  lib.transcribe_batch_n_segments.argtypes = [_c.c_void_p, _c.c_int]
262
+ lib.transcribe_batch_n_speaker_segments.restype = _c.c_int
263
+ lib.transcribe_batch_n_speaker_segments.argtypes = [_c.c_void_p, _c.c_int]
244
264
  lib.transcribe_batch_n_tokens.restype = _c.c_int
245
265
  lib.transcribe_batch_n_tokens.argtypes = [_c.c_void_p, _c.c_int]
246
266
  lib.transcribe_batch_n_words.restype = _c.c_int
247
267
  lib.transcribe_batch_n_words.argtypes = [_c.c_void_p, _c.c_int]
268
+ lib.transcribe_batch_raw_text.restype = _c.c_char_p
269
+ lib.transcribe_batch_raw_text.argtypes = [_c.c_void_p, _c.c_int]
248
270
  lib.transcribe_batch_returned_timestamp_kind.restype = _c.c_int
249
271
  lib.transcribe_batch_returned_timestamp_kind.argtypes = [_c.c_void_p, _c.c_int]
250
272
  lib.transcribe_batch_status.restype = _c.c_int
@@ -255,16 +277,24 @@ def configure(lib):
255
277
  lib.transcribe_close.argtypes = [_c.c_void_p]
256
278
  lib.transcribe_detected_language.restype = _c.c_char_p
257
279
  lib.transcribe_detected_language.argtypes = [_c.c_void_p]
280
+ lib.transcribe_device_count.restype = _c.c_int
281
+ lib.transcribe_device_count.argtypes = []
282
+ lib.transcribe_device_get.restype = _c.c_void_p
283
+ lib.transcribe_device_get.argtypes = [_c.c_int]
284
+ lib.transcribe_device_get_info.restype = _c.c_int
285
+ lib.transcribe_device_get_info.argtypes = [_c.c_void_p, _c.POINTER(transcribe_device_info)]
286
+ lib.transcribe_device_info_init.restype = None
287
+ lib.transcribe_device_info_init.argtypes = [_c.POINTER(transcribe_device_info)]
258
288
  lib.transcribe_ext_check.restype = _c.c_int
259
289
  lib.transcribe_ext_check.argtypes = [_c.POINTER(transcribe_ext), _c.c_uint32, _c.c_uint64]
260
290
  lib.transcribe_full_text.restype = _c.c_char_p
261
291
  lib.transcribe_full_text.argtypes = [_c.c_void_p]
262
- lib.transcribe_get_backend_device.restype = _c.c_int
263
- lib.transcribe_get_backend_device.argtypes = [_c.c_int, _c.POINTER(transcribe_backend_device)]
264
292
  lib.transcribe_get_model.restype = _c.c_void_p
265
293
  lib.transcribe_get_model.argtypes = [_c.c_void_p]
266
294
  lib.transcribe_get_segment.restype = _c.c_int
267
295
  lib.transcribe_get_segment.argtypes = [_c.c_void_p, _c.c_int, _c.POINTER(transcribe_segment)]
296
+ lib.transcribe_get_speaker_segment.restype = _c.c_int
297
+ lib.transcribe_get_speaker_segment.argtypes = [_c.c_void_p, _c.c_int, _c.POINTER(transcribe_speaker_segment)]
268
298
  lib.transcribe_get_timings.restype = _c.c_int
269
299
  lib.transcribe_get_timings.argtypes = [_c.c_void_p, _c.POINTER(transcribe_timings)]
270
300
  lib.transcribe_get_token.restype = _c.c_int
@@ -287,12 +317,12 @@ def configure(lib):
287
317
  lib.transcribe_model_arch_string.argtypes = [_c.c_void_p]
288
318
  lib.transcribe_model_backend.restype = _c.c_char_p
289
319
  lib.transcribe_model_backend.argtypes = [_c.c_void_p]
320
+ lib.transcribe_model_device.restype = _c.c_void_p
321
+ lib.transcribe_model_device.argtypes = [_c.c_void_p]
290
322
  lib.transcribe_model_free.restype = None
291
323
  lib.transcribe_model_free.argtypes = [_c.c_void_p]
292
324
  lib.transcribe_model_get_capabilities.restype = _c.c_int
293
325
  lib.transcribe_model_get_capabilities.argtypes = [_c.c_void_p, _c.POINTER(transcribe_capabilities)]
294
- lib.transcribe_model_get_device.restype = _c.c_int
295
- lib.transcribe_model_get_device.argtypes = [_c.c_void_p, _c.POINTER(transcribe_backend_device)]
296
326
  lib.transcribe_model_load_file.restype = _c.c_int
297
327
  lib.transcribe_model_load_file.argtypes = [_c.c_char_p, _c.POINTER(transcribe_model_load_params), _c.POINTER(_c.c_void_p)]
298
328
  lib.transcribe_model_load_params_init.restype = None
@@ -307,6 +337,8 @@ def configure(lib):
307
337
  lib.transcribe_moonshine_streaming_stream_ext_init.argtypes = [_c.POINTER(transcribe_moonshine_streaming_stream_ext)]
308
338
  lib.transcribe_n_segments.restype = _c.c_int
309
339
  lib.transcribe_n_segments.argtypes = [_c.c_void_p]
340
+ lib.transcribe_n_speaker_segments.restype = _c.c_int
341
+ lib.transcribe_n_speaker_segments.argtypes = [_c.c_void_p]
310
342
  lib.transcribe_n_tokens.restype = _c.c_int
311
343
  lib.transcribe_n_tokens.argtypes = [_c.c_void_p]
312
344
  lib.transcribe_n_words.restype = _c.c_int
@@ -319,6 +351,8 @@ def configure(lib):
319
351
  lib.transcribe_parakeet_stream_ext_init.argtypes = [_c.POINTER(transcribe_parakeet_stream_ext)]
320
352
  lib.transcribe_print_timings.restype = None
321
353
  lib.transcribe_print_timings.argtypes = [_c.c_void_p]
354
+ lib.transcribe_raw_text.restype = _c.c_char_p
355
+ lib.transcribe_raw_text.argtypes = [_c.c_void_p]
322
356
  lib.transcribe_reset_timings.restype = None
323
357
  lib.transcribe_reset_timings.argtypes = [_c.c_void_p]
324
358
  lib.transcribe_returned_timestamp_kind.restype = _c.c_int
@@ -343,6 +377,10 @@ def configure(lib):
343
377
  lib.transcribe_session_params_init.argtypes = [_c.POINTER(transcribe_session_params)]
344
378
  lib.transcribe_set_abort_callback.restype = None
345
379
  lib.transcribe_set_abort_callback.argtypes = [_c.c_void_p, _c.CFUNCTYPE(_c.c_bool, _c.c_void_p), _c.c_void_p]
380
+ lib.transcribe_sortformer_stream_ext_init.restype = None
381
+ lib.transcribe_sortformer_stream_ext_init.argtypes = [_c.POINTER(transcribe_sortformer_stream_ext)]
382
+ lib.transcribe_speaker_segment_init.restype = None
383
+ lib.transcribe_speaker_segment_init.argtypes = [_c.POINTER(transcribe_speaker_segment)]
346
384
  lib.transcribe_status_string.restype = _c.c_char_p
347
385
  lib.transcribe_status_string.argtypes = [_c.c_int]
348
386
  lib.transcribe_stream_begin.restype = _c.c_int
@@ -27,7 +27,7 @@ or an object) with the contract fields:
27
27
  Selecting a provider picks which native artifact loads into the process; the
28
28
  per-model ``backend=`` request is a *separate* axis resolved inside it. Selection
29
29
  policy: explicit ``provider`` argument → ``TRANSCRIBE_NATIVE_PROVIDER`` env var →
30
- best accelerated (CUDA/Metal, then Vulkan) → CPU. A discovered provider whose
30
+ best accelerated (CUDA/ROCm/Metal, then Vulkan) → CPU. A discovered provider whose
31
31
  declared version or header hash disagrees with this binding is a hard error
32
32
  *before* dlopen — pip pins are not enough; this runtime check is the backstop.
33
33
 
@@ -55,7 +55,7 @@ ENTRY_POINT_GROUP = "transcribe_cpp.native"
55
55
 
56
56
  #: Backend-kind preference when auto-selecting among installed providers. Higher
57
57
  #: wins; a provider's rank is the max over the kinds it advertises.
58
- _BACKEND_RANK = {"cuda": 3, "metal": 3, "vulkan": 2, "cpu_accel": 1, "cpu": 1}
58
+ _BACKEND_RANK = {"cuda": 3, "rocm": 3, "metal": 3, "vulkan": 2, "cpu_accel": 1, "cpu": 1}
59
59
 
60
60
  #: Set after a successful load so the package can surface it for diagnostics.
61
61
  #: None means the library came from the dev-tree / explicit-path fallback.
@@ -9,6 +9,8 @@ build configuration the suite runs against.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ from dataclasses import replace
13
+
12
14
  import pytest
13
15
 
14
16
  import transcribe_cpp as t
@@ -21,7 +23,7 @@ def test_at_least_one_device_registered():
21
23
  for dev in devices:
22
24
  assert dev.name
23
25
  assert dev.kind in {
24
- "cpu", "accel", "metal", "vulkan", "cuda", "sycl", "gpu", "unknown"
26
+ "cpu", "accel", "metal", "vulkan", "cuda", "rocm", "sycl", "gpu", "unknown"
25
27
  }
26
28
 
27
29
 
@@ -31,8 +33,8 @@ def test_backends_non_empty():
31
33
 
32
34
 
33
35
  def test_device_index_and_fields():
34
- # Each device carries its registry index (the value Model(..., gpu_device=)
35
- # selects with) and well-formed metadata. Pin the device-selection surface.
36
+ # Each device carries a process-local display index and an opaque selection
37
+ # handle, plus well-formed metadata. Pin the device-selection surface.
36
38
  devices = t.backends()
37
39
  for i, dev in enumerate(devices):
38
40
  assert dev.index == i, f"device {i} reported index {dev.index}"
@@ -46,6 +48,13 @@ def test_device_index_and_fields():
46
48
  assert isinstance(dev.kind, str) and dev.kind
47
49
 
48
50
 
51
+ def test_device_equality_uses_native_identity():
52
+ device = t.backends()[0]
53
+ refreshed = replace(device, memory_free=device.memory_free + 1, index=None)
54
+ assert refreshed == device
55
+ assert hash(refreshed) == hash(device)
56
+
57
+
49
58
  def test_cpu_always_available():
50
59
  # Every shipped configuration includes a CPU backend (compiled in or as
51
60
  # the baseline module); a process without one is mispackaged.
@@ -66,8 +75,8 @@ def test_available_kinds_match_device_list():
66
75
  # answer. (cpu_accel is satisfied by a CPU device per the C contract.)
67
76
  kinds = {d.kind for d in t.backends()}
68
77
  for request, device_kind in (("metal", "metal"), ("vulkan", "vulkan"),
69
- ("cuda", "cuda"), ("cpu", "cpu"),
70
- ("cpu_accel", "cpu")):
78
+ ("cuda", "cuda"), ("rocm", "rocm"),
79
+ ("cpu", "cpu"), ("cpu_accel", "cpu")):
71
80
  assert t.backend_available(request) == (device_kind in kinds), request
72
81
 
73
82
 
@@ -93,6 +102,6 @@ def test_init_backends_rejects_bad_dirs():
93
102
  def test_init_backends_idempotent():
94
103
  lib = t._lib
95
104
  adir = str(_library.artifact_dir()).encode("utf-8")
96
- n = lib.transcribe_backend_device_count()
105
+ n = lib.transcribe_device_count()
97
106
  assert lib.transcribe_init_backends(adir) == 0
98
- assert lib.transcribe_backend_device_count() == n # no re-registration
107
+ assert lib.transcribe_device_count() == n # no re-registration
@@ -0,0 +1,38 @@
1
+ """Exact opaque-device selection tests."""
2
+
3
+ import pytest
4
+
5
+
6
+ def _primary_devices(transcribe_cpp):
7
+ return [d for d in transcribe_cpp.backends() if d.device_type != "accel"]
8
+
9
+
10
+ def test_enumerated_device_can_be_passed_to_model(transcribe_cpp, model_path):
11
+ devices = _primary_devices(transcribe_cpp)
12
+ if not devices:
13
+ pytest.skip("no selectable devices")
14
+
15
+ device = devices[0]
16
+ try:
17
+ with transcribe_cpp.Model(model_path, device=device) as model:
18
+ assert model.device == device
19
+ except transcribe_cpp.BackendError:
20
+ # Registered devices may still fail driver initialization. Exact
21
+ # selection must report that failure rather than choosing another.
22
+ pass
23
+
24
+
25
+ def test_device_argument_rejects_non_device(transcribe_cpp, model_path):
26
+ with pytest.raises(TypeError):
27
+ transcribe_cpp.Model(model_path, device=0)
28
+
29
+
30
+ def test_backend_must_match_explicit_device(transcribe_cpp, model_path):
31
+ gpu = next(
32
+ (d for d in transcribe_cpp.backends() if d.device_type in ("gpu", "igpu")),
33
+ None,
34
+ )
35
+ if gpu is None:
36
+ pytest.skip("no GPU device")
37
+ with pytest.raises(transcribe_cpp.InvalidArgument):
38
+ transcribe_cpp.Model(model_path, backend="cpu", device=gpu)
@@ -22,6 +22,7 @@ ALL_OPTION_TYPES = [
22
22
  t.MoonshineStreamingOptions,
23
23
  t.ParakeetStreamOptions,
24
24
  t.ParakeetBufferedStreamOptions,
25
+ t.SortformerStreamOptions,
25
26
  t.VoxtralRealtimeStreamOptions,
26
27
  ]
27
28
 
@@ -73,6 +74,19 @@ def test_parakeet_buffered_partial_overrides():
73
74
  assert built.right_ms == fresh.right_ms
74
75
 
75
76
 
77
+ def test_sortformer_preset_maps_to_enum_value():
78
+ built = t.SortformerStreamOptions(preset="very_high_latency")._build()
79
+ assert built.preset == _generated.TRANSCRIBE_SORTFORMER_PRESET_VERY_HIGH_LATENCY
80
+ # None keeps the init default (DEFAULT = GGUF-shipped cfg).
81
+ default = t.SortformerStreamOptions()._build()
82
+ assert default.preset == _generated.TRANSCRIBE_SORTFORMER_PRESET_DEFAULT
83
+
84
+
85
+ def test_sortformer_unknown_preset_rejected():
86
+ with pytest.raises(ValueError, match="preset"):
87
+ t.SortformerStreamOptions(preset="ultra_low_latency") # type: ignore[arg-type]
88
+
89
+
76
90
  # --- model-gated: resolve_family validation + a real extension run ----------
77
91
 
78
92
 
@@ -130,7 +144,7 @@ def test_run_batch_accepts_family(model_path, audio_pcm):
130
144
  def test_supports_probe_all_features(model_path):
131
145
  with t.Model(model_path) as model:
132
146
  for feature in ("initial_prompt", "temperature_fallback", "long_form",
133
- "cancellation", "pnc", "itn"):
147
+ "cancellation", "pnc", "itn", "diarization"):
134
148
  assert model.supports(feature) in (True, False)
135
149
  with pytest.raises(t.InvalidArgument, match="unknown feature"):
136
150
  model.supports("levitation")
@@ -84,6 +84,7 @@ def test_matches_request(request_str, expected):
84
84
 
85
85
  def test_rank_orders_accelerated_above_cpu():
86
86
  assert make("m", ["metal", "cpu"]).rank == 3
87
+ assert make("r", ["rocm", "cpu"]).rank == 3
87
88
  assert make("v", ["vulkan"]).rank == 2
88
89
  assert make("c", ["cpu"]).rank == 1
89
90
  assert make("u", ["wat"]).rank == 0
@@ -386,7 +386,7 @@ wheels = [
386
386
 
387
387
  [[package]]
388
388
  name = "transcribe-cpp"
389
- version = "0.0.1"
389
+ version = "0.2.1"
390
390
  source = { editable = "." }
391
391
 
392
392
  [package.optional-dependencies]
@@ -402,8 +402,8 @@ test = [
402
402
  requires-dist = [
403
403
  { name = "numpy", marker = "extra == 'test'" },
404
404
  { name = "pytest", marker = "extra == 'test'", specifier = ">=7" },
405
- { name = "transcribe-cpp-native", specifier = "==0.0.1.*" },
406
- { name = "transcribe-cpp-native-cu12", marker = "extra == 'cu12'", specifier = "==0.0.1.*" },
405
+ { name = "transcribe-cpp-native", specifier = "==0.2.1.*" },
406
+ { name = "transcribe-cpp-native-cu12", marker = "extra == 'cu12'", specifier = "==0.2.1.*" },
407
407
  ]
408
408
 
409
409
  [[package]]
@@ -1,55 +0,0 @@
1
- """Model-gated device-selection tests.
2
-
3
- These take the ``model_path`` / ``transcribe_cpp`` fixtures, which ``skip``
4
- when the default whisper-tiny.en asset is absent (override with
5
- ``TRANSCRIBE_SMOKE_MODEL``). They pin the device-selection surface added
6
- alongside the per-device ``index`` field: ``Model.device`` reports where the
7
- model landed (its ``.index`` is ``None`` because it did not come from
8
- enumeration), and an out-of-range / negative ``gpu_device`` is rejected with
9
- ``InvalidArgument``.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import pytest
15
-
16
- import transcribe_cpp as t
17
-
18
-
19
- def test_model_device_matches_enumeration(transcribe_cpp, model_path):
20
- # The model lands on some registered device. Model.device does not come
21
- # from enumeration, so its .index is None; correlate it back to backends()
22
- # by name (and by device_id when that is reported).
23
- with transcribe_cpp.Model(model_path) as model:
24
- dev = model.device
25
- assert isinstance(dev, transcribe_cpp.BackendDevice)
26
- assert dev.index is None, "Model.device should not carry a registry index"
27
-
28
- devices = transcribe_cpp.backends()
29
- by_name = [d for d in devices if d.name == dev.name]
30
- assert by_name, (
31
- f"model device {dev.name!r} not found among backends() "
32
- f"{[d.name for d in devices]}"
33
- )
34
- if dev.device_id is not None:
35
- assert any(d.device_id == dev.device_id for d in by_name), (
36
- f"model device_id {dev.device_id!r} matched no enumerated device"
37
- )
38
-
39
-
40
- def test_negative_gpu_device_rejected(transcribe_cpp, model_path):
41
- with pytest.raises(transcribe_cpp.InvalidArgument):
42
- transcribe_cpp.Model(model_path, gpu_device=-1)
43
-
44
-
45
- def test_out_of_range_gpu_device_rejected(transcribe_cpp, model_path):
46
- bad = len(transcribe_cpp.backends()) + 1000
47
- with pytest.raises(transcribe_cpp.InvalidArgument):
48
- transcribe_cpp.Model(model_path, gpu_device=bad)
49
-
50
-
51
- def test_cpu_backend_with_gpu_index_rejected(transcribe_cpp, model_path):
52
- # Hardware-independent: a CPU backend has no GPU to select, so a non-zero
53
- # gpu_device is invalid regardless of what hardware is present.
54
- with pytest.raises(transcribe_cpp.InvalidArgument):
55
- transcribe_cpp.Model(model_path, backend="cpu", gpu_device=1)
File without changes