publish_hf
Publish a marola-sea GGUF to a Hugging Face model repo (MIP-0025 §5.1, MIP-0033 §5.3).
Uploads one or more .gguf files plus a generated model card and a CHECKSUMS file (sha256 per
file, so a Modelfile can pin an exact blob rather than a mutable filename — the same discipline
MIP-0008's image gate applies to marola-local). Needs pip install huggingface_hub and a prior
huggingface-cli login (or HF_TOKEN in the environment) — neither is run here, both are the
human's own one-time setup per AGENTS.md's "never hardcode a key" rule.
This is the export chain's last step only. Get the .gguf file(s) first:
just finetune-train preset=<preset>(LoRA adapter, finetune/out//adapter/) - merge + convert with llama.cpp's
convert_hf_to_gguf.py(base) or useollama create+Modelfile.adapterfor local-only use without merging (see finetune/README.md) - this script, pointed at the resulting .gguf file(s)
Deliberately does not shell out to ollama push (MIP-0025 §5.1(2)'s optional second channel) —
that needs a separate ollama signin and a private key this script has no business touching.
1#!/usr/bin/env python3 2"""Publish a marola-sea GGUF to a Hugging Face model repo (MIP-0025 §5.1, MIP-0033 §5.3). 3 4Uploads one or more `.gguf` files plus a generated model card and a `CHECKSUMS` file (sha256 per 5file, so a Modelfile can pin an exact blob rather than a mutable filename — the same discipline 6MIP-0008's image gate applies to `marola-local`). Needs `pip install huggingface_hub` and a prior 7`huggingface-cli login` (or `HF_TOKEN` in the environment) — neither is run here, both are the 8human's own one-time setup per `AGENTS.md`'s "never hardcode a key" rule. 9 10This is the export chain's *last* step only. Get the `.gguf` file(s) first: 11 1. `just finetune-train preset=<preset>` (LoRA adapter, finetune/out/<preset>/adapter/) 12 2. merge + convert with llama.cpp's `convert_hf_to_gguf.py` (base) or use `ollama create` + 13 `Modelfile.adapter` for local-only use without merging (see finetune/README.md) 14 3. this script, pointed at the resulting .gguf file(s) 15 16Deliberately does not shell out to `ollama push` (MIP-0025 §5.1(2)'s optional second channel) — 17that needs a separate `ollama signin` and a private key this script has no business touching. 18""" 19 20from __future__ import annotations 21 22import argparse 23import hashlib 24import sys 25from pathlib import Path 26 27 28def sha256_of(path: Path) -> str: 29 h = hashlib.sha256() 30 with path.open("rb") as f: 31 for chunk in iter(lambda: f.read(1 << 20), b""): 32 h.update(chunk) 33 return h.hexdigest() 34 35 36def checksums_file(gguf_paths: list[Path]) -> str: 37 """`sha256sum`-compatible output — one line per file, sorted by name for a stable diff.""" 38 lines = [f"{sha256_of(p)} {p.name}" for p in sorted(gguf_paths, key=lambda p: p.name)] 39 return "\n".join(lines) + "\n" 40 41 42def model_card( 43 repo_id: str, 44 base_model: str, 45 base_license: str, 46 gguf_paths: list[Path], 47 eval_note: str, 48) -> str: 49 """Check by hand against MIP-0025 §5.1(2)'s required sections before a real publish.""" 50 files = "\n".join(f"- `{p.name}` ({p.stat().st_size / 1e6:.0f} MB)" for p in gguf_paths) 51 return f"""--- 52base_model: {base_model} 53license: {base_license} 54tags: 55- gguf 56- marola 57- ocean 58pipeline_tag: text-generation 59--- 60 61# {repo_id.split("/")[-1]} 62 63A GGUF release of a marola-sea checkpoint — a small model tuned on marola's own 64question/answer shape (open-water swim conditions, safety, sea life), served locally through 65[Ollama](https://ollama.com) alongside marola's own RAG corpus and deterministic scoring 66(`Recommender`/`Swimability`, never the model itself). 67 68**Honest framing (MIP-0025 §6, §8):** tuning changes tone and format reliability, not factual 69grounding. This model does not replace marola's RAG corpus (`knowledge/`, cited answers only) or 70its `Reviewer` pass, and it is **not a standalone safety authority** — treat any first-aid or 71hazard answer as a starting point, not a substitute for a lifeguard or emergency services (marola's 72own answers carry this caveat automatically via the MIP-0022 safety footer; this raw checkpoint, 73used outside marola, does not). 74 75## Files 76 77{files} 78 79`CHECKSUMS` (sha256) ships alongside these files — pin a specific hash in your own `Modelfile` 80rather than a bare filename, so re-quantizing upstream can't silently change what you run. 81 82## Use with Ollama 83 84```bash 85ollama run hf.co/{repo_id} 86# or a specific quant tag, e.g.: 87ollama run hf.co/{repo_id}:Q4_K_M 88``` 89 90## Base model and training 91 92Fine-tuned from [{base_model}](https://huggingface.co/{base_model}) via LoRA (`finetune/train_lora.py` 93in [h0ffmann/marola](https://github.com/h0ffmann/marola)) on a small, hand-built dataset derived 94from marola's own DSPy-compiled demos, sea-lore entries, and knowledge-corpus Q&A 95(`finetune/build_dataset.py`) — a few dozen examples, enough to teach format and tone, not facts. 96 97{eval_note} 98 99## Licence 100 101Base model licence: {base_license}. See the base model's own repo for the full licence text. 102""" 103 104 105def main(argv: list[str] | None = None) -> int: 106 parser = argparse.ArgumentParser(description=__doc__) 107 parser.add_argument("--repo", required=True, help="e.g. yourname/marola-sea-tiny-GGUF") 108 parser.add_argument( 109 "--gguf", nargs="+", required=True, type=Path, help="one or more .gguf files" 110 ) 111 parser.add_argument( 112 "--base-model", required=True, help="e.g. HuggingFaceTB/SmolLM2-360M-Instruct" 113 ) 114 parser.add_argument( 115 "--base-license", 116 default="apache-2.0", 117 help="the BASE model's licence id (not a generic default — check it per MIP-0025 §5.1(3): " 118 "a Llama-derived model has a different name requirement entirely, see finetune/README.md)", 119 ) 120 parser.add_argument( 121 "--eval-note", 122 default="No `just benchmark` numbers recorded yet for this checkpoint — see " 123 "docs/benchmarks/ in the source repo before trusting this over a plain base model.", 124 ) 125 parser.add_argument("--private", action="store_true", help="create the repo private") 126 parser.add_argument( 127 "--dry-run", 128 action="store_true", 129 help="write CHECKSUMS + the model card locally, upload nothing", 130 ) 131 args = parser.parse_args(argv) 132 133 missing = [p for p in args.gguf if not p.is_file()] 134 if missing: 135 print(f"error: not found: {', '.join(str(p) for p in missing)}", file=sys.stderr) 136 return 1 137 138 checksums = checksums_file(args.gguf) 139 card = model_card(args.repo, args.base_model, args.base_license, args.gguf, args.eval_note) 140 141 if args.dry_run: 142 out_dir = args.gguf[0].parent 143 (out_dir / "CHECKSUMS").write_text(checksums) 144 (out_dir / "README.md").write_text(card) 145 print( 146 f"dry run: wrote {out_dir / 'CHECKSUMS'} and {out_dir / 'README.md'}, uploaded nothing" 147 ) 148 return 0 149 150 try: 151 from huggingface_hub import HfApi 152 except ImportError: 153 print("error: pip install huggingface_hub (see finetune/requirements.txt)", file=sys.stderr) 154 return 1 155 156 api = HfApi() 157 api.create_repo(args.repo, repo_type="model", private=args.private, exist_ok=True) 158 api.upload_file( 159 path_or_fileobj=checksums.encode("utf-8"), path_in_repo="CHECKSUMS", repo_id=args.repo 160 ) 161 api.upload_file( 162 path_or_fileobj=card.encode("utf-8"), path_in_repo="README.md", repo_id=args.repo 163 ) 164 for p in args.gguf: 165 api.upload_file(path_or_fileobj=str(p), path_in_repo=p.name, repo_id=args.repo) 166 print(f"uploaded {p.name}") 167 168 print(f"done: https://huggingface.co/{args.repo}") 169 print(f"try it: ollama run hf.co/{args.repo}") 170 return 0 171 172 173if __name__ == "__main__": 174 raise SystemExit(main())
37def checksums_file(gguf_paths: list[Path]) -> str: 38 """`sha256sum`-compatible output — one line per file, sorted by name for a stable diff.""" 39 lines = [f"{sha256_of(p)} {p.name}" for p in sorted(gguf_paths, key=lambda p: p.name)] 40 return "\n".join(lines) + "\n"
sha256sum-compatible output — one line per file, sorted by name for a stable diff.
43def model_card( 44 repo_id: str, 45 base_model: str, 46 base_license: str, 47 gguf_paths: list[Path], 48 eval_note: str, 49) -> str: 50 """Check by hand against MIP-0025 §5.1(2)'s required sections before a real publish.""" 51 files = "\n".join(f"- `{p.name}` ({p.stat().st_size / 1e6:.0f} MB)" for p in gguf_paths) 52 return f"""--- 53base_model: {base_model} 54license: {base_license} 55tags: 56- gguf 57- marola 58- ocean 59pipeline_tag: text-generation 60--- 61 62# {repo_id.split("/")[-1]} 63 64A GGUF release of a marola-sea checkpoint — a small model tuned on marola's own 65question/answer shape (open-water swim conditions, safety, sea life), served locally through 66[Ollama](https://ollama.com) alongside marola's own RAG corpus and deterministic scoring 67(`Recommender`/`Swimability`, never the model itself). 68 69**Honest framing (MIP-0025 §6, §8):** tuning changes tone and format reliability, not factual 70grounding. This model does not replace marola's RAG corpus (`knowledge/`, cited answers only) or 71its `Reviewer` pass, and it is **not a standalone safety authority** — treat any first-aid or 72hazard answer as a starting point, not a substitute for a lifeguard or emergency services (marola's 73own answers carry this caveat automatically via the MIP-0022 safety footer; this raw checkpoint, 74used outside marola, does not). 75 76## Files 77 78{files} 79 80`CHECKSUMS` (sha256) ships alongside these files — pin a specific hash in your own `Modelfile` 81rather than a bare filename, so re-quantizing upstream can't silently change what you run. 82 83## Use with Ollama 84 85```bash 86ollama run hf.co/{repo_id} 87# or a specific quant tag, e.g.: 88ollama run hf.co/{repo_id}:Q4_K_M 89``` 90 91## Base model and training 92 93Fine-tuned from [{base_model}](https://huggingface.co/{base_model}) via LoRA (`finetune/train_lora.py` 94in [h0ffmann/marola](https://github.com/h0ffmann/marola)) on a small, hand-built dataset derived 95from marola's own DSPy-compiled demos, sea-lore entries, and knowledge-corpus Q&A 96(`finetune/build_dataset.py`) — a few dozen examples, enough to teach format and tone, not facts. 97 98{eval_note} 99 100## Licence 101 102Base model licence: {base_license}. See the base model's own repo for the full licence text. 103"""
Check by hand against MIP-0025 §5.1(2)'s required sections before a real publish.
106def main(argv: list[str] | None = None) -> int: 107 parser = argparse.ArgumentParser(description=__doc__) 108 parser.add_argument("--repo", required=True, help="e.g. yourname/marola-sea-tiny-GGUF") 109 parser.add_argument( 110 "--gguf", nargs="+", required=True, type=Path, help="one or more .gguf files" 111 ) 112 parser.add_argument( 113 "--base-model", required=True, help="e.g. HuggingFaceTB/SmolLM2-360M-Instruct" 114 ) 115 parser.add_argument( 116 "--base-license", 117 default="apache-2.0", 118 help="the BASE model's licence id (not a generic default — check it per MIP-0025 §5.1(3): " 119 "a Llama-derived model has a different name requirement entirely, see finetune/README.md)", 120 ) 121 parser.add_argument( 122 "--eval-note", 123 default="No `just benchmark` numbers recorded yet for this checkpoint — see " 124 "docs/benchmarks/ in the source repo before trusting this over a plain base model.", 125 ) 126 parser.add_argument("--private", action="store_true", help="create the repo private") 127 parser.add_argument( 128 "--dry-run", 129 action="store_true", 130 help="write CHECKSUMS + the model card locally, upload nothing", 131 ) 132 args = parser.parse_args(argv) 133 134 missing = [p for p in args.gguf if not p.is_file()] 135 if missing: 136 print(f"error: not found: {', '.join(str(p) for p in missing)}", file=sys.stderr) 137 return 1 138 139 checksums = checksums_file(args.gguf) 140 card = model_card(args.repo, args.base_model, args.base_license, args.gguf, args.eval_note) 141 142 if args.dry_run: 143 out_dir = args.gguf[0].parent 144 (out_dir / "CHECKSUMS").write_text(checksums) 145 (out_dir / "README.md").write_text(card) 146 print( 147 f"dry run: wrote {out_dir / 'CHECKSUMS'} and {out_dir / 'README.md'}, uploaded nothing" 148 ) 149 return 0 150 151 try: 152 from huggingface_hub import HfApi 153 except ImportError: 154 print("error: pip install huggingface_hub (see finetune/requirements.txt)", file=sys.stderr) 155 return 1 156 157 api = HfApi() 158 api.create_repo(args.repo, repo_type="model", private=args.private, exist_ok=True) 159 api.upload_file( 160 path_or_fileobj=checksums.encode("utf-8"), path_in_repo="CHECKSUMS", repo_id=args.repo 161 ) 162 api.upload_file( 163 path_or_fileobj=card.encode("utf-8"), path_in_repo="README.md", repo_id=args.repo 164 ) 165 for p in args.gguf: 166 api.upload_file(path_or_fileobj=str(p), path_in_repo=p.name, repo_id=args.repo) 167 print(f"uploaded {p.name}") 168 169 print(f"done: https://huggingface.co/{args.repo}") 170 print(f"try it: ollama run hf.co/{args.repo}") 171 return 0