Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6e46609
placeholder bindings
cjpais Jun 9, 2026
7daf815
use proper logger
cjpais Jun 9, 2026
a032421
python bindings first go
cjpais Jun 10, 2026
32fbc9f
continue to get bindings, testing, asan
cjpais Jun 10, 2026
f9cff80
fix ci
cjpais Jun 10, 2026
0092271
ci
cjpais Jun 10, 2026
474ced0
python native package
cjpais Jun 10, 2026
2ce866d
manylinux
cjpais Jun 10, 2026
f3dd86c
try blacksmith
cjpais Jun 11, 2026
9250878
fix compilation on blacksmith?
cjpais Jun 11, 2026
0f51c8e
try selfhosted macos runner
cjpais Jun 11, 2026
1a993ee
newest ci, fix windows
cjpais Jun 11, 2026
d738d85
more fixes
cjpais Jun 11, 2026
5824f3d
vulkan test
cjpais Jun 11, 2026
dedb54a
cuda + modal + windows
cjpais Jun 11, 2026
9726ec1
fix windows ninja
cjpais Jun 11, 2026
181ced9
fix modal failure
cjpais Jun 11, 2026
bd34bba
add fat binary dist
cjpais Jun 11, 2026
eb52904
update workflows/cuda
cjpais Jun 11, 2026
1247612
blacksmith for now?
cjpais Jun 11, 2026
4d0f280
fix remaining two failures
cjpais Jun 11, 2026
03700c6
change ci workers a bit
cjpais Jun 12, 2026
66d25fe
fix broken
cjpais Jun 12, 2026
b67759a
windows cuda
cjpais Jun 12, 2026
87df2d4
untrack plan docs into notes/; fix cuda-windows toolkit install
cjpais Jun 12, 2026
4d350b8
test python release pipeline
cjpais Jun 12, 2026
867e577
upgrade node for ci
cjpais Jun 12, 2026
676e86d
review changes, consolidate ci, add better cmake, testing, etc
cjpais Jun 12, 2026
191d2b9
ci: pin setup-uv to v8.2.0 (no floating v8 tag exists)
cjpais Jun 12, 2026
1476711
merge main (workflow-registration stub); keep the branch's publish.yml
cjpais Jun 12, 2026
46f162b
ci: fix the three setup-uv pins the anchored sed missed
cjpais Jun 12, 2026
1f167d4
modal: stable build toolchain for the persistent tree (--no-isolation)
cjpais Jun 12, 2026
cd9f38d
ci: key ccache by CPU ISA signature — cross-machine restores SIGILLed
cjpais Jun 12, 2026
d1b5206
modal: retry the sccache download; never pipe curl into tar
cjpais Jun 12, 2026
25fed3c
modal: put the image pybin on PATH for the ExternalProject's inner cmake
cjpais Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/actions/fetch-canary/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: fetch-canary
description: >
Cache + fetch the private canary GGUFs and export TRANSCRIBE_SMOKE_MODEL /
TRANSCRIBE_SMOKE_STREAMING_MODEL. Skips cleanly when hf-token is empty
(forks have no secret): the model tests then skip, exactly as before this
action existed. Always fetches BOTH canaries (~95 MB total) under one cache
key — a per-consumer subset would let one job save the shared key with only
its subset in it, and every other consumer would re-download forever
(exact-key hits are never re-saved). Requires a checkout (composite actions
resolve from the repo) and uv on PATH (uvx fetches via huggingface_hub).

inputs:
hf-token:
description: HF token (pass secrets.HF_TOKEN; empty means skip cleanly)
required: false
default: ""
model-path-prefix:
description: >
Prefix for the exported env paths. Default is the workspace; pass
/project for cibuildwheel's Linux containers (the project bind mount).
required: false
default: ""

runs:
using: composite
steps:
- name: Canary model cache (avoid HF rate limits — 429s were real)
if: inputs.hf-token != ''
uses: actions/cache@v5
with:
path: canary
key: canary-models-v1
- name: Fetch canary models (cache miss only)
if: inputs.hf-token != ''
shell: bash
env:
HF_TOKEN: ${{ inputs.hf-token }}
run: |
[ -f canary/whisper-tiny-Q5_K_M.gguf ] || \
uvx --from huggingface_hub hf download handy-computer/whisper-tiny-gguf \
whisper-tiny-Q5_K_M.gguf --local-dir canary
[ -f canary/moonshine-streaming-tiny-Q8_0.gguf ] || \
uvx --from huggingface_hub hf download handy-computer/moonshine-streaming-tiny-gguf \
moonshine-streaming-tiny-Q8_0.gguf --local-dir canary
# GITHUB_WORKSPACE (not bash's $PWD) so Windows exports a path
# Python can open rather than an MSYS one.
prefix='${{ inputs.model-path-prefix }}'
if [ -z "$prefix" ]; then prefix="$GITHUB_WORKSPACE"; fi
echo "TRANSCRIBE_SMOKE_MODEL=$prefix/canary/whisper-tiny-Q5_K_M.gguf" >> "$GITHUB_ENV"
echo "TRANSCRIBE_SMOKE_STREAMING_MODEL=$prefix/canary/moonshine-streaming-tiny-Q8_0.gguf" >> "$GITHUB_ENV"
125 changes: 125 additions & 0 deletions .github/workflows/cuda-windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: cuda-windows

# Builds + packaging-smokes the transcribe-cpp-native-cu12 wheel for
# win_amd64 (plan: Windows CUDA is COMMITTED release scope, user decision
# 2026-06-11). Same design as the Linux cu12 wheel built on Modal:
# - the CUDA runtime is NEVER vendored: cudart/cublas come from the
# nvidia-*-cu12 runtime wheels (win_amd64 wheels exist — the PyTorch
# layout), preloaded by the package's prepare() hook, which knows the
# Windows DLL names (cudart64_12.dll, cublasLt64_12.dll,
# cublas64_12.dll under nvidia/<pkg>/bin/); nvcuda.dll is the driver.
# - the ggml-vulkan module is bundled alongside CUDA (superset-of-default
# rationale in bindings/python-native-cu12/pyproject.toml: a [cu12]
# install on an AMD/Intel-GPU Windows box must not silently lose Vulkan
# acceleration), built against the LunarG SDK like wheel-windows.
# - delvewheel repair EXCLUDES the CUDA DLLs + the driver + vulkan-1.dll,
# mirroring auditwheel's excludes on Linux.
# - GPU-less smoke: cu12 provider selected, declared backends
# {cuda, vulkan, cpu}, the cuda/vulkan modules quietly absent without a
# driver, CPU transcribes, full pytest suite. There is no Windows NVIDIA
# hardware in the fleet, so the RUNTIME evidence is carried by the Linux
# T4 smoke (same module mechanism, same prepare() contract) — documented
# gap.
#
# Trimmed cadence (user decision): NOT per PR push. Releases (tags) and
# manual dispatch only — nvcc over ggml-cuda × 5 arches is the heaviest
# build in the project.
#
# CUDA 12.9 to match the Linux cu12 provider (scripts/ci/modal_cuda_build.py
# pins cuda-toolkit-12-9; the nvidia runtime-wheel floors in the cu12
# pyproject cover this minor).

on:
# Called by publish.yml on release tags (which owns the tag trigger now —
# this workflow no longer listens to tags itself, so a release builds
# everything in ONE run and publishes those exact artifacts).
workflow_call:
workflow_dispatch:

env:
PYTHONUTF8: "1" # hf CLI prints ✓; Windows cp1252 console chokes

jobs:
cuda-wheel-windows:
# 16vcpu, deliberately breaking the 2vcpu rule: per-vcpu-minute billing
# makes the cost the same, and on 2vcpu this build (245 .cu × 5 arches
# under MSVC+nvcc) would risk the job time cap. It runs on releases and
# dispatch only, so the burn is rare and bounded.
runs-on: blacksmith-16vcpu-windows-2025
timeout-minutes: 180
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
CMAKE_GENERATOR: Ninja
# LunarG prunes old SDK downloads — when bumping, verify the URL exists.
# Keep in lockstep with wheel-windows (python-wheels.yml).
VULKAN_VERSION: "1.4.350.0"
steps:
- uses: actions/checkout@v6
# vcvars for the whole job: nvcc needs cl.exe, and the Ninja generator
# needs the MSVC environment (same reasoning as wheel-windows).
# (still node20 upstream — no node24 release yet; runners force node24 from
# 2026-06-16, which is fine for this env-setup action)
- uses: ilammy/msvc-dev-cmd@v1
- name: Install Vulkan SDK ${{ env.VULKAN_VERSION }} (glslc for the ggml-vulkan module)
run: |
curl.exe -o "$env:RUNNER_TEMP\vulkan_sdk.exe" -L "https://sdk.lunarg.com/sdk/download/${env:VULKAN_VERSION}/windows/vulkan_sdk.exe"
& "$env:RUNNER_TEMP\vulkan_sdk.exe" --accept-licenses --default-answer --confirm-command install
Add-Content $env:GITHUB_ENV "VULKAN_SDK=C:\VulkanSDK\${env:VULKAN_VERSION}"
Add-Content $env:GITHUB_PATH "C:\VulkanSDK\${env:VULKAN_VERSION}\bin"
- name: Install CUDA toolkit 12.9 (network installer, lean subset)
# Caching disabled on purpose: the first run (v0.2.27, caches on)
# died in 2.4s with a swallowed error surfaced as a cache-path
# failure — Blacksmith intercepts the Actions cache service and the
# action's @actions/cache path didn't survive it. The lane is
# tag/dispatch-rare; a fresh network install per run is fine.
uses: Jimver/cuda-toolkit@v0.2.35
with:
cuda: "12.9.0"
method: network
use-github-cache: false
use-local-cache: false
sub-packages: '["nvcc", "cudart", "cublas", "cublas_dev", "thrust", "visual_studio_integration"]'
- uses: astral-sh/setup-uv@v8.2.0
- name: Install zlib (vcpkg, static — libtranscribe requires it)
# Identical to wheel-windows: static-md so no zlib1.dll exists for
# delvewheel to vendor; forward slashes because CMAKE_ARGS passes
# through scikit-build-core's CMakeInit.txt where backslashes are
# eaten as escapes.
run: |
vcpkg install zlib:x64-windows-static-md
$tc = "$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" -replace '\\','/'
Add-Content $env:GITHUB_ENV "CMAKE_ARGS=-DCMAKE_TOOLCHAIN_FILE=$tc -DVCPKG_TARGET_TRIPLET=x64-windows-static-md"
- uses: ./.github/actions/fetch-canary
with:
hf-token: ${{ secrets.HF_TOKEN }}
- name: Build the raw cu12 wheel (scikit-build-core; lane posture in
bindings/python-native-cu12/pyproject.toml)
run: uv build --wheel bindings/python-native-cu12 --out-dir dist-cu12-raw
- name: Repair (delvewheel; CUDA runtime + driver + Vulkan loader stay OUT)
run: |
uvx delvewheel repair `
--exclude cudart64_12.dll `
--exclude cublas64_12.dll `
--exclude cublasLt64_12.dll `
--exclude nvcuda.dll `
--exclude vulkan-1.dll `
-w wheelhouse-cu12 (Get-Item dist-cu12-raw/*.whl).FullName
Get-ChildItem wheelhouse-cu12
- name: GPU-less packaging smoke (mirrors Modal's packaging_check)
run: |
uv venv --seed --python 3.12 smoke-venv
smoke-venv/Scripts/pip install -q --find-links wheelhouse-cu12 `
transcribe-cpp-native-cu12 "pytest>=7" numpy huggingface_hub
$env:TRANSCRIBE_SMOKE_PROVIDER = "transcribe-cpp-native-cu12"
# The default provider is deliberately absent (its pin can't
# resolve pre-release); wheel_smoke installs the API package
# dep-free. Declared-backends gate asserts {cuda, vulkan, cpu}.
$env:TRANSCRIBE_SMOKE_PIP_NO_DEPS = "1"
$env:CI = "1"
smoke-venv/Scripts/python scripts/ci/wheel_smoke.py .
# Named OUTSIDE the dist-* pattern, like cuda-dist-linux-x86_64: the
# co-import job merges dist-* and must never pick up a cu12 provider.
- uses: actions/upload-artifact@v7
with:
name: cuda-dist-windows-x86_64
path: wheelhouse-cu12/*.whl
23 changes: 15 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ jobs:
uses: ./.github/workflows/python-wheels.yml
secrets: inherit
cuda-windows:
# Tags only: the heaviest build in the project (nvcc over ggml-cuda x 5
# arches under MSVC, ~3 h on 16vcpu), and the TestPyPI rehearsal neither
# publishes nor smokes its output (cu12 wheels exceed TestPyPI's file
# cap). Validate it on demand with its own workflow_dispatch.
if: startsWith(github.ref, 'refs/tags/')
uses: ./.github/workflows/cuda-windows.yml
secrets: inherit

Expand All @@ -54,7 +59,7 @@ jobs:
permissions:
id-token: write # OIDC for trusted publishing
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
pattern: dist-*
merge-multiple: true
Expand All @@ -76,13 +81,15 @@ jobs:
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: dist-api
path: dist-api
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: smoke-assets # samples/jfk.wav, uploaded by the sdist job
# samples/jfk.wav (+ the degradation script), uploaded by api-wheel;
# files keep their repo-relative paths inside the artifact.
name: smoke-assets
path: smoke-assets
- name: Install from TestPyPI (exact just-published version) and transcribe
run: |
Expand Down Expand Up @@ -114,7 +121,7 @@ jobs:
import array, wave
import transcribe_cpp as t

with wave.open("smoke-assets/jfk.wav", "rb") as w:
with wave.open("smoke-assets/samples/jfk.wav", "rb") as w:
pcm16 = array.array("h"); pcm16.frombytes(w.readframes(w.getnframes()))
pcm = array.array("f", (s / 32768.0 for s in pcm16))
with t.Model("canary/whisper-tiny-Q5_K_M.gguf") as m, m.session() as s:
Expand All @@ -136,14 +143,14 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
pattern: dist-*
merge-multiple: true
path: dist
- name: Add cu12 wheels (only once the PyPI size request is granted)
if: vars.CU12_ON_PYPI == 'true'
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
pattern: cuda-dist-*
merge-multiple: true
Expand All @@ -164,7 +171,7 @@ jobs:
contents: write # create the release + upload assets
actions: write # dispatch wheel-index
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
pattern: cuda-dist-*
merge-multiple: true
Expand Down
Loading
Loading