transcribe-cpp-sys 0.0.6

Native FFI bindings for transcribe.cpp, a C/C++ speech-to-text library built on ggml
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
cmake_minimum_required(VERSION 3.16)

# -----------------------------------------------------------------------------
# Version (single source of truth: include/transcribe.h)
# -----------------------------------------------------------------------------
#
# The TRANSCRIBE_VERSION_MAJOR/MINOR/PATCH macros in the public header are the
# one place the library version is defined. Parse them here, before project(),
# so the CMake project version, the shared-library VERSION/SOVERSION, and the
# value transcribe_version() reports all derive from the same source and cannot
# drift. To bump the library version, edit include/transcribe.h.
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/include/transcribe.h" _transcribe_header)
string(REGEX MATCH "define +TRANSCRIBE_VERSION_MAJOR +([0-9]+)" _ "${_transcribe_header}")
set(TRANSCRIBE_VERSION_MAJOR "${CMAKE_MATCH_1}")
string(REGEX MATCH "define +TRANSCRIBE_VERSION_MINOR +([0-9]+)" _ "${_transcribe_header}")
set(TRANSCRIBE_VERSION_MINOR "${CMAKE_MATCH_1}")
string(REGEX MATCH "define +TRANSCRIBE_VERSION_PATCH +([0-9]+)" _ "${_transcribe_header}")
set(TRANSCRIBE_VERSION_PATCH "${CMAKE_MATCH_1}")
if(NOT TRANSCRIBE_VERSION_MAJOR MATCHES "^[0-9]+$" OR
   NOT TRANSCRIBE_VERSION_MINOR MATCHES "^[0-9]+$" OR
   NOT TRANSCRIBE_VERSION_PATCH MATCHES "^[0-9]+$")
    message(FATAL_ERROR
        "Failed to parse TRANSCRIBE_VERSION_{MAJOR,MINOR,PATCH} from "
        "include/transcribe.h")
endif()

project(transcribe
    VERSION ${TRANSCRIBE_VERSION_MAJOR}.${TRANSCRIBE_VERSION_MINOR}.${TRANSCRIBE_VERSION_PATCH}
    LANGUAGES C CXX)

# Short git commit for transcribe_version_commit(), captured at configure time.
# Best-effort: source tarballs and non-git trees fall back to "unknown". This is
# a configure-time snapshot (same posture as ggml's GGML_COMMIT); reconfigure to
# refresh it.
set(TRANSCRIBE_BUILD_COMMIT "unknown")
find_package(Git QUIET)
if(GIT_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git")
    execute_process(
        COMMAND ${GIT_EXECUTABLE} -C "${CMAKE_CURRENT_SOURCE_DIR}" rev-parse --short HEAD
        OUTPUT_VARIABLE _transcribe_git_sha
        OUTPUT_STRIP_TRAILING_WHITESPACE
        ERROR_QUIET)
    if(_transcribe_git_sha)
        set(TRANSCRIBE_BUILD_COMMIT "${_transcribe_git_sha}")
    endif()
endif()
message(STATUS "transcribe version: ${PROJECT_VERSION} (commit ${TRANSCRIBE_BUILD_COMMIT})")

# -----------------------------------------------------------------------------
# Standards
# -----------------------------------------------------------------------------

set(CMAKE_C_STANDARD            11)
set(CMAKE_C_STANDARD_REQUIRED   ON)
set(CMAKE_CXX_STANDARD          17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS        OFF)

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()

include(GNUInstallDirs)

# Hidden symbol visibility by default per PLAN.md.
set(CMAKE_C_VISIBILITY_PRESET       hidden)
set(CMAKE_CXX_VISIBILITY_PRESET     hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)

# -----------------------------------------------------------------------------
# Python wheel build (scikit-build-core sets SKBUILD)
# -----------------------------------------------------------------------------
#
# The repo-root pyproject.toml builds the transcribe-cpp-native provider
# package through scikit-build-core. Force the library shape that package
# ships (shared libtranscribe, no tests/examples/tools) and default to the
# official wheel-hygiene posture: no OpenMP and no non-Apple system BLAS, so
# nothing is vendored that collides with PyTorch/NumPy/MKL in-process, and
# Metal shaders embedded so the artifact has no sidecar files. The hygiene
# defaults (not the shape) can be overridden with -D for local source builds
# that want them. CMP0077 is NEW here, so option() below respects these.
if(SKBUILD)
    set(TRANSCRIBE_BUILD_SHARED   ON)
    set(TRANSCRIBE_BUILD_TESTS    OFF)
    set(TRANSCRIBE_BUILD_EXAMPLES OFF)
    set(TRANSCRIBE_BUILD_TOOLS    OFF)
    if(NOT DEFINED CACHE{TRANSCRIBE_USE_OPENMP})
        set(TRANSCRIBE_USE_OPENMP OFF)
    endif()
    if(NOT DEFINED CACHE{TRANSCRIBE_USE_SYSTEM_BLAS})
        set(TRANSCRIBE_USE_SYSTEM_BLAS OFF)
    endif()
    if(NOT DEFINED CACHE{GGML_METAL_EMBED_LIBRARY})
        set(GGML_METAL_EMBED_LIBRARY ON CACHE BOOL "" FORCE)
    endif()
endif()

# -----------------------------------------------------------------------------
# Apple Silicon detection (drives Metal default)
# -----------------------------------------------------------------------------

set(TRANSCRIBE_IS_APPLE_SILICON OFF)
if(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64")
    set(TRANSCRIBE_IS_APPLE_SILICON ON)
endif()

# -----------------------------------------------------------------------------
# Options
# -----------------------------------------------------------------------------

option(TRANSCRIBE_BUILD_TESTS    "Build unit / smoke tests" ON)
option(TRANSCRIBE_BUILD_EXAMPLES "Build example CLI"        ON)
option(TRANSCRIBE_BUILD_TOOLS    "Build inspect / quantize" OFF)  # post pass 2
option(TRANSCRIBE_METAL          "Enable Metal backend"     ${TRANSCRIBE_IS_APPLE_SILICON})
option(TRANSCRIBE_VULKAN         "Enable Vulkan backend"    OFF)  # post-v1
option(TRANSCRIBE_CUDA           "Enable CUDA backend"      OFF)  # opt-in; Linux + nvcc
option(TRANSCRIBE_SANITIZE       "Enable ASan + UBSan"      OFF)
option(TRANSCRIBE_LTO            "Enable LTO in Release"    OFF)

# Library link mode and wheel-hygiene switches.
#
# TRANSCRIBE_BUILD_SHARED OFF (default) builds a static libtranscribe + static
# ggml, so `cmake -B build` yields a self-contained transcribe-cli with no
# libtranscribe/libggml to chase at runtime — the posture every porting, test,
# and bench workflow relies on. The Python wheel/provider build sets it ON to
# produce a shared libtranscribe the FFI layer can dlopen.
#
# TRANSCRIBE_USE_OPENMP / TRANSCRIBE_USE_SYSTEM_BLAS default ON to keep the
# developer build fast (ggml OpenMP, Accelerate/system BLAS host decoder).
# Official provider wheels pass them OFF so no libgomp/libiomp or OpenBLAS/MKL
# runtime is vendored into the wheel where it can collide with PyTorch/NumPy.
option(TRANSCRIBE_BUILD_SHARED    "Build libtranscribe + ggml as shared libraries"  OFF)
option(TRANSCRIBE_USE_OPENMP      "Use OpenMP (ggml + Parakeet host-decoder TU)"     ON)
option(TRANSCRIBE_USE_SYSTEM_BLAS "Link non-Apple system BLAS for the host decoder"  ON)

# Dynamic ggml backends: each backend (CPU, Vulkan, CUDA, ...) becomes a
# separate loadable module living NEXT TO libtranscribe instead of being
# compiled in. This is the provider-directory artifact shape the Python
# wheels ship on Linux/Windows: the Vulkan module is present by default and
# simply fails to load (skipped; CPU keeps working) on machines without a
# Vulkan loader/driver. The host loads the module directory once via
# transcribe_init_backends(). Requires TRANSCRIBE_BUILD_SHARED=ON.
option(TRANSCRIBE_GGML_BACKEND_DL "Build ggml backends as loadable modules" OFF)

# Conservative x86 floor: default GGML_NATIVE plus EVERY x86 SIMD tier to
# OFF so the one binary runs on baseline x86-64 without SIGILL. This is the
# single switch behind every official-wheel CPU posture (the per-flag lists
# used to be mirrored across the root pyproject lanes, the cu12 pyproject,
# and CMakePresets.json — one option, one place to drift).
#
# It is a DEFAULT, not a clamp (same posture as the SKBUILD hygiene knobs
# above): an explicit -DGGML_AVX2=ON etc. on the command line still wins, so
# a user can take the conservative floor and selectively re-enable tiers for
# their machine. With the option OFF (the default) the raw GGML_* flags are
# untouched and fully tunable, exactly as without this option.
#
# Interaction with GGML_CPU_ALL_VARIANTS=ON (the fat-CPU wheel lanes): ggml
# gates the per-flag options behind `if(NOT GGML_CPU_ALL_VARIANTS)`, so
# these are inert there and serve as the explicit floor if ALL_VARIANTS is
# ever disabled. Inert (harmless) on non-x86 targets: ggml's Linux-ARM tier
# list has its own flags.
option(TRANSCRIBE_X86_CONSERVATIVE
    "Default GGML_NATIVE and every x86 SIMD tier to OFF (SIGILL-safe baseline; explicit -DGGML_* still wins)" OFF)

# Install rules for non-Python consumers: headers + the ABI digest +
# libtranscribe + the transcribe-link.json link manifest (ggml's own install
# rules supply the ggml libs/headers). This is what the Rust -sys crate's
# build.rs consumes (`cmake --install` into a staging prefix, then link per
# the manifest) — see cmake/transcribe-install.cmake. Default ON for
# top-level builds; OFF as a subproject, and never active under SKBUILD
# (the wheel ships its own component-filtered install rules instead).
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND NOT SKBUILD)
    set(_transcribe_install_default ON)
else()
    set(_transcribe_install_default OFF)
endif()
option(TRANSCRIBE_INSTALL
    "Install headers, libtranscribe, and the link manifest" ${_transcribe_install_default})

# Real-model gated tests. OFF by default because the tests need a
# converted Parakeet GGUF on disk (~2.4 GB) and CI can't ship one.
# When ON, the test reads TRANSCRIBE_REAL_PARAKEET_GGUF from the
# environment at run time. See tests/parakeet_real_smoke.cpp.
option(TRANSCRIBE_BUILD_REAL_MODEL_TESTS
    "Build tests that load real (multi-GB) model files via env var" OFF)

# -----------------------------------------------------------------------------
# Warnings (no -Werror in v1 per PLAN.md)
# -----------------------------------------------------------------------------
#
# transcribe_apply_warnings(target): apply our project warning flags to one
# target. Used per-target so we don't pollute the vendored ggml subdirectory
# with -Wshadow / -Wpedantic, which fire on a lot of legitimate ggml code.

function(transcribe_apply_warnings tgt)
    if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
        target_compile_options(${tgt} PRIVATE
            -Wall
            -Wextra
            -Wshadow
            -Wpedantic
            -Wno-unused-parameter
            # The output-side TRANSCRIBE_*_INIT macros (timings,
            # stream_update, capabilities, whisper_chunk_trace, ...)
            # are deliberate zero-fill aggregate initializers:
            # struct_size is set, every other field falls to
            # initializer zero by C aggregate-init rules. The
            # zero-means-absent contract is documented and load-bearing.
            # -Wmissing-field-initializers would fire on every use of
            # those macros; the warning's signal-to-noise on this
            # codebase is too low to justify keeping it on.
            -Wno-missing-field-initializers
        )
    endif()
endfunction()

# Sanitizers must be applied to every target in the build (including ggml)
# so the resulting binary is consistent. add_compile_options /
# add_link_options is the right scope for this.
#
# -fno-sanitize-recover=undefined makes UBSan reports fatal at runtime
# instead of merely printing them. ASan already aborts on error by
# default; UBSan does not, which would otherwise let ctest report
# success while the sanitizer printed real diagnostics.
if(TRANSCRIBE_SANITIZE)
    add_compile_options(
        -fsanitize=address,undefined
        -fno-sanitize-recover=undefined
        # ggml's `incr_ptr_aligned` (ggml/src/ggml.c) uses the
        # standard "use NULL as base, walk by offsets to compute
        # struct layout sizes" trick. UBSan flags this as a
        # `pointer-overflow` runtime error even though it's a
        # widely-used and safe-in-practice C idiom. Make this
        # specific check non-fatal so the encoder run path doesn't
        # abort under build-san; real bugs in our own code still
        # fail loud (the rest of -fno-sanitize-recover=undefined
        # is unaffected). When ggml gets patched upstream this can
        # come back out.
        -fsanitize-recover=pointer-overflow
        -fno-omit-frame-pointer
    )
    add_link_options(-fsanitize=address,undefined)
endif()

# -----------------------------------------------------------------------------
# Output layout
# -----------------------------------------------------------------------------
#
# Put all binaries in build/bin/ regardless of which subdirectory built them.
# ggml's CMakeLists already does this when invoked standalone; pinning it
# at the top level keeps the same layout when ggml is added as a subdirectory.

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

# -----------------------------------------------------------------------------
# Vendored ggml
# -----------------------------------------------------------------------------
#
# ggml lives at the repo root (matching whisper.cpp / llama.cpp). The pinned
# upstream SHA is recorded in ggml/UPSTREAM. We map our TRANSCRIBE_* options
# onto ggml's GGML_* options BEFORE add_subdirectory() so ggml's option()
# calls become no-ops and our defaults win.

set(GGML_METAL    ${TRANSCRIBE_METAL}  CACHE BOOL "" FORCE)
set(GGML_VULKAN   ${TRANSCRIBE_VULKAN} CACHE BOOL "" FORCE)
set(GGML_CUDA     ${TRANSCRIBE_CUDA}   CACHE BOOL "" FORCE)
set(GGML_BLAS OFF CACHE BOOL "" FORCE)

# Conservative x86 floor (see the option's comment above): one switch fans
# out to GGML_NATIVE plus the full x86 SIMD tier list. Placed BEFORE
# add_subdirectory(ggml) so ggml's own option() defaults never win — but
# NOT forced: a flag the user already put in the cache (-DGGML_AVX2=ON) is
# left alone, so the floor stays selectively tunable per machine.
if(TRANSCRIBE_X86_CONSERVATIVE)
    foreach(_simd_flag
            GGML_NATIVE
            GGML_SSE42 GGML_AVX GGML_AVX_VNNI GGML_AVX2 GGML_BMI2
            GGML_FMA GGML_F16C
            GGML_AVX512 GGML_AVX512_VBMI GGML_AVX512_VNNI GGML_AVX512_BF16)
        if(NOT DEFINED CACHE{${_simd_flag}})
            set(${_simd_flag} OFF CACHE BOOL "")
        endif()
    endforeach()
endif()

# OpenMP — CENTRAL POLICY (read before changing the Windows/MSVC threading).
#
# ggml defaults GGML_OPENMP ON and probes for it gracefully, so leave ggml's own
# default in place when we want OpenMP. Only force it OFF (matching the
# GGML_OPENMP=OFF posture official wheels use) when OpenMP is switched off, so
# one TRANSCRIBE_USE_OPENMP knob covers ggml and our host-decoder TU.
#
# The catch: ggml's NON-OpenMP CPU threadpool barrier (ggml_barrier's custom
# spin path) DEADLOCKS under MSVC codegen on Windows — any multi-threaded CPU
# run wedges its workers in the barrier spin (it works on every other compiler).
# OpenMP's `#pragma omp barrier` is the only working multi-threaded CPU path on
# MSVC. But OpenMP is a per-CONSUMER tradeoff, not a global flag, so this is not
# one switch — it is a deliberate, documented split:
#
#   - Rust binding (CPU is its default backend, standalone process): MUST have
#     OpenMP on Windows. Its build.rs passes -DGGML_OPENMP=ON; the guard below
#     honors that explicit value. MSVC auto-links vcomp via the /openmp pragma in
#     the ggml objects (no -fopenmp flag, so the link manifest is untouched);
#     vcomp140.dll ships in the VC++ runtime the binary already needs.
#   - Python wheels (default to the Vulkan GPU backend; deliberately vendor NO
#     OpenMP so ggml's runtime cannot collide with numpy/torch's own OpenMP/MKL
#     in one process — see python-wheels.yml): leave GGML_OPENMP unset and get
#     the force-off below. KNOWN LIMITATION as a consequence: multi-threaded CPU
#     compute on Windows is unsupported for the wheels (they use Vulkan). Lifting
#     it needs a non-OpenMP fix (e.g. single-threaded CPU on Windows, or patching
#     ggml's barrier) rather than forcing OpenMP and the coexistence hazard back.
#
# So: honor an explicit -DGGML_OPENMP=... (the Rust opt-in); otherwise force OFF.
if(NOT TRANSCRIBE_USE_OPENMP AND NOT DEFINED CACHE{GGML_OPENMP})
    set(GGML_OPENMP OFF CACHE BOOL "" FORCE)
endif()

# We don't want ggml's tests / examples leaking into our build tree. They
# default to OFF when ggml is added as a subdirectory (GGML_STANDALONE OFF),
# but pin them anyway so the contract is explicit.
set(GGML_BUILD_TESTS    OFF CACHE BOOL "" FORCE)
set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)

# Library link mode. Default OFF (static) keeps the self-contained CLI/test/
# bench build with no libtranscribe/libggml to chase at runtime; the Python
# wheel/provider build sets TRANSCRIBE_BUILD_SHARED=ON for a shared
# libtranscribe (+ libggml) the FFI layer can dlopen. Both ggml and
# libtranscribe follow this single switch. Symbol visibility is already hidden
# by default (set above), so a shared lib exports only the TRANSCRIBE_API
# surface.
set(BUILD_SHARED_LIBS ${TRANSCRIBE_BUILD_SHARED} CACHE BOOL "" FORCE)

# Dynamic backend modules (see the option's comment above). ggml requires
# shared libs for GGML_BACKEND_DL, so gate it on TRANSCRIBE_BUILD_SHARED
# here for a clear configure-time error.
if(TRANSCRIBE_GGML_BACKEND_DL)
    if(NOT TRANSCRIBE_BUILD_SHARED)
        message(FATAL_ERROR
            "TRANSCRIBE_GGML_BACKEND_DL requires TRANSCRIBE_BUILD_SHARED=ON "
            "(backend modules are shared libraries loaded at runtime)")
    endif()
    set(GGML_BACKEND_DL ON CACHE BOOL "" FORCE)
    # The package-local default loader resolves the directory that contains
    # libtranscribe itself. Install backend modules there too, unless a caller
    # explicitly chose a different provider layout and will pass it to
    # transcribe_init_backends(dir).
    #
    # Test emptiness, not DEFINED: ggml declares GGML_BACKEND_DIR as an empty
    # cache var of its own, so on any reconfigure it is already DEFINED (as "")
    # and a `NOT DEFINED CACHE{...}` guard would skip our override, leaving the
    # modules in ggml's default bin/ where the lib-dir loader cannot find them.
    # `NOT GGML_BACKEND_DIR` treats an empty/unset value as "ours to set" while
    # still respecting a non-empty caller-provided path.
    if(NOT GGML_BACKEND_DIR)
        # The directory that holds libtranscribe itself — which is where the
        # package-local loader scans. That's bin/ on Windows (a DLL is a RUNTIME
        # artifact installed to bin, so transcribe.dll lives there) and lib/
        # elsewhere. Using lib/ unconditionally would strand the modules away
        # from transcribe.dll on Windows, where the loader can't find them.
        if(WIN32)
            set(GGML_BACKEND_DIR "${CMAKE_INSTALL_BINDIR}" CACHE PATH "" FORCE)
        else()
            set(GGML_BACKEND_DIR "${CMAKE_INSTALL_LIBDIR}" CACHE PATH "" FORCE)
        endif()
    endif()
    # ggml hard-errors on GGML_NATIVE + GGML_BACKEND_DL (x86): native tuning
    # is incompatible with feature-scored modules. A module build is a
    # distribution build, so never tune it to the build host.
    set(GGML_NATIVE OFF CACHE BOOL "" FORCE)
endif()

# ggml's CMake declares its own warning flags; let it.
add_subdirectory(ggml)

# -----------------------------------------------------------------------------
# Subdirs
# -----------------------------------------------------------------------------

add_subdirectory(src)

if(TRANSCRIBE_BUILD_EXAMPLES)
    add_subdirectory(examples)
endif()

if(TRANSCRIBE_BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

if(TRANSCRIBE_BUILD_TOOLS)
    add_subdirectory(tools)
endif()

# Python provider-wheel packaging (after subdirs: needs the target list and
# ggml's GGML_AVAILABLE_BACKENDS).
if(SKBUILD)
    include(cmake/python-wheel-install.cmake)
endif()

# Consumer install rules (same placement constraint as above).
if(TRANSCRIBE_INSTALL AND NOT SKBUILD)
    include(cmake/transcribe-install.cmake)
endif()