Spaces:
Running on Zero
Running on Zero
Download src/hf_sync.py from niftym/ProxyPlayground: direct link, hf CLI and curl.
- Browser
- Download file 5.03 kB
-
https://huggingface.co/spaces/niftym/ProxyPlayground/resolve/main/src/hf_sync.py
- Command line
-
hf download hf://spaces/niftym/ProxyPlayground/src/hf_sync.py
-
curl -L -o hf_sync.py https://huggingface.co/spaces/niftym/ProxyPlayground/resolve/main/src/hf_sync.py
5.03 kB
| """ | |
| Sync a completed run's manifest to a shared Hugging Face Dataset repo. | |
| Why this exists: HF Spaces have ephemeral local storage -- anything written | |
| to disk during a Space session disappears on restart/sleep. Without this, | |
| every audit run (images included) would vanish once the Space goes idle. | |
| Instead, each run uploads its manifest -- WITH each row's image embedded | |
| via the `datasets` library's Image feature -- as its own Parquet file into | |
| a dataset repo. HF's dataset viewer renders those embedded images as | |
| thumbnails directly. Multiple Parquet files in one repo are treated as one | |
| combined dataset, so there's no read-modify-write step and no merge | |
| conflicts if two runs finish at the same time -- same append-only | |
| philosophy as ManifestWriter's chunking. | |
| The local manifest.parquet (written by ManifestWriter) stays lightweight | |
| and file-path-based -- this module builds a separate, image-embedded copy | |
| only at upload time, so local runs/iteration aren't slowed down by it. | |
| Auth: writing requires a write-scoped HF token. On a deployed Space this | |
| comes from an HF_TOKEN secret set by the Space owner (never exposed to | |
| visitors) -- a visitor's run triggers the write, but always under the | |
| Space owner's account. | |
| """ | |
| import os | |
| import uuid | |
| from datetime import datetime, timezone | |
| def _read_image_bytes(file_path): | |
| with open(file_path, "rb") as f: | |
| return {"bytes": f.read(), "path": None} | |
| def sync_manifest_to_hub(df, repo_id=None, token=None, include_images=True, run_id=None, | |
| on_progress=None, | |
| progress_prefix="Images generated… uploading to shared dataset"): | |
| """ | |
| Uploads `df` (one run's manifest) as a Parquet file in a shared | |
| HF dataset repo. Silently no-ops if not configured, so local/dev runs | |
| don't require HF credentials. | |
| repo_id: e.g. "niftymuch/bias-audit-manifests" (defaults to env HF_DATASET_REPO) | |
| token: write-scoped HF token (defaults to env HF_TOKEN) | |
| include_images: embed each row's image (read from its file_path) into | |
| the uploaded Parquet via datasets.Image, so the row is fully | |
| self-contained. Set False to upload metadata only. | |
| run_id: stable id for this run (e.g. local folder name). When set, the | |
| file is written to runs/{run_id}.parquet and later uploads with the | |
| same run_id overwrite that file (so Analyze updates Generate's save | |
| instead of creating a second copy). When omitted, a unique timestamp | |
| id is used (legacy append-only behavior). | |
| on_progress: optional callable(frac: float, desc: str) for UI progress | |
| during packing + upload (e.g. gr.Progress). | |
| progress_prefix: leading text for progress descriptions. | |
| Returns the dataset URL if the upload happened, else None. | |
| """ | |
| repo_id = repo_id or os.environ.get("HF_DATASET_REPO") | |
| token = token or os.environ.get("HF_TOKEN") | |
| if not repo_id or not token: | |
| return None | |
| def _report(frac, detail=None): | |
| if on_progress is not None: | |
| desc = progress_prefix if not detail else f"{progress_prefix} ({detail})" | |
| on_progress(frac, desc) | |
| from huggingface_hub import HfApi | |
| _report(0.0) | |
| api = HfApi(token=token) | |
| api.create_repo(repo_id, repo_type="dataset", exist_ok=True, private=True) | |
| if not run_id: | |
| run_id = f"{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}" | |
| # Keep path-safe (local folder names are already safe; sanitize just in case) | |
| run_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in str(run_id)) | |
| tmp_dir = os.environ.get("TMPDIR") or os.environ.get("TEMP") or "/tmp" | |
| tmp_path = os.path.join(tmp_dir, f"{run_id}.parquet") | |
| if include_images: | |
| from datasets import Dataset, Image | |
| upload_df = df.copy() | |
| paths = upload_df["file_path"].tolist() | |
| n = len(paths) | |
| images = [] | |
| # Packing images is usually the slow local part of the upload. | |
| for i, path in enumerate(paths): | |
| images.append(_read_image_bytes(path)) | |
| if i == 0 or (i + 1) % 25 == 0 or (i + 1) == n: | |
| _report( | |
| 0.05 + 0.70 * ((i + 1) / n if n else 1), | |
| f"packing {i + 1:,}/{n:,}", | |
| ) | |
| upload_df["image"] = images | |
| _report(0.80, "writing file") | |
| dataset = Dataset.from_pandas(upload_df, preserve_index=False) | |
| dataset = dataset.cast_column("image", Image()) | |
| dataset.to_parquet(tmp_path) | |
| else: | |
| _report(0.80, "writing file") | |
| df.to_parquet(tmp_path, index=False) | |
| try: | |
| _report(0.90, "sending to Hugging Face") | |
| api.upload_file( | |
| path_or_fileobj=tmp_path, | |
| path_in_repo=f"runs/{run_id}.parquet", | |
| repo_id=repo_id, | |
| repo_type="dataset", | |
| ) | |
| finally: | |
| if os.path.exists(tmp_path): | |
| os.remove(tmp_path) | |
| _report(1.0, "complete") | |
| return f"https://huggingface.co/datasets/{repo_id}" | |