mirror of
https://github.com/ruvnet/RuView
synced 2026-08-07 20:01:43 +00:00
feat: HuggingFace model publishing pipeline + model card
- publish-huggingface.sh: retrieves HF token from GCloud Secrets, uploads models to ruvnet/wifi-densepose-pretrained - publish-huggingface.py: Python alternative with --dry-run support - docs/huggingface/MODEL_CARD.md: beginner-friendly model card with WiFi sensing explanation, quick start code, hardware BOM, and citation GCloud Secret: HUGGINGFACE_API_KEY in project cognitum-20260110 Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Publish WiFi-DensePose pre-trained models to HuggingFace Hub.
|
||||
|
||||
Retrieves the HuggingFace API token from Google Cloud Secrets,
|
||||
then uploads model files from dist/models/ to a HuggingFace repo.
|
||||
|
||||
Prerequisites:
|
||||
- gcloud CLI authenticated with access to cognitum-20260110
|
||||
- pip install huggingface_hub google-cloud-secret-manager
|
||||
|
||||
Usage:
|
||||
python scripts/publish-huggingface.py
|
||||
python scripts/publish-huggingface.py --repo ruvnet/wifi-densepose-pretrained --version v0.5.4
|
||||
python scripts/publish-huggingface.py --dry-run
|
||||
python scripts/publish-huggingface.py --token hf_xxxxx # skip GCloud lookup
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
EXPECTED_FILES = [
|
||||
"pretrained-encoder.onnx",
|
||||
"pretrained-heads.onnx",
|
||||
"pretrained.rvf",
|
||||
"room-profiles.json",
|
||||
"collection-witness.json",
|
||||
"config.json",
|
||||
"README.md",
|
||||
]
|
||||
|
||||
|
||||
def get_token_from_gcloud(
|
||||
project: str = "cognitum-20260110",
|
||||
secret: str = "HUGGINGFACE_API_KEY",
|
||||
) -> str:
|
||||
"""Retrieve HuggingFace token from Google Cloud Secret Manager."""
|
||||
# Try the gcloud CLI first (simpler, no extra deps)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gcloud", "secrets", "versions", "access", "latest",
|
||||
f"--secret={secret}",
|
||||
f"--project={project}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
except FileNotFoundError:
|
||||
pass # gcloud not installed, try Python SDK
|
||||
|
||||
# Fall back to the Python SDK
|
||||
try:
|
||||
from google.cloud import secretmanager
|
||||
|
||||
client = secretmanager.SecretManagerServiceClient()
|
||||
name = f"projects/{project}/secrets/{secret}/versions/latest"
|
||||
response = client.access_secret_version(request={"name": name})
|
||||
return response.payload.data.decode("utf-8").strip()
|
||||
except ImportError:
|
||||
print(
|
||||
"ERROR: Neither gcloud CLI nor google-cloud-secret-manager is available.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("Install: pip install google-cloud-secret-manager", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as exc:
|
||||
print(f"ERROR: Failed to retrieve secret: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def auto_version() -> str:
|
||||
"""Detect version from git describe."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "describe", "--tags", "--always"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return "dev"
|
||||
|
||||
|
||||
def validate_model_dir(model_dir: Path) -> list[Path]:
|
||||
"""List available files and warn about missing expected files."""
|
||||
found: list[Path] = []
|
||||
missing: list[str] = []
|
||||
|
||||
for fname in EXPECTED_FILES:
|
||||
path = model_dir / fname
|
||||
if path.is_file():
|
||||
size = path.stat().st_size
|
||||
print(f" [OK] {fname} ({size:,} bytes)")
|
||||
found.append(path)
|
||||
else:
|
||||
print(f" [MISSING] {fname}")
|
||||
missing.append(fname)
|
||||
|
||||
# Also pick up any extra files not in the expected list
|
||||
for path in sorted(model_dir.iterdir()):
|
||||
if path.is_file() and path.name not in EXPECTED_FILES:
|
||||
size = path.stat().st_size
|
||||
print(f" [EXTRA] {path.name} ({size:,} bytes)")
|
||||
found.append(path)
|
||||
|
||||
if missing:
|
||||
print(f"\nWARNING: {len(missing)} expected file(s) missing.")
|
||||
print("Upload will proceed with available files.\n")
|
||||
|
||||
return found
|
||||
|
||||
|
||||
def publish(
|
||||
repo_id: str,
|
||||
model_dir: Path,
|
||||
version: str,
|
||||
token: str,
|
||||
dry_run: bool = False,
|
||||
) -> None:
|
||||
"""Upload model files to HuggingFace Hub."""
|
||||
try:
|
||||
from huggingface_hub import HfApi, login
|
||||
except ImportError:
|
||||
print("Installing huggingface_hub...")
|
||||
subprocess.check_call(
|
||||
[sys.executable, "-m", "pip", "install", "--quiet", "huggingface_hub"]
|
||||
)
|
||||
from huggingface_hub import HfApi, login
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Repo: https://huggingface.co/{repo_id}")
|
||||
print(f"Version: {version}")
|
||||
print(f"Model dir: {model_dir}")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
print("Validating model files...")
|
||||
files = validate_model_dir(model_dir)
|
||||
|
||||
if not files:
|
||||
print("ERROR: No files to upload.")
|
||||
sys.exit(1)
|
||||
|
||||
if dry_run:
|
||||
print(f"\n[DRY RUN] Would upload {len(files)} file(s) to {repo_id}")
|
||||
for f in files:
|
||||
print(f" - {f.name}")
|
||||
print(f"[DRY RUN] Version tag: {version}")
|
||||
return
|
||||
|
||||
print("Authenticating with HuggingFace...")
|
||||
login(token=token, add_to_git_credential=False)
|
||||
api = HfApi()
|
||||
|
||||
print("Creating repo (if needed)...")
|
||||
api.create_repo(
|
||||
repo_id=repo_id,
|
||||
repo_type="model",
|
||||
exist_ok=True,
|
||||
private=False,
|
||||
)
|
||||
|
||||
print("Uploading files...")
|
||||
commit_info = api.upload_folder(
|
||||
folder_path=str(model_dir),
|
||||
repo_id=repo_id,
|
||||
repo_type="model",
|
||||
commit_message=f"Upload WiFi-DensePose pretrained models ({version})",
|
||||
)
|
||||
|
||||
# Tag
|
||||
try:
|
||||
api.create_tag(
|
||||
repo_id=repo_id,
|
||||
repo_type="model",
|
||||
tag=version,
|
||||
tag_message=f"WiFi-DensePose pretrained models {version}",
|
||||
)
|
||||
print(f"Tagged as: {version}")
|
||||
except Exception as exc:
|
||||
print(f"Tag '{version}' may already exist: {exc}")
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print("Published successfully!")
|
||||
print(f"URL: https://huggingface.co/{repo_id}")
|
||||
print(f"Version: {version}")
|
||||
print(f"Commit: {commit_info.commit_url}")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Publish WiFi-DensePose models to HuggingFace Hub",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repo",
|
||||
default="ruvnet/wifi-densepose-pretrained",
|
||||
help="HuggingFace repo ID (default: ruvnet/wifi-densepose-pretrained)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
default="",
|
||||
help="Version tag (default: auto from git describe)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model-dir",
|
||||
default="dist/models",
|
||||
help="Directory containing model files (default: dist/models)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--project",
|
||||
default="cognitum-20260110",
|
||||
help="GCloud project ID (default: cognitum-20260110)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--secret",
|
||||
default="HUGGINGFACE_API_KEY",
|
||||
help="GCloud secret name (default: HUGGINGFACE_API_KEY)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--token",
|
||||
default="",
|
||||
help="HuggingFace token (skip GCloud lookup if provided)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Preview upload without actually uploading",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
model_dir = Path(args.model_dir)
|
||||
version = args.version or auto_version()
|
||||
|
||||
if not model_dir.is_dir():
|
||||
print(f"ERROR: Model directory does not exist: {model_dir}")
|
||||
print("Create it and populate with model files first.")
|
||||
sys.exit(1)
|
||||
|
||||
# Get token
|
||||
if args.dry_run:
|
||||
token = "dry-run-no-token-needed"
|
||||
elif args.token:
|
||||
token = args.token
|
||||
else:
|
||||
print(f"Retrieving HuggingFace token from GCloud ({args.project})...")
|
||||
token = get_token_from_gcloud(project=args.project, secret=args.secret)
|
||||
print("Token retrieved.")
|
||||
|
||||
publish(
|
||||
repo_id=args.repo,
|
||||
model_dir=model_dir,
|
||||
version=version,
|
||||
token=token,
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user