fsi-anomaly / hf_backup.py
FerrellSyntheticIntelligence's picture
Upload hf_backup.py with huggingface_hub
70a0a60 verified
Raw
History Blame Contribute Delete
4.09 kB
"""Full-project backup to Hugging Face (continuity snapshot).
Pushes the entire workspace (code, data, notes, pipeline, checkpoints) to
FerrellSyntheticIntelligence/fsi-anomaly so work can continue on another
machine. Skips .venv and python caches. Resume-safe: a local manifest
(logs/hf_backup_manifest.json) records uploaded files by sha256, and files
already present on the Hub are skipped, so re-running after an interruption
continues where it stopped. Progress is visible per commit.
Usage:
HF_TOKEN=hf_xxx .venv/bin/python hf_backup.py # everything
HF_TOKEN=hf_xxx .venv/bin/python hf_backup.py --stage ckpt # checkpoints only
"""
import argparse
import hashlib
import json
import os
import sys
from pathlib import Path
from huggingface_hub import CommitOperationAdd, HfApi
HERE = Path(__file__).resolve().parent
EXCLUDED_DIRS = {".venv", "__pycache__", ".pytest_cache"}
EXCLUDED_SUFFIXES = {".pyc"}
MANIFEST = HERE / "logs" / "hf_backup_manifest.json"
BATCH_FILES = 100
BATCH_BYTES = 800_000_000 # ~800MB per commit (safer on tablet network)
def sha256(path: Path) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def iter_files(stage: str):
for p in sorted(HERE.rglob("*")):
if not p.is_file():
continue
rel = p.relative_to(HERE).as_posix()
parts = rel.split("/")
if any(part in EXCLUDED_DIRS for part in parts):
continue
if p.suffix in EXCLUDED_SUFFIXES:
continue
if rel == "logs/hf_backup_manifest.json":
continue
if stage == "small" and parts[0] == "ckpt":
continue
if stage == "ckpt" and parts[0] != "ckpt":
continue
yield rel, p
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--repo", default="FerrellSyntheticIntelligence/fsi-anomaly")
ap.add_argument("--stage", choices=["all", "small", "ckpt"], default="all")
args = ap.parse_args()
token = os.environ.get("HF_TOKEN")
if not token:
sys.exit("HF_TOKEN env var required")
api = HfApi(token=token)
try:
api.repo_info(args.repo, repo_type="model")
print(f"repo exists: {args.repo}", flush=True)
except Exception:
api.create_repo(args.repo, private=True, repo_type="model")
print(f"created repo: {args.repo} (private)", flush=True)
manifest = {}
if MANIFEST.exists():
try:
manifest = json.loads(MANIFEST.read_text())
except json.JSONDecodeError:
manifest = {}
remote = set(api.list_repo_files(args.repo, repo_type="model"))
print(f"remote files already present: {len(remote)}", flush=True)
ops = []
batch_bytes = 0
n_uploaded = 0
n_skipped = 0
def flush(reason):
nonlocal ops, batch_bytes, n_uploaded
if not ops:
return
api.create_commit(
repo_id=args.repo,
operations=ops,
commit_message=f"backup {args.stage}: {len(ops)} files ({reason})",
repo_type="model",
)
for op in ops:
manifest[op.path_in_repo] = sha256(Path(op.path_or_fileobj))
MANIFEST.write_text(json.dumps(manifest, indent=0))
n_uploaded += len(ops)
print(f"committed {len(ops)} files -> {n_uploaded} total ({reason})", flush=True)
ops = []
batch_bytes = 0
for rel, p in iter_files(args.stage):
if rel in remote or manifest.get(rel) == sha256(p):
n_skipped += 1
continue
ops.append(CommitOperationAdd(path_in_repo=rel, path_or_fileobj=str(p)))
batch_bytes += p.stat().st_size
if len(ops) >= BATCH_FILES or batch_bytes >= BATCH_BYTES:
flush("batch")
flush("final")
print(f"DONE stage={args.stage}: uploaded={n_uploaded} skipped={n_skipped}", flush=True)
print(f"repo: https://huggingface.co/{args.repo}", flush=True)
if __name__ == "__main__":
main()