SharpMeow commited on
Commit
9b08343
·
verified ·
1 Parent(s): 079985f

Fiber Checker: page, checker, free judge weights and pictures

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/hero.webp filter=lfs diff=lfs merge=lfs -text
37
+ assets/howto-check.gif filter=lfs diff=lfs merge=lfs -text
38
+ assets/howto-game.gif filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -37,6 +37,7 @@ OUT = Path(tempfile.gettempdir()) / "fiber-checker-out"
37
  CT_CACHE = C.CACHE / "ct"
38
  CT_KEEP = int(8e9) # CT chunks kept between runs; the oldest go first
39
  GAME = HERE / "assets" / "game.npz"
 
40
  CONNECT_HTML = HERE / "connect.html"
41
  PATCHES = [("Quick: 60 patches, about 2 minutes", 60), ("Thorough: 150 patches, about 5 minutes", 150),
42
  ("Whole segment: up to 400 patches, about 15 minutes", 400)]
@@ -243,6 +244,34 @@ def compare_results(a_json, b_json):
243
 
244
  # ---------------------------------------------------------------- what the page shows
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  def pct(v):
247
  return "n/a" if v is None or v != v else f"{v:.0%}"
248
 
@@ -277,7 +306,7 @@ def summary_html(rec):
277
  · {setting} · judge: {'free local model' if rec['judge'] == 'local' else 'Claude, your key'}{cost}</p>
278
  <details class="fc-untested"><summary>Untested hints ({jumps} possible jumps)</summary>
279
  <p>These have not passed a test of their own yet, so read them as ideas, not results.
280
- <b>Jump hints</b>: {jumps} patches with low evidence where every neighbour is clear, which can mark a sheet jump or a
281
  stretch off the sheet. <b>Fiber direction</b>: {pct(s['direction_share_horizontal'])} of patches have strands
282
  running along the mesh's columns (horizontal in the map), the rest along its rows. <b>Best depth</b>: the median
283
  patch looks most fibrous {s['depth_offset_median']:+g} voxels from the traced surface.</p></details>
@@ -289,6 +318,40 @@ def tiles_of(out):
289
  return [(str(f), f"{f.stem.split('-')[-1]} {f.stem.split('-')[1]}") for f in files]
290
 
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  def report_frame(out):
293
  page = (Path(out) / "report.html").read_text()
294
  return f'<iframe class="fc-report" title="Report" srcdoc="{html.escape(page, quote=True)}"></iframe>'
@@ -442,6 +505,10 @@ texture: it never reads or finds text.
442
  judges them on the CPU.
443
  3. The map, the summary and the download appear on the right. Try the **Fiber or not?** tab while you wait.
444
 
 
 
 
 
445
  <h3 id="read">2. Read the result</h3>
446
 
447
  - **The map** is the segment as traced, flattened on its own mesh grid. Yellow and green: clear fibers. Purple: weak.
@@ -452,18 +519,25 @@ texture: it never reads or finds text.
452
  - **Unsure**: the judge's answer changed when the patch was turned by a quarter turn.
453
  - **Clearest and weakest patches** are the renders themselves, so you can see what the judge saw.
454
 
 
 
455
  <h3 id="own">3. Check your own trace</h3>
456
 
457
  Zip your tifxyz folder (x.tif, y.tif and z.tif; VC3D and villa write these) and upload it on **Check a segment**,
458
  under **Your own mesh**, with the 14-digit id of the scan its coordinates are in (level-0 voxels). The Space reads the
459
  CT of that scan from the open data. Meshes on 2.2 to 2.4 um scans are checked in the tested setting.
460
 
 
 
461
  <h3 id="ab">4. Compare two traces</h3>
462
 
463
  Check both (two segments, or your trace before and after an edit), download each zip, and give the two
464
  `fiber.json` files to **Compare**. When both are meshes of the same grid, it compares them patch by patch: the share
465
  of patches where each is clearly better, and the mean difference.
466
 
 
 
 
467
  <h3 id="prize">5. For prize entries</h3>
468
 
469
  `report.html` in the download is a one-page summary in the prize's words: the map, the self-check, the clearest and
@@ -507,7 +581,7 @@ the segment in tools that read tifxyz channels. `--judge claude` uses `ANTHROPIC
507
  - **Outside the tested setting** (other voxel sizes, your own meshes at other spacings): it still runs, and says so.
508
  - **Untested hints**: jump hints, fiber direction and best depth are ideas until a test of their own passes.
509
  - **Which sheet**: each patch is rendered at the brightest depth within 5 voxels of the trace (the tested rule), and
510
- papyrus sheets lie close together, so a trace a few voxels off, or on the neighbouring sheet, can still show fibers.
511
  A clear map says fibers show on and near your surface; it does not prove the trace stayed on one sheet.
512
 
513
  <h3 id="wrong">When something goes wrong</h3>
@@ -569,6 +643,19 @@ CSS = """
569
  .fc-legend { font-size:12.5px; color: var(--body-text-color-subdued); }
570
  .fc-legend i { display:inline-block; width:11px; height:11px; border-radius:3px; margin:0 4px -1px 10px; }
571
  .fc-report { width:100%; height:80vh; border:0; border-radius:12px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
572
  .fc-howto { max-width: 1000px; margin: 0 auto !important; }
573
  .fc-howto h3 { scroll-margin-top: 16px; margin-top: 1.6em !important; }
574
  .fc-toc { font-size: 14px; line-height: 1.9; padding: 10px 14px; border-radius: 10px;
@@ -582,6 +669,8 @@ CSS = """
582
  .fc-head h1 { font-size:24px; }
583
  .fc-head span { display:none; }
584
  #fc-tabs .tab-container button { padding:6px 6px !important; font-size:12.5px !important; }
 
 
585
  }
586
  """
587
 
@@ -602,10 +691,14 @@ model or by Claude with your own key. Model output, not a reading.">
602
  TAB_JS = """
603
  () => {
604
  const pick = (name) => {
605
- const ids = {check: 0, game: 1, compare: 2, howto: 3, agents: 4};
606
- const btns = document.querySelectorAll('#fc-tabs button[role="tab"]');
607
- if (name in ids && btns[ids[name]]) btns[ids[name]].click();
 
 
 
608
  };
 
609
  const q = new URLSearchParams(window.location.search).get('tab');
610
  if (q) setTimeout(() => pick(q), 300);
611
  }
@@ -721,6 +814,8 @@ def build_demo():
721
  gr.HTML('<div class="fc-head"><div class="weave"></div><h1>Fiber Checker</h1><span>Are papyrus fibers '
722
  'visible on a traced scroll surface? A free fiber map of any segment, patch by patch.</span></div>')
723
  with gr.Tabs(elem_id="fc-tabs"):
 
 
724
  with gr.Tab("Check a segment", id="check"):
725
  gr.Markdown(INTRO)
726
  with gr.Row(equal_height=False):
@@ -738,7 +833,7 @@ def build_demo():
738
  go = gr.Button("Check fibers", variant="primary", size="lg")
739
  with gr.Tab("Your own mesh"):
740
  upload = gr.File(label="Zip of your tifxyz folder (x.tif, y.tif, z.tif)",
741
- file_types=[".zip"], type="filepath", height=100)
742
  up_vol = gr.Textbox(label="14-digit scan id its coordinates are in (level-0 voxels)",
743
  placeholder="20250526151718")
744
  go_up = gr.Button("Check my mesh", variant="primary", size="lg")
@@ -785,12 +880,14 @@ def build_demo():
785
  gr.Markdown("Give two `fiber.json` files (from the downloads, or the command line tool): two "
786
  "segments, two tracers, or one trace before and after an edit.")
787
  with gr.Row():
788
- ca = gr.File(label="A: fiber.json", file_types=[".json"], type="filepath", height=100)
789
- cb = gr.File(label="B: fiber.json", file_types=[".json"], type="filepath", height=100)
790
  cgo = gr.Button("Compare", variant="primary")
791
- cout = gr.JSON(label="Side by side")
 
 
792
  with gr.Tab("How-to guides", id="howto"):
793
- gr.Markdown(HOW_TO, elem_classes="fc-howto")
794
  with gr.Tab("Agents and API", id="agents"):
795
  gr.Markdown(AGENTS.format(host=host))
796
  with gr.Accordion("Connect your tool: the scans and meshes by URL", open=True):
@@ -826,7 +923,10 @@ def build_demo():
826
  game_tab.select(guard(first_pair), st, [img_a, img_b, reveal, score_md, st], **hidden)
827
  pick_a.click(lambda s: answer("A", s), st, [reveal, score_md, st], **hidden)
828
  pick_b.click(lambda s: answer("B", s), st, [reveal, score_md, st], **hidden)
829
- cgo.click(guard(compare_results), [ca, cb], cout, **hidden)
 
 
 
830
 
831
  def api_check(sample_, segment_, volume_, patches_, request: gr.Request, progress=gr.Progress()):
832
  return check_segment(sample_, segment_, volume_, int(patches_ or 60), request=request, progress=progress)
@@ -861,6 +961,10 @@ def fix_mcp_multiple_outputs():
861
 
862
 
863
  def launch_kwargs():
 
 
 
 
864
  return dict(theme=theme(), css=CSS, head=HEAD, footer_links=["api", "gradio"], mcp_server=True,
865
  max_file_size="200mb", allowed_paths=[str(OUT)])
866
 
 
37
  CT_CACHE = C.CACHE / "ct"
38
  CT_KEEP = int(8e9) # CT chunks kept between runs; the oldest go first
39
  GAME = HERE / "assets" / "game.npz"
40
+ GUIDE = HERE / "guide" / "index.html"
41
  CONNECT_HTML = HERE / "connect.html"
42
  PATCHES = [("Quick: 60 patches, about 2 minutes", 60), ("Thorough: 150 patches, about 5 minutes", 150),
43
  ("Whole segment: up to 400 patches, about 15 minutes", 400)]
 
244
 
245
  # ---------------------------------------------------------------- what the page shows
246
 
247
+ def compare_html(res):
248
+ """compare_results' answer as two columns, with the better side of each row marked."""
249
+ def name(v):
250
+ if isinstance(v, dict):
251
+ v = " · ".join(str(v[k]) for k in ("sample", "name", "volume") if v.get(k))
252
+ v = str(v or "?").rstrip("/").rsplit("/", 1)[-1] # a mesh path: its folder name, not where it was cached
253
+ return html.escape(v.rsplit("_mesh_", 1)[-1])
254
+ rows = [("Patches checked", "patches", lambda v: f"{v}", None),
255
+ ("Fibers show on", "share_fibers_visible", pct, 1),
256
+ ("Median fiber evidence", "median_evidence", lambda v: f"{v:.2f}", 1),
257
+ ("Unsure", "share_unsure", pct, -1)]
258
+ body = []
259
+ for label, k, fmt, better in rows:
260
+ a, b = res["summary"][k]
261
+ cells = [fmt(v) if v is not None else "n/a" for v in (a, b)]
262
+ if better and a is not None and b is not None and abs(a - b) > 0.02:
263
+ win = 0 if (a - b) * better > 0 else 1
264
+ cells[win] = f"<b>{cells[win]}</b> ◀"
265
+ body.append(f"<tr><td>{label}</td><td>{cells[0]}</td><td>{cells[1]}</td></tr>")
266
+ pr = res.get("paired_same_grid")
267
+ paired = (f"<p class='fc-meta'>Same mesh grid, {pr['patches']} patches in both: A clearly better on "
268
+ f"{pct(pr['a_better'])}, B on {pct(pr['b_better'])}, mean difference {pr['mean_diff']:+.3f} (A minus B)."
269
+ "</p>" if pr else "<p class='fc-meta'>Different mesh grids, so the summaries are compared, not patch "
270
+ "by patch.</p>")
271
+ return (f"<table class='fc-cmp'><thead><tr><th></th><th>A<span>{name(res.get('a'))}</span></th>"
272
+ f"<th>B<span>{name(res.get('b'))}</span></th></tr></thead><tbody>{''.join(body)}</tbody></table>{paired}")
273
+
274
+
275
  def pct(v):
276
  return "n/a" if v is None or v != v else f"{v:.0%}"
277
 
 
306
  · {setting} · judge: {'free local model' if rec['judge'] == 'local' else 'Claude, your key'}{cost}</p>
307
  <details class="fc-untested"><summary>Untested hints ({jumps} possible jumps)</summary>
308
  <p>These have not passed a test of their own yet, so read them as ideas, not results.
309
+ <b>Jump hints</b>: {jumps} patches with low evidence where every neighbor is clear, which can mark a sheet jump or a
310
  stretch off the sheet. <b>Fiber direction</b>: {pct(s['direction_share_horizontal'])} of patches have strands
311
  running along the mesh's columns (horizontal in the map), the rest along its rows. <b>Best depth</b>: the median
312
  patch looks most fibrous {s['depth_offset_median']:+g} voxels from the traced surface.</p></details>
 
318
  return [(str(f), f"{f.stem.split('-')[-1]} {f.stem.split('-')[1]}") for f in files]
319
 
320
 
321
+ def with_figures(text):
322
+ """Fill the guides' [[fig NAME | caption]] slots with the pictures in assets/ (tools/build_media.py); a slot whose
323
+ picture this copy lacks is dropped, so the guides read the same without them."""
324
+ out, rest = [], text
325
+ while "[[fig " in rest:
326
+ head, _, tail = rest.partition("[[fig ")
327
+ spec, _, rest = tail.partition("]]")
328
+ name, _, caption = (x.strip() for x in spec.partition("|"))
329
+ out.append(head)
330
+ if (HERE / "assets" / name).is_file():
331
+ alt = html.escape(" ".join(caption.split()), quote=True)
332
+ out.append(f'<figure><img src="/gradio_api/file=assets/{name}" alt="{alt}" loading="lazy">'
333
+ f'<figcaption>{html.escape(" ".join(caption.split()))} Recorded on public scrolls on no prize '
334
+ 'list.</figcaption></figure>')
335
+ return "".join(out) + rest
336
+
337
+
338
+ def guide_html():
339
+ """The welcome page, its pictures read from this Space's assets/ folder (tools/build_media.py)."""
340
+ if not GUIDE.is_file():
341
+ return "<p>The welcome page is missing from this copy.</p>"
342
+ page = GUIDE.read_text()
343
+ # its pictures point at Gradio's file route (a <base> cannot do it); in-page anchors scroll inside the frame,
344
+ # ?tab= links switch the Space's tabs, other links open a new tab (as Lasagna Fields does)
345
+ page = page.replace("'assets/", "'/gradio_api/file=assets/").replace('"assets/', '"/gradio_api/file=assets/')
346
+ fix = ('<script>document.addEventListener("click",function(e){var a=e.target.closest&&e.target.closest("a");'
347
+ 'if(!a)return;var h=a.getAttribute("href")||"";if(h.charAt(0)==="#"){e.preventDefault();'
348
+ 'var t=document.getElementById(h.slice(1));if(t)t.scrollIntoView({behavior:"smooth"});}'
349
+ 'else if(h.indexOf("?tab=")===0){e.preventDefault();window.parent.postMessage({fcTab:h.slice(5)},"*");}'
350
+ 'else if(/^https?:/.test(h)){a.target="_blank";a.rel="noopener";}},true);</script>')
351
+ page = page.replace("<head>", "<head>" + fix, 1)
352
+ return f'<iframe title="Fiber Checker" srcdoc="{html.escape(page, quote=True)}"></iframe>'
353
+
354
+
355
  def report_frame(out):
356
  page = (Path(out) / "report.html").read_text()
357
  return f'<iframe class="fc-report" title="Report" srcdoc="{html.escape(page, quote=True)}"></iframe>'
 
505
  judges them on the CPU.
506
  3. The map, the summary and the download appear on the right. Try the **Fiber or not?** tab while you wait.
507
 
508
+ [[fig howto-check.gif | A check from start to finish, sped up: pick an example, press Check fibers, read the map.]]
509
+
510
+ [[fig howto-game.gif | Fiber or not?: say which render shows fibers, then see what the free judge said.]]
511
+
512
  <h3 id="read">2. Read the result</h3>
513
 
514
  - **The map** is the segment as traced, flattened on its own mesh grid. Yellow and green: clear fibers. Purple: weak.
 
519
  - **Unsure**: the judge's answer changed when the patch was turned by a quarter turn.
520
  - **Clearest and weakest patches** are the renders themselves, so you can see what the judge saw.
521
 
522
+ [[fig howto-result.webp | A finished check: the map, the summary with its self-check, and the download.]]
523
+
524
  <h3 id="own">3. Check your own trace</h3>
525
 
526
  Zip your tifxyz folder (x.tif, y.tif and z.tif; VC3D and villa write these) and upload it on **Check a segment**,
527
  under **Your own mesh**, with the 14-digit id of the scan its coordinates are in (level-0 voxels). The Space reads the
528
  CT of that scan from the open data. Meshes on 2.2 to 2.4 um scans are checked in the tested setting.
529
 
530
+ [[fig howto-upload.webp | Your own mesh: the zip goes in the box, the scan id below it.]]
531
+
532
  <h3 id="ab">4. Compare two traces</h3>
533
 
534
  Check both (two segments, or your trace before and after an edit), download each zip, and give the two
535
  `fiber.json` files to **Compare**. When both are meshes of the same grid, it compares them patch by patch: the share
536
  of patches where each is clearly better, and the mean difference.
537
 
538
+ [[fig howto-compare.gif | Two fiber.json files in, a side-by-side table out. These two are checks of the same segment on
539
+ two scans, whose meshes have different grids, so the summaries are compared.]]
540
+
541
  <h3 id="prize">5. For prize entries</h3>
542
 
543
  `report.html` in the download is a one-page summary in the prize's words: the map, the self-check, the clearest and
 
581
  - **Outside the tested setting** (other voxel sizes, your own meshes at other spacings): it still runs, and says so.
582
  - **Untested hints**: jump hints, fiber direction and best depth are ideas until a test of their own passes.
583
  - **Which sheet**: each patch is rendered at the brightest depth within 5 voxels of the trace (the tested rule), and
584
+ papyrus sheets lie close together, so a trace a few voxels off, or on the neighboring sheet, can still show fibers.
585
  A clear map says fibers show on and near your surface; it does not prove the trace stayed on one sheet.
586
 
587
  <h3 id="wrong">When something goes wrong</h3>
 
643
  .fc-legend { font-size:12.5px; color: var(--body-text-color-subdued); }
644
  .fc-legend i { display:inline-block; width:11px; height:11px; border-radius:3px; margin:0 4px -1px 10px; }
645
  .fc-report { width:100%; height:80vh; border:0; border-radius:12px; }
646
+ .fc-guide iframe { width:100%; height:calc(100vh - 150px); min-height:640px; border:0; border-radius:12px;
647
+ background:#f6f1e7; }
648
+ .fc-howto img { border-radius:10px; border:1px solid var(--border-color-primary); max-width:min(100%, 820px);
649
+ display:block; margin:10px 0 4px; }
650
+ .fc-howto figure { margin:10px 0 18px; } .fc-howto figcaption { font-size:13px; color: var(--body-text-color-subdued); }
651
+ .fc-strip { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:10px; max-width:820px; margin:10px 0; }
652
+ .fc-strip img { width:100%; margin:0; aspect-ratio:1; object-fit:cover; }
653
+ .fc-strip span { display:block; font-size:12.5px; color: var(--body-text-color-subdued); margin-top:4px; }
654
+ .fc-drop .wrap { padding:14px 10px !important; gap:4px !important; line-height:1.35 !important; }
655
+ .fc-cmp { width:100%; max-width:820px; border-collapse:collapse; font-size:15px; margin-top:6px; }
656
+ .fc-cmp th, .fc-cmp td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--border-color-primary); }
657
+ .fc-cmp th span { display:block; font-weight:400; font-size:12.5px; color: var(--body-text-color-subdued); }
658
+ .fc-cmp td:not(:first-child) { font-variant-numeric: tabular-nums; }
659
  .fc-howto { max-width: 1000px; margin: 0 auto !important; }
660
  .fc-howto h3 { scroll-margin-top: 16px; margin-top: 1.6em !important; }
661
  .fc-toc { font-size: 14px; line-height: 1.9; padding: 10px 14px; border-radius: 10px;
 
669
  .fc-head h1 { font-size:24px; }
670
  .fc-head span { display:none; }
671
  #fc-tabs .tab-container button { padding:6px 6px !important; font-size:12.5px !important; }
672
+ .fc-guide iframe { height:calc(100vh - 120px); min-height:520px; }
673
+ .fc-strip { grid-template-columns:repeat(2,minmax(0,1fr)); }
674
  }
675
  """
676
 
 
691
  TAB_JS = """
692
  () => {
693
  const pick = (name) => {
694
+ // by label: on a phone the later tabs sit in Gradio's overflow menu, as buttons without role="tab"
695
+ const labels = {home: 'Welcome', check: 'Check a segment', game: 'Fiber or not?', compare: 'Compare',
696
+ howto: 'How-to guides', agents: 'Agents and API'};
697
+ const btns = [...document.querySelectorAll('#fc-tabs button')].filter((b) => b.textContent.trim() === labels[name]);
698
+ const btn = btns.find((b) => b.getAttribute('role') === 'tab') || btns[btns.length - 1];
699
+ if (btn) btn.click();
700
  };
701
+ window.addEventListener('message', (e) => { if (e.data && e.data.fcTab) pick(e.data.fcTab); });
702
  const q = new URLSearchParams(window.location.search).get('tab');
703
  if (q) setTimeout(() => pick(q), 300);
704
  }
 
814
  gr.HTML('<div class="fc-head"><div class="weave"></div><h1>Fiber Checker</h1><span>Are papyrus fibers '
815
  'visible on a traced scroll surface? A free fiber map of any segment, patch by patch.</span></div>')
816
  with gr.Tabs(elem_id="fc-tabs"):
817
+ with gr.Tab("Welcome", id="home"):
818
+ gr.HTML(guide_html(), elem_classes="fc-guide")
819
  with gr.Tab("Check a segment", id="check"):
820
  gr.Markdown(INTRO)
821
  with gr.Row(equal_height=False):
 
833
  go = gr.Button("Check fibers", variant="primary", size="lg")
834
  with gr.Tab("Your own mesh"):
835
  upload = gr.File(label="Zip of your tifxyz folder (x.tif, y.tif, z.tif)",
836
+ file_types=[".zip"], type="filepath")
837
  up_vol = gr.Textbox(label="14-digit scan id its coordinates are in (level-0 voxels)",
838
  placeholder="20250526151718")
839
  go_up = gr.Button("Check my mesh", variant="primary", size="lg")
 
880
  gr.Markdown("Give two `fiber.json` files (from the downloads, or the command line tool): two "
881
  "segments, two tracers, or one trace before and after an edit.")
882
  with gr.Row():
883
+ ca = gr.File(label="A: fiber.json", file_types=[".json"], type="filepath", elem_classes="fc-drop")
884
+ cb = gr.File(label="B: fiber.json", file_types=[".json"], type="filepath", elem_classes="fc-drop")
885
  cgo = gr.Button("Compare", variant="primary")
886
+ cside = gr.HTML()
887
+ with gr.Accordion("Record (JSON)", open=False):
888
+ cout = gr.JSON(label="Side by side")
889
  with gr.Tab("How-to guides", id="howto"):
890
+ gr.Markdown(with_figures(HOW_TO), elem_classes="fc-howto")
891
  with gr.Tab("Agents and API", id="agents"):
892
  gr.Markdown(AGENTS.format(host=host))
893
  with gr.Accordion("Connect your tool: the scans and meshes by URL", open=True):
 
923
  game_tab.select(guard(first_pair), st, [img_a, img_b, reveal, score_md, st], **hidden)
924
  pick_a.click(lambda s: answer("A", s), st, [reveal, score_md, st], **hidden)
925
  pick_b.click(lambda s: answer("B", s), st, [reveal, score_md, st], **hidden)
926
+ def on_compare(a, b):
927
+ res = compare_results(a, b)
928
+ return compare_html(res), res
929
+ cgo.click(guard(on_compare), [ca, cb], [cside, cout], **hidden)
930
 
931
  def api_check(sample_, segment_, volume_, patches_, request: gr.Request, progress=gr.Progress()):
932
  return check_segment(sample_, segment_, volume_, int(patches_ or 60), request=request, progress=progress)
 
961
 
962
 
963
  def launch_kwargs():
964
+ import gradio as gr
965
+ if (HERE / "assets").is_dir():
966
+ gr.set_static_paths(paths=[str(HERE / "assets")])
967
+ os.chdir(HERE) # the welcome page's assets/... resolve against the Space's folder
968
  return dict(theme=theme(), css=CSS, head=HEAD, footer_links=["api", "gradio"], mcp_server=True,
969
  max_file_size="200mb", allowed_paths=[str(OUT)])
970
 
assets/hero.webp ADDED

Git LFS Details

  • SHA256: 5ca5c08ce750edaf4fa17326ec333b3767773e22d7a1333d1ae47c84722888e1
  • Pointer size: 131 Bytes
  • Size of remote file: 298 kB
assets/howto-check.gif ADDED

Git LFS Details

  • SHA256: 30fb730fb7675f8302901d224f519d324dadc1711055f7b45894ea4921265584
  • Pointer size: 131 Bytes
  • Size of remote file: 237 kB
assets/howto-compare.gif ADDED
assets/howto-game.gif ADDED

Git LFS Details

  • SHA256: 4b261af8b3fd91da519be9d2d691107f6da025c3937f44eb16a3181c13cd7cf4
  • Pointer size: 131 Bytes
  • Size of remote file: 498 kB
assets/howto-result.webp ADDED
assets/howto-upload.webp ADDED
assets/map-summary.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"patches": 60, "median_evidence": 0.988, "share_fibers_visible": 1.0, "share_unsure": 0.0, "self_check": {"against": {"noise": 1.0, "blur": 1.0, "scramble": 1.0}, "passed": true}, "depth_offset_median": 0.0, "direction_share_horizontal": 0.517}
assets/map.png ADDED
assets/media.json ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "note": "Renders of the Vesuvius Challenge open data (CC BY-NC 4.0) along open-data meshes of public scrolls on no prize list, made by fibercheck's renderer. Not model output except map.png.",
3
+ "pictures": {
4
+ "hero.webp": [
5
+ "PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r252 c252 (judge 0.99)",
6
+ "PHerc0814 \u00b7 scan 20260309142202 \u00b7 segment 20250925161630 \u00b7 patch r168 c672 (judge 1.00)",
7
+ "PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r196 c252 (judge 0.99)",
8
+ "PHerc0139 \u00b7 scan 20260102150214 \u00b7 segment 20250108000005 \u00b7 patch r868 c392 (judge 0.96)",
9
+ "PHerc0814 \u00b7 scan 20260309142202 \u00b7 segment 20250925161630 \u00b7 patch r336 c420 (judge 1.00)",
10
+ "PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r28 c196 (judge 0.99)",
11
+ "PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r448 c336 (judge 0.95)",
12
+ "PHerc0009B \u00b7 scan 20250820154339 \u00b7 segment 20250919064353 \u00b7 patch r868 c532 (judge 1.00)",
13
+ "PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r364 c308 (judge 0.99)",
14
+ "PHerc0814 \u00b7 scan 20260309142202 \u00b7 segment 20250925161630 \u00b7 patch r616 c1008 (judge 1.00)",
15
+ "PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r28 c112 (judge 0.99)",
16
+ "PHerc0139 \u00b7 scan 20260102150214 \u00b7 segment 20250108000005 \u00b7 patch r980 c896 (judge 0.99)",
17
+ "PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r364 c140 (judge 1.00)",
18
+ "PHerc0814 \u00b7 scan 20260309142202 \u00b7 segment 20250925161630 \u00b7 patch r336 c2772 (judge 1.00)",
19
+ "PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r532 c420 (judge 0.97)",
20
+ "PHerc0009B \u00b7 scan 20250820154339 \u00b7 segment 20250919064353 \u00b7 patch r784 c924 (judge 1.00)",
21
+ "PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r420 c308 (judge 0.96)",
22
+ "PHerc0814 \u00b7 scan 20260309142202 \u00b7 segment 20250925161630 \u00b7 patch r308 c3136 (judge 0.89)",
23
+ "PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r84 c196 (judge 1.00)",
24
+ "PHerc0139 \u00b7 scan 20260102150214 \u00b7 segment 20250108000005 \u00b7 patch r700 c504 (judge 0.99)",
25
+ "PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r84 c252 (judge 0.99)",
26
+ "PHerc0814 \u00b7 scan 20260309142202 \u00b7 segment 20250925161630 \u00b7 patch r700 c2268 (judge 0.98)",
27
+ "PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r336 c140 (judge 0.98)",
28
+ "PHerc0009B \u00b7 scan 20250820154339 \u00b7 segment 20250919064353 \u00b7 patch r1092 c840 (judge 1.00)"
29
+ ],
30
+ "pair-0-*.webp": "PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r252 c252",
31
+ "pair-1-*.webp": "PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r532 c420",
32
+ "pair-2-*.webp": "PHerc0139 \u00b7 scan 20260102150214 \u00b7 segment 20250108000005 \u00b7 patch r868 c392",
33
+ "map.png": {
34
+ "model_output": true,
35
+ "segment": "/tmp/fiber-checker/meshes/vesuvius-challenge-open-data.s3.us-east-1.amazonaws.com_PHerc0139_segments_20250108000005-w030_2025010818_mesh_20250108000005-on-20260102150214-2.399um.tifxyz",
36
+ "ct": "https://vesuvius-challenge-open-data.s3.us-east-1.amazonaws.com/PHerc0139/volumes/20260102150214-2.399um-0.2m-78keV-masked.zarr/",
37
+ "summary": {
38
+ "patches": 60,
39
+ "median_evidence": 0.988,
40
+ "share_fibers_visible": 1.0,
41
+ "share_unsure": 0.0,
42
+ "self_check": {
43
+ "against": {
44
+ "noise": 1.0,
45
+ "blur": 1.0,
46
+ "scramble": 1.0
47
+ },
48
+ "passed": true
49
+ },
50
+ "depth_offset_median": 0.0,
51
+ "direction_share_horizontal": 0.517
52
+ }
53
+ },
54
+ "ui.webp": "interface screenshot of the Check tab with a finished check (PHerc0139 w030)",
55
+ "howto-check.gif": "interface recording of the check guide at 1100 px (public scrolls on no prize list)",
56
+ "howto-game.gif": "interface recording of the game guide at 1100 px (public scrolls on no prize list)",
57
+ "howto-compare.gif": "interface recording of the compare guide at 1100 px (public scrolls on no prize list)",
58
+ "howto-result.webp": "interface screenshot (result, PHerc0139 w030)",
59
+ "howto-upload.webp": "interface screenshot (upload, PHerc0139 w030)"
60
+ }
61
+ }
assets/pair-0-off.webp ADDED
assets/pair-0-on.webp ADDED
assets/pair-0-scramble.webp ADDED
assets/pair-0-shuffle.webp ADDED
assets/pair-1-off.webp ADDED
assets/pair-1-on.webp ADDED
assets/pair-1-scramble.webp ADDED
assets/pair-1-shuffle.webp ADDED
assets/pair-2-off.webp ADDED
assets/pair-2-on.webp ADDED
assets/pair-2-scramble.webp ADDED
assets/pair-2-shuffle.webp ADDED
assets/tile-0.webp ADDED
assets/tile-1.webp ADDED
assets/tile-2.webp ADDED
assets/tile-3.webp ADDED
assets/tile-4.webp ADDED
assets/tile-5.webp ADDED
assets/ui.webp ADDED
guide/index.html ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Fiber Checker</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,650&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
9
+ <style>
10
+ :root{--bg:#f6f1e7;--card:#fffdf8;--ink:#2a2118;--ink2:#5e5144;--ink3:#857767;--line:#e3d7c3;--accent:#9a5b1f;--accent2:#6b3b16;--shadow:0 1px 2px rgba(60,40,20,.06),0 6px 20px rgba(60,40,20,.06)}
11
+ @media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#14120f;--card:#1f1c18;--ink:#f1e9dc;--ink2:#c9bba8;--ink3:#9c8f7e;--line:#332e28;--accent:#d89a52;--accent2:#e8b273;--shadow:0 1px 2px rgba(0,0,0,.3)}}
12
+ :root[data-theme="dark"]{--bg:#14120f;--card:#1f1c18;--ink:#f1e9dc;--ink2:#c9bba8;--ink3:#9c8f7e;--line:#332e28;--accent:#d89a52;--accent2:#e8b273;--shadow:0 1px 2px rgba(0,0,0,.3)}
13
+ *{box-sizing:border-box}
14
+ html{scroll-behavior:smooth}
15
+ body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.6 Inter,system-ui,sans-serif}
16
+ a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}
17
+ h1,h2,h3{font-family:Fraunces,Georgia,serif;font-weight:650;letter-spacing:-.01em;line-height:1.15}
18
+ h2{font-size:clamp(28px,3.4vw,38px);margin:0 0 10px}
19
+ h3{font-size:20px;margin:0 0 6px}
20
+ .wrap{max-width:1180px;margin:0 auto;padding:0 24px}
21
+ nav{position:sticky;top:0;z-index:5;background:color-mix(in srgb,var(--bg) 88%,transparent);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
22
+ nav .wrap{display:flex;align-items:center;gap:18px;height:52px}
23
+ .brand{font-family:Fraunces,serif;font-weight:650;font-size:18px;color:var(--ink);white-space:nowrap}
24
+ .navlinks{display:flex;gap:18px;overflow-x:auto;scrollbar-width:none;flex:1}
25
+ .navlinks a{color:var(--ink2);font-size:14px;white-space:nowrap}
26
+ header.hero{position:relative;overflow:hidden;border-bottom:1px solid var(--line);min-height:560px}
27
+ .hero .photos{position:absolute;top:0;right:0;bottom:0;width:62%;background:url('assets/hero.webp') center/cover no-repeat;filter:sepia(.35) saturate(1.1)}
28
+ .hero:after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(90deg,var(--bg) 0,var(--bg) 40%,color-mix(in srgb,var(--bg) 82%,transparent) 54%,color-mix(in srgb,var(--bg) 25%,transparent) 76%,transparent 100%)}
29
+ .hero .wrap{position:relative;z-index:2;padding-top:72px;padding-bottom:64px}
30
+ .eyebrow{text-transform:uppercase;letter-spacing:.14em;font-size:12px;font-weight:600;color:var(--accent)}
31
+ .hero h1{font-size:clamp(36px,5.6vw,64px);max-width:700px;margin:12px 0 18px}
32
+ .lede{font-size:19px;max-width:600px;margin:0}
33
+ .by{margin-top:18px;color:var(--ink2);font-size:15px}.by b{color:var(--ink)}
34
+ .stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-top:36px;max-width:720px}
35
+ .stat{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 16px;box-shadow:var(--shadow)}
36
+ .stat b{display:block;font-family:Fraunces,serif;font-size:28px;font-weight:650;line-height:1.1}
37
+ .stat span{font-size:13px;color:var(--ink3)}
38
+ .cta{display:flex;gap:12px;flex-wrap:wrap;margin-top:28px}
39
+ .btn{display:inline-flex;align-items:center;gap:8px;border-radius:10px;padding:10px 16px;font-weight:600;font-size:15px;border:1px solid var(--line);background:var(--card);color:var(--ink)}
40
+ .btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
41
+ .btn:hover{text-decoration:none;filter:brightness(1.05)}
42
+ section{padding:72px 0;border-bottom:1px solid var(--line)}
43
+ .sub{color:var(--ink2);max-width:760px;margin:0 0 28px;font-size:17px}
44
+ .pairs{display:grid;gap:22px}
45
+ .pair{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;background:var(--card);border:1px solid var(--line);border-radius:16px;padding:14px;box-shadow:var(--shadow)}
46
+ .pair figure{margin:0}
47
+ .pair img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:10px;display:block}
48
+ figcaption{font-size:13px;color:var(--ink3);margin-top:6px;line-height:1.4}
49
+ figcaption b{color:var(--ink);font-weight:600}
50
+ .tag{display:inline-block;font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;padding:2px 8px;border-radius:99px;margin-bottom:4px}
51
+ .yes{background:#e3f1e7;color:#1f6b45}.no{background:#f5e3df;color:#9a2e20}
52
+ @media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .yes{background:#1d3427;color:#7fd8a6}:root:not([data-theme="light"]) .no{background:#3a221d;color:#ff9e8f}}
53
+ .pairsrc{grid-column:1/-1;font-size:12.5px;color:var(--ink3);margin:0}
54
+ .mapgrid{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,1fr);gap:24px;align-items:start}
55
+ .mapbox{background:#18181c;border-radius:16px;padding:14px;text-align:center;box-shadow:var(--shadow)}
56
+ .mapbox img{max-width:100%;max-height:560px;image-rendering:pixelated}
57
+ .legend{font-size:13px;color:var(--ink3);margin-top:10px}
58
+ .legend i{display:inline-block;width:11px;height:11px;border-radius:3px;margin:0 4px -1px 10px}
59
+ .tiles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-top:14px}
60
+ .tiles img{width:100%;border-radius:10px;display:block}
61
+ .cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}
62
+ .card{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:18px;box-shadow:var(--shadow)}
63
+ .card p{margin:0;color:var(--ink2);font-size:15px}
64
+ .card .k{font-size:26px;margin-bottom:6px}
65
+ .shot{width:100%;border-radius:14px;border:1px solid var(--line);box-shadow:var(--shadow);display:block}
66
+ table{border-collapse:collapse;width:100%;font-size:15px;background:var(--card);border-radius:14px;overflow:hidden;box-shadow:var(--shadow)}
67
+ th,td{text-align:left;padding:10px 14px;border-bottom:1px solid var(--line)}
68
+ th{font-weight:600;color:var(--ink2);font-size:13px;text-transform:uppercase;letter-spacing:.05em}
69
+ td.n{font-variant-numeric:tabular-nums;font-weight:600}
70
+ .note{font-size:14px;color:var(--ink3);margin-top:14px;max-width:820px}
71
+ .limits li{margin-bottom:8px}
72
+ footer{padding:36px 0 48px;color:var(--ink3);font-size:14px;text-align:center}
73
+ @media (max-width:860px){
74
+ .hero .photos{width:100%;opacity:.35}
75
+ .hero:after{background:color-mix(in srgb,var(--bg) 70%,transparent)}
76
+ .stats{grid-template-columns:repeat(2,minmax(0,1fr))}
77
+ .pair{grid-template-columns:repeat(2,minmax(0,1fr))}
78
+ .mapgrid{grid-template-columns:1fr}
79
+ .cards{grid-template-columns:repeat(2,minmax(0,1fr))}
80
+ }
81
+ @media (max-width:520px){
82
+ .wrap{padding:0 16px}
83
+ header.hero{min-height:0}
84
+ .hero .wrap{padding-top:40px;padding-bottom:40px}
85
+ .lede{font-size:17px}
86
+ .cards{grid-template-columns:1fr}
87
+ section{padding:48px 0}
88
+ th,td{padding:8px 10px;font-size:14px}
89
+ }
90
+ </style>
91
+ </head>
92
+ <body>
93
+ <nav><div class="wrap">
94
+ <span class="brand">Fiber Checker</span>
95
+ <div class="navlinks">
96
+ <a href="#see">What it sees</a><a href="#map">A fiber map</a><a href="#who">Who it is for</a>
97
+ <a href="#tested">How it was tested</a><a href="#limits">Limits</a><a href="?tab=check">Check a segment</a>
98
+ </div>
99
+ </div></nav>
100
+
101
+ <header class="hero" id="top">
102
+ <div class="photos" role="img" aria-label="A mosaic of CT renders along traced surfaces of public scrolls, each showing papyrus fibers"></div>
103
+ <div class="wrap">
104
+ <div class="eyebrow">Vesuvius Challenge open data · free, no key</div>
105
+ <h1>Are papyrus fibers visible on your traced surface?</h1>
106
+ <p class="lede">Pick any traced segment of the open data, or upload your own mesh. Fiber Checker renders the CT along the surface patch by patch and tells you where long, thin papyrus strands show, with a check of itself on every run.</p>
107
+ <p class="by">Made and shared by <b>Chase Hendrick</b>, October 2026. Scans and meshes: Vesuvius Challenge open data.</p>
108
+ <div class="stats">
109
+ <div class="stat"><b>1.00</b><span>sheet vs the gap beside it, on two scrolls it never saw</span></div>
110
+ <div class="stat"><b>92 to 98%</b><span>sheet vs a tile shuffle (pixel measure 53 to 57%)</span></div>
111
+ <div class="stat"><b>0 USD</b><span>the free judge runs on this page's CPU</span></div>
112
+ <div class="stat"><b>~80 s</b><span>for a 60-patch check of a segment</span></div>
113
+ </div>
114
+ <div class="cta">
115
+ <a class="btn primary" href="?tab=check">Check a segment</a>
116
+ <a class="btn primary" href="?tab=game">Play Fiber or not?</a>
117
+ <a class="btn" href="#see">What the judge sees</a>
118
+ <a class="btn" href="?tab=howto">How-to guides</a>
119
+ <a class="btn" href="?tab=agents">For agents and code</a>
120
+ </div>
121
+ </div>
122
+ </header>
123
+
124
+ <section id="see"><div class="wrap">
125
+ <h2>What the judge sees</h2>
126
+ <p class="sub">Each patch is the CT rendered on the traced surface. The judge has to prefer it over copies that keep its brightness and texture but lose the strands. On a surface that sits on papyrus, it does, by a wide margin.</p>
127
+ <div class="pairs" id="pairs"></div>
128
+ </div></section>
129
+
130
+ <section id="map"><div class="wrap">
131
+ <h2>A fiber map</h2>
132
+ <p class="sub">The result of a check: every patch colored by its fiber evidence, on the segment's own mesh grid, so it lines up with the trace. Below the map, the clearest patches the judge saw.</p>
133
+ <div class="mapgrid">
134
+ <div>
135
+ <div class="mapbox"><img src="assets/map.png" alt="A fiber map of PHerc0139 segment w030"></div>
136
+ <div class="legend"><i style="background:#fde725"></i>clear fibers<i style="background:#21918c"></i>some<i style="background:#440154"></i>weak<i style="background:#6e6e6e"></i>no sign<i style="background:#343640"></i>not scored</div>
137
+ </div>
138
+ <div>
139
+ <h3 id="mapnums">PHerc0139, segment w030</h3>
140
+ <p class="sub" style="margin-bottom:8px" id="mapline">A public scroll on no prize list, the 2.4 um scan of 2 January 2026 at level 2. This winding was one the free judge trained on, so it shows the output, not evidence of how well it works.</p>
141
+ <div class="tiles">
142
+ <img src="assets/tile-0.webp" alt="clearest patch 1"><img src="assets/tile-1.webp" alt="clearest patch 2"><img src="assets/tile-2.webp" alt="clearest patch 3">
143
+ <img src="assets/tile-3.webp" alt="clearest patch 4"><img src="assets/tile-4.webp" alt="clearest patch 5"><img src="assets/tile-5.webp" alt="clearest patch 6">
144
+ </div>
145
+ <p class="note">Model output about texture, never a reading of text.</p>
146
+ </div>
147
+ </div>
148
+ </div></section>
149
+
150
+ <section id="page"><div class="wrap">
151
+ <h2>The page</h2>
152
+ <p class="sub">Choose a scroll, a segment and a scan, press Check fibers, and the map, the numbers, the clearest and weakest patches and a download appear beside it. The download holds <code>fiber.json</code>, <code>fiber.tif</code>, the map and a one-page report.</p>
153
+ <img class="shot" src="assets/ui.webp" alt="The Check a segment tab with a finished check">
154
+ </div></section>
155
+
156
+ <section id="who"><div class="wrap">
157
+ <h2>Who it is for</h2>
158
+ <p class="sub">One check, read four ways.</p>
159
+ <div class="cards">
160
+ <div class="card"><div class="k">🌱</div><h3>Beginners</h3><p>See what papyrus fibers look like in CT, play Fiber or not? against the model, and check an example in two minutes.</p></div>
161
+ <div class="card"><div class="k">🏆</div><h3>Prize entrants</h3><p>The Progress Prize asks you to show fibers on your output surface. Attach the one-page report, and say which judge you used.</p></div>
162
+ <div class="card"><div class="k">🔬</div><h3>Researchers</h3><p>Preregistered, tested once on held-out scrolls against built controls, with the failed first round on record. Compare two tracers patch by patch.</p></div>
163
+ <div class="card"><div class="k">🧭</div><h3>Tracers and tool builders</h3><p>Upload a tifxyz, read <code>fiber.tif</code> on your own mesh grid, or call the API or MCP server from a script or an agent.</p></div>
164
+ </div>
165
+ </div></section>
166
+
167
+ <section id="tested"><div class="wrap">
168
+ <h2>How it was tested</h2>
169
+ <p class="sub">The free judge is a small network trained on Claude's answers about renders of four public scrolls. Its test was written down before any test image existed, then run once on two scrolls it never saw.</p>
170
+ <table>
171
+ <thead><tr><th>Pair the judge had to get right</th><th>PHerc0500P2</th><th>PHerc0343P</th><th>Pixel measure</th></tr></thead>
172
+ <tbody>
173
+ <tr><td>Sheet vs the gap beside it</td><td class="n">1.00</td><td class="n">1.00</td><td class="n">0.79, 0.76</td></tr>
174
+ <tr><td>Sheet vs a 14-pixel tile shuffle, a size never trained on</td><td class="n">0.98</td><td class="n">0.92</td><td class="n">0.53, 0.57</td></tr>
175
+ <tr><td>Sheet vs 3 voxels off</td><td class="n">0.87</td><td class="n">0.68</td><td class="n"></td></tr>
176
+ </tbody>
177
+ </table>
178
+ <p class="note">120 and 62 patches; the pixel measure is structure-tensor coherence. A first round failed (it preferred tile-shuffled renders, because it rewarded the edges a shuffle adds) and stays on record; the second added shuffles of other sizes to training. All of it is model output.</p>
179
+ </div></section>
180
+
181
+ <section id="limits"><div class="wrap">
182
+ <h2>Limits</h2>
183
+ <ul class="limits">
184
+ <li><b>It does not tell which sheet.</b> Each patch is rendered at the brightest depth within 5 voxels of the trace, and sheets lie close together, so a trace on the neighboring sheet can still show fibers.</li>
185
+ <li><b>Tested setting:</b> open-data meshes on 2.2 to 2.4 um scans at level 2. Other scans still run, and the page says so.</li>
186
+ <li><b>Untested hints:</b> jump hints, fiber direction and best depth are ideas until a test of their own passes.</li>
187
+ <li><b>Built controls, not real faulty traces,</b> were what it was tested against.</li>
188
+ </ul>
189
+ </div></section>
190
+
191
+ <footer><div class="wrap">Fiber Checker · code and weights Apache 2.0 · scans and meshes: <a href="https://scrollprize.org/data">Vesuvius Challenge open data</a>, CC BY-NC 4.0, read live and never copied · model output, not a reading</div></footer>
192
+
193
+ <script>
194
+ const PAIRS=[0,1,2].map(n=>({n}));
195
+ const KIND=[["on","yes","On the traced surface","the CT as it is: strands run across the patch"],
196
+ ["scramble","no","Scrambled","same brightness and texture, strands gone"],
197
+ ["shuffle","no","Tile shuffled","cut into 14-pixel tiles, turned and shuffled"],
198
+ ["off","no","Off the surface","the CT 5 voxels from the trace"]];
199
+ const SRC=["PHerc0139","PHerc0500P2","PHerc0343P"];
200
+ document.getElementById('pairs').innerHTML=PAIRS.map(p=>'<div class="pair">'+KIND.map(k=>'<figure><img loading="lazy" src="assets/pair-'+p.n+'-'+k[0]+'.webp" alt="'+k[2]+'"><figcaption><span class="tag '+k[1]+'">'+(k[1]==='yes'?'fibers':'control')+'</span><br><b>'+k[2]+'</b>: '+k[3]+'</figcaption></figure>').join('')+'<p class="pairsrc" data-n="'+p.n+'"></p></div>').join('');
201
+ fetch('assets/media.json').then(r=>r.json()).then(m=>{document.querySelectorAll('.pairsrc').forEach(el=>{const s=m.pictures['pair-'+el.dataset.n+'-*.webp'];if(s)el.textContent='From '+s+'. Public scroll on no prize list.'})}).catch(()=>{});
202
+ </script>
203
+ </body>
204
+ </html>