Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- metadata_search/app.py +2 -5
- metadata_search/config.py +1 -1
- metadata_search/paper_figures.py +59 -27
metadata_search/app.py
CHANGED
|
@@ -273,12 +273,9 @@ def paper_pdf_file(filename: str):
|
|
| 273 |
)
|
| 274 |
|
| 275 |
|
| 276 |
-
@app.get("/figures/<path:paper_key>/<
|
| 277 |
def paper_figure_file(paper_key: str, filename: str):
|
| 278 |
-
|
| 279 |
-
safe_name = Path(filename).name
|
| 280 |
-
|
| 281 |
-
target = resolve_figure_asset(safe_paper_key, safe_name)
|
| 282 |
if target is None:
|
| 283 |
return jsonify({"error": "Figure file not found."}), 404
|
| 284 |
|
|
|
|
| 273 |
)
|
| 274 |
|
| 275 |
|
| 276 |
+
@app.get("/figures/<path:paper_key>/<filename>")
|
| 277 |
def paper_figure_file(paper_key: str, filename: str):
|
| 278 |
+
target = resolve_figure_asset(paper_key, filename)
|
|
|
|
|
|
|
|
|
|
| 279 |
if target is None:
|
| 280 |
return jsonify({"error": "Figure file not found."}), 404
|
| 281 |
|
metadata_search/config.py
CHANGED
|
@@ -55,7 +55,7 @@ METADATA_SEARCH_PORT = int(
|
|
| 55 |
PAPER_FIGURES_DIR = Path(
|
| 56 |
os.getenv(
|
| 57 |
"PAPER_FIGURES_DIR",
|
| 58 |
-
str(PROJECT_ROOT / "
|
| 59 |
)
|
| 60 |
)
|
| 61 |
|
|
|
|
| 55 |
PAPER_FIGURES_DIR = Path(
|
| 56 |
os.getenv(
|
| 57 |
"PAPER_FIGURES_DIR",
|
| 58 |
+
str(PROJECT_ROOT / "Media"),
|
| 59 |
)
|
| 60 |
)
|
| 61 |
|
metadata_search/paper_figures.py
CHANGED
|
@@ -6,7 +6,7 @@ import json
|
|
| 6 |
import logging
|
| 7 |
import os
|
| 8 |
import unicodedata
|
| 9 |
-
from pathlib import Path
|
| 10 |
from urllib.parse import quote
|
| 11 |
|
| 12 |
from metadata_search.config import PAPER_FIGURES_DIR
|
|
@@ -23,7 +23,7 @@ HF_DATASET_REPO = os.getenv(
|
|
| 23 |
"WATERSCOPE_DATASET_REPO",
|
| 24 |
"EnvironmentalAI/WaterScopeAI-Data",
|
| 25 |
).strip()
|
| 26 |
-
HF_FIGURES_PREFIX = os.getenv("WATERSCOPE_FIGURES_PREFIX", "
|
| 27 |
HF_FIGURES_CACHE = Path(
|
| 28 |
os.getenv("WATERSCOPE_FIGURES_CACHE", "/tmp/waterscope-figure-cache")
|
| 29 |
).expanduser().resolve()
|
|
@@ -66,10 +66,37 @@ def _hf_download(repo_file: str) -> Path | None:
|
|
| 66 |
return path if path.is_file() else None
|
| 67 |
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
def resolve_figure_asset(folder: str, filename: str) -> Path | None:
|
| 70 |
"""Resolve one figure asset locally, then fetch only that asset if needed."""
|
| 71 |
-
safe_folder =
|
| 72 |
-
|
|
|
|
|
|
|
| 73 |
|
| 74 |
root = figures_dir()
|
| 75 |
if root is not None:
|
|
@@ -133,10 +160,13 @@ def _load_index() -> tuple[dict[str, str], dict[str, dict]]:
|
|
| 133 |
# 1) Existing local behavior.
|
| 134 |
root = figures_dir()
|
| 135 |
if root is not None:
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
| 140 |
if not manifest_path.is_file():
|
| 141 |
continue
|
| 142 |
try:
|
|
@@ -145,34 +175,36 @@ def _load_index() -> tuple[dict[str, str], dict[str, dict]]:
|
|
| 145 |
logger.warning("Skipping figure manifest %s: %s", manifest_path, exc)
|
| 146 |
continue
|
| 147 |
|
| 148 |
-
folder = entry.name
|
| 149 |
manifests[folder] = manifest
|
| 150 |
-
_register_stem(stem_index, folder, folder)
|
| 151 |
|
| 152 |
source_pdf = (manifest.get("source_pdf") or "").strip()
|
| 153 |
if source_pdf:
|
| 154 |
-
_register_stem(stem_index,
|
| 155 |
|
| 156 |
# 2) Cloud fallback. List repository paths once, then download only
|
| 157 |
# manifest.json files. Image assets remain on-demand.
|
| 158 |
if not manifests and HF_FIGURES_ON_DEMAND:
|
| 159 |
try:
|
| 160 |
-
repo_files = _hf_api().list_repo_files(
|
| 161 |
-
repo_id=HF_DATASET_REPO,
|
| 162 |
-
repo_type="dataset",
|
| 163 |
-
)
|
| 164 |
suffix = "/manifest.json"
|
| 165 |
prefix = f"{HF_FIGURES_PREFIX}/"
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
for repo_file in manifest_files:
|
| 172 |
relative = repo_file[len(prefix):]
|
| 173 |
folder = relative[:-len(suffix)]
|
| 174 |
-
#
|
| 175 |
-
if
|
| 176 |
continue
|
| 177 |
|
| 178 |
manifest_path = _hf_download(repo_file)
|
|
@@ -185,11 +217,11 @@ def _load_index() -> tuple[dict[str, str], dict[str, dict]]:
|
|
| 185 |
continue
|
| 186 |
|
| 187 |
manifests[folder] = manifest
|
| 188 |
-
_register_stem(stem_index, folder, folder)
|
| 189 |
|
| 190 |
source_pdf = (manifest.get("source_pdf") or "").strip()
|
| 191 |
if source_pdf:
|
| 192 |
-
_register_stem(stem_index,
|
| 193 |
|
| 194 |
logger.info(
|
| 195 |
"Indexed figures for %s papers from HF Dataset %s",
|
|
@@ -219,11 +251,11 @@ def _lookup_folder(*, txt_file: str = "", pdf_file: str = "", paper_key: str = "
|
|
| 219 |
return paper_key
|
| 220 |
target = normalize_stem(paper_key)
|
| 221 |
for folder in manifests:
|
| 222 |
-
if normalize_stem(folder) == target:
|
| 223 |
return folder
|
| 224 |
|
| 225 |
for candidate in (txt_file, pdf_file):
|
| 226 |
-
stem = normalize_stem(candidate)
|
| 227 |
if not stem:
|
| 228 |
continue
|
| 229 |
if stem in stem_index:
|
|
@@ -262,7 +294,7 @@ def resolve_figure_folder(
|
|
| 262 |
|
| 263 |
|
| 264 |
def figure_image_url(folder: str, filename: str) -> str:
|
| 265 |
-
return f"/figures/{quote(folder, safe='')}/{quote(filename, safe='')}"
|
| 266 |
|
| 267 |
|
| 268 |
def figures_payload_for_folder(folder: str) -> dict | None:
|
|
@@ -293,7 +325,7 @@ def figures_payload_for_folder(folder: str) -> dict | None:
|
|
| 293 |
source_pdf = (manifest.get("source_pdf") or "").strip()
|
| 294 |
return {
|
| 295 |
"paper_key": folder,
|
| 296 |
-
"source_pdf":
|
| 297 |
"item_count": len(items),
|
| 298 |
"items": items,
|
| 299 |
}
|
|
|
|
| 6 |
import logging
|
| 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
|
|
|
|
| 23 |
"WATERSCOPE_DATASET_REPO",
|
| 24 |
"EnvironmentalAI/WaterScopeAI-Data",
|
| 25 |
).strip()
|
| 26 |
+
HF_FIGURES_PREFIX = os.getenv("WATERSCOPE_FIGURES_PREFIX", "Media").strip() or "Media"
|
| 27 |
HF_FIGURES_CACHE = Path(
|
| 28 |
os.getenv("WATERSCOPE_FIGURES_CACHE", "/tmp/waterscope-figure-cache")
|
| 29 |
).expanduser().resolve()
|
|
|
|
| 66 |
return path if path.is_file() else None
|
| 67 |
|
| 68 |
|
| 69 |
+
def _media_folder(folder: str) -> str | None:
|
| 70 |
+
"""Preserve relative year/study paths without allowing path traversal."""
|
| 71 |
+
if not folder or "\\" in folder or ":" in folder:
|
| 72 |
+
return None
|
| 73 |
+
path = PurePosixPath(folder)
|
| 74 |
+
if path.is_absolute() or any(part in {"", ".", ".."} for part in folder.split("/")):
|
| 75 |
+
return None
|
| 76 |
+
return path.as_posix()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _batch_folders(path: Path) -> list[str]:
|
| 80 |
+
"""The overall manifest supplies study folders; yearly reports are not scanned."""
|
| 81 |
+
batch = json.loads(path.read_text(encoding="utf-8-sig"))
|
| 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 folders:
|
| 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)
|
| 97 |
+
if safe_folder is None or not filename or "/" in filename or "\\" in filename or filename in {".", ".."}:
|
| 98 |
+
return None
|
| 99 |
+
safe_name = filename
|
| 100 |
|
| 101 |
root = figures_dir()
|
| 102 |
if root is not None:
|
|
|
|
| 160 |
# 1) Existing local behavior.
|
| 161 |
root = figures_dir()
|
| 162 |
if root is not None:
|
| 163 |
+
batch_path = root / "batch_manifest.json"
|
| 164 |
+
folders = (_batch_folders(batch_path) if batch_path.is_file() else
|
| 165 |
+
[path.parent.relative_to(root).as_posix()
|
| 166 |
+
for path in sorted(root.rglob("manifest.json"))
|
| 167 |
+
if path.parent != root])
|
| 168 |
+
for folder in folders:
|
| 169 |
+
manifest_path = root / folder / "manifest.json"
|
| 170 |
if not manifest_path.is_file():
|
| 171 |
continue
|
| 172 |
try:
|
|
|
|
| 175 |
logger.warning("Skipping figure manifest %s: %s", manifest_path, exc)
|
| 176 |
continue
|
| 177 |
|
|
|
|
| 178 |
manifests[folder] = manifest
|
| 179 |
+
_register_stem(stem_index, PurePosixPath(folder).name, folder)
|
| 180 |
|
| 181 |
source_pdf = (manifest.get("source_pdf") or "").strip()
|
| 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)
|
|
|
|
| 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",
|
|
|
|
| 251 |
return paper_key
|
| 252 |
target = normalize_stem(paper_key)
|
| 253 |
for folder in manifests:
|
| 254 |
+
if normalize_stem(folder) == target or normalize_stem(PurePosixPath(folder).name) == target:
|
| 255 |
return folder
|
| 256 |
|
| 257 |
for candidate in (txt_file, pdf_file):
|
| 258 |
+
stem = normalize_stem(PureWindowsPath(candidate).name)
|
| 259 |
if not stem:
|
| 260 |
continue
|
| 261 |
if stem in stem_index:
|
|
|
|
| 294 |
|
| 295 |
|
| 296 |
def figure_image_url(folder: str, filename: str) -> str:
|
| 297 |
+
return f"/figures/{quote(folder, safe='/')}/{quote(filename, safe='')}"
|
| 298 |
|
| 299 |
|
| 300 |
def figures_payload_for_folder(folder: str) -> dict | None:
|
|
|
|
| 325 |
source_pdf = (manifest.get("source_pdf") or "").strip()
|
| 326 |
return {
|
| 327 |
"paper_key": folder,
|
| 328 |
+
"source_pdf": PureWindowsPath(source_pdf).name if source_pdf else folder,
|
| 329 |
"item_count": len(items),
|
| 330 |
"items": items,
|
| 331 |
}
|