transcribe-cpp 0.0.3__tar.gz → 0.0.5__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 (29) hide show
  1. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/PKG-INFO +3 -3
  2. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/pyproject.toml +3 -3
  3. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/src/transcribe_cpp/__init__.py +80 -8
  4. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/src/transcribe_cpp/_generated.py +9 -3
  5. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/tests/test_backends.py +21 -0
  6. transcribe_cpp-0.0.5/tests/test_device_select.py +55 -0
  7. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/.gitignore +0 -0
  8. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/LICENSE +0 -0
  9. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/README.md +0 -0
  10. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/_generate/README.md +0 -0
  11. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/_generate/check_version_sync.py +0 -0
  12. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/_generate/generate.py +0 -0
  13. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/examples/stream_wav.py +0 -0
  14. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/examples/transcribe_wav.py +0 -0
  15. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/src/transcribe_cpp/_abi.py +0 -0
  16. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/src/transcribe_cpp/_library.py +0 -0
  17. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/src/transcribe_cpp/errors.py +0 -0
  18. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/src/transcribe_cpp/py.typed +0 -0
  19. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/tests/conftest.py +0 -0
  20. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/tests/test_abi.py +0 -0
  21. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/tests/test_errors.py +0 -0
  22. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/tests/test_example.py +0 -0
  23. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/tests/test_family_ext.py +0 -0
  24. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/tests/test_lifetime.py +0 -0
  25. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/tests/test_pcm.py +0 -0
  26. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/tests/test_provider_discovery.py +0 -0
  27. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/tests/test_streaming.py +0 -0
  28. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/tests/test_transcribe.py +0 -0
  29. {transcribe_cpp-0.0.3 → transcribe_cpp-0.0.5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: transcribe-cpp
3
- Version: 0.0.3
3
+ Version: 0.0.5
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.0.3.*
24
+ Requires-Dist: transcribe-cpp-native==0.0.5.*
25
25
  Provides-Extra: cu12
26
- Requires-Dist: transcribe-cpp-native-cu12==0.0.3.*; extra == 'cu12'
26
+ Requires-Dist: transcribe-cpp-native-cu12==0.0.5.*; extra == 'cu12'
27
27
  Provides-Extra: test
28
28
  Requires-Dist: numpy; extra == 'test'
29
29
  Requires-Dist: pytest>=7; extra == 'test'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "transcribe-cpp"
7
- version = "0.0.3"
7
+ version = "0.0.5"
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.0.3.*"]
37
+ dependencies = ["transcribe-cpp-native==0.0.5.*"]
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.0.3.*"]
43
+ cu12 = ["transcribe-cpp-native-cu12==0.0.5.*"]
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.
@@ -25,7 +25,7 @@ import os
25
25
  import threading
26
26
  import weakref
27
27
  from dataclasses import dataclass
28
- from typing import Literal, Sequence, Union
28
+ from typing import Literal, Optional, Sequence, Union
29
29
 
30
30
  from . import _abi, _generated
31
31
  from ._library import _base_version, artifact_dir, load_library, selected_provider
@@ -46,7 +46,7 @@ from .errors import (
46
46
  raise_for_status,
47
47
  )
48
48
 
49
- __version__ = "0.0.3"
49
+ __version__ = "0.0.5"
50
50
 
51
51
  # String-enum types, exported so callers (and type checkers) can name them.
52
52
  Backend = Literal["auto", "cpu", "metal", "vulkan", "cpu_accel", "cuda"]
@@ -146,6 +146,10 @@ if _artifact is not None:
146
146
 
147
147
  _byref = ctypes.byref
148
148
 
149
+ # transcribe_tokenize returns INT32_MIN to signal "this model has no tokenizer
150
+ # encode path" (distinct from the negative grow-buffer signal).
151
+ _INT32_MIN = -(2 ** 31)
152
+
149
153
  # Callback function types — must match the generated argtypes for
150
154
  # transcribe_log_set / transcribe_set_abort_callback. ctypes acquires the GIL
151
155
  # when C invokes these, and a CFUNCTYPE instance must be kept alive for as long
@@ -240,6 +244,14 @@ def native_provider() -> str | None:
240
244
  return selected_provider()
241
245
 
242
246
 
247
+ _DEVICE_TYPE_NAMES = {
248
+ _generated.TRANSCRIBE_DEVICE_TYPE_CPU: "cpu",
249
+ _generated.TRANSCRIBE_DEVICE_TYPE_GPU: "gpu",
250
+ _generated.TRANSCRIBE_DEVICE_TYPE_IGPU: "igpu",
251
+ _generated.TRANSCRIBE_DEVICE_TYPE_ACCEL: "accel",
252
+ }
253
+
254
+
243
255
  @dataclass(frozen=True)
244
256
  class BackendDevice:
245
257
  """One registered compute device (owned copies of the C strings)."""
@@ -247,23 +259,53 @@ class BackendDevice:
247
259
  name: str
248
260
  description: str
249
261
  kind: str # "cpu" | "accel" | "metal" | "vulkan" | "cuda" | "sycl" | "gpu" | "unknown"
262
+ # Vendor-agnostic class: "cpu" | "gpu" | "igpu" | "accel", or "unknown" for a
263
+ # value reported by a runtime newer than this binding (tell such devices
264
+ # apart by device_id / name, not by this axis).
265
+ device_type: str
266
+ device_id: Optional[str] # stable hw id (PCI bus id), or None (e.g. Metal)
267
+ memory_total: int # reported capacity in bytes, or 0 if unreported
268
+ # Available bytes — a SNAPSHOT at query time, or 0 if unreported. Re-query
269
+ # (via backends() or Model.device) to refresh; backend-defined and not
270
+ # comparable across device kinds.
271
+ 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.
278
+ index: Optional[int] = None
279
+
280
+
281
+ def _backend_device_from_raw(dev, index: Optional[int] = None) -> BackendDevice:
282
+ """Build a BackendDevice from a library-filled transcribe_backend_device."""
283
+ return BackendDevice(
284
+ name=_decode(dev.name),
285
+ description=_decode(dev.description),
286
+ kind=_decode(dev.kind),
287
+ device_type=_DEVICE_TYPE_NAMES.get(dev.device_type, "unknown"),
288
+ device_id=_decode(dev.device_id) if dev.device_id else None,
289
+ memory_total=int(dev.memory_total),
290
+ memory_free=int(dev.memory_free),
291
+ index=index,
292
+ )
250
293
 
251
294
 
252
295
  def backends() -> list[BackendDevice]:
253
296
  """The compute devices registered with the native runtime — what the
254
297
  process can actually run on, after backend-module loading and graceful
255
- degradation (e.g. a Vulkan module skipped on a machine without Vulkan)."""
298
+ degradation (e.g. a Vulkan module skipped on a machine without Vulkan).
299
+
300
+ Each device's ``memory_free`` is live as of the call; call again to poll
301
+ a device's available memory over time."""
256
302
  devices = []
257
303
  for i in range(_lib.transcribe_backend_device_count()):
258
304
  dev = _generated.transcribe_backend_device()
259
305
  _lib.transcribe_backend_device_init(_byref(dev))
260
306
  _check(_lib.transcribe_get_backend_device(i, _byref(dev)),
261
307
  f"reading backend device {i}")
262
- devices.append(BackendDevice(
263
- name=_decode(dev.name),
264
- description=_decode(dev.description),
265
- kind=_decode(dev.kind),
266
- ))
308
+ devices.append(_backend_device_from_raw(dev, index=i))
267
309
  return devices
268
310
 
269
311
 
@@ -781,6 +823,18 @@ class Model:
781
823
  def backend(self) -> str:
782
824
  return _decode(_lib.transcribe_model_backend(self._h))
783
825
 
826
+ @property
827
+ def device(self) -> BackendDevice:
828
+ """The compute device this model is running on. ``memory_free`` is a
829
+ live snapshot, so read this again to poll how much device memory is
830
+ left after the model loaded. Raises if the model has no resolved
831
+ compute device."""
832
+ dev = _generated.transcribe_backend_device()
833
+ _lib.transcribe_backend_device_init(_byref(dev))
834
+ _check(_lib.transcribe_model_get_device(self._h, _byref(dev)),
835
+ "model_get_device")
836
+ return _backend_device_from_raw(dev)
837
+
784
838
  @property
785
839
  def capabilities(self) -> Capabilities:
786
840
  caps = _Capabilities()
@@ -815,6 +869,24 @@ class Model:
815
869
  return bool(_lib.transcribe_model_accepts_ext_kind(
816
870
  self._h, _EXT_SLOTS[options._slot], options._kind))
817
871
 
872
+ def tokenize(self, text: str) -> list[int]:
873
+ """Tokenize plain UTF-8 ``text`` into the model's vocabulary ids (no
874
+ BOS/EOS, no special tags). Raises :class:`NotImplementedByModel` for
875
+ families whose tokenizer has no encode path (e.g. SentencePiece today).
876
+ """
877
+ data = text.encode("utf-8")
878
+ cap = max(len(data), 16)
879
+ while True:
880
+ buf = (ctypes.c_int32 * cap)()
881
+ n = _lib.transcribe_tokenize(self._h, data, buf, cap)
882
+ if n == _INT32_MIN:
883
+ raise NotImplementedByModel(
884
+ "model tokenizer has no encode path")
885
+ if n < 0: # buffer too small: library asked for -n slots, retry
886
+ cap = -n
887
+ continue
888
+ return [buf[i] for i in range(n)]
889
+
818
890
  def session(self, *, n_threads: int = 0, kv_type: KVType = "auto",
819
891
  n_ctx: int = 0) -> "Session":
820
892
  return Session(self, n_threads=n_threads, kv_type=kv_type, n_ctx=n_ctx)
@@ -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 = "2273744299e5aa65"
16
+ PUBLIC_HEADER_HASH = "ebe6a6816e34a24e"
17
17
 
18
18
  # === enum constants ===
19
19
  TRANSCRIBE_OK = 0
@@ -79,6 +79,10 @@ TRANSCRIBE_BACKEND_METAL = 2
79
79
  TRANSCRIBE_BACKEND_VULKAN = 3
80
80
  TRANSCRIBE_BACKEND_CPU_ACCEL = 4
81
81
  TRANSCRIBE_BACKEND_CUDA = 5
82
+ TRANSCRIBE_DEVICE_TYPE_CPU = 0
83
+ TRANSCRIBE_DEVICE_TYPE_GPU = 1
84
+ TRANSCRIBE_DEVICE_TYPE_IGPU = 2
85
+ TRANSCRIBE_DEVICE_TYPE_ACCEL = 3
82
86
  TRANSCRIBE_FEATURE_INITIAL_PROMPT = 0
83
87
  TRANSCRIBE_FEATURE_TEMPERATURE_FALLBACK = 1
84
88
  TRANSCRIBE_FEATURE_LONG_FORM = 2
@@ -145,7 +149,7 @@ class transcribe_whisper_chunk_trace(_c.Structure):
145
149
  pass
146
150
 
147
151
  transcribe_ext._fields_ = [("size", _c.c_uint64), ("kind", _c.c_uint32)]
148
- transcribe_backend_device._fields_ = [("struct_size", _c.c_uint64), ("name", _c.c_char_p), ("description", _c.c_char_p), ("kind", _c.c_char_p)]
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)]
149
153
  transcribe_model_load_params._fields_ = [("struct_size", _c.c_uint64), ("backend", _c.c_int), ("gpu_device", _c.c_int)]
150
154
  transcribe_session_params._fields_ = [("struct_size", _c.c_uint64), ("n_threads", _c.c_int), ("kv_type", _c.c_int), ("n_ctx", _c.c_int32)]
151
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)]
@@ -187,7 +191,7 @@ ABI_STRUCT_IDS = {
187
191
  # C-compiler layout captured at generation (for offset self-check).
188
192
  STRUCT_LAYOUT = {
189
193
  'transcribe_ext': {'size': 16, 'align': 8, 'offsets': {'size': 0, 'kind': 8}},
190
- 'transcribe_backend_device': {'size': 32, 'align': 8, 'offsets': {'struct_size': 0, 'name': 8, 'description': 16, 'kind': 24}},
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}},
191
195
  'transcribe_model_load_params': {'size': 16, 'align': 8, 'offsets': {'struct_size': 0, 'backend': 8, 'gpu_device': 12}},
192
196
  'transcribe_session_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'n_threads': 8, 'kv_type': 12, 'n_ctx': 16}},
193
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}},
@@ -287,6 +291,8 @@ def configure(lib):
287
291
  lib.transcribe_model_free.argtypes = [_c.c_void_p]
288
292
  lib.transcribe_model_get_capabilities.restype = _c.c_int
289
293
  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)]
290
296
  lib.transcribe_model_load_file.restype = _c.c_int
291
297
  lib.transcribe_model_load_file.argtypes = [_c.c_char_p, _c.POINTER(transcribe_model_load_params), _c.POINTER(_c.c_void_p)]
292
298
  lib.transcribe_model_load_params_init.restype = None
@@ -25,6 +25,27 @@ def test_at_least_one_device_registered():
25
25
  }
26
26
 
27
27
 
28
+ def test_backends_non_empty():
29
+ # A CPU device always exists, so the list is never empty.
30
+ assert t.backends(), "backends() returned an empty list — a CPU device must exist"
31
+
32
+
33
+ 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
+ devices = t.backends()
37
+ for i, dev in enumerate(devices):
38
+ assert dev.index == i, f"device {i} reported index {dev.index}"
39
+ assert dev.device_type in {"cpu", "gpu", "igpu", "accel", "unknown"}, (
40
+ f"device {i} device_type {dev.device_type!r}"
41
+ )
42
+ assert dev.memory_total >= 0
43
+ assert dev.memory_free >= 0
44
+ assert dev.device_id is None or isinstance(dev.device_id, str)
45
+ assert isinstance(dev.name, str) and dev.name
46
+ assert isinstance(dev.kind, str) and dev.kind
47
+
48
+
28
49
  def test_cpu_always_available():
29
50
  # Every shipped configuration includes a CPU backend (compiled in or as
30
51
  # the baseline module); a process without one is mispackaged.
@@ -0,0 +1,55 @@
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
File without changes
File without changes