diff --git a/CMakeLists.txt b/CMakeLists.txt
index d366e8ea..2191715f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -111,6 +111,16 @@ endif()
option(TRANSCRIBE_BUILD_TESTS "Build unit / smoke tests" ON)
option(TRANSCRIBE_BUILD_EXAMPLES "Build example CLI" ON)
option(TRANSCRIBE_BUILD_TOOLS "Build inspect / quantize" OFF) # post pass 2
+# Numerical-parity validation hooks: reference-mel injection (TRANSCRIBE_MEL_FROM_REF)
+# and per-layer tensor dumps (TRANSCRIBE_DUMP_ALL_BLOCKS / _SUB_BLOCKS). OFF for
+# release so the hook code is not compiled into shipped binaries — the env-var
+# reads, ref-mel loaders, and dump scaffolding all live behind this guard, so
+# their names do not even appear in a release binary's strings. The validation
+# pipeline (scripts/validate.py, the Modal sweep image) builds with this ON;
+# validate.py hard-fails --mel-from-ref against an OFF build rather than
+# silently ignoring it.
+option(TRANSCRIBE_ENABLE_VALIDATION_HOOKS
+ "Compile in numerical-parity validation hooks" OFF)
option(TRANSCRIBE_METAL "Enable Metal backend" ${TRANSCRIBE_IS_APPLE_SILICON})
option(TRANSCRIBE_VULKAN "Enable Vulkan backend" OFF) # post-v1
option(TRANSCRIBE_CUDA "Enable CUDA backend" OFF) # opt-in; Linux + nvcc
@@ -182,7 +192,7 @@ option(TRANSCRIBE_INSTALL
# Real-model gated tests. OFF by default because the tests need a
# converted Parakeet GGUF on disk (~2.4 GB) and CI can't ship one.
-# When ON, the test reads TRANSCRIBE_REAL_PARAKEET_GGUF from the
+# When ON, the test reads TRANSCRIBE_PARAKEET_GGUF from the
# environment at run time. See tests/parakeet_real_smoke.cpp.
option(TRANSCRIBE_BUILD_REAL_MODEL_TESTS
"Build tests that load real (multi-GB) model files via env var" OFF)
diff --git a/CMakePresets.json b/CMakePresets.json
index 2229bfb5..334f11ce 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -56,6 +56,15 @@
"inherits": "wheel-linux-cpu-vulkan",
"displayName": "Windows x86_64 default wheel: CPU + Vulkan backend modules",
"description": "Same provider shape as wheel-linux-cpu-vulkan on Windows: conservative CPU module + Vulkan module, loaded package-locally (the Python loader calls os.add_dll_directory before CDLL). Build image needs the Vulkan SDK (glslc)."
+ },
+ {
+ "name": "validation",
+ "displayName": "Local numerical-parity validation build (build/)",
+ "description": "Dev build for scripts/validate.py: enables TRANSCRIBE_ENABLE_VALIDATION_HOOKS so the reference-mel injection (TRANSCRIBE_MEL_FROM_REF) and per-layer tensor dumps (TRANSCRIBE_DUMP_ALL_BLOCKS / TRANSCRIBE_DUMP_SUB_BLOCKS) are compiled in. Targets build/ (the dir validate.py runs build/bin/transcribe-cli from) and reuses the existing generator. NOT for release — shipped and wheel builds leave the hooks out (TRANSCRIBE_ENABLE_VALIDATION_HOOKS defaults OFF).",
+ "binaryDir": "${sourceDir}/build",
+ "cacheVariables": {
+ "TRANSCRIBE_ENABLE_VALIDATION_HOOKS": "ON"
+ }
}
]
}
diff --git a/README.md b/README.md
index 50de6fb1..f922c55e 100644
--- a/README.md
+++ b/README.md
@@ -119,7 +119,7 @@ Some tests require a real model file. Enable them with:
```bash
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_PARAKEET_GGUF=path/to/model.gguf ctest --test-dir build
+TRANSCRIBE_PARAKEET_GGUF=path/to/model.gguf ctest --test-dir build
```
For the model-family smoke-test, numerical-validation, and benchmark
diff --git a/docs/environment-variables.md b/docs/environment-variables.md
new file mode 100644
index 00000000..c1f21d78
--- /dev/null
+++ b/docs/environment-variables.md
@@ -0,0 +1,91 @@
+# Environment Variables
+
+A single reference for the environment variables transcribe.cpp recognizes,
+split by audience. The library follows one parse convention for boolean
+toggles: a flag is **on** when it is set, non-empty, and its first character is
+not `0` (so `FOO=1`, `FOO=on`, `FOO=yes` are on; `FOO=0`, `FOO=` and unset are
+off). Path-valued vars are off when unset or empty.
+
+## Tier 1 — runtime configuration
+
+Compiled into every build, including release. These are operational knobs, not
+build-gated. Note that `TRANSCRIBE_DUMP_DIR` — while it ships — is still a
+tensor-dumping debug/validation hook: enabling it does per-tensor device→host
+copies and writes raw activations to disk, so it carries I/O cost and a
+data-leak surface. Leave it unset in production unless you are deliberately
+collecting dumps.
+
+| Variable | Effect |
+| --- | --- |
+| `TRANSCRIBE_NO_FLASH` | Disable flash attention on encoder and decoder (forces the manual F32 path). |
+| `TRANSCRIBE_FORCE_FLASH` | Force flash attention on. Wins over `TRANSCRIBE_NO_FLASH` if both are set. |
+| `TRANSCRIBE_CONV_DIRECT_DW` / `TRANSCRIBE_CONV_NO_DIRECT_DW` | Force the depthwise-conv dispatch to the direct `conv_2d_dw` path / the im2col path, overriding the per-family backend default. |
+| `TRANSCRIBE_CONV_DIRECT_PW` / `TRANSCRIBE_CONV_NO_DIRECT_PW` | Force the pointwise-conv dispatch to direct `mul_mat` / im2col, overriding the backend default. |
+| `TRANSCRIBE_DUMP_DIR=
` | Enable the per-stage tensor dumper; writes `.f32` + `.json` per dumped tensor into ``. The basis for the numerical-comparison harness (`scripts/compare_tensors.py`). |
+| `TRANSCRIBE_PERF_DEBUG` | Print a per-stage timing breakdown to stderr (DEBUG log) on the families that profile (`cohere`, `granite`, `canary`, `canary_qwen`, `moonshine`, `moonshine_streaming`, `qwen3_asr`, `whisper`). For whisper, a value containing `cpu` or `all` additionally prints the CPU sub-section breakdown. |
+| `TRANSCRIBE_VOXTRAL_REALTIME_STREAM_TIMING` | Print a per-component streaming wall-time breakdown at stream finalize (voxtral_realtime). |
+
+## Tier 2 — validation hooks
+
+Numerical-parity hooks for porting and validation. **Compiled in only when the
+library is built with `-DTRANSCRIBE_ENABLE_VALIDATION_HOOKS=ON`** (see the CMake
+option of the same name, or the `validation` preset). Release/wheel builds leave
+them out entirely — the env-var reads, ref-mel loaders, and dump scaffolding are
+all behind the compile guard, so these names do not even appear in a release
+binary. In a build *without* the flag, setting any of these has no effect;
+`scripts/validate.py` hard-fails `--mel-from-ref` against such a build rather
+than silently falling back.
+
+| Variable | Effect |
+| --- | --- |
+| `TRANSCRIBE_MEL_FROM_REF=` | Inject a reference log-mel from `` instead of computing it, so encoder drift can be isolated from frontend drift. Each family reads its own dump filename/layout from the directory (`enc.mel.in.f32` for whisper / voxtral_realtime, `frontend.mel.out.f32` for gigaam, `mel.in.f32` for medasr). Driven by `scripts/validate.py --mel-from-ref`. |
+| `TRANSCRIBE_DUMP_ALL_BLOCKS` | Dump every encoder block output (not just mid/last) for a layer-by-layer divergence bisect (parakeet, gigaam). Requires `TRANSCRIBE_DUMP_DIR`. |
+| `TRANSCRIBE_DUMP_SUB_BLOCKS=` | Dump intermediate sub-layer activations (ff1/attn/conv/ff2) for the listed block indices, e.g. `0,12,23` (parakeet). Requires `TRANSCRIBE_DUMP_DIR`. |
+
+Build a validation-capable `build/` with:
+
+```bash
+cmake --preset validation # configures build/ with the hooks ON
+cmake --build build --target transcribe-cli
+```
+
+When **toggling** `TRANSCRIBE_ENABLE_VALIDATION_HOOKS` on an existing `build/`,
+do a clean build of the target — an incremental build can leave a stale object
+that did not pick up the changed compile definition:
+
+```bash
+cmake --build build --clean-first --target transcribe-cli
+```
+
+(`scripts/validate.py` guards against the OFF case: it hard-fails
+`--mel-from-ref` when `build/CMakeCache.txt` has the flag off. Longer term, a
+dedicated validation build directory would avoid the toggle entirely.)
+
+## Test & tooling
+
+Not part of the library runtime.
+
+**Real-model test model paths.** Built only with
+`-DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON`; each test skips (exit code 77) when
+its var is unset. Convention: `TRANSCRIBE__GGUF`.
+
+| Variable | Test(s) |
+| --- | --- |
+| `TRANSCRIBE_PARAKEET_GGUF` | `parakeet_real_smoke`, `decoder_smoke` |
+| `TRANSCRIBE_COHERE_GGUF` | `cohere_real_smoke`, `cohere_e2e_smoke` |
+| `TRANSCRIBE_WHISPER_GGUF` | `whisper_e2e_smoke`, `whisper_tokenize_parity` |
+| `TRANSCRIBE_QWEN3_ASR_GGUF` (+ `_0_6B_GGUF` / `_1_7B_GGUF`) | qwen3_asr smokes / parity |
+| `TRANSCRIBE_MOONSHINE_STREAMING_TINY_GGUF` | moonshine_streaming smokes |
+| `TRANSCRIBE_VOXTRAL_REALTIME_GGUF` | `voxtral_realtime_real_smoke` |
+| `TRANSCRIBE_WHISPER_BIN_*` | whisper.cpp `.bin` parser/e2e fixtures |
+
+Other test/tooling vars:
+
+- `TRANSCRIBE_TEST_AUDIO` — override the `jfk.wav` path for ad-hoc e2e runs.
+- `TRANSCRIBE_TEST_SAMPLES_DIR` / `TRANSCRIBE_TEST_FIXTURES_DIR` — sample and
+ fixture directories, passed at configure time as compile definitions (not
+ runtime env vars).
+- Python tooling: `TRANSCRIBE_MODELS_DIR`, `TRANSCRIBE_DUMP_DIR` (shared with
+ the library), `HF_TOKEN`, and the binding-level `TRANSCRIBE_BACKEND` /
+ `TRANSCRIBE_LIBRARY`. See [`tools/conversion.md`](tools/conversion.md) and
+ [`tools/validate.md`](tools/validate.md).
diff --git a/docs/model-family-testing.md b/docs/model-family-testing.md
index 2f355188..4c5ebf03 100644
--- a/docs/model-family-testing.md
+++ b/docs/model-family-testing.md
@@ -53,7 +53,9 @@ Expected shape:
- Built only when `TRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON`.
- Skips with return code `77` when the model path is not available.
-- Uses one family-specific environment variable for the model path.
+- Uses one family-specific environment variable for the model path,
+ following the `TRANSCRIBE__GGUF` convention (see
+ [`environment-variables.md`](environment-variables.md)).
- Verifies architecture string, variant string if applicable,
capabilities, hparams, language list, expected tensor count or
tensor-table coverage, and canonical weight shapes.
diff --git a/docs/models/cohere-transcribe-03-2026.md b/docs/models/cohere-transcribe-03-2026.md
index dee43432..f605605b 100644
--- a/docs/models/cohere-transcribe-03-2026.md
+++ b/docs/models/cohere-transcribe-03-2026.md
@@ -179,6 +179,6 @@ uv run scripts/validate.py all --family cohere
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_COHERE_MODEL=models/cohere-transcribe-03-2026/cohere-transcribe-03-2026-BF16.gguf \
+TRANSCRIBE_COHERE_GGUF=models/cohere-transcribe-03-2026/cohere-transcribe-03-2026-BF16.gguf \
ctest --test-dir build --output-on-failure -R 'cohere'
```
diff --git a/docs/models/parakeet-tdt-0.6b-v2.md b/docs/models/parakeet-tdt-0.6b-v2.md
index 0cecb2b0..87ada6d3 100644
--- a/docs/models/parakeet-tdt-0.6b-v2.md
+++ b/docs/models/parakeet-tdt-0.6b-v2.md
@@ -164,6 +164,6 @@ uv run scripts/validate.py all --family parakeet
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_PARAKEET_GGUF=models/parakeet-tdt-0.6b-v2/parakeet-tdt-0.6b-v2-F32.gguf \
+TRANSCRIBE_PARAKEET_GGUF=models/parakeet-tdt-0.6b-v2/parakeet-tdt-0.6b-v2-F32.gguf \
ctest --test-dir build --output-on-failure -R 'parakeet|encoder|decoder'
```
diff --git a/docs/models/parakeet-tdt-0.6b-v3.md b/docs/models/parakeet-tdt-0.6b-v3.md
index baa3cfce..567fd42a 100644
--- a/docs/models/parakeet-tdt-0.6b-v3.md
+++ b/docs/models/parakeet-tdt-0.6b-v3.md
@@ -166,6 +166,6 @@ uv run scripts/validate.py all --family parakeet --variant parakeet-tdt-0.6b-v3
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_PARAKEET_GGUF=models/parakeet-tdt-0.6b-v3/parakeet-tdt-0.6b-v3-F32.gguf \
+TRANSCRIBE_PARAKEET_GGUF=models/parakeet-tdt-0.6b-v3/parakeet-tdt-0.6b-v3-F32.gguf \
ctest --test-dir build --output-on-failure -R 'parakeet|encoder|decoder'
```
diff --git a/docs/models/whisper-base.en.md b/docs/models/whisper-base.en.md
index 925cf4f6..af23eece 100644
--- a/docs/models/whisper-base.en.md
+++ b/docs/models/whisper-base.en.md
@@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-base.en
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-base.en/whisper-base.en-Q8_0.gguf \
+TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-base.en/whisper-base.en-Q8_0.gguf \
ctest --test-dir build --output-on-failure -R whisper
```
diff --git a/docs/models/whisper-base.md b/docs/models/whisper-base.md
index c1549545..a01e31ec 100644
--- a/docs/models/whisper-base.md
+++ b/docs/models/whisper-base.md
@@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-base
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-base/whisper-base-Q8_0.gguf \
+TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-base/whisper-base-Q8_0.gguf \
ctest --test-dir build --output-on-failure -R whisper
```
diff --git a/docs/models/whisper-large-v2.md b/docs/models/whisper-large-v2.md
index 2dd25a3e..974d4027 100644
--- a/docs/models/whisper-large-v2.md
+++ b/docs/models/whisper-large-v2.md
@@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-large-v2
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-large-v2/whisper-large-v2-Q8_0.gguf \
+TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-large-v2/whisper-large-v2-Q8_0.gguf \
ctest --test-dir build --output-on-failure -R whisper
```
diff --git a/docs/models/whisper-large-v3-turbo.md b/docs/models/whisper-large-v3-turbo.md
index b6716562..797a9d02 100644
--- a/docs/models/whisper-large-v3-turbo.md
+++ b/docs/models/whisper-large-v3-turbo.md
@@ -174,6 +174,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-large-v3-turbo
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-large-v3-turbo/whisper-large-v3-turbo-Q8_0.gguf \
+TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-large-v3-turbo/whisper-large-v3-turbo-Q8_0.gguf \
ctest --test-dir build --output-on-failure -R whisper
```
diff --git a/docs/models/whisper-large-v3.md b/docs/models/whisper-large-v3.md
index d12d7f75..5fc3fd02 100644
--- a/docs/models/whisper-large-v3.md
+++ b/docs/models/whisper-large-v3.md
@@ -174,6 +174,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-large-v3
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-large-v3/whisper-large-v3-Q8_0.gguf \
+TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-large-v3/whisper-large-v3-Q8_0.gguf \
ctest --test-dir build --output-on-failure -R whisper
```
diff --git a/docs/models/whisper-large.md b/docs/models/whisper-large.md
index f4aa17c7..8306a0a3 100644
--- a/docs/models/whisper-large.md
+++ b/docs/models/whisper-large.md
@@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-large
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-large/whisper-large-Q8_0.gguf \
+TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-large/whisper-large-Q8_0.gguf \
ctest --test-dir build --output-on-failure -R whisper
```
diff --git a/docs/models/whisper-medium.en.md b/docs/models/whisper-medium.en.md
index 54293730..3842e1d1 100644
--- a/docs/models/whisper-medium.en.md
+++ b/docs/models/whisper-medium.en.md
@@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-medium.en
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-medium.en/whisper-medium.en-Q8_0.gguf \
+TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-medium.en/whisper-medium.en-Q8_0.gguf \
ctest --test-dir build --output-on-failure -R whisper
```
diff --git a/docs/models/whisper-medium.md b/docs/models/whisper-medium.md
index 38dad1c7..62b2c725 100644
--- a/docs/models/whisper-medium.md
+++ b/docs/models/whisper-medium.md
@@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-medium
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-medium/whisper-medium-Q8_0.gguf \
+TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-medium/whisper-medium-Q8_0.gguf \
ctest --test-dir build --output-on-failure -R whisper
```
diff --git a/docs/models/whisper-small.en.md b/docs/models/whisper-small.en.md
index b2918787..8297555c 100644
--- a/docs/models/whisper-small.en.md
+++ b/docs/models/whisper-small.en.md
@@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-small.en
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-small.en/whisper-small.en-Q8_0.gguf \
+TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-small.en/whisper-small.en-Q8_0.gguf \
ctest --test-dir build --output-on-failure -R whisper
```
diff --git a/docs/models/whisper-small.md b/docs/models/whisper-small.md
index f39acea8..ac8c44e0 100644
--- a/docs/models/whisper-small.md
+++ b/docs/models/whisper-small.md
@@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-small
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-small/whisper-small-Q8_0.gguf \
+TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-small/whisper-small-Q8_0.gguf \
ctest --test-dir build --output-on-failure -R whisper
```
diff --git a/docs/models/whisper-tiny.en.md b/docs/models/whisper-tiny.en.md
index f2981f67..f1bb0b10 100644
--- a/docs/models/whisper-tiny.en.md
+++ b/docs/models/whisper-tiny.en.md
@@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-tiny.en
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-tiny.en/whisper-tiny.en-Q8_0.gguf \
+TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-tiny.en/whisper-tiny.en-Q8_0.gguf \
ctest --test-dir build --output-on-failure -R whisper
```
diff --git a/docs/models/whisper-tiny.md b/docs/models/whisper-tiny.md
index 9649ab75..9510ddfb 100644
--- a/docs/models/whisper-tiny.md
+++ b/docs/models/whisper-tiny.md
@@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-tiny
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-tiny/whisper-tiny-Q8_0.gguf \
+TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-tiny/whisper-tiny-Q8_0.gguf \
ctest --test-dir build --output-on-failure -R whisper
```
diff --git a/docs/porting/families/cohere.md b/docs/porting/families/cohere.md
index cb2b1402..d078566a 100644
--- a/docs/porting/families/cohere.md
+++ b/docs/porting/families/cohere.md
@@ -64,7 +64,7 @@ Real-model smokes:
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_COHERE_MODEL=models/cohere-transcribe-03-2026/cohere-transcribe-03-2026-BF16.gguf \
+TRANSCRIBE_COHERE_GGUF=models/cohere-transcribe-03-2026/cohere-transcribe-03-2026-BF16.gguf \
ctest --test-dir build --output-on-failure -R cohere
```
diff --git a/docs/porting/families/parakeet.md b/docs/porting/families/parakeet.md
index 72f67ebb..1a70c958 100644
--- a/docs/porting/families/parakeet.md
+++ b/docs/porting/families/parakeet.md
@@ -96,7 +96,7 @@ Real-model smokes:
cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON
cmake --build build
-TRANSCRIBE_REAL_PARAKEET_GGUF=models/parakeet-tdt-0.6b-v2/parakeet-tdt-0.6b-v2-F32.gguf \
+TRANSCRIBE_PARAKEET_GGUF=models/parakeet-tdt-0.6b-v2/parakeet-tdt-0.6b-v2-F32.gguf \
ctest --test-dir build --output-on-failure -R 'parakeet|encoder|decoder'
```
diff --git a/docs/porting/families/whisper.md b/docs/porting/families/whisper.md
index 4bd41c8f..e7d2e9aa 100644
--- a/docs/porting/families/whisper.md
+++ b/docs/porting/families/whisper.md
@@ -209,7 +209,7 @@ See `reports/porting/whisper/whisper-tiny/intake.json::known_risks`. Highlights:
These items are real C++ work for Stage 4 / 5 but do NOT flow through intake → convert → validate (the GGUF remains the canonical numerical reference):
-- **C++ mel frontend (deferred).** Stage 4 bringup runs the encoder against the reference mel injected via `TRANSCRIBE_WHISPER_MEL_FROM_REF=[`. `validate.py cpp` sets this env var automatically for the whisper family. The C++ mel frontend (slaney filterbank + Hann periodic window + whisper_logmel dynamic-range compression) is a follow-up. Tolerance file currently records `enc.mel.in` as zero-drift because we read the reference dump; the entry will need widening when the C++ frontend lands.
+- **C++ mel frontend (deferred).** Stage 4 bringup runs the encoder against the reference mel injected via `TRANSCRIBE_MEL_FROM_REF=][`. `validate.py cpp` sets this env var automatically for the whisper family. The C++ mel frontend (slaney filterbank + Hann periodic window + whisper_logmel dynamic-range compression) is a follow-up. Tolerance file currently records `enc.mel.in` as zero-drift because we read the reference dump; the entry will need widening when the C++ frontend lands.
- **whisper.cpp `.bin` loader compatibility (deferred).** The plan calls for accepting upstream whisper.cpp `.bin` files alongside our GGUF. Not implemented in Stage 4; tracked for a later stage.
- **KV-cached decoder (shipped).** The decoder runs through a KV-cached path: cross K/V are precomputed once from the encoder output into the cross cache; self K/V are written per-step into the self cache. The same graph builder handles both the prompt pass (`n_tokens > 1, n_past = 0`) and per-step generation (`n_tokens = 1, n_past = current`). Self- and cross-caches are F16 by default (flip with `--kv-type f32` for tighter parity). F16 cache introduces ~8× wider per-block `max_abs` drift than F32, concentrated in a few outlier elements; transcripts and 300-sample LibriSpeech WER are unchanged within CI.
- **Beam search / temperature fallback (deferred).** Greedy argmax with `suppress_tokens` + first-step `begin_suppress_tokens` is enough to match the reference dumps' transcripts byte-for-byte under `do_sample=False, num_beams=1`. Beam search and temperature fallback (whisper.cpp's `temperature_inc` strategy) are follow-ups for the production decode path.
diff --git a/docs/tools/README.md b/docs/tools/README.md
index b1eb4cb3..5e3b8e47 100644
--- a/docs/tools/README.md
+++ b/docs/tools/README.md
@@ -62,6 +62,10 @@ This split mirrors `llama.cpp`'s `convert_hf_to_gguf.py` →
## Other
+- [**environment-variables.md**](../environment-variables.md) — the single
+ reference for every env var the library, tests, and tooling recognize, split
+ into Tier 1 runtime config, Tier 2 validation hooks (build-gated), and
+ test/tooling.
- `scripts/envs//pyproject.toml` — per-family `uv` env. Each
converter and reference dumper has its own env because NeMo and
Transformers have conflicting dependency graphs.
diff --git a/reports/porting/medasr/forward-map.md b/reports/porting/medasr/forward-map.md
index f096e405..c9ff5cc0 100644
--- a/reports/porting/medasr/forward-map.md
+++ b/reports/porting/medasr/forward-map.md
@@ -18,7 +18,7 @@ relative-position. The block is therefore hand-built in
| Stage | Reference location | Output shape | Gate tensor | ggml / C++ pattern | In-tree analog |
|-------|--------------------|--------------|-------------|--------------------|----------------|
-| log-mel | `feature_extraction_lasr.LasrFeatureExtractor.__call__` | `[T_mel=floor((n_pcm-400)/160)+1, 128]` f32 | `mel.in` | host-side `transcribe::MelFrontend`; LASR knobs require `center=false`, `pad_mode="zero"`, `pre_emphasis=0.0`, `normalize="none"`, `window=ckpt`, `filterbank=ckpt`, `log_compression="log_clamp_1e-5"`. Step 2 brings up the encoder with `TRANSCRIBE_MEDASR_MEL_FROM_REF=` so Step 7 (frontend parity) is the first place the C++ mel actually runs. | `src/transcribe-mel.{h,cpp}` (extend with `center=false` knob and `log_compression` enum) |
+| log-mel | `feature_extraction_lasr.LasrFeatureExtractor.__call__` | `[T_mel=floor((n_pcm-400)/160)+1, 128]` f32 | `mel.in` | host-side `transcribe::MelFrontend`; LASR knobs require `center=false`, `pad_mode="zero"`, `pre_emphasis=0.0`, `normalize="none"`, `window=ckpt`, `filterbank=ckpt`, `log_compression="log_clamp_1e-5"`. Step 2 brings up the encoder with `TRANSCRIBE_MEL_FROM_REF=` so Step 7 (frontend parity) is the first place the C++ mel actually runs. | `src/transcribe-mel.{h,cpp}` (extend with `center=false` knob and `log_compression` enum) |
For jfk.wav (176 400 samples): T_mel = (176 400 − 400)/160 + 1 = 1 099 frames — reference dump rounds to 1 098 (one trailing frame dropped because LASR's manual unfold does `len // win` floor on the residual; not centered). The forward-map records the formula; bit-exact frame count is a Step 7 parity check.
diff --git a/scripts/validate.py b/scripts/validate.py
index c237aec3..32baa139 100644
--- a/scripts/validate.py
+++ b/scripts/validate.py
@@ -252,10 +252,29 @@ def find_cli(repo: Path) -> Path:
return candidate
raise SystemExit(
"error: transcribe-cli not found in build/bin/\n"
- " Run: cmake --build build --target transcribe-cli"
+ " Run: cmake --build build --target transcribe-cli\n"
+ " For --mel-from-ref and per-layer dumps, configure with\n"
+ " -DTRANSCRIBE_ENABLE_VALIDATION_HOOKS=ON (or: cmake --preset validation)."
)
+def validation_hooks_enabled(repo: Path) -> bool:
+ """True if build/ was configured with the validation hooks compiled in.
+
+ Reads build/CMakeCache.txt next to the CLI that find_cli() runs. Used to
+ hard-fail --mel-from-ref against a build that would otherwise silently
+ ignore it (the hook code is compiled out unless the flag is ON).
+ """
+ cache = repo / "build" / "CMakeCache.txt"
+ try:
+ for line in cache.read_text().splitlines():
+ if line.startswith("TRANSCRIBE_ENABLE_VALIDATION_HOOKS:"):
+ return line.rstrip().endswith("=ON")
+ except OSError:
+ pass
+ return False
+
+
def run_cmd(cmd: list[str], repo: Path, label: str) -> None:
print(f"\n{'=' * 60}", file=sys.stderr)
print(f" {label}", file=sys.stderr)
@@ -420,8 +439,16 @@ def cmd_cpp(args: argparse.Namespace) -> int:
# to ref-mel hid a base.en regression once (the mel-precision
# change in 4613129); we don't want that blind spot back.
if args.family == "whisper" and getattr(args, "mel_from_ref", False):
+ if not validation_hooks_enabled(repo):
+ raise SystemExit(
+ "error: --mel-from-ref requires validation hooks compiled "
+ "in, but build/ has TRANSCRIBE_ENABLE_VALIDATION_HOOKS=OFF "
+ "(the hook is compiled out and would be silently ignored).\n"
+ " Reconfigure: cmake --preset validation && "
+ "cmake --build build --target transcribe-cli"
+ )
ref_dir = repo / "build" / "validate" / args.family / variant / case_name / "ref"
- env["TRANSCRIBE_WHISPER_MEL_FROM_REF"] = str(ref_dir)
+ env["TRANSCRIBE_MEL_FROM_REF"] = str(ref_dir)
cmd = [
str(cli),
@@ -650,7 +677,7 @@ def cmd_mel(args: argparse.Namespace) -> int:
env = os.environ.copy()
env["TRANSCRIBE_DUMP_DIR"] = str(out_dir)
- env.pop("TRANSCRIBE_WHISPER_MEL_FROM_REF", None)
+ env.pop("TRANSCRIBE_MEL_FROM_REF", None)
cmd = [
str(cli),
@@ -872,10 +899,11 @@ def add_common(sp: argparse.ArgumentParser) -> None:
)
sp_cpp.add_argument(
"--mel-from-ref", action="store_true",
- help="Whisper-only debug knob: inject the reference mel via "
- "TRANSCRIBE_WHISPER_MEL_FROM_REF so enc.mel.in is bit-"
+ help="Whisper-only validation hook: inject the reference mel via "
+ "TRANSCRIBE_MEL_FROM_REF so enc.mel.in is bit-"
"identical to HF's WhisperFeatureExtractor. Use to isolate "
"graph drift from frontend drift when a regression fires. "
+ "Requires a build with -DTRANSCRIBE_ENABLE_VALIDATION_HOOKS=ON. "
"Default is the production C++ MelFrontend.",
)
sp_cpp.set_defaults(func=cmd_cpp)
@@ -912,9 +940,10 @@ def add_common(sp: argparse.ArgumentParser) -> None:
)
sp_all.add_argument(
"--mel-from-ref", action="store_true",
- help="Whisper-only debug knob: inject the reference mel via "
- "TRANSCRIBE_WHISPER_MEL_FROM_REF for the cpp dump. Default "
- "is the production C++ MelFrontend.",
+ help="Whisper-only validation hook: inject the reference mel via "
+ "TRANSCRIBE_MEL_FROM_REF for the cpp dump. Requires a build with "
+ "-DTRANSCRIBE_ENABLE_VALIDATION_HOOKS=ON. Default is the "
+ "production C++ MelFrontend.",
)
sp_all.set_defaults(func=cmd_all)
diff --git a/scripts/wer/remote/modal_sweep.py b/scripts/wer/remote/modal_sweep.py
index ec85ffac..868d549c 100644
--- a/scripts/wer/remote/modal_sweep.py
+++ b/scripts/wer/remote/modal_sweep.py
@@ -239,6 +239,9 @@ def build(arch: str, build_dir: str, clean: bool = False) -> None:
"-DCMAKE_BUILD_TYPE=Release",
"-DTRANSCRIBE_CUDA=ON",
"-DTRANSCRIBE_BUILD_TESTS=OFF",
+ # The sweep's validation/debug paths rely on the numerical-parity hooks
+ # (reference-mel injection + per-layer dumps), so compile them in here.
+ "-DTRANSCRIBE_ENABLE_VALIDATION_HOOKS=ON",
f"-DCMAKE_CUDA_ARCHITECTURES={arch}",
"-DCMAKE_C_COMPILER_LAUNCHER=ccache",
"-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",
@@ -259,94 +262,6 @@ def build(arch: str, build_dir: str, clean: bool = False) -> None:
# 2. Dataset prefetch (idempotent; cached on /data volume).
# ---------------------------------------------------------------------------
-@app.function(
- image=image, timeout=1800, gpu="L4",
- secrets=[modal.Secret.from_name("huggingface-secret")],
- volumes={
- "/build": build_vol,
- "/root/.cache/huggingface": hf_vol,
- "/data": data_vol,
- },
-)
-def debug_dump(model_repo: str, model_file: str) -> dict:
- """Run transcribe-cli once on jfk.wav with TRANSCRIBE_MEDASR_DEBUG_STATS
- set, capturing stderr (where the encoder writes per-block tensor stats).
- Returns the model_repo, model_file, and full stderr string so the local
- caller can grep for the first divergence point between F16 and Q4_K_M.
- """
- from huggingface_hub import hf_hub_download
- prepare_work()
- build_dir = _build_dir("L4")
- build.remote(arch=GPU_TO_ARCH["L4"], build_dir=build_dir, clean=False)
- # build runs in a different container that commits build_vol; our view
- # of /build is stale until we reload the volume.
- build_vol.reload()
- cli_path = f"{build_dir}/bin/transcribe-cli"
- assert os.path.exists(cli_path), f"missing CLI: {cli_path}"
- model_path = hf_hub_download(
- repo_id=model_repo,
- filename=model_file,
- token=os.environ["HF_TOKEN"],
- )
- audio = "/work/samples/jfk.wav"
- if not os.path.exists(audio):
- # samples/** is excluded from the source mount (it holds the multi-GB
- # LibriSpeech extract), so synthesize a short 16 kHz tone — enough to
- # exercise model load + first compute, which is where load-time CUDA
- # crashes surface.
- import wave, struct, math
- audio = "/tmp/_debug_tone.wav"
- with wave.open(audio, "w") as w:
- w.setnchannels(1); w.setsampwidth(2); w.setframerate(16000)
- w.writeframes(b"".join(
- struct.pack(" None:
- """Local entrypoint: `modal run scripts/wer/remote/modal_sweep.py::dump_debug
- --model-repo handy-computer/medasr-gguf --model-file medasr-Q4_K_M.gguf`.
- Prints stderr lines that contain STATS so we can diff F16 vs Q4_K_M."""
- result = debug_dump.remote(model_repo, model_file)
- print(f"=== {result['model_repo']} / {result['model_file']} (rc={result['rc']}) ===")
- if result["rc"] != 0:
- # Crash case (e.g. CUDA load/compute abort): the filtered STATS view
- # would hide the actual error, so dump full stderr + stdout verbatim.
- print("--- FULL STDERR (rc != 0) ---")
- print(result["stderr"])
- print("--- FULL STDOUT (rc != 0) ---")
- print(result["stdout"])
- return
- for line in result["stderr"].splitlines():
- if "STATS " in line or "FAIL" in line.upper() or "ERR" in line.upper():
- print(line)
- print(f"--- transcript from stdout ---")
- for line in result["stdout"].splitlines():
- if line.startswith("text:"):
- print(line)
-
-
@app.function(
image=image,
timeout=3600,
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c9fd52b1..e8b9ef13 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -22,6 +22,7 @@ add_library(transcribe
transcribe-unicode.cpp
transcribe-unicode-data.cpp
transcribe-debug.cpp
+ transcribe-env.cpp
transcribe-flash-policy.cpp
transcribe-backend.cpp
transcribe-load-common.cpp
@@ -150,6 +151,10 @@ if(TRANSCRIBE_GGML_BACKEND_DL)
endif()
endif()
+if(TRANSCRIBE_ENABLE_VALIDATION_HOOKS)
+ target_compile_definitions(transcribe PRIVATE TRANSCRIBE_ENABLE_VALIDATION_HOOKS)
+endif()
+
# Static posture: tell transcribe.h to drop the Windows __declspec(dllimport/
# export) decoration. PUBLIC so it reaches both this archive's own TUs and any
# CMake consumer linking `transcribe` (C++ tests/examples) — without it a static
diff --git a/src/arch/canary/encoder.cpp b/src/arch/canary/encoder.cpp
index 62ca3c09..9385b00b 100644
--- a/src/arch/canary/encoder.cpp
+++ b/src/arch/canary/encoder.cpp
@@ -26,11 +26,9 @@ namespace conf = transcribe::conformer;
// Direct dw on every backend for the in-block site; im2col for pre-encode
// (canary's encoder geometry matches parakeet's at the dw site).
bool detect_direct_dw_in_block(const char * /*backend*/) {
- const char * env = std::getenv("TRANSCRIBE_CONV_DIRECT_DW");
- if (env != nullptr) return true;
- env = std::getenv("TRANSCRIBE_CONV_NO_DIRECT_DW");
- if (env != nullptr) return false;
- return true;
+ return conf::resolve_conv_direct("TRANSCRIBE_CONV_DIRECT_DW",
+ "TRANSCRIBE_CONV_NO_DIRECT_DW",
+ /*backend_default=*/true);
}
conf::PreEncodeView to_view(const CanaryPreEncode & pe) {
diff --git a/src/arch/canary/model.cpp b/src/arch/canary/model.cpp
index 82f6fa50..f1848605 100644
--- a/src/arch/canary/model.cpp
+++ b/src/arch/canary/model.cpp
@@ -13,6 +13,7 @@
#include "transcribe-arch.h"
#include "transcribe-batch-util.h"
#include "transcribe-debug.h"
+#include "transcribe-env.h"
#include "transcribe-flash-policy.h"
#include "transcribe-load-common.h"
#include "transcribe-loader.h"
@@ -1786,7 +1787,7 @@ transcribe_status run_batch(
cc->batch_results.push_back(std::move(rs));
}
- if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') {
+ if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) {
log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG,
"canary run_batch: n=%d T_enc_max=%d kv_cap=%d prompt=%d\n"
" mel=%.1fms (parallel) enc=%.1fms (serial x%d) decode=%.1fms (batched)",
diff --git a/src/arch/canary_qwen/canary_qwen.h b/src/arch/canary_qwen/canary_qwen.h
index 8bdffbac..422b66a6 100644
--- a/src/arch/canary_qwen/canary_qwen.h
+++ b/src/arch/canary_qwen/canary_qwen.h
@@ -114,8 +114,8 @@ struct CanaryQwenSession final : public transcribe_session {
std::vector enc_host; // perception output [hidden=2048, T_enc]
// Reference (NeMo SALM) ran flash off; we follow suit by default for
- // tightest tensor parity. Override with TRANSCRIBE_FLASH_DECODER=1
- // (or =encoder) at runtime.
+ // tightest tensor parity. Override with TRANSCRIBE_NO_FLASH /
+ // TRANSCRIBE_FORCE_FLASH (both stages) at runtime.
bool encoder_use_flash = false;
bool decoder_use_flash = false;
diff --git a/src/arch/canary_qwen/encoder.cpp b/src/arch/canary_qwen/encoder.cpp
index ff0cc785..4eee347f 100644
--- a/src/arch/canary_qwen/encoder.cpp
+++ b/src/arch/canary_qwen/encoder.cpp
@@ -32,9 +32,9 @@ namespace {
namespace conf = transcribe::conformer;
bool detect_direct_dw_in_block(const char * /*backend*/) {
- if (std::getenv("TRANSCRIBE_CONV_DIRECT_DW") != nullptr) return true;
- if (std::getenv("TRANSCRIBE_CONV_NO_DIRECT_DW") != nullptr) return false;
- return true;
+ return conf::resolve_conv_direct("TRANSCRIBE_CONV_DIRECT_DW",
+ "TRANSCRIBE_CONV_NO_DIRECT_DW",
+ /*backend_default=*/true);
}
conf::PreEncodeView to_view(const PreEncodeSlots & pe) {
diff --git a/src/arch/canary_qwen/model.cpp b/src/arch/canary_qwen/model.cpp
index df856f60..1d8cd953 100644
--- a/src/arch/canary_qwen/model.cpp
+++ b/src/arch/canary_qwen/model.cpp
@@ -27,6 +27,7 @@
#include "transcribe-arch.h"
#include "transcribe-batch-util.h"
#include "transcribe-debug.h"
+#include "transcribe-env.h"
#include "transcribe-flash-policy.h"
#include "transcribe-load-common.h"
#include "transcribe-loader.h"
@@ -716,7 +717,7 @@ transcribe_status init_context(
// Flash defaults: encoder off (the FastConformer rel-pos path has a manual
// rel_shift trick ggml_flash_attn_ext doesn't subsume), decoder ON (~2.4x
// decode speedup on jfk.wav, the Qwen3 step graph is dispatch-bound on
- // Metal). TRANSCRIBE_FLASH_DECODER=1 (or =encoder) overrides.
+ // Metal). TRANSCRIBE_NO_FLASH / TRANSCRIBE_FORCE_FLASH override both stages.
cc->encoder_use_flash = false;
cc->decoder_use_flash = true;
transcribe::flash::apply_env_overrides(cc->encoder_use_flash,
@@ -1057,7 +1058,7 @@ transcribe_status run(transcribe_session * context,
0, mask.size() * sizeof(ggml_fp16_t));
}
- const bool profile_decode = (std::getenv("TRANSCRIBE_PROFILE_DECODE") != nullptr);
+ const bool profile_decode = transcribe::env::flag("TRANSCRIBE_PERF_DEBUG");
int64_t t_prefill_us = 0;
int64_t t_prefill_compute_us = 0;
{
diff --git a/src/arch/cohere/encoder.cpp b/src/arch/cohere/encoder.cpp
index ecec71ff..a3911892 100644
--- a/src/arch/cohere/encoder.cpp
+++ b/src/arch/cohere/encoder.cpp
@@ -29,14 +29,12 @@ namespace conf = transcribe::conformer;
// (native kernels), im2col + mul_mat on Metal/CPU. See the ConvPolicy
// comment in conformer.h for why the two dw sites share one detect result.
bool detect_direct_dw(const char * backend) {
- const char * env = std::getenv("TRANSCRIBE_CONV_DIRECT_DW");
- if (env != nullptr) return true;
- env = std::getenv("TRANSCRIBE_CONV_NO_DIRECT_DW");
- if (env != nullptr) return false;
- if (backend == nullptr) return false;
- if (std::strstr(backend, "Vulkan") != nullptr) return true;
- if (std::strstr(backend, "CUDA") != nullptr) return true;
- return false;
+ const bool backend_default = backend != nullptr &&
+ (std::strstr(backend, "Vulkan") != nullptr ||
+ std::strstr(backend, "CUDA") != nullptr);
+ return conf::resolve_conv_direct("TRANSCRIBE_CONV_DIRECT_DW",
+ "TRANSCRIBE_CONV_NO_DIRECT_DW",
+ backend_default);
}
// ----- Views ------------------------------------------------------
diff --git a/src/arch/cohere/model.cpp b/src/arch/cohere/model.cpp
index 039fadf2..bf8cb262 100644
--- a/src/arch/cohere/model.cpp
+++ b/src/arch/cohere/model.cpp
@@ -14,6 +14,7 @@
#include "transcribe-arch.h"
#include "transcribe-batch-util.h"
#include "transcribe-debug.h"
+#include "transcribe-env.h"
#include "transcribe-flash-policy.h"
#include "transcribe-load-common.h"
#include "transcribe-loader.h"
@@ -1806,7 +1807,7 @@ transcribe_status run_batch(
cc->batch_results.push_back(std::move(rs));
}
- if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') {
+ if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) {
log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG,
"cohere run_batch: n=%d T_enc_max=%d kv_cap=%d prompt=%d\n"
" mel=%.1fms (parallel) enc=%.1fms (serial x%d) decode=%.1fms (batched)",
diff --git a/src/arch/gigaam/model.cpp b/src/arch/gigaam/model.cpp
index 9e0b2bd6..7b8a441c 100644
--- a/src/arch/gigaam/model.cpp
+++ b/src/arch/gigaam/model.cpp
@@ -13,6 +13,7 @@
#include "transcribe-arch.h"
#include "transcribe-batch-util.h"
#include "transcribe-debug.h"
+#include "transcribe-env.h"
#include "transcribe-load-common.h"
#include "transcribe-loader.h"
#include "transcribe-log.h"
@@ -209,6 +210,7 @@ namespace {
// ne=[T_mel, n_mels] (fast=T_mel, slow=n_mels), and ggml's
// fast-to-slow layout maps byte i to ne_index (t=i%T_mel, m=i/T_mel).
// Both byte orders are identical — just a memcpy is needed.
+#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS
transcribe_status load_ref_mel(const std::string & dir,
int n_mels,
int & n_mel_frames,
@@ -247,6 +249,7 @@ transcribe_status load_ref_mel(const std::string & dir,
}
return TRANSCRIBE_OK;
}
+#endif // TRANSCRIBE_ENABLE_VALIDATION_HOOKS
// Host-side RNN-T / CTC greedy decode of one utterance's encoder slice,
// publishing the transcript into the session scratch slot (full_text /
@@ -370,14 +373,17 @@ transcribe_status run(transcribe_session * session,
// center=False + periodic Hann). The env-var injection is a debug knob
// for tensor-parity isolation.
int mel_n_frames = 0;
+ bool mel_from_ref = false;
const int64_t t_mel_start = ggml_time_us();
- if (const char * ref_dir = std::getenv("TRANSCRIBE_GIGAAM_MEL_FROM_REF");
- ref_dir != nullptr && ref_dir[0] != '\0')
- {
+#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS
+ if (const char * ref_dir = transcribe::env::str("TRANSCRIBE_MEL_FROM_REF")) {
if (auto st = load_ref_mel(ref_dir, gm->hparams.fe_num_mels,
mel_n_frames, gc->mel_buf);
st != TRANSCRIBE_OK) return st;
- } else {
+ mel_from_ref = true;
+ }
+#endif
+ if (!mel_from_ref) {
if (auto st = gm->mel.compute(pcm, static_cast(n_samples),
gc->mel_buf, mel_n_frames);
st != TRANSCRIBE_OK) return st;
@@ -674,7 +680,7 @@ transcribe_status run_batch_encode(GigaamSession * gc,
// Debug-only full [d, T, n] intermediates, gated on the env var.
if (transcribe::debug::enabled() &&
- std::getenv("TRANSCRIBE_DUMP_ALL_BLOCKS") != nullptr)
+ transcribe::debug::dump_all_blocks_requested())
{
if (eb.dumps.pre_encode_out != nullptr) {
transcribe::debug::dump_tensor("enc.subsample.out",
diff --git a/src/arch/granite/model.cpp b/src/arch/granite/model.cpp
index 0c808279..597d792e 100644
--- a/src/arch/granite/model.cpp
+++ b/src/arch/granite/model.cpp
@@ -10,6 +10,7 @@
#include "transcribe-arch.h"
#include "transcribe-batch-util.h"
#include "transcribe-debug.h"
+#include "transcribe-env.h"
#include "transcribe-flash-policy.h"
#include "transcribe-load-common.h"
#include "transcribe-loader.h"
@@ -1625,7 +1626,7 @@ transcribe_status run_batch(
}
const int64_t step_us = step_stats.step_us;
- if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') {
+ if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) {
int total_steps = 0;
for (int b = 0; b < n; ++b)
total_steps = std::max(total_steps, static_cast(generated[b].size()));
diff --git a/src/arch/medasr/encoder.cpp b/src/arch/medasr/encoder.cpp
index 858f911f..d8395d84 100644
--- a/src/arch/medasr/encoder.cpp
+++ b/src/arch/medasr/encoder.cpp
@@ -15,6 +15,7 @@
#include "conformer/conformer.h"
#include "transcribe-debug.h"
+#include "transcribe-flash-policy.h"
#include "transcribe-log.h"
#include "ggml.h"
@@ -503,10 +504,13 @@ EncoderBuild build_encoder_graph(ggml_context * ctx,
eb.bn_bias_inputs[i] = bt;
}
- // Flash attention defaults on. Forced off when the manual SDPA path
- // is required (variable-length key-padding mask, single-shot/same-
- // length batches keep flash on).
- const bool use_flash = (std::getenv("TRANSCRIBE_NO_FLASH") == nullptr);
+ // Flash attention defaults on. Forced off when the manual SDPA path is
+ // required (variable-length key-padding mask, single-shot/same-length
+ // batches keep flash on). TRANSCRIBE_NO_FLASH / TRANSCRIBE_FORCE_FLASH
+ // override; MedASR is CTC-only so the decoder flag is a throwaway.
+ bool enc_use_flash = true, dec_use_flash = false;
+ transcribe::flash::apply_env_overrides(enc_use_flash, dec_use_flash);
+ const bool use_flash = enc_use_flash;
eb.dumps.all_block_outs.assign(n_layers, nullptr);
diff --git a/src/arch/medasr/model.cpp b/src/arch/medasr/model.cpp
index 2ffc3c8e..cb48be61 100644
--- a/src/arch/medasr/model.cpp
+++ b/src/arch/medasr/model.cpp
@@ -7,7 +7,7 @@
// init_context() allocates the per-context state.
//
// run() builds the encoder graph, uploads the mel (or the reference
-// mel via TRANSCRIBE_MEDASR_MEL_FROM_REF), allocates and uploads the
+// mel via TRANSCRIBE_MEL_FROM_REF), allocates and uploads the
// per-block fused-BN scale/bias inputs, computes the graph, reads
// back the CTC logits, performs greedy collapse (drop blanks, collapse
// repeats), and populates the result snapshot.
@@ -20,6 +20,7 @@
#include "transcribe-arch.h"
#include "transcribe-batch-util.h"
#include "transcribe-debug.h"
+#include "transcribe-env.h"
#include "transcribe-load-common.h"
#include "transcribe-loader.h"
#include "transcribe-log.h"
@@ -299,6 +300,7 @@ transcribe_status init_context(transcribe_model * model,
return TRANSCRIBE_OK;
}
+#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS
// Load reference mel sidecar from /mel.in.f32 (or
// frontend.mel.out.f32).
transcribe_status load_ref_mel(const std::string & dir,
@@ -349,6 +351,7 @@ transcribe_status load_ref_mel(const std::string & dir,
}
return TRANSCRIBE_OK;
}
+#endif // TRANSCRIBE_ENABLE_VALIDATION_HOOKS
// Greedy CTC decode: argmax per frame, collapse repeats, drop blanks
// AND skip the LASR special tokens (]=1, =2, =3) — matching
@@ -402,14 +405,17 @@ transcribe_status run(transcribe_session * session,
// ----- Mel acquisition -----------------------------------------------
int mel_n_frames = 0;
+ bool mel_from_ref = false;
const int64_t t_mel_start = ggml_time_us();
- if (const char * ref_dir = std::getenv("TRANSCRIBE_MEDASR_MEL_FROM_REF");
- ref_dir != nullptr && ref_dir[0] != '\0')
- {
+#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS
+ if (const char * ref_dir = transcribe::env::str("TRANSCRIBE_MEL_FROM_REF")) {
if (auto st = load_ref_mel(ref_dir, gm->hparams.fe_num_mels,
mel_n_frames, gc->mel_buf);
st != TRANSCRIBE_OK) return st;
- } else {
+ mel_from_ref = true;
+ }
+#endif
+ if (!mel_from_ref) {
int out_n_mels = 0;
std::vector m_major;
if (auto st = gm->mel->compute(pcm, static_cast(n_samples),
@@ -578,54 +584,6 @@ transcribe_status run(transcribe_session * session,
try_dump("enc.out_norm.out", eb.dumps.out_norm_out, "encoder.out_norm");
try_dump("enc.ctc_logits", eb.dumps.ctc_logits_for_dump, "encoder.ctc_logits");
- // Diagnostic: per-block stats gated by TRANSCRIBE_MEDASR_DEBUG_STATS.
- // Prints min / max / mean / has_nan / has_inf for each marker tensor
- // to stderr (Modal captures and surfaces stderr in the run logs).
- // Used to localize where CUDA Q* output diverges from F32 / F16.
- if (const char * dbg = std::getenv("TRANSCRIBE_MEDASR_DEBUG_STATS");
- dbg != nullptr && dbg[0] != '\0')
- {
- std::vector tmp;
- auto report = [&](const char * name, ggml_tensor * t) {
- if (t == nullptr) return;
- const size_t n = static_cast(ggml_nelements(t));
- tmp.assign(n, 0.0f);
- ggml_backend_tensor_get(t, tmp.data(), 0, n * sizeof(float));
- float mn = +1e30f, mx = -1e30f;
- double sum = 0.0;
- int n_nan = 0, n_inf = 0;
- for (size_t i = 0; i < n; ++i) {
- const float v = tmp[i];
- if (std::isnan(v)) { ++n_nan; continue; }
- if (std::isinf(v)) { ++n_inf; continue; }
- sum += v;
- if (v < mn) mn = v;
- if (v > mx) mx = v;
- }
- const double mean = n > 0 ? sum / static_cast(n) : 0.0;
- log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG,
- "STATS %-26s n=%zu min=%+.4e max=%+.4e mean=%+.4e nan=%d inf=%d",
- name, n, mn, mx, mean, n_nan, n_inf);
- };
- report("mel.in", eb.mel_in);
- report("sub.after_dense0", eb.dumps.sub_after_dense0);
- report("sub.after_conv0", eb.dumps.sub_after_conv0);
- report("sub.after_conv1", eb.dumps.sub_after_conv1);
- report("enc.subsampling.out", eb.dumps.subsampling_out);
- report("enc.block.0.post_ff1", eb.dumps.block0_post_ff1);
- report("enc.block.0.post_attn", eb.dumps.block0_post_attn);
- report("enc.block.0.post_conv", eb.dumps.block0_post_conv);
- report("enc.block.0.post_ff2", eb.dumps.block0_post_ff2);
- for (int i = 0; i < n_layers; ++i) {
- if (eb.dumps.all_block_outs[i] == nullptr) continue;
- char nm[32];
- std::snprintf(nm, sizeof(nm), "enc.block.%d.out", i);
- report(nm, eb.dumps.all_block_outs[i]);
- }
- report("enc.out_norm.out", eb.dumps.out_norm_out);
- report("enc.ctc_logits", eb.dumps.ctc_logits_for_dump);
- }
-
// Per-utterance encoder-output dump (single-shot baseline for the
// batched-equals-single-shot gate). Matches the parakeet / gigaam name.
if (transcribe::debug::enabled() && eb.dumps.out_norm_out != nullptr) {
diff --git a/src/arch/moonshine/model.cpp b/src/arch/moonshine/model.cpp
index 8accd8dc..dde59f6b 100644
--- a/src/arch/moonshine/model.cpp
+++ b/src/arch/moonshine/model.cpp
@@ -14,6 +14,7 @@
#include "transcribe-arch.h"
#include "transcribe-batch-util.h"
#include "transcribe-debug.h"
+#include "transcribe-env.h"
#include "transcribe-flash-policy.h"
#include "transcribe-load-common.h"
#include "transcribe-loader.h"
@@ -1120,7 +1121,7 @@ transcribe_status run_batch(
cc->batch_results.push_back(std::move(rs));
}
- if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') {
+ if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) {
log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG,
"moonshine run_batch: n=%d T_enc_max=%d max_n_kv=%d\n"
" enc=%.1fms (serial x%d) decode=%.1fms (batched)",
diff --git a/src/arch/moonshine_streaming/model.cpp b/src/arch/moonshine_streaming/model.cpp
index 2eaad940..3c6983e1 100644
--- a/src/arch/moonshine_streaming/model.cpp
+++ b/src/arch/moonshine_streaming/model.cpp
@@ -31,6 +31,7 @@
#include "transcribe-arch.h"
#include "transcribe-batch-util.h"
#include "transcribe-debug.h"
+#include "transcribe-env.h"
#include "transcribe-flash-policy.h"
#include "transcribe-load-common.h"
#include "transcribe-loader.h"
@@ -2232,7 +2233,7 @@ transcribe_status run_batch(
cc->batch_results.push_back(std::move(rs));
}
- if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') {
+ if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) {
log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG,
"moonshine_streaming run_batch: n=%d T_enc_max=%d kv_window=%d (cap %d)\n"
" enc+adapter=%.1fms (serial x%d) decode=%.1fms (batched)",
diff --git a/src/arch/parakeet/encoder.cpp b/src/arch/parakeet/encoder.cpp
index 4d680b24..0d9b4b64 100644
--- a/src/arch/parakeet/encoder.cpp
+++ b/src/arch/parakeet/encoder.cpp
@@ -26,6 +26,7 @@
#include "conformer/conformer.h"
#include "transcribe-debug.h"
+#include "transcribe-flash-policy.h"
#include "transcribe-log.h"
#include "ggml.h"
@@ -49,12 +50,10 @@ namespace conf = transcribe::conformer;
// backend (faster than the 31x im2col expansion). Env vars override
// both sites.
bool detect_direct_dw_in_block(const char * backend) {
- const char * env = std::getenv("TRANSCRIBE_CONV_DIRECT_DW");
- if (env != nullptr) return true; // user override
- env = std::getenv("TRANSCRIBE_CONV_NO_DIRECT_DW");
- if (env != nullptr) return false; // user override
(void)backend;
- return true;
+ return conf::resolve_conv_direct("TRANSCRIBE_CONV_DIRECT_DW",
+ "TRANSCRIBE_CONV_NO_DIRECT_DW",
+ /*backend_default=*/true);
}
// Pre-encode depthwise dispatch: direct path EXCEPT on Metal, where
@@ -63,13 +62,12 @@ bool detect_direct_dw_in_block(const char * backend) {
// CONV_2D_DW; the direct path avoids the ~31x im2col expansion and the
// degenerate per-channel [1 x K] matmul (~40-50ms here).
bool detect_direct_dw_in_pre_encode(const char * backend) {
- if (std::getenv("TRANSCRIBE_CONV_DIRECT_DW") != nullptr) return true;
- if (std::getenv("TRANSCRIBE_CONV_NO_DIRECT_DW") != nullptr) return false;
- if (backend != nullptr &&
- (std::strstr(backend, "Metal") != nullptr || std::strstr(backend, "metal") != nullptr)) {
- return false;
- }
- return true;
+ const bool is_metal = backend != nullptr &&
+ (std::strstr(backend, "Metal") != nullptr ||
+ std::strstr(backend, "metal") != nullptr);
+ return conf::resolve_conv_direct("TRANSCRIBE_CONV_DIRECT_DW",
+ "TRANSCRIBE_CONV_NO_DIRECT_DW",
+ /*backend_default=*/!is_metal);
}
// ----- Views ------------------------------------------------------
@@ -216,7 +214,7 @@ void parakeet_subblock_observer_cb(void * user,
std::vector parse_sub_block_env(int n_layers)
{
std::vector out;
- const char * raw = std::getenv("TRANSCRIBE_DUMP_SUB_BLOCKS");
+ const char * raw = transcribe::debug::dump_sub_blocks_spec();
if (raw == nullptr || *raw == '\0') return out;
const char * p = raw;
while (*p != '\0') {
@@ -431,10 +429,14 @@ EncoderBuild build_encoder_graph(ggml_context * ctx,
bparams.kv_type = kv_type;
// Flash attention by default. Flash batches correctly (the batched
// encoder output is bit-identical to single-shot flash), so the batch
- // axis does not change the path. TRANSCRIBE_NO_FLASH=1 forces the
- // manual F32 mul_mat + soft_max path — used by the bit-exact CPU
- // tensor gate (flash casts the rel-pos mask to F16, manual stays F32).
- bparams.use_flash = (std::getenv("TRANSCRIBE_NO_FLASH") == nullptr);
+ // axis does not change the path. TRANSCRIBE_NO_FLASH forces the manual
+ // F32 mul_mat + soft_max path — used by the bit-exact CPU tensor gate
+ // (flash casts the rel-pos mask to F16, manual stays F32);
+ // TRANSCRIBE_FORCE_FLASH forces it back on. Parakeet has no attention
+ // decoder, so the decoder flag is a throwaway.
+ bool enc_use_flash = true, dec_use_flash = false;
+ transcribe::flash::apply_env_overrides(enc_use_flash, dec_use_flash);
+ bparams.use_flash = enc_use_flash;
bparams.policy = policy;
bparams.att_context_left = hp.enc_att_context_left;
bparams.att_context_right = hp.enc_att_context_right;
@@ -476,7 +478,7 @@ EncoderBuild build_encoder_graph(ggml_context * ctx,
// Per-block dump opt-in for the divergence bisect
// (TRANSCRIBE_DUMP_ALL_BLOCKS).
const bool dump_all_blocks =
- std::getenv("TRANSCRIBE_DUMP_ALL_BLOCKS") != nullptr;
+ transcribe::debug::dump_all_blocks_requested();
if (dump_all_blocks && eb.dumps.block0_out != nullptr) {
transcribe::debug::mark_tensor_for_dump(eb.dumps.block0_out);
}
@@ -756,10 +758,13 @@ EncoderBuild build_encoder_graph_streaming(
bparams.n_head = hp.enc_n_heads;
bparams.conv_kernel = hp.enc_conv_kernel;
bparams.kv_type = kv_type;
- // Flash-attention opt-out (TRANSCRIBE_NO_FLASH). On CPU at streaming
- // geometry the flash kernel is dramatically slower (~33 ms/layer vs
- // ~1 ms on a Cortex-A55).
- bparams.use_flash = (std::getenv("TRANSCRIBE_NO_FLASH") == nullptr);
+ // Flash-attention opt-out (TRANSCRIBE_NO_FLASH / TRANSCRIBE_FORCE_FLASH).
+ // On CPU at streaming geometry the flash kernel is dramatically slower
+ // (~33 ms/layer vs ~1 ms on a Cortex-A55). Parakeet has no attention
+ // decoder, so the decoder flag is a throwaway.
+ bool enc_use_flash = true, dec_use_flash = false;
+ transcribe::flash::apply_env_overrides(enc_use_flash, dec_use_flash);
+ bparams.use_flash = enc_use_flash;
bparams.policy = policy;
// No att_context_left/right: ChunkedLimited derives the band entirely
// from attn_chunked_mask (build_conformer_block reads them only on the
diff --git a/src/arch/parakeet/model.cpp b/src/arch/parakeet/model.cpp
index 9c34cd5d..02d14914 100644
--- a/src/arch/parakeet/model.cpp
+++ b/src/arch/parakeet/model.cpp
@@ -1213,7 +1213,7 @@ static transcribe_status run_one_shot_inner(
}
// Per-block dump for the layer-by-layer divergence bisect (gated by
// TRANSCRIBE_DUMP_ALL_BLOCKS).
- if (std::getenv("TRANSCRIBE_DUMP_ALL_BLOCKS") != nullptr) {
+ if (transcribe::debug::dump_all_blocks_requested()) {
for (size_t i = 0; i < eb.dumps.all_block_outs.size(); ++i) {
ggml_tensor * t = eb.dumps.all_block_outs[i];
if (t == nullptr) continue;
@@ -1531,7 +1531,7 @@ static transcribe_status run_batch_encode(
// [d_model, T_max, n] tensors, so a batched-vs-single divergence can be
// located per stage. Gated on TRANSCRIBE_DUMP_ALL_BLOCKS.
if (transcribe::debug::enabled() &&
- std::getenv("TRANSCRIBE_DUMP_ALL_BLOCKS") != nullptr)
+ transcribe::debug::dump_all_blocks_requested())
{
if (eb.dumps.pre_encode_out != nullptr) {
transcribe::debug::dump_tensor("enc.pre_encode.out",
@@ -2547,7 +2547,7 @@ transcribe_status emit_buffered_chunk(
transcribe::debug::dump_tensor(name, eb.dumps.last_block_out,
"encoder.block.last.out");
}
- if (std::getenv("TRANSCRIBE_DUMP_ALL_BLOCKS") != nullptr) {
+ if (transcribe::debug::dump_all_blocks_requested()) {
for (size_t i = 0; i < eb.dumps.all_block_outs.size(); ++i) {
ggml_tensor * t = eb.dumps.all_block_outs[i];
if (t == nullptr) continue;
diff --git a/src/arch/qwen3_asr/model.cpp b/src/arch/qwen3_asr/model.cpp
index cd1e632d..2b55d245 100644
--- a/src/arch/qwen3_asr/model.cpp
+++ b/src/arch/qwen3_asr/model.cpp
@@ -10,6 +10,7 @@
#include "transcribe-arch.h"
#include "transcribe-batch-util.h"
#include "transcribe-debug.h"
+#include "transcribe-env.h"
#include "transcribe-flash-policy.h"
#include "transcribe-load-common.h"
#include "transcribe-loader.h"
@@ -1143,8 +1144,7 @@ transcribe_status run(
// Optional perf breakdown (finer split than the public mel/encode/decode
// timings), gated on env var.
- if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG");
- e != nullptr && *e != '\0' && *e != '0')
+ if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG"))
{
const double ms = 1.0 / 1000.0;
const double sum_ms = (cc->t_mel_us + t_enc_build_us +
@@ -1790,8 +1790,7 @@ transcribe_status run_batch(
cc->batch_results.push_back(std::move(rs));
}
- if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG");
- e != nullptr && *e != '\0' && *e != '0') {
+ if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) {
log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG,
"qwen3_asr run_batch: n=%d valid=%d max_n_kv=%d steps=%d (all phases batched x%d)\n"
" enc_pass=%.1fms (mel=%.1f parallel + enc_compute=%.1f, 1 graph)\n"
diff --git a/src/arch/voxtral_realtime/model.cpp b/src/arch/voxtral_realtime/model.cpp
index f05414a7..799d4720 100644
--- a/src/arch/voxtral_realtime/model.cpp
+++ b/src/arch/voxtral_realtime/model.cpp
@@ -17,6 +17,7 @@
#include "transcribe-arch.h"
#include "transcribe-batch-util.h"
#include "transcribe-debug.h"
+#include "transcribe-env.h"
#include "transcribe-flash-policy.h"
#include "transcribe-load-common.h"
#include "transcribe-loader.h"
@@ -99,6 +100,7 @@ void apply_threads(ggml_backend_sched_t sched, int n_threads) {
transcribe::configure_sched_n_threads(sched, n_threads);
}
+#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS
// Read a reference enc.mel.in.f32 ([n_mels, n_frames] mel-major) for numerical
// isolation. Returns true and fills mel_buf (mel-major) + n_frames on success.
bool read_ref_mel(const std::string & dir, int n_mels,
@@ -118,6 +120,7 @@ bool read_ref_mel(const std::string & dir, int n_mels,
n_frames = static_cast(n / n_mels);
return true;
}
+#endif // TRANSCRIBE_ENABLE_VALIDATION_HOOKS
// Input-length contract (see docs/input-limits.md). Chunked/unbounded family:
// the encoder is causal + sliding-window (750 frames) advanced on a
@@ -434,14 +437,18 @@ transcribe_status forward_buffer(Session * cc, Model * cm,
// ----- Mel (or reference-injected mel for numerical isolation) -----
const int n_mels = cm->hparams.enc_num_mel_bins;
int mel_n_frames = 0;
- const char * ref_mel_dir = std::getenv("TRANSCRIBE_VOXTRAL_REALTIME_MEL_FROM_REF");
+ bool mel_from_ref = false;
const int64_t t_mel_start = ggml_time_us();
- if (ref_mel_dir != nullptr && ref_mel_dir[0] != '\0') {
+#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS
+ if (const char * ref_mel_dir = transcribe::env::str("TRANSCRIBE_MEL_FROM_REF")) {
if (!read_ref_mel(ref_mel_dir, n_mels, cc->mel_buf, mel_n_frames)) {
log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run: failed to read ref mel from %s", ref_mel_dir);
return TRANSCRIBE_ERR_GGUF;
}
- } else {
+ mel_from_ref = true;
+ }
+#endif
+ if (!mel_from_ref) {
// Streaming offline audio padding (mistral-common encode_transcription,
// StreamingMode.OFFLINE): pad the audio into the 12.5 Hz token grid as
// [n_left_pad zeros] + [raw rounded up to raw_per_tok] + [n_right zeros]
@@ -1534,7 +1541,7 @@ transcribe_status stream_process(Session * cc, Model * cm, bool is_final, bool *
}
// Per-component timing (diagnostic): printed once at finalize.
- if (is_final && std::getenv("TRANSCRIBE_VOXTRAL_REALTIME_STREAM_TIMING") != nullptr) {
+ if (is_final && transcribe::env::flag("TRANSCRIBE_VOXTRAL_REALTIME_STREAM_TIMING")) {
const double mel = cc->stream_t_mel_us / 1000.0, conv = cc->stream_t_conv_us / 1000.0;
const double enc = cc->stream_t_enc_us / 1000.0, dec = cc->stream_t_dec_us / 1000.0;
const double enc_c = cc->stream_t_enc_compute_us / 1000.0; // pure graph_compute
diff --git a/src/arch/whisper/model.cpp b/src/arch/whisper/model.cpp
index 94f7c1ab..f0479077 100644
--- a/src/arch/whisper/model.cpp
+++ b/src/arch/whisper/model.cpp
@@ -1,7 +1,7 @@
// arch/whisper/model.cpp - Whisper ASR family handler.
//
// Normal inference uses the C++ mel frontend and the autoregressive decoder
-// path below; TRANSCRIBE_WHISPER_MEL_FROM_REF can inject a reference mel tensor
+// path below; TRANSCRIBE_MEL_FROM_REF can inject a reference mel tensor
// to isolate encoder/decoder drift during numerical validation.
#include "whisper.h"
@@ -13,6 +13,7 @@
#include "transcribe-arch.h"
#include "transcribe-batch-util.h"
#include "transcribe-debug.h"
+#include "transcribe-env.h"
#include "transcribe-flash-policy.h"
#include "transcribe-load-common.h"
#include "transcribe-loader.h"
@@ -305,7 +306,7 @@ bool ensure_compute_ctx(WhisperSession * cc, size_t mem) {
}
// Print the per-stage performance summary collected during the most
-// recent whisper_run. Opt-in via TRANSCRIBE_WHISPER_PROFILE=1 — gated
+// recent whisper_run. Opt-in via TRANSCRIBE_PERF_DEBUG — gated
// by the caller, this helper just formats the counters. Output is one
// summary line per stage (encoder / cross / prompt / step) plus a
// per-step average for the steady-state generation loop, which is the
@@ -372,11 +373,11 @@ void print_whisper_perf(const WhisperPerf & p) {
avg_us(p.step_compute), avg_us(p.step_tensor_get),
avg_us(p.step_cpu));
- // CPU sub-section breakdown — opt-in via TRANSCRIBE_WHISPER_PROFILE
- // values that contain "cpu" or "all". Keeps the default profile
- // output unchanged for existing benchmarks while still surfacing
+ // CPU sub-section breakdown — opt-in via TRANSCRIBE_PERF_DEBUG values
+ // that contain "cpu" or "all". Keeps the default profile output
+ // unchanged for existing benchmarks while still surfacing
// suppress/timestamp/sample/logprob splits when needed.
- const char * profile_env = std::getenv("TRANSCRIBE_WHISPER_PROFILE");
+ const char * profile_env = transcribe::env::str("TRANSCRIBE_PERF_DEBUG");
bool show_cpu_breakdown = false;
if (profile_env != nullptr) {
const std::string v = profile_env;
@@ -409,8 +410,7 @@ void print_whisper_perf(const WhisperPerf & p) {
}
bool whisper_perf_enabled() {
- const char * s = std::getenv("TRANSCRIBE_WHISPER_PROFILE");
- return s != nullptr && s[0] != '\0' && s[0] != '0';
+ return transcribe::env::flag("TRANSCRIBE_PERF_DEBUG");
}
// load
@@ -990,6 +990,7 @@ float logprob_of_token_hf(
return logits[token_id] * rescale_T - log_Z;
}
+#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS
// Load 3000 * 80 f32 floats from /enc.mel.in.f32. Layout on disk
// matches the reference dump: row-major (3000, 80) — exactly what we
// need to upload into the ggml ne=[80, 3000] input tensor via a flat
@@ -1029,6 +1030,7 @@ transcribe_status load_mel_from_ref(const char * ref_dir,
}
return TRANSCRIBE_OK;
}
+#endif // TRANSCRIBE_ENABLE_VALIDATION_HOOKS
} // namespace
@@ -1290,9 +1292,10 @@ transcribe_status whisper_run(
// ----- Mel frontend -----
//
- // 1. TRANSCRIBE_WHISPER_MEL_FROM_REF= — debug knob that reads the
- // reference mel dump from disk so encoder drift can be isolated from
- // C++ mel drift. The default path exercises the production frontend.
+ // 1. TRANSCRIBE_MEL_FROM_REF= — validation hook (compiled in only
+ // with -DTRANSCRIBE_ENABLE_VALIDATION_HOOKS) that reads the reference
+ // mel dump from disk so encoder drift can be isolated from C++ mel
+ // drift. The default path exercises the production frontend.
//
// 2. C++ MelFrontend (default):
// - Short-form (n_samples <= fe_n_samples): pad PCM to fe_n_samples
@@ -1312,9 +1315,9 @@ transcribe_status whisper_run(
const int64_t t_mel_start = ggml_time_us();
int total_mel_frames = 0;
- if (const char * ref_dir = std::getenv("TRANSCRIBE_WHISPER_MEL_FROM_REF");
- ref_dir != nullptr && ref_dir[0] != '\0')
- {
+ bool mel_from_ref = false;
+#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS
+ if (const char * ref_dir = transcribe::env::str("TRANSCRIBE_MEL_FROM_REF")) {
if (const transcribe_status st = load_mel_from_ref(
ref_dir, n_mels, n_mel_frames_per_chunk, cc->mel_buf);
st != TRANSCRIBE_OK)
@@ -1322,7 +1325,10 @@ transcribe_status whisper_run(
return st;
}
total_mel_frames = n_mel_frames_per_chunk;
- } else {
+ mel_from_ref = true;
+ }
+#endif
+ if (!mel_from_ref) {
if (!cm->mel.has_value()) {
log_msg(TRANSCRIBE_LOG_LEVEL_ERROR,
"whisper run: model has no MelFrontend "
@@ -3341,7 +3347,7 @@ transcribe_status whisper_run_batch(
have_result[b] = 1;
}
- if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') {
+ if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) {
int n_serial = 0; for (int b = 0; b < n; ++b) n_serial += needs_serial[b] ? 1 : 0;
log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG,
"whisper run_batch: n=%d batched=%d serial=%d tiers=%d want_ts=%d "
diff --git a/src/arch/whisper/whisper.h b/src/arch/whisper/whisper.h
index 3686cce2..4546fbfa 100644
--- a/src/arch/whisper/whisper.h
+++ b/src/arch/whisper/whisper.h
@@ -178,7 +178,7 @@ bool kv_cache_init_batched(WhisperKvCache & cache,
ggml_type kv_type);
// Per-stage timing counters. Always-on (timestamps are negligible); printing
-// is opt-in via TRANSCRIBE_WHISPER_PROFILE=1. Reset at the top of whisper_run.
+// is opt-in via TRANSCRIBE_PERF_DEBUG. Reset at the top of whisper_run.
struct WhisperPerfStage {
int64_t total_us = 0;
int count = 0;
@@ -220,7 +220,7 @@ struct WhisperPerf {
WhisperPerfStage step_cpu;
// CPU-section sub-counters. Always populated (the timestamps are
- // negligible) but only printed when TRANSCRIBE_WHISPER_PROFILE
+ // negligible) but only printed when TRANSCRIBE_PERF_DEBUG
// includes "cpu" or "all". Splits the prompt_cpu / step_cpu
// total into the four sub-stages of post-graph processing.
WhisperPerfStage prompt_cpu_suppress;
@@ -313,7 +313,7 @@ struct WhisperSession final : public transcribe_session {
// at the top of each run alongside cc->clear_result().
std::vector chunk_traces;
- // Per-stage timing counters; summary printed when TRANSCRIBE_WHISPER_PROFILE=1.
+ // Per-stage timing counters; summary printed when TRANSCRIBE_PERF_DEBUG is set.
WhisperPerf perf;
// Reusable scratch for the multinomial T>0 sampler, sized to vocab_size on
diff --git a/src/conformer/conformer.cpp b/src/conformer/conformer.cpp
index 8f533eca..26722912 100644
--- a/src/conformer/conformer.cpp
+++ b/src/conformer/conformer.cpp
@@ -3,9 +3,12 @@
// See conformer.h for the API contract. Each family's encoder.cpp keeps
// only the glue binding these helpers to its weights / hparams / graph
// driver; per-family policy knobs (ConvPolicy, BlockParams) are passed in.
-// No environment variables are read here except by detect_direct_pw.
+// Conv-dispatch env vars (TRANSCRIBE_CONV_DIRECT_PW/DW and their NO_ variants)
+// are read only via resolve_conv_direct here, which detect_direct_pw and every
+// family's depthwise detect delegate to — one place owns the parsing.
#include "conformer/conformer.h"
+#include "transcribe-env.h"
#include "transcribe-log.h"
#include "ggml.h"
@@ -309,18 +312,23 @@ ggml_tensor * add_conv_bias(ggml_context * ctx,
return ggml_add(ctx, conv_out, bias_4d);
}
+bool resolve_conv_direct(const char * direct_env,
+ const char * no_direct_env,
+ bool backend_default) {
+ if (transcribe::env::flag(direct_env)) return true; // user override
+ if (transcribe::env::flag(no_direct_env)) return false; // user override
+ return backend_default;
+}
+
bool detect_direct_pw(const char * backend) {
- const char * env = std::getenv("TRANSCRIBE_CONV_NO_DIRECT_PW");
- if (env != nullptr) return false; // user override
- env = std::getenv("TRANSCRIBE_CONV_DIRECT_PW");
- if (env != nullptr) return true; // user override
// Vulkan defaults to im2col: on AMD Renoir, im2col + mul_mat measured
// ~200 ms/encode faster than direct for f32 weights. Metal/CPU prefer
// direct.
- if (backend != nullptr && std::strstr(backend, "Vulkan") != nullptr) {
- return false;
- }
- return true;
+ const bool backend_default =
+ !(backend != nullptr && std::strstr(backend, "Vulkan") != nullptr);
+ return resolve_conv_direct("TRANSCRIBE_CONV_DIRECT_PW",
+ "TRANSCRIBE_CONV_NO_DIRECT_PW",
+ backend_default);
}
// Conv module (pointwise -> GLU -> depthwise -> BN/LN -> SiLU -> pointwise).
diff --git a/src/conformer/conformer.h b/src/conformer/conformer.h
index 59c80b69..8e9b891a 100644
--- a/src/conformer/conformer.h
+++ b/src/conformer/conformer.h
@@ -123,9 +123,19 @@ struct ConvPolicy {
bool causal_pre_encode = false;
};
+// Resolve a conv-dispatch toggle from its env overrides. The DIRECT var forces
+// true, the NO_DIRECT var forces false (DIRECT wins if both are set); otherwise
+// `backend_default` is used. Centralizes the override parsing shared by the
+// pointwise and the per-family depthwise dispatch helpers, so the env-var read
+// has exactly one definition. `direct_env` / `no_direct_env` are env var names
+// (e.g. "TRANSCRIBE_CONV_DIRECT_DW" / "TRANSCRIBE_CONV_NO_DIRECT_DW").
+bool resolve_conv_direct(const char * direct_env,
+ const char * no_direct_env,
+ bool backend_default);
+
// Pointwise conv dispatch auto-detect (same answer for every family today).
-// Env overrides: TRANSCRIBE_CONV_NO_DIRECT_PW=1 forces im2col,
-// TRANSCRIBE_CONV_DIRECT_PW=1 forces direct. Vulkan defaults to im2col: on
+// Env overrides: TRANSCRIBE_CONV_NO_DIRECT_PW forces im2col,
+// TRANSCRIBE_CONV_DIRECT_PW forces direct. Vulkan defaults to im2col: on
// AMD Renoir, im2col + mul_mat measured ~200 ms/encode faster than direct for
// f32 weights. Metal and CPU prefer direct.
bool detect_direct_pw(const char * backend);
diff --git a/src/transcribe-debug.cpp b/src/transcribe-debug.cpp
index c1d21aa7..30f92a3d 100644
--- a/src/transcribe-debug.cpp
+++ b/src/transcribe-debug.cpp
@@ -8,6 +8,8 @@
#include "transcribe-debug.h"
+#include "transcribe-env.h"
+
#include "ggml.h"
#include "ggml-backend.h"
@@ -136,6 +138,30 @@ const char * dump_dir() {
return enabled() ? g_dump_dir.c_str() : nullptr;
}
+bool validation_hooks_enabled() {
+#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS
+ return true;
+#else
+ return false;
+#endif
+}
+
+bool dump_all_blocks_requested() {
+#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS
+ return transcribe::env::flag("TRANSCRIBE_DUMP_ALL_BLOCKS");
+#else
+ return false;
+#endif
+}
+
+const char * dump_sub_blocks_spec() {
+#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS
+ return transcribe::env::str("TRANSCRIBE_DUMP_SUB_BLOCKS");
+#else
+ return nullptr;
+#endif
+}
+
void push_name_prefix(const char * prefix) {
if (!enabled() || prefix == nullptr) return;
g_name_prefix_stack.emplace_back(prefix);
diff --git a/src/transcribe-debug.h b/src/transcribe-debug.h
index f10befc2..79c6f8a2 100644
--- a/src/transcribe-debug.h
+++ b/src/transcribe-debug.h
@@ -42,6 +42,28 @@ bool enabled();
// as the dumper is enabled. Useful for tests.
const char * dump_dir();
+// Validation-hook gating ------------------------------------------------------
+//
+// The reference-mel injection (TRANSCRIBE_MEL_FROM_REF) and the per-layer
+// tensor dumps (TRANSCRIBE_DUMP_ALL_BLOCKS / TRANSCRIBE_DUMP_SUB_BLOCKS) are
+// numerical-parity hooks for porting/validation. They are compiled in only when
+// the library is built with -DTRANSCRIBE_ENABLE_VALIDATION_HOOKS=ON (see the
+// CMake option). Release builds leave them out.
+
+// True if the library was compiled with validation hooks enabled.
+bool validation_hooks_enabled();
+
+// Per-layer block dump opt-in (TRANSCRIBE_DUMP_ALL_BLOCKS). Always false unless
+// the library was built with validation hooks. The env var name lives only in
+// transcribe-debug.cpp (behind the compile guard) so it is absent from the
+// strings of a release binary.
+bool dump_all_blocks_requested();
+
+// TRANSCRIBE_DUMP_SUB_BLOCKS spec (CSV of block indices to dump sub-layer
+// activations for) or nullptr. Always nullptr unless built with validation
+// hooks.
+const char * dump_sub_blocks_spec();
+
// Push/pop a name prefix that gets prepended to every subsequent
// dump_tensor / dump_host_f32 call. Used by buffered streaming to
// scope per-chunk intermediate dumps (e.g. "stream.chunk.5.") so the
diff --git a/src/transcribe-env.cpp b/src/transcribe-env.cpp
new file mode 100644
index 00000000..a1d4a618
--- /dev/null
+++ b/src/transcribe-env.cpp
@@ -0,0 +1,22 @@
+// transcribe-env.cpp - see transcribe-env.h.
+
+#include "transcribe-env.h"
+
+#include
+
+namespace transcribe::env {
+
+const char * str(const char * name) {
+ const char * v = std::getenv(name);
+ if (v == nullptr || v[0] == '\0') {
+ return nullptr;
+ }
+ return v;
+}
+
+bool flag(const char * name) {
+ const char * v = str(name);
+ return v != nullptr && v[0] != '0';
+}
+
+} // namespace transcribe::env
diff --git a/src/transcribe-env.h b/src/transcribe-env.h
new file mode 100644
index 00000000..5bdd56cb
--- /dev/null
+++ b/src/transcribe-env.h
@@ -0,0 +1,24 @@
+// transcribe-env.h - tiny helpers for reading environment variables with one
+// consistent parse convention across the codebase.
+//
+// INTERNAL, C++17. Not part of the public ABI.
+//
+// A "flag" env var is considered ON when it is set, non-empty, and its first
+// character is not '0' (so FOO=1 / FOO=on / FOO=yes are ON; FOO=0 and FOO=
+// and unset are OFF). This matches the dominant pre-existing convention
+// (TRANSCRIBE_PERF_DEBUG) and is the single source of truth for boolean env
+// toggles — do not hand-roll getenv() comparisons elsewhere.
+
+#pragma once
+
+namespace transcribe::env {
+
+// True iff `name` is set, non-empty, and its first character is not '0'.
+bool flag(const char * name);
+
+// The value of `name`, or nullptr if it is unset or empty. The returned
+// pointer is owned by the environment (do not free); valid until the next
+// setenv/putenv.
+const char * str(const char * name);
+
+} // namespace transcribe::env
diff --git a/src/transcribe-flash-policy.cpp b/src/transcribe-flash-policy.cpp
index d269c9b5..168f835f 100644
--- a/src/transcribe-flash-policy.cpp
+++ b/src/transcribe-flash-policy.cpp
@@ -4,19 +4,17 @@
#include "transcribe-flash-policy.h"
-#include
+#include "transcribe-env.h"
namespace transcribe::flash {
void apply_env_overrides(bool & encoder_use_flash,
bool & decoder_use_flash) {
- const char * no_flash = std::getenv("TRANSCRIBE_NO_FLASH");
- if (no_flash != nullptr && no_flash[0] == '1') {
+ if (transcribe::env::flag("TRANSCRIBE_NO_FLASH")) {
encoder_use_flash = false;
decoder_use_flash = false;
}
- const char * force_flash = std::getenv("TRANSCRIBE_FORCE_FLASH");
- if (force_flash != nullptr && force_flash[0] == '1') {
+ if (transcribe::env::flag("TRANSCRIBE_FORCE_FLASH")) {
encoder_use_flash = true;
decoder_use_flash = true;
}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 64039f46..abbabde3 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -489,7 +489,7 @@ endif()
#
# Gated two ways: by the CMake option above (so the binary is not
# built unless the developer opts in) AND by the runtime
-# TRANSCRIBE_REAL_PARAKEET_GGUF env var (so a configured-but-no-file
+# TRANSCRIBE_PARAKEET_GGUF env var (so a configured-but-no-file
# build still skips the test cleanly via SKIP_RETURN_CODE 77 instead
# of failing the suite).
@@ -524,7 +524,7 @@ endif()
#
# Same gating as the other real-model tests: built only when
# TRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON; skipped at run time when
-# TRANSCRIBE_REAL_PARAKEET_GGUF is unset. The reference text is
+# TRANSCRIBE_PARAKEET_GGUF is unset. The reference text is
# baked into the test source, not a golden file on disk, so the
# test is hermetic — it does not need a golden generation step.
@@ -727,7 +727,7 @@ endif()
#
# Same gating as the Parakeet real-model tests: built only when
# TRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON. At run time,
-# TRANSCRIBE_COHERE_MODEL must point at a Cohere ASR GGUF. Exits 77
+# TRANSCRIBE_COHERE_GGUF must point at a Cohere ASR GGUF. Exits 77
# (CTest "skipped") when the env var is unset or the file is missing.
if(TRANSCRIBE_BUILD_REAL_MODEL_TESTS)
@@ -841,7 +841,7 @@ endif()
#
# Gated two ways: by TRANSCRIBE_BUILD_REAL_MODEL_TESTS (so the binary
# is not built unless the developer opts in) AND by the runtime
-# TRANSCRIBE_COHERE_MODEL env var (same var that cohere_e2e_smoke.cpp
+# TRANSCRIBE_COHERE_GGUF env var (same var that cohere_e2e_smoke.cpp
# uses, so a single export drives both tests). Skipped via
# SKIP_RETURN_CODE 77 when the env var is unset or the file is
# missing.
diff --git a/tests/cohere_e2e_smoke.cpp b/tests/cohere_e2e_smoke.cpp
index 6b66ad08..13cf54d3 100644
--- a/tests/cohere_e2e_smoke.cpp
+++ b/tests/cohere_e2e_smoke.cpp
@@ -20,7 +20,7 @@
//
// - The CMake option TRANSCRIBE_BUILD_REAL_MODEL_TESTS (default OFF)
// controls whether this binary is built.
-// - At runtime, the GGUF path comes from TRANSCRIBE_COHERE_MODEL
+// - At runtime, the GGUF path comes from TRANSCRIBE_COHERE_GGUF
// env var. If unset, the test exits 77 (CTest "skipped").
// Numerical accuracy validation lives in validate.py; this
// test covers API behavior and structural correctness only.
@@ -140,10 +140,10 @@ int main() {
// ---- Resolve model path ------------------------------------------
std::string model_path;
{
- const char * env = std::getenv("TRANSCRIBE_COHERE_MODEL");
+ const char * env = std::getenv("TRANSCRIBE_COHERE_GGUF");
if (env == nullptr || env[0] == '\0') {
std::fprintf(stderr,
- "cohere_e2e_smoke: TRANSCRIBE_COHERE_MODEL not set. "
+ "cohere_e2e_smoke: TRANSCRIBE_COHERE_GGUF not set. "
"Skipping.\n");
return 77; // CTest "skipped"
}
@@ -153,7 +153,7 @@ int main() {
if (!file_exists(model_path)) {
std::fprintf(stderr,
"cohere_e2e_smoke: model not found: %s\n"
- "Set TRANSCRIBE_COHERE_MODEL to a valid GGUF path. "
+ "Set TRANSCRIBE_COHERE_GGUF to a valid GGUF path. "
"Skipping.\n",
model_path.c_str());
return 77; // CTest "skipped"
diff --git a/tests/cohere_real_smoke.cpp b/tests/cohere_real_smoke.cpp
index 06874e95..0f62e556 100644
--- a/tests/cohere_real_smoke.cpp
+++ b/tests/cohere_real_smoke.cpp
@@ -11,7 +11,7 @@
// - The CMake option TRANSCRIBE_BUILD_REAL_MODEL_TESTS (default OFF)
// controls whether this binary is even built.
// - At runtime, the GGUF path comes from the
-// TRANSCRIBE_COHERE_MODEL environment variable (same var that
+// TRANSCRIBE_COHERE_GGUF environment variable (same var that
// cohere_smoke.cpp uses, so a single export drives both tests).
// If unset, the test exits 77 (CTest "skipped") with a
// regeneration hint.
@@ -120,14 +120,14 @@ cohere_view(const struct transcribe_model * m) {
} // namespace
int main() {
- const char * env = std::getenv("TRANSCRIBE_COHERE_MODEL");
+ const char * env = std::getenv("TRANSCRIBE_COHERE_GGUF");
if (env == nullptr || env[0] == '\0') {
std::fprintf(stderr,
- "cohere_real_smoke: TRANSCRIBE_COHERE_MODEL not "
+ "cohere_real_smoke: TRANSCRIBE_COHERE_GGUF not "
"set; skipping. Convert a real model with:\n"
" uv run scripts/convert-cohere.py "
" --repo-id CohereLabs/cohere-transcribe-03-2026\n"
- "and re-run with TRANSCRIBE_COHERE_MODEL=models/cohere-transcribe-03-2026/cohere-transcribe-03-2026-BF16.gguf\n");
+ "and re-run with TRANSCRIBE_COHERE_GGUF=models/cohere-transcribe-03-2026/cohere-transcribe-03-2026-BF16.gguf\n");
return 77;
}
const std::string fixture = env;
diff --git a/tests/decoder_smoke.cpp b/tests/decoder_smoke.cpp
index 407a2aa4..d993ad80 100644
--- a/tests/decoder_smoke.cpp
+++ b/tests/decoder_smoke.cpp
@@ -26,7 +26,7 @@
// validation.
//
// Gating: built only when TRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON; at run
-// time TRANSCRIBE_REAL_PARAKEET_GGUF must point at a v2 GGUF. The test
+// time TRANSCRIBE_PARAKEET_GGUF must point at a v2 GGUF. The test
// exits 77 (CTest "skipped") when the model path is unset or missing.
//
// The reference text was validated against v2; v3's encoder weights are
@@ -118,10 +118,10 @@ constexpr int k_max_edit_distance = 3;
int main() {
// ---- Resolve env -----------------------------------------------
- const char * gguf_env = std::getenv("TRANSCRIBE_REAL_PARAKEET_GGUF");
+ const char * gguf_env = std::getenv("TRANSCRIBE_PARAKEET_GGUF");
if (gguf_env == nullptr || gguf_env[0] == '\0') {
std::fprintf(stderr,
- "decoder_smoke: TRANSCRIBE_REAL_PARAKEET_GGUF not set; "
+ "decoder_smoke: TRANSCRIBE_PARAKEET_GGUF not set; "
"skipping\n");
return 77;
}
diff --git a/tests/parakeet_real_smoke.cpp b/tests/parakeet_real_smoke.cpp
index 3a9e6303..d2a351a8 100644
--- a/tests/parakeet_real_smoke.cpp
+++ b/tests/parakeet_real_smoke.cpp
@@ -10,7 +10,7 @@
// - The CMake option TRANSCRIBE_BUILD_REAL_MODEL_TESTS (default OFF)
// controls whether this binary is even built.
// - At runtime, the GGUF path comes from the
-// TRANSCRIBE_REAL_PARAKEET_GGUF environment variable. If unset,
+// TRANSCRIBE_PARAKEET_GGUF environment variable. If unset,
// the test exits 77 (CTest "skipped") with a regeneration hint.
//
// CI never builds this — it's a developer-local manual gate. The
@@ -117,14 +117,14 @@ parakeet_view(const struct transcribe_model * m) {
} // namespace
int main() {
- const char * env = std::getenv("TRANSCRIBE_REAL_PARAKEET_GGUF");
+ const char * env = std::getenv("TRANSCRIBE_PARAKEET_GGUF");
if (env == nullptr || env[0] == '\0') {
std::fprintf(stderr,
- "parakeet_real_smoke: TRANSCRIBE_REAL_PARAKEET_GGUF not "
+ "parakeet_real_smoke: TRANSCRIBE_PARAKEET_GGUF not "
"set; skipping. Convert a real model with:\n"
" uv run scripts/convert-parakeet.py "
" /tmp/parakeet.gguf\n"
- "and re-run with TRANSCRIBE_REAL_PARAKEET_GGUF=/tmp/parakeet.gguf\n");
+ "and re-run with TRANSCRIBE_PARAKEET_GGUF=/tmp/parakeet.gguf\n");
return 77;
}
const std::string fixture = env;
diff --git a/tests/whisper_e2e_smoke.cpp b/tests/whisper_e2e_smoke.cpp
index 16a157e9..9505f3ce 100644
--- a/tests/whisper_e2e_smoke.cpp
+++ b/tests/whisper_e2e_smoke.cpp
@@ -59,10 +59,10 @@ bool file_exists(const std::string & path) {
} // namespace
int main() {
- const char * env = std::getenv("TRANSCRIBE_WHISPER_MODEL");
+ const char * env = std::getenv("TRANSCRIBE_WHISPER_GGUF");
if (env == nullptr || env[0] == '\0') {
std::fprintf(stderr,
- "whisper_e2e_smoke: TRANSCRIBE_WHISPER_MODEL not set; skipping.\n");
+ "whisper_e2e_smoke: TRANSCRIBE_WHISPER_GGUF not set; skipping.\n");
return 77;
}
const std::string model_path = env;
diff --git a/tests/whisper_tokenize_parity.cpp b/tests/whisper_tokenize_parity.cpp
index 5e05eb3f..764d2629 100644
--- a/tests/whisper_tokenize_parity.cpp
+++ b/tests/whisper_tokenize_parity.cpp
@@ -12,7 +12,7 @@
// but the text-only BPE pieces tested here sit in the shared prefix
// of both vocabs, so the expected ids match there too.
//
-// Gated by TRANSCRIBE_WHISPER_MODEL (same env var used by
+// Gated by TRANSCRIBE_WHISPER_GGUF (same env var used by
// whisper_e2e_smoke). Exits 77 (cmake SKIP_RETURN_CODE) when unset.
#include "transcribe.h"
@@ -73,10 +73,10 @@ void check_case(const struct transcribe_model * model, const Case & c) {
} // namespace
int main() {
- const char * env = std::getenv("TRANSCRIBE_WHISPER_MODEL");
+ const char * env = std::getenv("TRANSCRIBE_WHISPER_GGUF");
if (env == nullptr || env[0] == '\0') {
std::fprintf(stderr,
- "whisper_tokenize_parity: TRANSCRIBE_WHISPER_MODEL "
+ "whisper_tokenize_parity: TRANSCRIBE_WHISPER_GGUF "
"not set; skipping.\n");
return 77;
}