| |
| """Dry-run by default uploader for the FLUX.1-dev ConvRot Hugging Face repo.""" |
| from __future__ import annotations |
|
|
| import argparse |
| import hashlib |
| import json |
| import os |
| import subprocess |
| import sys |
| from pathlib import Path |
| from typing import Any |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| MANIFEST_PATH = ROOT / "release_manifest.json" |
| VERIFY_SCRIPT = ROOT / "scripts/verify_release.py" |
| VERIFICATION_PATH = ROOT / "release_verification.json" |
| CHUNK_BYTES = 16 * 1024 * 1024 |
|
|
|
|
| def parse_args() -> argparse.Namespace: |
| manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8")) |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("--repo-id", default=manifest["suggested_repo_id"]) |
| parser.add_argument( |
| "--model-root", |
| type=Path, |
| default=Path(os.environ.get("COMFYUI_MODELS_ROOT", "/mnt/d/comfyui/models")), |
| ) |
| parser.add_argument("--revision", default="main") |
| parser.add_argument("--execute", action="store_true", help="Perform network writes; default is dry-run.") |
| parser.add_argument( |
| "--support-only", |
| action="store_true", |
| help="Upload documentation/evidence only; do not re-upload model artifacts.", |
| ) |
| parser.add_argument( |
| "--create-private", |
| action="store_true", |
| help="Create a new private model repository before upload.", |
| ) |
| parser.add_argument( |
| "--accept-flux-license", |
| action="store_true", |
| help="Confirm that the uploader reviewed and accepts the upstream FLUX.1-dev license.", |
| ) |
| return parser.parse_args() |
|
|
|
|
| def load_manifest() -> dict[str, Any]: |
| value = json.loads(MANIFEST_PATH.read_text(encoding="utf-8")) |
| if not isinstance(value, dict): |
| raise RuntimeError("release manifest must be a JSON object") |
| return value |
|
|
|
|
| def sha256_file(path: Path) -> str: |
| digest = hashlib.sha256() |
| with path.open("rb") as handle: |
| for chunk in iter(lambda: handle.read(CHUNK_BYTES), b""): |
| digest.update(chunk) |
| return digest.hexdigest() |
|
|
|
|
| def existing_verification_is_current(manifest: dict[str, Any], model_root: Path) -> bool: |
| if not VERIFICATION_PATH.is_file(): |
| return False |
| verification = json.loads(VERIFICATION_PATH.read_text(encoding="utf-8")) |
| if verification.get("status") != "PASS" or not verification.get("model_hashes_verified"): |
| return False |
| if verification.get("artifact_total_bytes") != manifest.get("artifact_total_bytes"): |
| return False |
| support = {record["path"]: record for record in verification.get("support_files", [])} |
| for relative in manifest["required_repository_files"]: |
| record = support.get(relative) |
| path = ROOT / relative |
| if record is None or not path.is_file() or path.is_symlink(): |
| return False |
| if path.stat().st_size != record["bytes"] or sha256_file(path) != record["sha256"]: |
| return False |
| verified_artifacts = { |
| record["repo_path"]: record for record in verification.get("artifacts", []) |
| } |
| verification_mtime_ns = VERIFICATION_PATH.stat().st_mtime_ns |
| for artifact in manifest["artifacts"]: |
| record = verified_artifacts.get(artifact["repo_path"]) |
| source = model_root / artifact["source_relative_path"] |
| if record is None or not record.get("hash_verified") or not source.is_file(): |
| return False |
| if record.get("sha256") != artifact["sha256"]: |
| return False |
| if source.stat().st_size != artifact["bytes"] or source.stat().st_mtime_ns > verification_mtime_ns: |
| return False |
| return True |
|
|
|
|
| def main() -> int: |
| args = parse_args() |
| manifest = load_manifest() |
| model_root = args.model_root.expanduser().resolve(strict=True) |
| plan = { |
| "mode": "execute" if args.execute else "dry-run", |
| "repo_id": args.repo_id, |
| "revision": args.revision, |
| "create_private": args.create_private, |
| "support_only": args.support_only, |
| "support_root": str(ROOT), |
| "artifact_total_bytes": manifest["artifact_total_bytes"], |
| "artifacts": [ |
| { |
| "source": str(model_root / artifact["source_relative_path"]), |
| "repo_path": artifact["repo_path"], |
| "bytes": artifact["bytes"], |
| "sha256": artifact["sha256"], |
| } |
| for artifact in manifest["artifacts"] |
| ], |
| } |
| print(json.dumps(plan, indent=2)) |
| if not args.execute: |
| print("DRY RUN: no repository was created and no files were uploaded.") |
| return 0 |
| if not args.accept_flux_license: |
| raise SystemExit("Refusing upload: pass --accept-flux-license after reviewing LICENSE_NOTICE.md") |
|
|
| if existing_verification_is_current(manifest, model_root): |
| print("Reusing current full-hash release_verification.json") |
| else: |
| subprocess.run( |
| [sys.executable, str(VERIFY_SCRIPT), "--model-root", str(model_root), "--models"], |
| check=True, |
| ) |
|
|
| from huggingface_hub import HfApi |
|
|
| api = HfApi() |
| if args.create_private: |
| api.create_repo(repo_id=args.repo_id, repo_type="model", private=True, exist_ok=False) |
|
|
| support_paths = [ |
| ".gitattributes", |
| "LICENSE_NOTICE.md", |
| "README.md", |
| "assets/*", |
| "manifests/*", |
| "release_manifest.json", |
| "release_verification.json", |
| "scripts/*", |
| "workflows/*", |
| ] |
| api.upload_folder( |
| repo_id=args.repo_id, |
| repo_type="model", |
| folder_path=str(ROOT), |
| path_in_repo=".", |
| revision=args.revision, |
| allow_patterns=support_paths, |
| ignore_patterns=["**/__pycache__/**", "**/*.pyc"], |
| commit_message="Add model card, workflows, and verified release evidence", |
| ) |
| if args.support_only: |
| print(f"Support-file upload complete: https://huggingface.co/{args.repo_id}") |
| return 0 |
| for artifact in manifest["artifacts"]: |
| source = model_root / artifact["source_relative_path"] |
| api.upload_file( |
| repo_id=args.repo_id, |
| repo_type="model", |
| path_or_fileobj=str(source), |
| path_in_repo=artifact["repo_path"], |
| revision=args.revision, |
| commit_message=f"Add {artifact['repo_path']}", |
| ) |
| print(f"Upload complete: https://huggingface.co/{args.repo_id}") |
| print("If publication is intended, configure gating and review visibility in Hugging Face settings before making the repository public.") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|