Skip to content

build(arm64): cross-compile parakeet-cli for the Raspberry Pi 5 #200

build(arm64): cross-compile parakeet-cli for the Raspberry Pi 5

build(arm64): cross-compile parakeet-cli for the Raspberry Pi 5 #200

Workflow file for this run

name: ci
on:
push:
pull_request:
# closed-loop job runs on pull_request (gate) and manual dispatch (see below).
workflow_dispatch:
jobs:
# -------------------------------------------------------------------------
# build: configure, compile, run model-independent unit tests.
# Runs on every push and pull_request. Never downloads a model — fast gate.
# -------------------------------------------------------------------------
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: configure
# GGML_NATIVE=OFF for portability in CI (no host-cpu ISA extensions)
run: cmake -B build -DPARAKEET_BUILD_TESTS=ON -DGGML_NATIVE=OFF
- name: build
run: cmake --build build -j
- name: test (model-independent only)
# -LE model excludes the model-labelled tests (check_convert,
# test_model_loader, check_baseline) that need the Python venv and the
# cached NeMo checkpoint. Those will run in a separate workflow_dispatch
# job once a models bundle is published (Phase 4).
run: ctest --test-dir build --output-on-failure -LE model
# -------------------------------------------------------------------------
# server-e2e: drive the real parakeet-server over HTTP.
#
# Runs on pull_request (merge gate) and manual workflow_dispatch. Not on every
# push: it downloads the ~125 MB tdt_ctc-110m-q4_k model via the alias path.
# Much lighter than closed-loop (no NeMo/Python venv) — it builds the server,
# then tests/server_e2e.sh starts it, transcribes tests/fixtures/speech.wav in
# json/text/verbose_json (with word timestamps), and checks the 400 paths.
# -------------------------------------------------------------------------
server-e2e:
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake build-essential curl ca-certificates
- name: Configure
run: |
cmake -B build \
-DPARAKEET_BUILD_SERVER=ON \
-DPARAKEET_BUILD_TESTS=ON \
-DGGML_NATIVE=OFF
- name: Build parakeet-server
run: cmake --build build --target parakeet-server -j
- name: Run server e2e
# PARAKEET_SERVER_E2E=1 flips the test from skip (77) to a real run.
env:
PARAKEET_SERVER_E2E: "1"
# First-run model download speed varies on hosted runners; the job
# timeout remains the outer cap.
PARAKEET_SERVER_E2E_READY_TIMEOUT: "600"
run: ctest --test-dir build --output-on-failure -R '^server_e2e$'
# -------------------------------------------------------------------------
# closed-loop: full end-to-end transcript assertion.
#
# Runs on pull_request (merge gate) and manual workflow_dispatch. Not on every
# push, since this job downloads nvidia/parakeet-tdt_ctc-110m (~440 MB via
# NeMo/HuggingFace Hub) and installs the nemo_toolkit Python env (~60 min).
#
# What it does:
# 1. Check out the repo (submodules recursive).
# 2. Install system build deps + Python 3.12 + pip.
# 3. Create a venv and install CPU torch + scripts/requirements.txt
# (nemo_toolkit[asr] + gguf) — same setup as the developer guide.
# 4. Build the project (GGML_NATIVE=OFF for runner portability,
# PARAKEET_BUILD_TESTS=ON, PARAKEET_BUILD_CLI=ON).
# 5. Convert nvidia/parakeet-tdt_ctc-110m → /tmp/pk110m_cl.gguf
# (F32, default dtype; NeMo auto-downloads the checkpoint on first use).
# 6. Run `parakeet-cli transcribe` on tests/fixtures/speech.wav with
# --decoder tdt (the TDT head of the hybrid model).
# 7. Assert the output matches the committed NeMo reference transcript:
# "Well, I don't wish to see it any more, observed Phoebe,
# turning away her eyes. It is certainly very like the old portrait."
# Fail the job if it doesn't match.
# -------------------------------------------------------------------------
closed-loop:
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout (with submodules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install system build dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake build-essential python3 python3-venv python3-pip
- name: Set up Python venv
# CPU-only torch wheel; nemo_toolkit[asr] + gguf from requirements.txt.
# Mirror the developer setup documented in AGENTS.md / README.md.
run: |
python3 -m venv .venv
.venv/bin/pip install --quiet --upgrade pip
.venv/bin/pip install --quiet torch --index-url https://download.pytorch.org/whl/cpu
.venv/bin/pip install --quiet -r scripts/requirements.txt
- name: Configure CMake
run: |
cmake -B build \
-DPARAKEET_BUILD_TESTS=ON \
-DPARAKEET_BUILD_CLI=ON \
-DGGML_NATIVE=OFF
- name: Build
run: cmake --build build -j
- name: Convert nvidia/parakeet-tdt_ctc-110m to GGUF
# NeMo downloads the ~440 MB checkpoint from HuggingFace on first use.
# The resulting GGUF is ~438 MB (F32).
run: |
.venv/bin/python scripts/convert_parakeet_to_gguf.py \
--model nvidia/parakeet-tdt_ctc-110m \
--output /tmp/pk110m_cl.gguf
- name: Transcribe speech fixture with TDT decoder
run: |
./build/examples/cli/parakeet-cli transcribe \
--model /tmp/pk110m_cl.gguf \
--input tests/fixtures/speech.wav \
--decoder tdt \
> /tmp/cl_transcript.txt
echo "--- transcript ---"
cat /tmp/cl_transcript.txt
- name: Assert transcript matches NeMo reference
# Reference transcript (TDT head, hybrid 110m) committed to docs/parity.md
# and validated at WER 0.0 vs NeMo 2.7.3 (see docs/parity.md § Phase 3).
run: |
EXPECTED="Well, I don't wish to see it any more, observed Phoebe, turning away her eyes. It is certainly very like the old portrait."
ACTUAL="$(cat /tmp/cl_transcript.txt)"
python3 - <<'PYEOF'
import sys
expected = "Well, I don't wish to see it any more, observed Phoebe, turning away her eyes. It is certainly very like the old portrait."
with open("/tmp/cl_transcript.txt") as f:
actual = f.read().strip()
if actual == expected:
print("PASS: transcript matches NeMo reference (WER 0.0)")
sys.exit(0)
else:
print("FAIL: transcript mismatch")
print(f" expected: {expected!r}")
print(f" actual: {actual!r}")
sys.exit(1)
PYEOF