Spaces:
Sleeping
Sleeping
File size: 8,355 Bytes
4b81334 6be14c8 4b81334 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | """Hugging Face Dataset acts as the persistent 'cloud drive' for uploaded PDFs.
Layout inside the dataset repo:
pdfs/<filename>.pdf <- raw PDFs (source of truth)
vector_cache/manifest.json <- vector cache metadata
vector_cache/index/** <- persisted LanceDB files
Local disk on HF Spaces is ephemeral, so we try to restore the vector cache
from the dataset first and only rebuild from PDFs when the cache is missing or
stale.
"""
from __future__ import annotations
import json
import logging
from pathlib import Path
import shutil
from tempfile import TemporaryDirectory
from typing import List
from huggingface_hub import HfApi, hf_hub_download
from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
from config import DATASET_ID, HF_TOKEN
logger = logging.getLogger(__name__)
PDF_PREFIX = "pdfs/"
VECTOR_PREFIX = "vector_cache/"
VECTOR_INDEX_PREFIX = f"{VECTOR_PREFIX}index/"
VECTOR_MANIFEST_PATH = f"{VECTOR_PREFIX}manifest.json"
VECTOR_CACHE_SCHEMA_VERSION = 1
def is_configured() -> bool:
return bool(HF_TOKEN and DATASET_ID)
def _vector_manifest(pdf_filenames: List[str], embed_model: str) -> dict:
return {
"schema_version": VECTOR_CACHE_SCHEMA_VERSION,
"embed_model": embed_model,
"pdf_files": sorted(pdf_filenames),
}
def _api() -> HfApi:
if not HF_TOKEN or not DATASET_ID:
raise RuntimeError(
"HF_TOKEN and DATASET_ID must be set as environment variables / Space secrets."
)
return HfApi(token=HF_TOKEN)
def _ensure_dataset_exists(api: HfApi) -> None:
try:
api.repo_info(repo_id=DATASET_ID, repo_type="dataset")
except RepositoryNotFoundError:
try:
api.create_repo(
repo_id=DATASET_ID,
repo_type="dataset",
private=True,
exist_ok=True,
)
logger.info("Created dataset repo %s", DATASET_ID)
except Exception as exc: # noqa: BLE001
raise RuntimeError(
f"Dataset {DATASET_ID!r} does not exist and could not be created automatically. "
"Create it on Hugging Face or update DATASET_ID / HF_TOKEN permissions."
) from exc
def list_remote_pdfs() -> List[str]:
"""Return PDF filenames (basename only) currently stored in the dataset."""
api = _api()
try:
files = api.list_repo_files(repo_id=DATASET_ID, repo_type="dataset")
except RepositoryNotFoundError:
return []
return [
f[len(PDF_PREFIX):]
for f in files
if f.startswith(PDF_PREFIX) and f.lower().endswith(".pdf")
]
def download_vector_store(
target_dir: Path,
expected_pdf_filenames: List[str],
embed_model: str,
) -> bool:
"""Restore the persisted LanceDB folder when its manifest matches the dataset PDFs."""
if not is_configured():
return False
api = _api()
try:
manifest_path = hf_hub_download(
repo_id=DATASET_ID,
repo_type="dataset",
filename=VECTOR_MANIFEST_PATH,
token=HF_TOKEN,
)
except (EntryNotFoundError, RepositoryNotFoundError):
logger.info("No vector cache manifest in dataset yet")
return False
except Exception as exc: # noqa: BLE001
logger.warning("Could not download vector cache manifest: %s", exc)
return False
try:
manifest = json.loads(Path(manifest_path).read_text("utf-8"))
except Exception as exc: # noqa: BLE001
logger.warning("Could not parse vector cache manifest: %s", exc)
return False
expected_manifest = _vector_manifest(expected_pdf_filenames, embed_model)
if manifest != expected_manifest:
logger.info("Vector cache manifest is stale; rebuilding from PDFs")
return False
try:
files = api.list_repo_files(repo_id=DATASET_ID, repo_type="dataset")
except RepositoryNotFoundError:
return False
vector_files = [f for f in files if f.startswith(VECTOR_INDEX_PREFIX)]
if not vector_files:
logger.info("Vector cache manifest exists but no cache files were found")
return False
shutil.rmtree(target_dir, ignore_errors=True)
target_dir.mkdir(parents=True, exist_ok=True)
for repo_path in vector_files:
relative_path = repo_path[len(VECTOR_INDEX_PREFIX):]
if not relative_path:
continue
downloaded = hf_hub_download(
repo_id=DATASET_ID,
repo_type="dataset",
filename=repo_path,
token=HF_TOKEN,
)
destination = target_dir / relative_path
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(downloaded, destination)
logger.info("Vector cache restored from dataset (%d files)", len(vector_files))
return True
def delete_vector_store() -> bool:
"""Remove the persisted vector cache folder from the dataset."""
if not is_configured():
return False
api = _api()
try:
api.delete_folder(
path_in_repo=VECTOR_PREFIX.rstrip("/"),
repo_id=DATASET_ID,
repo_type="dataset",
commit_message="Delete vector cache",
)
logger.info("Vector cache deleted from HF Dataset")
return True
except (EntryNotFoundError, RepositoryNotFoundError):
return False
def upload_vector_store(local_dir: Path, pdf_filenames: List[str], embed_model: str) -> bool:
"""Persist the local LanceDB folder into the dataset along with a manifest."""
if not is_configured():
return False
api = _api()
_ensure_dataset_exists(api)
local_files = [p for p in local_dir.rglob("*") if p.is_file()]
if not pdf_filenames or not local_files:
delete_vector_store()
return bool(not pdf_filenames)
with TemporaryDirectory(prefix="iam-earth-vector-cache-") as tmp_dir:
stage_dir = Path(tmp_dir)
index_dir = stage_dir / "index"
shutil.copytree(local_dir, index_dir, dirs_exist_ok=True)
(stage_dir / "manifest.json").write_text(
json.dumps(_vector_manifest(pdf_filenames, embed_model), indent=2, ensure_ascii=False),
"utf-8",
)
api.upload_folder(
repo_id=DATASET_ID,
repo_type="dataset",
folder_path=stage_dir,
path_in_repo=VECTOR_PREFIX.rstrip("/"),
commit_message="Sync vector cache",
delete_patterns="**",
ignore_patterns=["**/.DS_Store"],
)
logger.info("Vector cache synced to HF Dataset")
return True
def download_all_pdfs(target_dir: Path) -> List[Path]:
"""Download every PDF in the dataset into target_dir. Returns local paths."""
target_dir.mkdir(parents=True, exist_ok=True)
paths: List[Path] = []
for name in list_remote_pdfs():
try:
local = hf_hub_download(
repo_id=DATASET_ID,
repo_type="dataset",
filename=f"{PDF_PREFIX}{name}",
local_dir=str(target_dir),
token=HF_TOKEN,
)
paths.append(Path(local))
except EntryNotFoundError:
continue
return paths
def upload_pdf(local_path: Path, filename: str) -> None:
"""Push a single PDF into the dataset under pdfs/<filename>."""
api = _api()
try:
_ensure_dataset_exists(api)
api.upload_file(
path_or_fileobj=str(local_path),
path_in_repo=f"{PDF_PREFIX}{filename}",
repo_id=DATASET_ID,
repo_type="dataset",
commit_message=f"Add {filename}",
)
except RuntimeError:
raise
except Exception as exc: # noqa: BLE001
raise RuntimeError(
f"Failed to upload {filename} to dataset {DATASET_ID!r}: {exc}"
) from exc
def delete_pdf(filename: str) -> bool:
"""Delete pdfs/<filename> from the dataset. Returns True if removed."""
api = _api()
try:
api.delete_file(
path_in_repo=f"{PDF_PREFIX}{filename}",
repo_id=DATASET_ID,
repo_type="dataset",
commit_message=f"Delete {filename}",
)
return True
except EntryNotFoundError:
return False
|