| """On-demand figure fetch from ArxivCap on HuggingFace, with a local cache. |
| |
| Each figure is fetched at most once: the JPEG and a sidecar json holding the |
| paper title and full caption are written to the cache dir, keyed by |
| (arxiv_id, fig_index). Misses are grouped by ArxivCap month chunk so one |
| query's candidates cost one DuckDB range read per chunk. |
| """ |
| import json |
| import os |
| import re |
| from collections import defaultdict |
| from pathlib import Path |
|
|
| HF_SRC = "hf://datasets/MMInstruction/ArxivCap/data" |
|
|
|
|
| def yymm_of(arxiv_id): |
| s = str(arxiv_id) |
| if "/" in s: |
| return re.sub(r"\D", "", s.split("/", 1)[1])[:4] |
| return s[:4] |
|
|
|
|
| def safe_stem(arxiv_id, fig_index): |
| return re.sub(r"[^0-9a-zA-Z.]+", "_", str(arxiv_id)) + f"_f{fig_index}" |
|
|
|
|
| def _cached(cache, a, f): |
| stem = safe_stem(a, f) |
| img, meta = cache / f"{stem}.jpg", cache / f"{stem}.json" |
| if img.exists() and meta.exists(): |
| d = json.loads(meta.read_text()) |
| d["image_path"] = str(img) |
| return d |
| return None |
|
|
|
|
| def fetch_figures(needed, cache_dir="figure_cache", log=None): |
| """needed: iterable of (arxiv_id, fig_index 1-based). |
| Returns {(a, f): {"image_path", "title", "caption"}} for what was found.""" |
| cache = Path(cache_dir) |
| cache.mkdir(parents=True, exist_ok=True) |
| out, todo = {}, set() |
| for a, f in needed: |
| hit = _cached(cache, a, f) |
| if hit: |
| out[(a, f)] = hit |
| else: |
| todo.add((a, f)) |
| if not todo: |
| return out |
|
|
| import duckdb |
| con = duckdb.connect() |
| for s in ["SET http_retries = 6", "SET http_retry_wait_ms = 1500", |
| "SET threads = 2"]: |
| try: |
| con.execute(s) |
| except Exception: |
| pass |
| token = os.environ.get("HF_TOKEN", "") |
| if token: |
| con.execute(f"CREATE SECRET hf (TYPE HUGGINGFACE, TOKEN '{token}')") |
|
|
| by_chunk = defaultdict(set) |
| for a, f in todo: |
| by_chunk[yymm_of(a)].add((a, f)) |
| for yymm, pairs in sorted(by_chunk.items()): |
| if log: |
| log(f" fetching {len(pairs)} figure(s) from chunk {yymm}...") |
| ids = sorted({a for a, _ in pairs}) |
| idlist = ",".join("'" + i.replace("'", "''") + "'" for i in ids) |
| src = f"{HF_SRC}/arXiv_src_{yymm}_*.parquet" |
| try: |
| rows = con.execute( |
| f"SELECT arxiv_id, title, caption_images " |
| f"FROM read_parquet('{src}') " |
| f"WHERE arxiv_id IN ({idlist})").fetch_arrow_table().to_pylist() |
| except Exception as e: |
| if log: |
| log(f" chunk {yymm} failed: {str(e)[:80]}") |
| continue |
| for row in rows: |
| a = row["arxiv_id"] |
| caps = row["caption_images"] or [] |
| for (aa, f) in pairs: |
| if aa != a or f > len(caps): |
| continue |
| entry = caps[f - 1] or {} |
| cil = entry.get("cil_pairs") or [] |
| img = (cil[0] or {}).get("image") if cil else None |
| data = img.get("bytes") if isinstance(img, dict) else img |
| if not data: |
| continue |
| stem = safe_stem(a, f) |
| (cache / f"{stem}.jpg").write_bytes(data) |
| d = {"title": row.get("title") or "", |
| "caption": entry.get("caption") or ""} |
| (cache / f"{stem}.json").write_text(json.dumps(d)) |
| d["image_path"] = str(cache / f"{stem}.jpg") |
| out[(a, f)] = d |
| return out |
|
|