<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Panagiotis Gkilis</title>
    <description>The latest articles on DEV Community by Panagiotis Gkilis (@bedvibe_studios).</description>
    <link>https://dev.to/bedvibe_studios</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4057536%2F53ace3a5-ee4f-4cc1-abcb-f85ebf51ddc6.jpg</url>
      <title>DEV Community: Panagiotis Gkilis</title>
      <link>https://dev.to/bedvibe_studios</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bedvibe_studios"/>
    <language>en</language>
    <item>
      <title>I audited my own ML linter and had to withdraw its best evidence</title>
      <dc:creator>Panagiotis Gkilis</dc:creator>
      <pubDate>Mon, 21 Sep 2026 20:54:14 +0000</pubDate>
      <link>https://dev.to/bedvibe_studios/i-audited-my-own-ml-linter-and-had-to-withdraw-its-best-evidence-1g96</link>
      <guid>https://dev.to/bedvibe_studios/i-audited-my-own-ml-linter-and-had-to-withdraw-its-best-evidence-1g96</guid>
      <description>&lt;p&gt;I maintain &lt;a href="https://github.com/Mormolykos/trainproof" rel="noopener noreferrer"&gt;trainproof&lt;/a&gt;, a deterministic linter for ML training runs. No model scores your run — every verdict is a rule that either fires or doesn't, and every finding prints the numbers behind it.&lt;/p&gt;

&lt;p&gt;Before releasing 0.22.0 I put it through two independent forensic audits, one of them working blind. &lt;strong&gt;They took away the single best piece of evidence I had.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The claim
&lt;/h2&gt;

&lt;p&gt;Every fault in my validation gallery is injected on purpose — learning rate cranked 100×, fp16 overflow, shuffled labels. Injected faults are clean, and that's the known weakness of any fault-injection study. So the one log I was proudest of was the one nobody touched: a 9.8-hour Coqui XTTS fine-tune from my own work that, I said, "diverged on its own."&lt;/p&gt;

&lt;p&gt;It was on the README. It was in two places in my long-form write-up. It was the thing that made the study stand on something real.&lt;/p&gt;

&lt;h2&gt;
  
  
  It was computed from a truncated prefix
&lt;/h2&gt;

&lt;p&gt;The log I shipped stops at &lt;strong&gt;step 72,900 of a run that went to 125,039.&lt;/strong&gt; I had truncated it, published the verdict computed from the truncation, and cited the trainer's own bookkeeping as corroboration — the last &lt;code&gt;BEST MODEL&lt;/code&gt; line in the prefix is &lt;code&gt;best_model_49880.pth&lt;/code&gt;, which does sit a long way behind the end of the prefix.&lt;/p&gt;

&lt;p&gt;The rest of the run was still on disk. In the complete log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the trainer promotes &lt;code&gt;best_model_124700.pth&lt;/code&gt; — &lt;strong&gt;step 124,700 of 125,039, 99.7% of the way through&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;all six retained held-out evaluations improve, the last one included: &lt;strong&gt;4.8813 → 2.5894&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bookkeeping I cited as corroboration was corroborating a sentence about the first 58% of a run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What replaces it, exactly
&lt;/h2&gt;

&lt;p&gt;The temptation here is to overcorrect, and that would be the same mistake pointing the other way. So, precisely:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the evidence supports:&lt;/strong&gt; the per-micro-batch training display loss ended above its own minimum. That is a property of that one series.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does not support:&lt;/strong&gt; that the model got worse. And it does &lt;em&gt;not&lt;/em&gt; support that the model got better either — no audio, MOS or perceptual evaluation of that run was kept. Held-out &lt;em&gt;loss&lt;/em&gt; improved. That is all that was measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that matters more than my mistake
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;trainproof still returns FAIL on that run, and 0.22.0 does not fix it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TP-DIVERGE&lt;/code&gt; reads one training series. That run's event file carries two:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;series&lt;/th&gt;
&lt;th&gt;points&lt;/th&gt;
&lt;th&gt;ends&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;per-micro-batch loss&lt;/td&gt;
&lt;td&gt;1,251&lt;/td&gt;
&lt;td&gt;1.88× above its minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;epoch-aggregated loss&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;at&lt;/strong&gt; its minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The reader resolves competing series by density. The 1,251-point series wins; the 5-point series is discarded before any rule sees it. An improving held-out curve sitting in the same file cannot overrule the survivor.&lt;/p&gt;

&lt;p&gt;I could have changed the threshold until this particular run went green. That is tuning a rule against a single example until it produces the answer I now prefer, and it would have been the worst available option. So the run still FAILs, the limitation is written into the README, the rules document and the release's machine-readable record, and it is the top item for the next architecture instead of something quietly smoothed over.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else the audits found
&lt;/h2&gt;

&lt;p&gt;Same principle decided each one: &lt;strong&gt;remove a claim the tool can't support, or make an advertised behaviour real — never strengthen a claim to close a gap.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A missing optional dependency was a verdict about your work.&lt;/strong&gt; No &lt;code&gt;sentencepiece&lt;/code&gt; installed? &lt;code&gt;trainproof tokenizer&lt;/code&gt; exited 1 with a FAIL — the tool reporting &lt;em&gt;its own&lt;/em&gt; missing package as a fault in your tokenizer. Now &lt;code&gt;NOT-CHECKED&lt;/code&gt;, exit 2. FAIL has to mean something about your run, never "the tool had a problem."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The HuggingFace callback could change the run it was watching.&lt;/strong&gt; It read the first batches of labels at &lt;code&gt;on_train_begin&lt;/code&gt;, which means opening a fresh iterator over your training dataloader. With a map-style loader and a random sampler drawing from a generator, that consumes generator state and shifts your batch order. A measurement tool perturbing its own experiment. &lt;code&gt;objective_check&lt;/code&gt; now defaults to &lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four evidence strings described something other than what was measured&lt;/strong&gt; — including a zero-learning-rate finding that printed &lt;code&gt;100.0% of steps have lr=0&lt;/code&gt; for a run where every value was &lt;code&gt;-1e-4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No rule ID, threshold or detection predicate changed in 0.22.0. It adds nothing. 497 tests, each correction pinned by a test that fails against the pre-repair code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm not claiming
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No rule in trainproof is calibrated.&lt;/strong&gt; There is no false-positive rate for any check, because nothing here has been run against a population where the answer is independently known. The gallery is a set of faults I injected on purpose — a regression suite, not a sample.&lt;/p&gt;

&lt;p&gt;What the tool offers is determinism and evidence: the same log gives the same verdict, every finding prints the numbers behind it, and a check that could not run says &lt;code&gt;NOT-CHECKED&lt;/code&gt; instead of &lt;code&gt;PASS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Read a finding's &lt;em&gt;evidence&lt;/em&gt; as the measurement and its &lt;em&gt;message&lt;/em&gt; as an interpretation that may exceed it. That sentence is in the README now because the audits showed it needed to be.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;trainproof
trainproof doctor &lt;span class="nb"&gt;.&lt;/span&gt;   &lt;span class="c"&gt;# discovers and judges every training log it finds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Release 0.22.0 is archived at &lt;a href="https://doi.org/10.5281/zenodo.22882964" rel="noopener noreferrer"&gt;10.5281/zenodo.22882964&lt;/a&gt;. The full write-up, including what the audits found that I haven't covered here, is &lt;a href="https://ai.bedvibe.studio/trainproof/#withdrawn" rel="noopener noreferrer"&gt;on my notebook&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If a deterministic check would have caught a failure that burned your GPU hours, tell me on the repo — it goes in, with credit.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Panagiotis (Panos) Gkilis&lt;/strong&gt; — founder, BedVibe Studios, the operating brand of BED VIBE GKILIS, a sole proprietorship registered in Norway (org. no. 935 267 897).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bedvibe.studio/" rel="noopener noreferrer"&gt;Main hub&lt;/a&gt; · &lt;a href="https://tts.bedvibe.studio/portfolio/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://ai.bedvibe.studio/work/" rel="noopener noreferrer"&gt;Work&lt;/a&gt; · &lt;a href="https://ai.bedvibe.studio/" rel="noopener noreferrer"&gt;Research &amp;amp; articles&lt;/a&gt; · &lt;a href="https://bedvibe.studio/panagiotis-panos-gkilis/" rel="noopener noreferrer"&gt;About&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/panagiotis-gkilis-57995117b/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/Mormolykos" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://orcid.org/0009-0007-3805-170X" rel="noopener noreferrer"&gt;ORCID&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Genuine Human Speech, Called Synthetic. Nothing Was Generated.</title>
      <dc:creator>Panagiotis Gkilis</dc:creator>
      <pubDate>Fri, 18 Sep 2026 03:27:45 +0000</pubDate>
      <link>https://dev.to/bedvibe_studios/genuine-human-speech-called-synthetic-nothing-was-generated-j0e</link>
      <guid>https://dev.to/bedvibe_studios/genuine-human-speech-called-synthetic-nothing-was-generated-j0e</guid>
      <description>&lt;p&gt;I did not set out to test synthetic-speech detectors. I was validating my own voice pipeline and needed to know whether the processing stage changed what a provenance checker would say about the output.&lt;/p&gt;

&lt;p&gt;It did. And it did so on recordings where nothing had been generated at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The premise being tested
&lt;/h2&gt;

&lt;p&gt;A synthetic-speech detector is used as though its score answers one question: &lt;em&gt;was this audio generated?&lt;/em&gt; Those scores are entering forensic, journalistic and platform-moderation settings on that reading. In those settings a false positive is not a benchmark loss — it is a genuine recording being called fake, with consequences for the person who made it.&lt;/p&gt;

&lt;p&gt;Here is the structural problem. Detectors are trained on corpora where synthetic audio and particular signal-processing histories are almost perfectly confounded. Every generated clip in a text-to-speech corpus has passed through a vocoder or a neural codec. Almost no bona fide clip has. A model minimising training loss has no incentive to separate what its training data never separated.&lt;/p&gt;

&lt;p&gt;So I ran the confound backwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design
&lt;/h2&gt;

&lt;p&gt;Hold everything fixed — speaker, words, performance, room, microphone. Apply a benign transformation that produces &lt;strong&gt;no generated speech tokens&lt;/strong&gt;. Measure the paired change in a frozen pretrained detector's score on the same utterance.&lt;/p&gt;

&lt;p&gt;The transformations are deliberately unremarkable and the statistics deliberately conservative. The question is not whether an adversarial perturbation can fool a classifier. Of course it can. The question is whether &lt;em&gt;ordinary processing of genuine speech&lt;/em&gt; is enough to move a deployed one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thirteen detectors, declared and hashed before any of them was scored.&lt;/strong&gt; Four pre-registered experiments on the same 47 utterances from three speakers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;experiment&lt;/th&gt;
&lt;th&gt;transformation&lt;/th&gt;
&lt;th&gt;family size&lt;/th&gt;
&lt;th&gt;Holm survivors&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Detector panel&lt;/td&gt;
&lt;td&gt;neural-codec round trip&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processing 2×2&lt;/td&gt;
&lt;td&gt;denoise / EQ+compression / both&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconstruction ladder&lt;/td&gt;
&lt;td&gt;Griffin-Lim ×4, BigVGAN, Vocos, 2 rate controls&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codec family&lt;/td&gt;
&lt;td&gt;EnCodec 6 kbps, 12 kbps, DAC 44.1 kHz&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two design decisions were fixed in advance and both matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No AUC gate.&lt;/strong&gt; Running the causal test only on detectors that discriminate on this corpus would be circular — by my own hypothesis, discrimination on this corpus is substantially a measure of codec sensitivity, so gating on it would preferentially admit the detectors most likely to show the effect. Every detector ran on every condition regardless of its AUC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A negative control on purpose.&lt;/strong&gt; The panel includes a detector already known to be inverted, so it is visibly not a set of models picked to agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Genuine speech, called synthetic, with zero generation
&lt;/h2&gt;

&lt;p&gt;A codec encode-decode of real studio recordings moved &lt;strong&gt;9 of 13 detectors&lt;/strong&gt; past Holm correction.&lt;/p&gt;

&lt;p&gt;Three of those were &lt;em&gt;fully prospective&lt;/em&gt; — nothing about their behaviour had been observed when the protocol was frozen — and all three moved on &lt;strong&gt;47 of 47 utterances&lt;/strong&gt;, at matched-pairs rank-biserial of exactly −1.000, the maximum a rank statistic can reach.&lt;/p&gt;

&lt;p&gt;It held across three professional microphones capturing the same physical performance simultaneously, so it is not a capture-channel artefact. It held on a speaker outside the corpus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it is not about neural codecs.&lt;/strong&gt; Griffin-Lim — phase retrieval published in 1984, no neural network anywhere — moved &lt;strong&gt;12 of 13&lt;/strong&gt; detectors in the same direction. An ordinary denoise-and-master chain, the kind any production pipeline applies without thinking about it, moved two detectors by −2.95 and −4.22 native units.&lt;/p&gt;

&lt;p&gt;The phenomenon is about &lt;strong&gt;reconstruction and processing history&lt;/strong&gt;, not about codecs being special.&lt;/p&gt;

&lt;h2&gt;
  
  
  There is no best detector to pick
&lt;/h2&gt;

&lt;p&gt;The obvious mitigation is to choose a better detector. The measurements close that door.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;detector family&lt;/th&gt;
&lt;th&gt;codec round trip&lt;/th&gt;
&lt;th&gt;ordinary denoise + EQ + compression&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;codec-trained&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;collapses&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;barely moves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ASVspoof-era&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;collapses&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two families fail on &lt;strong&gt;opposite inputs&lt;/strong&gt;. The ASVspoof-era models could not separate real speech from synthetic speech on this material at all — and ordinary mastering still moved them on 41 and 46 of 47 genuine human recordings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choosing a detector is choosing which false positive to accept, not avoiding one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Which transformation moves which detector tracks each checkpoint's documented training exposure across three architectures. I report that as an &lt;em&gt;association among frozen checkpoints&lt;/em&gt;, not as a causal effect of training data — the checkpoints differ in frontend construction and optimisation as well as in corpus, and no isolating intervention was performed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prediction I got wrong
&lt;/h2&gt;

&lt;p&gt;I predicted that a detector &lt;em&gt;trained on codec audio&lt;/em&gt; would resist a codec pass.&lt;/p&gt;

&lt;p&gt;It collapsed hardest of anything in the panel.&lt;/p&gt;

&lt;p&gt;That prediction is in the frozen pre-registration, so it is in the paper. An independent adversarial audit then ran three rounds against the frozen estate, parsing the stored raw scores directly rather than executing my code. It reproduced every statistic — and &lt;strong&gt;thirteen of my own claims were withdrawn as a result.&lt;/strong&gt; The withdrawals are published, because a corrected claim is more informative than one that was never challenged:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;withdrawn&lt;/th&gt;
&lt;th&gt;replaced by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"the mechanism is analysis–resynthesis"&lt;/td&gt;
&lt;td&gt;no mechanism identified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"any operation that discards phase flips these detectors"&lt;/td&gt;
&lt;td&gt;the tested operations do&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"ASVspoof-era detectors are blind to codecs"&lt;/td&gt;
&lt;td&gt;insensitive to this codec specifically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Griffin-Lim moves the panel harder than any codec"&lt;/td&gt;
&lt;td&gt;EnCodec also reaches median −1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What this does not establish
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detectors are not broken in general.&lt;/strong&gt; The supported claim is narrower: on this material, detector output can be strongly affected by a recording's reconstruction and signal-processing history rather than uniquely identifying synthetic provenance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No mechanism is identified.&lt;/strong&gt; The paper says so explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No listening test was run.&lt;/strong&gt; Nothing here says any transformation is perceptually transparent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three speakers, 47 utterances, neutral speech, one language.&lt;/strong&gt; Enough for a paired effect. Not a population claim.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seven of thirteen detectors come from one research group.&lt;/strong&gt; That is not seven replications.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Not an evasion method
&lt;/h2&gt;

&lt;p&gt;This is a detector-robustness and forensic-attribution result and I do not present it as anything else. No parameter anywhere in this work was chosen, searched or tuned against a detector output.&lt;/p&gt;

&lt;p&gt;The one time an intervention &lt;em&gt;was&lt;/em&gt; tuned to a statistic, it made the audio &lt;strong&gt;more&lt;/strong&gt; separable rather than less. That experiment is retained in the paper deliberately, because a failed attempt that points the opposite way to the hypothesis is worth more than a quiet deletion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that generalises past audio
&lt;/h2&gt;

&lt;p&gt;An instrument's score is not interpretable without knowing the provenance of what it was computed on.&lt;/p&gt;

&lt;p&gt;Before trusting a detector panel, the question worth asking is which detectors on it can do the task on &lt;strong&gt;your&lt;/strong&gt; material at all — and what else, besides generation, moves them. That is the question the companion tool answers, and it is the reason I wrote it as an instrument for measuring detectors rather than as another detector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducibility
&lt;/h2&gt;

&lt;p&gt;The deposit is 26 files: the paper, the frozen per-utterance detector scores, the result and pre-registration freeze records, the five figures, and the two scripts that regenerate the figures and the rank-biserial bootstrap intervals. Both scripts were verified to run &lt;strong&gt;from the deposit alone&lt;/strong&gt; and to reproduce every figure and interval file byte-for-byte.&lt;/p&gt;

&lt;p&gt;The 11.5 GiB audio evidence estate is not released. Speaker and sample identifiers are retained deliberately as provenance under signed contract; only workstation path prefixes are stripped.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Article (canonical):&lt;/strong&gt; &lt;a href="https://ai.bedvibe.studio/called-synthetic/" rel="noopener noreferrer"&gt;https://ai.bedvibe.studio/called-synthetic/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DOI:&lt;/strong&gt; &lt;a href="https://doi.org/10.5281/zenodo.22819223" rel="noopener noreferrer"&gt;https://doi.org/10.5281/zenodo.22819223&lt;/a&gt; — all versions: &lt;a href="https://doi.org/10.5281/zenodo.22819222" rel="noopener noreferrer"&gt;https://doi.org/10.5281/zenodo.22819222&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CC BY 4.0.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>audio</category>
      <category>security</category>
    </item>
    <item>
      <title>Three of Twenty Decoders Actually Stream, and My Quality Metric Was Beaten by an Algorithm from 1984</title>
      <dc:creator>Panagiotis Gkilis</dc:creator>
      <pubDate>Thu, 17 Sep 2026 12:24:44 +0000</pubDate>
      <link>https://dev.to/bedvibe_studios/three-of-twenty-decoders-actually-stream-and-my-quality-metric-was-beaten-by-an-algorithm-from-1984-5e77</link>
      <guid>https://dev.to/bedvibe_studios/three-of-twenty-decoders-actually-stream-and-my-quality-metric-was-beaten-by-an-algorithm-from-1984-5e77</guid>
      <description>&lt;p&gt;If you are building streaming text-to-speech, you have to pick a decoder — the thing that turns a representation into a waveform. I tried to pick one from the literature and could not, so I measured twenty of them under one frozen protocol on identical audio.&lt;/p&gt;

&lt;p&gt;Three things came out of it. Two are about the decoders. The most useful one is about my own measuring stick, which failed a test I had written in advance specifically to catch it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with the published record
&lt;/h2&gt;

&lt;p&gt;Real-time factors quoted without naming the hardware. "Streaming" claimed for architectures that need the whole utterance before they emit anything. Quality figures from different corpora, at different sample rates, through different measurement paths. Each number is probably fine on its own. None of them compose into a decision.&lt;/p&gt;

&lt;p&gt;So: one GPU, one corpus, one protocol, twenty arms. &lt;strong&gt;Representation → waveform only.&lt;/strong&gt; Generator and autoregressive timing are a different question, and a decode-only number reported as end-to-end TTS performance is a failed report.&lt;/p&gt;

&lt;p&gt;One framing decision changed the candidate set before any measurement happened. I nearly excluded several codecs for not being causal — then realised &lt;strong&gt;only the decoder needs to be causal for a TTS build&lt;/strong&gt;. The encoder runs at training time on complete utterances; at inference the generator emits tokens directly and no encoder sits in the serving path. NVIDIA's NanoCodec makes it concrete: non-causal encoder, causal HiFi-GAN decoder. "Is this codec causal?" is the wrong question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five gates, each frozen before it ran
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;gate&lt;/th&gt;
&lt;th&gt;question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 — candidate&lt;/td&gt;
&lt;td&gt;which decoders are runnable, licensed and comparable at all?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 — functionality&lt;/td&gt;
&lt;td&gt;does it produce structurally valid audio?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 — streaming&lt;/td&gt;
&lt;td&gt;does it &lt;em&gt;truly&lt;/em&gt; stream, or is it stateless chunking?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 — performance&lt;/td&gt;
&lt;td&gt;latency, real-time factor, first-audio, jitter, memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 — quality&lt;/td&gt;
&lt;td&gt;what does the round trip cost the signal?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5 — identity&lt;/td&gt;
&lt;td&gt;does the speaker survive, and do they stay one person?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The protocol for each gate was written and hashed before the data it governs existed. Two later freezes were not prospective — Gate 4's analysis layer and three revisions of the Q5 spec were post-measurement repairs, and the records say so rather than pretending otherwise. That distinction matters more than it sounds like it does, and I got it wrong in the first published version of this work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 2: "streaming" is a property almost nothing has
&lt;/h2&gt;

&lt;p&gt;An arm counts as truly incremental only if &lt;strong&gt;both&lt;/strong&gt; hold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;stateful chunked decode reproduces full-context output to &lt;code&gt;max|err| ≤ 1e-3&lt;/code&gt;, &lt;strong&gt;and&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;state is load-bearing — stateless error is at least 10× the stateful error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second condition exists because the first can be satisfied by an arm that ignores state completely. If you only check condition 1, a stateless chunker passes and you ship it believing it streams.&lt;/p&gt;

&lt;p&gt;Of twenty arms: &lt;strong&gt;three&lt;/strong&gt; are truly incremental, all three explicitly causal FocalCodec configurations. &lt;strong&gt;Fifteen&lt;/strong&gt; are stateless chunking. One streams its network but never emits audio from partial input. One was blocked on platform and reports no number at all rather than a fabricated one.&lt;/p&gt;

&lt;p&gt;The three that pass do so with state ratios of &lt;strong&gt;3,530 to 36,740&lt;/strong&gt; against a threshold of 10. State is not marginally load-bearing there; it is decisive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The negative controls are what make that mean anything.&lt;/strong&gt; The non-causal FocalCodec configurations run through the &lt;em&gt;identical&lt;/em&gt; stateful code path. Their ratio is exactly &lt;strong&gt;1&lt;/strong&gt; — carrying state changes nothing. If a negative control had passed, the experiment would have been void, not the arm promoted.&lt;/p&gt;

&lt;p&gt;Several of the fifteen never claimed to stream. The classification measures what they do; it is not an accusation about what they advertised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 3: speed is not validity
&lt;/h2&gt;

&lt;p&gt;First-audio latency (TTFA) across the arms that have one spans &lt;strong&gt;1.37 ms to 27.64 ms&lt;/strong&gt; at the ~80 ms chunk anchor. MelFlow does not have one: it emits spectrogram frames, and producing playable PCM from a partial stream needs an overlap-add stage this study records as not established. Its 375.91 ms is a &lt;strong&gt;steady-state median&lt;/strong&gt;, a different quantity, reported separately and never mixed into the TTFA range. (I mixed them in v1.0.0. More on that below.)&lt;/p&gt;

&lt;p&gt;Real-time margin spans &lt;strong&gt;64×&lt;/strong&gt; down to &lt;strong&gt;0.2×&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But two arms produce streamed output that fails the study's own validity gate at &lt;em&gt;every&lt;/em&gt; chunk size tested. A third fails at the anchor and passes at larger sizes — its smallest viable configuration is &lt;strong&gt;640.8 ms&lt;/strong&gt;, not 80 ms.&lt;/p&gt;

&lt;p&gt;The sharpest case: &lt;code&gt;vocos_mel24&lt;/code&gt; has the &lt;strong&gt;fastest first-audio in the whole study and no achievable streaming configuration at all&lt;/strong&gt;. Its timing numbers are real. What they timed is not a valid streaming configuration. If your benchmark reports latency without a validity gate, this is the arm that wins it.&lt;/p&gt;

&lt;p&gt;One more result governs how any of these numbers may be read. A controlled probe — byte-identical tokens, same GPU, one arm run in every environment — found a &lt;strong&gt;50.2% difference attributable purely to the &lt;code&gt;transformers&lt;/code&gt; version&lt;/strong&gt;, device-side, at identical torch. So one arm in this study may not be compared numerically against the others at all. That is not a statement about whether it is fast. It is a statement that the comparison does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 4: the metric failed the test I wrote to catch it
&lt;/h2&gt;

&lt;p&gt;This is the part worth your time even if you never touch audio.&lt;/p&gt;

&lt;p&gt;Gate 4 ranks on one metric, mel-cepstral distance, chosen after nine variants were implemented and attacked. Alongside it, the pre-registration installed a &lt;strong&gt;validity instrument&lt;/strong&gt;: put Griffin-Lim in the panel. Griffin-Lim is a phase-retrieval algorithm from 1984 with &lt;strong&gt;zero trained parameters&lt;/strong&gt;. The pre-registration stated in advance what it would mean if Griffin-Lim scored close to the trained neural codecs — &lt;em&gt;a finding about the metric, not about the decoders&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It did not score close. It scored &lt;strong&gt;best, in six states of six&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The obvious explanation was tested and refuted. The hypothesis that the metric's low-level frame floor favours a magnitude-matching algorithm failed: splitting each cell at its own median energy, Griffin-Lim leads in &lt;strong&gt;both&lt;/strong&gt; halves, and by &lt;em&gt;more&lt;/em&gt; in the loud half.&lt;/p&gt;

&lt;p&gt;What remains is an explanation consistent with the result rather than a demonstrated mechanism, and the difference is worth being careful about. Mel-cepstral distance here is derived from the magnitude spectrum and is blind to phase by construction. Griffin-Lim iterates toward consistency of the &lt;em&gt;linear STFT magnitude&lt;/em&gt; and optimises nothing else, accepting whatever phase error that leaves. Aligned in domain — but &lt;strong&gt;not the same quantity&lt;/strong&gt;, and nothing in this package demonstrates that Griffin-Lim minimises the released cepstral distance exactly.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;is&lt;/em&gt; established does not depend on the mechanism: &lt;strong&gt;a metric whose ranking puts an untrained phase-retrieval algorithm first is not, on its own, a sufficient authority for cross-decoder quality.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the whole lesson, and it generalises well past audio. The pre-registration naming this outcome in advance is the only reason it reads as a finding rather than an embarrassment — and the reason no metric was swapped in afterwards to produce a tidier table. If you are choosing anything on a published quality number, the question worth asking first is whether that metric has ever been shown a floor it should beat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q5: does the speaker survive?
&lt;/h2&gt;

&lt;p&gt;A single similarity score conflates two different failures, so this measures them separately. &lt;strong&gt;Retention&lt;/strong&gt;: how close a reconstruction sits to its own source. &lt;strong&gt;Dispersion&lt;/strong&gt;: how tightly &lt;em&gt;different&lt;/em&gt; reconstructions of the same speaker cluster. A decoder can move a voice consistently — a stable new identity — or make the speaker wander. Those are not the same problem and they do not have the same fix.&lt;/p&gt;

&lt;p&gt;Three separately calibrated speaker encoders, chosen mechanically from a previously frozen fourteen-encoder study before any of these numbers existed. &lt;strong&gt;Cosine values are encoder-specific and never averaged.&lt;/strong&gt; The three encoders show broadly similar ordering with some pairwise reversals, which is exactly why a bare cosine without its encoder name is not a result.&lt;/p&gt;

&lt;p&gt;The streaming detector compares each arm against &lt;strong&gt;its own offline decode&lt;/strong&gt; on the same recording, so no ceiling or floor enters and no cross-arm ranking is implied. Two distinct quantities, never quoted as one range: &lt;strong&gt;S1&lt;/strong&gt;, the offline↔streamed cosine, and &lt;strong&gt;S2&lt;/strong&gt;, the paired retention delta.&lt;/p&gt;

&lt;p&gt;For the three causal arms, across all three encoders: S1 &lt;strong&gt;0.999866–0.999946&lt;/strong&gt;, S2 &lt;strong&gt;−3.9×10⁻⁵ to −5.3×10⁻⁶&lt;/strong&gt;. A near-zero median additional change, under the tested encoders, supported recordings and imposed chunking regime. Small, and consistently negative — this study establishes no minimum detectable change and no equivalence threshold, so that is not a claim that the difference is below what the instrument can see.&lt;/p&gt;

&lt;p&gt;The other thirteen lose &lt;strong&gt;−0.078 to −0.742 on S2&lt;/strong&gt;, which is nowhere near that scale. For one arm the offline-versus-streamed cosine itself reaches &lt;strong&gt;−0.013&lt;/strong&gt;: the streamed output is essentially orthogonal to that arm's own offline decode &lt;em&gt;in this embedding space&lt;/em&gt;. That says nothing about waveform or linguistic relationships, which were not measured.&lt;/p&gt;

&lt;p&gt;This agrees with Gate 4's separate streaming detector — an aligned maximum-error detector on the waveform — which found the same three arms at &lt;strong&gt;0.69–0.74%&lt;/strong&gt; error where every other arm sat at 105–225%. Two measurements sharing no implementation, the same three survivors.&lt;/p&gt;

&lt;p&gt;They do share their input: the same reconstructions of the same corpus. So the agreement is &lt;strong&gt;corroborating evidence&lt;/strong&gt;, not proof. Two implementations agreeing makes an independent error in both less likely; it cannot rule out an error in either, and it says nothing about an artefact of the material both were computed on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not establish
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Perceptual quality was never measured.&lt;/strong&gt; No listening test was run. Which decoder &lt;em&gt;sounds&lt;/em&gt; best is not established and no sentence here implies it. Two metrics ranked a phase-blind algorithm first; both are blind to the same thing.&lt;/li&gt;
&lt;li&gt;The identity result is about &lt;strong&gt;speaker-encoder representations&lt;/strong&gt;, not about how audio sounds or about cloning quality.&lt;/li&gt;
&lt;li&gt;A large majority of cells fall outside the quality metric's validated support domain. Support is carried as a covariate, never as a silent exclusion.&lt;/li&gt;
&lt;li&gt;For one decoder family, dispersion and band-limiting &lt;strong&gt;cannot be separated&lt;/strong&gt; in this design, so no causal claim is made about it.&lt;/li&gt;
&lt;li&gt;Seven speakers, one studio, one recording chain, English. Absolute values are not comparable to VoxCeleb-scale benchmarks.&lt;/li&gt;
&lt;li&gt;Thirteen arms were driven with chunked context they were never designed for. The result is the cost of streaming a decoder that was not built to stream — not a defect report.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part I got wrong, and why it is in the article
&lt;/h2&gt;

&lt;p&gt;Six rounds of independent adversarial review ran against this work. Four before publication found three defects that &lt;strong&gt;changed conclusions&lt;/strong&gt; — a reference that admitted pairs the measurement forbade, a ceiling measured on the wrong duration, and a pair-grouping convention that was order-dependent.&lt;/p&gt;

&lt;p&gt;Then a fifth round ran &lt;strong&gt;after publication, against the published package&lt;/strong&gt;, and found what four earlier rounds structurally could not: they had all audited the private research tree, not what shipped from it.&lt;/p&gt;

&lt;p&gt;All three of my advertised public reproduction commands failed from a clean copy of the release. The scripts resolved their data relative to where they sat in my private tree — one directory above the artifacts — and the release ships them flat. The claim audit opened a deliberately withheld file unconditionally. And the verifier I pointed readers at was the manifest of the &lt;em&gt;private&lt;/em&gt; tree, which cannot verify a redacted package and duly reported 26 of 32 artifacts as drift. A first-audio range had borrowed a steady-state number for its upper end. Three sentences claimed more than the measurements carry.&lt;/p&gt;

&lt;p&gt;A sixth round then attacked the correction itself and found five more, two of them claims in the &lt;em&gt;repair&lt;/em&gt; that outran their evidence in the same way as the originals.&lt;/p&gt;

&lt;p&gt;The pattern has now recurred six times in different disguises: &lt;strong&gt;verification that checks what is present rather than what is required.&lt;/strong&gt; The estimator without its reference. The instrument without its evidence. The manifest without its inventory. A package that verified its own private origin rather than what it shipped. A privacy scanner that listed the paths it knew about instead of declaring what counts as private. A verifier that printed its own integrity fingerprint without ever comparing it to anything.&lt;/p&gt;

&lt;p&gt;That last one is my favourite, because it looks like working code. It computed the hash. It printed the hash. It printed the recorded hash right underneath. It never compared them, so corrupting the fingerprint exited 0. &lt;strong&gt;Printing a number beside another number is not a check. Only a comparison that can fail is one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All of it is corrected in v1.0.1, itemised with the original wording beside the corrected wording. &lt;strong&gt;No measurement changed&lt;/strong&gt; — 51,495 non-string scalar leaves were compared across every JSON artifact common to both versions, and none differ. v1.0.0 stays permanently available, because deleting the version that was wrong would be the same mistake in a different disguise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it
&lt;/h2&gt;

&lt;p&gt;Three commands run from the public package with no private input: regenerate every table, re-trace every headline claim to the artifact it came from, and re-hash the bytes that shipped.&lt;/p&gt;

&lt;p&gt;That is a &lt;strong&gt;consistency check&lt;/strong&gt;, and the README now says so rather than overclaiming. It is not independent reconstruction — rebuilding the aggregates and their confidence intervals needs the withheld per-recording rows and speaker embeddings, which are not published because an embedding of a named consenting human is biometric data. One published claim is marked &lt;code&gt;WITHHELD&lt;/code&gt; rather than &lt;code&gt;PASS&lt;/code&gt; in the public audit for exactly that reason. Source audio, embeddings and the 35.65 GB of reconstructions stay private; the reconstructions are retained because human listening and adversarial detectability are declared later phases.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Article (canonical):&lt;/strong&gt; &lt;a href="https://ai.bedvibe.studio/decoder-benchmark/" rel="noopener noreferrer"&gt;https://ai.bedvibe.studio/decoder-benchmark/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paper + code + full audit trail:&lt;/strong&gt; &lt;a href="https://github.com/Mormolykos/decoder-vocoder-benchmark" rel="noopener noreferrer"&gt;https://github.com/Mormolykos/decoder-vocoder-benchmark&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/Mormolykos/decoder-vocoder-benchmark/releases/tag/v1.0.1" rel="noopener noreferrer"&gt;https://github.com/Mormolykos/decoder-vocoder-benchmark/releases/tag/v1.0.1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DOI (v1.0.1):&lt;/strong&gt; &lt;a href="https://doi.org/10.5281/zenodo.22811349" rel="noopener noreferrer"&gt;https://doi.org/10.5281/zenodo.22811349&lt;/a&gt; — all versions: &lt;a href="https://doi.org/10.5281/zenodo.22798415" rel="noopener noreferrer"&gt;https://doi.org/10.5281/zenodo.22798415&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corrections:&lt;/strong&gt; &lt;a href="https://github.com/Mormolykos/decoder-vocoder-benchmark/blob/main/CORRECTIONS_v1.0.1.md" rel="noopener noreferrer"&gt;https://github.com/Mormolykos/decoder-vocoder-benchmark/blob/main/CORRECTIONS_v1.0.1.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CC BY 4.0.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>audio</category>
      <category>testing</category>
    </item>
    <item>
      <title>The Compiler Got 5% Slower. The Benchmark Called It a 10% Regression a Quarter of the Time.</title>
      <dc:creator>Panagiotis Gkilis</dc:creator>
      <pubDate>Fri, 04 Sep 2026 20:43:20 +0000</pubDate>
      <link>https://dev.to/bedvibe_studios/the-compiler-got-5-slower-the-benchmark-called-it-a-10-regression-a-quarter-of-the-time-50pp</link>
      <guid>https://dev.to/bedvibe_studios/the-compiler-got-5-slower-the-benchmark-called-it-a-10-regression-a-quarter-of-the-time-50pp</guid>
      <description>&lt;p&gt;A benchmark suite is an instrument. You point it at two versions of a compiler, it tells you whether the new one got worse, and somebody merges or reverts a pull request on the strength of that answer.&lt;/p&gt;

&lt;p&gt;If the compiler is stochastic and the suite does not pin its seed, that answer is a draw from a distribution nobody measured.&lt;/p&gt;

&lt;p&gt;I measured the distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding
&lt;/h2&gt;

&lt;p&gt;At the pinned revision I inspected, IBM's Benchpress fixes the seeds that &lt;strong&gt;build&lt;/strong&gt; its circuits — &lt;code&gt;seed=12345&lt;/code&gt;, fourteen occurrences — and passes no &lt;code&gt;seed_transpiler&lt;/code&gt; to the code that &lt;strong&gt;compiles&lt;/strong&gt; them. Circuit construction is deterministic; compilation is not.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;bv_n140&lt;/code&gt; mapped to a heavy-hex lattice, going from Qiskit 1.4.3 to 2.0.0, across &lt;strong&gt;400 seeds per version and 21 OS processes&lt;/strong&gt; — a separate measurement from the pre-registered experiment below, and &lt;strong&gt;not part of the pre-registered endpoint&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;quantity&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;th&gt;95% interval&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;long-run mean change&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+5.37%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+4.27% to +6.50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;called a ≥10% regression, 3 runs/version&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;24.4%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;19.5% to 31.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The true change is nowhere near the threshold. The verdict crosses it a quarter of the time anyway.&lt;/p&gt;

&lt;p&gt;I did not trust that at first, because all 200 original seeds came from one contiguous block inside one process — so per-process state, hash randomisation and allocator layout were all held constant. A &lt;strong&gt;disjoint&lt;/strong&gt; set of 200 seeds spanning 5.9 million to 1.08 billion, across ten fresh processes with differing &lt;code&gt;PYTHONHASHSEED&lt;/code&gt;, gave &lt;strong&gt;22.6%&lt;/strong&gt; [16.4, 30.6] against &lt;strong&gt;26.3%&lt;/strong&gt; [18.5, 34.2] from the contiguous set. Each point falls inside the other's interval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it more does not fix it
&lt;/h2&gt;

&lt;p&gt;The obvious response is "run it more times". Pooling all 400 seeds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;runs per version&lt;/th&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;3&lt;/th&gt;
&lt;th&gt;5&lt;/th&gt;
&lt;th&gt;8&lt;/th&gt;
&lt;th&gt;10&lt;/th&gt;
&lt;th&gt;20&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;false-positive rate&lt;/td&gt;
&lt;td&gt;34.58%&lt;/td&gt;
&lt;td&gt;24.38%&lt;/td&gt;
&lt;td&gt;18.56%&lt;/td&gt;
&lt;td&gt;12.94%&lt;/td&gt;
&lt;td&gt;10.36%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.74%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Twenty runs per version is 20 × 2 versions × roughly two hours per suite run — the issue's own figure, which I could not independently confirm — so &lt;strong&gt;on the order of 80 compute hours&lt;/strong&gt;. It still leaves 3.74%.&lt;/p&gt;

&lt;p&gt;Fixing &lt;code&gt;seed_transpiler&lt;/code&gt; removes the variance &lt;em&gt;attributable to the transpiler seed&lt;/em&gt; at a single run. It does &lt;strong&gt;not&lt;/strong&gt; make one run correct. A fixed seed makes the answer repeatable, including when that answer is wrong: on &lt;code&gt;bv_n280&lt;/code&gt;, recorded seed 663193 gives A = 1040 against B = 1157 — a +11.25% call against a measured θ of +4.89%. Frozen, reproducible, and the wrong side of the threshold.&lt;/p&gt;

&lt;p&gt;Taking the &lt;strong&gt;minimum&lt;/strong&gt; of three runs instead of the mean — a common instinct — makes it worse. Every rate roughly doubles.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pre-registered part
&lt;/h2&gt;

&lt;p&gt;One circuit is an anecdote. So I wrote down the selection rule, the analysis, the endpoint and the labels and committed them &lt;strong&gt;before the first tracked commit of the 39-circuit raw dataset&lt;/strong&gt;. The commit ordering is checkable in the repository.&lt;/p&gt;

&lt;p&gt;What that ordering supports is a &lt;em&gt;frozen follow-up design after exploration&lt;/em&gt;, not blindness to earlier outcomes — and the paper corrects that distinction twice. A census and exploratory results &lt;strong&gt;preceded&lt;/strong&gt; the pre-registration: an earlier commit the same day already identifies heavy-hex as the worst case, and the ≤10 s selection rule is computed from that pre-existing census. Git records when files were committed; it does not record when their outputs were first looked at.&lt;/p&gt;

&lt;p&gt;The rule selected every &lt;code&gt;qasmbench-large&lt;/code&gt; circuit whose twelve-seed heavy-hex runtime in that census was under ten seconds: &lt;strong&gt;39 circuits, 200 recorded seeds per arm, 15,600 primary transpilations.&lt;/strong&gt; 36 resolved; 26 were eligible after pre-registered boundary exclusions.&lt;/p&gt;

&lt;p&gt;Over those 26:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;12/26&lt;/strong&gt; met the pre-registered endpoint — a risk interval excluding zero&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7/26&lt;/strong&gt; carry a risk of at least 5%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4/26&lt;/strong&gt; carry a risk of at least 10%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The median circuit admits an &lt;strong&gt;ambiguity band of about 10.9 percentage points&lt;/strong&gt; — the width of the interval of true change over which the three-run rule's call probability runs from 5% to 95%. That band is built from the measured residuals of &lt;em&gt;this&lt;/em&gt; version pair under a stated model. It is not claimed to be version-pair-independent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I withdrew
&lt;/h2&gt;

&lt;p&gt;That 46.2% headline rate is the number I trust least, so I want to say why rather than let someone find it.&lt;/p&gt;

&lt;p&gt;The 26 circuits are not 26 independent observations. They come from eleven algorithm families and within a family the results are nearly all-hit or all-miss. A cluster bootstrap over families gives &lt;strong&gt;[17.4%, 81.0%]&lt;/strong&gt; against the Wilson interval's [28.8%, 64.5%]. The effective sample size is far below 26, so no interval I can compute is informative about a population of circuits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The suite-level rate is withdrawn.&lt;/strong&gt; 12/26 stands as a descriptive count of these 26 circuits and nothing more.&lt;/p&gt;

&lt;p&gt;Three other claims went the same way. One was a correlation of ρ = +0.876 between "compilation is stochastic" and risk, which I had presented as a discovered mechanism. Circuits whose two arms are constant have zero risk &lt;em&gt;by arithmetic&lt;/em&gt;, so a correlation computed over a sample containing them measures a definition. Simulated data containing no compiler at all reproduces the statistic.&lt;/p&gt;

&lt;p&gt;All four withdrawals are still in the repository, with the evidence that defeated them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking the checker
&lt;/h2&gt;

&lt;p&gt;The paper argues that measurement apparatus should be checked rather than trusted, so it would be poor form not to turn that on myself.&lt;/p&gt;

&lt;p&gt;The repository ships a thirteen-stage read-only verifier and a suite of &lt;strong&gt;44 deliberate corruptions&lt;/strong&gt;, each of which must be rejected by a named stage for a named reason — not merely cause a nonzero exit. The paper's own account of the review of its &lt;em&gt;claims&lt;/em&gt; is four rounds of hostile review plus two first-principles audits, which removed five claims from the work. Every audit round after that attacked the apparatus instead. Every one of them ended with a green verifier that the next reader then broke, and not one of them changed a scientific number. What they broke was the checking.&lt;/p&gt;

&lt;p&gt;The best example: a claim scan that could not see the sentence &lt;em&gt;"Seven of the twenty-six eligible circuits have an interval that excludes zero"&lt;/em&gt; — because &lt;code&gt;twenty-six&lt;/code&gt; was not a number it recognised, so no claim object was ever created and the fail-closed classifier was never asked. Four rounds of widening a recogniser lost to the fifth spelling. The fix was to stop asking "can we recognise every English sentence that means 7 of 26" and instead register every reader-visible unit of the frozen manuscript by hash, so an unregistered block is refused before anything reads a word of it.&lt;/p&gt;

&lt;p&gt;That is evidence the verifier can turn red. It is not a proof that nothing gets through, and the paper says so in those words.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not say
&lt;/h2&gt;

&lt;p&gt;It does not say Qiskit's transpiler got worse, or that Benchpress is a bad suite, or that any published decision was wrong. It measures one property of one benchmarking protocol: an unseeded stochastic compiler, sampled three times, produces regression verdicts that disagree with its own long-run behaviour at rates that are not small.&lt;/p&gt;

&lt;p&gt;Limits, stated plainly: one SDK, one version pair, one topology family, one machine. The +10% threshold is mine — Benchpress defines none. &lt;code&gt;seed_transpiler&lt;/code&gt; is not a universal remedy either: it removed false positives on the circuits I tested but was &lt;em&gt;worse&lt;/em&gt; on three of the four showing false negatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you maintain a benchmark like this
&lt;/h2&gt;

&lt;p&gt;The cheap version of the check: run the same comparison twenty times without changing anything, and look at the spread of &lt;strong&gt;verdicts&lt;/strong&gt;, not the spread of values. If the verdict moves, the number of runs is part of your instrument and it belongs in the write-up.&lt;/p&gt;

&lt;p&gt;Version 4 is the audited release. Paper, code, raw per-seed data and the assurance apparatus:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DOI — &lt;a href="https://doi.org/10.5281/zenodo.22848938" rel="noopener noreferrer"&gt;10.5281/zenodo.22848938&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tagged release — &lt;a href="https://github.com/Mormolykos/qvalidation/releases/tag/v4.0.0" rel="noopener noreferrer"&gt;github.com/Mormolykos/qvalidation @ v4.0.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Full write-up — &lt;a href="https://ai.bedvibe.studio/decision-risk/" rel="noopener noreferrer"&gt;ai.bedvibe.studio/decision-risk/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CC-BY-4.0.&lt;/p&gt;

</description>
      <category>quantum</category>
      <category>python</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Assumed My Retriever Failed at Stage One. The Bigger Failure Was at Stage Three.</title>
      <dc:creator>Panagiotis Gkilis</dc:creator>
      <pubDate>Mon, 31 Aug 2026 16:38:31 +0000</pubDate>
      <link>https://dev.to/bedvibe_studios/i-assumed-my-retriever-failed-at-stage-one-the-bigger-failure-was-at-stage-three-2k0k</link>
      <guid>https://dev.to/bedvibe_studios/i-assumed-my-retriever-failed-at-stage-one-the-bigger-failure-was-at-stage-three-2k0k</guid>
      <description>&lt;p&gt;Hierarchical retrieval is supposed to help on long documents. Pick the right chapter, then search inside it. Narrow the haystack, then find the needle.&lt;/p&gt;

&lt;p&gt;On my long-book benchmark it came &lt;strong&gt;third of five&lt;/strong&gt;, behind a chapter-summary chain and flat chunk RAG, and only barely ahead of naively grabbing the end of the book.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;method&lt;/th&gt;
&lt;th&gt;context precision&lt;/th&gt;
&lt;th&gt;context recall&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;naive_first_context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.1475&lt;/td&gt;
&lt;td&gt;0.1458&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;naive_last_context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.4150&lt;/td&gt;
&lt;td&gt;0.3302&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flat_chunk_rag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.3375&lt;/td&gt;
&lt;td&gt;0.4302&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chapter_summary_chain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.4000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.4771&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hierarchical_book_rag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.3475&lt;/td&gt;
&lt;td&gt;0.3365&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My hypothesis was &lt;strong&gt;error compounding&lt;/strong&gt;: if stage one picks the wrong chapter, every later stage searches the wrong text. That turned out to be true and incomplete — which is worse than being wrong, because it would have sent me to fix the right thing and stop there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ablation
&lt;/h2&gt;

&lt;p&gt;Same corpus (240,767 words), same 80 gold questions, five variants, 400 scored rows. Two of the variants are &lt;em&gt;given&lt;/em&gt; the correct chapter — diagnostics, not deployable methods — purely to measure how much is lost before versus after chapter selection.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;method&lt;/th&gt;
&lt;th&gt;recall&lt;/th&gt;
&lt;th&gt;precision&lt;/th&gt;
&lt;th&gt;hit@1&lt;/th&gt;
&lt;th&gt;hit@3&lt;/th&gt;
&lt;th&gt;hit@5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hier_current&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.3365&lt;/td&gt;
&lt;td&gt;0.3475&lt;/td&gt;
&lt;td&gt;0.2000&lt;/td&gt;
&lt;td&gt;0.3875&lt;/td&gt;
&lt;td&gt;0.4375&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hier_no_neighbors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.4771&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.4000&lt;/td&gt;
&lt;td&gt;0.2000&lt;/td&gt;
&lt;td&gt;0.3875&lt;/td&gt;
&lt;td&gt;0.4375&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chapter_summary_chain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.4771&lt;/td&gt;
&lt;td&gt;0.4000&lt;/td&gt;
&lt;td&gt;0.2000&lt;/td&gt;
&lt;td&gt;0.3875&lt;/td&gt;
&lt;td&gt;0.4375&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hier_oracle_chapter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.7844&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.6796&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hier_oracle_chapter_neighbors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.7688&lt;/td&gt;
&lt;td&gt;0.6925&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Deleting a feature closed the entire gap.&lt;/strong&gt; &lt;code&gt;hier_no_neighbors&lt;/code&gt; lands on 0.4771 / 0.4000 — the same numbers as the baseline, to four decimals. The method I thought was structurally worse wasn't worse. It was carrying a stage that was hurting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And look at hit@k.&lt;/strong&gt; Identical across the first three rows. Chapter selection did not change at all between &lt;code&gt;hier_current&lt;/code&gt; and &lt;code&gt;hier_no_neighbors&lt;/code&gt; — same code. Every point of recall that moved, moved downstream, in the stage that pads results with adjacent chunks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting failures directly
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;failure type&lt;/th&gt;
&lt;th&gt;count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;neighbor_dilution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;32&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wrong_chapter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;27&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;right_chapter_wrong_chunk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ok&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My hypothesis accounted for 27 questions. The stage I hadn't suspected accounted for 32. I would have fixed chapter routing, seen a real improvement, and never looked at expansion — because the improvement would have confirmed the theory I walked in with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Neighbour expansion is not simply bad
&lt;/h2&gt;

&lt;p&gt;With the oracle chapter, adding neighbours moves recall 0.7844 → 0.7688 and precision 0.6796 → &lt;strong&gt;0.6925&lt;/strong&gt;. Recall down slightly, precision &lt;em&gt;up&lt;/em&gt;. When you're already in the right chapter, expansion is roughly a wash and can help precision.&lt;/p&gt;

&lt;p&gt;The damage happens when expansion runs on top of an uncertain chapter choice, crowding out the good chunks you did find with text that is only adjacent, not relevant. It is a stage whose value depends on the confidence of the stage above it — exactly the interaction a single end-to-end score cannot show you.&lt;/p&gt;

&lt;p&gt;So the finding is not "turn off neighbour expansion." It is &lt;strong&gt;treat it as a tunable stage rather than a default&lt;/strong&gt;, conditioned on router confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sequence that worked
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Disable each optional stage in turn.&lt;/strong&gt; Cheap, and it found the larger of my two problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insert an oracle at each boundary.&lt;/strong&gt; Not deployable, but it partitions the loss into before/after.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only then attribute the failure.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If I had skipped to step three, I'd have published "hierarchical RAG underperforms chapter-summary retrieval on long narrative corpora" — well-supported by my headline numbers, and wrong about the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;One private narrative corpus, 80 gold questions written from the corpus rather than by independent annotators. Evidence-term overlap scoring, which is a lightweight audit signal and not full semantic correctness. &lt;strong&gt;No confidence intervals&lt;/strong&gt; — the package didn't compute them and I won't imply precision I didn't measure. The oracle variants read gold labels and are not production-realistic. This is not a universal rule against hierarchical retrieval.&lt;/p&gt;




&lt;p&gt;Paper: &lt;em&gt;Diagnosing Hierarchical Retrieval Failure in Long-Document RAG&lt;/em&gt;&lt;br&gt;
DOI: &lt;a href="https://doi.org/10.5281/zenodo.20692450" rel="noopener noreferrer"&gt;10.5281/zenodo.20692450&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>rag</category>
      <category>llm</category>
      <category>python</category>
    </item>
    <item>
      <title>One Integer Deleted the Stop Token From My Loss. The Curve Never Noticed.</title>
      <dc:creator>Panagiotis Gkilis</dc:creator>
      <pubDate>Mon, 31 Aug 2026 16:37:39 +0000</pubDate>
      <link>https://dev.to/bedvibe_studios/one-integer-deleted-the-stop-token-from-my-loss-the-curve-never-noticed-2dll</link>
      <guid>https://dev.to/bedvibe_studios/one-integer-deleted-the-stop-token-from-my-loss-the-curve-never-noticed-2dll</guid>
      <description>&lt;p&gt;For a long time I had a model that trained cleanly and produced nothing usable. The loss fell. Gradients were finite. Nothing crashed. It simply never learned to stop — every generation ran to the length cap and got truncated.&lt;/p&gt;

&lt;p&gt;The cause was two lines of my own source that were individually correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The collision
&lt;/h2&gt;

&lt;p&gt;In a neural codec language model the audio vocabulary has a fixed size, and the stop token is one extra class on top. So the output layer is one wider than the codebook:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nn.Linear(d_model, NUM_AUDIO_TOKENS + 1)   # 1025 classes: 0..1023 audio, 1024 = EOS
eos_id = NUM_AUDIO_TOKENS                  # 1024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Correct. EOS is the last class and the layer has room for it.&lt;/p&gt;

&lt;p&gt;Then, elsewhere, the loss:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;F.cross_entropy(logits, targets, ignore_index=NUM_AUDIO_TOKENS)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Also reasonable on its own. &lt;code&gt;ignore_index&lt;/code&gt; is how you skip padding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But the sentinel and the stop token are the same integer.&lt;/strong&gt; Every position whose target was "stop" was discarded before the loss was computed. Not down-weighted — removed. The model was never once shown an example of stopping, across every epoch it ever ran.&lt;/p&gt;

&lt;p&gt;PyTorch defaults &lt;code&gt;ignore_index&lt;/code&gt; to &lt;code&gt;-100&lt;/code&gt; precisely because it must be a value that can never be a real class. Replace it with a real vocabulary constant and that guarantee is gone, silently: shapes valid, loss finite, run healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The curves are identical
&lt;/h2&gt;

&lt;p&gt;Minimal reproduction, two arms differing only in the sentinel value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The broken arm finished at 0.0035. The fixed arm finished at 0.0034.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same curve to any human, any dashboard, any threshold you would write. One has a working objective and one has an objective with a hole in it, and the loss cannot distinguish them — because the loss is computed &lt;em&gt;over what survived the mask&lt;/em&gt;. A metric cannot report on examples it never received.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second half: lowest loss, worse model
&lt;/h2&gt;

&lt;p&gt;200 epochs at lr = 2e-5, evaluated every 50 on a fixed split held out by utterance (n = 32). &lt;em&gt;Rank&lt;/em&gt; is the stop token's position among 1025 classes at the true terminal frame.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;epoch&lt;/th&gt;
&lt;th&gt;training loss&lt;/th&gt;
&lt;th&gt;mean P(stop)&lt;/th&gt;
&lt;th&gt;argmax = stop&lt;/th&gt;
&lt;th&gt;self-terminated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;2.199&lt;/td&gt;
&lt;td&gt;0.4218&lt;/td&gt;
&lt;td&gt;16/32&lt;/td&gt;
&lt;td&gt;2/8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;100&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.628&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.4655&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18/32&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6/8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.302&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.2159&lt;/td&gt;
&lt;td&gt;8/32&lt;/td&gt;
&lt;td&gt;3/8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1.429&lt;/td&gt;
&lt;td&gt;0.1818&lt;/td&gt;
&lt;td&gt;5/32&lt;/td&gt;
&lt;td&gt;3/8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Between epochs 100 and 150 the training loss &lt;strong&gt;improved by 20%&lt;/strong&gt; while mean P(stop) fell &lt;strong&gt;54%&lt;/strong&gt;, top-1 stop accuracy went 18/32 to 8/32, and autonomous termination halved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selecting by lowest training loss returns epoch 150. The model that terminates reliably is epoch 100.&lt;/strong&gt; The reversal was observed independently in a shorter run, which is why I am willing to state it.&lt;/p&gt;

&lt;p&gt;"Save the checkpoint with the lowest validation loss" is the default in more or less every training script in existence, including mine. On this run it was actively the wrong rule, and the number it optimised looked better the whole way down.&lt;/p&gt;

&lt;p&gt;For completeness, what fixing the collision bought on the real model, same held-out split:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;checkpoint&lt;/th&gt;
&lt;th&gt;mean P(stop)&lt;/th&gt;
&lt;th&gt;argmax = stop&lt;/th&gt;
&lt;th&gt;rank&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;random initialisation&lt;/td&gt;
&lt;td&gt;0.001848&lt;/td&gt;
&lt;td&gt;0/32&lt;/td&gt;
&lt;td&gt;111.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;after correction&lt;/td&gt;
&lt;td&gt;0.4655&lt;/td&gt;
&lt;td&gt;18/32&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;End-to-end synthesis then terminated on its own at frame 203 against a 350-frame ceiling. Before the fix that was impossible by two independent mechanisms: the training-time collision above, and an inference-time mask that set the logit of every index at or beyond the codebook size — including end-of-sequence — to negative infinity before sampling.&lt;/p&gt;

&lt;p&gt;One honest loose end: P(stop) plateaus in the range &lt;strong&gt;0.35–0.47&lt;/strong&gt; across two learning rates and a 5.9x increase in training data (224 to 1313 utterances, with speaker, language, emotion and the held-out split all held constant). So the plateau is not a data-quantity limit. I have no confirmed explanation for it. Terminal timing in speech is genuinely ambiguous, and hedging with the stop token ranked second of 1025 may simply be correct behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same integer, safe one stage over
&lt;/h2&gt;

&lt;p&gt;The detail I find most instructive: the identical line is harmless in the next stage of the same model.&lt;/p&gt;

&lt;p&gt;The autoregressive stage predicts the first codebook plus EOS — &lt;strong&gt;1025 classes&lt;/strong&gt;, so &lt;code&gt;1024&lt;/code&gt; is a real class and using it as a sentinel is fatal. The non-autoregressive stage predicts audio codes only — &lt;strong&gt;1024 classes&lt;/strong&gt;, so &lt;code&gt;1024&lt;/code&gt; is out of range and the exact same &lt;code&gt;ignore_index&lt;/code&gt; is correct.&lt;/p&gt;

&lt;p&gt;One &lt;code&gt;+ 1&lt;/code&gt; in a different file decides whether that line destroys your objective. Both stages read identically at the call site. That is not a mistake you catch by reading carefully; it is a mistake you catch by checking a relationship between two numbers that never appear together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which is what a linter is for
&lt;/h2&gt;

&lt;p&gt;This is now two rules in &lt;code&gt;trainproof&lt;/code&gt;, my linter for training runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sentinel collision.&lt;/strong&gt; Compare the output layer's class count against &lt;code&gt;ignore_index&lt;/code&gt;. If the sentinel is a valid class, fail. If it sits exactly one past the end, say so explicitly — because the same integer is fatal one class earlier, and that distinction deserves to be visible rather than silently passed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead class.&lt;/strong&gt; Accumulate which classes ever reach the loss as a positive target during the first epoch, then flag any class the output layer can emit but that never once appears as an answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My own two stages are the regression fixture — the fatal case and its safe twin, one integer apart. Not a synthetic example.&lt;/p&gt;

&lt;p&gt;The design decision worth stating: the dead-class rule only fires when coverage is already broad and few classes are missing. One unseen class out of 1025 is a structural exclusion. Nine hundred unseen is a small sample. Without that guard the check screams on every short run and gets switched off — which is how good checks die.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you take one thing from this
&lt;/h2&gt;

&lt;p&gt;I do not think this is rare. Any codebase where a padding sentinel, an end-of-sequence id and a vocabulary size are all defined as named constants in different files can produce it, and none of your instrumentation will complain.&lt;/p&gt;

&lt;p&gt;If you fine-tune anything with a custom &lt;code&gt;ignore_index&lt;/code&gt;, go and check it against your output layer's width right now. It takes thirty seconds and the failure mode is completely silent.&lt;/p&gt;




&lt;p&gt;Paper: &lt;em&gt;The Loss Curve Is Not a Sufficient Statistic — Silent Objective Failures from Sentinel-Class Collisions in Neural Codec Language Models&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;DOI: &lt;a href="https://doi.org/10.5281/zenodo.21864658" rel="noopener noreferrer"&gt;10.5281/zenodo.21864658&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pip install trainproof&lt;/code&gt; — &lt;a href="https://github.com/Mormolykos/trainproof" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, MIT&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>deeplearning</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Fourteen Speaker Encoders Heard the Same Voice. Their Error Rates Differed Five-Fold.</title>
      <dc:creator>Panagiotis Gkilis</dc:creator>
      <pubDate>Mon, 31 Aug 2026 16:37:35 +0000</pubDate>
      <link>https://dev.to/bedvibe_studios/fourteen-speaker-encoders-heard-the-same-voice-their-error-rates-differed-five-fold-1pmn</link>
      <guid>https://dev.to/bedvibe_studios/fourteen-speaker-encoders-heard-the-same-voice-their-error-rates-differed-five-fold-1pmn</guid>
      <description>&lt;p&gt;A speaker encoder turns a few seconds of speech into a vector, and the distance between two vectors is treated as an answer to "are these the same person." That number gets used for authentication, for voice-cloning pipelines, and increasingly as the yardstick in papers asking whether a synthetic voice preserved someone's identity.&lt;/p&gt;

&lt;p&gt;The number is not neutral. It is a reading from an instrument, and I wanted to know how much the instrument was contributing.&lt;/p&gt;

&lt;p&gt;So I ran fourteen of them over an identical, frozen list of &lt;strong&gt;11,935 trials&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The corpus is why the question is answerable
&lt;/h2&gt;

&lt;p&gt;Most emotional-speech corpora use different sentences for different emotions. A model can then learn angry &lt;em&gt;vocabulary&lt;/em&gt; instead of angry &lt;em&gt;delivery&lt;/em&gt;, and you cannot separate them afterwards.&lt;/p&gt;

&lt;p&gt;This one can't make that mistake. &lt;strong&gt;Eight speakers each recorded the same 1,360 sentences in six phonation states&lt;/strong&gt; — neutral, happy, angry, scared, shouting, whisper. Lexical content is fixed while phonation varies.&lt;/p&gt;

&lt;p&gt;Enrolment is neutral speech. The test is whether the encoder still recognises the person while they shout or whisper. The trial list was frozen in writing before a single model loaded, and every encoder scored the identical list — so every comparison is &lt;strong&gt;paired&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Equal error rate spans 0.047 to 0.233 across the panel&lt;/strong&gt; — same trials, same audio, nothing varying but the encoder. &lt;strong&gt;29 of 91&lt;/strong&gt; pairwise comparisons survive Holm–Bonferroni correction.&lt;/p&gt;

&lt;p&gt;If you are choosing an encoder for expressive audio, that five-fold spread is your decision. It is larger than most architectural differences people argue about.&lt;/p&gt;

&lt;h2&gt;
  
  
  CAMPPlus is last, without exception
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Highest error of the fourteen, all 13 of its comparisons surviving family-wise correction&lt;/strong&gt; — for every one of the eight speakers and every one of the six conditions. Margins run from &lt;strong&gt;+0.087 [+0.064, +0.127]&lt;/strong&gt; against ECAPA-TDNN to &lt;strong&gt;+0.185 [+0.150, +0.215]&lt;/strong&gt; against the lowest-error encoder.&lt;/p&gt;

&lt;p&gt;The mechanism is the useful part. Its &lt;em&gt;genuine&lt;/em&gt; scores are unremarkable; its &lt;strong&gt;impostor&lt;/strong&gt; scores are the problem. It places different people at cosine &lt;strong&gt;0.28–0.34&lt;/strong&gt; where ECAPA-TDNN and ReDimNet place them near &lt;strong&gt;0.034&lt;/strong&gt;. That is a compressed embedding space — different speakers land close together.&lt;/p&gt;

&lt;p&gt;It is visible at neutral speech too (0.031 EER where several ReDimNet checkpoints reach 0.000). Expressive speech doesn't cause it. Expressive speech makes it visible.&lt;/p&gt;

&lt;p&gt;A widely used open-source TTS system conditions on CAMPPlus. To be precise: that is a verified fact about its source code, &lt;strong&gt;not&lt;/strong&gt; a measurement of its audio. I measured encoders in isolation on human recordings. Whether that choice degrades output is a separate experiment I have not run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parameter count does not order the panel
&lt;/h2&gt;

&lt;p&gt;Spearman ρ(parameters, expressive EER) = &lt;strong&gt;+0.135&lt;/strong&gt; over fourteen encoders. The 20.8M model ranks 13th of 14. A 4.81M model ranks 1st. Published VoxCeleb1-O position doesn't transfer either — that leaderboard is computed on calm read speech, which is not the condition failing you.&lt;/p&gt;

&lt;p&gt;I retracted my claim about the winner, incidentally. It survives as an argmin (selected in 89.4% of speaker resamples, leading all eight leave-one-speaker-out refits) but after correction it is separated from only &lt;strong&gt;7 of 13&lt;/strong&gt;. The defensible wording is &lt;em&gt;lowest observed error&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A clean pre-registered negative
&lt;/h2&gt;

&lt;p&gt;Spectral denoising does not materially change the measurement: &lt;strong&gt;|ΔEER| ≤ 0.039 over 10,765 twinned trials&lt;/strong&gt;, and whispered speech — where removing aspiration noise was the stated worry — is among the &lt;em&gt;least&lt;/em&gt; affected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten retracted claims, published on purpose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An 11.7 dB spectral effect, asserted with an instrument whose F0 artifact budget is 12.09 dB.&lt;/strong&gt; Sweeping F0 with the spectral envelope held fixed moved the descriptor further than the claimed effect. It was never a measurement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Shouting does not transfer."&lt;/strong&gt; Judged on RMS after peak-normalising every clip, so the figure was crest factor, not loudness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tie-break defect in the EER estimator.&lt;/strong&gt; &lt;code&gt;argmin&lt;/code&gt; returns the first minimiser rather than the balanced operating point — invisible at unit weights, common under the integer speaker multiplicities a bootstrap runs at. Fixing it moved one comparison across Holm, &lt;strong&gt;28 → 29 of 91&lt;/strong&gt;. The correction &lt;em&gt;favoured&lt;/em&gt; the paper, which is why it is published rather than quietly kept.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule I have now paid for three times: &lt;strong&gt;validate the instrument before measuring with it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Statistics
&lt;/h2&gt;

&lt;p&gt;The trial list is a directed graph on 8 speakers — genuine trials are self-loops, impostor trials the 56 ordered edges. The independent unit is the speaker, on both sides. So the resample is a &lt;strong&gt;vertex bootstrap&lt;/strong&gt;: draw 8 vertices with replacement, take the induced sub-multigraph, weight the impostor edge by the product of multiplicities.&lt;/p&gt;

&lt;p&gt;An earlier scheme resampled enrolment speakers only, dropping a speaker as enrollee while leaving them in as impostor. Fixing it widened marginal intervals (width ratio median 1.11, max 1.58) and &lt;strong&gt;cost two already-written claims&lt;/strong&gt;. Paired differences barely moved, because both encoders score the same impostor clips and the dependence cancels in the difference.&lt;/p&gt;

&lt;p&gt;B = 20,000, seed fixed, families declared before testing, Holm–Bonferroni at α = 0.05.&lt;/p&gt;

&lt;h2&gt;
  
  
  The companion result, which is the cheap fix
&lt;/h2&gt;

&lt;p&gt;Enrolling on expressive material instead of neutral-only recovers the loss &lt;strong&gt;on all 14 encoders&lt;/strong&gt;, every one surviving Holm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mechanism is the opposite of the intuitive one.&lt;/strong&gt; Enrolling on a &lt;em&gt;single&lt;/em&gt; expressive state helps that state and slightly harms the others — transfer-matrix diagonal median &lt;strong&gt;−0.082&lt;/strong&gt;, off-diagonal median &lt;strong&gt;+0.010&lt;/strong&gt;. Guessing which state a user will be in, and guessing wrong, is worse than enrolling on calm speech.&lt;/p&gt;

&lt;p&gt;A pre-committed control ruled out the competing explanation: acoustic diversity alone delivers &lt;strong&gt;+0.002&lt;/strong&gt; where composition delivers &lt;strong&gt;−0.065&lt;/strong&gt;. &lt;strong&gt;It is coverage, not expressiveness.&lt;/strong&gt; Spread enrolment thinly across states.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not claim
&lt;/h2&gt;

&lt;p&gt;Nothing about any TTS system's output quality. Eight speakers, one studio, one recording chain. Enrolment and test share a session, so absolute error rates are optimistic and not comparable to VoxCeleb — only relative comparisons on identical trials are licensed. Architecture is fully confounded with training corpus; separating them requires training.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generalisable part
&lt;/h2&gt;

&lt;p&gt;An established metric is not automatically a neutral one. An encoder, an evaluator, a classifier, an LLM judge — any of them can become part of the experiment rather than a window onto it.&lt;/p&gt;

&lt;p&gt;Before asking whether a system preserves identity under some transformation, ask whether your instrument has been validated under that transformation. Otherwise you attribute the encoder's behaviour to the speaker, and the number looks perfectly reasonable while you do it.&lt;/p&gt;




&lt;p&gt;Paper: &lt;em&gt;Speaker Encoders Disagree About Who You Are When You Shout&lt;/em&gt; — Zenodo, CC-BY-4.0.&lt;br&gt;
DOI: &lt;a href="https://doi.org/10.5281/zenodo.22158030" rel="noopener noreferrer"&gt;10.5281/zenodo.22158030&lt;/a&gt;&lt;br&gt;
Code, data and the full decision record: &lt;a href="https://github.com/Mormolykos/speaker-encoders-disagree" rel="noopener noreferrer"&gt;github.com/Mormolykos/speaker-encoders-disagree&lt;/a&gt;&lt;br&gt;
The eval library the analysis checks itself against: &lt;code&gt;pip install spkproof&lt;/code&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>python</category>
      <category>datascience</category>
      <category>ai</category>
    </item>
    <item>
      <title>Two of My Instruments Were Wrong, in Opposite Directions</title>
      <dc:creator>Panagiotis Gkilis</dc:creator>
      <pubDate>Fri, 28 Aug 2026 13:30:49 +0000</pubDate>
      <link>https://dev.to/bedvibe_studios/two-of-my-instruments-were-wrong-in-opposite-directions-1jc8</link>
      <guid>https://dev.to/bedvibe_studios/two-of-my-instruments-were-wrong-in-opposite-directions-1jc8</guid>
      <description>&lt;p&gt;On 28 August 2026 I audited 38 live pages and fixed some ordinary defects. The two things worth writing down both came from my own tooling being broken. One script reported three pages I had just fixed as still broken, because it did not recognise a relative link. One date parser had thrown an exception on every crawl-statistics read since it was written, so the report that answers "is the crawler arriving at all" had never once returned a number.&lt;/p&gt;

&lt;p&gt;This is a note about measurement rather than about search engines. The setting is an indexing problem on one of my hosts, but the transferable part is that I spent a day acting on numbers produced by instruments I had never checked, and both were wrong — one in each direction. One made a working fix look broken. One made a working crawler invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crawling and indexing are not the same thing
&lt;/h2&gt;

&lt;p&gt;Everything below depends on this, and most writing on the subject collapses it into one word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crawling&lt;/strong&gt; is retrieval. A crawler requests a URL and receives bytes. That is all it means. A page can be crawled every day for a year and appear in no search result ever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Indexing&lt;/strong&gt; is selection. Having retrieved the page, the engine decides whether to keep it in a form it will serve. That is a judgement, each engine makes it independently, and none of them will tell you the reasoning.&lt;/p&gt;

&lt;p&gt;So there are at least three states worth separating: &lt;em&gt;never fetched&lt;/em&gt;, &lt;em&gt;fetched and declined&lt;/em&gt;, and &lt;em&gt;fetched and kept&lt;/em&gt;. "My pages aren't indexed" describes the middle two identically, and their causes have nothing in common.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit, kept in proportion
&lt;/h2&gt;

&lt;p&gt;On &lt;strong&gt;28 August 2026&lt;/strong&gt; I fetched every URL in the sitemap — 38 pages on the affected host, 20 on my notebook site — over plain HTTP with no JavaScript executed, and produced a number per page rather than a verdict. That rule exists because an earlier audit returned "all clean" for months while the homepage was handing crawlers a fraction of its navigation in a form they could use.&lt;/p&gt;

&lt;p&gt;It found real defects. Five pages had &lt;strong&gt;zero followable links&lt;/strong&gt;: a crawler arriving there could go nowhere. The worst had two navigation controls, both &lt;code&gt;&amp;lt;button onclick&amp;gt;&lt;/code&gt; — usable with a mouse, invisible to a crawler. Three more bound their only navigation to a JavaScript click handler, which has the same effect.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;38 pages, fetched live&lt;/th&gt;
&lt;th&gt;Before 28 Aug&lt;/th&gt;
&lt;th&gt;After 28 Aug&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pages with zero followable links&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navigation a crawler cannot follow&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages with an incorrect &lt;code&gt;h1&lt;/code&gt; count&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing robots directive&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical problems&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images with no &lt;code&gt;alt&lt;/code&gt; attribute (of 199)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-200 responses&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is housekeeping. It is worth doing and it is not a finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrument one: a false negative that looked exactly like a failed fix
&lt;/h2&gt;

&lt;p&gt;After deploying, I re-ran the audit against the live site. It reported that three of the pages I had just fixed still had zero followable links.&lt;/p&gt;

&lt;p&gt;They did not. I fetched one by hand and the anchor was plainly in the served HTML.&lt;/p&gt;

&lt;p&gt;The bug was mine. The script counted links by matching &lt;code&gt;href&lt;/code&gt; values that started with &lt;code&gt;/&lt;/code&gt; or &lt;code&gt;http&lt;/code&gt;. The anchors I had added were relative — &lt;code&gt;href="./"&lt;/code&gt; — so they matched nothing and counted as zero.&lt;/p&gt;

&lt;p&gt;What makes this worth writing down is the shape of the failure. &lt;strong&gt;A measurement that silently under-reports is indistinguishable from the defect it is supposed to detect.&lt;/strong&gt; Every downstream step would have been reasonable and wrong: conclude the deploy failed, hunt for a caching problem that did not exist, possibly "fix" three pages that were already correct and introduce a real defect doing it. Nothing in the output looked like an error. It looked like bad news.&lt;/p&gt;

&lt;p&gt;The only thing that caught it was retrieving the page and reading the markup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrument two: a report that had never returned a number
&lt;/h2&gt;

&lt;p&gt;The second failure was older and much more expensive.&lt;/p&gt;

&lt;p&gt;Bing's webmaster API serialises dates in a Microsoft-era format — &lt;code&gt;/Date(1784190745000)/&lt;/code&gt;, epoch milliseconds wrapped in a string. My parser handled that correctly. But one endpoint, and as far as I can tell only one, returns a second variant carrying a UTC offset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/Date(1786345200000-0700)/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My pattern captured "digits and minus signs". It swallowed the offset into the number, and the integer conversion raised an exception every time.&lt;/p&gt;

&lt;p&gt;Because that variant appears in exactly one report, the failure was invisible everywhere else. Every other call worked. And the one report it broke was &lt;strong&gt;crawl statistics&lt;/strong&gt; — the nearest available substitute for origin access logs, and the first thing to check when asking whether a crawler is showing up.&lt;/p&gt;

&lt;p&gt;It had never once returned a value. I had been reasoning about crawler behaviour for weeks with the most direct instrument for it throwing an exception on every call. The fix is one line; the test asserts that both offset signs resolve to the same instant, because the value before the offset is already UTC.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the repaired report showed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Pages crawled that day (Bing)&lt;/th&gt;
&lt;th&gt;Pages held in index (Bing)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-08-24&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-08-25&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-08-26&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-08-27&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Bing crawls this host every day, 15 to 36 pages, and holds 34 of roughly 38 in its index. Google holds none of them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is not a host a major crawler visits occasionally. It is one a major crawler works through continuously, and has done throughout the period I spent trying to work out why crawlers were not engaging with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sorting the evidence
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Verified directly from the live sites (2026-08-28):&lt;/strong&gt; every row of the 38-page audit, fetched over HTTP with no JavaScript, re-verified after deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measured from Bing's API (2026-08-24 to 2026-08-28):&lt;/strong&gt; the daily crawl volume and index count. One engine's figures about its own behaviour — good evidence about that engine, no evidence at all about any other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Corrected tooling:&lt;/strong&gt; two bugs, both mine, both fixed on 2026-08-28, one now covered by a regression test. The numbers here are post-correction. The pre-correction ones were wrong in a way that read as a genuine finding, which is the entire point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observation:&lt;/strong&gt; two major engines, given the same host, the same bytes, the same permissive &lt;code&gt;robots.txt&lt;/code&gt; and the same sitemap, reached opposite conclusions. One indexes nearly everything. One indexes nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unresolved:&lt;/strong&gt; why. I do not know, and I am not going to name a mechanism to round the story off. Index selection is independent between engines, so an asymmetry is permitted and is not by itself evidence of a fault anywhere.&lt;/p&gt;

&lt;p&gt;What the asymmetry does do is retire a family of explanations. A page a large crawler retrieves daily and chooses to keep is demonstrably reachable, parseable, renderable and acceptable to a serious indexing pipeline.&lt;/p&gt;

&lt;p&gt;To be explicit, because the genre invites the opposite reading: &lt;strong&gt;nothing here fixed the zero.&lt;/strong&gt; It was zero before the audit and zero after. This is not a recovery story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prediction, recorded before the outcome
&lt;/h2&gt;

&lt;p&gt;The obvious next question is whether the fixes helped. I cannot answer that today — the changes went live on 28 August and there is no after to compare against.&lt;/p&gt;

&lt;p&gt;So instead of waiting and then claiming a prediction I never wrote down, here it is in advance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Baseline, 2026-08-28.&lt;/strong&gt; Google: 0 pages indexed on the affected host. Bing: 34 held, 15–36 crawled per day. Audit defects all at zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I will re-measure on 2026-09-15&lt;/strong&gt;, with the same script and the same API calls, and publish the result whichever way it goes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I expect: no material change in either engine.&lt;/strong&gt; The defects I fixed were real, but none plausibly explains a host-wide zero, and Bing was already crawling this host thoroughly while every one of them was still present — fairly direct evidence they were not what stood in the way. Earlier in this investigation I also ran the standard remedies against a held-out control group and measured no effect: &lt;a href="https://ai.bedvibe.studio/crawled-not-indexed/" rel="noopener noreferrer"&gt;treatment 32%, control 33%&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If the numbers move, my model is wrong and that is the more interesting outcome. If they do not, I have a dated negative result instead of a vague impression.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would take from this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check instruments against reality before trusting their output.&lt;/strong&gt; Two of mine were wrong on the same day, in opposite directions. Both would have survived indefinitely if I had only read what they printed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A silent nothing is worse than a loud failure.&lt;/strong&gt; Make a missing measurement say &lt;em&gt;missing&lt;/em&gt; — never zero, and never an empty report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate crawled from indexed in your own vocabulary first.&lt;/strong&gt; The fixes for "never fetched" and "fetched and declined" have almost nothing in common.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count links on the page, not links to it.&lt;/strong&gt; A page with no outgoing anchors is a dead end however many things point at it, and a click handler is invisible to the thing you are trying to persuade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetch it and read it.&lt;/strong&gt; Every real finding here came from the actual bytes. Every error came from believing a summary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the prediction down first.&lt;/strong&gt; It costs one paragraph and it is the difference between a result and a story.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest summary of the day: I improved a site that was already technically sound, repaired two instruments that had been quietly lying to me, and confirmed an asymmetry I can describe precisely and cannot explain at all.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://ai.bedvibe.studio/broken-instruments/" rel="noopener noreferrer"&gt;ai.bedvibe.studio/broken-instruments&lt;/a&gt;, where I will post the 15 September re-measurement against the prediction above.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I write up measurement and instrumentation work at &lt;a href="https://ai.bedvibe.studio/articles/" rel="noopener noreferrer"&gt;ai.bedvibe.studio&lt;/a&gt; — including the &lt;a href="https://ai.bedvibe.studio/crawled-not-indexed/" rel="noopener noreferrer"&gt;control-group indexing experiment&lt;/a&gt; this one follows, and a &lt;a href="https://geo.bedvibe.studio/" rel="noopener noreferrer"&gt;crawler-behaviour observatory&lt;/a&gt; that logs which AI and search crawlers actually arrive. The studio itself is at &lt;a href="https://bedvibe.studio/" rel="noopener noreferrer"&gt;bedvibe.studio&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>testing</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My Agent Answers From 0.6% of Its Corpus and Reports It Like a Full Read</title>
      <dc:creator>Panagiotis Gkilis</dc:creator>
      <pubDate>Tue, 25 Aug 2026 08:18:39 +0000</pubDate>
      <link>https://dev.to/bedvibe_studios/my-agent-answers-from-06-of-its-corpus-and-reports-it-like-a-full-read-35e7</link>
      <guid>https://dev.to/bedvibe_studios/my-agent-answers-from-06-of-its-corpus-and-reports-it-like-a-full-read-35e7</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://ai.bedvibe.studio/retrieval-coverage/" rel="noopener noreferrer"&gt;ai.bedvibe.studio&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My portfolio RAG agent holds 1,003 indexed chunks. A question retrieves six and answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Six of 1,003 is 0.598%.&lt;/strong&gt; Nothing in the response, the logs or the trace says the other 997 were never looked at. It reads exactly like an answer built from reading everything.&lt;/p&gt;

&lt;p&gt;That is fine for &lt;em&gt;"what does he say about X."&lt;/em&gt; It is &lt;strong&gt;not evidence for&lt;/strong&gt; &lt;em&gt;"does he mention X anywhere"&lt;/em&gt; — and there is nothing in a normal RAG stack that tells those two apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The claim retrieval cannot support
&lt;/h2&gt;

&lt;p&gt;A compliance corpus: 2,431 policy documents. Someone asks whether there is a remote-work reimbursement policy. Retrieval returns eight chunks. None mentions reimbursement. The agent answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There is no remote-work reimbursement policy in the corpus.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That claim requires knowledge of 2,431 documents. It was made from eight. The agent did not lie and it did not hallucinate — it correctly reported what it found, and the &lt;strong&gt;shape of the sentence&lt;/strong&gt; quietly upgraded a statement about eight documents into a statement about the corpus.&lt;/p&gt;

&lt;p&gt;Absence of evidence in a retrieved fragment is not evidence of absence in the whole. Everyone knows this. No retrieval stack I have used records enough to enforce it, because the denominator is not carried anywhere near the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But our faithfulness score is 1.0"
&lt;/h2&gt;

&lt;p&gt;This is the first objection and it deserves a straight answer: &lt;strong&gt;faithfulness cannot catch this, by construction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Faithfulness asks whether the answer is supported by the retrieved context — did the model invent anything beyond what it was given. The compliance answer above invents nothing. It scores a perfect faithfulness and may be false about 2,423 documents.&lt;/p&gt;

&lt;p&gt;Faithfulness scores &lt;em&gt;answer against context&lt;/em&gt;. Coverage scores &lt;em&gt;context against corpus&lt;/em&gt;. Different axes, and a system can be perfect on the first while completely silent on the second.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it needs&lt;/th&gt;
&lt;th&gt;Available at answer time?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Faithfulness&lt;/td&gt;
&lt;td&gt;answer + context&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context precision&lt;/td&gt;
&lt;td&gt;answer + context&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context &lt;strong&gt;recall&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;ground-truth annotations&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — offline evaluation only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Corpus coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;corpus size + retrieved count&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes — and it is not reported&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Context recall is the metric that would catch it, and it needs labelled ground truth, so it lives in your evaluation harness and not in production. Corpus coverage needs two integers you already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The control is the shape of the sentence, not a percentage
&lt;/h2&gt;

&lt;p&gt;This took longest to see, and it is why there is no threshold anywhere in the implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;0.598% is not a bad number. It is a bad number for one class of sentence.&lt;/strong&gt; It is entirely adequate for &lt;em&gt;"he mentions Rust"&lt;/em&gt; — you need the one chunk you are quoting and nothing else. It cannot support &lt;em&gt;"he never mentions Rust."&lt;/em&gt; Same retrieval, same six chunks, opposite verdicts, because the claim changed.&lt;/p&gt;

&lt;p&gt;A single coverage threshold cannot serve both. Set it low and it licenses the second sentence; set it high and it forbids the first.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Coverage required&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;"The policy says X"&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Any — you need only the units you cite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;"There is no policy about X"&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Exhaustive&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;"All policies require X"&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Exhaustive&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;"The most recent policy is…"&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Exhaustive&lt;/strong&gt; — the unread remainder may hold the true maximum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;"There are three mentions"&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Exhaustive&lt;/strong&gt; — a count over a sample is an estimate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row needing exhaustive coverage has the same reason: it asserts something about the units that were &lt;em&gt;not&lt;/em&gt; read. 999,999 of 1,000,000 is 99.9999% and still cannot establish absence — a threshold there would be a lie with a decimal point on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three failures wearing one label
&lt;/h2&gt;

&lt;p&gt;The second thing the missing denominator costs you is diagnosis. When a RAG answer is wrong, the post-mortem usually terminates at &lt;em&gt;the model hallucinated&lt;/em&gt;. That sentence hides at least three separate engineering problems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What actually happened&lt;/th&gt;
&lt;th&gt;What to fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The relevant chunk was never retrieved&lt;/td&gt;
&lt;td&gt;The retriever, embedding or query — &lt;em&gt;not&lt;/em&gt; the model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;It was retrieved, then dropped during context assembly&lt;/td&gt;
&lt;td&gt;Context construction — the model never saw it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;It reached the model, which reasoned past it&lt;/td&gt;
&lt;td&gt;The prompt or the model — the only reasoning failure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three different people, three different days, one identical symptom. And the distinction is trivially recordable — &lt;code&gt;retrieved&lt;/code&gt;, &lt;code&gt;in_context&lt;/code&gt;, and whether the answer was right — but almost nobody logs the middle number, so the first two are permanently indistinguishable after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;An MCP server for &lt;a href="https://github.com/Mormolykos/notchecked" rel="noopener noreferrer"&gt;notchecked&lt;/a&gt;, the coverage-accounting schema I wrote after hitting the silent-skip failure in four domains. The library types the gaps a &lt;em&gt;program&lt;/em&gt; leaves. The server does it for an agent, which is where the failure moved.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;coverage_retrieval(
  target      = "portfolio corpus",
  query       = "Rust experience",
  corpus_size = 1003,
  retrieved   = 6,
  in_context  = 6,
  claim_type  = "absence",
)

→ scope:      6 of 1003 corpus units (0.598%)
  exhaustive: false
  claim_supported: false

  REFUSE_THIS_CLAIM: An ABSENCE claim asserts something about every unit
  you did NOT read, and 997 were never read. Downgrade the answer to what
  you found, or inspect the whole corpus.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;retrieved = 0&lt;/code&gt; is not a thin answer, it is a retrieval failure, and it records as one. Everything retrieved and then dropped is a context failure and records as that instead. Neither is a gap in the agent's reasoning, and calling them one sends someone to debug a prompt for a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug this found in its own implementation
&lt;/h2&gt;

&lt;p&gt;I wrote a suite that replays six investigations of my own that produced wrong claims — a page judged from 3,000 of its 10,828 words, a search that had stripped the HTML so anything named only in an &lt;code&gt;href&lt;/code&gt; was invisible, three different counts from three broken filesystem walks.&lt;/p&gt;

&lt;p&gt;On its first run it failed, and it failed on my code rather than on the cases. &lt;code&gt;exhaustive&lt;/code&gt; was a bare assertion: an agent could pass &lt;code&gt;exhaustive: true&lt;/code&gt; alongside &lt;code&gt;scope: "3,000 of 10,828 words"&lt;/code&gt; and the absence warning was dropped. &lt;strong&gt;The tool committed the exact failure it exists to prevent, one layer above the schema it protects.&lt;/strong&gt; Where the scope carries "N of M", the contradiction is machine-visible and is now refused.&lt;/p&gt;

&lt;p&gt;That is the third time this idea has caught its own implementation. I have stopped finding it funny.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not do
&lt;/h2&gt;

&lt;p&gt;If your retriever reports &lt;code&gt;corpus_size=100000, retrieved=20, in_context=12, cited=3&lt;/code&gt;, this records those numbers and what they can support. &lt;strong&gt;It does not know whether the retriever chose the right twenty.&lt;/strong&gt; It is not a retriever, a vector database, a reranker or a context assembler, and it is not competing with the one you have.&lt;/p&gt;

&lt;p&gt;Three limits are recorded as &lt;em&gt;passing tests&lt;/em&gt; rather than left out of the README: an exhaustive search of the &lt;strong&gt;wrong instrument&lt;/strong&gt; is still exhaustive; the target list is &lt;strong&gt;self-declared&lt;/strong&gt;, so nothing can know what the caller failed to think of; the retrieval counts are &lt;strong&gt;self-reported&lt;/strong&gt;, with only internal consistency enforced.&lt;/p&gt;

&lt;p&gt;What it removes is the &lt;em&gt;silence&lt;/em&gt;. That is a smaller claim than "this makes agents honest," and it is the one the evidence supports.&lt;/p&gt;

&lt;p&gt;I have measured this in &lt;strong&gt;one&lt;/strong&gt; live system, my own. I have not established what other RAG deployments report, and I am not claiming it from a sample of one.&lt;/p&gt;




&lt;p&gt;&lt;code&gt;pip install notchecked&lt;/code&gt; · &lt;a href="https://github.com/Mormolykos/notchecked" rel="noopener noreferrer"&gt;github.com/Mormolykos/notchecked&lt;/a&gt; — MIT, zero runtime dependencies, MCP over stdio written from the JSON-RPC wire format.&lt;/p&gt;

&lt;p&gt;The eight coverage states were reviewed publicly by &lt;strong&gt;Boris Teplitsky&lt;/strong&gt;, an IBM Certified Expert IT Architect who hit the same shape in infrastructure compliance and gave three corrections that changed the schema. They are frozen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you run RAG in production and you &lt;em&gt;do&lt;/em&gt; record retrieval coverage alongside answers, I would like to know&lt;/strong&gt; — that would make this a solved problem I had not found the solution to, which is a better outcome than being right.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>architecture</category>
      <category>python</category>
    </item>
    <item>
      <title>I Shipped a Feature in May. Nobody Could Reach It Until Yesterday.</title>
      <dc:creator>Panagiotis Gkilis</dc:creator>
      <pubDate>Tue, 25 Aug 2026 03:33:52 +0000</pubDate>
      <link>https://dev.to/bedvibe_studios/i-shipped-a-feature-in-may-nobody-could-reach-it-until-yesterday-36d2</link>
      <guid>https://dev.to/bedvibe_studios/i-shipped-a-feature-in-may-nobody-could-reach-it-until-yesterday-36d2</guid>
      <description>&lt;p&gt;I shipped a feature in May. Cross-page session memory for a retrieval agent — follow a link, the conversation comes with you. It was deployed, tested, and running in production.&lt;/p&gt;

&lt;p&gt;Yesterday I found out no visitor had ever been able to reach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that was actually broken
&lt;/h2&gt;

&lt;p&gt;The obvious problem was that the chat widget existed on exactly one page out of thirty-nine. That is bad, but it is the kind of bad you notice.&lt;/p&gt;

&lt;p&gt;The real problem was underneath. The widget never sent a &lt;code&gt;session_id&lt;/code&gt; field. Or a &lt;code&gt;scope&lt;/code&gt; field. The server had both, documented, tested, deployed — and the browser had simply never been wired to send either one.&lt;/p&gt;

&lt;p&gt;So the feature was not "confined to one page." It was &lt;strong&gt;never invoked from anywhere.&lt;/strong&gt; A &lt;code&gt;sessions_active&lt;/code&gt; counter that had read &lt;code&gt;0&lt;/code&gt; since May, on an endpoint that was working perfectly.&lt;/p&gt;

&lt;p&gt;Both halves passed their own tests. The server tests posted a session id, because the test author knew the field existed. The widget tests asserted the chat rendered and sent a message. Nothing tested the seam, and the seam was the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that class of bug looks like
&lt;/h2&gt;

&lt;p&gt;The failure has a shape worth naming: &lt;strong&gt;two components, each correct, and an integration nobody owns.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It does not throw. It does not log. The health endpoint is green. In my case the endpoint even reported the exact number that would have given it away — &lt;code&gt;sessions_active: 0&lt;/code&gt; — sitting in a JSON response I read regularly and never questioned, because zero active sessions on a low-traffic site is completely plausible.&lt;/p&gt;

&lt;p&gt;That is the tell, and it generalises past my code: &lt;strong&gt;a metric whose broken value is indistinguishable from its healthy value tells you nothing.&lt;/strong&gt; Zero sessions means "nobody is using it" and "it is impossible to use it," and the number is identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving the fix, rather than asserting it
&lt;/h2&gt;

&lt;p&gt;Once the browser sent a session id, I did not want a green test. I wanted the thing a visitor does.&lt;/p&gt;

&lt;p&gt;So: send one question with a session id from the datasets page. Then send a &lt;em&gt;second&lt;/em&gt; question from the portfolio page with the client history deliberately empty — which is what a real navigation does to a JavaScript variable — and see whether the answer still knows what we were talking about.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;page 1  /datasets/    "What speech datasets are there?"
page 2  /portfolio/   "Who recorded it?"     history: []
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second answer came back about the speech datasets. The server had carried it. That is a test I trust, because the thing I removed — the client's memory — is exactly the thing the feature claims to replace.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug I nearly shipped while fixing it
&lt;/h2&gt;

&lt;p&gt;Rolling the widget out, I decided each page should tell the agent which section of the corpus it was on, so the datasets page ranks dataset material first.&lt;/p&gt;

&lt;p&gt;Then I tested the datasets page with a piece of internal vocabulary — a codename from our own research notes — and it answered "the documents do not contain information about that." I read that as a coverage failure and widened the page's search to the whole corpus.&lt;/p&gt;

&lt;p&gt;That was wrong, and the correction came from the person who owns the product rather than from me. Those codenames are internal research nomenclature for how a training corpus was built. They are not products. Nothing under those names is for sale. They appear zero times on the sales page and only in an engineering report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The narrow scope was not a limitation. It was the mechanism keeping internal research vocabulary out of a commercial answer.&lt;/strong&gt; By widening it I had connected a buyer's licensing question to the research corpus, in the name of fixing a defect that was the system working correctly.&lt;/p&gt;

&lt;p&gt;Reverted. The lesson is cheap to state and I clearly needed to learn it expensively: &lt;strong&gt;do not test a sales surface with internal vocabulary, and do not read a narrow result as a broken one until you know what the narrowness is for.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would take from this
&lt;/h2&gt;

&lt;p&gt;Ship a feature and then, before believing it works, ask what a visitor would have to do to reach it — out loud, as a sequence of clicks. Mine had no answer. There was no page to navigate &lt;em&gt;from&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And look at your health metrics for one specific property: is there any value this number could report that means "broken" but reads as "quiet"? Those are the numbers that let a feature sit dead in production for three months while everything stays green.&lt;/p&gt;




&lt;p&gt;The agent this is about is live: the architecture and its 32-case evaluation gate are documented at &lt;a href="https://tts.bedvibe.studio/portfolio/agent-architecture/" rel="noopener noreferrer"&gt;tts.bedvibe.studio/portfolio/agent-architecture&lt;/a&gt;, the eval results at &lt;a href="https://tts.bedvibe.studio/portfolio/agent-evals/" rel="noopener noreferrer"&gt;tts.bedvibe.studio/portfolio/agent-evals&lt;/a&gt;, and the widget itself is on every page of &lt;a href="https://tts.bedvibe.studio/" rel="noopener noreferrer"&gt;tts.bedvibe.studio&lt;/a&gt; now — including, finally, more than one.&lt;/p&gt;

&lt;p&gt;If you have found a feature of your own that was deployed and unreachable, I would like to hear how you noticed. In my case it was luck, and I would prefer a method.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>My Validation Layer Was Correctly Deleting 16% of My Good Data</title>
      <dc:creator>Panagiotis Gkilis</dc:creator>
      <pubDate>Tue, 25 Aug 2026 03:12:31 +0000</pubDate>
      <link>https://dev.to/bedvibe_studios/my-validation-layer-was-correctly-deleting-16-of-my-good-data-1fpj</link>
      <guid>https://dev.to/bedvibe_studios/my-validation-layer-was-correctly-deleting-16-of-my-good-data-1fpj</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://ai.bedvibe.studio/observation-time/" rel="noopener noreferrer"&gt;ai.bedvibe.studio&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I built a real-time tracker in Rust — about two thousand lines — that reads a live ADS-B feed, keeps a Kalman-filtered track per aircraft, and screens every pair for closest approach against separation minima. Roughly 150 aircraft, a full cycle in under a millisecond.&lt;/p&gt;

&lt;p&gt;It ran clean. Tests passed, the picture looked right, the numbers were plausible.&lt;/p&gt;

&lt;p&gt;It was refusing about &lt;strong&gt;one measurement in nine&lt;/strong&gt;, and the only reason I ever found out is that the rejections went to a counter instead of a log line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate has a sub-second tolerance for clock error
&lt;/h2&gt;

&lt;p&gt;The tracker runs an innovation gate: when a position arrives, the filter predicts where the aircraft should be, and if the measurement is too far from that prediction it is rejected as physically impossible rather than believed.&lt;/p&gt;

&lt;p&gt;Once a track converges the innovation standard deviation settles around 36 m, so a five-sigma gate sits at roughly 180 m. An airliner at 250 m/s covers 180 m in &lt;strong&gt;0.7 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So the gate's entire tolerance for a wrong timestamp is under one second. Any pipeline that mis-times its measurements by more than that will have them rejected — correctly, and invisibly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feed reports its own staleness. The pipeline dropped it.
&lt;/h2&gt;

&lt;p&gt;Every ADS-B record carries a field saying how old that position already was when the response was generated. In the original build it was parsed into the contact struct and never read again — the only other place that field appeared in the entire codebase was as &lt;code&gt;0.0&lt;/code&gt; in test fixtures. Every measurement was therefore stamped with the tracker's own cycle clock, as though it had been observed at the instant it landed.&lt;/p&gt;

&lt;p&gt;This is the common case, not an exotic one. &lt;strong&gt;A field that is decoded and then unused looks identical to a field that is decoded and used&lt;/strong&gt;, right up until you go looking for its second reference.&lt;/p&gt;

&lt;p&gt;Here is what that field actually contains, sampled across two consecutive polls of the live feed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reported age of position    median   0.31 s
                            p90      3.97 s
                            max     48.53 s

change per aircraft
between consecutive polls   -15.76 s  to  +3.00 s

re-served identical
positions                   17 of 135 contacts  (12.6%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  It is the jitter, not the lag
&lt;/h2&gt;

&lt;p&gt;The intuitive diagnosis is that the lag itself is the problem. It is not, and the distinction turns out to be the whole thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A constant lag is invisible to a constant-velocity filter.&lt;/strong&gt; If every measurement is uniformly two seconds old, the filter simply tracks a target that is uniformly two seconds behind. The innovations stay small. Nothing is rejected. The picture is late, but it is self-consistent.&lt;/p&gt;

&lt;p&gt;That has a sharp consequence for anyone writing a regression test: a fixture built with a constant age will not reproduce the fault no matter how large the age. The test passes and the bug survives. The fixture has to carry the &lt;em&gt;variation&lt;/em&gt;, or it is testing nothing.&lt;/p&gt;

&lt;p&gt;Look at that middle row again. Between two polls two seconds apart, one aircraft's reported age fell by nearly sixteen seconds — a genuinely new observation arriving after a long gap. Stamped on arrival, both the stale one and the fresh one are marked "now", so the filter sees an aircraft that has apparently teleported. A steadily flying aeroplane appears to lurch, and a correctly functioning gate refuses to believe it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the obvious fix makes it worse
&lt;/h2&gt;

&lt;p&gt;This is the part worth reading, and the reason the change is architectural rather than a one-line patch.&lt;/p&gt;

&lt;p&gt;The obvious repair: stamp each measurement with &lt;code&gt;arrival − age&lt;/code&gt;, feed that to the filter, and guard against a measurement arriving from before the filter's current state, because you cannot predict backwards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;observed_time&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;track&lt;/span&gt;&lt;span class="py"&gt;.epoch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;reject&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That guard is fatal. Every cycle, the display loop advanced &lt;em&gt;every&lt;/em&gt; track to the current wall-clock time so the picture and the collision screen would agree. So by the time a two-second-old measurement arrived, the track's clock already read later than the measurement, and the guard would have discarded it as stale. &lt;strong&gt;A patch aimed at an 11% rejection rate would have rejected considerably more — and from the outside it would have looked exactly like the fix working.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The general form: &lt;strong&gt;a Kalman filter's state is valid for exactly one instant.&lt;/strong&gt; Once anything other than a measurement is allowed to advance that instant, every measurement is applied to a state from a different moment than the one it describes. A rendering loop is not usually thought of as mutating the estimator, which is precisely why this survives review.&lt;/p&gt;

&lt;p&gt;The pre-fix code was not wrong about this. It was &lt;em&gt;consistently&lt;/em&gt; wrong — the display and the measurements were stamped with the same fictional clock, so they agreed with each other. That is why correcting one half in isolation breaks it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that actually fixes it
&lt;/h2&gt;

&lt;p&gt;One sentence: &lt;strong&gt;the filter's validity time advances only when a measurement arrives. Nothing else may move it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The picture and the conjunction screen do not predict tracks forward — they take a view, which extrapolates a copy and leaves the filter parked at the moment it was last given evidence for. A test asserts that rendering the screen thirty times does not move a single filter.&lt;/p&gt;

&lt;p&gt;Two properties follow, and both are load-bearing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The absolute observation time is never reconstructed at all.&lt;/strong&gt; The step between two observations is computed entirely from differences — elapsed time between two arrivals the process witnessed, plus the two ages the sensor reported. Nothing subtracts a duration from a monotonic clock, so there is no underflow path. That closes a real crash: &lt;code&gt;Instant - Duration&lt;/code&gt; panics in Rust if the result would precede the clock's origin, and on Windows that origin is boot time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deduplication becomes free.&lt;/strong&gt; A re-served snapshot has both its arrival and its reported age advance by the same amount, so it computes a step of exactly zero and is rejected by the same comparison that catches out-of-order data. One comparison covers both cases, which is a good sign the invariant is the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing an A/B the upstream cannot distort
&lt;/h2&gt;

&lt;p&gt;Measuring this against a live third-party feed is harder than it looks, and two designs had to be discarded before one held.&lt;/p&gt;

&lt;p&gt;Running both binaries simultaneously fails: the corrected build recorded 32 polls and 32 HTTP errors — zero data — while the old one ran fine. That is not a regression, it is the upstream rate-limiting per IP. Running them sequentially fails differently: the results invert, because the API throttles progressively and whichever build runs first gets the fresher quota. Time-to-first-byte on three consecutive manual requests climbed 0.43 s, 0.91 s, 4.22 s.&lt;/p&gt;

&lt;p&gt;Both would have produced a clean-looking number and a wrong conclusion. What caught them was running the corrected build alone, which worked — proving the failure was the environment, not the code under test.&lt;/p&gt;

&lt;p&gt;The design that survives: alternating order, 45-second windows, 75-second cooldowns, comparing gated plots as a &lt;em&gt;fraction&lt;/em&gt; of observations so traffic volume cancels.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;window   build       observations   gated   rate
  1      corrected       2,137          3    0.14%
  2      pre-fix         2,752        308   11.19%
  3      corrected       2,208         81    3.67%
  4      pre-fix         2,755        578   20.98%

pooled   pre-fix       886 / 5,507         16.1%
         corrected      84 / 4,345          1.9%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both corrected windows sit below both pre-fix windows with no overlap. The corrected build varies — 0.14% against 3.67% — and I would not claim a precise point estimate from four windows. &lt;strong&gt;The separation is the defensible result, not the number.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two unrelated faults the counters surfaced
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It was silently failing most of its polls.&lt;/strong&gt; 888 observations at roughly 135 per response is about seven successful polls in sixty seconds, not thirty. Two-thirds of its requests were failing and the system reported nothing, because there were no sensor-health counters at all — a failed poll produces no data, which looks exactly like quiet airspace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It never aged out a single track.&lt;/strong&gt; Zero drops in both windows. Of course not: if every measurement is stamped with arrival time, every track's last-seen time is always now, so nothing is ever stale. A track whose aircraft stopped reporting forty seconds ago sat on the display looking as current as everything else.&lt;/p&gt;

&lt;p&gt;Those counters were added to report on something else entirely — a limit on response body size. They immediately found a different fault. That is the lesson I would take from this project ahead of the timestamp one: &lt;strong&gt;observability added for one problem finds problems you were not looking for, and a system with no failure counters cannot tell you the difference between "nothing is happening" and "nothing is working".&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check in your own pipeline
&lt;/h2&gt;

&lt;p&gt;If you ingest anything from a source that reports its own staleness — market data, IoT sensors, GPS, log shipping, any polled API with a timestamp in the payload — check whether your pipeline records when the record &lt;em&gt;arrived&lt;/em&gt; instead of when it was &lt;em&gt;observed&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The failure mode is nasty specifically because it is quiet. Nothing crashes. No error is logged. Your validation layer does its job perfectly and deletes your good data, and if the rejections are counted anywhere at all you will read the number as evidence that the validation is working.&lt;/p&gt;

&lt;p&gt;And a constant lag hides it completely. It is the jitter that bites — which means the systems most likely to have this bug are the ones whose feeds are &lt;em&gt;usually&lt;/em&gt; fast.&lt;/p&gt;




&lt;p&gt;MIT, 55 tests, clippy clean at &lt;code&gt;-D warnings&lt;/code&gt;: &lt;a href="https://github.com/Mormolykos/aether" rel="noopener noreferrer"&gt;github.com/Mormolykos/aether&lt;/a&gt;. Surveillance and state estimation only — no targeting, engagement or weapon functionality of any kind. The README carries the full measured results and a limitations section considerably longer than this post.&lt;/p&gt;

&lt;p&gt;More measured write-ups of the same kind, including the retrieval work and the long-document RAG ablation, are at &lt;a href="https://tts.bedvibe.studio/portfolio/" rel="noopener noreferrer"&gt;tts.bedvibe.studio/portfolio&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>architecture</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your TTS Model Sounds Great — Until It Says "GPUB"</title>
      <dc:creator>Panagiotis Gkilis</dc:creator>
      <pubDate>Tue, 25 Aug 2026 03:10:05 +0000</pubDate>
      <link>https://dev.to/bedvibe_studios/your-tts-model-sounds-great-until-it-says-gpub-1d77</link>
      <guid>https://dev.to/bedvibe_studios/your-tts-model-sounds-great-until-it-says-gpub-1d77</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://ai.bedvibe.studio/ttsproof/" rel="noopener noreferrer"&gt;ai.bedvibe.studio&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I built a text-to-speech product and kept getting burned by the same thing. On normal sentences the model sounded great. Then it would hit a number, a date, an acronym or a name, and quietly mangle it.&lt;/p&gt;

&lt;p&gt;Worse, the metric everyone reaches for — Word Error Rate — was lying to me in &lt;strong&gt;both&lt;/strong&gt; directions. It flagged perfectly good audio as broken because the script said &lt;code&gt;3:30 PM&lt;/code&gt; and the transcript said "three thirty pee em." And it &lt;em&gt;missed&lt;/em&gt; real failures on short tokens, where the speech recogniser is as unreliable as the TTS.&lt;/p&gt;

&lt;p&gt;So I wrote the QA framework I wished I had, packaged it as &lt;strong&gt;ttsproof&lt;/strong&gt;, and then ran it as a blind study against a production TTS service so the results would be more than an opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two failures WER cannot see
&lt;/h2&gt;

&lt;p&gt;A TTS pipeline breaks in two different ways, and a single WER number blurs both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structural defects.&lt;/strong&gt; The clip is empty, truncated, three times too long, stuck in a repeated-chunk loop, clipping, or has a click at the tail. These have nothing to do with pronunciation — you can catch them with no model at all, straight from the waveform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pronunciation and content errors&lt;/strong&gt; on the hard cases: numbers, decimals, dates, clock times, acronyms, single letters, URLs, names.&lt;/p&gt;

&lt;p&gt;ttsproof splits them apart and handles each one honestly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structural checks, no model needed&lt;/strong&gt; — empty or truncated audio, duration explosions, long internal silences, clipping, loop detection, end-of-clip artifacts. numpy and soundfile, nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Equivalence-aware WER/CER&lt;/strong&gt; — the expected text and the ASR transcript are &lt;em&gt;both&lt;/em&gt; canonicalised to spoken form before scoring, so &lt;code&gt;3:30 PM&lt;/code&gt; against "three thirty" stops counting as an error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASR-uncertainty quarantine&lt;/strong&gt; — when the audio is structurally clean but the recogniser disagrees on a very short utterance, the sample is set aside for a human instead of being auto-failed. At that length the ASR is as likely to be wrong as the TTS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The study: 390 samples, and a blind human check
&lt;/h2&gt;

&lt;p&gt;I evaluated the method against a production neural TTS service — &lt;strong&gt;130 edge cases × 3 voices = 390 samples&lt;/strong&gt; — and published it as a citable technical report (&lt;a href="https://doi.org/10.5281/zenodo.20757553" rel="noopener noreferrer"&gt;DOI 10.5281/zenodo.20757553&lt;/a&gt;, CC-BY-4.0).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero structural audio-integrity defects&lt;/strong&gt; across all 390 clips. That matters: it means every failure that did exist was pronunciation, exactly the kind WER mislabels.&lt;/li&gt;
&lt;li&gt;Exact-match rate &lt;strong&gt;0.769&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Then the honest part — a &lt;strong&gt;blind human review&lt;/strong&gt; of the ASR-uncertain quarantine zone. Of 42 uncertain clips, &lt;strong&gt;23 (55%) were ASR false negatives&lt;/strong&gt; (the TTS said it right and the recogniser misheard) and &lt;strong&gt;19 (45%) were genuine TTS mispronunciations.&lt;/strong&gt; Fifteen control clips came back 15/15 correct, so the rater was reliable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That 45/55 split is the entire argument for having a quarantine verdict at all. Auto-passing that zone ships 19 real mispronunciations. Naive ASR-WER auto-failing it wrongly kills 23 correct clips. Neither is acceptable, so ttsproof refuses to guess there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the real failures looked like
&lt;/h2&gt;

&lt;p&gt;All 19 genuine failures were short isolated letters and acronyms, and the pattern is oddly specific:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A-vowel substitution&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NATO&lt;/code&gt; → "NITO", &lt;code&gt;USA&lt;/code&gt; → "USI", &lt;code&gt;CIA&lt;/code&gt; → "CII"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trailing appended phoneme&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GPU&lt;/code&gt; → "GPUB", &lt;code&gt;EU&lt;/code&gt; → "EUU"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Early truncation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;R&lt;/code&gt; chopped short&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Doubling&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;X&lt;/code&gt; said twice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other substitution&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CEO&lt;/code&gt; → "CEE", &lt;code&gt;Z&lt;/code&gt; → "SZ"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The structural detectors did &lt;strong&gt;not&lt;/strong&gt; fire on any of these. "GPUB" is intelligible speech, not a click. Structural checks and ASR quarantine are complementary; neither alone catches everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark any engine in one command
&lt;/h2&gt;

&lt;p&gt;Beyond the study, ttsproof ships a corpus of &lt;strong&gt;817 curated edge cases across 39 categories&lt;/strong&gt; — numbers, currencies, dates, ISO timestamps, phone numbers, URLs, file paths, pronunciation-torture words (Worcestershire, synecdoche), proper names (Reykjavík, Nguyễn), Greek, Norwegian and more. The corpus is versioned independently of the software, so published scores stay comparable across tool updates.&lt;/p&gt;

&lt;p&gt;It is engine-agnostic — point it at any TTS via a command template, or at a folder of audio you already generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ttsproof benchmark &lt;span class="nt"&gt;--cmd&lt;/span&gt; &lt;span class="s2"&gt;"mytts --text {text} --wav {out}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a category scoreboard, a self-contained &lt;code&gt;report.html&lt;/code&gt; with waveforms, an audio player and what the ASR actually heard, and a CI regression gate. Closed-source engines work too through a SpeechSDK wrapper — an integration a user suggested after the first release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ttsproof            &lt;span class="c"&gt;# structural checks + metrics + corpus&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"ttsproof[asr]"&lt;/span&gt;     &lt;span class="c"&gt;# + faster-whisper for pronunciation gating&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/Mormolykos/ttsproof" rel="noopener noreferrer"&gt;github.com/Mormolykos/ttsproof&lt;/a&gt; (MIT)&lt;/li&gt;
&lt;li&gt;The study: &lt;a href="https://doi.org/10.5281/zenodo.20757553" rel="noopener noreferrer"&gt;doi.org/10.5281/zenodo.20757553&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It has already had its first outside contribution, a community fix for a real number-formatting bug, which is exactly what I hoped for. If your TTS breaks on something, open an issue with the case — the corpus grows from real failures.&lt;/p&gt;

&lt;p&gt;The text-to-speech platform this came out of is live at &lt;a href="https://tts.bedvibe.studio/" rel="noopener noreferrer"&gt;tts.bedvibe.studio&lt;/a&gt; — ttsproof exists because I needed to QA that, and the &lt;a href="https://tts.bedvibe.studio/portfolio/bedvibe-tts/" rel="noopener noreferrer"&gt;model's engineering write-up&lt;/a&gt; covers what it is built on.&lt;/p&gt;

</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>testing</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
