From eed26b55e4bc18d95168ae2a99a65a75750d4ffb Mon Sep 17 00:00:00 2001 From: ogad-tether Date: Fri, 17 Jul 2026 09:59:03 +0100 Subject: [PATCH 1/7] ci: add per-engine CI lanes for parakeet-cpp and tts-cpp + repo-reorg QIP (PR 0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing in this repo's CI built or tested our engines until now — real verification happened three repos downstream in tetherto/qvac. This adds path-filtered workflows that, per PR: - build qvac-ext-ggml@speech (CPU-only, cached by branch-tip SHA — the same fork the ggml-speech vcpkg port ships) - build each engine + all test harnesses against it as system ggml - run the non-GPU ctest lanes (`-LE 'gpu|perf'`); model-dependent tests auto-DISABLE to "Not Run" per the existing harness design Validated locally on macOS arm64: tts lane 68/68 pass, parakeet lane green (1 runnable test model-free; parity suites need staged GGUF fixtures — follow-up documented in the QIP). GPU lanes are stubbed behind workflow_dispatch until self-hosted GPU runners exist. docs/QIP-speech-repo-reorg.md is the reorg proposal this is "PR 0" of (symmetric engines/ layout, upstream vendored as a minimally-divergent git subtree under third_party/). The workflows land against today's paths; the reorg PRs only touch the path filters. Co-Authored-By: Claude Fable 5 --- .github/workflows/parakeet-ci.yml | 128 ++++++++++++++++++++++ .github/workflows/tts-ci.yml | 128 ++++++++++++++++++++++ docs/QIP-speech-repo-reorg.md | 176 ++++++++++++++++++++++++++++++ 3 files changed, 432 insertions(+) create mode 100644 .github/workflows/parakeet-ci.yml create mode 100644 .github/workflows/tts-ci.yml create mode 100644 docs/QIP-speech-repo-reorg.md diff --git a/.github/workflows/parakeet-ci.yml b/.github/workflows/parakeet-ci.yml new file mode 100644 index 00000000000..311e93b3fcd --- /dev/null +++ b/.github/workflows/parakeet-ci.yml @@ -0,0 +1,128 @@ +# CI for the parakeet engine (parakeet-cpp/ — engines/parakeet/ after the +# repo reorg QIP lands; update the path filters below when it does). +# +# Scope (QIP "PR 0" lane): build the engine + every test harness against +# system ggml (qvac-ext-ggml @ speech, the same fork the ggml-speech vcpkg +# port ships), then run the non-GPU ctest suites. Tests whose model/reference +# fixtures aren't present are registered as DISABLED by the CMakeLists and +# show up as "Not Run" — the lane stays green with the committed fixtures +# only. GPU suites (`ctest -L gpu`) need real GPUs → self-hosted lane, see +# the gpu job stub at the bottom. +name: parakeet CI + +on: + push: + branches: [master] + paths: + - 'parakeet-cpp/**' + - '.github/workflows/parakeet-ci.yml' + pull_request: + paths: + - 'parakeet-cpp/**' + - '.github/workflows/parakeet-ci.yml' + workflow_dispatch: + inputs: + run_gpu: + description: 'Also run the GPU ctest lane (needs a [self-hosted, gpu] runner)' + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: parakeet-ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + GGML_REPO: https://github.com/tetherto/qvac-ext-ggml.git + GGML_BRANCH: speech # keep in sync with parakeet-cpp/scripts/setup-ggml.sh + +jobs: + build-test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-14] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - name: Install ccache + run: | + if [ "$RUNNER_OS" = "macOS" ]; then brew install ccache; else sudo apt-get update -q && sudo apt-get install -y ccache; fi + echo "CCACHE_DIR=${{ github.workspace }}/.ccache" >> "$GITHUB_ENV" + + - name: Resolve ggml pin (tip of ${{ env.GGML_BRANCH }}) + id: ggml + run: echo "sha=$(git ls-remote "$GGML_REPO" "refs/heads/$GGML_BRANCH" | cut -f1)" >> "$GITHUB_OUTPUT" + + - name: Cache ggml install + id: ggml-cache + uses: actions/cache@v4 + with: + path: ggml-install + key: ggml-install-${{ matrix.os }}-${{ steps.ggml.outputs.sha }} + + - name: Cache ccache + uses: actions/cache@v4 + with: + path: .ccache + key: ccache-parakeet-${{ matrix.os }}-${{ github.sha }} + restore-keys: ccache-parakeet-${{ matrix.os }}- + + # CPU-only ggml: this lane runs `-LE gpu` tests, and hosted runners + # have no Vulkan/OpenCL; Metal is exercised by the gpu lane instead. + - name: Build ggml (system ggml, qvac-ext-ggml@${{ env.GGML_BRANCH }}) + if: steps.ggml-cache.outputs.cache-hit != 'true' + run: | + git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src + git -C ggml-src checkout ${{ steps.ggml.outputs.sha }} || true + cmake -S ggml-src -B ggml-src/build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=ON \ + -DGGML_METAL=OFF \ + -DGGML_BUILD_TESTS=OFF \ + -DGGML_BUILD_EXAMPLES=OFF \ + -DCMAKE_INSTALL_PREFIX="$PWD/ggml-install" + cmake --build ggml-src/build -j4 + cmake --install ggml-src/build + + - name: Configure + run: | + cmake -S parakeet-cpp -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DPARAKEET_USE_SYSTEM_GGML=ON \ + -DPARAKEET_BUILD_TESTS=ON \ + -DCMAKE_PREFIX_PATH="$PWD/ggml-install" + + - name: Build + run: cmake --build build -j4 + + # Excludes gpu (no GPU on hosted runners) and perf (timing gates are + # meaningless on shared runners). Model-dependent tests are DISABLED + # at configure time when fixtures are absent and report as "Not Run". + - name: Test (non-GPU) + run: ctest --test-dir build -LE 'gpu|perf' --output-on-failure --timeout 600 + + # GPU lane stub: opt-in until self-hosted GPU runners exist. Trial note: + # hosted macos-14 exposes Metal, so moving `-L gpu` into the macOS + # build-test job is a cheap first experiment. + gpu: + if: github.event_name == 'workflow_dispatch' && inputs.run_gpu + runs-on: [self-hosted, gpu] + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - name: Build ggml + engine (native backends) and run GPU suites + run: | + git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src + cmake -S ggml-src -B ggml-src/build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \ + -DCMAKE_INSTALL_PREFIX="$PWD/ggml-install" + cmake --build ggml-src/build -j && cmake --install ggml-src/build + cmake -S parakeet-cpp -B build -DCMAKE_BUILD_TYPE=Release \ + -DPARAKEET_USE_SYSTEM_GGML=ON -DPARAKEET_BUILD_TESTS=ON \ + -DCMAKE_PREFIX_PATH="$PWD/ggml-install" + cmake --build build -j + ctest --test-dir build -L gpu --output-on-failure --timeout 1200 diff --git a/.github/workflows/tts-ci.yml b/.github/workflows/tts-ci.yml new file mode 100644 index 00000000000..9293dc938af --- /dev/null +++ b/.github/workflows/tts-ci.yml @@ -0,0 +1,128 @@ +# CI for the TTS engines (tts-cpp/ — engines/tts/ after the repo reorg QIP +# lands; update the path filters below when it does). +# +# Scope (QIP "PR 0" lane): build chatterbox + supertonic + lavasr and every +# test harness against system ggml (qvac-ext-ggml @ speech — mandatory here: +# TTS_CPP_USE_SYSTEM_GGML defaults ON and the bundled path is rejected in +# this tree), then run the non-GPU ctest suites (~68 pass with committed +# fixtures; model-dependent ones auto-DISABLE to "Not Run"). GPU suites need +# real GPUs → self-hosted lane, see the gpu job stub at the bottom. +name: tts CI + +on: + push: + branches: [master] + paths: + - 'tts-cpp/**' + - '.github/workflows/tts-ci.yml' + pull_request: + paths: + - 'tts-cpp/**' + - '.github/workflows/tts-ci.yml' + workflow_dispatch: + inputs: + run_gpu: + description: 'Also run the GPU ctest lane (needs a [self-hosted, gpu] runner)' + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: tts-ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + GGML_REPO: https://github.com/tetherto/qvac-ext-ggml.git + GGML_BRANCH: speech # single ggml pin shared with parakeet-ci.yml + +jobs: + build-test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-14] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - name: Install ccache + run: | + if [ "$RUNNER_OS" = "macOS" ]; then brew install ccache; else sudo apt-get update -q && sudo apt-get install -y ccache; fi + echo "CCACHE_DIR=${{ github.workspace }}/.ccache" >> "$GITHUB_ENV" + + - name: Resolve ggml pin (tip of ${{ env.GGML_BRANCH }}) + id: ggml + run: echo "sha=$(git ls-remote "$GGML_REPO" "refs/heads/$GGML_BRANCH" | cut -f1)" >> "$GITHUB_OUTPUT" + + - name: Cache ggml install + id: ggml-cache + uses: actions/cache@v4 + with: + path: ggml-install + key: ggml-install-${{ matrix.os }}-${{ steps.ggml.outputs.sha }} + + - name: Cache ccache + uses: actions/cache@v4 + with: + path: .ccache + key: ccache-tts-${{ matrix.os }}-${{ github.sha }} + restore-keys: ccache-tts-${{ matrix.os }}- + + # CPU-only ggml: this lane runs `-LE gpu` tests, and hosted runners + # have no Vulkan/OpenCL; Metal is exercised by the gpu lane instead. + # Cache key is shared with parakeet-ci.yml so whichever lane builds + # first serves both. + - name: Build ggml (system ggml, qvac-ext-ggml@${{ env.GGML_BRANCH }}) + if: steps.ggml-cache.outputs.cache-hit != 'true' + run: | + git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src + git -C ggml-src checkout ${{ steps.ggml.outputs.sha }} || true + cmake -S ggml-src -B ggml-src/build \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=ON \ + -DGGML_METAL=OFF \ + -DGGML_BUILD_TESTS=OFF \ + -DGGML_BUILD_EXAMPLES=OFF \ + -DCMAKE_INSTALL_PREFIX="$PWD/ggml-install" + cmake --build ggml-src/build -j4 + cmake --install ggml-src/build + + - name: Configure + run: | + cmake -S tts-cpp -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DTTS_CPP_BUILD_TESTS=ON \ + -DCMAKE_PREFIX_PATH="$PWD/ggml-install" + + - name: Build + run: cmake --build build -j4 + + # Excludes gpu (no GPU on hosted runners) and perf (timing gates are + # meaningless on shared runners). Model-dependent tests are DISABLED + # at configure time when fixtures are absent and report as "Not Run". + - name: Test (non-GPU) + run: ctest --test-dir build -LE 'gpu|perf' --output-on-failure --timeout 600 + + # GPU lane stub: opt-in until self-hosted GPU runners exist. Trial note: + # hosted macos-14 exposes Metal, so moving `-L gpu` / the mtl-* suites + # into the macOS build-test job is a cheap first experiment. + gpu: + if: github.event_name == 'workflow_dispatch' && inputs.run_gpu + runs-on: [self-hosted, gpu] + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - name: Build ggml + engine (native backends) and run GPU suites + run: | + git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src + cmake -S ggml-src -B ggml-src/build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \ + -DCMAKE_INSTALL_PREFIX="$PWD/ggml-install" + cmake --build ggml-src/build -j && cmake --install ggml-src/build + cmake -S tts-cpp -B build -DCMAKE_BUILD_TYPE=Release \ + -DTTS_CPP_BUILD_TESTS=ON \ + -DCMAKE_PREFIX_PATH="$PWD/ggml-install" + cmake --build build -j + ctest --test-dir build -L gpu --output-on-failure --timeout 1200 diff --git a/docs/QIP-speech-repo-reorg.md b/docs/QIP-speech-repo-reorg.md new file mode 100644 index 00000000000..4a07a3354f3 --- /dev/null +++ b/docs/QIP-speech-repo-reorg.md @@ -0,0 +1,176 @@ +# QIP: Reorganize `qvac-ext-lib-whisper.cpp` into a symmetric multi-engine speech repo + +| | | +|---|---| +| **Status** | Draft — for approval | +| **Owner** | Omar Gad | +| **Date** | 2026-07-17 | +| **Repo** | `tetherto/qvac-ext-lib-whisper.cpp` | +| **Related** | Ticket 2 (`transcription-ggml` NPM pkg), Ticket 4 (umbrella vcpkg port), repo rename (deferred) | + +## Summary + +Restructure the repo into a symmetric layout: upstream `whisper.cpp` vendored under `third_party/whisper.cpp/` via **git subtree**, our engines under `engines/{whisper,parakeet,tts}` (future: `engines/qwen-asr`), and a feature-gated umbrella `CMakeLists.txt` at the root that we own. Alongside the reorg, stand up **in-repo CI** that builds each engine and runs its existing `ctest` suites, so the team stops depending on the 3-PR downstream chain (`repo → registry → qvac`) for basic verification. + +One material amendment to the original plan: the vendored tree cannot be "pristine, never hand-edited" — **we already carry ~374 lines of patches on upstream files** (see [Audit findings](#audit-findings)). The policy is amended to *upstream-tracking, minimally divergent*, with a committed patch manifest and a CI guard that enforces the divergence stays declared and minimal. + +## Problem + +Our C++ speech engines live in asymmetric, hard-to-extend places: + +- Upstream `whisper.cpp` sits at the repo **root**, interleaved with our build glue, org workflows, and patches — there is no boundary between "upstream" and "ours". +- `parakeet-cpp/` and `tts-cpp/` are isolated sibling folders with their own conventions, not built or tested by anything in this repo's CI. +- Upstream now ships **its own `parakeet`** (`src/parakeet.cpp`, `include/parakeet.h`, C API `parakeet_*`) as of upstream PR #3735 / v1.9.1 — the root tree is no longer even purely "whisper", and it name-collides conceptually with our `parakeet-cpp` (C++ `namespace parakeet`). +- Upstream syncs are a manual squashed re-apply of the whole tree (commit `cb91a378`, single parent). Every sync re-resolves *all* of our divergence by hand instead of only the files we actually touch. +- There is no clean home for future engines (`qwen-asr`). +- **No CI in this repo exercises our engines.** The active workflows are upstream's build matrix (partially adapted in QVAC-19386) plus org-wide `check-approvals` / `security-baseline`. Real verification of parakeet/tts happens three repos downstream in `tetherto/qvac` (`on-pr-tts-ggml`), behind a registry pin bump and a `verified` label — a multi-day loop for changes that a `ctest` run would catch in minutes. + +## Audit findings + +Facts verified against the repo on 2026-07-17 that correct assumptions in the original draft: + +1. **The root tree is a fork, not a vendored copy.** Diff vs the upstream `v1.9.1` tag on upstream-owned paths: `src/whisper.cpp` +205 lines (vocab-logits slice, decoder QKV fusion — QVAC-21623), `include/whisper.h` +4, `src/whisper-logits-slice.h` (new), `ggml/src/ggml-backend-reg.cpp` +91, `ggml/src/ggml-vulkan/ggml-vulkan.cpp` +44, plus CMake glue — ~374 lines total. The v1.9.1 sync commit itself (`cb91a378`) carried ~278 of these forward. Any "never edit `third_party/`" rule is dead on arrival unless these patches get an explicit home. +2. **Canonical ggml is not in this repo.** Production builds consume the `ggml-speech` vcpkg port, built from the `qvac-ext-ggml` repo's `speech` branch. `tts-cpp` already *requires* system ggml in-tree (`TTS_CPP_USE_SYSTEM_GGML=ON`, bundled path rejected at configure time). The in-repo `ggml/` patches therefore only affect standalone dev builds — they duplicate (and can skew from) `qvac-ext-ggml/speech`. +3. **`git subtree add` cannot be applied to the existing tree directly** — the upstream files already exist at root with local modifications, and `git subtree pull` requires prior subtree metadata. The migration needs an explicit conversion step (recipe below). +4. **Both engines already have real, CI-ready test suites**: `parakeet-cpp/test` (19 harnesses: parity, streaming, determinism, perf regression) and `tts-cpp/test` (17+ harnesses), wired into `ctest` with **labels separating GPU and perf tests from the rest** (`ctest -LE 'gpu|perf'` is the hosted-runner lane). Fixtures are small (13 MB + 40 KB); tests whose model/reference files are absent auto-register as `DISABLED`, so a model-free run is still green. +5. Upstream's `.github/workflows` currently run on our repo. Moving the upstream tree under `third_party/` makes them inert automatically (GitHub only reads the root `.github/`) — a benefit, but it means we **must** ship replacement CI in the same change, or we lose the whisper build coverage `build.yml` provides today. +6. Minor: an `upstream` remote *is* configured in working clones; what's missing is scripted/documented sync, not the remote. + +## Decision + +Adopt **Model B (symmetric layout, upstream as git subtree)** with the amendments below. + +### Target layout + +``` +qvac-ext-lib-whisper.cpp/ (rename deferred — see Out of scope) + CMakeLists.txt # umbrella superbuild, feature-gated; ours + cmake/ # shared toolchain/helper modules; ours + .github/workflows/ # OUR CI only (upstream's becomes inert under third_party/) + docs/ + third_party/ + whisper.cpp/ # upstream via git subtree; minimally divergent (see policy) + PATCHES.md # manifest of every QVAC delta vs the pinned upstream tag + engines/ + whisper/ # thin adapter/glue over third_party (see Open decisions) + parakeet/ # git mv from parakeet-cpp/ + tts/ # git mv from tts-cpp/ (chatterbox + supertonic + lavasr) + qwen-asr/ # future +``` + +### Divergence policy for `third_party/whisper.cpp` (amendment) + +*Upstream-tracking, minimally divergent* — not "pristine": + +- Upstream lands via `git subtree pull --prefix third_party/whisper.cpp upstream --squash`. Subtree does a real 3-way merge, so conflicts occur **only on the handful of files we touch** (today: `src/whisper.cpp`, `src/CMakeLists.txt`, `include/whisper.h`), instead of today's whole-tree re-apply. +- Every QVAC change inside the prefix must be listed in `third_party/whisper.cpp/PATCHES.md` (file, ticket, rationale) and marked in code with `// QVAC:` comments where practical. +- **A CI guard job diffs the subtree against the pinned upstream tag and fails if any file outside the manifest differs.** This is what actually keeps divergence honest — not a convention in a doc. +- **ggml divergence is banned in this repo.** All ggml patches go to `qvac-ext-ggml` (`speech` branch), which feeds the `ggml-speech` port. The umbrella build forces system ggml (`WHISPER_USE_SYSTEM_GGML=ON` and the engines' equivalents), so the vendored `ggml/` is never compiled in our builds. The existing in-tree `ggml-backend-reg.cpp` / `ggml-vulkan.cpp` deltas must be confirmed present in `qvac-ext-ggml/speech` (or ported there) during migration, then dropped from the subtree. +- Whisper-core patches should be considered for upstreaming when generic; QVAC-specific ones (logits slice, QKV fusion) stay in the manifest. +- Upstream's `parakeet` (C API `parakeet_*`) is **not built** by the umbrella (feature-gated off) to avoid confusion with `engines/parakeet` (C++ `namespace parakeet`). Documented in `PATCHES.md` prose. + +### Umbrella build + +Root `CMakeLists.txt` (ours, from scratch — the current root one is upstream's with edits): + +```cmake +option(SPEECH_BUILD_WHISPER "Build the whisper engine" ON) +option(SPEECH_BUILD_PARAKEET "Build the parakeet engine" ON) +option(SPEECH_BUILD_TTS "Build the TTS engines" ON) +option(SPEECH_BUILD_TESTS "Build engine test harnesses" OFF) +``` + +- One `find_package(ggml CONFIG REQUIRED)` resolved from the `ggml-speech` port; all engines link the same ggml (already validated for whisper + parakeet). +- Each engine remains independently configurable standalone (their own `CMakeLists.txt` keep working) — the umbrella is additive, so per-engine vcpkg ports keep functioning until the umbrella port (Ticket 4) replaces them. + +## Migration plan + +Ordered to de-risk: CI first (so the moves are protected), then pure renames (reviewable as renames), then the vendor conversion. + +**PR 0 — CI scaffold (do now, before any reorg).** See [CI & testing](#ci--testing). Lands against current paths (`parakeet-cpp/`, `tts-cpp/`); the path updates in later PRs are one-line changes. + +**PR 1 — engine moves (rename-only).** +- `git mv parakeet-cpp engines/parakeet` and `git mv tts-cpp engines/tts` in a commit containing *no content changes*, so Git records pure renames and `git log --follow` / blame survive. Any path-string fixups (docs, scripts, CI paths) go in a separate follow-up commit in the same PR. +- Coordinate with the registry: the `parakeet-cpp` / `tts-cpp` vcpkg ports reference these paths, so the next pin bump after this PR must carry the port-side path updates (normal 3-PR propagation). + +**PR 2 — vendor conversion.** `git subtree add` can't target a prefix whose files already exist, so the conversion is: +1. `git rm -r` the upstream-owned root paths (`src/ include/ ggml/ examples/ bindings/ ci/ models/ samples/ grammars/ tests/ scripts/ Makefile …`). +2. `git subtree add --prefix third_party/whisper.cpp upstream v1.9.1 --squash` — creates proper subtree metadata so future `git subtree pull` works. +3. Re-apply our whisper delta as **one commit** ("QVAC patches on vendored whisper.cpp", cherry-picked from the pre-conversion tree) + commit `PATCHES.md`. One auditable divergence commit; the ggml deltas are *not* re-applied (routed to `qvac-ext-ggml` instead, verified first). +4. Add the new root umbrella `CMakeLists.txt` + prune root `.github/workflows` down to our own. +- Trade-off, stated plainly: blame *inside* the vendored tree points at the squash commit rather than per-line upstream history (acceptable for vendored code — upstream history lives upstream); our delta stays visible as its own commit + manifest. + +**PR 3 — sync docs + guard.** `docs/UPSTREAM-SYNC.md` (the subtree pull runbook: remote setup, `git subtree pull … --squash`, expected conflict files, post-pull checklist incl. re-verifying `PATCHES.md`), plus the CI divergence-guard job. + +**Freeze window:** PRs 1–2 rewrite most paths; land them back-to-back with open feature branches rebased immediately after (renames make `git rebase` handle it mostly automatically, but coordinate in the team channel). + +## CI & testing + +### What we can stand up now (PR 0, pre-reorg) + +Everything below runs against the engines as they exist today; the reorg only changes path filters. + +**`parakeet-ci.yml` / `tts-ci.yml`** — per-PR, path-filtered, no third-party actions beyond `actions/checkout` + `actions/cache`. *Validated end-to-end locally (macOS arm64): both engines build against a CPU-only system ggml and the test lanes pass — tts 68/68, parakeet 1 runnable (rest auto-`DISABLED`, see below).* + +| Job | Runner | What it does | +|---|---|---| +| `parakeet build-test` | `ubuntu-24.04`, `macos-14` | Build `qvac-ext-ggml@speech` (CPU-only, shared cache keyed by branch SHA) → configure `PARAKEET_USE_SYSTEM_GGML=ON` + `PARAKEET_BUILD_TESTS=ON` → build → `ctest -LE 'gpu\|perf'` | +| `tts build-test` | same | Same ggml install → `TTS_CPP_BUILD_TESTS=ON` (system ggml already mandatory in-tree) → `ctest -LE 'gpu\|perf'` — 68 tests pass model-free today | +| `gpu` (both files) | `[self-hosted, gpu]` | Stub behind `workflow_dispatch` input until runners exist; runs `ctest -L gpu` | + +- **Test filter:** the suites label tests `unit`/`fixture`/`cpu`/`gpu`/`perf`/`mtl-*`; the CI lane runs `-LE 'gpu|perf'` (GPU absent on hosted runners, perf gates meaningless on shared ones). +- **Path filters:** `parakeet-cpp/**` and `tts-cpp/**` respectively (post-reorg: `engines/parakeet/**`, `engines/tts/**`; `third_party/**` → all). +- **ggml provisioning:** both repos are public, so CI builds `qvac-ext-ggml@speech` directly (shallow clone, `actions/cache` on the install dir keyed by the branch tip SHA) — no vcpkg/registry auth in the loop. This *is* the production ggml source (`ggml-speech` port ships the same branch). The vcpkg overlay-pin variant moves to the port-smoke follow-up. +- **Models/fixtures:** committed fixtures are tiny (13 MB parakeet, 40 KB tts) and drive the lane above. The parity suites need converted GGUFs — `download-all-models.sh` pulls **~14.5 GiB of `.nemo` checkpoints requiring Python conversion**, which is not CI-viable. Both harnesses already accept pre-staged fixture roots (`PARAKEET_TEST_{MODEL,REF}_DIR` cache paths) and auto-`DISABLE` (→ "Not Run") when files are absent, so the enablement path is clear: host the small q8_0 GGUFs + `.npy` reference dumps on HF/S3 and restore them via `actions/cache` (follow-up, not PR 0). + +**`port-smoke.yml`** (follow-up to PR 0 — needs the registry portfiles as input) — packages repo HEAD as a vcpkg source tarball (codeload-style) into overlay ports for `parakeet-cpp`/`tts-cpp` and builds a minimal consumer. Catches "the port will break" *before* the registry PR, removing the most common failure of the 3-PR chain. + +**Whisper build job** — deferred to PR 2: upstream's (adapted) `build.yml` still runs at root today, so adding another whisper build now would duplicate it. PR 2 replaces it with a targeted build + `whisper-cli` smoke job when the upstream workflows go inert. + +**GPU lane (deferred, documented):** `ctest -L gpu` (`test_vk_vs_cpu`, `test_metal_ops`, Adreno/Mali suites) needs self-hosted runners with real GPUs. Scaffold the job behind a `workflow_dispatch` + `runs-on: [self-hosted, gpu]` label now, leave it manual until runners exist. Hosted `macos-14` runners do expose Metal, so `ctest -L gpu` on the macOS jobs can be trialled cheaply first. + +### What the reorg adds (PR 2/3) + +- **Umbrella job:** configure the root superbuild with all engines ON — proves the "one ggml, all engines" invariant on every PR that touches shared surface. +- **Divergence guard:** checks out the pinned upstream tag, diffs against `third_party/whisper.cpp`, fails if files outside `PATCHES.md` differ. Runs only when `third_party/**` changes. +- **Upstream CI noise gone:** upstream's workflows move under `third_party/` and stop executing; root `.github/workflows` contains only ours + org baselines. + +### What stays downstream + +`tetherto/qvac` e2e (`on-pr-tts-ggml`) remains the integration gate — this QIP doesn't replace it, it front-loads the failures that today only surface there. + +## Alternatives considered + +1. **Keep separate folders/repos + per-engine ports** — perpetuates version skew (whisper@one commit vs parakeet@another) and the asymmetry this QIP exists to remove. +2. **`git submodule` for upstream** — incompatible with vcpkg (`vcpkg_from_github` uses source tarballs, which exclude submodule content). Subtree content is baked into the tarball. +3. **Plain `git merge -X subtree=…` of upstream into the prefix** — works mechanically but leaves no subtree metadata and makes the pin implicit; `git subtree --squash` gives an explicit, greppable pin commit per sync. +4. **Truly pristine subtree + patch files applied at build time** (`engines/whisper/patches/*.patch`) — keeps the vendored tree byte-identical, but wrecks day-to-day DX (IDE navigation, debugging, blame all see un-patched sources), complicates the vcpkg port, and turns every upstream sync into patch-fuzz whack-a-mole. Rejected in favor of the minimally-divergent policy + CI guard. +5. **Pristine subtree + all our logic in an adapter layer** — impossible for the existing patches: QKV fusion and logits-slice modify whisper's internal graph construction; there is no seam an adapter could hook. + +## Consequences + +1. Symmetric engine layout; upstream pulls become 3-way merges touching ~3 known files instead of whole-tree re-applies; one in-repo upstream pin shared by all engines. +2. Engine regressions surface on the PR in minutes (`ctest -L cpu`) instead of days later in `qvac` e2e. +3. Divergence from upstream is explicit (manifest + guard) instead of implicit (scattered through a root tree that looks upstream-owned). +4. ggml patching gets a single home (`qvac-ext-ggml/speech`), removing today's silent two-copy skew. +5. Costs: blame inside the vendored tree coarsens to the squash/sync commits; a one-time freeze window for the moves; registry ports need a coordinated path bump; the team must learn one rule — *"don't touch `third_party/` outside a sync or manifest PR"* — with CI enforcing it. + +## Out of scope + +1. Repo rename to `qvac-fabric-speech.cpp` (tracked separately). +2. The umbrella vcpkg port (Ticket 4) — enabled by this reorg, delivered separately. +3. `transcription-ggml` NPM package + SDK changes (Ticket 2). +4. GPU self-hosted runner procurement (scaffolded, not delivered). + +## Open decisions + +1. **`engines/whisper/` adapter vs consuming `third_party/` directly.** Recommendation: create `engines/whisper/` now but keep it *thin* — the umbrella-facing CMake target, QVAC-side glue/headers, and the natural future home for whisper-adjacent code that would otherwise grow the `PATCHES.md`. Core patches stay in the subtree. +2. **`engines/qwen-asr/` scaffolding now vs at implementation time.** Recommendation: a README stub only; empty scaffolds rot. + +## Approvals + +| Reviewer | Role | Status | +|---|---|---| +| _TBD_ | Speech team lead | ☐ | +| _TBD_ | Infra/registry owner | ☐ | +| _TBD_ | Downstream (qvac) owner | ☐ | From 97e8ab04f232bb8c513b4bdec997beeafda8c70d Mon Sep 17 00:00:00 2001 From: ogad-tether Date: Fri, 17 Jul 2026 10:04:47 +0100 Subject: [PATCH 2/7] fix(tts): link backend_selection.cpp into campplus test targets; move QIP to #94 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First run of the new tts CI lane caught a real cross-platform bug: test-campplus and test-campplus-backward-parity compile src/campplus.cpp (which calls tts_cpp::detail::init_cpu_backend()) without src/backend_selection.cpp, where that symbol lives. Apple's toolchain happened to optimize the reference away; GNU ld on Linux fails with an undefined reference — and Linux-with-tests was never built by any CI until now. Mirror the source list test-voice-embedding already uses. Also drop docs/QIP-speech-repo-reorg.md from this branch — the QIP now has its own approval PR (#94); this PR is CI + fixes only. Co-Authored-By: Claude Fable 5 --- docs/QIP-speech-repo-reorg.md | 176 ---------------------------------- tts-cpp/CMakeLists.txt | 6 +- 2 files changed, 4 insertions(+), 178 deletions(-) delete mode 100644 docs/QIP-speech-repo-reorg.md diff --git a/docs/QIP-speech-repo-reorg.md b/docs/QIP-speech-repo-reorg.md deleted file mode 100644 index 4a07a3354f3..00000000000 --- a/docs/QIP-speech-repo-reorg.md +++ /dev/null @@ -1,176 +0,0 @@ -# QIP: Reorganize `qvac-ext-lib-whisper.cpp` into a symmetric multi-engine speech repo - -| | | -|---|---| -| **Status** | Draft — for approval | -| **Owner** | Omar Gad | -| **Date** | 2026-07-17 | -| **Repo** | `tetherto/qvac-ext-lib-whisper.cpp` | -| **Related** | Ticket 2 (`transcription-ggml` NPM pkg), Ticket 4 (umbrella vcpkg port), repo rename (deferred) | - -## Summary - -Restructure the repo into a symmetric layout: upstream `whisper.cpp` vendored under `third_party/whisper.cpp/` via **git subtree**, our engines under `engines/{whisper,parakeet,tts}` (future: `engines/qwen-asr`), and a feature-gated umbrella `CMakeLists.txt` at the root that we own. Alongside the reorg, stand up **in-repo CI** that builds each engine and runs its existing `ctest` suites, so the team stops depending on the 3-PR downstream chain (`repo → registry → qvac`) for basic verification. - -One material amendment to the original plan: the vendored tree cannot be "pristine, never hand-edited" — **we already carry ~374 lines of patches on upstream files** (see [Audit findings](#audit-findings)). The policy is amended to *upstream-tracking, minimally divergent*, with a committed patch manifest and a CI guard that enforces the divergence stays declared and minimal. - -## Problem - -Our C++ speech engines live in asymmetric, hard-to-extend places: - -- Upstream `whisper.cpp` sits at the repo **root**, interleaved with our build glue, org workflows, and patches — there is no boundary between "upstream" and "ours". -- `parakeet-cpp/` and `tts-cpp/` are isolated sibling folders with their own conventions, not built or tested by anything in this repo's CI. -- Upstream now ships **its own `parakeet`** (`src/parakeet.cpp`, `include/parakeet.h`, C API `parakeet_*`) as of upstream PR #3735 / v1.9.1 — the root tree is no longer even purely "whisper", and it name-collides conceptually with our `parakeet-cpp` (C++ `namespace parakeet`). -- Upstream syncs are a manual squashed re-apply of the whole tree (commit `cb91a378`, single parent). Every sync re-resolves *all* of our divergence by hand instead of only the files we actually touch. -- There is no clean home for future engines (`qwen-asr`). -- **No CI in this repo exercises our engines.** The active workflows are upstream's build matrix (partially adapted in QVAC-19386) plus org-wide `check-approvals` / `security-baseline`. Real verification of parakeet/tts happens three repos downstream in `tetherto/qvac` (`on-pr-tts-ggml`), behind a registry pin bump and a `verified` label — a multi-day loop for changes that a `ctest` run would catch in minutes. - -## Audit findings - -Facts verified against the repo on 2026-07-17 that correct assumptions in the original draft: - -1. **The root tree is a fork, not a vendored copy.** Diff vs the upstream `v1.9.1` tag on upstream-owned paths: `src/whisper.cpp` +205 lines (vocab-logits slice, decoder QKV fusion — QVAC-21623), `include/whisper.h` +4, `src/whisper-logits-slice.h` (new), `ggml/src/ggml-backend-reg.cpp` +91, `ggml/src/ggml-vulkan/ggml-vulkan.cpp` +44, plus CMake glue — ~374 lines total. The v1.9.1 sync commit itself (`cb91a378`) carried ~278 of these forward. Any "never edit `third_party/`" rule is dead on arrival unless these patches get an explicit home. -2. **Canonical ggml is not in this repo.** Production builds consume the `ggml-speech` vcpkg port, built from the `qvac-ext-ggml` repo's `speech` branch. `tts-cpp` already *requires* system ggml in-tree (`TTS_CPP_USE_SYSTEM_GGML=ON`, bundled path rejected at configure time). The in-repo `ggml/` patches therefore only affect standalone dev builds — they duplicate (and can skew from) `qvac-ext-ggml/speech`. -3. **`git subtree add` cannot be applied to the existing tree directly** — the upstream files already exist at root with local modifications, and `git subtree pull` requires prior subtree metadata. The migration needs an explicit conversion step (recipe below). -4. **Both engines already have real, CI-ready test suites**: `parakeet-cpp/test` (19 harnesses: parity, streaming, determinism, perf regression) and `tts-cpp/test` (17+ harnesses), wired into `ctest` with **labels separating GPU and perf tests from the rest** (`ctest -LE 'gpu|perf'` is the hosted-runner lane). Fixtures are small (13 MB + 40 KB); tests whose model/reference files are absent auto-register as `DISABLED`, so a model-free run is still green. -5. Upstream's `.github/workflows` currently run on our repo. Moving the upstream tree under `third_party/` makes them inert automatically (GitHub only reads the root `.github/`) — a benefit, but it means we **must** ship replacement CI in the same change, or we lose the whisper build coverage `build.yml` provides today. -6. Minor: an `upstream` remote *is* configured in working clones; what's missing is scripted/documented sync, not the remote. - -## Decision - -Adopt **Model B (symmetric layout, upstream as git subtree)** with the amendments below. - -### Target layout - -``` -qvac-ext-lib-whisper.cpp/ (rename deferred — see Out of scope) - CMakeLists.txt # umbrella superbuild, feature-gated; ours - cmake/ # shared toolchain/helper modules; ours - .github/workflows/ # OUR CI only (upstream's becomes inert under third_party/) - docs/ - third_party/ - whisper.cpp/ # upstream via git subtree; minimally divergent (see policy) - PATCHES.md # manifest of every QVAC delta vs the pinned upstream tag - engines/ - whisper/ # thin adapter/glue over third_party (see Open decisions) - parakeet/ # git mv from parakeet-cpp/ - tts/ # git mv from tts-cpp/ (chatterbox + supertonic + lavasr) - qwen-asr/ # future -``` - -### Divergence policy for `third_party/whisper.cpp` (amendment) - -*Upstream-tracking, minimally divergent* — not "pristine": - -- Upstream lands via `git subtree pull --prefix third_party/whisper.cpp upstream --squash`. Subtree does a real 3-way merge, so conflicts occur **only on the handful of files we touch** (today: `src/whisper.cpp`, `src/CMakeLists.txt`, `include/whisper.h`), instead of today's whole-tree re-apply. -- Every QVAC change inside the prefix must be listed in `third_party/whisper.cpp/PATCHES.md` (file, ticket, rationale) and marked in code with `// QVAC:` comments where practical. -- **A CI guard job diffs the subtree against the pinned upstream tag and fails if any file outside the manifest differs.** This is what actually keeps divergence honest — not a convention in a doc. -- **ggml divergence is banned in this repo.** All ggml patches go to `qvac-ext-ggml` (`speech` branch), which feeds the `ggml-speech` port. The umbrella build forces system ggml (`WHISPER_USE_SYSTEM_GGML=ON` and the engines' equivalents), so the vendored `ggml/` is never compiled in our builds. The existing in-tree `ggml-backend-reg.cpp` / `ggml-vulkan.cpp` deltas must be confirmed present in `qvac-ext-ggml/speech` (or ported there) during migration, then dropped from the subtree. -- Whisper-core patches should be considered for upstreaming when generic; QVAC-specific ones (logits slice, QKV fusion) stay in the manifest. -- Upstream's `parakeet` (C API `parakeet_*`) is **not built** by the umbrella (feature-gated off) to avoid confusion with `engines/parakeet` (C++ `namespace parakeet`). Documented in `PATCHES.md` prose. - -### Umbrella build - -Root `CMakeLists.txt` (ours, from scratch — the current root one is upstream's with edits): - -```cmake -option(SPEECH_BUILD_WHISPER "Build the whisper engine" ON) -option(SPEECH_BUILD_PARAKEET "Build the parakeet engine" ON) -option(SPEECH_BUILD_TTS "Build the TTS engines" ON) -option(SPEECH_BUILD_TESTS "Build engine test harnesses" OFF) -``` - -- One `find_package(ggml CONFIG REQUIRED)` resolved from the `ggml-speech` port; all engines link the same ggml (already validated for whisper + parakeet). -- Each engine remains independently configurable standalone (their own `CMakeLists.txt` keep working) — the umbrella is additive, so per-engine vcpkg ports keep functioning until the umbrella port (Ticket 4) replaces them. - -## Migration plan - -Ordered to de-risk: CI first (so the moves are protected), then pure renames (reviewable as renames), then the vendor conversion. - -**PR 0 — CI scaffold (do now, before any reorg).** See [CI & testing](#ci--testing). Lands against current paths (`parakeet-cpp/`, `tts-cpp/`); the path updates in later PRs are one-line changes. - -**PR 1 — engine moves (rename-only).** -- `git mv parakeet-cpp engines/parakeet` and `git mv tts-cpp engines/tts` in a commit containing *no content changes*, so Git records pure renames and `git log --follow` / blame survive. Any path-string fixups (docs, scripts, CI paths) go in a separate follow-up commit in the same PR. -- Coordinate with the registry: the `parakeet-cpp` / `tts-cpp` vcpkg ports reference these paths, so the next pin bump after this PR must carry the port-side path updates (normal 3-PR propagation). - -**PR 2 — vendor conversion.** `git subtree add` can't target a prefix whose files already exist, so the conversion is: -1. `git rm -r` the upstream-owned root paths (`src/ include/ ggml/ examples/ bindings/ ci/ models/ samples/ grammars/ tests/ scripts/ Makefile …`). -2. `git subtree add --prefix third_party/whisper.cpp upstream v1.9.1 --squash` — creates proper subtree metadata so future `git subtree pull` works. -3. Re-apply our whisper delta as **one commit** ("QVAC patches on vendored whisper.cpp", cherry-picked from the pre-conversion tree) + commit `PATCHES.md`. One auditable divergence commit; the ggml deltas are *not* re-applied (routed to `qvac-ext-ggml` instead, verified first). -4. Add the new root umbrella `CMakeLists.txt` + prune root `.github/workflows` down to our own. -- Trade-off, stated plainly: blame *inside* the vendored tree points at the squash commit rather than per-line upstream history (acceptable for vendored code — upstream history lives upstream); our delta stays visible as its own commit + manifest. - -**PR 3 — sync docs + guard.** `docs/UPSTREAM-SYNC.md` (the subtree pull runbook: remote setup, `git subtree pull … --squash`, expected conflict files, post-pull checklist incl. re-verifying `PATCHES.md`), plus the CI divergence-guard job. - -**Freeze window:** PRs 1–2 rewrite most paths; land them back-to-back with open feature branches rebased immediately after (renames make `git rebase` handle it mostly automatically, but coordinate in the team channel). - -## CI & testing - -### What we can stand up now (PR 0, pre-reorg) - -Everything below runs against the engines as they exist today; the reorg only changes path filters. - -**`parakeet-ci.yml` / `tts-ci.yml`** — per-PR, path-filtered, no third-party actions beyond `actions/checkout` + `actions/cache`. *Validated end-to-end locally (macOS arm64): both engines build against a CPU-only system ggml and the test lanes pass — tts 68/68, parakeet 1 runnable (rest auto-`DISABLED`, see below).* - -| Job | Runner | What it does | -|---|---|---| -| `parakeet build-test` | `ubuntu-24.04`, `macos-14` | Build `qvac-ext-ggml@speech` (CPU-only, shared cache keyed by branch SHA) → configure `PARAKEET_USE_SYSTEM_GGML=ON` + `PARAKEET_BUILD_TESTS=ON` → build → `ctest -LE 'gpu\|perf'` | -| `tts build-test` | same | Same ggml install → `TTS_CPP_BUILD_TESTS=ON` (system ggml already mandatory in-tree) → `ctest -LE 'gpu\|perf'` — 68 tests pass model-free today | -| `gpu` (both files) | `[self-hosted, gpu]` | Stub behind `workflow_dispatch` input until runners exist; runs `ctest -L gpu` | - -- **Test filter:** the suites label tests `unit`/`fixture`/`cpu`/`gpu`/`perf`/`mtl-*`; the CI lane runs `-LE 'gpu|perf'` (GPU absent on hosted runners, perf gates meaningless on shared ones). -- **Path filters:** `parakeet-cpp/**` and `tts-cpp/**` respectively (post-reorg: `engines/parakeet/**`, `engines/tts/**`; `third_party/**` → all). -- **ggml provisioning:** both repos are public, so CI builds `qvac-ext-ggml@speech` directly (shallow clone, `actions/cache` on the install dir keyed by the branch tip SHA) — no vcpkg/registry auth in the loop. This *is* the production ggml source (`ggml-speech` port ships the same branch). The vcpkg overlay-pin variant moves to the port-smoke follow-up. -- **Models/fixtures:** committed fixtures are tiny (13 MB parakeet, 40 KB tts) and drive the lane above. The parity suites need converted GGUFs — `download-all-models.sh` pulls **~14.5 GiB of `.nemo` checkpoints requiring Python conversion**, which is not CI-viable. Both harnesses already accept pre-staged fixture roots (`PARAKEET_TEST_{MODEL,REF}_DIR` cache paths) and auto-`DISABLE` (→ "Not Run") when files are absent, so the enablement path is clear: host the small q8_0 GGUFs + `.npy` reference dumps on HF/S3 and restore them via `actions/cache` (follow-up, not PR 0). - -**`port-smoke.yml`** (follow-up to PR 0 — needs the registry portfiles as input) — packages repo HEAD as a vcpkg source tarball (codeload-style) into overlay ports for `parakeet-cpp`/`tts-cpp` and builds a minimal consumer. Catches "the port will break" *before* the registry PR, removing the most common failure of the 3-PR chain. - -**Whisper build job** — deferred to PR 2: upstream's (adapted) `build.yml` still runs at root today, so adding another whisper build now would duplicate it. PR 2 replaces it with a targeted build + `whisper-cli` smoke job when the upstream workflows go inert. - -**GPU lane (deferred, documented):** `ctest -L gpu` (`test_vk_vs_cpu`, `test_metal_ops`, Adreno/Mali suites) needs self-hosted runners with real GPUs. Scaffold the job behind a `workflow_dispatch` + `runs-on: [self-hosted, gpu]` label now, leave it manual until runners exist. Hosted `macos-14` runners do expose Metal, so `ctest -L gpu` on the macOS jobs can be trialled cheaply first. - -### What the reorg adds (PR 2/3) - -- **Umbrella job:** configure the root superbuild with all engines ON — proves the "one ggml, all engines" invariant on every PR that touches shared surface. -- **Divergence guard:** checks out the pinned upstream tag, diffs against `third_party/whisper.cpp`, fails if files outside `PATCHES.md` differ. Runs only when `third_party/**` changes. -- **Upstream CI noise gone:** upstream's workflows move under `third_party/` and stop executing; root `.github/workflows` contains only ours + org baselines. - -### What stays downstream - -`tetherto/qvac` e2e (`on-pr-tts-ggml`) remains the integration gate — this QIP doesn't replace it, it front-loads the failures that today only surface there. - -## Alternatives considered - -1. **Keep separate folders/repos + per-engine ports** — perpetuates version skew (whisper@one commit vs parakeet@another) and the asymmetry this QIP exists to remove. -2. **`git submodule` for upstream** — incompatible with vcpkg (`vcpkg_from_github` uses source tarballs, which exclude submodule content). Subtree content is baked into the tarball. -3. **Plain `git merge -X subtree=…` of upstream into the prefix** — works mechanically but leaves no subtree metadata and makes the pin implicit; `git subtree --squash` gives an explicit, greppable pin commit per sync. -4. **Truly pristine subtree + patch files applied at build time** (`engines/whisper/patches/*.patch`) — keeps the vendored tree byte-identical, but wrecks day-to-day DX (IDE navigation, debugging, blame all see un-patched sources), complicates the vcpkg port, and turns every upstream sync into patch-fuzz whack-a-mole. Rejected in favor of the minimally-divergent policy + CI guard. -5. **Pristine subtree + all our logic in an adapter layer** — impossible for the existing patches: QKV fusion and logits-slice modify whisper's internal graph construction; there is no seam an adapter could hook. - -## Consequences - -1. Symmetric engine layout; upstream pulls become 3-way merges touching ~3 known files instead of whole-tree re-applies; one in-repo upstream pin shared by all engines. -2. Engine regressions surface on the PR in minutes (`ctest -L cpu`) instead of days later in `qvac` e2e. -3. Divergence from upstream is explicit (manifest + guard) instead of implicit (scattered through a root tree that looks upstream-owned). -4. ggml patching gets a single home (`qvac-ext-ggml/speech`), removing today's silent two-copy skew. -5. Costs: blame inside the vendored tree coarsens to the squash/sync commits; a one-time freeze window for the moves; registry ports need a coordinated path bump; the team must learn one rule — *"don't touch `third_party/` outside a sync or manifest PR"* — with CI enforcing it. - -## Out of scope - -1. Repo rename to `qvac-fabric-speech.cpp` (tracked separately). -2. The umbrella vcpkg port (Ticket 4) — enabled by this reorg, delivered separately. -3. `transcription-ggml` NPM package + SDK changes (Ticket 2). -4. GPU self-hosted runner procurement (scaffolded, not delivered). - -## Open decisions - -1. **`engines/whisper/` adapter vs consuming `third_party/` directly.** Recommendation: create `engines/whisper/` now but keep it *thin* — the umbrella-facing CMake target, QVAC-side glue/headers, and the natural future home for whisper-adjacent code that would otherwise grow the `PATCHES.md`. Core patches stay in the subtree. -2. **`engines/qwen-asr/` scaffolding now vs at implementation time.** Recommendation: a README stub only; empty scaffolds rot. - -## Approvals - -| Reviewer | Role | Status | -|---|---|---| -| _TBD_ | Speech team lead | ☐ | -| _TBD_ | Infra/registry owner | ☐ | -| _TBD_ | Downstream (qvac) owner | ☐ | diff --git a/tts-cpp/CMakeLists.txt b/tts-cpp/CMakeLists.txt index 23d047cc99b..c0947de4d10 100644 --- a/tts-cpp/CMakeLists.txt +++ b/tts-cpp/CMakeLists.txt @@ -589,7 +589,8 @@ if (TTS_CPP_BUILD_TESTS) add_executable(test-campplus test/test_campplus.cpp - src/campplus.cpp) + src/campplus.cpp + src/backend_selection.cpp) target_link_libraries(test-campplus PRIVATE ggml) target_include_directories(test-campplus PRIVATE ggml/include src) if (OpenMP_CXX_FOUND) @@ -1165,7 +1166,8 @@ if (TTS_CPP_BUILD_TESTS) add_executable(test-campplus-backward-parity test/test_campplus_backward_parity.cpp src/campplus_backward.cpp - src/campplus.cpp) + src/campplus.cpp + src/backend_selection.cpp) target_link_libraries(test-campplus-backward-parity PRIVATE ggml) target_include_directories(test-campplus-backward-parity PRIVATE ggml/include src) if (OpenMP_CXX_FOUND) From e39334b1ebefa22efcee40f1da6781bb940636e4 Mon Sep 17 00:00:00 2001 From: ogad-tether Date: Fri, 17 Jul 2026 10:09:14 +0100 Subject: [PATCH 3/7] ci: export LD_LIBRARY_PATH for the ctest step (transitive ggml .so loads) On ubuntu the test binaries resolve libqvac-speech-ggml.so via their build RUNPATH, but RUNPATH is not consulted for that library's own transitive dependencies (libqvac-speech-ggml-cpu.so.0 et al), so 35 tts tests failed at exec with "cannot open shared object file". macOS is unaffected (@rpath resolution walks the executable's rpath stack for the whole load chain). Point LD_LIBRARY_PATH at the ggml install dir for the ctest step in both lanes. Co-Authored-By: Claude Fable 5 --- .github/workflows/parakeet-ci.yml | 6 ++++++ .github/workflows/tts-ci.yml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/parakeet-ci.yml b/.github/workflows/parakeet-ci.yml index 311e93b3fcd..b84d15f1446 100644 --- a/.github/workflows/parakeet-ci.yml +++ b/.github/workflows/parakeet-ci.yml @@ -103,7 +103,13 @@ jobs: # Excludes gpu (no GPU on hosted runners) and perf (timing gates are # meaningless on shared runners). Model-dependent tests are DISABLED # at configure time when fixtures are absent and report as "Not Run". + # LD_LIBRARY_PATH: the ggml install ships libqvac-speech-ggml.so whose + # own dependency on the ggml-cpu/base siblings is resolved transitively + # — Linux RUNPATH doesn't apply to transitive loads, so the loader + # needs the directory explicitly (macOS @rpath chains handle it). - name: Test (non-GPU) + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/ggml-install/lib run: ctest --test-dir build -LE 'gpu|perf' --output-on-failure --timeout 600 # GPU lane stub: opt-in until self-hosted GPU runners exist. Trial note: diff --git a/.github/workflows/tts-ci.yml b/.github/workflows/tts-ci.yml index 9293dc938af..9b9f98e5cfb 100644 --- a/.github/workflows/tts-ci.yml +++ b/.github/workflows/tts-ci.yml @@ -103,7 +103,13 @@ jobs: # Excludes gpu (no GPU on hosted runners) and perf (timing gates are # meaningless on shared runners). Model-dependent tests are DISABLED # at configure time when fixtures are absent and report as "Not Run". + # LD_LIBRARY_PATH: the ggml install ships libqvac-speech-ggml.so whose + # own dependency on the ggml-cpu/base siblings is resolved transitively + # — Linux RUNPATH doesn't apply to transitive loads, so the loader + # needs the directory explicitly (macOS @rpath chains handle it). - name: Test (non-GPU) + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/ggml-install/lib run: ctest --test-dir build -LE 'gpu|perf' --output-on-failure --timeout 600 # GPU lane stub: opt-in until self-hosted GPU runners exist. Trial note: From 176336f1b0356984cab1f90c2101d6e6f37dcd6a Mon Sep 17 00:00:00 2001 From: ogad-tether Date: Fri, 17 Jul 2026 10:30:30 +0100 Subject: [PATCH 4/7] ci: add Windows build+test and Android/iOS compile-smoke lanes for the engines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit engines-cross-ci.yml: engine x platform matrix complementing the linux/mac lanes — - windows-2022: MSVC, static system ggml, full build + non-GPU ctest - android: NDK arm64-v8a compile smoke (no device to run tests on) - ios: device arm64 compile smoke, Metal embedded (flags mirror build-xcframework.sh) iOS recipe validated locally (ggml + both engines compile clean against the iphoneos 26.2 SDK). On-device e2e remains downstream in tetherto/qvac; these lanes front-load cross-compile/link breaks ahead of the repo-reorg moves (QIP #94). Co-Authored-By: Claude Fable 5 --- .github/workflows/engines-cross-ci.yml | 215 +++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 .github/workflows/engines-cross-ci.yml diff --git a/.github/workflows/engines-cross-ci.yml b/.github/workflows/engines-cross-ci.yml new file mode 100644 index 00000000000..ba3afd8791b --- /dev/null +++ b/.github/workflows/engines-cross-ci.yml @@ -0,0 +1,215 @@ +# Cross-platform lanes for the engines (parakeet-cpp/, tts-cpp/ — engines/* +# after the repo reorg QIP; update the path filters + SRC map when it lands). +# +# Complements parakeet-ci.yml / tts-ci.yml (linux+mac build+test): +# - windows: full build + non-GPU ctest (MSVC, static ggml) +# - android: compile smoke, arm64-v8a via NDK (no device to run tests on) +# - ios: compile smoke, arm64 device slice, Metal embedded (flags +# mirror build-xcframework.sh) +# On-device e2e stays downstream in tetherto/qvac; these lanes exist to +# catch cross-compile/link breaks *before* the registry pin chain. +name: engines cross CI + +on: + push: + branches: [master] + paths: + - 'parakeet-cpp/**' + - 'tts-cpp/**' + - '.github/workflows/engines-cross-ci.yml' + pull_request: + paths: + - 'parakeet-cpp/**' + - 'tts-cpp/**' + - '.github/workflows/engines-cross-ci.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: engines-cross-ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + GGML_REPO: https://github.com/tetherto/qvac-ext-ggml.git + GGML_BRANCH: speech # single ggml pin shared with parakeet-ci.yml / tts-ci.yml + +jobs: + windows: + strategy: + fail-fast: false + matrix: + engine: [parakeet, tts] + include: + - engine: parakeet + src: parakeet-cpp + cmake_flags: -DPARAKEET_USE_SYSTEM_GGML=ON -DPARAKEET_BUILD_TESTS=ON + - engine: tts + src: tts-cpp + cmake_flags: -DTTS_CPP_BUILD_TESTS=ON + runs-on: windows-2022 + timeout-minutes: 90 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + + - name: Resolve ggml pin (tip of ${{ env.GGML_BRANCH }}) + id: ggml + run: echo "sha=$(git ls-remote "$GGML_REPO" "refs/heads/$GGML_BRANCH" | cut -f1)" >> "$GITHUB_OUTPUT" + + - name: Cache ggml install + id: ggml-cache + uses: actions/cache@v4 + with: + path: ggml-install + key: ggml-install-windows-2022-msvc-static-${{ steps.ggml.outputs.sha }} + + # Static ggml on Windows sidesteps DLL discovery for the test exes. + - name: Build ggml (MSVC, static, CPU-only) + if: steps.ggml-cache.outputs.cache-hit != 'true' + run: | + git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src + cmake -S ggml-src -B ggml-src/build -A x64 \ + -DBUILD_SHARED_LIBS=OFF \ + -DGGML_BUILD_TESTS=OFF \ + -DGGML_BUILD_EXAMPLES=OFF \ + -DCMAKE_INSTALL_PREFIX="$PWD/ggml-install" + cmake --build ggml-src/build --config Release -j4 + cmake --install ggml-src/build --config Release + + - name: Configure + run: | + cmake -S ${{ matrix.src }} -B build -A x64 \ + ${{ matrix.cmake_flags }} \ + -DCMAKE_PREFIX_PATH="$PWD/ggml-install" + + - name: Build + run: cmake --build build --config Release -j4 + + - name: Test (non-GPU) + run: ctest --test-dir build -C Release -LE 'gpu|perf' --output-on-failure --timeout 600 + + android: + strategy: + fail-fast: false + matrix: + engine: [parakeet, tts] + include: + - engine: parakeet + src: parakeet-cpp + cmake_flags: -DPARAKEET_USE_SYSTEM_GGML=ON -DPARAKEET_BUILD_TESTS=OFF + - engine: tts + src: tts-cpp + cmake_flags: -DTTS_CPP_BUILD_TESTS=OFF + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - name: Resolve ggml pin (tip of ${{ env.GGML_BRANCH }}) + id: ggml + run: echo "sha=$(git ls-remote "$GGML_REPO" "refs/heads/$GGML_BRANCH" | cut -f1)" >> "$GITHUB_OUTPUT" + + - name: Cache ggml install + id: ggml-cache + uses: actions/cache@v4 + with: + path: ggml-install + key: ggml-install-android-arm64-${{ steps.ggml.outputs.sha }} + + - name: Build ggml (NDK arm64-v8a, static, CPU-only) + if: steps.ggml-cache.outputs.cache-hit != 'true' + run: | + git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src + cmake -S ggml-src -B ggml-src/build \ + -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_LATEST_HOME/build/cmake/android.toolchain.cmake" \ + -DANDROID_ABI=arm64-v8a \ + -DANDROID_PLATFORM=android-28 \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DGGML_OPENMP=OFF \ + -DGGML_BUILD_TESTS=OFF \ + -DGGML_BUILD_EXAMPLES=OFF \ + -DCMAKE_INSTALL_PREFIX="$PWD/ggml-install" + cmake --build ggml-src/build -j4 + cmake --install ggml-src/build + + # Compile smoke only: hosted runners can't execute arm64 Android + # binaries. CMAKE_FIND_ROOT_PATH (not PREFIX_PATH) because the NDK + # toolchain restricts find_package() to the find-root. + - name: Configure + build engine (compile smoke) + run: | + cmake -S ${{ matrix.src }} -B build \ + -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_LATEST_HOME/build/cmake/android.toolchain.cmake" \ + -DANDROID_ABI=arm64-v8a \ + -DANDROID_PLATFORM=android-28 \ + -DCMAKE_BUILD_TYPE=Release \ + ${{ matrix.cmake_flags }} \ + -DCMAKE_FIND_ROOT_PATH="$PWD/ggml-install" + cmake --build build -j4 + + ios: + strategy: + fail-fast: false + matrix: + engine: [parakeet, tts] + include: + - engine: parakeet + src: parakeet-cpp + cmake_flags: -DPARAKEET_USE_SYSTEM_GGML=ON -DPARAKEET_BUILD_TESTS=OFF + - engine: tts + src: tts-cpp + cmake_flags: -DTTS_CPP_BUILD_TESTS=OFF + runs-on: macos-14 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - name: Resolve ggml pin (tip of ${{ env.GGML_BRANCH }}) + id: ggml + run: echo "sha=$(git ls-remote "$GGML_REPO" "refs/heads/$GGML_BRANCH" | cut -f1)" >> "$GITHUB_OUTPUT" + + - name: Cache ggml install + id: ggml-cache + uses: actions/cache@v4 + with: + path: ggml-install + key: ggml-install-ios-arm64-${{ steps.ggml.outputs.sha }} + + # Flags mirror build-xcframework.sh's iOS-device slice (Metal embedded). + - name: Build ggml (iOS device arm64, static, Metal embedded) + if: steps.ggml-cache.outputs.cache-hit != 'true' + run: | + git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src + cmake -S ggml-src -B ggml-src/build \ + -DCMAKE_SYSTEM_NAME=iOS \ + -DCMAKE_OSX_SYSROOT=iphoneos \ + -DCMAKE_OSX_ARCHITECTURES=arm64 \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=16.4 \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DGGML_METAL=ON \ + -DGGML_METAL_EMBED_LIBRARY=ON \ + -DGGML_METAL_USE_BF16=ON \ + -DGGML_OPENMP=OFF \ + -DGGML_BUILD_TESTS=OFF \ + -DGGML_BUILD_EXAMPLES=OFF \ + -DCMAKE_INSTALL_PREFIX="$PWD/ggml-install" + cmake --build ggml-src/build -j4 + cmake --install ggml-src/build + + # Compile smoke only (no iOS device on hosted runners). + - name: Configure + build engine (compile smoke) + run: | + cmake -S ${{ matrix.src }} -B build \ + -DCMAKE_SYSTEM_NAME=iOS \ + -DCMAKE_OSX_SYSROOT=iphoneos \ + -DCMAKE_OSX_ARCHITECTURES=arm64 \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=16.4 \ + -DCMAKE_BUILD_TYPE=Release \ + ${{ matrix.cmake_flags }} \ + -DCMAKE_FIND_ROOT_PATH="$PWD/ggml-install" + cmake --build build -j4 From 882c8149b65ea64acf2ff5381e4ee13e1e33d383 Mon Sep 17 00:00:00 2001 From: ogad-tether Date: Fri, 17 Jul 2026 10:38:09 +0100 Subject: [PATCH 5/7] fix(tts): portable setenv/unsetenv shim for MSVC test builds; skip examples in cross lanes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new Windows lane's first run caught 9 test files calling POSIX setenv/unsetenv, which MSVC's CRT doesn't provide — add test/test_env_portable.h (same-named _putenv_s shims on _MSC_VER, plain elsewhere) and include it where used. Also -DPARAKEET_BUILD_EXAMPLES=OFF in the windows/android/ios lanes: the live-mic examples hand GCC-style warning flags to cl.exe and use mic-capture APIs that don't exist on the iOS device SDK; examples are dev conveniences, not part of the cross-platform surface. Co-Authored-By: Claude Fable 5 --- .github/workflows/engines-cross-ci.yml | 6 +++--- tts-cpp/test/test_env_portable.h | 13 +++++++++++++ tts-cpp/test/test_s3gen_sched_equivalence.cpp | 1 + tts-cpp/test/test_supertonic_profile_csv.cpp | 1 + tts-cpp/test/test_supertonic_sched_equivalence.cpp | 1 + .../test/test_supertonic_vulkan_env_overrides.cpp | 1 + tts-cpp/test/test_t3_alignment_analyzer.cpp | 1 + tts-cpp/test/test_t3_caches.cpp | 1 + tts-cpp/test/test_t3_sched_dispatch.cpp | 1 + tts-cpp/test/test_t3_sched_equivalence.cpp | 1 + tts-cpp/test/test_t3_stop_controller.cpp | 1 + 11 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 tts-cpp/test/test_env_portable.h diff --git a/.github/workflows/engines-cross-ci.yml b/.github/workflows/engines-cross-ci.yml index ba3afd8791b..278e0c490c6 100644 --- a/.github/workflows/engines-cross-ci.yml +++ b/.github/workflows/engines-cross-ci.yml @@ -44,7 +44,7 @@ jobs: include: - engine: parakeet src: parakeet-cpp - cmake_flags: -DPARAKEET_USE_SYSTEM_GGML=ON -DPARAKEET_BUILD_TESTS=ON + cmake_flags: -DPARAKEET_USE_SYSTEM_GGML=ON -DPARAKEET_BUILD_TESTS=ON -DPARAKEET_BUILD_EXAMPLES=OFF - engine: tts src: tts-cpp cmake_flags: -DTTS_CPP_BUILD_TESTS=ON @@ -100,7 +100,7 @@ jobs: include: - engine: parakeet src: parakeet-cpp - cmake_flags: -DPARAKEET_USE_SYSTEM_GGML=ON -DPARAKEET_BUILD_TESTS=OFF + cmake_flags: -DPARAKEET_USE_SYSTEM_GGML=ON -DPARAKEET_BUILD_TESTS=OFF -DPARAKEET_BUILD_EXAMPLES=OFF - engine: tts src: tts-cpp cmake_flags: -DTTS_CPP_BUILD_TESTS=OFF @@ -159,7 +159,7 @@ jobs: include: - engine: parakeet src: parakeet-cpp - cmake_flags: -DPARAKEET_USE_SYSTEM_GGML=ON -DPARAKEET_BUILD_TESTS=OFF + cmake_flags: -DPARAKEET_USE_SYSTEM_GGML=ON -DPARAKEET_BUILD_TESTS=OFF -DPARAKEET_BUILD_EXAMPLES=OFF - engine: tts src: tts-cpp cmake_flags: -DTTS_CPP_BUILD_TESTS=OFF diff --git a/tts-cpp/test/test_env_portable.h b/tts-cpp/test/test_env_portable.h new file mode 100644 index 00000000000..05edff52279 --- /dev/null +++ b/tts-cpp/test/test_env_portable.h @@ -0,0 +1,13 @@ +#pragma once +// Portable setenv/unsetenv for the test harnesses. MSVC's CRT has no +// POSIX setenv/unsetenv (only _putenv_s), so provide same-named shims +// there; every other platform gets the real functions from . +#include +#ifdef _MSC_VER +inline int setenv(const char * name, const char * value, int /*overwrite*/) { + return _putenv_s(name, value); +} +inline int unsetenv(const char * name) { + return _putenv_s(name, ""); // empty value removes the variable +} +#endif diff --git a/tts-cpp/test/test_s3gen_sched_equivalence.cpp b/tts-cpp/test/test_s3gen_sched_equivalence.cpp index 31b7dfb7c9a..90cf0a7102e 100644 --- a/tts-cpp/test/test_s3gen_sched_equivalence.cpp +++ b/tts-cpp/test/test_s3gen_sched_equivalence.cpp @@ -23,6 +23,7 @@ // // usage: test-s3gen-sched-equivalence MODEL.gguf [n_gpu_layers] +#include "test_env_portable.h" #include "tts-cpp/chatterbox/s3gen_pipeline.h" #include diff --git a/tts-cpp/test/test_supertonic_profile_csv.cpp b/tts-cpp/test/test_supertonic_profile_csv.cpp index 780fc376e76..d07565d8f18 100644 --- a/tts-cpp/test/test_supertonic_profile_csv.cpp +++ b/tts-cpp/test/test_supertonic_profile_csv.cpp @@ -37,6 +37,7 @@ // Registered with `LABEL "unit"` in CMakeLists.txt — no GGUF // required. +#include "test_env_portable.h" #include "supertonic_internal.h" #include diff --git a/tts-cpp/test/test_supertonic_sched_equivalence.cpp b/tts-cpp/test/test_supertonic_sched_equivalence.cpp index 169aa081c6e..918b490164e 100644 --- a/tts-cpp/test/test_supertonic_sched_equivalence.cpp +++ b/tts-cpp/test/test_supertonic_sched_equivalence.cpp @@ -25,6 +25,7 @@ // // usage: test-supertonic-sched-equivalence MODEL.gguf [n_gpu_layers] +#include "test_env_portable.h" #include "tts-cpp/supertonic/engine.h" #include diff --git a/tts-cpp/test/test_supertonic_vulkan_env_overrides.cpp b/tts-cpp/test/test_supertonic_vulkan_env_overrides.cpp index c578c096eab..516c94293ff 100644 --- a/tts-cpp/test/test_supertonic_vulkan_env_overrides.cpp +++ b/tts-cpp/test/test_supertonic_vulkan_env_overrides.cpp @@ -51,6 +51,7 @@ // Whole TU MUST fail to compile before the symbols are added, // then pass after. +#include "test_env_portable.h" #include "tts-cpp/supertonic/engine.h" #include "supertonic_internal.h" diff --git a/tts-cpp/test/test_t3_alignment_analyzer.cpp b/tts-cpp/test/test_t3_alignment_analyzer.cpp index af35ef36f76..65c18372e71 100644 --- a/tts-cpp/test/test_t3_alignment_analyzer.cpp +++ b/tts-cpp/test/test_t3_alignment_analyzer.cpp @@ -11,6 +11,7 @@ // - token repetition only forces EOS once complete (avoids #519/#587 early cut) // - short text / empty row / disabled => no force +#include "test_env_portable.h" #include "t3_alignment_analyzer.h" #include diff --git a/tts-cpp/test/test_t3_caches.cpp b/tts-cpp/test/test_t3_caches.cpp index ee079d4eb0b..85cb5f7ee02 100644 --- a/tts-cpp/test/test_t3_caches.cpp +++ b/tts-cpp/test/test_t3_caches.cpp @@ -28,6 +28,7 @@ // Without arguments, runs only the lightweight default-state // invariants (no model load required). +#include "test_env_portable.h" #include "chatterbox_t3_internal.h" #include "chatterbox_tts_test_hooks.h" diff --git a/tts-cpp/test/test_t3_sched_dispatch.cpp b/tts-cpp/test/test_t3_sched_dispatch.cpp index 5bc3119a0ea..ba242c19b5b 100644 --- a/tts-cpp/test/test_t3_sched_dispatch.cpp +++ b/tts-cpp/test/test_t3_sched_dispatch.cpp @@ -12,6 +12,7 @@ // Registered with `LABEL "unit"` in CMakeLists.txt so a fresh // checkout's `ctest` exercises this without needing any fixture. +#include "test_env_portable.h" #include "backend_selection.h" #include "sched_dispatch.h" diff --git a/tts-cpp/test/test_t3_sched_equivalence.cpp b/tts-cpp/test/test_t3_sched_equivalence.cpp index 9cdcdc98305..c19cd043f4a 100644 --- a/tts-cpp/test/test_t3_sched_equivalence.cpp +++ b/tts-cpp/test/test_t3_sched_equivalence.cpp @@ -23,6 +23,7 @@ // it, phase B must BYPASS the cached graphs, and phase A' proves they // were left intact. +#include "test_env_portable.h" #include "chatterbox_t3_internal.h" #include "ggml.h" diff --git a/tts-cpp/test/test_t3_stop_controller.cpp b/tts-cpp/test/test_t3_stop_controller.cpp index 55a28c44844..c0fdff90c45 100644 --- a/tts-cpp/test/test_t3_stop_controller.cpp +++ b/tts-cpp/test/test_t3_stop_controller.cpp @@ -16,6 +16,7 @@ // EOS, no post-check stop. // - make_mtl_stop_params budget scaling + env overrides. +#include "test_env_portable.h" #include "t3_stop_controller.h" #include From 50183e36a94262ad8423c03e3604adf8a4131315 Mon Sep 17 00:00:00 2001 From: ogad-tether Date: Fri, 17 Jul 2026 10:46:22 +0100 Subject: [PATCH 6/7] fix(tts): portable temp-dir fallback for tests that write scratch GGUFs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test-gguf-stream and test-lavasr-enhancer-ggml consulted only TMPDIR with a hard "/tmp" fallback — Windows sets TEMP/TMP and has no /tmp, so both failed on the new Windows lane. Add test_tmpdir() to test_env_portable.h (TMPDIR -> TEMP -> TMP -> "/tmp") and use it. Co-Authored-By: Claude Fable 5 --- tts-cpp/test/test_env_portable.h | 12 ++++++++++++ tts-cpp/test/test_gguf_stream.cpp | 4 ++-- tts-cpp/test/test_lavasr_enhancer_ggml.cpp | 5 ++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/tts-cpp/test/test_env_portable.h b/tts-cpp/test/test_env_portable.h index 05edff52279..3e46cbd2414 100644 --- a/tts-cpp/test/test_env_portable.h +++ b/tts-cpp/test/test_env_portable.h @@ -3,6 +3,8 @@ // POSIX setenv/unsetenv (only _putenv_s), so provide same-named shims // there; every other platform gets the real functions from . #include +#include +#include #ifdef _MSC_VER inline int setenv(const char * name, const char * value, int /*overwrite*/) { return _putenv_s(name, value); @@ -11,3 +13,13 @@ inline int unsetenv(const char * name) { return _putenv_s(name, ""); // empty value removes the variable } #endif + +// Writable scratch directory for tests that emit temp fixtures. POSIX +// runners set TMPDIR; Windows sets TEMP/TMP and has no /tmp, so fall +// through the whole chain before defaulting. +inline std::string test_tmpdir() { + for (const char * var : { "TMPDIR", "TEMP", "TMP" }) { + if (const char * v = std::getenv(var); v && *v) return v; + } + return "/tmp"; +} diff --git a/tts-cpp/test/test_gguf_stream.cpp b/tts-cpp/test/test_gguf_stream.cpp index 1dd5f90f6db..113fea1a276 100644 --- a/tts-cpp/test/test_gguf_stream.cpp +++ b/tts-cpp/test/test_gguf_stream.cpp @@ -8,6 +8,7 @@ // chunk boundary) into a temp file, loads it through both paths, and // compares. +#include "test_env_portable.h" #include "ggml.h" #include "ggml-backend.h" #include "ggml-cpu.h" @@ -47,8 +48,7 @@ static const spec SPECS[] = { static const size_t N_SPECS = sizeof(SPECS) / sizeof(SPECS[0]); static std::string write_fixture() { - const char * tmpdir = getenv("TMPDIR"); - std::string path = std::string(tmpdir ? tmpdir : "/tmp") + "/test-gguf-stream-fixture.gguf"; + std::string path = test_tmpdir() + "/test-gguf-stream-fixture.gguf"; size_t total = 0; for (size_t i = 0; i < N_SPECS; ++i) { diff --git a/tts-cpp/test/test_lavasr_enhancer_ggml.cpp b/tts-cpp/test/test_lavasr_enhancer_ggml.cpp index a25cd443441..8eb962a586b 100644 --- a/tts-cpp/test/test_lavasr_enhancer_ggml.cpp +++ b/tts-cpp/test/test_lavasr_enhancer_ggml.cpp @@ -17,6 +17,7 @@ // is present it additionally compares the ggml forward against the onnxruntime // golden real/imag with the real model weights. +#include "test_env_portable.h" #include "backend_selection.h" #include "lavasr/enhancer.h" // scalar enhance() + enhance_with() #include "lavasr/enhancer_core.h" @@ -505,9 +506,7 @@ static std::string write_enhancer_gguf(const EnhancerWeights & w) { add_gguf_tensor(ctx, g, w, e.name, e.ne); } - const char * tmpdir = std::getenv("TMPDIR"); - std::string path = - std::string(tmpdir ? tmpdir : "/tmp") + "/test-lavasr-enhancer-load.gguf"; + std::string path = test_tmpdir() + "/test-lavasr-enhancer-load.gguf"; const bool ok = gguf_write_to_file(g, path.c_str(), /*only_meta=*/false); gguf_free(g); ggml_free(ctx); From 126d8b5438ab65032a48318325134a36e600f464 Mon Sep 17 00:00:00 2001 From: ogad-tether Date: Fri, 17 Jul 2026 16:21:31 +0100 Subject: [PATCH 7/7] =?UTF-8?q?ci+fix:=20address=20#93=20review=20?= =?UTF-8?q?=E2=80=94=20exact-SHA=20ggml=20fetch,=20POSIX-faithful=20setenv?= =?UTF-8?q?=20shim,=20GPU=20flags=20in=20gpu=20stubs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - All ggml provisioning steps now fetch the exact SHA the cache key was derived from (git init + fetch --depth 1 + checkout FETCH_HEAD) instead of cloning the branch tip with a swallowed checkout — the tip advancing between ls-remote and clone could poison the cache with a commit the key never described. Applies to parakeet-ci, tts-ci and all three engines-cross-ci jobs. - test_env_portable.h: the MSVC setenv shim now honors overwrite=0 (existing variable left untouched, return 0) instead of silently diverging from POSIX. - GPU stub jobs: build ggml with -DGGML_VULKAN=ON (the backend the -L gpu suites target; Metal is default-on on Apple hosts) so a dispatch run can't falsely pass against a CPU-only ggml; flag set to be revisited when the runner fleet exists. Co-Authored-By: Claude Fable 5 --- .github/workflows/engines-cross-ci.yml | 21 ++++++++++++++++++--- .github/workflows/parakeet-ci.yml | 16 ++++++++++++++-- .github/workflows/tts-ci.yml | 16 ++++++++++++++-- tts-cpp/test/test_env_portable.h | 7 ++++++- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/.github/workflows/engines-cross-ci.yml b/.github/workflows/engines-cross-ci.yml index 278e0c490c6..1500d130bb5 100644 --- a/.github/workflows/engines-cross-ci.yml +++ b/.github/workflows/engines-cross-ci.yml @@ -71,7 +71,12 @@ jobs: - name: Build ggml (MSVC, static, CPU-only) if: steps.ggml-cache.outputs.cache-hit != 'true' run: | - git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src + # Fetch the exact SHA the cache key was derived from (the branch + # tip may advance between ls-remote and this step). + git init -q ggml-src + git -C ggml-src remote add origin "$GGML_REPO" + git -C ggml-src fetch --depth 1 origin ${{ steps.ggml.outputs.sha }} + git -C ggml-src checkout -q FETCH_HEAD cmake -S ggml-src -B ggml-src/build -A x64 \ -DBUILD_SHARED_LIBS=OFF \ -DGGML_BUILD_TESTS=OFF \ @@ -123,7 +128,12 @@ jobs: - name: Build ggml (NDK arm64-v8a, static, CPU-only) if: steps.ggml-cache.outputs.cache-hit != 'true' run: | - git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src + # Fetch the exact SHA the cache key was derived from (the branch + # tip may advance between ls-remote and this step). + git init -q ggml-src + git -C ggml-src remote add origin "$GGML_REPO" + git -C ggml-src fetch --depth 1 origin ${{ steps.ggml.outputs.sha }} + git -C ggml-src checkout -q FETCH_HEAD cmake -S ggml-src -B ggml-src/build \ -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_LATEST_HOME/build/cmake/android.toolchain.cmake" \ -DANDROID_ABI=arm64-v8a \ @@ -183,7 +193,12 @@ jobs: - name: Build ggml (iOS device arm64, static, Metal embedded) if: steps.ggml-cache.outputs.cache-hit != 'true' run: | - git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src + # Fetch the exact SHA the cache key was derived from (the branch + # tip may advance between ls-remote and this step). + git init -q ggml-src + git -C ggml-src remote add origin "$GGML_REPO" + git -C ggml-src fetch --depth 1 origin ${{ steps.ggml.outputs.sha }} + git -C ggml-src checkout -q FETCH_HEAD cmake -S ggml-src -B ggml-src/build \ -DCMAKE_SYSTEM_NAME=iOS \ -DCMAKE_OSX_SYSROOT=iphoneos \ diff --git a/.github/workflows/parakeet-ci.yml b/.github/workflows/parakeet-ci.yml index b84d15f1446..02ce8ee9d49 100644 --- a/.github/workflows/parakeet-ci.yml +++ b/.github/workflows/parakeet-ci.yml @@ -77,8 +77,14 @@ jobs: - name: Build ggml (system ggml, qvac-ext-ggml@${{ env.GGML_BRANCH }}) if: steps.ggml-cache.outputs.cache-hit != 'true' run: | - git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src - git -C ggml-src checkout ${{ steps.ggml.outputs.sha }} || true + # Fetch the exact SHA the cache key was derived from — the branch + # tip may have advanced since the ls-remote resolution, and a + # tip-only shallow clone would then silently build (and cache) + # a different commit than the key claims. + git init -q ggml-src + git -C ggml-src remote add origin "$GGML_REPO" + git -C ggml-src fetch --depth 1 origin ${{ steps.ggml.outputs.sha }} + git -C ggml-src checkout -q FETCH_HEAD cmake -S ggml-src -B ggml-src/build \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=ON \ @@ -121,10 +127,16 @@ jobs: timeout-minutes: 90 steps: - uses: actions/checkout@v4 + # GGML_VULKAN=ON is the backend the -L gpu suites primarily target + # (test_vk_vs_cpu et al); Metal is on by default on Apple hosts. + # Revisit the flag set (OpenCL? CUDA?) when the GPU runner fleet is + # provisioned — a CPU-only ggml here would skip or falsely pass the + # GPU suites. - name: Build ggml + engine (native backends) and run GPU suites run: | git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src cmake -S ggml-src -B ggml-src/build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \ + -DGGML_VULKAN=ON \ -DCMAKE_INSTALL_PREFIX="$PWD/ggml-install" cmake --build ggml-src/build -j && cmake --install ggml-src/build cmake -S parakeet-cpp -B build -DCMAKE_BUILD_TYPE=Release \ diff --git a/.github/workflows/tts-ci.yml b/.github/workflows/tts-ci.yml index 9b9f98e5cfb..00234903010 100644 --- a/.github/workflows/tts-ci.yml +++ b/.github/workflows/tts-ci.yml @@ -78,8 +78,14 @@ jobs: - name: Build ggml (system ggml, qvac-ext-ggml@${{ env.GGML_BRANCH }}) if: steps.ggml-cache.outputs.cache-hit != 'true' run: | - git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src - git -C ggml-src checkout ${{ steps.ggml.outputs.sha }} || true + # Fetch the exact SHA the cache key was derived from — the branch + # tip may have advanced since the ls-remote resolution, and a + # tip-only shallow clone would then silently build (and cache) + # a different commit than the key claims. + git init -q ggml-src + git -C ggml-src remote add origin "$GGML_REPO" + git -C ggml-src fetch --depth 1 origin ${{ steps.ggml.outputs.sha }} + git -C ggml-src checkout -q FETCH_HEAD cmake -S ggml-src -B ggml-src/build \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=ON \ @@ -121,10 +127,16 @@ jobs: timeout-minutes: 90 steps: - uses: actions/checkout@v4 + # GGML_VULKAN=ON is the backend the -L gpu suites primarily target + # (test_vk_vs_cpu et al); Metal is on by default on Apple hosts. + # Revisit the flag set (OpenCL? CUDA?) when the GPU runner fleet is + # provisioned — a CPU-only ggml here would skip or falsely pass the + # GPU suites. - name: Build ggml + engine (native backends) and run GPU suites run: | git clone --depth 1 --branch "$GGML_BRANCH" "$GGML_REPO" ggml-src cmake -S ggml-src -B ggml-src/build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \ + -DGGML_VULKAN=ON \ -DCMAKE_INSTALL_PREFIX="$PWD/ggml-install" cmake --build ggml-src/build -j && cmake --install ggml-src/build cmake -S tts-cpp -B build -DCMAKE_BUILD_TYPE=Release \ diff --git a/tts-cpp/test/test_env_portable.h b/tts-cpp/test/test_env_portable.h index 3e46cbd2414..1eee6f3dfa6 100644 --- a/tts-cpp/test/test_env_portable.h +++ b/tts-cpp/test/test_env_portable.h @@ -6,7 +6,12 @@ #include #include #ifdef _MSC_VER -inline int setenv(const char * name, const char * value, int /*overwrite*/) { +inline int setenv(const char * name, const char * value, int overwrite) { + // POSIX: with overwrite=0 an existing variable must be left untouched + // (and 0 returned) — don't let the shim diverge from that silently. + if (!overwrite && std::getenv(name) != nullptr) { + return 0; + } return _putenv_s(name, value); } inline int unsetenv(const char * name) {