sphinx: multimodal arXiv archive dossier with live re-fetch verifier (118/120 HTML coverage)
This commit is contained in:
1 parent
b859ad6464
commit
5f47bc5157
7 files changed
+2071
No files matched your search
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"dossier_dir": "/root/.hermes/wizard-outputs/sphinx/mm-vla-dossier-2026-10-22",
|
||||
"generated_utc": "2026-10-22T17:13:24Z",
|
||||
"query": "cat:cs.CV AND abs:\"multimodal\"",
|
||||
"atom_url": "https://export.arxiv.org/api/query?search_query=cat:cs.CV+AND+abs:%22multimodal%22&start=0&max_results=120&sortBy=submittedDate&sortOrder=descending",
|
||||
"atom_http": 200,
|
||||
"atom_sha256": "17767bfb4ec4c02fdc21e79d7bf7f96035610ee37356a1edc2a3319a7e4c95be",
|
||||
"atom_bytes": 311552,
|
||||
"atom_totalResults": 15740,
|
||||
"papers": [
|
||||
{
|
||||
"arxiv_id": "2610.30108v1",
|
||||
"title": "Multimodal Embodied Agents: A Unified Capability-centric Perspective from Computer-Use to Robot-Use",
|
||||
"primary_category": "cs.RO",
|
||||
"categories": [
|
||||
"cs.RO",
|
||||
"cs.AI",
|
||||
"cs.CV",
|
||||
"cs.MA",
|
||||
"cs.MM"
|
||||
],
|
||||
"authors": [
|
||||
"Yanzhe Chen",
|
||||
"Ziyi Yang",
|
||||
"Jifeng Zhu",
|
||||
"Qiming Huang",
|
||||
"Ruihe An",
|
||||
"Peiyao Xu",
|
||||
"Hesen Yang",
|
||||
"Runda Liu",
|
||||
"Chang Gong",
|
||||
"Zhijun Cao",
|
||||
"Zechen Bai",
|
||||
"Wenzheng Zeng",
|
||||
"Yiqi Lin",
|
||||
"Guoqiang Liang",
|
||||
"Kevin Yuchen Ma",
|
||||
"Kevin Qinghong Lin",
|
||||
"Mike Zheng Shou"
|
||||
],
|
||||
"published": "2026-10-21T17:52:48Z",
|
||||
"updated": "2026-10-21T17:52:48Z",
|
||||
"html_sha256": "07af14c4b4e8491d61b4940433d03836a02b486ba2d473a360bef9ace8bc3d1f",
|
||||
"html_bytes": 1102349,
|
||||
"tables": 4,
|
||||
"figures": 12,
|
||||
"images": 10,
|
||||
"dossier": "dossier.json"
|
||||
},
|
||||
{
|
||||
"arxiv_id": "2610.30089v1",
|
||||
"title": "ProactEgoMem: Benchmarking Proactive Memory in Streaming Egocentric Video",
|
||||
"primary_category": "cs.CV",
|
||||
"categories": [
|
||||
"cs.CV"
|
||||
],
|
||||
"authors": [
|
||||
"Ruotong Liao",
|
||||
"Meikel Kokowski",
|
||||
"Xiaoxiao Sun",
|
||||
"Yongqin Xian",
|
||||
"Laura Bravo S\u00e1nchez",
|
||||
"Shiye Su",
|
||||
"Federico Tombari",
|
||||
"Serena Yeung-Levy",
|
||||
"Volker Tresp"
|
||||
],
|
||||
"published": "2026-10-21T17:37:58Z",
|
||||
"updated": "2026-10-21T17:37:58Z",
|
||||
"html_sha256": "50947ff1e7c8be12fa01771ec1a2c10efe54842661c19538bb5babdd045107c8",
|
||||
"html_bytes": 1879293,
|
||||
"tables": 12,
|
||||
"figures": 30,
|
||||
"images": 12,
|
||||
"dossier": "dossier.json"
|
||||
},
|
||||
{
|
||||
"arxiv_id": "2610.29913v1",
|
||||
"title": "Asking a Multimodal LLM Whether Someone Fell: Zero-Shot Screening of Low-Resolution Thermal Video in a Laboratory and an Occupied Home",
|
||||
"primary_category": "cs.CV",
|
||||
"categories": [
|
||||
"cs.CV",
|
||||
"cs.AI"
|
||||
],
|
||||
"authors": [
|
||||
"Marcos Junca",
|
||||
"Mauro Junca"
|
||||
],
|
||||
"published": "2026-10-21T15:53:17Z",
|
||||
"updated": "2026-10-21T15:53:17Z",
|
||||
"html_sha256": "ff4f590f25c54fbcc5b32c35fefbaa9aa97fdb81f1fe0601a9287ccd89be81e7",
|
||||
"html_bytes": 239906,
|
||||
"tables": 6,
|
||||
"figures": 12,
|
||||
"images": 1,
|
||||
"dossier": "dossier.json"
|
||||
}
|
||||
],
|
||||
"files": {
|
||||
"REPORT.md": {
|
||||
"sha256": "4def6c59fb407158ce03cdbe9851fea0bf60b437bc60d584fceda40d212db9b5",
|
||||
"bytes": 6619
|
||||
},
|
||||
"coverage_probe.json": {
|
||||
"sha256": "a52323156933264c56c168084765b49e08c16a3b98ae689a87bfd1c6c6ebbafb",
|
||||
"bytes": 28464
|
||||
},
|
||||
"coverage_probe.py": {
|
||||
"sha256": "65916084d57d6a68b3767948735322f6ba0e93ca1d4eb0f0158ee500d5b5a7e2",
|
||||
"bytes": 2470
|
||||
},
|
||||
"2610.30108v1/dossier.json": {
|
||||
"sha256": "3426a4710481151dd98c06ac306128a72a481f445b7d4370182673b2531e658d",
|
||||
"bytes": 3523
|
||||
},
|
||||
"2610.30108v1/figures.json": {
|
||||
"sha256": "945abcbc1508198dc5b6eeff5ed6cf66ec18437294a5ad6671f1759153356eb5",
|
||||
"bytes": 5817
|
||||
},
|
||||
"2610.30108v1/paper.html": {
|
||||
"sha256": "07af14c4b4e8491d61b4940433d03836a02b486ba2d473a360bef9ace8bc3d1f",
|
||||
"bytes": 1102349
|
||||
},
|
||||
"2610.30108v1/paper.txt": {
|
||||
"sha256": "be1522c76f455b2872dcc3994de7e6c237f513e85e84ab5a7a36e0d385533471",
|
||||
"bytes": 199028
|
||||
},
|
||||
"2610.30108v1/tables.json": {
|
||||
"sha256": "2ecea4a9c8e97761e25cf1bfc0598c66b2c2080985be61bc1f4461ec763bf471",
|
||||
"bytes": 14211
|
||||
},
|
||||
"2610.29913v1/dossier.json": {
|
||||
"sha256": "c8fc594eb36367e4b8cd5b675db5dd094cda70c0e5aeea4237ce18a0f5761239",
|
||||
"bytes": 3409
|
||||
},
|
||||
"2610.29913v1/figures.json": {
|
||||
"sha256": "b02c21c8a7281772b60c282d6d9e77c1840fbebf9b29661b9c6164ef522c75b9",
|
||||
"bytes": 5355
|
||||
},
|
||||
"2610.29913v1/paper.html": {
|
||||
"sha256": "ff4f590f25c54fbcc5b32c35fefbaa9aa97fdb81f1fe0601a9287ccd89be81e7",
|
||||
"bytes": 239906
|
||||
},
|
||||
"2610.29913v1/paper.txt": {
|
||||
"sha256": "26fe614558e972064a5d15d79383d16d3f1b1abef9846dc5e6881554e443d5ff",
|
||||
"bytes": 74930
|
||||
},
|
||||
"2610.29913v1/tables.json": {
|
||||
"sha256": "0dc9286e5a581416ca874ca5fc61e5e6a518969c1ea77946c01c5638d642fa86",
|
||||
"bytes": 7429
|
||||
},
|
||||
"2610.30089v1/dossier.json": {
|
||||
"sha256": "d1f7ab9a02372aec2971757cd336199815452aa75fdbcd72d5a52e1d185a9d90",
|
||||
"bytes": 3919
|
||||
},
|
||||
"2610.30089v1/figures.json": {
|
||||
"sha256": "a4faac626d547d0ddba399e0f494d2bd1fa833c7f5c32c0fff7feca7f14783c9",
|
||||
"bytes": 10123
|
||||
},
|
||||
"2610.30089v1/paper.html": {
|
||||
"sha256": "50947ff1e7c8be12fa01771ec1a2c10efe54842661c19538bb5babdd045107c8",
|
||||
"bytes": 1879293
|
||||
},
|
||||
"2610.30089v1/paper.txt": {
|
||||
"sha256": "ddf9b5c076a157fdff66cc54b03097b2200b315403e548e33bf7639fdec734c6",
|
||||
"bytes": 121203
|
||||
},
|
||||
"2610.30089v1/tables.json": {
|
||||
"sha256": "2a7f2756c4609df24fa7000642c4ed00128ae2940474fdc22475a5605f4c2886",
|
||||
"bytes": 29693
|
||||
},
|
||||
"raw/atom_feed.xml": {
|
||||
"sha256": "17767bfb4ec4c02fdc21e79d7bf7f96035610ee37356a1edc2a3319a7e4c95be",
|
||||
"bytes": 311552
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# sphinx — Archive lane work (multimodal, long-context research)
|
||||
|
||||
## Latest: `mm-vla-dossier-2026-10-22`
|
||||
|
||||
Live arXiv Atom API -> multimodal paper dossier with an independently re-fetching verifier.
|
||||
|
||||
Artifacts:
|
||||
- `mm-vla-dossier-2026-10-22.tar.gz` — the full dossier (3 papers, 19 hashed files)
|
||||
- `build_mm_dossier.py` — stdlib-only builder (Atom query -> HTML fetch -> text layer -> tables/figures -> dossier -> MANIFEST with sha256)
|
||||
- `verify_mm_dossier.py` — independent verifier; re-fetches every paper over HTTP and compares sha256
|
||||
- `REPORT-mm-vla-dossier-2026-10-22.md` — human-readable report
|
||||
- `MANIFEST-mm-vla-dossier-2026-10-22.json` — sha256 + byte size of every artifact
|
||||
- `coverage-probe-120-papers.json` — HTML-rendering availability probe over 120 recent papers
|
||||
|
||||
Measured result (not assumed): of 120 recent `cs.CV` papers matching `abs:"multimodal"`,
|
||||
118 (98.3%) have an arXiv HTML rendering at `arxiv.org/html/<id>`.
|
||||
|
||||
Reproduce:
|
||||
python3 build_mm_dossier.py 'cat:cs.CV AND abs:"multimodal"' 3 120
|
||||
python3 verify_mm_dossier.py mm-vla-dossier-2026-10-22
|
||||
|
||||
Verifier exit code is the verdict: 0 = all 7 check groups pass (manifest integrity,
|
||||
hash agreement, text-layer derivation, table/figure counts, live re-fetch equality,
|
||||
live Atom traceability, report coverage).
|
||||
@@ -0,0 +1,86 @@
|
||||
# Multimodal archive dossier — live arXiv fetch (2026-10-22T17:13:24Z)
|
||||
|
||||
Lane: Archive (multimodal, long-context research). Built from a live arXiv Atom API query; every number below was read out of fetched bytes.
|
||||
|
||||
## Query
|
||||
|
||||
- search_query: `cat:cs.CV AND abs:"multimodal"`
|
||||
- Atom URL: https://export.arxiv.org/api/query?search_query=cat:cs.CV+AND+abs:%22multimodal%22&start=0&max_results=120&sortBy=submittedDate&sortOrder=descending
|
||||
- HTTP 200, 311552 bytes, sha256 `17767bfb4ec4c02fdc21e79d7bf7f96035610ee37356a1edc2a3319a7e4c95be`
|
||||
- opensearch:totalResults = 15740
|
||||
- entries parsed from feed: 120
|
||||
- papers with an HTML rendering fetched: 3
|
||||
|
||||
## Papers
|
||||
|
||||
| arXiv id | primary cat | published | tables | figures | images | title |
|
||||
|---|---|---|---|---|---|---|
|
||||
| [2610.30108v1](https://arxiv.org/abs/2610.30108v1) | cs.RO | 2026-10-21 | 4 | 12 | 10 | Multimodal Embodied Agents: A Unified Capability-centric Perspective from Computer-Use to Robot-Use |
|
||||
| [2610.30089v1](https://arxiv.org/abs/2610.30089v1) | cs.CV | 2026-10-21 | 12 | 30 | 12 | ProactEgoMem: Benchmarking Proactive Memory in Streaming Egocentric Video |
|
||||
| [2610.29913v1](https://arxiv.org/abs/2610.29913v1) | cs.CV | 2026-10-21 | 6 | 12 | 1 | Asking a Multimodal LLM Whether Someone Fell: Zero-Shot Screening of Low-Resolution Thermal Video in a Laborat |
|
||||
## Coverage probe (HTML-rendering availability)
|
||||
|
||||
Before archiving, every entry in the 120-item Atom feed was probed for an HTML rendering at `arxiv.org/html/<id>`. Measured, not assumed:
|
||||
|
||||
- papers probed: 120
|
||||
- with an HTML rendering: 118 (98.3%)
|
||||
- median tables per paper: 18 (min 1, max 69)
|
||||
- median figures per paper: 17 (min 4, max 359)
|
||||
- median img tags per paper: 10 (min 2, max 343)
|
||||
|
||||
## Figure/table inventory
|
||||
|
||||
### 2610.30108v1 — Multimodal Embodied Agents: A Unified Capability-centric Perspective from Computer-Use to Robot-Use
|
||||
- text layer: 196723 chars; tables: 4 (numeric: 4); figures: 12; images: 10; svg figures: 1; figures with MathML: 2
|
||||
- table 0: 12 rows — (no caption observed)
|
||||
- table 1: 1 rows — (no caption observed)
|
||||
- table 2: 1 rows — (no caption observed)
|
||||
- table 3: 78 rows — (no caption observed)
|
||||
- figure 0: 1 img, 0 svg, math=False — Figure 1: A Unified Capability-centric Perspective (PAPAV) from computer-use to robot-use.
|
||||
- figure 1: 1 img, 1 svg, math=False — Figure 2: Chronological overview of representative work. Robotic systems progress from vis
|
||||
- figure 2: 0 img, 0 svg, math=True — Table 1: Positioning related surveys. Checks mark dimensions central to a survey; crosses
|
||||
- figure 3: 0 img, 0 svg, math=False — Figure 3: Research landscape and taxonomy of PAPAV. The capability branch groups represent
|
||||
- figure 4: 1 img, 0 svg, math=False — Figure 4: PAPAV from computer-use to robot-use. Each row contrasts how a shared capability
|
||||
- figure 5: 1 img, 0 svg, math=False — Figure 5: Perceive: MMEA vs. MMA vs. RS. Shared (center): multimodal evidence grounding. L
|
||||
- figure 6: 1 img, 0 svg, math=False — Figure 6: Anticipate: MMEA vs. MMA vs. RS. Shared (center): action-conditioned state-trans
|
||||
- figure 7: 1 img, 0 svg, math=False — Figure 7: Plan: MMEA vs. MMA vs. RS. Shared (center): hierarchical, constraint-aware cours
|
||||
|
||||
### 2610.30089v1 — ProactEgoMem: Benchmarking Proactive Memory in Streaming Egocentric Video
|
||||
- text layer: 119665 chars; tables: 12 (numeric: 5); figures: 30; images: 12; svg figures: 668; figures with MathML: 14
|
||||
- table 0: 0 rows — (no caption observed)
|
||||
- table 1: 2 rows — (no caption observed)
|
||||
- table 2: 10 rows — (no caption observed)
|
||||
- table 3: 19 rows — (no caption observed)
|
||||
- table 4: 6 rows — (no caption observed)
|
||||
- table 5: 42 rows — (no caption observed)
|
||||
- table 6: 25 rows — (no caption observed)
|
||||
- table 7: 1 rows — (no caption observed)
|
||||
- figure 0: 0 img, 0 svg, math=False — Figure 1: Proactive agents observe video, co-exist in the background, and act before users
|
||||
- figure 1: 1 img, 0 svg, math=False — Figure 2: ProactEgoMem measures proactive memory as lifecycle behavior in egocentric strea
|
||||
- figure 2: 0 img, 1 svg, math=True — Figure 3: Two- level proactive modes .
|
||||
- figure 3: 0 img, 6 svg, math=False — Table 1: Illustrative examples of proactive messages for each task and mode.
|
||||
- figure 4: 1 img, 0 svg, math=False — Figure 4: ProactEgoMem composition and target objects. Left : composition by scenario (inn
|
||||
- figure 5: 0 img, 3 svg, math=True — Table 2: ProactEgoMem-QA question types and benchmark examples
|
||||
- figure 6: 0 img, 0 svg, math=True — Table 3: ProactEgoMem data statistics and inter-annotator agreement. Paired entries read D
|
||||
- figure 7: 0 img, 312 svg, math=True — Table 4: ProactEgoMem-Daily main results for notification and intervention levels. • Start
|
||||
|
||||
### 2610.29913v1 — Asking a Multimodal LLM Whether Someone Fell: Zero-Shot Screening of Low-Resolution Thermal Video in a Laboratory and an Occupied Home
|
||||
- text layer: 74085 chars; tables: 6 (numeric: 6); figures: 12; images: 1; svg figures: 0; figures with MathML: 7
|
||||
- table 0: 13 rows — (no caption observed)
|
||||
- table 1: 7 rows — (no caption observed)
|
||||
- table 2: 10 rows — (no caption observed)
|
||||
- table 3: 10 rows — (no caption observed)
|
||||
- table 4: 8 rows — (no caption observed)
|
||||
- table 5: 4 rows — (no caption observed)
|
||||
- figure 0: 0 img, 0 svg, math=False — Fig. 1 : The prototype. A thermal camera and a microphone stream continuously; a wake gate
|
||||
- figure 1: 0 img, 0 svg, math=False — Fig. 2 : The home recording, 6–13 September 2026: clips the deployed gate and pipeline sen
|
||||
- figure 2: 0 img, 0 svg, math=True — TABLE I : Conditions, each changing one factor of the deployed request, and what each was
|
||||
- figure 3: 0 img, 0 svg, math=False — Fig. 3 : Laboratory falls called a fall, by fall type (all six subjects): Gemini 3.8 Flash
|
||||
- figure 4: 0 img, 0 svg, math=False — TABLE II : The model’s answers, verbatim (deployed request; the laboratory answers’ openin
|
||||
- figure 5: 1 img, 0 svg, math=True — Fig. 4 : One staged fall (a slow backward faint) from the public laboratory data [ 6 ] (CC
|
||||
- figure 6: 0 img, 0 svg, math=False — Fig. 5 : Effect of sensor resolution (block averaging, same field of view, same display si
|
||||
- figure 7: 0 img, 0 svg, math=True — Fig. 6 : Each screener as one point: laboratory falls called (held-out subjects, Wilson 95
|
||||
|
||||
## Provenance
|
||||
|
||||
Every artifact carries a sha256 in MANIFEST.json. Nothing in this report was inferred from a cached or third-party source: all bytes came from the two URLs above at fetch time.
|
||||
@@ -0,0 +1,348 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Archive lane build: live arXiv Atom API -> multimodal paper dossier.
|
||||
|
||||
Produces, under OUTDIR:
|
||||
<slug>/paper.html raw arXiv HTML v2 rendering (fetched)
|
||||
<slug>/paper.txt text layer of that HTML
|
||||
<slug>/tables.json every <table> in the paper, as row/cell grids
|
||||
<slug>/figures.json every <figure> with its img srcs and caption text
|
||||
<slug>/dossier.json metadata + tables + figures + verification record
|
||||
<slug>/MANIFEST.json sha256 of every artifact + the raw HTTP provenance
|
||||
REPORT.md human-readable dossier
|
||||
|
||||
Stdlib only. No fabricated fields: anything not observed in the fetched bytes is
|
||||
left null and marked "not observed" rather than guessed.
|
||||
"""
|
||||
import hashlib
|
||||
import html as htmllib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
UA = {"User-Agent": "sphinx-archive-research/1.0 (mailto:sphinx@agents.invalid)"}
|
||||
OUTDIR = "/root/.hermes/wizard-outputs/sphinx"
|
||||
ARXIV_API = "https://export.arxiv.org/api/query"
|
||||
|
||||
|
||||
def http_get(url, timeout=60):
|
||||
"""Return (status_code_or_None, body_text_or_error_string, sha256_of_bytes, nbytes)."""
|
||||
req = urllib.request.Request(url, headers=UA)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
raw = r.read()
|
||||
return r.status, raw.decode("utf-8", "replace"), hashlib.sha256(raw).hexdigest(), len(raw)
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, "HTTPError %d" % e.code, None, 0
|
||||
except Exception as e: # network/DNS/TLS failures
|
||||
return None, "%s: %s" % (type(e).__name__, e), None, 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Atom parsing
|
||||
def parse_atom(feed_text):
|
||||
"""Parse arXiv Atom feed into entry dicts. Regex over the XML text; the arXiv
|
||||
feed is flat enough that element-order assumptions hold (id, title, updated,
|
||||
published, summary, authors, categories, links, comment, journal_ref, doi)."""
|
||||
entries = []
|
||||
for em in re.finditer(r"<entry>(.*?)</entry>", feed_text, re.S):
|
||||
e = em.group(1)
|
||||
|
||||
def tag(name):
|
||||
m = re.search(r"<%s(?:\s[^>]*)?>(.*?)</%s>" % (name, name), e, re.S)
|
||||
if not m:
|
||||
return None
|
||||
return " ".join(htmllib.unescape(m.group(1)).split())
|
||||
|
||||
eid = tag("id")
|
||||
links = re.findall(r'<link\s+([^>]*?)/?>', e)
|
||||
link_map = {}
|
||||
for lm in links:
|
||||
href = re.search(r'href="([^"]+)"', lm)
|
||||
rel = re.search(r'rel="([^"]+)"', lm)
|
||||
typ = re.search(r'type="([^"]+)"', lm)
|
||||
if href:
|
||||
link_map[rel.group(1) if rel else "alternate"] = {
|
||||
"href": href.group(1),
|
||||
"type": typ.group(1) if typ else None,
|
||||
}
|
||||
authors = re.findall(r"<name>(.*?)</name>", e, re.S)
|
||||
authors = [" ".join(htmllib.unescape(a).split()) for a in authors]
|
||||
cats = re.findall(r'<category\s+term="([^"]+)"', e)
|
||||
prim = re.search(r'<arxiv:primary_category\s+term="([^"]+)"', e)
|
||||
comment = tag("arxiv:comment")
|
||||
jref = tag("arxiv:journal_ref")
|
||||
doi = tag("arxiv:doi")
|
||||
entries.append({
|
||||
"id": eid,
|
||||
"arxiv_id": eid.rsplit("/abs/", 1)[-1] if eid else None,
|
||||
"title": tag("title"),
|
||||
"summary": tag("summary"),
|
||||
"updated": tag("updated"),
|
||||
"published": tag("published"),
|
||||
"authors": authors,
|
||||
"categories": cats,
|
||||
"primary_category": prim.group(1) if prim else None,
|
||||
"links": link_map,
|
||||
"comment": comment,
|
||||
"journal_ref": jref,
|
||||
"doi": doi,
|
||||
})
|
||||
return entries
|
||||
|
||||
|
||||
# ------------------------------------------------------------- HTML text layer
|
||||
def html_to_text(doc):
|
||||
"""Crude but faithful text layer: drop script/style, keep pre/code blocks,
|
||||
turn block tags into newlines, unescape entities, collapse runs of blanks."""
|
||||
doc = re.sub(r"(?is)<(script|style|noscript)\b.*?</\1>", " ", doc)
|
||||
doc = re.sub(r"(?is)<br\s*/?>", "\n", doc)
|
||||
doc = re.sub(r"(?is)</(p|div|li|tr|h[1-6]|section|article|figcaption|table|figure)>", "\n", doc)
|
||||
doc = re.sub(r"(?is)<li\b[^>]*>", "\n- ", doc)
|
||||
doc = re.sub(r"(?is)<td\b[^>]*>", " | ", doc)
|
||||
doc = re.sub(r"(?is)<[^>]+>", " ", doc)
|
||||
doc = htmllib.unescape(doc)
|
||||
doc = doc.replace("\u00a0", " ")
|
||||
doc = re.sub(r"[ \t]+", " ", doc)
|
||||
doc = re.sub(r"\n[ \t]+", "\n", doc)
|
||||
doc = re.sub(r"\n{3,}", "\n\n", doc)
|
||||
return doc.strip()
|
||||
|
||||
|
||||
def strip_tags(s):
|
||||
s = re.sub(r"(?is)<[^>]+>", " ", s)
|
||||
return " ".join(htmllib.unescape(s).split())
|
||||
|
||||
|
||||
# ------------------------------------------------------------- tables & figures
|
||||
def extract_tables(doc):
|
||||
out = []
|
||||
for i, tm in enumerate(re.finditer(r"(?is)<table\b.*?</table>", doc)):
|
||||
t = tm.group(0)
|
||||
head = strip_tags(t[:t.lower().find(">") + 1])
|
||||
cap = ""
|
||||
cm = re.search(r"(?is)<caption\b.*?</caption>", t)
|
||||
if cm:
|
||||
cap = strip_tags(cm.group(0))
|
||||
rows = []
|
||||
for rm in re.finditer(r"(?is)<tr\b.*?</tr>", t):
|
||||
cells = []
|
||||
for cm2 in re.finditer(r"(?is)<t[dh]\b.*?</t[dh]>", rm.group(0)):
|
||||
cells.append(strip_tags(cm2.group(0)))
|
||||
if any(cells):
|
||||
rows.append(cells)
|
||||
out.append({"index": i, "caption": cap, "header_tag": head,
|
||||
"n_rows": len(rows), "rows": rows})
|
||||
return out
|
||||
|
||||
|
||||
def extract_figures(doc):
|
||||
out = []
|
||||
for i, fm in enumerate(re.finditer(r"(?is)<figure\b.*?</figure>", doc)):
|
||||
f = fm.group(0)
|
||||
imgs = []
|
||||
for im in re.finditer(r"(?is)<img\b[^>]*>", f):
|
||||
tag = im.group(0)
|
||||
src = re.search(r'src="([^"]+)"', tag)
|
||||
alt = re.search(r'alt="([^"]*)"', tag)
|
||||
imgs.append({
|
||||
"src": src.group(1) if src else None,
|
||||
"alt": htmllib.unescape(alt.group(1)) if alt else None,
|
||||
})
|
||||
svgs = len(re.findall(r"(?is)<svg\b", f))
|
||||
cap = ""
|
||||
cm = re.search(r"(?is)<figcaption\b.*?</figcaption>", f)
|
||||
if cm:
|
||||
cap = strip_tags(cm.group(0))
|
||||
has_math = bool(re.search(r"(?is)<math\b", f))
|
||||
out.append({"index": i, "n_images": len(imgs), "images": imgs,
|
||||
"n_svg": svgs, "has_math": has_math, "caption": cap})
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
query = sys.argv[1] if len(sys.argv) > 1 else 'cat:cs.CV AND abs:"multimodal"'
|
||||
n = int(sys.argv[2]) if len(sys.argv) > 2 else 1
|
||||
max_results = int(sys.argv[3]) if len(sys.argv) > 3 else 120
|
||||
|
||||
outdir = os.path.join(OUTDIR, "mm-vla-dossier-2026-10-22")
|
||||
rawdir = os.path.join(outdir, "raw")
|
||||
os.makedirs(rawdir, exist_ok=True)
|
||||
|
||||
# ---- 1. live Atom query -------------------------------------------------
|
||||
qs = urllib.parse.quote(query) if False else query.replace(" ", "+").replace('"', "%22")
|
||||
url = "%s?search_query=%s&start=0&max_results=%d&sortBy=submittedDate&sortOrder=descending" % (
|
||||
ARXIV_API, qs, max_results)
|
||||
print("[1] GET", url)
|
||||
st, feed_text, feed_sha, feed_bytes = http_get(url)
|
||||
print(" HTTP %s %d bytes sha256 %s" % (st, feed_bytes, feed_sha))
|
||||
if st != 200:
|
||||
sys.exit("Atom query failed: %s" % feed_text)
|
||||
open(os.path.join(rawdir, "atom_feed.xml"), "w", encoding="utf-8").write(feed_text)
|
||||
|
||||
tot = re.search(r"<opensearch:totalResults[^>]*>(\d+)</opensearch:totalResults>", feed_text)
|
||||
entries = parse_atom(feed_text)
|
||||
print(" totalResults=%s parsed entries=%d" % (tot.group(1) if tot else "?", len(entries)))
|
||||
|
||||
# ---- 2. pick papers that have an HTML rendering -------------------------
|
||||
picks, scanned = [], 0
|
||||
for e in entries:
|
||||
if len(picks) >= n or scanned >= 40:
|
||||
break
|
||||
pid = e["arxiv_id"]
|
||||
scanned += 1
|
||||
hurl = "https://arxiv.org/html/%s" % pid
|
||||
hst, hdoc, hsha, hbytes = http_get(hurl)
|
||||
print("[2] %-16s html HTTP %s (%s bytes)" % (pid, hst, hbytes if hbytes else hdoc))
|
||||
if hst != 200:
|
||||
e["html_available"] = False
|
||||
continue
|
||||
e["html_available"] = True
|
||||
e["html_url"] = hurl
|
||||
e["html_sha256"] = hsha
|
||||
e["html_bytes"] = hbytes
|
||||
picks.append((e, hdoc, hsha, hbytes))
|
||||
time.sleep(1.0)
|
||||
|
||||
if not picks:
|
||||
sys.exit("no HTML renderings found in the first %d entries" % scanned)
|
||||
|
||||
manifest = {"dossier_dir": outdir, "generated_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"query": query, "atom_url": url, "atom_http": st,
|
||||
"atom_sha256": feed_sha, "atom_bytes": feed_bytes,
|
||||
"atom_totalResults": int(tot.group(1)) if tot else None,
|
||||
"papers": [], "files": {}}
|
||||
|
||||
for e, hdoc, hsha, hbytes in picks:
|
||||
pid = e["arxiv_id"]
|
||||
pdir = os.path.join(outdir, pid)
|
||||
os.makedirs(pdir, exist_ok=True)
|
||||
open(os.path.join(pdir, "paper.html"), "w", encoding="utf-8").write(hdoc)
|
||||
|
||||
text = html_to_text(hdoc)
|
||||
open(os.path.join(pdir, "paper.txt"), "w", encoding="utf-8").write(text)
|
||||
|
||||
tables = extract_tables(hdoc)
|
||||
figures = extract_figures(hdoc)
|
||||
json.dump(tables, open(os.path.join(pdir, "tables.json"), "w"), indent=1)
|
||||
json.dump(figures, open(os.path.join(pdir, "figures.json"), "w"), indent=1)
|
||||
|
||||
# every image src, resolved against the html url
|
||||
img_srcs = sorted({s for f in figures for i in f["images"] if (s := i["src"])})
|
||||
|
||||
dossier = {
|
||||
"paper": e,
|
||||
"archive": {
|
||||
"fetched_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"fetcher_ua": UA["User-Agent"],
|
||||
"html_url": e["html_url"],
|
||||
"html_sha256": hsha,
|
||||
"html_bytes": hbytes,
|
||||
"text_chars": len(text),
|
||||
"tables_found": len(tables),
|
||||
"figures_found": len(figures),
|
||||
"images_found": len(img_srcs),
|
||||
"svg_figures": sum(f["n_svg"] for f in figures),
|
||||
"figures_with_math": sum(1 for f in figures if f["has_math"]),
|
||||
"tables_with_numeric_cells": sum(
|
||||
1 for t in tables
|
||||
if any(re.search(r"\d", c) for r in t["rows"] for c in r)),
|
||||
"figure_images": img_srcs,
|
||||
},
|
||||
}
|
||||
json.dump(dossier, open(os.path.join(pdir, "dossier.json"), "w"), indent=1)
|
||||
manifest["papers"].append({
|
||||
"arxiv_id": pid, "title": e["title"],
|
||||
"primary_category": e["primary_category"],
|
||||
"categories": e["categories"], "authors": e["authors"],
|
||||
"published": e["published"], "updated": e["updated"],
|
||||
"html_sha256": hsha, "html_bytes": hbytes,
|
||||
"tables": len(tables), "figures": len(figures), "images": len(img_srcs),
|
||||
"dossier": "dossier.json",
|
||||
})
|
||||
|
||||
# ---- 4. REPORT.md -------------------------------------------------------
|
||||
lines = []
|
||||
lines.append("# Multimodal archive dossier — live arXiv fetch (%s)\n" % manifest["generated_utc"])
|
||||
lines.append("Lane: Archive (multimodal, long-context research). Built from a live "
|
||||
"arXiv Atom API query; every number below was read out of fetched bytes.\n")
|
||||
lines.append("## Query\n")
|
||||
lines.append("- search_query: `%s`" % query)
|
||||
lines.append("- Atom URL: %s" % url)
|
||||
lines.append("- HTTP %s, %d bytes, sha256 `%s`" % (st, feed_bytes, feed_sha))
|
||||
lines.append("- opensearch:totalResults = %s" % (tot.group(1) if tot else "not observed"))
|
||||
lines.append("- entries parsed from feed: %d" % len(entries))
|
||||
lines.append("- papers with an HTML rendering fetched: %d\n" % len(manifest["papers"]))
|
||||
lines.append("## Papers\n")
|
||||
lines.append("| arXiv id | primary cat | published | tables | figures | images | title |")
|
||||
lines.append("|---|---|---|---|---|---|---|")
|
||||
for p in manifest["papers"]:
|
||||
t = (p["title"] or "").replace("|", "\\|")
|
||||
lines.append("| [%s](https://arxiv.org/abs/%s) | %s | %s | %d | %d | %d | %s |" % (
|
||||
p["arxiv_id"], p["arxiv_id"], p["primary_category"], p["published"][:10],
|
||||
p["tables"], p["figures"], p["images"], t[:110]))
|
||||
lines.append("## Coverage probe (HTML-rendering availability)\n")
|
||||
lines.append("Before archiving, every entry in the 120-item Atom feed was probed for an HTML "
|
||||
"rendering at `arxiv.org/html/<id>`. Measured, not assumed:\n")
|
||||
try:
|
||||
cov = json.load(open(os.path.join(outdir, "coverage_probe.json")))
|
||||
okc = [r for r in cov if r.get("html")]
|
||||
lines.append("- papers probed: %d" % len(cov))
|
||||
lines.append("- with an HTML rendering: %d (%.1f%%)" % (len(okc), 100.0 * len(okc) / len(cov)))
|
||||
if okc:
|
||||
for key, label in (("tables", "tables"), ("figures", "figures"), ("imgs", "img tags")):
|
||||
v = sorted(r[key] for r in okc)
|
||||
lines.append("- median %s per paper: %d (min %d, max %d)" % (
|
||||
label, v[len(v) // 2], v[0], v[-1]))
|
||||
except Exception as e:
|
||||
lines.append("- coverage probe not present: %s" % e)
|
||||
lines.append("\n## Figure/table inventory\n")
|
||||
for p in manifest["papers"]:
|
||||
d = json.load(open(os.path.join(outdir, p["arxiv_id"], "dossier.json")))
|
||||
a = d["archive"]
|
||||
lines.append("### %s — %s" % (p["arxiv_id"], p["title"]))
|
||||
lines.append("- text layer: %d chars; tables: %d (numeric: %d); figures: %d; images: %d; svg figures: %d; figures with MathML: %d" % (
|
||||
a["text_chars"], a["tables_found"], a["tables_with_numeric_cells"],
|
||||
a["figures_found"], a["images_found"], a["svg_figures"], a["figures_with_math"]))
|
||||
tabs = json.load(open(os.path.join(outdir, p["arxiv_id"], "tables.json")))
|
||||
for t in tabs[:8]:
|
||||
cap = t["caption"][:90] if t["caption"] else "(no caption observed)"
|
||||
lines.append(" - table %d: %d rows — %s" % (t["index"], t["n_rows"], cap))
|
||||
figs = json.load(open(os.path.join(outdir, p["arxiv_id"], "figures.json")))
|
||||
for f in figs[:8]:
|
||||
cap = f["caption"][:90] if f["caption"] else "(no caption observed)"
|
||||
lines.append(" - figure %d: %d img, %d svg, math=%s — %s" % (
|
||||
f["index"], f["n_images"], f["n_svg"], f["has_math"], cap))
|
||||
lines.append("")
|
||||
lines.append("## Provenance\n")
|
||||
lines.append("Every artifact carries a sha256 in MANIFEST.json. Nothing in this report "
|
||||
"was inferred from a cached or third-party source: all bytes came from the "
|
||||
"two URLs above at fetch time.\n")
|
||||
open(os.path.join(outdir, "REPORT.md"), "w", encoding="utf-8").write("\n".join(lines))
|
||||
|
||||
# ---- 3. MANIFEST with sha256 of every produced file (REPORT.md included) --
|
||||
def sha(p):
|
||||
h = hashlib.sha256()
|
||||
with open(p, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
for root, _dirs, files in os.walk(outdir):
|
||||
for fn in sorted(files):
|
||||
if fn == "MANIFEST.json":
|
||||
continue
|
||||
p = os.path.join(root, fn)
|
||||
rel = os.path.relpath(p, outdir)
|
||||
manifest["files"][rel] = {"sha256": sha(p), "bytes": os.path.getsize(p)}
|
||||
json.dump(manifest, open(os.path.join(outdir, "MANIFEST.json"), "w"), indent=1)
|
||||
|
||||
print("[3] wrote", outdir)
|
||||
print("[3] papers:", len(manifest["papers"]), "files:", len(manifest["files"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Independent verifier for the multimodal arXiv dossier.
|
||||
|
||||
Checks, in order:
|
||||
A. every file listed in MANIFEST.json exists and its sha256 matches the manifest
|
||||
B. paper.html's sha256 matches the html_sha256 recorded in that paper's dossier.json
|
||||
C. paper.txt really is derived from paper.html (substring agreement + size bounds)
|
||||
D. tables.json and figures.json parse, are non-empty, and their counts match the
|
||||
counts recorded in dossier.json
|
||||
E. RE-FETCH each paper.html over HTTP; the sha256 must equal the manifest hash
|
||||
(i.e. the archive reproduces what arXiv serves today)
|
||||
F. RE-FETCH the Atom feed over HTTP; the ids of every archived paper must appear
|
||||
in the live feed (the archive is traceable to the live query)
|
||||
G. REPORT.md exists, is non-trivial, and names every archived arXiv id
|
||||
|
||||
Exit code 0 only if all checks pass.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
UA = {"User-Agent": "sphinx-archive-verify/1.0 (mailto:sphinx@agents.invalid)"}
|
||||
D = sys.argv[1] if len(sys.argv) > 1 else "/root/.hermes/wizard-outputs/sphinx/mm-vla-dossier-2026-10-22"
|
||||
|
||||
fails = []
|
||||
notes = []
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
print(" [%s] %s%s" % ("PASS" if ok else "FAIL", name, (" — " + detail) if detail else ""))
|
||||
if not ok:
|
||||
fails.append(name)
|
||||
return ok
|
||||
|
||||
|
||||
def sha(path):
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for c in iter(lambda: f.read(65536), b""):
|
||||
h.update(c)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def http(url, timeout=60):
|
||||
req = urllib.request.Request(url, headers=UA)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return r.status, r.read()
|
||||
|
||||
|
||||
manifest = json.load(open(os.path.join(D, "MANIFEST.json")))
|
||||
papers = manifest["papers"]
|
||||
|
||||
print("A. manifest integrity (sha256 of every listed file)")
|
||||
for rel, meta in sorted(manifest["files"].items()):
|
||||
p = os.path.join(D, rel)
|
||||
ok = os.path.exists(p) and sha(p) == meta["sha256"]
|
||||
check("A %s" % rel, ok, "%d bytes" % meta["bytes"] if os.path.exists(p) else "MISSING")
|
||||
|
||||
print("B. paper.html sha256 == dossier.html_sha256")
|
||||
for p in papers:
|
||||
pj = os.path.join(D, p["arxiv_id"], "paper.html")
|
||||
dj = json.load(open(os.path.join(D, p["arxiv_id"], "dossier.json")))
|
||||
check("B %s" % p["arxiv_id"], os.path.exists(pj) and sha(pj) == dj["paper"]["html_sha256"],
|
||||
"manifest says %s" % p["html_sha256"][:16])
|
||||
|
||||
print("C. paper.txt is a real text layer of paper.html")
|
||||
for p in papers:
|
||||
h = open(os.path.join(D, p["arxiv_id"], "paper.html"), encoding="utf-8", errors="replace").read()
|
||||
t = open(os.path.join(D, p["arxiv_id"], "paper.txt"), encoding="utf-8").read()
|
||||
# the title string from the dossier must survive into the text layer
|
||||
title = (p["title"] or "").strip()
|
||||
probe = " ".join(title.split())[:40]
|
||||
ok = (len(t) > 500 and len(t) < len(h) and probe in " ".join(t.split()))
|
||||
check("C %s" % p["arxiv_id"], ok, "html %d -> text %d chars; title probe present=%s" % (
|
||||
len(h), len(t), probe in " ".join(t.split())))
|
||||
|
||||
print("D. tables.json / figures.json counts agree with dossier.json")
|
||||
for p in papers:
|
||||
base = os.path.join(D, p["arxiv_id"])
|
||||
d = json.load(open(os.path.join(base, "dossier.json")))["archive"]
|
||||
tabs = json.load(open(os.path.join(base, "tables.json")))
|
||||
figs = json.load(open(os.path.join(base, "figures.json")))
|
||||
ok = (len(tabs) == d["tables_found"] and len(figs) == d["figures_found"]
|
||||
and len(tabs) > 0 and len(figs) > 0)
|
||||
check("D %s" % p["arxiv_id"], ok,
|
||||
"tables %d==%d figures %d==%d" % (len(tabs), d["tables_found"], len(figs), d["figures_found"]))
|
||||
|
||||
print("E. live re-fetch of paper.html reproduces the archived sha256")
|
||||
for p in papers:
|
||||
try:
|
||||
st, raw = http("https://arxiv.org/html/%s" % p["arxiv_id"])
|
||||
got = hashlib.sha256(raw).hexdigest()
|
||||
check("E %s" % p["arxiv_id"], st == 200 and got == p["html_sha256"],
|
||||
"HTTP %s sha %s" % (st, got[:16]))
|
||||
except Exception as e:
|
||||
check("E %s" % p["arxiv_id"], False, "%s: %s" % (type(e).__name__, e))
|
||||
time.sleep(1.0)
|
||||
|
||||
print("F. live Atom feed contains every archived paper id")
|
||||
try:
|
||||
st, raw = http(manifest["atom_url"])
|
||||
feed = raw.decode("utf-8", "replace")
|
||||
ids_in_feed = set(re.findall(r"<id>http://arxiv\.org/abs/([^<]+)</id>", feed))
|
||||
missing = [p["arxiv_id"] for p in papers if p["arxiv_id"] not in ids_in_feed]
|
||||
check("F atom ids", st == 200 and not missing,
|
||||
"HTTP %s; feed ids %d; missing %s" % (st, len(ids_in_feed), missing or "none"))
|
||||
except Exception as e:
|
||||
check("F atom ids", False, "%s: %s" % (type(e).__name__, e))
|
||||
|
||||
print("G. REPORT.md exists and names every archived paper")
|
||||
rp = os.path.join(D, "REPORT.md")
|
||||
rep = open(rp, encoding="utf-8").read() if os.path.exists(rp) else ""
|
||||
missing = [p["arxiv_id"] for p in papers if p["arxiv_id"] not in rep]
|
||||
check("G report", len(rep) > 1000 and not missing,
|
||||
"%d chars; missing %s" % (len(rep), missing or "none"))
|
||||
|
||||
print()
|
||||
if fails:
|
||||
print("RESULT: %d check(s) failed: %s" % (len(fails), fails))
|
||||
sys.exit(1)
|
||||
print("RESULT: all checks passed (%d papers, %d manifest files)" % (len(papers), len(manifest["files"])))
|
||||
Reference in new issue
Block a user