Spaces:
Sleeping
Sleeping
Upload paper_figures.py
Browse files- metadata_search/paper_figures.py +63 -53
metadata_search/paper_figures.py
CHANGED
|
@@ -8,6 +8,7 @@ import os
|
|
| 8 |
import unicodedata
|
| 9 |
from pathlib import Path, PurePosixPath, PureWindowsPath
|
| 10 |
from urllib.parse import quote
|
|
|
|
| 11 |
|
| 12 |
from metadata_search.config import PAPER_FIGURES_DIR
|
| 13 |
|
|
@@ -16,7 +17,8 @@ logger = logging.getLogger(__name__)
|
|
| 16 |
STEM_PREFIX_MIN = 40
|
| 17 |
|
| 18 |
_index: dict[str, str] | None = None
|
| 19 |
-
_manifests: dict[str, dict] | None = None
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
HF_DATASET_REPO = os.getenv(
|
|
@@ -82,15 +84,33 @@ def _batch_folders(path: Path) -> list[str]:
|
|
| 82 |
if not isinstance(batch.get("papers"), list):
|
| 83 |
raise ValueError(f"Missing papers list in media batch manifest: {path}")
|
| 84 |
folders = []
|
|
|
|
| 85 |
for record in batch["papers"]:
|
| 86 |
folder = _media_folder(str(record.get("output_directory") or ""))
|
| 87 |
if folder is None:
|
| 88 |
raise ValueError("Invalid media output_directory in batch manifest")
|
| 89 |
-
if folder not in
|
| 90 |
folders.append(folder)
|
|
|
|
| 91 |
return folders
|
| 92 |
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
def resolve_figure_asset(folder: str, filename: str) -> Path | None:
|
| 95 |
"""Resolve one figure asset locally, then fetch only that asset if needed."""
|
| 96 |
safe_folder = _media_folder(folder)
|
|
@@ -149,7 +169,12 @@ def _register_stem(stem_index: dict[str, str], key: str, folder: str) -> None:
|
|
| 149 |
stem_index[stem] = folder
|
| 150 |
|
| 151 |
|
| 152 |
-
def _load_index() -> tuple[dict[str, str], dict[str, dict]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
global _index, _manifests
|
| 154 |
if _index is not None and _manifests is not None:
|
| 155 |
return _index, _manifests
|
|
@@ -157,6 +182,25 @@ def _load_index() -> tuple[dict[str, str], dict[str, dict]]:
|
|
| 157 |
stem_index: dict[str, str] = {}
|
| 158 |
manifests: dict[str, dict] = {}
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
# 1) Existing local behavior.
|
| 161 |
root = figures_dir()
|
| 162 |
if root is not None:
|
|
@@ -182,55 +226,6 @@ def _load_index() -> tuple[dict[str, str], dict[str, dict]]:
|
|
| 182 |
if source_pdf:
|
| 183 |
_register_stem(stem_index, PureWindowsPath(source_pdf).name, folder)
|
| 184 |
|
| 185 |
-
# 2) Cloud fallback. List repository paths once, then download only
|
| 186 |
-
# manifest.json files. Image assets remain on-demand.
|
| 187 |
-
if not manifests and HF_FIGURES_ON_DEMAND:
|
| 188 |
-
try:
|
| 189 |
-
suffix = "/manifest.json"
|
| 190 |
-
prefix = f"{HF_FIGURES_PREFIX}/"
|
| 191 |
-
batch_path = _hf_download(f"{prefix}batch_manifest.json")
|
| 192 |
-
if batch_path is not None:
|
| 193 |
-
manifest_files = [f"{prefix}{folder}{suffix}"
|
| 194 |
-
for folder in _batch_folders(batch_path)]
|
| 195 |
-
else:
|
| 196 |
-
# Support data without an overall manifest; only study manifests.
|
| 197 |
-
repo_files = _hf_api().list_repo_files(
|
| 198 |
-
repo_id=HF_DATASET_REPO, repo_type="dataset",
|
| 199 |
-
)
|
| 200 |
-
manifest_files = [path for path in repo_files
|
| 201 |
-
if path.startswith(prefix) and path.endswith(suffix)]
|
| 202 |
-
|
| 203 |
-
for repo_file in manifest_files:
|
| 204 |
-
relative = repo_file[len(prefix):]
|
| 205 |
-
folder = relative[:-len(suffix)]
|
| 206 |
-
# Keep the year component when resolving study manifests.
|
| 207 |
-
if _media_folder(folder) is None:
|
| 208 |
-
continue
|
| 209 |
-
|
| 210 |
-
manifest_path = _hf_download(repo_file)
|
| 211 |
-
if manifest_path is None:
|
| 212 |
-
continue
|
| 213 |
-
try:
|
| 214 |
-
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 215 |
-
except (OSError, json.JSONDecodeError) as exc:
|
| 216 |
-
logger.warning("Skipping cloud figure manifest %s: %s", repo_file, exc)
|
| 217 |
-
continue
|
| 218 |
-
|
| 219 |
-
manifests[folder] = manifest
|
| 220 |
-
_register_stem(stem_index, PurePosixPath(folder).name, folder)
|
| 221 |
-
|
| 222 |
-
source_pdf = (manifest.get("source_pdf") or "").strip()
|
| 223 |
-
if source_pdf:
|
| 224 |
-
_register_stem(stem_index, PureWindowsPath(source_pdf).name, folder)
|
| 225 |
-
|
| 226 |
-
logger.info(
|
| 227 |
-
"Indexed figures for %s papers from HF Dataset %s",
|
| 228 |
-
len(manifests),
|
| 229 |
-
HF_DATASET_REPO,
|
| 230 |
-
)
|
| 231 |
-
except Exception as exc:
|
| 232 |
-
logger.warning("Unable to build HF figure manifest index: %s", exc)
|
| 233 |
-
|
| 234 |
_index = stem_index
|
| 235 |
_manifests = manifests
|
| 236 |
return _index, _manifests
|
|
@@ -299,7 +294,22 @@ def figure_image_url(folder: str, filename: str) -> str:
|
|
| 299 |
|
| 300 |
def figures_payload_for_folder(folder: str) -> dict | None:
|
| 301 |
_, manifests = _load_index()
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
if not manifest:
|
| 304 |
return None
|
| 305 |
|
|
|
|
| 8 |
import unicodedata
|
| 9 |
from pathlib import Path, PurePosixPath, PureWindowsPath
|
| 10 |
from urllib.parse import quote
|
| 11 |
+
from threading import RLock
|
| 12 |
|
| 13 |
from metadata_search.config import PAPER_FIGURES_DIR
|
| 14 |
|
|
|
|
| 17 |
STEM_PREFIX_MIN = 40
|
| 18 |
|
| 19 |
_index: dict[str, str] | None = None
|
| 20 |
+
_manifests: dict[str, dict | None] | None = None
|
| 21 |
+
_index_lock = RLock()
|
| 22 |
|
| 23 |
|
| 24 |
HF_DATASET_REPO = os.getenv(
|
|
|
|
| 84 |
if not isinstance(batch.get("papers"), list):
|
| 85 |
raise ValueError(f"Missing papers list in media batch manifest: {path}")
|
| 86 |
folders = []
|
| 87 |
+
seen = set()
|
| 88 |
for record in batch["papers"]:
|
| 89 |
folder = _media_folder(str(record.get("output_directory") or ""))
|
| 90 |
if folder is None:
|
| 91 |
raise ValueError("Invalid media output_directory in batch manifest")
|
| 92 |
+
if folder not in seen:
|
| 93 |
folders.append(folder)
|
| 94 |
+
seen.add(folder)
|
| 95 |
return folders
|
| 96 |
|
| 97 |
|
| 98 |
+
|
| 99 |
+
def _batch_index(path: Path) -> tuple[dict[str, str], dict[str, dict | None]]:
|
| 100 |
+
"""Index folder and PDF aliases without reading individual study manifests."""
|
| 101 |
+
folders = dict.fromkeys(_batch_folders(path))
|
| 102 |
+
stem_index: dict[str, str] = {}
|
| 103 |
+
for folder in folders:
|
| 104 |
+
_register_stem(stem_index, PurePosixPath(folder).name, folder)
|
| 105 |
+
batch = json.loads(path.read_text(encoding="utf-8-sig"))
|
| 106 |
+
for record in batch["papers"]:
|
| 107 |
+
folder = _media_folder(str(record.get("output_directory") or ""))
|
| 108 |
+
source_pdf = str(record.get("source_pdf") or "").strip()
|
| 109 |
+
if source_pdf:
|
| 110 |
+
_register_stem(stem_index, PureWindowsPath(source_pdf).name, folder)
|
| 111 |
+
return stem_index, folders
|
| 112 |
+
|
| 113 |
+
|
| 114 |
def resolve_figure_asset(folder: str, filename: str) -> Path | None:
|
| 115 |
"""Resolve one figure asset locally, then fetch only that asset if needed."""
|
| 116 |
safe_folder = _media_folder(folder)
|
|
|
|
| 169 |
stem_index[stem] = folder
|
| 170 |
|
| 171 |
|
| 172 |
+
def _load_index() -> tuple[dict[str, str], dict[str, dict | None]]:
|
| 173 |
+
with _index_lock:
|
| 174 |
+
return _load_index_locked()
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def _load_index_locked() -> tuple[dict[str, str], dict[str, dict | None]]:
|
| 178 |
global _index, _manifests
|
| 179 |
if _index is not None and _manifests is not None:
|
| 180 |
return _index, _manifests
|
|
|
|
| 182 |
stem_index: dict[str, str] = {}
|
| 183 |
manifests: dict[str, dict] = {}
|
| 184 |
|
| 185 |
+
# The overall manifest is sufficient for health counts and paper lookup.
|
| 186 |
+
# None marks an indexed study whose detailed manifest is not loaded yet.
|
| 187 |
+
root = figures_dir()
|
| 188 |
+
batch_path = root / "batch_manifest.json" if root is not None else None
|
| 189 |
+
if batch_path is None or not batch_path.is_file():
|
| 190 |
+
batch_path = _hf_download(f"{HF_FIGURES_PREFIX}/batch_manifest.json")
|
| 191 |
+
if batch_path is not None:
|
| 192 |
+
try:
|
| 193 |
+
_index, _manifests = _batch_index(batch_path)
|
| 194 |
+
logger.info("Indexed media for %s papers from overall manifest", len(_manifests))
|
| 195 |
+
return _index, _manifests
|
| 196 |
+
except (OSError, ValueError, TypeError, AttributeError) as exc:
|
| 197 |
+
logger.warning("Unable to read overall media manifest %s: %s", batch_path, exc)
|
| 198 |
+
# Do not permanently cache a transient failure.
|
| 199 |
+
return {}, {}
|
| 200 |
+
if root is None:
|
| 201 |
+
logger.warning("Overall media manifest unavailable; will retry on next request")
|
| 202 |
+
return {}, {}
|
| 203 |
+
|
| 204 |
# 1) Existing local behavior.
|
| 205 |
root = figures_dir()
|
| 206 |
if root is not None:
|
|
|
|
| 226 |
if source_pdf:
|
| 227 |
_register_stem(stem_index, PureWindowsPath(source_pdf).name, folder)
|
| 228 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
_index = stem_index
|
| 230 |
_manifests = manifests
|
| 231 |
return _index, _manifests
|
|
|
|
| 294 |
|
| 295 |
def figures_payload_for_folder(folder: str) -> dict | None:
|
| 296 |
_, manifests = _load_index()
|
| 297 |
+
if folder not in manifests:
|
| 298 |
+
return None
|
| 299 |
+
with _index_lock:
|
| 300 |
+
manifest = manifests.get(folder)
|
| 301 |
+
if manifest is None:
|
| 302 |
+
manifest_path = resolve_figure_asset(folder, "manifest.json")
|
| 303 |
+
if manifest_path is None:
|
| 304 |
+
return None
|
| 305 |
+
try:
|
| 306 |
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
|
| 307 |
+
if not isinstance(manifest, dict):
|
| 308 |
+
raise ValueError("Study manifest must be a JSON object")
|
| 309 |
+
except (OSError, ValueError) as exc:
|
| 310 |
+
logger.warning("Unable to read study manifest %s: %s", manifest_path, exc)
|
| 311 |
+
return None
|
| 312 |
+
manifests[folder] = manifest
|
| 313 |
if not manifest:
|
| 314 |
return None
|
| 315 |
|