AuditByProxy / src /hf_sync.py
niftym's picture
Update Visualize UI, charts, and TVD bootstrap CIs
ddfd0d0 verified
Raw History Blame Contribute Delete
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}"