transcribe-cpp 0.1.2__tar.gz → 0.2.0__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.2 → transcribe_cpp-0.2.0}/.gitignore +4 -0
  2. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/PKG-INFO +19 -7
  3. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/README.md +15 -3
  4. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/pyproject.toml +3 -3
  5. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/src/transcribe_cpp/__init__.py +152 -35
  6. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/src/transcribe_cpp/_generated.py +58 -20
  7. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/src/transcribe_cpp/_library.py +2 -2
  8. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/tests/test_backends.py +16 -7
  9. transcribe_cpp-0.2.0/tests/test_device_select.py +38 -0
  10. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/tests/test_family_ext.py +15 -1
  11. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/tests/test_provider_discovery.py +1 -0
  12. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/uv.lock +3 -3
  13. transcribe_cpp-0.1.2/tests/test_device_select.py +0 -55
  14. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/LICENSE +0 -0
  15. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/_generate/README.md +0 -0
  16. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/_generate/check_version_sync.py +0 -0
  17. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/_generate/generate.py +0 -0
  18. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/examples/stream_wav.py +0 -0
  19. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/examples/transcribe_wav.py +0 -0
  20. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/src/transcribe_cpp/_abi.py +0 -0
  21. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/src/transcribe_cpp/errors.py +0 -0
  22. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/src/transcribe_cpp/py.typed +0 -0
  23. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/tests/conftest.py +0 -0
  24. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/tests/test_abi.py +0 -0
  25. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/tests/test_errors.py +0 -0
  26. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/tests/test_example.py +0 -0
  27. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/tests/test_lifetime.py +0 -0
  28. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/tests/test_pcm.py +0 -0
  29. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/tests/test_streaming.py +0 -0
  30. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/tests/test_transcribe.py +0 -0
  31. {transcribe_cpp-0.1.2 → transcribe_cpp-0.2.0}/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.2
3
+ Version: 0.2.0
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.2.*
24
+ Requires-Dist: transcribe-cpp-native==0.2.0.*
25
25
  Provides-Extra: cu12
26
- Requires-Dist: transcribe-cpp-native-cu12==0.1.2.*; extra == 'cu12'
26
+ Requires-Dist: transcribe-cpp-native-cu12==0.2.0.*; 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.2"
7
+ version = "0.2.0"
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.2.*"]
37
+ dependencies = ["transcribe-cpp-native==0.2.0.*"]
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.2.*"]
43
+ cu12 = ["transcribe-cpp-native-cu12==0.2.0.*"]
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.2"
49
+ __version__ = "0.2.0"
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,17 +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 selects the auto / first device).
274
- # None when the device came from Model.device, since the underlying
275
- # transcribe_model_get_device() does not expose an index; correlate such a
276
- # device back to backends() by device_id / name instead. The index is
277
- # 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.
278
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)
279
306
 
280
307
 
281
- def _backend_device_from_raw(dev, index: Optional[int] = None) -> BackendDevice:
282
- """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."""
283
310
  return BackendDevice(
284
311
  name=_decode(dev.name),
285
312
  description=_decode(dev.description),
@@ -289,6 +316,7 @@ def _backend_device_from_raw(dev, index: Optional[int] = None) -> BackendDevice:
289
316
  memory_total=int(dev.memory_total),
290
317
  memory_free=int(dev.memory_free),
291
318
  index=index,
319
+ _handle=handle,
292
320
  )
293
321
 
294
322
 
@@ -300,12 +328,15 @@ def backends() -> list[BackendDevice]:
300
328
  Each device's ``memory_free`` is live as of the call; call again to poll
301
329
  a device's available memory over time."""
302
330
  devices = []
303
- for i in range(_lib.transcribe_backend_device_count()):
304
- dev = _generated.transcribe_backend_device()
305
- _lib.transcribe_backend_device_init(_byref(dev))
306
- _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)),
307
338
  f"reading backend device {i}")
308
- devices.append(_backend_device_from_raw(dev, index=i))
339
+ devices.append(_backend_device_from_raw(dev, int(handle), index=i))
309
340
  return devices
310
341
 
311
342
 
@@ -433,6 +464,18 @@ class Segment:
433
464
  n_words: int
434
465
  first_token: int
435
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
436
479
 
437
480
 
438
481
  @dataclass(frozen=True)
@@ -498,9 +541,14 @@ class Result:
498
541
  returned, so it stays valid after later runs."""
499
542
 
500
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
501
548
  language: str
502
549
  timestamp_kind: str
503
550
  segments: tuple[Segment, ...]
551
+ speaker_segments: tuple[SpeakerSegment, ...]
504
552
  words: tuple[Word, ...]
505
553
  tokens: tuple[Token, ...]
506
554
  timings: Timings
@@ -547,6 +595,14 @@ def _segment_from(s) -> Segment:
547
595
  text=_decode(s.text), t0_ms=s.t0_ms, t1_ms=s.t1_ms,
548
596
  first_word=s.first_word, n_words=s.n_words,
549
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,
550
606
  )
551
607
 
552
608
 
@@ -582,7 +638,7 @@ def _stream_update_from(u) -> StreamUpdate:
582
638
 
583
639
 
584
640
  def _build_run_params(task, language, target_language, timestamps,
585
- keep_special_tags, spec_k_drafts):
641
+ keep_special_tags, spec_k_drafts, diarize="default"):
586
642
  if not isinstance(spec_k_drafts, int) or spec_k_drafts < -1:
587
643
  raise InvalidArgument(
588
644
  f"spec_k_drafts must be -1 (family default), 0 (disabled), or a "
@@ -592,6 +648,7 @@ def _build_run_params(task, language, target_language, timestamps,
592
648
  _lib.transcribe_run_params_init(_byref(params))
593
649
  params.task = _enum(_TASKS, task, "task")
594
650
  params.timestamps = _enum(_TIMESTAMPS, timestamps, "timestamps")
651
+ params.diarize = _enum(_DIARIZE, diarize, "diarize")
595
652
  params.language = language.encode("utf-8") if language else None
596
653
  params.target_language = target_language.encode("utf-8") if target_language else None
597
654
  params.keep_special_tags = keep_special_tags
@@ -762,6 +819,40 @@ class VoxtralRealtimeStreamOptions(FamilyExtension):
762
819
  ext.min_decode_interval_ms = self.min_decode_interval_ms
763
820
 
764
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
+
765
856
  # --- high-level handles ---------------------------------------------------
766
857
 
767
858
 
@@ -782,7 +873,7 @@ class Model:
782
873
  """
783
874
 
784
875
  def __init__(self, path: str | os.PathLike, *,
785
- backend: Backend = "auto", gpu_device: int = 0):
876
+ backend: Backend = "auto", device: BackendDevice | None = None):
786
877
  # Live sessions, tracked weakly: close() must free them before the
787
878
  # model, because transcribe_model_free is only valid once every
788
879
  # derived session is gone (use-after-free otherwise). Created before
@@ -795,7 +886,10 @@ class Model:
795
886
  params = _ModelLoadParams()
796
887
  _lib.transcribe_model_load_params_init(_byref(params))
797
888
  params.backend = _enum(_BACKENDS, backend, backend_source)
798
- 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
799
893
 
800
894
  handle = ctypes.c_void_p()
801
895
  status = _lib.transcribe_model_load_file(
@@ -830,11 +924,14 @@ class Model:
830
924
  live snapshot, so read this again to poll how much device memory is
831
925
  left after the model loaded. Raises if the model has no resolved
832
926
  compute device."""
833
- dev = _generated.transcribe_backend_device()
834
- _lib.transcribe_backend_device_init(_byref(dev))
835
- _check(_lib.transcribe_model_get_device(self._h, _byref(dev)),
836
- "model_get_device")
837
- 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))
838
935
 
839
936
  @property
840
937
  def capabilities(self) -> Capabilities:
@@ -982,6 +1079,7 @@ class Session:
982
1079
  language: str | None = None,
983
1080
  target_language: str | None = None,
984
1081
  timestamps: Timestamps = "auto",
1082
+ diarize: Diarize = "default",
985
1083
  keep_special_tags: bool = False,
986
1084
  spec_k_drafts: int = -1,
987
1085
  family: FamilyExtension | None = None) -> Result:
@@ -999,7 +1097,7 @@ class Session:
999
1097
  self._cancel.clear()
1000
1098
  array, n_samples = _pcm_to_carray(pcm)
1001
1099
  params = _build_run_params(task, language, target_language, timestamps,
1002
- keep_special_tags, spec_k_drafts)
1100
+ keep_special_tags, spec_k_drafts, diarize)
1003
1101
  ext = self._resolve_family(family, "run") if family is not None else None
1004
1102
  if ext is not None:
1005
1103
  params.family = ctypes.cast(
@@ -1018,6 +1116,7 @@ class Session:
1018
1116
  language: str | None = None,
1019
1117
  target_language: str | None = None,
1020
1118
  timestamps: Timestamps = "auto",
1119
+ diarize: Diarize = "default",
1021
1120
  keep_special_tags: bool = False,
1022
1121
  spec_k_drafts: int = -1,
1023
1122
  family: FamilyExtension | None = None,
@@ -1054,7 +1153,7 @@ class Session:
1054
1153
  counts[k] = n
1055
1154
 
1056
1155
  params = _build_run_params(task, language, target_language, timestamps,
1057
- keep_special_tags, spec_k_drafts)
1156
+ keep_special_tags, spec_k_drafts, diarize)
1058
1157
  ext = self._resolve_family(family, "run") if family is not None else None
1059
1158
  if ext is not None:
1060
1159
  params.family = ctypes.cast(
@@ -1105,6 +1204,7 @@ class Session:
1105
1204
 
1106
1205
  def stream(self, *, task: Task = "transcribe", language: str | None = None,
1107
1206
  target_language: str | None = None, timestamps: Timestamps = "none",
1207
+ diarize: Diarize = "default",
1108
1208
  keep_special_tags: bool = False, commit_policy: CommitPolicy = "auto",
1109
1209
  stable_prefix_agreement_n: int = 0,
1110
1210
  family: FamilyExtension | None = None) -> Stream:
@@ -1119,7 +1219,7 @@ class Session:
1119
1219
  # spec_k_drafts is an offline-decode knob; streaming always uses the
1120
1220
  # family default (-1).
1121
1221
  run_params = _build_run_params(task, language, target_language, timestamps,
1122
- keep_special_tags, -1)
1222
+ keep_special_tags, -1, diarize)
1123
1223
  sp = _StreamParams()
1124
1224
  _lib.transcribe_stream_params_init(_byref(sp))
1125
1225
  sp.commit_policy = _enum(_COMMIT_POLICIES, commit_policy, "commit_policy")
@@ -1151,7 +1251,10 @@ class Session:
1151
1251
  get_word = lambda j, out: _lib.transcribe_get_word(h, j, out)
1152
1252
  n_tok = lambda: _lib.transcribe_n_tokens(h)
1153
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)
1154
1256
  full_text = _lib.transcribe_full_text(h)
1257
+ raw_text = _lib.transcribe_raw_text(h)
1155
1258
  language = _lib.transcribe_detected_language(h)
1156
1259
  kind = _lib.transcribe_returned_timestamp_kind(h)
1157
1260
  get_tim = lambda out: _lib.transcribe_get_timings(h, out)
@@ -1162,7 +1265,10 @@ class Session:
1162
1265
  get_word = lambda j, out: _lib.transcribe_batch_get_word(h, utt, j, out)
1163
1266
  n_tok = lambda: _lib.transcribe_batch_n_tokens(h, utt)
1164
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)
1165
1270
  full_text = _lib.transcribe_batch_full_text(h, utt)
1271
+ raw_text = _lib.transcribe_batch_raw_text(h, utt)
1166
1272
  language = _lib.transcribe_batch_detected_language(h, utt)
1167
1273
  kind = _lib.transcribe_batch_returned_timestamp_kind(h, utt)
1168
1274
  get_tim = lambda out: _lib.transcribe_batch_get_timings(h, utt, out)
@@ -1188,15 +1294,24 @@ class Session:
1188
1294
  get_tok(j, _byref(tok))
1189
1295
  tokens.append(_token_from(tok))
1190
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
+
1191
1304
  tm = _Timings()
1192
1305
  _lib.transcribe_timings_init(_byref(tm))
1193
1306
  get_tim(_byref(tm))
1194
1307
 
1195
1308
  return Result(
1196
1309
  text=_decode(full_text),
1310
+ raw_text=_decode(raw_text),
1197
1311
  language=_decode(language),
1198
1312
  timestamp_kind=_TIMESTAMP_NAMES.get(kind, "unknown"),
1199
1313
  segments=tuple(segments),
1314
+ speaker_segments=tuple(speaker_segments),
1200
1315
  words=tuple(words),
1201
1316
  tokens=tuple(tokens),
1202
1317
  timings=_timings_from(tm),
@@ -1338,7 +1453,7 @@ def transcribe(
1338
1453
  pcm: PCMLike,
1339
1454
  *,
1340
1455
  backend: Backend = "auto",
1341
- gpu_device: int = 0,
1456
+ device: BackendDevice | None = None,
1342
1457
  n_threads: int = 0,
1343
1458
  kv_type: KVType = "auto",
1344
1459
  n_ctx: int = 0,
@@ -1346,6 +1461,7 @@ def transcribe(
1346
1461
  language: str | None = None,
1347
1462
  target_language: str | None = None,
1348
1463
  timestamps: Timestamps = "auto",
1464
+ diarize: Diarize = "default",
1349
1465
  keep_special_tags: bool = False,
1350
1466
  spec_k_drafts: int = -1,
1351
1467
  family: FamilyExtension | None = None,
@@ -1355,19 +1471,20 @@ def transcribe(
1355
1471
  *model* may be a path (loaded and freed within this call) or an existing
1356
1472
  Model (reused and left open). Loading a model is not free, so to transcribe
1357
1473
  many clips keep a Model and call ``model.session().run(...)`` yourself; this
1358
- 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
1359
1475
  *model* is a path — they are ignored when an already-loaded Model is passed.
1360
1476
  ``family`` / ``spec_k_drafts`` pass through to :meth:`Session.run`.
1361
1477
  """
1362
1478
  session_opts = dict(n_threads=n_threads, kv_type=kv_type, n_ctx=n_ctx)
1363
1479
  run_opts = dict(task=task, language=language, target_language=target_language,
1364
- timestamps=timestamps, keep_special_tags=keep_special_tags,
1480
+ timestamps=timestamps, diarize=diarize,
1481
+ keep_special_tags=keep_special_tags,
1365
1482
  spec_k_drafts=spec_k_drafts, family=family)
1366
1483
 
1367
1484
  if isinstance(model, Model):
1368
1485
  with model.session(**session_opts) as session:
1369
1486
  return session.run(pcm, **run_opts)
1370
1487
 
1371
- with Model(model, backend=backend, gpu_device=gpu_device) as owned:
1488
+ with Model(model, backend=backend, device=device) as owned:
1372
1489
  with owned.session(**session_opts) as session:
1373
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.0"
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.0.*" },
406
+ { name = "transcribe-cpp-native-cu12", marker = "extra == 'cu12'", specifier = "==0.2.0.*" },
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