transcribe-cpp 0.2.4__tar.gz → 0.3.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 (32) hide show
  1. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/PKG-INFO +14 -3
  2. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/README.md +11 -0
  3. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/pyproject.toml +3 -3
  4. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/src/transcribe_cpp/__init__.py +84 -23
  5. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/src/transcribe_cpp/_generated.py +28 -3
  6. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/src/transcribe_cpp/errors.py +13 -0
  7. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_errors.py +9 -0
  8. transcribe_cpp-0.3.1/tests/test_prompting.py +116 -0
  9. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/uv.lock +3 -3
  10. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/.gitignore +0 -0
  11. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/LICENSE +0 -0
  12. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/_generate/README.md +0 -0
  13. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/_generate/check_version_sync.py +0 -0
  14. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/_generate/generate.py +0 -0
  15. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/examples/stream_wav.py +0 -0
  16. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/examples/transcribe_wav.py +0 -0
  17. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/src/transcribe_cpp/_abi.py +0 -0
  18. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/src/transcribe_cpp/_library.py +0 -0
  19. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/src/transcribe_cpp/py.typed +0 -0
  20. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/conftest.py +0 -0
  21. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_abi.py +0 -0
  22. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_backends.py +0 -0
  23. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_device_select.py +0 -0
  24. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_example.py +0 -0
  25. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_family_ext.py +0 -0
  26. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_lifetime.py +0 -0
  27. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_pcm.py +0 -0
  28. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_provider_discovery.py +0 -0
  29. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_streaming.py +0 -0
  30. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_text_controls.py +0 -0
  31. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_transcribe.py +0 -0
  32. {transcribe_cpp-0.2.4 → transcribe_cpp-0.3.1}/tests/test_utf8_paths.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: transcribe-cpp
3
- Version: 0.2.4
3
+ Version: 0.3.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.2.4.*
24
+ Requires-Dist: transcribe-cpp-native==0.3.1.*
25
25
  Provides-Extra: cu12
26
- Requires-Dist: transcribe-cpp-native-cu12==0.2.4.*; extra == 'cu12'
26
+ Requires-Dist: transcribe-cpp-native-cu12==0.3.1.*; extra == 'cu12'
27
27
  Provides-Extra: test
28
28
  Requires-Dist: numpy; extra == 'test'
29
29
  Requires-Dist: pytest>=7; extra == 'test'
@@ -74,6 +74,17 @@ and the one-shot `transcribe()` helper.
74
74
  result = session.run(pcm, pnc="off", itn="on")
75
75
  ```
76
76
 
77
+ ### Prompting
78
+
79
+ `vocabulary` (custom terms), `prompt` (context, or the instruction under
80
+ `task="instruct"`) and `prefix` (text the model continues from) take effect
81
+ where `model.supports()` reports `"vocabulary"`, `"context_prompt"`,
82
+ `"instruct"` or `"transcript_prefix"`.
83
+
84
+ ```python
85
+ result = session.run(pcm, vocabulary=["Kubernetes", "gRPC"])
86
+ ```
87
+
77
88
  Streaming models expose incremental transcription with committed/tentative
78
89
  text views — see `examples/stream_wav.py`:
79
90
 
@@ -43,6 +43,17 @@ and the one-shot `transcribe()` helper.
43
43
  result = session.run(pcm, pnc="off", itn="on")
44
44
  ```
45
45
 
46
+ ### Prompting
47
+
48
+ `vocabulary` (custom terms), `prompt` (context, or the instruction under
49
+ `task="instruct"`) and `prefix` (text the model continues from) take effect
50
+ where `model.supports()` reports `"vocabulary"`, `"context_prompt"`,
51
+ `"instruct"` or `"transcript_prefix"`.
52
+
53
+ ```python
54
+ result = session.run(pcm, vocabulary=["Kubernetes", "gRPC"])
55
+ ```
56
+
46
57
  Streaming models expose incremental transcription with committed/tentative
47
58
  text views — see `examples/stream_wav.py`:
48
59
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "transcribe-cpp"
7
- version = "0.2.4"
7
+ version = "0.3.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.2.4.*"]
37
+ dependencies = ["transcribe-cpp-native==0.3.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.2.4.*"]
43
+ cu12 = ["transcribe-cpp-native-cu12==0.3.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.
@@ -39,6 +39,7 @@ from .errors import (
39
39
  ModelLoadError,
40
40
  NotImplementedByModel,
41
41
  OutOfMemory,
42
+ OutputRepetition,
42
43
  OutputTruncated,
43
44
  TranscribeError,
44
45
  UnsupportedRequest,
@@ -46,12 +47,12 @@ from .errors import (
46
47
  raise_for_status,
47
48
  )
48
49
 
49
- __version__ = "0.2.4"
50
+ __version__ = "0.3.1"
50
51
 
51
52
  # String-enum types, exported so callers (and type checkers) can name them.
52
53
  Backend = Literal["auto", "cpu", "metal", "vulkan", "cpu_accel", "cuda", "rocm"]
53
54
  KVType = Literal["auto", "f32", "f16"]
54
- Task = Literal["transcribe", "translate"]
55
+ Task = Literal["transcribe", "translate", "instruct"]
55
56
  Timestamps = Literal["none", "auto", "segment", "word", "token"]
56
57
  Pnc = Literal["default", "off", "on"]
57
58
  Itn = Literal["default", "off", "on"]
@@ -61,6 +62,7 @@ CommitPolicy = Literal["auto", "on_finalize", "stable_prefix"]
61
62
  Feature = Literal[
62
63
  "initial_prompt", "temperature_fallback", "long_form",
63
64
  "cancellation", "pnc", "itn", "diarization",
65
+ "vocabulary", "context_prompt", "instruct", "transcript_prefix",
64
66
  ]
65
67
 
66
68
  __all__ = [
@@ -108,6 +110,7 @@ __all__ = [
108
110
  "Aborted",
109
111
  "InputTooLong",
110
112
  "OutputTruncated",
113
+ "OutputRepetition",
111
114
  "native_version",
112
115
  "native_commit",
113
116
  "library_path",
@@ -201,6 +204,7 @@ _KV_TYPES = {
201
204
  _TASKS = {
202
205
  "transcribe": _generated.TRANSCRIBE_TASK_TRANSCRIBE,
203
206
  "translate": _generated.TRANSCRIBE_TASK_TRANSLATE,
207
+ "instruct": _generated.TRANSCRIBE_TASK_INSTRUCT,
204
208
  }
205
209
  _TIMESTAMPS = {
206
210
  "none": _generated.TRANSCRIBE_TIMESTAMPS_NONE,
@@ -248,6 +252,10 @@ _FEATURES = {
248
252
  "pnc": _generated.TRANSCRIBE_FEATURE_PNC,
249
253
  "itn": _generated.TRANSCRIBE_FEATURE_ITN,
250
254
  "diarization": _generated.TRANSCRIBE_FEATURE_DIARIZATION,
255
+ "vocabulary": _generated.TRANSCRIBE_FEATURE_VOCABULARY,
256
+ "context_prompt": _generated.TRANSCRIBE_FEATURE_CONTEXT_PROMPT,
257
+ "instruct": _generated.TRANSCRIBE_FEATURE_INSTRUCT,
258
+ "transcript_prefix": _generated.TRANSCRIBE_FEATURE_TRANSCRIPT_PREFIX,
251
259
  }
252
260
 
253
261
 
@@ -651,9 +659,17 @@ def _stream_update_from(u) -> StreamUpdate:
651
659
  )
652
660
 
653
661
 
662
+ def _cstr(value: str, name: str) -> bytes:
663
+ """UTF-8 bytes for a C string option; a NUL would silently cut it in C."""
664
+ if "\x00" in value:
665
+ raise InvalidArgument(f"{name} contains a NUL character")
666
+ return value.encode("utf-8")
667
+
668
+
654
669
  def _build_run_params(task, language, target_language, timestamps,
655
670
  keep_special_tags, spec_k_drafts, diarize="default",
656
- pnc="default", itn="default"):
671
+ pnc="default", itn="default", vocabulary=None,
672
+ prompt=None, prefix=None):
657
673
  if not isinstance(spec_k_drafts, int) or spec_k_drafts < -1:
658
674
  raise InvalidArgument(
659
675
  f"spec_k_drafts must be -1 (family default), 0 (disabled), or a "
@@ -666,10 +682,27 @@ def _build_run_params(task, language, target_language, timestamps,
666
682
  params.pnc = _enum(_PNC, pnc, "pnc")
667
683
  params.itn = _enum(_ITN, itn, "itn")
668
684
  params.diarize = _enum(_DIARIZE, diarize, "diarize")
669
- params.language = language.encode("utf-8") if language else None
670
- params.target_language = target_language.encode("utf-8") if target_language else None
685
+ params.language = _cstr(language, "language") if language else None
686
+ params.target_language = _cstr(target_language, "target_language") if target_language else None
671
687
  params.keep_special_tags = keep_special_tags
672
688
  params.spec_k_drafts = spec_k_drafts
689
+ if vocabulary is not None:
690
+ if isinstance(vocabulary, (str, bytes)):
691
+ raise InvalidArgument("vocabulary must be a sequence of terms, not a single string")
692
+ if isinstance(vocabulary, (set, frozenset, dict)):
693
+ raise InvalidArgument("vocabulary is in priority order; pass a list, not a set or dict")
694
+ vocabulary = list(vocabulary) # an iterator would be used up by the check below
695
+ if not all(isinstance(t, str) for t in vocabulary):
696
+ raise InvalidArgument("vocabulary terms must be strings")
697
+ terms = [_cstr(t, "vocabulary") for t in vocabulary]
698
+ if terms:
699
+ arr = (ctypes.c_char_p * len(terms))(*terms)
700
+ params.vocabulary = ctypes.cast(arr, ctypes.POINTER(ctypes.c_char_p))
701
+ params.n_vocabulary = len(terms)
702
+ # The C struct holds raw pointers; keep the buffers alive with it.
703
+ params._prompting_keepalive = (arr, terms)
704
+ params.prompt = _cstr(prompt, "prompt") if prompt else None
705
+ params.prefix = _cstr(prefix, "prefix") if prefix else None
673
706
  return params
674
707
 
675
708
 
@@ -738,7 +771,7 @@ class WhisperRunOptions(FamilyExtension):
738
771
 
739
772
  def _apply(self, ext) -> None:
740
773
  if self.initial_prompt is not None:
741
- ext.initial_prompt = self.initial_prompt.encode("utf-8")
774
+ ext.initial_prompt = _cstr(self.initial_prompt, "initial_prompt")
742
775
  if self.condition_on_prev_tokens is not None:
743
776
  ext.condition_on_prev_tokens = self.condition_on_prev_tokens
744
777
  if self.temperature is not None:
@@ -979,8 +1012,7 @@ class Model:
979
1012
  )
980
1013
 
981
1014
  def supports(self, feature: Feature) -> bool:
982
- """Whether the model exposes a behavioral feature (initial prompt,
983
- temperature fallback, long-form, cancellation, pnc, itn)."""
1015
+ """Whether the model exposes a behavioral feature (see ``Feature``)."""
984
1016
  return bool(_lib.transcribe_model_supports(
985
1017
  self._h, _enum(_FEATURES, feature, "feature")))
986
1018
 
@@ -1101,7 +1133,10 @@ class Session:
1101
1133
  diarize: Diarize = "default",
1102
1134
  keep_special_tags: bool = False,
1103
1135
  spec_k_drafts: int = -1,
1104
- family: FamilyExtension | None = None) -> Result:
1136
+ family: FamilyExtension | None = None,
1137
+ vocabulary: Sequence[str] | None = None,
1138
+ prompt: str | None = None,
1139
+ prefix: str | None = None) -> Result:
1105
1140
  """Transcribe 16 kHz mono float32 PCM and return a materialized Result.
1106
1141
 
1107
1142
  ``pnc`` controls punctuation/capitalization and ``itn`` controls
@@ -1111,14 +1146,23 @@ class Session:
1111
1146
  ``spec_k_drafts`` tunes speculative decoding on models whose
1112
1147
  capabilities advertise ``supports_spec_decode`` (-1 = family default,
1113
1148
  0 = disabled, >0 = draft length; silently ignored elsewhere).
1114
-
1115
- On ``Aborted`` (via :meth:`cancel`) and ``OutputTruncated`` the
1116
- partial transcript is preserved and attached to the exception as
1117
- ``partial_result``."""
1149
+ ``vocabulary`` (terms, priority order), ``prompt`` and ``prefix`` are
1150
+ the generic prompting inputs; probe ``model.supports()`` for
1151
+ ``"vocabulary"``, ``"context_prompt"``, ``"instruct"`` and
1152
+ ``"transcript_prefix"``. With ``task="instruct"`` the ``prompt`` is the
1153
+ required instruction and the output is free text. Unsupported
1154
+ vocabulary/context is ignored with a warning; an unsupported prefix
1155
+ or instruct task raises. With ``prefix``, ``text`` holds only the
1156
+ continuation and ``raw_text`` leads with the prefix.
1157
+
1158
+ On ``Aborted`` (via :meth:`cancel`) and ``OutputTruncated`` (including
1159
+ its ``OutputRepetition`` subclass) the partial transcript is preserved
1160
+ and attached to the exception as ``partial_result``."""
1118
1161
  self._cancel.clear()
1119
1162
  array, n_samples = _pcm_to_carray(pcm)
1120
1163
  params = _build_run_params(task, language, target_language, timestamps,
1121
- keep_special_tags, spec_k_drafts, diarize, pnc, itn)
1164
+ keep_special_tags, spec_k_drafts, diarize, pnc, itn,
1165
+ vocabulary, prompt, prefix)
1122
1166
  ext = self._resolve_family(family, "run") if family is not None else None
1123
1167
  if ext is not None:
1124
1168
  params.family = ctypes.cast(
@@ -1128,7 +1172,8 @@ class Session:
1128
1172
  "transcribe_run")
1129
1173
  except (Aborted, OutputTruncated) as exc:
1130
1174
  # The C API preserves the partial transcript on the session for
1131
- # exactly these two statuses; surface it rather than discard it.
1175
+ # these statuses (OutputRepetition included, as an OutputTruncated
1176
+ # subclass); surface it rather than discard it.
1132
1177
  exc.partial_result = self._materialize()
1133
1178
  raise
1134
1179
  return self._materialize()
@@ -1143,7 +1188,9 @@ class Session:
1143
1188
  keep_special_tags: bool = False,
1144
1189
  spec_k_drafts: int = -1,
1145
1190
  family: FamilyExtension | None = None,
1146
- return_exceptions: bool = False) -> list[Result | TranscribeError]:
1191
+ return_exceptions: bool = False,
1192
+ vocabulary: Sequence[str] | None = None,
1193
+ prompt: str | None = None) -> list[Result | TranscribeError]:
1147
1194
  """Transcribe several utterances in one dispatch — one Result each.
1148
1195
 
1149
1196
  Families with a batched compute path process every utterance in a single
@@ -1160,7 +1207,10 @@ class Session:
1160
1207
  view (``Result`` or ``TranscribeError`` each) so completed work is
1161
1208
  never discarded. With ``return_exceptions=True`` no exception is
1162
1209
  raised for utterance failures and that mixed list is returned
1163
- directly (the ``asyncio.gather`` convention)."""
1210
+ directly (the ``asyncio.gather`` convention).
1211
+
1212
+ ``vocabulary`` / ``prompt`` apply to every utterance (see :meth:`run`);
1213
+ a transcript prefix is per-utterance and so is not accepted here."""
1164
1214
  self._cancel.clear()
1165
1215
  pcms = list(pcms)
1166
1216
  if not pcms:
@@ -1176,7 +1226,8 @@ class Session:
1176
1226
  counts[k] = n
1177
1227
 
1178
1228
  params = _build_run_params(task, language, target_language, timestamps,
1179
- keep_special_tags, spec_k_drafts, diarize, pnc, itn)
1229
+ keep_special_tags, spec_k_drafts, diarize, pnc, itn,
1230
+ vocabulary, prompt)
1180
1231
  ext = self._resolve_family(family, "run") if family is not None else None
1181
1232
  if ext is not None:
1182
1233
  params.family = ctypes.cast(
@@ -1231,19 +1282,24 @@ class Session:
1231
1282
  diarize: Diarize = "default",
1232
1283
  keep_special_tags: bool = False, commit_policy: CommitPolicy = "auto",
1233
1284
  stable_prefix_agreement_n: int = 0,
1234
- family: FamilyExtension | None = None) -> Stream:
1285
+ family: FamilyExtension | None = None,
1286
+ vocabulary: Sequence[str] | None = None,
1287
+ prompt: str | None = None) -> Stream:
1235
1288
  """Begin streaming on this session and return a Stream to feed audio to.
1236
1289
 
1237
1290
  Requires a model whose capabilities advertise ``supports_streaming``;
1238
1291
  otherwise raises NotImplementedByModel. ``family`` is an optional
1239
1292
  family-specific stream extension (e.g. MoonshineStreamingOptions). The
1240
1293
  session is single-threaded and runs at most one stream at a time. Use
1241
- the Stream as a context manager so it is reset when you are done."""
1294
+ the Stream as a context manager so it is reset when you are done.
1295
+ ``vocabulary`` and ``prompt`` are as in :meth:`run`; ``task="instruct"``
1296
+ raises."""
1242
1297
  self._cancel.clear()
1243
1298
  # spec_k_drafts is an offline-decode knob; streaming always uses the
1244
1299
  # family default (-1).
1245
1300
  run_params = _build_run_params(task, language, target_language, timestamps,
1246
- keep_special_tags, -1, diarize, pnc, itn)
1301
+ keep_special_tags, -1, diarize, pnc, itn,
1302
+ vocabulary, prompt)
1247
1303
  sp = _StreamParams()
1248
1304
  _lib.transcribe_stream_params_init(_byref(sp))
1249
1305
  sp.commit_policy = _enum(_COMMIT_POLICIES, commit_policy, "commit_policy")
@@ -1496,6 +1552,9 @@ def transcribe(
1496
1552
  keep_special_tags: bool = False,
1497
1553
  spec_k_drafts: int = -1,
1498
1554
  family: FamilyExtension | None = None,
1555
+ vocabulary: Sequence[str] | None = None,
1556
+ prompt: str | None = None,
1557
+ prefix: str | None = None,
1499
1558
  ) -> Result:
1500
1559
  """Transcribe *pcm* in one call and return a materialized Result.
1501
1560
 
@@ -1504,13 +1563,15 @@ def transcribe(
1504
1563
  many clips keep a Model and call ``model.session().run(...)`` yourself; this
1505
1564
  helper is for the one-shot case. ``backend`` / ``device`` apply only when
1506
1565
  *model* is a path — they are ignored when an already-loaded Model is passed.
1507
- ``family`` / ``spec_k_drafts`` pass through to :meth:`Session.run`.
1566
+ ``family`` / ``spec_k_drafts`` and the prompting inputs (``vocabulary``,
1567
+ ``prompt``, ``prefix``) pass through to :meth:`Session.run`.
1508
1568
  """
1509
1569
  session_opts = dict(n_threads=n_threads, kv_type=kv_type, n_ctx=n_ctx)
1510
1570
  run_opts = dict(task=task, language=language, target_language=target_language,
1511
1571
  timestamps=timestamps, pnc=pnc, itn=itn, diarize=diarize,
1512
1572
  keep_special_tags=keep_special_tags,
1513
- spec_k_drafts=spec_k_drafts, family=family)
1573
+ spec_k_drafts=spec_k_drafts, family=family,
1574
+ vocabulary=vocabulary, prompt=prompt, prefix=prefix)
1514
1575
 
1515
1576
  if isinstance(model, Model):
1516
1577
  with model.session(**session_opts) as session:
@@ -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 = "7df72bf9e667b8c2"
16
+ PUBLIC_HEADER_HASH = "57b1af43650f195d"
17
17
 
18
18
  # === enum constants ===
19
19
  TRANSCRIBE_OK = 0
@@ -35,6 +35,7 @@ TRANSCRIBE_ERR_UNSUPPORTED_PNC = 15
35
35
  TRANSCRIBE_ERR_UNSUPPORTED_ITN = 16
36
36
  TRANSCRIBE_ERR_INPUT_TOO_LONG = 17
37
37
  TRANSCRIBE_ERR_OUTPUT_TRUNCATED = 18
38
+ TRANSCRIBE_ERR_OUTPUT_REPETITION = 19
38
39
  TRANSCRIBE_ABI_MODEL_LOAD_PARAMS = 0
39
40
  TRANSCRIBE_ABI_SESSION_PARAMS = 1
40
41
  TRANSCRIBE_ABI_RUN_PARAMS = 2
@@ -50,6 +51,7 @@ TRANSCRIBE_ABI_SESSION_LIMITS = 11
50
51
  TRANSCRIBE_ABI_EXT = 12
51
52
  TRANSCRIBE_ABI_DEVICE_INFO = 13
52
53
  TRANSCRIBE_ABI_SPEAKER_SEGMENT = 14
54
+ TRANSCRIBE_ABI_BACKEND_INIT_PARAMS = 15
53
55
  TRANSCRIBE_LOG_LEVEL_NONE = 0
54
56
  TRANSCRIBE_LOG_LEVEL_INFO = 1
55
57
  TRANSCRIBE_LOG_LEVEL_WARN = 2
@@ -58,6 +60,7 @@ TRANSCRIBE_LOG_LEVEL_DEBUG = 4
58
60
  TRANSCRIBE_LOG_LEVEL_CONT = 5
59
61
  TRANSCRIBE_TASK_TRANSCRIBE = 0
60
62
  TRANSCRIBE_TASK_TRANSLATE = 1
63
+ TRANSCRIBE_TASK_INSTRUCT = 2
61
64
  TRANSCRIBE_TIMESTAMPS_NONE = 0
62
65
  TRANSCRIBE_TIMESTAMPS_AUTO = 1
63
66
  TRANSCRIBE_TIMESTAMPS_SEGMENT = 2
@@ -95,6 +98,10 @@ TRANSCRIBE_FEATURE_CANCELLATION = 3
95
98
  TRANSCRIBE_FEATURE_PNC = 4
96
99
  TRANSCRIBE_FEATURE_ITN = 5
97
100
  TRANSCRIBE_FEATURE_DIARIZATION = 6
101
+ TRANSCRIBE_FEATURE_VOCABULARY = 7
102
+ TRANSCRIBE_FEATURE_CONTEXT_PROMPT = 8
103
+ TRANSCRIBE_FEATURE_INSTRUCT = 9
104
+ TRANSCRIBE_FEATURE_TRANSCRIPT_PREFIX = 10
98
105
  TRANSCRIBE_STREAM_IDLE = 0
99
106
  TRANSCRIBE_STREAM_ACTIVE = 1
100
107
  TRANSCRIBE_STREAM_FINISHED = 2
@@ -110,6 +117,13 @@ TRANSCRIBE_WHISPER_PROMPT_FIRST_SEGMENT = 0
110
117
  TRANSCRIBE_WHISPER_PROMPT_ALL_SEGMENTS = 1
111
118
 
112
119
  # === macro constants (integer object-like macros) ===
120
+ TRANSCRIBE_BACKEND_MASK_ALL = 4294967295
121
+ TRANSCRIBE_BACKEND_MASK_CPU = 1
122
+ TRANSCRIBE_BACKEND_MASK_CUDA = 8
123
+ TRANSCRIBE_BACKEND_MASK_METAL = 2
124
+ TRANSCRIBE_BACKEND_MASK_OTHER = 2147483648
125
+ TRANSCRIBE_BACKEND_MASK_ROCM = 16
126
+ TRANSCRIBE_BACKEND_MASK_VULKAN = 4
113
127
  TRANSCRIBE_EXT_KIND_MOONSHINE_STREAMING_STREAM = 1414746957
114
128
  TRANSCRIBE_EXT_KIND_PARAKEET_BUFFERED_STREAM = 1396853584
115
129
  TRANSCRIBE_EXT_KIND_PARAKEET_STREAM = 1414744912
@@ -120,6 +134,8 @@ TRANSCRIBE_EXT_KIND_WHISPER_RUN = 1314015319
120
134
  # === structs ===
121
135
  class transcribe_ext(_c.Structure):
122
136
  pass
137
+ class transcribe_backend_init_params(_c.Structure):
138
+ pass
123
139
  class transcribe_device_info(_c.Structure):
124
140
  pass
125
141
  class transcribe_model_load_params(_c.Structure):
@@ -164,10 +180,11 @@ class transcribe_whisper_chunk_trace(_c.Structure):
164
180
  pass
165
181
 
166
182
  transcribe_ext._fields_ = [("size", _c.c_uint64), ("kind", _c.c_uint32)]
183
+ transcribe_backend_init_params._fields_ = [("struct_size", _c.c_uint64), ("artifact_dir", _c.c_char_p), ("allowed_backends", _c.c_uint32)]
167
184
  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
185
  transcribe_model_load_params._fields_ = [("struct_size", _c.c_uint64), ("backend", _c.c_int), ("device", _c.c_void_p)]
169
186
  transcribe_session_params._fields_ = [("struct_size", _c.c_uint64), ("n_threads", _c.c_int), ("kv_type", _c.c_int), ("n_ctx", _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)]
187
+ 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), ("vocabulary", _c.POINTER(_c.c_char_p)), ("n_vocabulary", _c.c_int32), ("prompt", _c.c_char_p), ("prefix", _c.c_char_p)]
171
188
  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))]
172
189
  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)]
173
190
  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)]
@@ -190,6 +207,7 @@ transcribe_whisper_chunk_trace._fields_ = [("struct_size", _c.c_uint64), ("t0_ms
190
207
  # transcribe_abi_struct id per struct (for the native size/align check).
191
208
  ABI_STRUCT_IDS = {
192
209
  'transcribe_ext': 12,
210
+ 'transcribe_backend_init_params': 15,
193
211
  'transcribe_device_info': 13,
194
212
  'transcribe_model_load_params': 0,
195
213
  'transcribe_session_params': 1,
@@ -209,10 +227,11 @@ ABI_STRUCT_IDS = {
209
227
  # C-compiler layout captured at generation (for offset self-check).
210
228
  STRUCT_LAYOUT = {
211
229
  'transcribe_ext': {'size': 16, 'align': 8, 'offsets': {'size': 0, 'kind': 8}},
230
+ 'transcribe_backend_init_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'artifact_dir': 8, 'allowed_backends': 16}},
212
231
  '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
232
  'transcribe_model_load_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'backend': 8, 'device': 16}},
214
233
  'transcribe_session_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'n_threads': 8, 'kv_type': 12, 'n_ctx': 16}},
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}},
234
+ 'transcribe_run_params': {'size': 104, '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, 'vocabulary': 72, 'n_vocabulary': 80, 'prompt': 88, 'prefix': 96}},
216
235
  '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}},
217
236
  'transcribe_session_limits': {'size': 32, 'align': 8, 'offsets': {'struct_size': 0, 'effective_n_ctx': 8, 'effective_max_audio_ms': 16, 'max_kv_bytes': 24}},
218
237
  'transcribe_stream_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'family': 8, 'commit_policy': 16, 'stable_prefix_agreement_n': 20}},
@@ -239,8 +258,12 @@ def configure(lib):
239
258
  lib.transcribe_abi_struct_align.argtypes = [_c.c_int]
240
259
  lib.transcribe_abi_struct_size.restype = _c.c_size_t
241
260
  lib.transcribe_abi_struct_size.argtypes = [_c.c_int]
261
+ lib.transcribe_allowed_backends.restype = _c.c_uint32
262
+ lib.transcribe_allowed_backends.argtypes = []
242
263
  lib.transcribe_backend_available.restype = _c.c_bool
243
264
  lib.transcribe_backend_available.argtypes = [_c.c_int]
265
+ lib.transcribe_backend_init_params_init.restype = None
266
+ lib.transcribe_backend_init_params_init.argtypes = [_c.POINTER(transcribe_backend_init_params)]
244
267
  lib.transcribe_batch_detected_language.restype = _c.c_char_p
245
268
  lib.transcribe_batch_detected_language.argtypes = [_c.c_void_p, _c.c_int]
246
269
  lib.transcribe_batch_full_text.restype = _c.c_char_p
@@ -309,6 +332,8 @@ def configure(lib):
309
332
  lib.transcribe_init_backends.argtypes = [_c.c_char_p]
310
333
  lib.transcribe_init_backends_default.restype = _c.c_int
311
334
  lib.transcribe_init_backends_default.argtypes = []
335
+ lib.transcribe_init_backends_ex.restype = _c.c_int
336
+ lib.transcribe_init_backends_ex.argtypes = [_c.POINTER(transcribe_backend_init_params)]
312
337
  lib.transcribe_log_set.restype = None
313
338
  lib.transcribe_log_set.argtypes = [_c.CFUNCTYPE(None, _c.c_int, _c.c_char_p, _c.c_void_p), _c.c_void_p]
314
339
  lib.transcribe_model_accepts_ext_kind.restype = _c.c_bool
@@ -22,6 +22,7 @@ from ._generated import (
22
22
  TRANSCRIBE_ERR_INVALID_ARG as ERR_INVALID_ARG,
23
23
  TRANSCRIBE_ERR_NOT_IMPLEMENTED as ERR_NOT_IMPLEMENTED,
24
24
  TRANSCRIBE_ERR_OOM as ERR_OOM,
25
+ TRANSCRIBE_ERR_OUTPUT_REPETITION as ERR_OUTPUT_REPETITION,
25
26
  TRANSCRIBE_ERR_OUTPUT_TRUNCATED as ERR_OUTPUT_TRUNCATED,
26
27
  TRANSCRIBE_ERR_SAMPLE_RATE as ERR_SAMPLE_RATE,
27
28
  TRANSCRIBE_ERR_UNSUPPORTED_ARCH as ERR_UNSUPPORTED_ARCH,
@@ -113,6 +114,17 @@ class OutputTruncated(TranscribeError):
113
114
  partial_result: "Optional[Result]" = None
114
115
 
115
116
 
117
+ class OutputRepetition(OutputTruncated):
118
+ """The decode was stopped because the output began repeating itself —
119
+ the transcript is incomplete by contract.
120
+
121
+ A subclass of :class:`OutputTruncated`, so a handler for incomplete
122
+ transcripts catches both. ``partial_result`` holds the partial transcript
123
+ with the repeats dropped (one copy kept), or None when the status surfaced
124
+ outside a result-bearing call.
125
+ """
126
+
127
+
116
128
  _STATUS_TO_EXC = {
117
129
  ERR_INVALID_ARG: InvalidArgument,
118
130
  ERR_NOT_IMPLEMENTED: NotImplementedByModel,
@@ -132,6 +144,7 @@ _STATUS_TO_EXC = {
132
144
  ERR_UNSUPPORTED_ITN: UnsupportedRequest,
133
145
  ERR_INPUT_TOO_LONG: InputTooLong,
134
146
  ERR_OUTPUT_TRUNCATED: OutputTruncated,
147
+ ERR_OUTPUT_REPETITION: OutputRepetition,
135
148
  }
136
149
 
137
150
 
@@ -41,6 +41,7 @@ def test_every_status_maps_to_documented_subclass():
41
41
  errors.ERR_UNSUPPORTED_ITN: t.UnsupportedRequest,
42
42
  errors.ERR_INPUT_TOO_LONG: t.InputTooLong,
43
43
  errors.ERR_OUTPUT_TRUNCATED: t.OutputTruncated,
44
+ errors.ERR_OUTPUT_REPETITION: t.OutputRepetition,
44
45
  }
45
46
  # The mapping table covers every non-OK status the header defines, and
46
47
  # nothing else (a new C status must be mapped deliberately, not by
@@ -64,6 +65,14 @@ def test_unknown_status_degrades_to_base_class():
64
65
  assert exc.status == 999
65
66
 
66
67
 
68
+ def test_output_repetition_is_an_output_truncated():
69
+ # A handler that keeps the partial of an incomplete transcript catches both.
70
+ exc = errors.exception_for_status(errors.ERR_OUTPUT_REPETITION, "looped", "run")
71
+ assert isinstance(exc, t.OutputRepetition)
72
+ assert isinstance(exc, t.OutputTruncated)
73
+ assert exc.partial_result is None
74
+
75
+
67
76
  def test_exception_for_status_builds_without_raising():
68
77
  exc = errors.exception_for_status(errors.ERR_ABORTED, "aborted", "run")
69
78
  assert isinstance(exc, t.Aborted)
@@ -0,0 +1,116 @@
1
+ """Generic prompting inputs: run-params marshalling and model-gated behavior."""
2
+
3
+ import ctypes
4
+
5
+ import pytest
6
+
7
+ import transcribe_cpp as t
8
+ from transcribe_cpp import _generated
9
+
10
+
11
+ def test_run_params_carry_prompting_fields():
12
+ params = t._build_run_params("instruct", None, None, "none", False, -1,
13
+ vocabulary=["GGUF", "ggml"], prompt="Summarize.",
14
+ prefix="And so")
15
+ assert params.task == _generated.TRANSCRIBE_TASK_INSTRUCT
16
+ assert params.n_vocabulary == 2
17
+ assert [params.vocabulary[i] for i in range(2)] == [b"GGUF", b"ggml"]
18
+ assert params.prompt == b"Summarize."
19
+ assert params.prefix == b"And so"
20
+
21
+
22
+ def test_run_params_default_to_no_prompting():
23
+ params = t._build_run_params("transcribe", None, None, "auto", False, -1)
24
+ assert params.n_vocabulary == 0
25
+ assert not params.vocabulary
26
+ assert params.prompt is None and params.prefix is None
27
+
28
+
29
+ def test_vocabulary_rejects_single_string():
30
+ with pytest.raises(t.InvalidArgument):
31
+ t._build_run_params("transcribe", None, None, "auto", False, -1, vocabulary="GGUF")
32
+
33
+
34
+ def test_nul_in_string_option_is_rejected():
35
+ for kwargs in ({"prompt": "a\x00b"}, {"prefix": "a\x00b"}, {"vocabulary": ["a\x00b"]}):
36
+ with pytest.raises(t.InvalidArgument):
37
+ t._build_run_params("transcribe", None, None, "auto", False, -1, **kwargs)
38
+
39
+
40
+ def test_vocabulary_accepts_iterators_rejects_sets():
41
+ params = t._build_run_params("transcribe", None, None, "auto", False, -1,
42
+ vocabulary=(w for w in ["GGUF", "ggml"]))
43
+ assert params.n_vocabulary == 2
44
+ with pytest.raises(t.InvalidArgument):
45
+ t._build_run_params("transcribe", None, None, "auto", False, -1, vocabulary={"GGUF"})
46
+
47
+
48
+ def test_prompting_features_probe(model_path):
49
+ with t.Model(model_path, backend="cpu") as model:
50
+ for feature in ("vocabulary", "context_prompt", "instruct", "transcript_prefix"):
51
+ assert isinstance(model.supports(feature), bool)
52
+
53
+
54
+ def test_unsupported_prefix_raises(streaming_model_path, audio_pcm):
55
+ with t.Model(streaming_model_path, backend="cpu") as model, model.session() as session:
56
+ if model.supports("transcript_prefix"):
57
+ pytest.skip("model supports a transcript prefix")
58
+ with pytest.raises(t.InvalidArgument):
59
+ session.run(audio_pcm, prefix="And so")
60
+
61
+
62
+ def test_stream_prompting(streaming_model_path):
63
+ with t.Model(streaming_model_path, backend="cpu") as model, model.session() as session:
64
+ with pytest.raises(t.UnsupportedRequest):
65
+ session.stream(task="instruct", prompt="Summarize.")
66
+ with session.stream(vocabulary=["Kennedy", "Americans"], prompt="A speech."):
67
+ pass
68
+
69
+
70
+ def _whisper(model):
71
+ return model.arch == "whisper"
72
+
73
+
74
+ def test_whisper_prefix_contract(model_path, audio_pcm):
75
+ """The prefix is forced decoder text: text holds only the continuation,
76
+ raw_text leads with the prefix, and nothing is duplicated."""
77
+ prefix = "And so my fellow Americans,"
78
+ with t.Model(model_path, backend="cpu") as model, model.session() as session:
79
+ if not _whisper(model):
80
+ pytest.skip("whisper-specific rendering")
81
+ assert model.supports("transcript_prefix")
82
+ res = session.run(audio_pcm, prefix=prefix)
83
+ assert res.raw_text.strip().startswith(prefix)
84
+ assert not res.text.lower().startswith("and so")
85
+ assert "ask not" in res.text.lower()
86
+ # Whisper's timestamp rules do not compose with a prefix.
87
+ with t.Model(model_path, backend="cpu") as model, model.session() as session:
88
+ with pytest.raises(t.InvalidArgument):
89
+ session.run(audio_pcm, prefix=prefix, timestamps="segment")
90
+
91
+
92
+ def test_whisper_vocabulary_and_context(model_path, audio_pcm):
93
+ with t.Model(model_path, backend="cpu") as model, model.session() as session:
94
+ if not _whisper(model):
95
+ pytest.skip("whisper-specific rendering")
96
+ assert model.supports("vocabulary") and model.supports("context_prompt")
97
+ res = session.run(audio_pcm, vocabulary=["Kennedy", "Americans"],
98
+ prompt="An inaugural address.")
99
+ assert "country" in res.text.lower()
100
+ batch = session.run_batch([audio_pcm, audio_pcm], vocabulary=["Kennedy", "Americans"])
101
+ assert len(batch) == 2 and all("country" in r.text.lower() for r in batch)
102
+ # The whisper extension's prompt and the generic fields share one slot.
103
+ with pytest.raises(t.InvalidArgument):
104
+ session.run(audio_pcm, vocabulary=["Kennedy"],
105
+ family=t.WhisperRunOptions(initial_prompt="x"))
106
+
107
+
108
+ def test_control_token_literal_rejected(model_path, audio_pcm):
109
+ """A control-token literal is rejected and leaves the previous result."""
110
+ with t.Model(model_path, backend="cpu") as model, model.session() as session:
111
+ if not _whisper(model):
112
+ pytest.skip("whisper-specific rendering")
113
+ first = session.run(audio_pcm)
114
+ with pytest.raises(t.InvalidArgument):
115
+ session.run(audio_pcm, prompt="hello <|endoftext|>")
116
+ assert session._materialize().text == first.text
@@ -387,7 +387,7 @@ wheels = [
387
387
 
388
388
  [[package]]
389
389
  name = "transcribe-cpp"
390
- version = "0.2.4"
390
+ version = "0.3.1"
391
391
  source = { editable = "." }
392
392
 
393
393
  [package.optional-dependencies]
@@ -403,8 +403,8 @@ test = [
403
403
  requires-dist = [
404
404
  { name = "numpy", marker = "extra == 'test'" },
405
405
  { name = "pytest", marker = "extra == 'test'", specifier = ">=7" },
406
- { name = "transcribe-cpp-native", specifier = "==0.2.4.*" },
407
- { name = "transcribe-cpp-native-cu12", marker = "extra == 'cu12'", specifier = "==0.2.4.*" },
406
+ { name = "transcribe-cpp-native", specifier = "==0.3.1.*" },
407
+ { name = "transcribe-cpp-native-cu12", marker = "extra == 'cu12'", specifier = "==0.3.1.*" },
408
408
  ]
409
409
  provides-extras = ["cu12", "test"]
410
410
 
File without changes