NagaYu commited on
Commit
2f0e27b
·
verified ·
1 Parent(s): b46c545

Closed at this problem framing: converged results beside pre-convergence ones, empty-result and competitiveness notice, viewer with unseen conditions

Browse files
Files changed (46) hide show
  1. .gitattributes +4 -0
  2. README.md +18 -5
  3. data.json +0 -0
  4. figures/fig1_recall_by_condition.dark.png +2 -2
  5. figures/fig1_recall_by_condition.light.png +2 -2
  6. figures/fig1_recall_by_condition.test_heldout.dark.png +0 -0
  7. figures/fig1_recall_by_condition.test_heldout.light.png +0 -0
  8. figures/fig1_recall_by_condition.test_heldout_v2.dark.png +0 -0
  9. figures/fig1_recall_by_condition.test_heldout_v2.light.png +0 -0
  10. figures/fig2_union_complementarity.dark.png +0 -0
  11. figures/fig2_union_complementarity.light.png +0 -0
  12. figures/fig2_union_complementarity.test_heldout.dark.png +0 -0
  13. figures/fig2_union_complementarity.test_heldout.light.png +0 -0
  14. figures/fig2_union_complementarity.test_heldout_v2.dark.png +0 -0
  15. figures/fig2_union_complementarity.test_heldout_v2.light.png +0 -0
  16. figures/fig3_recall_by_category.dark.png +0 -0
  17. figures/fig3_recall_by_category.light.png +0 -0
  18. figures/fig3_recall_by_category.test_heldout.dark.png +0 -0
  19. figures/fig3_recall_by_category.test_heldout.light.png +0 -0
  20. figures/fig3_recall_by_category.test_heldout_v2.dark.png +0 -0
  21. figures/fig3_recall_by_category.test_heldout_v2.light.png +0 -0
  22. figures/fig4_recall_vs_false_alarms.dark.png +0 -0
  23. figures/fig4_recall_vs_false_alarms.light.png +0 -0
  24. figures/fig4_recall_vs_false_alarms.test_heldout.dark.png +0 -0
  25. figures/fig4_recall_vs_false_alarms.test_heldout.light.png +0 -0
  26. figures/fig4_recall_vs_false_alarms.test_heldout_v2.dark.png +0 -0
  27. figures/fig4_recall_vs_false_alarms.test_heldout_v2.light.png +0 -0
  28. figures/pre-convergence/fig1_recall_by_condition.dark.png +3 -0
  29. figures/pre-convergence/fig1_recall_by_condition.light.png +3 -0
  30. figures/pre-convergence/fig1_recall_by_condition.test_heldout.dark.png +0 -0
  31. figures/pre-convergence/fig1_recall_by_condition.test_heldout.light.png +0 -0
  32. figures/pre-convergence/fig2_union_complementarity.dark.png +0 -0
  33. figures/pre-convergence/fig2_union_complementarity.light.png +0 -0
  34. figures/pre-convergence/fig2_union_complementarity.test_heldout.dark.png +0 -0
  35. figures/pre-convergence/fig2_union_complementarity.test_heldout.light.png +0 -0
  36. figures/pre-convergence/fig3_recall_by_category.dark.png +0 -0
  37. figures/pre-convergence/fig3_recall_by_category.light.png +0 -0
  38. figures/pre-convergence/fig3_recall_by_category.test_heldout.dark.png +0 -0
  39. figures/pre-convergence/fig3_recall_by_category.test_heldout.light.png +0 -0
  40. figures/pre-convergence/fig4_recall_vs_false_alarms.dark.png +0 -0
  41. figures/pre-convergence/fig4_recall_vs_false_alarms.light.png +0 -0
  42. figures/pre-convergence/fig4_recall_vs_false_alarms.test_heldout.dark.png +0 -0
  43. figures/pre-convergence/fig4_recall_vs_false_alarms.test_heldout.light.png +0 -0
  44. figures/val_curve.dark.png +3 -0
  45. figures/val_curve.light.png +3 -0
  46. index.html +145 -66
.gitattributes CHANGED
@@ -35,3 +35,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  figures/fig1_recall_by_condition.dark.png filter=lfs diff=lfs merge=lfs -text
37
  figures/fig1_recall_by_condition.light.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  figures/fig1_recall_by_condition.dark.png filter=lfs diff=lfs merge=lfs -text
37
  figures/fig1_recall_by_condition.light.png filter=lfs diff=lfs merge=lfs -text
38
+ figures/pre-convergence/fig1_recall_by_condition.dark.png filter=lfs diff=lfs merge=lfs -text
39
+ figures/pre-convergence/fig1_recall_by_condition.light.png filter=lfs diff=lfs merge=lfs -text
40
+ figures/val_curve.dark.png filter=lfs diff=lfs merge=lfs -text
41
+ figures/val_curve.light.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -18,17 +18,30 @@ tags:
18
  - redaction
19
  - speech
20
  - negative-results
 
21
  ---
22
 
23
  # Bleep
24
 
25
  **Find *where* sensitive speech is, without transcribing *what* was said.**
26
 
27
- A results dashboard for a published **negative result**: at the ASR baseline's
28
- own measured false-alarm rate, Bleep reaches 0.16 recall against the baseline's
29
- 0.83. The page sets out the three separable reasons the benchmark was built to
30
- distinguish, and includes a timeline viewer over real test utterances showing
31
- ground truth against what each detector actually found.
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  No audio is shipped — the corpus was rendered with macOS system voices, which
34
  are licensed for use and not for republication. The viewer draws a downsampled
 
18
  - redaction
19
  - speech
20
  - negative-results
21
+ - closed
22
  ---
23
 
24
  # Bleep
25
 
26
  **Find *where* sensitive speech is, without transcribing *what* was said.**
27
 
28
+ > **An empty result means nothing was flagged. It does not mean nothing is there.**
29
+ >
30
+ > This detector is not competitive with an ASR-based pipeline outside the
31
+ > conditions it was trained on. See the
32
+ > [model card](https://huggingface.co/NagaYu/bleep-0.09b).
33
+
34
+ **Status: closed at this problem framing.** A perfect transcript fed to the
35
+ same text rules scores 0.986, so the room above the ASR baseline (0.826) is
36
+ bounded by the ASR's word errors. Retrained to convergence on 2026-10-18,
37
+ Bleep reaches 0.903 at the baseline's false-alarm rate on the degradation types
38
+ it was trained on (0.156 before convergence); on unseen ones it raises 3.3–3.5×
39
+ the baseline's false alarms.
40
+
41
+ A results dashboard with a timeline viewer over real test utterances — from
42
+ conditions the model was trained on and from conditions it never saw — showing
43
+ ground truth against what each detector actually found, with the
44
+ pre-convergence model drawn as its own lane.
45
 
46
  No audio is shipped — the corpus was rendered with macOS system voices, which
47
  are licensed for use and not for republication. The viewer draws a downsampled
data.json CHANGED
The diff for this file is too large to render. See raw diff
 
figures/fig1_recall_by_condition.dark.png CHANGED

Git LFS Details

  • SHA256: 37696246a3db9f2f23906448461fc1b6e11d9e1062642d2a4142fd7e309cad7d
  • Pointer size: 131 Bytes
  • Size of remote file: 105 kB

Git LFS Details

  • SHA256: b38683c2423684d56d94674a8977ae10a98554dab382d6fad8e8be5328a147fb
  • Pointer size: 131 Bytes
  • Size of remote file: 104 kB
figures/fig1_recall_by_condition.light.png CHANGED

Git LFS Details

  • SHA256: 52c9ee7ba550e97f804d9f12e00c447ae2a7211dc7f629147e24f8b5fee6afbb
  • Pointer size: 131 Bytes
  • Size of remote file: 107 kB

Git LFS Details

  • SHA256: 994f2d759077a484dd2ecf4ef184517e811dc358a98ca9f655dd15e6a8354306
  • Pointer size: 131 Bytes
  • Size of remote file: 106 kB
figures/fig1_recall_by_condition.test_heldout.dark.png CHANGED
figures/fig1_recall_by_condition.test_heldout.light.png CHANGED
figures/fig1_recall_by_condition.test_heldout_v2.dark.png ADDED
figures/fig1_recall_by_condition.test_heldout_v2.light.png ADDED
figures/fig2_union_complementarity.dark.png CHANGED
figures/fig2_union_complementarity.light.png CHANGED
figures/fig2_union_complementarity.test_heldout.dark.png CHANGED
figures/fig2_union_complementarity.test_heldout.light.png CHANGED
figures/fig2_union_complementarity.test_heldout_v2.dark.png ADDED
figures/fig2_union_complementarity.test_heldout_v2.light.png ADDED
figures/fig3_recall_by_category.dark.png CHANGED
figures/fig3_recall_by_category.light.png CHANGED
figures/fig3_recall_by_category.test_heldout.dark.png CHANGED
figures/fig3_recall_by_category.test_heldout.light.png CHANGED
figures/fig3_recall_by_category.test_heldout_v2.dark.png ADDED
figures/fig3_recall_by_category.test_heldout_v2.light.png ADDED
figures/fig4_recall_vs_false_alarms.dark.png CHANGED
figures/fig4_recall_vs_false_alarms.light.png CHANGED
figures/fig4_recall_vs_false_alarms.test_heldout.dark.png CHANGED
figures/fig4_recall_vs_false_alarms.test_heldout.light.png CHANGED
figures/fig4_recall_vs_false_alarms.test_heldout_v2.dark.png ADDED
figures/fig4_recall_vs_false_alarms.test_heldout_v2.light.png ADDED
figures/pre-convergence/fig1_recall_by_condition.dark.png ADDED

Git LFS Details

  • SHA256: 37696246a3db9f2f23906448461fc1b6e11d9e1062642d2a4142fd7e309cad7d
  • Pointer size: 131 Bytes
  • Size of remote file: 105 kB
figures/pre-convergence/fig1_recall_by_condition.light.png ADDED

Git LFS Details

  • SHA256: 52c9ee7ba550e97f804d9f12e00c447ae2a7211dc7f629147e24f8b5fee6afbb
  • Pointer size: 131 Bytes
  • Size of remote file: 107 kB
figures/pre-convergence/fig1_recall_by_condition.test_heldout.dark.png ADDED
figures/pre-convergence/fig1_recall_by_condition.test_heldout.light.png ADDED
figures/pre-convergence/fig2_union_complementarity.dark.png ADDED
figures/pre-convergence/fig2_union_complementarity.light.png ADDED
figures/pre-convergence/fig2_union_complementarity.test_heldout.dark.png ADDED
figures/pre-convergence/fig2_union_complementarity.test_heldout.light.png ADDED
figures/pre-convergence/fig3_recall_by_category.dark.png ADDED
figures/pre-convergence/fig3_recall_by_category.light.png ADDED
figures/pre-convergence/fig3_recall_by_category.test_heldout.dark.png ADDED
figures/pre-convergence/fig3_recall_by_category.test_heldout.light.png ADDED
figures/pre-convergence/fig4_recall_vs_false_alarms.dark.png ADDED
figures/pre-convergence/fig4_recall_vs_false_alarms.light.png ADDED
figures/pre-convergence/fig4_recall_vs_false_alarms.test_heldout.dark.png ADDED
figures/pre-convergence/fig4_recall_vs_false_alarms.test_heldout.light.png ADDED
figures/val_curve.dark.png ADDED

Git LFS Details

  • SHA256: 88d90bd5d361327aba4a525e07c74bf74080d0b9a152021279eb8950fd3fd98d
  • Pointer size: 131 Bytes
  • Size of remote file: 160 kB
figures/val_curve.light.png ADDED

Git LFS Details

  • SHA256: 8d36f9c863555cea4623b5670eaa7185962ff4f941598e0f695720d6ff92af3b
  • Pointer size: 131 Bytes
  • Size of remote file: 166 kB
index.html CHANGED
@@ -46,6 +46,10 @@
46
  border:1px solid var(--rule);border-radius:9px;text-decoration:none;
47
  color:var(--ink);background:var(--panel);font-size:.92rem}
48
  .links a:hover{border-color:var(--a)}
 
 
 
 
49
  .callout{background:var(--warn-bg);border-left:3px solid var(--warn-br);
50
  border-radius:0 10px 10px 0;padding:18px 22px;margin:26px 0}
51
  .callout h2{margin-top:0}
@@ -92,42 +96,63 @@
92
  <a href="https://huggingface.co/datasets/NagaYu/bleep-spans">📊 Dataset</a>
93
  </div>
94
 
 
 
 
 
 
 
 
95
  <div class="callout">
96
- <h2>This is a published negative result</h2>
97
- <p>At the ASR baseline's own measured false-alarm rate, on a speaker-disjoint
98
- test split, <strong>Bleep loses badly</strong>. That is the finding, and it
99
- belongs at the top rather than in a footnote.</p>
 
 
 
 
 
100
  <div class="tablewrap"><table id="armTable"></table></div>
101
  <p class="note" id="armNote"></p>
102
  </div>
103
 
104
- <h2>Why, specifically</h2>
105
- <p>The benchmark was built to separate the reasons, not just report the
106
- number.</p>
107
 
108
- <h3>1 · The failure mode is real, but the corpus put it in the wrong place</h3>
109
- <p id="whyCond">…</p>
110
 
111
- <h3>2 · The text rules were never the bottleneck</h3>
112
- <p>An <em>oracle</em> arm — a perfect transcript fed to the same text-PII
113
- rules — scores 0.96–1.00 in every condition. So every baseline miss is an ASR
114
- <em>word</em> error rather than a detector gap. That one arm turns “the
115
- baseline failed” into “the baseline failed here, for this reason”.</p>
116
 
117
- <h3>3 · The model is badly undertrained</h3>
118
  <p>1500 steps, batch 4, ~1.8 epochs over 9.15 hours — 70 minutes on one
119
- consumer GPU. Validation loss was still falling monotonically at the final
120
- step (0.218 → 0.183): a stopped run, not a converged one.</p>
 
 
 
 
 
 
 
 
 
 
121
 
122
  <h3>One directional hint</h3>
123
  <p id="whyCat">…</p>
124
 
125
- <h2>What did hold up</h2>
126
  <p id="heldUp">…</p>
127
 
128
  <h2>Timeline viewer</h2>
129
- <p>Real utterances from the test split. Ground truth against what each
130
- detector actually found, at the operating points the benchmark reports.</p>
 
 
 
131
  <p class="note"><strong>No audio is shipped.</strong> These recordings were
132
  rendered with the macOS system voices, which are licensed for use and not for
133
  republication as a corpus. What you see is a downsampled amplitude envelope —
@@ -140,7 +165,8 @@
140
  <div class="legend">
141
  <span><i class="sw" style="background:var(--neutral)"></i>ground truth</span>
142
  <span><i class="sw" style="background:var(--a)"></i>ASR + text PII</span>
143
- <span><i class="sw" style="background:var(--c)"></i>Bleep</span>
 
144
  <span><i class="sw" style="background:var(--d)"></i>union</span>
145
  </div>
146
  </div>
@@ -150,15 +176,16 @@
150
  <img src="figures/fig1_recall_by_condition.light.png"
151
  alt="Recall by degradation condition for each arm, training conditions"
152
  id="figCondLight">
153
- <figcaption>Training conditions. The subtitle is generated from the data, so
154
- the chart cannot assert a conclusion the numbers do not support — it prints
155
- the opposite of the project's hypothesis.</figcaption>
156
  </figure>
157
  <figure>
158
- <img src="figures/fig1_recall_by_condition.test_heldout.light.png"
159
- alt="Recall by held-out degradation condition" id="figHeldLight">
160
- <figcaption>Held-out conditions — the more informative set. The ASR arm's
161
- collapse under heavy overlap appears only here.</figcaption>
 
162
  </figure>
163
 
164
  <h2>Recall by category</h2>
@@ -166,24 +193,32 @@
166
  <img src="figures/fig3_recall_by_category.light.png"
167
  alt="Recall by category of sensitive speech" id="figCatLight">
168
  <figcaption>★ marks the failure modes reported for transcription-first
169
- pipelines.</figcaption>
 
170
  </figure>
171
 
172
  <h2>What this is not</h2>
173
- <p><strong>Not a redaction guarantee.</strong> It misses most things. An empty
174
- result means “nothing was flagged”, never “nothing is there”.</p>
175
- <p><strong>Not a replacement for your ASR-based detector.</strong> It is
176
- designed for union mode, and the union is proven never to drop a region the
177
- existing detector found — zero violations across both splits.</p>
 
 
 
 
 
178
  <p><strong>Not trained on real audio.</strong> Every recording is synthetic.
179
  No real call, no clinical recording, nothing derived from either, at any
180
  stage. That is also a limitation: TTS speech is far easier to transcribe than
181
  conversation, which is part of why the baseline looks so strong here.</p>
 
 
182
 
183
  <div class="foot">
184
  <p id="foot">…</p>
185
  <p>Apache-2.0. Negative results are cheap to hide and expensive to
186
- rediscover.</p>
187
  </div>
188
  </div>
189
 
@@ -194,74 +229,116 @@ const el = (t, a = {}) => { const n = document.createElementNS(NS, t);
194
  const f3 = v => (v === null || v === undefined || Number.isNaN(v)) ? "—" : v.toFixed(3);
195
 
196
  fetch("data.json").then(r => r.json()).then(D => {
197
- const S = D.summary.test, m = S.matched_fa, arms = S.arms, comp = S.complementarity;
198
-
199
- // ---- arm table -------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
200
  const rows = [
201
  ["A — ASR (whisper-tiny.en) + text PII", arms.A, false],
202
  ["B — ASR (whisper-small.en) + text PII", arms.B, false],
203
- ["C — Bleep alone", arms.C, true],
204
- ["D — A ∪ Bleep", arms.D, false],
 
 
205
  ["oracle — perfect transcript + same rules", arms.oracle, false],
206
  ].filter(r => r[1]);
207
  const t = document.getElementById("armTable");
208
- t.innerHTML = "<tr><th>arm</th><th style='text-align:right'>recall</th>"
209
  + "<th style='text-align:right'>false alarms/hour</th></tr>"
210
  + rows.map(([name, a, hero]) =>
211
  `<tr class="${hero ? "hero" : ""}"><td>${name}</td>`
212
  + `<td class="num">${f3(a.recall)}</td>`
213
- + `<td class="num">${a.fa_per_hour.toFixed(1)}</td></tr>`).join("");
214
  document.getElementById("armNote").textContent =
215
- `${S.n_utterances} utterances (${S.audio_hours.toFixed(2)} h). Bleep's `
216
- + `threshold was calibrated on a disjoint split to match arm A's measured `
217
- + `false-alarm rate of ${m.arm_a_fa_per_hour_on_test.toFixed(1)}/hour, so the `
218
- + `comparison is like-for-like. Of the regions arm A missed, Bleep found `
219
- + `${(comp.asr_misses_rescued_by_bleep * 100).toFixed(0)}%.`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
  // ---- data-derived prose ---------------------------------------------
222
- const pc = S.per_condition, ph = D.summary.test_heldout.per_condition;
223
  document.getElementById("whyCond").textContent =
224
  `On the training-condition test split the ASR baseline is essentially flat: `
225
  + `${f3(pc.clean.A)} on clean audio and ${f3(pc.telephony_8k.A)} at 8 kHz — `
226
  + `i.e. better than clean. A band-limit plus G.711 is not a hard condition `
227
- + `for a modern ASR on hyper-articulated TTS speech. The collapse appears `
228
  + `only in the harsher held-out conditions: `
229
  + `${f3(ph.overlap_0db_three_way.A)} at 0 dB three-way overlap and `
230
- + `${f3(ph.telephony_overlap_3db.A)} at narrowband-plus-overlap. The premise `
231
- + `survives; the training conditions were too mild to exercise it.`;
 
 
232
 
233
- const cat = S.per_category;
234
- const best = Object.entries(cat).filter(([, v]) => v.C !== null)
235
  .sort((a, b) => b[1].C - a[1].C).slice(0, 2);
236
  document.getElementById("whyCat").textContent =
237
- `Bleep's per-category recall has the shape the acoustic argument predicts. `
238
- + `Its best categories are ${best.map(([k, v]) =>
239
  k.replace(/_/g, " ").toLowerCase() + " (" + f3(v.C) + ")").join(" and ")}`
240
  + ` — the two with the most distinctive rhythm — against ~0.00 for run-on `
241
- + `digits, money and identifiers. The cue family it managed to learn is the `
242
- + `predicted one. There is just far too little of it to be useful.`;
243
 
244
  const sp = S.speed.bleep, asr = S.speed.asr_arms_rtf;
245
  document.getElementById("heldUp").textContent =
246
  `Speed: ${sp.rtf_median} real-time factor on CPU with the front end `
247
  + `included — ${sp.xrealtime_best}× real time, `
248
  + `${(asr.A / sp.rtf_median).toFixed(1)}× faster than the cheapest ASR arm `
249
- + `and ${(asr.B / sp.rtf_median).toFixed(0)}× faster than the better one. `
250
- + `${(sp.param_count / 1e6).toFixed(1)} M parameters, inside the committed `
251
- + `0.05–0.15 B band. The union containment guarantee held with zero `
252
- + `violations across both splits, and the no-content invariant is enforced `
253
- + `structurally by tests rather than asserted.`;
 
 
 
 
254
 
255
  document.getElementById("foot").textContent =
256
- `Benchmark generated ${D.generated_utc}. Every figure on this page is `
257
- + `produced by scripts in the repository from the benchmark's own JSON.`;
 
258
 
259
  // ---- timeline viewer -------------------------------------------------
260
  const pick = document.getElementById("pick");
261
  D.examples.forEach((e, i) => {
262
  const o = document.createElement("option");
263
  o.value = i;
264
- o.textContent = `${e.condition.replace(/_/g, " ")} — ${e.duration.toFixed(1)}s `
 
265
  + `(${e.truth.length} sensitive region${e.truth.length === 1 ? "" : "s"})`;
266
  pick.appendChild(o);
267
  });
@@ -270,6 +347,7 @@ fetch("data.json").then(r => r.json()).then(D => {
270
  ["ground truth", "truth", "var(--neutral)"],
271
  ["ASR + text PII", "asr", "var(--a)"],
272
  ["Bleep", "bleep", "var(--c)"],
 
273
  ["union", "union", "var(--d)"],
274
  ];
275
 
@@ -345,7 +423,8 @@ fetch("data.json").then(r => r.json()).then(D => {
345
  // dark-mode figure swap
346
  const swap = dark => {
347
  [["figCondLight", "fig1_recall_by_condition"],
348
- ["figHeldLight", "fig1_recall_by_condition.test_heldout"],
 
349
  ["figCatLight", "fig3_recall_by_category"]].forEach(([id, base]) => {
350
  const n = document.getElementById(id);
351
  if (n) n.src = `figures/${base}.${dark ? "dark" : "light"}.png`;
 
46
  border:1px solid var(--rule);border-radius:9px;text-decoration:none;
47
  color:var(--ink);background:var(--panel);font-size:.92rem}
48
  .links a:hover{border-color:var(--a)}
49
+ .banner{border:2px solid var(--warn-br);background:var(--warn-bg);border-radius:12px;
50
+ padding:14px 18px;margin:18px 0 6px;font-size:1rem}
51
+ .banner p{margin:0 0 4px}
52
+ .banner p:last-child{margin:0}
53
  .callout{background:var(--warn-bg);border-left:3px solid var(--warn-br);
54
  border-radius:0 10px 10px 0;padding:18px 22px;margin:26px 0}
55
  .callout h2{margin-top:0}
 
96
  <a href="https://huggingface.co/datasets/NagaYu/bleep-spans">📊 Dataset</a>
97
  </div>
98
 
99
+ <div class="banner" role="note">
100
+ <p><strong>An empty result means nothing was flagged. It does not mean nothing is there.</strong></p>
101
+ <p>This detector is not competitive with an ASR-based pipeline outside the
102
+ conditions it was trained on. See the
103
+ <a href="https://huggingface.co/NagaYu/bleep-0.09b">model card</a>.</p>
104
+ </div>
105
+
106
  <div class="callout">
107
+ <h2>STATUS: closed at this problem framing</h2>
108
+ <p><strong>The oracle arm — a perfect transcript fed to the same text rules —
109
+ scores <span id="oracleRecall">…</span>. <span id="missSplit">…</span> At this
110
+ problem framing the room above the baseline is bounded by that gap —
111
+ <span id="headroom">…</span> recall on the test split — and this is the
112
+ measurement that establishes it.</strong></p>
113
+ <p>Retrained to convergence on 2026-10-18. Pre-convergence numbers are kept
114
+ beside the new ones. These are final; there will not be a third training
115
+ run.</p>
116
  <div class="tablewrap"><table id="armTable"></table></div>
117
  <p class="note" id="armNote"></p>
118
  </div>
119
 
120
+ <h2>Where it does not hold</h2>
121
+ <p id="heldoutText">…</p>
122
+ <div class="tablewrap"><table id="heldTable"></table></div>
123
 
124
+ <h2>Why it was negative before convergence</h2>
 
125
 
126
+ <h3>The training conditions were too mild</h3>
127
+ <p id="whyCond">…</p>
 
 
 
128
 
129
+ <h3>The model was badly undertrained</h3>
130
  <p>1500 steps, batch 4, ~1.8 epochs over 9.15 hours — 70 minutes on one
131
+ consumer GPU. The validation loss recorded during that run (0.218, 0.200,
132
+ 0.191, 0.203, 0.192, 0.183) was measured on freshly re-sampled crops, rose
133
+ once, and ended under a learning rate already annealed to 2% of its peak: a
134
+ stopped run, not a converged one. (This page previously said the loss was
135
+ "still falling monotonically"; that was not accurate.) The retrain ran to a
136
+ stopping rule fixed and hashed before it started, and stopped by that rule.</p>
137
+ <figure>
138
+ <img src="figures/val_curve.light.png" alt="Dev loss against training step for the retrain"
139
+ id="figValLight">
140
+ <figcaption>The retrain's dev loss on a fixed set of crops. The dashed line
141
+ is the pre-convergence checkpoint on the same crops.</figcaption>
142
+ </figure>
143
 
144
  <h3>One directional hint</h3>
145
  <p id="whyCat">…</p>
146
 
147
+ <h2>What is complete and load-bearing</h2>
148
  <p id="heldUp">…</p>
149
 
150
  <h2>Timeline viewer</h2>
151
+ <p>Real utterances: the first ones from <code>test</code> (conditions the
152
+ model was trained on) and from <code>test_heldout_v2</code> (conditions it
153
+ never saw). Ground truth against what each detector actually found, at the
154
+ operating points the benchmark reports. The pre-convergence model has its own
155
+ lane.</p>
156
  <p class="note"><strong>No audio is shipped.</strong> These recordings were
157
  rendered with the macOS system voices, which are licensed for use and not for
158
  republication as a corpus. What you see is a downsampled amplitude envelope —
 
165
  <div class="legend">
166
  <span><i class="sw" style="background:var(--neutral)"></i>ground truth</span>
167
  <span><i class="sw" style="background:var(--a)"></i>ASR + text PII</span>
168
+ <span><i class="sw" style="background:var(--c)"></i>Bleep (converged)</span>
169
+ <span><i class="sw" style="background:var(--b)"></i>Bleep (before convergence)</span>
170
  <span><i class="sw" style="background:var(--d)"></i>union</span>
171
  </div>
172
  </div>
 
176
  <img src="figures/fig1_recall_by_condition.light.png"
177
  alt="Recall by degradation condition for each arm, training conditions"
178
  id="figCondLight">
179
+ <figcaption>Training conditions, converged model. The subtitle is generated
180
+ from the data, so the chart cannot assert a conclusion the numbers do not
181
+ support.</figcaption>
182
  </figure>
183
  <figure>
184
+ <img src="figures/fig4_recall_vs_false_alarms.test_heldout_v2.light.png"
185
+ alt="Recall against false-alarm rate on unseen degradations" id="figFaLight">
186
+ <figcaption>Unseen conditions: the converged model's whole recall/false-alarm
187
+ curve sits to the right of the ASR baseline — no threshold reaches the
188
+ baseline's false-alarm rate.</figcaption>
189
  </figure>
190
 
191
  <h2>Recall by category</h2>
 
193
  <img src="figures/fig3_recall_by_category.light.png"
194
  alt="Recall by category of sensitive speech" id="figCatLight">
195
  <figcaption>★ marks the failure modes reported for transcription-first
196
+ pipelines. Converged model; the pre-convergence figures are in
197
+ <code>figures/pre-convergence/</code>.</figcaption>
198
  </figure>
199
 
200
  <h2>What this is not</h2>
201
+ <p><strong>Not a redaction guarantee.</strong> Even on the conditions it was
202
+ trained on it misses about one region in ten; on conditions it was not, roughly
203
+ one in five to one in four, while raising several times the baseline's false
204
+ alarms. An empty result means “nothing was flagged”, never “nothing is
205
+ there”.</p>
206
+ <p><strong>Not a replacement for your ASR-based detector.</strong> It beats the
207
+ baseline only on the degradation types it was trained on. It is designed for
208
+ union mode, and the union is verified never to drop a region the existing
209
+ detector found — zero violations on every utterance of all three evaluation
210
+ splits.</p>
211
  <p><strong>Not trained on real audio.</strong> Every recording is synthetic.
212
  No real call, no clinical recording, nothing derived from either, at any
213
  stage. That is also a limitation: TTS speech is far easier to transcribe than
214
  conversation, which is part of why the baseline looks so strong here.</p>
215
+ <p><strong>English only.</strong> Japanese generators exist in the code but are
216
+ not benchmarked.</p>
217
 
218
  <div class="foot">
219
  <p id="foot">…</p>
220
  <p>Apache-2.0. Negative results are cheap to hide and expensive to
221
+ rediscover; closed projects are worth closing in writing.</p>
222
  </div>
223
  </div>
224
 
 
229
  const f3 = v => (v === null || v === undefined || Number.isNaN(v)) ? "—" : v.toFixed(3);
230
 
231
  fetch("data.json").then(r => r.json()).then(D => {
232
+ const S = D.summary.test, B = D.summary_before.test;
233
+ const m = S.matched_fa, arms = S.arms, comp = S.complementarity;
234
+ const f1 = v => (v === null || v === undefined) ? "—" : v.toFixed(1);
235
+
236
+ // ---- status numbers (from the data, not typed) ------------------------
237
+ const oracle = arms.oracle.recall, baseA = arms.A.recall;
238
+ document.getElementById("oracleRecall").textContent = f3(oracle);
239
+ document.getElementById("headroom").textContent = f3(oracle - baseA);
240
+ const C = D.ceiling;
241
+ document.getElementById("missSplit").textContent =
242
+ `${C.baseline_misses - C.also_missed_by_oracle} of the baseline's `
243
+ + `${C.baseline_misses} misses on the test split are ASR word errors, not `
244
+ + `detector gaps (the oracle misses the other ${C.also_missed_by_oracle} too).`;
245
+
246
+ // ---- arm table: before and after convergence ---------------------------
247
  const rows = [
248
  ["A — ASR (whisper-tiny.en) + text PII", arms.A, false],
249
  ["B — ASR (whisper-small.en) + text PII", arms.B, false],
250
+ ["C — Bleep alone, converged", arms.C, true],
251
+ ["C — Bleep alone, before convergence", B.arms.C, false],
252
+ ["D — A ∪ Bleep, converged", arms.D, false],
253
+ ["D — A ∪ Bleep, before convergence", B.arms.D, false],
254
  ["oracle — perfect transcript + same rules", arms.oracle, false],
255
  ].filter(r => r[1]);
256
  const t = document.getElementById("armTable");
257
+ t.innerHTML = "<tr><th>arm (test split)</th><th style='text-align:right'>recall</th>"
258
  + "<th style='text-align:right'>false alarms/hour</th></tr>"
259
  + rows.map(([name, a, hero]) =>
260
  `<tr class="${hero ? "hero" : ""}"><td>${name}</td>`
261
  + `<td class="num">${f3(a.recall)}</td>`
262
+ + `<td class="num">${f1(a.fa_per_hour)}</td></tr>`).join("");
263
  document.getElementById("armNote").textContent =
264
+ `${S.n_utterances} utterances (${S.audio_hours.toFixed(2)} h) from the eight `
265
+ + `degradation conditions the model was trained on, speakers it never saw. `
266
+ + `Bleep's threshold was calibrated on a disjoint split to arm A's measured `
267
+ + `false-alarm rate of ${f1(m.arm_a_fa_per_hour_on_test)}/hour. Of the regions `
268
+ + `arm A missed, the converged model found `
269
+ + `${(comp.asr_misses_rescued_by_bleep * 100).toFixed(0)}% (before convergence: `
270
+ + `${(B.complementarity.asr_misses_rescued_by_bleep * 100).toFixed(0)}%).`;
271
+
272
+ // ---- held-out: the false-alarm failure ---------------------------------
273
+ const H = D.summary.test_heldout, H2 = D.summary.test_heldout_v2;
274
+ const ratio = h => h.arms.C.fa_per_hour / h.matched_fa.arm_a_fa_per_hour_on_test;
275
+ document.getElementById("heldoutText").textContent =
276
+ `On degradation conditions it was not trained on, the threshold carried over `
277
+ + `from the calibration split does not carry its false-alarm rate: the `
278
+ + `converged model raises ${ratio(H).toFixed(1)}–${ratio(H2).toFixed(1)}× the `
279
+ + `baseline's false alarms, and no threshold brings it down to the baseline's `
280
+ + `rate. None of these recall comparisons is at a matched false-alarm rate.`;
281
+ const ht = document.getElementById("heldTable");
282
+ const hrow = (name, h) => `<tr><td>${name}</td>`
283
+ + `<td class="num">${f3(h.arms.A.recall)} · ${f1(h.arms.A.fa_per_hour)}</td>`
284
+ + `<td class="num">${f3(h.arms.C.recall)} · ${f1(h.arms.C.fa_per_hour)}</td></tr>`;
285
+ ht.innerHTML = "<tr><th>held-out split</th><th style='text-align:right'>ASR baseline: recall · FA/h</th>"
286
+ + "<th style='text-align:right'>Bleep, converged: recall · FA/h</th></tr>"
287
+ + hrow("test_heldout (2 of 5 conditions later trained)", H)
288
+ + hrow("test_heldout_v2 (all 5 conditions unseen)", H2);
289
 
290
  // ---- data-derived prose ---------------------------------------------
291
+ const pc = S.per_condition, ph = H.per_condition, ph2 = H2.per_condition;
292
  document.getElementById("whyCond").textContent =
293
  `On the training-condition test split the ASR baseline is essentially flat: `
294
  + `${f3(pc.clean.A)} on clean audio and ${f3(pc.telephony_8k.A)} at 8 kHz — `
295
  + `i.e. better than clean. A band-limit plus G.711 is not a hard condition `
296
+ + `for a modern ASR on hyper-articulated TTS speech. The collapse appeared `
297
  + `only in the harsher held-out conditions: `
298
  + `${f3(ph.overlap_0db_three_way.A)} at 0 dB three-way overlap and `
299
+ + `${f3(ph.telephony_overlap_3db.A)} at narrowband-plus-overlap. For the `
300
+ + `retrain those two were moved into training; of five new unseen conditions `
301
+ + `the baseline collapses only under overlap again (`
302
+ + `${f3(ph2.overlap_minus3db.A)} at −3 dB).`;
303
 
304
+ const catB = B.per_category, cat = S.per_category;
305
+ const best = Object.entries(catB).filter(([, v]) => v.C !== null)
306
  .sort((a, b) => b[1].C - a[1].C).slice(0, 2);
307
  document.getElementById("whyCat").textContent =
308
+ `Before convergence, Bleep's per-category recall had the shape the acoustic `
309
+ + `argument predicts. Its best categories were ${best.map(([k, v]) =>
310
  k.replace(/_/g, " ").toLowerCase() + " (" + f3(v.C) + ")").join(" and ")}`
311
  + ` — the two with the most distinctive rhythm — against ~0.00 for run-on `
312
+ + `digits, money and identifiers. After convergence those two reach `
313
+ + `${best.map(([k]) => f3(cat[k].C)).join(" and ")}.`;
314
 
315
  const sp = S.speed.bleep, asr = S.speed.asr_arms_rtf;
316
  document.getElementById("heldUp").textContent =
317
  `Speed: ${sp.rtf_median} real-time factor on CPU with the front end `
318
  + `included — ${sp.xrealtime_best}× real time, `
319
  + `${(asr.A / sp.rtf_median).toFixed(1)}× faster than the cheapest ASR arm `
320
+ + `and ${(asr.B / sp.rtf_median).toFixed(0)}× faster than the better one; `
321
+ + `on the Apple Neural Engine via Core ML, about 0.002 real-time factor. `
322
+ + `${(sp.param_count / 1e6).toFixed(1)} M parameters. The no-content invariant `
323
+ + `is enforced structurally by tests (an AST walk over the module graph); a `
324
+ + `span carries only start, end, label, score and source. The union never `
325
+ + `drops a region the existing detector found: zero violations on every `
326
+ + `utterance of all three evaluation splits. Chunked inference is bit-exact `
327
+ + `against whole-file inference, the receptive field is capped at 8.98 s on `
328
+ + `purpose, and redaction writes a review plan rather than touching audio.`;
329
 
330
  document.getElementById("foot").textContent =
331
+ `Converged benchmark generated ${D.generated_utc}; pre-convergence benchmark `
332
+ + `${D.generated_utc_before}. Every number on this page is read from the `
333
+ + `benchmarks' own JSON by the page itself.`;
334
 
335
  // ---- timeline viewer -------------------------------------------------
336
  const pick = document.getElementById("pick");
337
  D.examples.forEach((e, i) => {
338
  const o = document.createElement("option");
339
  o.value = i;
340
+ o.textContent = `${e.split === "test" ? "trained" : "UNSEEN"} · `
341
+ + `${e.condition.replace(/_/g, " ")} — ${e.duration.toFixed(1)}s `
342
  + `(${e.truth.length} sensitive region${e.truth.length === 1 ? "" : "s"})`;
343
  pick.appendChild(o);
344
  });
 
347
  ["ground truth", "truth", "var(--neutral)"],
348
  ["ASR + text PII", "asr", "var(--a)"],
349
  ["Bleep", "bleep", "var(--c)"],
350
+ ["Bleep (before)", "bleep_before", "var(--b)"],
351
  ["union", "union", "var(--d)"],
352
  ];
353
 
 
423
  // dark-mode figure swap
424
  const swap = dark => {
425
  [["figCondLight", "fig1_recall_by_condition"],
426
+ ["figFaLight", "fig4_recall_vs_false_alarms.test_heldout_v2"],
427
+ ["figValLight", "val_curve"],
428
  ["figCatLight", "fig3_recall_by_category"]].forEach(([id, base]) => {
429
  const n = document.getElementById(id);
430
  if (n) n.src = `figures/${base}.${dark ? "dark" : "light"}.png`;