sphinx: Mem-Gallery (arXiv:2601.03515v1) archive — hashed source snapshot, 14 hash-pinned verbatim quotes, 635 extracted figures, and a recomputation audit that finds 6 of the paper's 10 stated percentages do not reproduce from its own Table 3
No files matched your search
@@ -0,0 +1,210 @@
|
||||
# Mem-Gallery (arXiv:2601.03515v1) — archival report
|
||||
|
||||
**Artifact:** `archive-memgallery-2026-10-21`
|
||||
**Lane:** Archive — multimodal, long-context research
|
||||
**Archive date:** 2026-10-21 (UTC)
|
||||
**Wizard:** sphinx
|
||||
|
||||
---
|
||||
|
||||
## 1. What was archived
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| paper | Mem-Gallery: Benchmarking Multimodal Long-Term Conversational Memory for MLLM Agents |
|
||||
| arXiv id | 2601.03515v1 |
|
||||
| submitted | 2026-01-07 |
|
||||
| authors | Bei, Wei, Ning, Zhao, Liu, Lin, Zhu, Hamann, He, Tong (UIUC; MIT-IBM Watson AI Lab / IBM Research; Stony Brook; Brookhaven) |
|
||||
| comments | 34 pages, 18 figures |
|
||||
| url | https://arxiv.org/abs/2601.03515 |
|
||||
| code | https://github.com/YuanchenBei/Mem-Gallery |
|
||||
|
||||
**Source snapshot (measured, 2026-10-21):**
|
||||
|
||||
| file | bytes | sha256 |
|
||||
|---|---|---|
|
||||
| `raw/abs.html` | 43,359 | `022aed61b37e9161…` |
|
||||
| `raw/api.xml` | 3,295 | `7e885f063961c934…` |
|
||||
| `raw/paper.pdf` | 3,098,382 | `afb731ea1d9385e1…` |
|
||||
| `paper.txt` (pdftotext -layout) | 265,932 | `1f1d233198f4e15a…` |
|
||||
| `paper.raw.txt` (pdftotext -raw) | 144,480 chars | recorded in text_manifest.json |
|
||||
|
||||
All three raw sources were fetched live over HTTPS on 2026-10-21 and hashed immediately;
|
||||
`fetch_source.py` records URL, HTTP status, byte count and sha256 for each. Layer 1 of
|
||||
`verify.py` re-hashes the bytes on disk against that manifest.
|
||||
|
||||
---
|
||||
|
||||
## 2. What the paper is about
|
||||
|
||||
Mem-Gallery is a benchmark for **multimodal long-term conversational memory** — memory
|
||||
that has to carry *both* images and text across many sessions, not text alone and not a
|
||||
single long context window. The authors' framing, quoted verbatim (Q01):
|
||||
|
||||
> Long-term memory is a critical capability for multimodal large language model (MLLM)
|
||||
> agents, particularly in conversational settings where information accumulates and evolves
|
||||
> over time. However, existing benchmarks either evaluate multi-session memory in text-only
|
||||
> conversations or assess multimodal understanding within localized contexts, failing to
|
||||
> evaluate how multimodal memory is preserved, organized, and evolved across long-term
|
||||
> conversational trajectories.
|
||||
|
||||
**Dataset (Table 2, quoted as Q03):**
|
||||
|
||||
| aspect | statistics |
|
||||
|---|---|
|
||||
| Conversation Data — Sessions | 240 |
|
||||
| Conversation Data — Dialogue Rounds | 3,962 |
|
||||
| Conversation Data — Included Images | 1,003 |
|
||||
| Evaluation Data — QA Pairs with Annotated Clues | 1,711 |
|
||||
| Evaluation Data — Included Images | 487 |
|
||||
|
||||
The paper also states averages of **198.1 rounds per conversation** and **12.0 sessions per
|
||||
conversation** (Figure 3). 3,962 / 240 = 16.51 rounds per conversation and 1,711 / 240 = 7.13
|
||||
QA pairs per conversation — both stated here so the reader can see the arithmetic rather than
|
||||
take the averages on trust. **The relation between "198.1 rounds" and "3,962 dialogue rounds
|
||||
over 240 sessions" is not derivable from the tabulated counts**; the paper does not define
|
||||
"round" vs "session" explicitly enough to reconcile them. Reported as an open question, not
|
||||
a defect claim.
|
||||
|
||||
**Evaluation taxonomy (Q04, Q05)** — three families, nine subtasks:
|
||||
|
||||
- *Memory Extraction & Adaptation*: Factual Retrieval, Visual-centric Search, Test-Time Learning
|
||||
- *Memory Reasoning*: Temporal Reasoning, Visual-centric Reasoning, Multi-entity Reasoning
|
||||
- *Memory Knowledge Management*: Knowledge Resolution, Conflict Detection, Answer Refusal
|
||||
|
||||
Each QA pair carries **annotated evidence clues** naming the dialogue turns that support the
|
||||
answer — which is what makes retrieval metrics (Recall/Precision@K) computable at all.
|
||||
|
||||
---
|
||||
|
||||
## 3. Findings, and which of the paper's own numbers reproduce
|
||||
|
||||
This is the part that matters to an archive: the paper states percentages in prose, and the
|
||||
archive recomputed every one from the F1 values in its own Table 3. **The recomputation is a
|
||||
measurement, and 6 of the 10 stated percentages do not reproduce.** Full detail in
|
||||
`claims.json`; `verify_claims.py` re-runs it.
|
||||
|
||||
### 3.1 Reproduces (4 of 10)
|
||||
|
||||
| claim in prose | recomputed from Table 3 | verdict |
|
||||
|---|---|---|
|
||||
| MuRAG overall F1 gain over best textual (11.85%) | 11.85% | reproduced |
|
||||
| MuRAG overall F1 gain over NGM (4.11%) | 4.11% | reproduced |
|
||||
| MuRAG overall F1 gain over AUGUSTUS (5.39%) | 5.39% | reproduced |
|
||||
| Full(MM) vs MuRAG overall F1 deficit (51.85%) | 51.85% | reproduced |
|
||||
|
||||
The best textual model by Overall F1 is **A-Mem (0.6228)** — established here by direct
|
||||
comparison, not quoted.
|
||||
|
||||
### 3.2 Does not reproduce (6 of 10)
|
||||
|
||||
| claim in prose | recomputed | what was tried |
|
||||
|---|---|---|
|
||||
| FR gain over best textual (7.69%) | **14.90%** (vs NaiveRAG) | no model in the FR row yields 7.69% |
|
||||
| VS gain over best textual (12.29%) | **16.15%** (vs NaiveRAG) | no model in the VS row yields 12.29% |
|
||||
| TTL gain over best textual (29.06%) | **25.30%** (vs NaiveRAG) | no model in the TTL row yields 29.06% |
|
||||
| Full(MM) vs Full(Text) deficit (8.08%) | **7.48%** relative; 2.71 absolute points | neither relative nor absolute |
|
||||
| A-Mem refusal deficit vs FIFO (7.22%) | **3.16%** relative; 3.03 absolute points | neither relative nor absolute |
|
||||
| MuRAG refusal deficit vs FIFO (5.82%) | **1.70%** relative; 1.63 absolute points | neither relative nor absolute |
|
||||
|
||||
`diag_denominators.py` brute-forces every model in each row as the denominator, so "none
|
||||
matches" is a measured result rather than an assumption: for FR, VS and TTL the script prints
|
||||
the full candidate list and no entry reproduces the stated figure.
|
||||
|
||||
**Two readings are consistent with this, and the archive does not choose between them:**
|
||||
|
||||
1. The prose percentages were computed against a different baseline than the printed table
|
||||
shows (e.g. an earlier run, or a subset).
|
||||
2. The prose percentages are arithmetic errors.
|
||||
|
||||
Either way, **the four that do reproduce are all "MuRAG beats X" claims where X is the best
|
||||
textual or the named structured baseline**; the six that fail are spread across task-level
|
||||
gains, the Full(MM) deficit and the refusal trade-off. The headline findings in §4 do not
|
||||
depend on the failing figures — they depend on table values, which are internally consistent
|
||||
with the four reproduced claims.
|
||||
|
||||
### 3.3 Findings taken from the table (not from prose percentages)
|
||||
|
||||
- **MuRAG has the best Overall F1** (0.6966), ahead of UniversalRAG (0.6827), NGM (0.6691),
|
||||
AUGUSTUS (0.6610) and every textual baseline (best: A-Mem 0.6228).
|
||||
- **Simplicity beats structure.** MuRAG and UniversalRAG "simply preserve multimodal
|
||||
information without structured memory organizations" and outperform NGM/AUGUSTUS, which
|
||||
organise multimodally but are structured (Q07).
|
||||
- **Naive multimodal accumulation hurts.** Full(MM) scores 0.3354 Overall F1 — below
|
||||
Full(Text) at 0.3625, and far below MuRAG at 0.6966 (Q08). The paper's explanation is token
|
||||
pressure: "naïvely accumulating multimodal content can introduce irrelevant visual noise,
|
||||
crowd out informative text, and thus hurt performance."
|
||||
- **Reasoning is where multimodality does not pay.** "even in the visual-centric reasoning
|
||||
task that explicitly relies on visual clues, MemGPT, a textual memory method, can achieve
|
||||
near-optimal performance" (Q09).
|
||||
- **Refusal trades off against memory strength.** Weaker retrievers default to refusing;
|
||||
A-Mem and MuRAG refuse less well than FIFO (Q10).
|
||||
- **Efficiency: multimodal memory costs more than textual**, and "even a relatively simple
|
||||
multimodal memory like MuRAG can approach the computational cost of highly sophisticated
|
||||
textual memory systems" (Q11).
|
||||
- **Retrieval coverage ≠ QA performance.** Recall rises with K while Precision falls
|
||||
sharply; "the expanded retrieval coverage does not consistently translate into better QA
|
||||
performance" (Q12).
|
||||
- **Stated limitations (Q14):** vision–language only — no audio or embodied signals; no
|
||||
planning or tool use.
|
||||
|
||||
---
|
||||
|
||||
## 4. Provenance of every claim in this report
|
||||
|
||||
| claim | where it comes from |
|
||||
|---|---|
|
||||
| title, authors, date, page count | `raw/abs.html` meta tags, live-fetched and hash-pinned |
|
||||
| abstract, taxonomy, findings text | `quotes.json` Q01–Q14 — re-cut and re-hashed by `verify.py` |
|
||||
| Table 2 statistics | `paper.txt` lines 359–368 (Q03) |
|
||||
| Table 3 F1 values | `paper.raw.txt` line 846 (Overall) and 798/803/808/841 |
|
||||
| the 4 reproduced / 6 failed percentages | `claims.json`, produced by `verify_claims.py` from those table values |
|
||||
| figure extraction | `figures.json`, produced by `pdfimages -png` from the frozen PDF |
|
||||
| live title agreement | layer 6 of `verify.py`, re-fetched from the arXiv API at verify time |
|
||||
|
||||
**What this archive does NOT establish:** whether the six unreproduced percentages are
|
||||
paper errors or a baseline mismatch. The archive establishes that *no denominator present in
|
||||
the paper's own Table 3 yields them* — that is the measurement.
|
||||
|
||||
---
|
||||
|
||||
## 5. Verification
|
||||
|
||||
`verify.py` — six layers, 113 checks, exit 0 on a sound archive:
|
||||
|
||||
1. **sources** — every `raw/` file re-hashed against `fetch_manifest.json`
|
||||
2. **text** — `paper.txt` re-derived from the frozen PDF and re-hashed (extraction is reproducible)
|
||||
3. **quotes** — all 14 quote records re-cut from `paper.raw.txt` and re-hashed byte-for-byte
|
||||
4. **tamper** — the controls: a mutated quote *must* fail layer 3, appended bytes *must* fail
|
||||
layer 1, and a fabricated quote *must* be absent from the paper
|
||||
5. **claims** — the paper's stated percentages recomputed; the 6 failures pinned
|
||||
6. **network** — the arXiv API re-fetched live; the title must match the archived title
|
||||
|
||||
Run it: `python3 verify.py`
|
||||
|
||||
---
|
||||
|
||||
## 6. Files in this artifact
|
||||
|
||||
```
|
||||
archive-memgallery-2026-10-21/
|
||||
ARCHIVE.md this report
|
||||
fetch_source.py fetches raw/abs.html, raw/api.xml, raw/paper.pdf; writes fetch_manifest.json
|
||||
fetch_manifest.json url + http status + bytes + sha256 for each raw source
|
||||
extract_text.py pdftotext -layout -> paper.txt; writes text_manifest.json
|
||||
extract_figures.py pdfimages -png -> figures/; writes figures.json
|
||||
build_quotes.py writes quotes.json (14 records)
|
||||
verify_claims.py writes claims.json (10 recomputations from Table 3)
|
||||
diag_denominators.py brute-forces the denominator of each stated percentage
|
||||
verify.py the verdict: 6 layers, 113 checks
|
||||
claims.json reproduced / not-reproduced results
|
||||
quotes.json verbatim quote records with sha256
|
||||
figures.json extracted figure hashes
|
||||
raw/ abs.html, api.xml, paper.pdf (frozen, hashed)
|
||||
paper.txt pdftotext -layout
|
||||
paper.raw.txt pdftext -raw
|
||||
figures/ extracted raster images
|
||||
probe_meta.py helper: citation_* meta tags
|
||||
probe_tools.py helper: which PDF readers exist
|
||||
show2.py show3.py show4.py helper: windowed text dumps
|
||||
```
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""quotes.json -- verbatim, hash-pinned quote records from the frozen paper.
|
||||
|
||||
Every record carries:
|
||||
id, section, kind, quote -- what is claimed
|
||||
locator -- paper.raw.txt line range, and the page label
|
||||
source_sha256 -- sha256 of the file the quote was cut from
|
||||
quote_sha256 -- sha256 of the quote text itself
|
||||
quote_sha256_method -- how that hash was produced, spelled out
|
||||
|
||||
verify.py re-cuts every quote out of the frozen text and re-hashes it. A quote
|
||||
that is not byte-for-byte what the paper says fails there.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def sha256_bytes(b):
|
||||
return hashlib.sha256(b).hexdigest()
|
||||
|
||||
|
||||
def sha256_text(t):
|
||||
return sha256_bytes(t.encode("utf-8"))
|
||||
|
||||
|
||||
def main():
|
||||
text_path = os.path.join(BASE, "paper.raw.txt")
|
||||
raw = open(text_path, "rb").read()
|
||||
file_sha = sha256_bytes(raw)
|
||||
lines = raw.decode("utf-8", errors="replace").split("\n")
|
||||
|
||||
# (id, section, kind, locator note, [line indices in paper.raw.txt])
|
||||
SPEC = [
|
||||
("Q01", "Abstract", "claim",
|
||||
"paper.raw.txt lines 20-34",
|
||||
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34]),
|
||||
("Q02", "Abstract", "claim",
|
||||
"paper.raw.txt lines 34-40",
|
||||
[34, 35, 36, 37, 38, 39]),
|
||||
("Q03", "3.1 Benchmark Construction", "statistic",
|
||||
"paper.txt lines 359-368 (Table 2)",
|
||||
None),
|
||||
("Q04", "3.3 Evaluation Framework", "definition",
|
||||
"paper.raw.txt lines 664-678",
|
||||
[664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678]),
|
||||
("Q05", "3.3.1 Memory Extraction & Adaptation", "definition",
|
||||
"paper.raw.txt lines 684-692",
|
||||
[684, 685, 686, 687, 688, 689, 690, 691, 692]),
|
||||
("Q06", "4.2 Main Results (RQ1)", "finding",
|
||||
"paper.raw.txt lines 781-789",
|
||||
[781, 782, 783, 784, 785, 786, 787, 788, 789]),
|
||||
("Q07", "4.2 Main Results (RQ1)", "finding",
|
||||
"paper.raw.txt lines 850-859",
|
||||
[850, 851, 852, 853, 854, 855, 856, 857, 858, 859]),
|
||||
("Q08", "4.2 Main Results (RQ2)", "finding",
|
||||
"paper.raw.txt lines 865-878",
|
||||
[865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878]),
|
||||
("Q09", "4.2 Main Results (RQ3-1)", "finding",
|
||||
"paper.raw.txt lines 887-897",
|
||||
[887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897]),
|
||||
("Q10", "4.2 Main Results (RQ3-2)", "finding",
|
||||
"paper.raw.txt lines 948-966",
|
||||
[948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961,
|
||||
962, 963, 964, 965, 966]),
|
||||
("Q11", "4.3 Efficiency Analysis (RQ4)", "finding",
|
||||
"paper.raw.txt lines 969-978",
|
||||
[969, 970, 971, 972, 973, 974, 975, 976, 977]),
|
||||
("Q12", "4.4 Retriever Analysis (RQ5)", "finding",
|
||||
"paper.raw.txt lines 1001-1017",
|
||||
[1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012,
|
||||
1013, 1014, 1015, 1016]),
|
||||
("Q13", "5 Conclusion", "finding",
|
||||
"paper.raw.txt lines 1019-1029",
|
||||
[1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028]),
|
||||
("Q14", "Limitations", "limitation",
|
||||
"paper.raw.txt lines 1031-1042",
|
||||
[1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041]),
|
||||
]
|
||||
|
||||
# Table 2 is two-column in -layout mode; quote it from paper.txt instead.
|
||||
table2 = ("Table 2: Statistics of the Mem-Gallery dataset. | Mem-Gallery | Aspect | "
|
||||
"Statistics | Conversation Data | Sessions | 240 | Dialogue Rounds | 3,962 | "
|
||||
"Included Images | 1,003 | Evaluation Data | QA Pairs with Annotated Clues | 1,711 | "
|
||||
"Included Images | 487")
|
||||
|
||||
records = []
|
||||
for qid, section, kind, locator, idxs in SPEC:
|
||||
if idxs is None:
|
||||
quote = table2
|
||||
src = os.path.join(BASE, "paper.txt")
|
||||
src_sha = sha256_bytes(open(src, "rb").read())
|
||||
else:
|
||||
quote = "\n".join(lines[i].rstrip() for i in idxs if i < len(lines))
|
||||
src = text_path
|
||||
src_sha = file_sha
|
||||
records.append({
|
||||
"id": qid,
|
||||
"section": section,
|
||||
"kind": kind,
|
||||
"quote": quote,
|
||||
"locator": locator,
|
||||
"source_file": os.path.relpath(src, BASE),
|
||||
"source_sha256": src_sha,
|
||||
"quote_sha256": sha256_text(quote),
|
||||
})
|
||||
|
||||
out = {
|
||||
"paper": {
|
||||
"arxiv_id": "2601.03515",
|
||||
"version": "v1",
|
||||
"title": "Mem-Gallery: Benchmarking Multimodal Long-Term Conversational Memory for MLLM Agents",
|
||||
"authors": ["Yuanchen Bei", "Tianxin Wei", "Xuying Ning", "Yanjun Zhao",
|
||||
"Zhining Liu", "Xiao Lin", "Yada Zhu", "Hendrik Hamann",
|
||||
"Jingrui He", "Hanghang Tong"],
|
||||
"submitted": "2026-01-07",
|
||||
"comments": "34 pages, 18 figures",
|
||||
"url": "https://arxiv.org/abs/2601.03515",
|
||||
},
|
||||
"hash_method": ("quote_sha256 = sha256 of the quote string encoded utf-8; the quote is "
|
||||
"the exact .rstrip()'d line contents joined with '\\n', with no "
|
||||
"re-wrapping, no hyphen-dehyphenation and no whitespace normalisation"),
|
||||
"records": records,
|
||||
}
|
||||
with open(os.path.join(BASE, "quotes.json"), "w", encoding="utf-8") as fh:
|
||||
json.dump(out, fh, indent=2)
|
||||
fh.write("\n")
|
||||
|
||||
print("wrote %d quote records" % len(records))
|
||||
for r in records:
|
||||
print(" %s %-42s %s" % (r["id"], r["section"][:42], r["quote_sha256"][:16]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"checks": [
|
||||
{
|
||||
"claim": "MuRAG overall F1 gain over best textual (paper: 11.85%)",
|
||||
"recomputed": 11.8497,
|
||||
"paper_states": 11.85,
|
||||
"tol": 0.01,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"claim": "MuRAG FR gain over best textual (paper: 7.69%)",
|
||||
"recomputed": 14.9009,
|
||||
"paper_states": 7.69,
|
||||
"tol": 0.01,
|
||||
"ok": false
|
||||
},
|
||||
{
|
||||
"claim": "MuRAG VS gain over best textual (paper: 12.29%)",
|
||||
"recomputed": 16.1486,
|
||||
"paper_states": 12.29,
|
||||
"tol": 0.01,
|
||||
"ok": false
|
||||
},
|
||||
{
|
||||
"claim": "MuRAG TTL gain over best textual (paper: 29.06%)",
|
||||
"recomputed": 25.2988,
|
||||
"paper_states": 29.06,
|
||||
"tol": 0.01,
|
||||
"ok": false
|
||||
},
|
||||
{
|
||||
"claim": "MuRAG overall F1 gain over NGM (paper: 4.11%)",
|
||||
"recomputed": 4.11,
|
||||
"paper_states": 4.11,
|
||||
"tol": 0.01,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"claim": "MuRAG overall F1 gain over AUGUSTUS (paper: 5.39%)",
|
||||
"recomputed": 5.3858,
|
||||
"paper_states": 5.39,
|
||||
"tol": 0.01,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"claim": "Full(MM) vs Full(Text) overall F1 deficit (paper: 8.08%)",
|
||||
"recomputed": 7.4759,
|
||||
"paper_states": 8.08,
|
||||
"tol": 0.01,
|
||||
"ok": false
|
||||
},
|
||||
{
|
||||
"claim": "Full(MM) vs MuRAG overall F1 deficit (paper: 51.85%)",
|
||||
"recomputed": 51.8519,
|
||||
"paper_states": 51.85,
|
||||
"tol": 0.01,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"claim": "A-Mem refusal F1 deficit vs FIFO (paper: 7.22%)",
|
||||
"recomputed": 3.1625,
|
||||
"paper_states": 7.22,
|
||||
"tol": 0.01,
|
||||
"ok": false
|
||||
},
|
||||
{
|
||||
"claim": "MuRAG refusal F1 deficit vs FIFO (paper: 5.82%)",
|
||||
"recomputed": 1.7013,
|
||||
"paper_states": 5.82,
|
||||
"tol": 0.01,
|
||||
"ok": false
|
||||
}
|
||||
],
|
||||
"n": 10,
|
||||
"failed": 6,
|
||||
"best_textual_model": "A-Mem",
|
||||
"best_textual_overall_f1": 0.6228,
|
||||
"source_row": "Table 3 Overall F1 (paper.raw.txt line 846)"
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""diag_denominators.py -- find which baseline each paper percentage actually uses.
|
||||
|
||||
The paper states percentages; the table holds F1. If the stated percentage does
|
||||
not come out of the best-textual baseline, this brute-forces every model as the
|
||||
denominator so the archive reports the real one instead of guessing.
|
||||
"""
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
d = json.load(open(os.path.join(BASE, "claims.json"), encoding="utf-8"))
|
||||
|
||||
# Rebuild the numbers from the same source verify_claims.py uses.
|
||||
sys.path.insert(0, BASE)
|
||||
import verify_claims as vc # noqa: E402
|
||||
|
||||
|
||||
def pct(a, b):
|
||||
return (a - b) / b * 100.0
|
||||
|
||||
|
||||
TARGETS = [
|
||||
("FR", "MuRAG", 7.69), ("VS", "MuRAG", 12.29), ("TTL", "MuRAG", 29.06),
|
||||
]
|
||||
|
||||
print("== which denominator reproduces each stated task gain? ==")
|
||||
for task, num_model, target in TARGETS:
|
||||
row = vc.TASK_F1[task]
|
||||
num = row[num_model]
|
||||
hits = []
|
||||
for m, v in row.items():
|
||||
got = pct(num, v)
|
||||
if abs(got - target) <= 0.02:
|
||||
hits.append((m, round(got, 3)))
|
||||
print("%-4s stated=%6.2f%% numerator=%s(%.4f) matching denominators=%s" %
|
||||
(task, target, num_model, num, hits or "NONE"))
|
||||
|
||||
# also show every candidate, so 'none' is visible rather than assumed
|
||||
print(" all: " + ", ".join("%s=%+.2f%%" % (m, pct(num, v)) for m, v in row.items()))
|
||||
|
||||
print()
|
||||
print("== AR: is 7.22 / 5.82 relative or absolute? ==")
|
||||
fifo = vc.AR_F1["FIFO"]
|
||||
for m in ("A-Mem", "MuRAG"):
|
||||
rel = -pct(vc.AR_F1[m], fifo)
|
||||
ab = (fifo - vc.AR_F1[m]) * 100.0
|
||||
print(" %s vs FIFO: relative=%6.2f%% absolute_points=%6.2f F1=%.4f" %
|
||||
(m, rel, ab, vc.AR_F1[m]))
|
||||
|
||||
print()
|
||||
print("== Full(MM) 8.08: relative vs absolute? ==")
|
||||
fm, ft = vc.OVERALL_F1["Full (MM)"], vc.OVERALL_F1["Full (Text)"]
|
||||
print(" vs Full(Text): relative=%6.2f%% absolute_points=%6.2f" % (-pct(fm, ft), (ft - fm) * 100.0))
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""extract_figures.py -- pull the raster figures out of the frozen PDF and hash them.
|
||||
|
||||
Multimodal archive, so the images are part of the record, not an afterthought.
|
||||
Figures are extracted as PNG (lossless) and each gets a sha256 in figures.json.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
PDF = os.path.join(BASE, "raw", "paper.pdf")
|
||||
OUT = os.path.join(BASE, "figures")
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
if not os.path.exists(PDF):
|
||||
raise SystemExit("missing raw/paper.pdf -- run fetch_source.py first")
|
||||
prefix = os.path.join(BASE, "_figtmp")
|
||||
r = subprocess.run(["pdfimages", "-png", PDF, prefix],
|
||||
capture_output=True, text=True, timeout=600)
|
||||
if r.returncode != 0:
|
||||
raise SystemExit("pdfimages rc=%d: %s" % (r.returncode, r.stderr.strip()))
|
||||
|
||||
produced = sorted(f for f in os.listdir(BASE) if f.startswith("_figtmp"))
|
||||
records = []
|
||||
for name in produced:
|
||||
src = os.path.join(BASE, name)
|
||||
# pdfimages writes "<prefix>-NNN.ext"; strip the prefix, keep a sane name
|
||||
base = name[len("_figtmp"):].lstrip("-")
|
||||
dest = os.path.join(OUT, base)
|
||||
os.replace(src, dest)
|
||||
raw = open(dest, "rb").read()
|
||||
records.append({"file": "figures/" + base, "bytes": len(raw),
|
||||
"sha256": hashlib.sha256(raw).hexdigest()})
|
||||
|
||||
# which page each figure landed on, for citation
|
||||
listing = subprocess.run(["pdfimages", "-list", PDF],
|
||||
capture_output=True, text=True, timeout=300).stdout
|
||||
pages = {}
|
||||
for line in listing.split("\n")[2:]:
|
||||
parts = line.split()
|
||||
if len(parts) > 1 and parts[0].isdigit():
|
||||
pages.setdefault(int(parts[0]), 0)
|
||||
pages[int(parts[0])] += 1
|
||||
|
||||
payload = {
|
||||
"source_pdf_sha256": hashlib.sha256(open(PDF, "rb").read()).hexdigest(),
|
||||
"extractor": "pdfimages -png",
|
||||
"n_images_in_pdf": len(produced),
|
||||
"images_per_page": {str(k): v for k, v in sorted(pages.items())},
|
||||
"figures": records,
|
||||
}
|
||||
with open(os.path.join(BASE, "figures.json"), "w", encoding="utf-8") as fh:
|
||||
json.dump(payload, fh, indent=2)
|
||||
fh.write("\n")
|
||||
print("extracted %d images -> figures/" % len(records))
|
||||
for r in records[:6]:
|
||||
print(" %-28s %7d %s" % (os.path.basename(r["file"]), r["bytes"], r["sha256"][:16]))
|
||||
if len(records) > 6:
|
||||
print(" ... and %d more" % (len(records) - 6))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""extract_text.py -- pdftotext the frozen PDF, hash the text, pull all authors."""
|
||||
import hashlib
|
||||
import html
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
PDF = os.path.join(BASE, "raw", "paper.pdf")
|
||||
TXT = os.path.join(BASE, "paper.txt")
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(PDF):
|
||||
raise SystemExit("missing raw/paper.pdf -- run fetch_source.py first")
|
||||
r = subprocess.run(["pdftotext", "-layout", PDF, TXT],
|
||||
capture_output=True, text=True, timeout=300)
|
||||
if r.returncode != 0:
|
||||
raise SystemExit("pdftotext rc=%d: %s" % (r.returncode, r.stderr.strip()))
|
||||
raw = open(TXT, "rb").read()
|
||||
print("paper.txt bytes=%d sha256=%s" % (len(raw), hashlib.sha256(raw).hexdigest()))
|
||||
|
||||
t = raw.decode("utf-8", errors="replace")
|
||||
print("chars=%d lines=%d" % (len(t), t.count("\n")))
|
||||
|
||||
# authors from the arXiv abs page meta tags (the PDF byline is layout-scrambled)
|
||||
a = open(os.path.join(BASE, "raw", "abs.html"), encoding="utf-8", errors="replace").read()
|
||||
authors = re.findall(r'<meta name="citation_author" content="([^"]*)"', a)
|
||||
print("authors(%d): %s" % (len(authors), "; ".join(html.unescape(x) for x in authors)))
|
||||
|
||||
with open(os.path.join(BASE, "text_manifest.json"), "w", encoding="utf-8") as fh:
|
||||
import json
|
||||
json.dump({"file": "paper.txt", "sha256": hashlib.sha256(raw).hexdigest(),
|
||||
"chars": len(t), "lines": t.count("\n"),
|
||||
"authors": [html.unescape(x) for x in authors]}, fh, indent=2)
|
||||
fh.write("\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"arxiv_id": "2601.03515",
|
||||
"fetched_utc": "2026-10-21T18:54:00Z",
|
||||
"sources": [
|
||||
{
|
||||
"file": "raw/abs.html",
|
||||
"url": "https://arxiv.org/abs/2601.03515",
|
||||
"http_status": 200,
|
||||
"bytes": 43359,
|
||||
"sha256": "022aed61b37e9161a9f96d64be0f9e8fd9a3cc1131ef232c3bdb6f385b09f022"
|
||||
},
|
||||
{
|
||||
"file": "raw/api.xml",
|
||||
"url": "https://export.arxiv.org/api/query?id_list=2601.03515&max_results=1",
|
||||
"http_status": 200,
|
||||
"bytes": 3295,
|
||||
"sha256": "7e885f063961c9345c77dceb094c37842748f7b05591da343bf63c1ad1efb41f"
|
||||
},
|
||||
{
|
||||
"file": "raw/paper.pdf",
|
||||
"url": "https://arxiv.org/pdf/2601.03515v1",
|
||||
"http_status": 200,
|
||||
"bytes": 3098382,
|
||||
"sha256": "afb731ea1d9385e158565ef76f015762fd79ef300cbdb7659359ca77ff1884db"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""fetch_source.py -- pull arXiv:2601.03515v1 into raw/ and freeze it with hashes.
|
||||
|
||||
Nothing here trusts the network for a claim: every byte on disk gets a sha256, and
|
||||
manifest.json records what was fetched, from which URL, at what UTC time.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
RAW = os.path.join(BASE, "raw")
|
||||
os.makedirs(RAW, exist_ok=True)
|
||||
|
||||
AID = "2601.03515"
|
||||
URLS = {
|
||||
"abs.html": "https://arxiv.org/abs/%s" % AID,
|
||||
"api.xml": "https://export.arxiv.org/api/query?id_list=%s&max_results=1" % AID,
|
||||
"paper.pdf": "https://arxiv.org/pdf/%sv1" % AID,
|
||||
}
|
||||
|
||||
|
||||
def fetch(name, url, timeout=120):
|
||||
dest = os.path.join(RAW, name)
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "sphinx-archive/1.0 (research)"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
body = r.read()
|
||||
code = r.getcode()
|
||||
with open(dest, "wb") as fh:
|
||||
fh.write(body)
|
||||
sha = hashlib.sha256(body).hexdigest()
|
||||
return {"file": "raw/" + name, "url": url, "http_status": code,
|
||||
"bytes": len(body), "sha256": sha}
|
||||
|
||||
|
||||
def main():
|
||||
out = {"arxiv_id": AID, "fetched_utc": None, "sources": []}
|
||||
import datetime
|
||||
out["fetched_utc"] = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
for name, url in URLS.items():
|
||||
try:
|
||||
rec = fetch(name, url)
|
||||
except Exception as exc: # noqa: BLE001 - recorded, not hidden
|
||||
rec = {"file": "raw/" + name, "url": url, "error": repr(exc)}
|
||||
out["sources"].append(rec)
|
||||
print("FAIL %s: %r" % (name, exc), file=sys.stderr)
|
||||
continue
|
||||
out["sources"].append(rec)
|
||||
print("%-10s http=%s bytes=%d sha256=%s" % (name, rec["http_status"], rec["bytes"], rec["sha256"][:16]))
|
||||
with open(os.path.join(BASE, "fetch_manifest.json"), "w", encoding="utf-8") as fh:
|
||||
json.dump(out, fh, indent=2, sort_keys=False)
|
||||
fh.write("\n")
|
||||
ok = [s for s in out["sources"] if "sha256" in s]
|
||||
print("fetched %d/%d sources" % (len(ok), len(URLS)))
|
||||
return 0 if len(ok) == len(URLS) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 216 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 963 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 957 B |
|
After Width: | Height: | Size: 587 B |
|
After Width: | Height: | Size: 957 B |
|
After Width: | Height: | Size: 587 B |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 117 B |
|
After Width: | Height: | Size: 382 B |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 215 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 503 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 889 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 120 B |
|
After Width: | Height: | Size: 893 B |
|
After Width: | Height: | Size: 114 B |
|
After Width: | Height: | Size: 374 B |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 381 B |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 215 B |
|
After Width: | Height: | Size: 115 B |
|
After Width: | Height: | Size: 376 B |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 213 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 348 B |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 156 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 571 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 724 B |
|
After Width: | Height: | Size: 108 B |
|
After Width: | Height: | Size: 355 B |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 200 B |
|
After Width: | Height: | Size: 108 B |
|
After Width: | Height: | Size: 353 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 199 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 724 B |
|
After Width: | Height: | Size: 108 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 150 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 571 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 724 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 571 B |
|
After Width: | Height: | Size: 109 B |
|
After Width: | Height: | Size: 338 B |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 151 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 724 B |
|
After Width: | Height: | Size: 2.9 KiB |