Spaces:
Running on Zero
Running on Zero
| # Phase 3: Real-Audio Validation Report | |
| ## How real audio got into this sandbox | |
| The user uploaded a real Parquet shard: | |
| `/mnt/user-data/uploads/train-00000-of-00010.parquet` (464 MB, confirmed | |
| via `du`), one of ten training shards of | |
| `pipecat-ai/smart-turn-data-v3.2-train`. Verified as a genuine Parquet | |
| file by its magic bytes (`PAR1` at both start and end) before doing | |
| anything else. | |
| **No `pyarrow`, `fastparquet`, or `datasets` library is installed in this | |
| sandbox, and none are installable** β `pip`/`uv pip install pyarrow` | |
| fails with the same `403 host_not_allowed` block that covers | |
| `huggingface.co` (confirmed again this phase: `pypi.org` returns the | |
| identical proxy denial). So reading this real file required writing a | |
| **pure-Python Parquet reader from scratch**: | |
| `src/turn_detector/parquet_reader.py`. This is the single biggest piece of | |
| new engineering this phase, and it's worth documenting honestly, including | |
| the real bugs it had and how they were caught β because "trust but verify" | |
| matters more than usual when the verification code and the code under test | |
| were written by the same process in the same session. | |
| ## What was built, and the real bugs caught while building it | |
| 1. **Thrift compact-protocol decoder** (generic struct/list/map/binary | |
| reader) β needed because Parquet's footer metadata is Thrift-encoded. | |
| 2. **Footer parser** β extracts schema, row groups, column chunk metadata | |
| (codec, sizes, offsets) from the file's trailing metadata block. | |
| **First real result**: this shard has **3,153 rows across 32 row | |
| groups**, not the ~27,000 I'd have guessed by linearly scaling | |
| 270,946/10 β a good reminder not to estimate when you can just read the | |
| actual footer. | |
| 3. **Hand-written Snappy decompressor** (raw block format) β the shard's | |
| columns are all SNAPPY-compressed (confirmed from the footer, not | |
| assumed). Verified correct via round-trip: decompressed sizes matched | |
| the footer's declared `uncompressed_size` exactly for every page | |
| decoded. | |
| 4. **RLE/bit-packed hybrid decoder** for definition levels (needed because | |
| every column here is `OPTIONAL`/nullable, including scalars). | |
| - **Real bug #1, caught and fixed**: my first version hardcoded a | |
| 1-bit definition-level width for every column. This happened to work | |
| for top-level scalar columns (`language`, `endpoint_bool`, etc.) but | |
| silently corrupted `audio.bytes`, which is nested two levels deep | |
| inside the `OPTIONAL` `audio` struct and therefore needs a 2-bit | |
| definition level (0 = audio missing, 1 = audio present but bytes | |
| null, 2 = value present). The symptom was concrete and easy to | |
| catch: every decoded `audio.bytes` value came back `None`, even | |
| though `language` for the same rows decoded correctly. Fixed by | |
| computing the true max-definition-level per column from a real | |
| schema-tree walk (`_build_max_def_levels`), not an assumption. | |
| - **Real bug #2, caught and fixed**: dictionary-page decoding only | |
| handled `BYTE_ARRAY` values; `spoken_text` (typed `INT32`, entirely | |
| null) hit a dictionary page and raised `ValueError: Unsupported | |
| dictionary value type: INT32`. Fixed by adding an INT32 dictionary | |
| decode path. Re-verified afterward: `spoken_text` decodes cleanly to | |
| 100% `None` across the row group tested β consistent with the | |
| schema-level `dtype: null` fact already confirmed in Phase 1, now | |
| independently re-confirmed by actually decoding real values, not | |
| just trusting the declared schema. | |
| 5. **FLAC STREAMINFO parser** (`parse_flac_streaminfo`) β once | |
| `audio.bytes` decoded correctly, the first 4 bytes were `fLaC`: **the | |
| audio is FLAC, not raw WAV.** Rather than write a full FLAC decoder | |
| (LPC prediction + Rice coding β a much bigger undertaking), I parse | |
| just the 34-byte `STREAMINFO` metadata block to get sample rate, | |
| channel count, and total samples (hence duration) without decoding any | |
| audio β cheap enough to run across the entire 3,153-row shard in a few | |
| seconds. Cross-checked against `ffmpeg`'s own reported duration for a | |
| sample file: **exact match** (7.62775s both ways). | |
| 6. **`ffmpeg`-based audio decoding** for actual PCM samples (real decoding, | |
| not header-only): `ffmpeg` is already installed in this sandbox (unlike | |
| `soundfile`), so `src/turn_detector/audio_io.py` gained an `ffmpeg` | |
| subprocess fallback path, used transparently when `soundfile` is | |
| unavailable. Verified end-to-end: FLAC bytes β `ffmpeg` β WAV β loaded | |
| via the same fallback path β duration matches the STREAMINFO-derived | |
| duration exactly. | |
| Every one of these was tested against the real file at each step, not | |
| just written and assumed correct β see the tool-call history in this | |
| conversation for the actual intermediate outputs (row counts, sample | |
| values, byte-for-byte size checks) that caught bugs #1 and #2 above. | |
| ## Step 1: Full-shard inventory (real, all 3,153 rows) | |
| Computed by fully decoding every scalar metadata column plus | |
| STREAMINFO-derived duration for **every row in the shard** (not a sample β | |
| this was cheap enough, ~5s, to just do exhaustively): | |
| | Property | Value | | |
| |---|---| | |
| | Total rows | 3,153 | | |
| | Corrupted/unreadable rows | **0** | | |
| | `endpoint_bool` | 1,589 True (END) / 1,564 False (CONTINUE) β 50.4%/49.6%, matching the dataset authors' stated 50:50 design target almost exactly | | |
| | Sample rate | 16,000 Hz for all 3,153 rows (uniform) | | |
| | Channels | 1 (mono) for all 3,153 rows (uniform) | | |
| | Duration | min 0.36s, max 30.0s, mean 7.73s, median 7.32s | | |
| | `synthetic` | 2,575 True (82%) / 578 False (18%) | | |
| | `midfiller` | 1,293 True / 1,187 False / **673 None** (null β false β unavailable for those rows) | | |
| | `endfiller` | 810 True / 1,670 False / **673 None** | | |
| **Language distribution (real, full shard, 23 languages present):** | |
| English dominates (789, 25%); Hindi (`hin`) = **132 rows (4.2%)** β a real | |
| measurement, not the ~7.6% rough estimate from Phase 1's 79-row hand | |
| sample (which was too small and non-random to trust for this β this is | |
| exactly why that estimate was labeled provisional at the time). | |
| **Source (`dataset` column) distribution β 12 distinct values, all now | |
| seen** (Phase 1's 79-row sample only ever saw 6 of these): | |
| `chirp3_1` (1,617), `chirp3_2` (852), `liva_1` (386), `midcentury_1` (108), | |
| `mundo_1` (40), `rime_2` (39), `human_5` (35), `orpheus_grammar_1` (24), | |
| `orpheus_endfiller_1` (19), `orpheus_midfiller_1` (13), | |
| `chirp3_3_short` (11), `human_convcollector_1` (9). The `orpheus_*` and | |
| `human_*` sources are new information this phase β not visible in Phase 1's | |
| smaller sample. | |
| ## Step 2: Verify label mapping | |
| Confirmed the same way documented in Phase 1 (upstream contribution guide: | |
| `endpoint_bool=true`βEND, `false`βCONTINUE), plus a real smoke test on 60 | |
| real clips this phase: mean trailing silence for `endpoint_bool=True` | |
| clips was 0.300s vs. 0.114s for `endpoint_bool=False` clips β directionally | |
| consistent with the documented semantics (not proof, but nothing suggests | |
| an inverted or meaningless label). | |
| ## Step 3: Development sample materialized | |
| Stratified reservoir sample (seed=42, `src/turn_detector/data.py`), | |
| target 300, actual 300, stratified across `endpoint_bool` Γ `language` Γ | |
| `dataset` Γ `synthetic` Γ `midfiller` Γ `endfiller` Γ duration bucket. | |
| Materialized as real 16kHz mono WAV files at | |
| `data/raw/phase3_sample/audio/<id>.wav` + `data/raw/phase3_sample/metadata.csv`. | |
| **300/300 clips converted successfully, 0 ffmpeg failures.** | |
| ## Steps 4-6: EXP-001 / EXP-002 / EXP-003(b) results | |
| All **SMALL REAL-AUDIO VALIDATION** β 210 dev / 90 val clips (random | |
| 70/30 split, seed=42), threshold/model tuned on dev only, all metrics | |
| below on held-out val: | |
| | | EXP-001 (energy/silence) | EXP-002 (classical features + LR) | EXP-003b Ξ (temporal features) | | |
| |---|---|---|---| | |
| | Accuracy | 0.567 | 0.622 | global-only 0.511 β both 0.622 | | |
| | F1 | 0.400 | 0.575 | **+0.112** | | |
| | False END rate | 0.309 | 0.400 | **β0.109** (improved) | | |
| | False CONTINUE rate | 0.629 | 0.343 | **β0.114** (improved) | | |
| | Params / size | 2 thresholds | 92 params / 2,920 bytes | same classifier, feature-set ablation | | |
| | Latency (this machine) | negligible (rule-based) | mean 12.2ms / p95 22.4ms per clip | n/a | | |
| **EXP-003b's hypothesis is supported on this validation set**: adding | |
| recent-window (100β1000ms tail) features to the global whole-clip | |
| statistics improved F1 by +0.112 and reduced *both* error rates | |
| simultaneously β not just a precision/recall tradeoff in one direction. | |
| This is a real, if small-sample, result in favor of temporal features | |
| mattering for this task, consistent with what streaming-relevant intuition | |
| would predict. | |
| ## Step 7: Slice / filler analysis (real, on EXP-002's val predictions) | |
| - **By language**: only `eng` (n=22) met the `min_samples=10` bar in the | |
| 90-clip val set; every other language slice (including Hindi, n=3 in | |
| val) was too small and excluded rather than reported as if meaningful. | |
| `eng`-only: accuracy 0.636, F1 0.600. | |
| - **By synthetic flag**: non-synthetic (n=12): accuracy 0.583, F1 0.667. | |
| Synthetic (n=78): accuracy 0.628, F1 0.554. Interesting but n=12 for the | |
| non-synthetic slice is thin β flagged as exploratory, not conclusive. | |
| - **Filler metadata**: 72/90 val rows had `midfiller` populated (18 `None` | |
| β correctly excluded, not treated as "no filler"). `has_midfiller` | |
| (n=39): F1 0.529. `no_midfiller` (n=33): F1 0.593 β filler-present clips | |
| were somewhat harder for EXP-002, consistent with the qualitative error | |
| pattern below. | |
| ## Step 8: Error analysis (real false END / false CONTINUE cases) | |
| Full write-up: `docs/ERROR_ANALYSIS.md`. Headline, real, measured finding: | |
| **16 of 22 (73%) false-END errors have a filler flag present**, versus | |
| 50% for false-CONTINUE errors β and false-END clips have longer mean | |
| trailing silence (0.332s) than false-CONTINUE clips (0.217s). This is | |
| exactly the failure mode the assessment brief names as the hard case: | |
| a pause that acoustically resembles an ending but linguistically isn't | |
| (filler-preceded), which an audio-only, transcript-blind model has no way | |
| to catch beyond acoustic residue. | |
| ## Step 10/11: Decision | |
| **Question:** is the acoustic-only approach promising enough to continue, | |
| per the three-option framework? | |
| **Answer: Option B** β acoustic-only provides useful signal but is | |
| insufficient. Evidence: | |
| - EXP-002 (0.622 accuracy / 0.575 F1) clearly beats EXP-001 (0.567 / 0.400) | |
| and both clearly beat chance-level performance on a roughly balanced | |
| task, so acoustic features are picking up **real** signal β this isn't | |
| noise. | |
| - But 0.622 accuracy on a near-50/50 binary task is not close to | |
| production-usable on its own, and the error analysis identifies a | |
| specific, well-understood, real ceiling: **filler-word-preceded pauses | |
| are systematically harder**, and no acoustic-only feature set can fully | |
| resolve that without something closer to lexical/semantic content β the | |
| exact thing Whisper Tiny's speech representations (not just its | |
| transcription output) are trained to encode implicitly, which is the | |
| actual empirical justification the brief asked for, now grounded in a | |
| real measured failure mode rather than "the brief suggested it." | |
| - EXP-003b gives a concrete, real reason to believe **temporal/recency | |
| features help** (+0.112 F1), which is a design element worth carrying | |
| forward into whichever architecture comes next, Whisper-based or not. | |
| This is 90 validation clips from one of ten shards β a first real signal, | |
| not a final verdict. It's a directionally clear enough result to justify | |
| moving toward EXP-003 (Whisper Tiny) as the next architecture to actually | |
| try, while keeping EXP-002 as the benchmark it needs to beat. | |
| ## One recommended next experiment | |
| **EXP-003: Whisper Tiny encoder representations + the same lightweight | |
| classifier head, evaluated on the identical 210/90 dev/val split used | |
| here**, so the comparison against EXP-002's 0.575 F1 / 0.400 false-END | |
| rate / 0.343 false-CONTINUE rate is apples-to-apples. This directly tests | |
| whether Whisper's representations close the filler-word gap identified in | |
| error analysis, which is the specific, evidenced reason to expect it might | |
| help β not just "the brief suggested Whisper Tiny." | |