#!/usr/bin/env python3 """Prepare local GGUF files for manual Hugging Face publishing. This script performs no network calls and never runs `hf upload`. """ from __future__ import annotations import argparse import json import shutil from datetime import datetime, timezone from pathlib import Path from typing import Any DEFAULT_GGUF_FILE = Path("finetune/outputs/gguf/minicpm5-actor-q4_k_m.gguf") DEFAULT_OUTPUT_DIR = Path("finetune/publish_gguf") DEFAULT_MODEL_CARD = Path("finetune/model_cards/actor_gguf_README.md") DEFAULT_EVAL_FILE = Path("finetune/eval_outputs/minicpm5_actor_gguf_eval.jsonl") DEFAULT_REPO_ID = "build-small-hackathon/AI-Puppet-Theater-MiniCPM5-Actor-GGUF" COMMIT_MESSAGE = "Add Q4_K_M GGUF actor model" def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--gguf_file", type=Path, default=DEFAULT_GGUF_FILE) parser.add_argument("--output_dir", type=Path, default=DEFAULT_OUTPUT_DIR) parser.add_argument("--model_card", type=Path, default=DEFAULT_MODEL_CARD) parser.add_argument("--eval_file", type=Path, default=DEFAULT_EVAL_FILE) parser.add_argument("--repo_id", default=DEFAULT_REPO_ID) parser.add_argument("--dry_run", action="store_true", help="Print the plan without copying files.") parser.add_argument("--clean", action="store_true", help="Remove output_dir before preparing files.") return parser.parse_args(argv) def main(argv: list[str] | None = None) -> None: args = parse_args(argv) plan = build_plan(args) print_plan(plan, args.dry_run) print_manual_publish_commands(args.repo_id, args.output_dir) if args.dry_run: return prepare_files(args, plan) def build_plan(args: argparse.Namespace) -> dict[str, Any]: gguf_file = args.gguf_file model_card = args.model_card eval_file = args.eval_file if not gguf_file.exists(): raise SystemExit(f"GGUF file does not exist: {gguf_file}") if not gguf_file.is_file(): raise SystemExit(f"GGUF path is not a file: {gguf_file}") if not model_card.exists(): raise SystemExit(f"Model card does not exist: {model_card}") files = [ {"source": gguf_file, "dest": args.output_dir / gguf_file.name, "required": True}, {"source": model_card, "dest": args.output_dir / "README.md", "required": True}, ] eval_present = eval_file.exists() and eval_file.is_file() if eval_present: files.append( { "source": eval_file, "dest": args.output_dir / "eval" / eval_file.name, "required": False, } ) return { "repo_id": args.repo_id, "output_dir": args.output_dir, "files": files, "eval_present": eval_present, } def print_plan(plan: dict[str, Any], dry_run: bool) -> None: if dry_run: print("DRY RUN: no files will be copied and no network calls will be made.") else: print("Preparing local GGUF publish directory. No network calls will be made.") print(f"repo_id: {plan['repo_id']}") print(f"output_dir: {plan['output_dir']}") print("files:") for file_plan in plan["files"]: required = "required" if file_plan["required"] else "optional" print(f" {file_plan['source']} -> {file_plan['dest']} ({required})") def prepare_files(args: argparse.Namespace, plan: dict[str, Any]) -> None: output_dir = args.output_dir if args.clean and output_dir.exists(): shutil.rmtree(output_dir) output_dir.mkdir(parents=True, exist_ok=True) copied_files: list[dict[str, Any]] = [] for file_plan in plan["files"]: source = file_plan["source"] dest = file_plan["dest"] dest.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source, dest) copied_files.append( { "source": str(source), "path": str(dest.relative_to(output_dir)), "bytes": dest.stat().st_size, "required": file_plan["required"], } ) print(f"copied {source} -> {dest}") manifest = { "repo_id": args.repo_id, "created_at": datetime.now(timezone.utc).isoformat(), "output_dir": str(output_dir), "source_gguf_file": str(args.gguf_file), "model_card": str(args.model_card), "eval_file": str(args.eval_file) if plan["eval_present"] else None, "files": copied_files, "notes": [ "Prep-only local staging directory.", "No Hugging Face API calls or uploads were performed by this script.", "Q4_K_M GGUF is the current llama.cpp artifact for the AI Puppet Theater Actor model.", ], } manifest_path = output_dir / "publish_manifest.json" manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=True) + "\n", encoding="utf-8") print(f"wrote {manifest_path}") print(f"prepared GGUF publish directory: {output_dir}") def print_manual_publish_commands(repo_id: str, output_dir: Path) -> None: print("\nManual publish commands, not executed by this script:") print(f"hf repo create {repo_id} --type model --public") print( f'hf upload {repo_id} {output_dir} . --repo-type model ' f'--commit-message "{COMMIT_MESSAGE}"' ) if __name__ == "__main__": main()