neuralbroker commited on
Commit
ccde9ab
Β·
verified Β·
1 Parent(s): b56b94e

Add scripts/push_to_hub.py

Browse files
Files changed (1) hide show
  1. scripts/push_to_hub.py +587 -0
scripts/push_to_hub.py ADDED
@@ -0,0 +1,587 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Push a PEFT/LoRA adapter checkpoint to the Hugging Face Hub.
3
+
4
+ Usage examples
5
+ --------------
6
+ # Validate everything without pushing:
7
+ python scripts/push_to_hub.py --dry-run
8
+
9
+ # Push with defaults (reads HF_TOKEN from environment):
10
+ python scripts/push_to_hub.py
11
+
12
+ # Explicit options:
13
+ python scripts/push_to_hub.py \\
14
+ --checkpoint checkpoints/available-lora-0.5b-full/final \\
15
+ --repo-id neuralbroker/blitzkode-lora-0.5b \\
16
+ --commit-message "Add trained adapter v2.1"
17
+
18
+ # Private repo push with explicit token:
19
+ python scripts/push_to_hub.py --private --token hf_...
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import json
26
+ import os
27
+ import sys
28
+ import textwrap
29
+ from pathlib import Path
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Constants / defaults
33
+ # ---------------------------------------------------------------------------
34
+ REPO_ROOT = Path(__file__).resolve().parents[1]
35
+
36
+ DEFAULT_CHECKPOINT = REPO_ROOT / "checkpoints" / "available-lora-0.5b-full" / "final"
37
+ DEFAULT_REPO_ID = "neuralbroker/blitzkode-lora-0.5b"
38
+ DEFAULT_REPO_TYPE = "model"
39
+ DEFAULT_COMMIT_MSG = "Upload BlitzKode LoRA adapter"
40
+
41
+ # Files that must be present for a valid PEFT adapter
42
+ REQUIRED_FILES = ["adapter_config.json", "adapter_model.safetensors"]
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # CLI argument parsing
47
+ # ---------------------------------------------------------------------------
48
+ def parse_args() -> argparse.Namespace:
49
+ parser = argparse.ArgumentParser(
50
+ description=__doc__,
51
+ formatter_class=argparse.RawDescriptionHelpFormatter,
52
+ )
53
+ parser.add_argument(
54
+ "--checkpoint",
55
+ type=Path,
56
+ default=DEFAULT_CHECKPOINT,
57
+ metavar="PATH",
58
+ help=(
59
+ f"Path to the adapter checkpoint directory. "
60
+ f"(default: {DEFAULT_CHECKPOINT})"
61
+ ),
62
+ )
63
+ parser.add_argument(
64
+ "--repo-id",
65
+ default=DEFAULT_REPO_ID,
66
+ metavar="OWNER/REPO",
67
+ help=f"HuggingFace repo to push to. (default: {DEFAULT_REPO_ID})",
68
+ )
69
+ parser.add_argument(
70
+ "--repo-type",
71
+ default=DEFAULT_REPO_TYPE,
72
+ choices=("model", "dataset", "space"),
73
+ help=f"Repository type. (default: {DEFAULT_REPO_TYPE})",
74
+ )
75
+ parser.add_argument(
76
+ "--private",
77
+ action="store_true",
78
+ help="Create the repository as private. (default: public)",
79
+ )
80
+ parser.add_argument(
81
+ "--token",
82
+ default=None,
83
+ metavar="HF_TOKEN",
84
+ help=(
85
+ "HuggingFace API write token. "
86
+ "Falls back to the HF_TOKEN environment variable if not set."
87
+ ),
88
+ )
89
+ parser.add_argument(
90
+ "--create-repo",
91
+ action=argparse.BooleanOptionalAction,
92
+ default=True,
93
+ help=(
94
+ "Create the HuggingFace repo if it does not exist. "
95
+ "Use --no-create-repo to skip. (default: True)"
96
+ ),
97
+ )
98
+ parser.add_argument(
99
+ "--commit-message",
100
+ default=DEFAULT_COMMIT_MSG,
101
+ metavar="MSG",
102
+ help=f"Commit message for the Hub upload. (default: '{DEFAULT_COMMIT_MSG}')",
103
+ )
104
+ parser.add_argument(
105
+ "--dry-run",
106
+ action="store_true",
107
+ help=(
108
+ "Validate the checkpoint and configuration but do NOT push anything "
109
+ "to Hugging Face Hub. Useful for CI or pre-flight checks."
110
+ ),
111
+ )
112
+ return parser.parse_args()
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # Dependency check
117
+ # ---------------------------------------------------------------------------
118
+ def check_huggingface_hub() -> None:
119
+ """Abort with a helpful message if huggingface_hub is not installed."""
120
+ try:
121
+ import huggingface_hub # noqa: F401 # type: ignore[import]
122
+ except ImportError:
123
+ print(
124
+ "\n[ERROR] The `huggingface_hub` package is not installed.\n"
125
+ "Install it with one of the following commands:\n\n"
126
+ " pip install huggingface_hub\n"
127
+ " pip install -r requirements-training.txt\n",
128
+ file=sys.stderr,
129
+ )
130
+ sys.exit(1)
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Checkpoint validation
135
+ # ---------------------------------------------------------------------------
136
+ def validate_checkpoint(checkpoint: Path) -> dict:
137
+ """Ensure the checkpoint directory is valid.
138
+
139
+ Checks that the directory exists and contains every file listed in
140
+ REQUIRED_FILES. Returns the parsed ``adapter_config.json`` dict.
141
+ """
142
+ if not checkpoint.exists():
143
+ print(
144
+ f"\n[ERROR] Checkpoint directory not found: {checkpoint}\n"
145
+ "Run training first, e.g.:\n"
146
+ " python scripts/train_available.py\n",
147
+ file=sys.stderr,
148
+ )
149
+ sys.exit(1)
150
+
151
+ if not checkpoint.is_dir():
152
+ print(
153
+ f"\n[ERROR] Checkpoint path is not a directory: {checkpoint}\n",
154
+ file=sys.stderr,
155
+ )
156
+ sys.exit(1)
157
+
158
+ missing = [f for f in REQUIRED_FILES if not (checkpoint / f).exists()]
159
+ if missing:
160
+ print(
161
+ f"\n[ERROR] Missing required files in {checkpoint}:\n"
162
+ + "\n".join(f" - {f}" for f in missing)
163
+ + "\n\nIs this a valid PEFT adapter checkpoint?\n",
164
+ file=sys.stderr,
165
+ )
166
+ sys.exit(1)
167
+
168
+ config_path = checkpoint / "adapter_config.json"
169
+ try:
170
+ adapter_config: dict = json.loads(config_path.read_text(encoding="utf-8"))
171
+ except json.JSONDecodeError as exc:
172
+ print(
173
+ f"\n[ERROR] adapter_config.json is not valid JSON: {exc}\n",
174
+ file=sys.stderr,
175
+ )
176
+ sys.exit(1)
177
+ except OSError as exc:
178
+ print(
179
+ f"\n[ERROR] Could not read adapter_config.json: {exc}\n",
180
+ file=sys.stderr,
181
+ )
182
+ sys.exit(1)
183
+
184
+ return adapter_config
185
+
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # Token resolution
189
+ # ---------------------------------------------------------------------------
190
+ def resolve_token(args_token: str | None, *, dry_run: bool = False) -> str:
191
+ """Return the HF token, or abort with instructions if none is found."""
192
+ token = args_token or os.environ.get("HF_TOKEN", "")
193
+ if token:
194
+ return token
195
+
196
+ if dry_run:
197
+ # A token is not needed for dry runs, return a placeholder.
198
+ return "__dry_run_placeholder__"
199
+
200
+ print(
201
+ "\n[ERROR] No HuggingFace API token found.\n"
202
+ "Provide a write token using one of these methods:\n\n"
203
+ " 1. CLI flag:\n"
204
+ " python scripts/push_to_hub.py --token hf_YOUR_TOKEN\n\n"
205
+ " 2. Environment variable (recommended):\n"
206
+ " Windows CMD : set HF_TOKEN=hf_YOUR_TOKEN\n"
207
+ " PowerShell : $env:HF_TOKEN = 'hf_YOUR_TOKEN'\n"
208
+ " Linux/macOS : export HF_TOKEN=hf_YOUR_TOKEN\n\n"
209
+ " 3. HuggingFace CLI login (persists across sessions):\n"
210
+ " pip install huggingface_hub\n"
211
+ " huggingface-cli login\n\n"
212
+ "Generate a token at: https://huggingface.co/settings/tokens\n"
213
+ "Make sure the token has **write** access to the target repo.\n",
214
+ file=sys.stderr,
215
+ )
216
+ sys.exit(1)
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # Model card / README generation
221
+ # ---------------------------------------------------------------------------
222
+ def build_model_card(adapter_config: dict, repo_id: str) -> str:
223
+ """Generate the HuggingFace-compatible README.md content for the adapter repo."""
224
+ base_model = adapter_config.get(
225
+ "base_model_name_or_path", "Qwen/Qwen2.5-0.5B-Instruct"
226
+ )
227
+ lora_r = adapter_config.get("r", 16)
228
+ lora_alpha = adapter_config.get("lora_alpha", 32)
229
+ lora_dropout = adapter_config.get("lora_dropout", 0.05)
230
+ target_modules: list = adapter_config.get("target_modules", [])
231
+ modules_str = (
232
+ ", ".join(f"`{m}`" for m in target_modules)
233
+ if target_modules
234
+ else "`q_proj`, `k_proj`, `v_proj`, `o_proj`"
235
+ )
236
+
237
+ # YAML frontmatter -------------------------------------------------------
238
+ frontmatter = textwrap.dedent(f"""\
239
+ ---
240
+ language:
241
+ - en
242
+ license: mit
243
+ library_name: peft
244
+ tags:
245
+ - code-generation
246
+ - lora
247
+ - qwen2.5
248
+ - blitzkode
249
+ - coding-assistant
250
+ - fine-tuned
251
+ - peft
252
+ base_model: {base_model}
253
+ pipeline_tag: text-generation
254
+ ---
255
+ """)
256
+
257
+ # README body ------------------------------------------------------------
258
+ body = textwrap.dedent(f"""\
259
+ # BlitzKode LoRA Adapter (0.5B)
260
+
261
+ **BlitzKode** is a local AI coding assistant fine-tuned from
262
+ **[{base_model}](https://huggingface.co/{base_model})** using LoRA
263
+ (Low-Rank Adaptation). This repository contains the PEFT adapter β€” the
264
+ research-friendly version that can be hot-loaded on top of the base model.
265
+
266
+ > **Creator:** [Sajad (neuralbroker)](https://github.com/neuralbroker)
267
+ > **GitHub:** <https://github.com/neuralbroker/blitzkode>
268
+ > **Production GGUF:** [`neuralbroker/blitzkode`](https://huggingface.co/neuralbroker/blitzkode)
269
+
270
+ ---
271
+
272
+ ## Model Details
273
+
274
+ | Property | Value |
275
+ |---|---|
276
+ | **Adapter version** | 2.1 |
277
+ | **Base model** | `{base_model}` |
278
+ | **LoRA rank (r)** | {lora_r} |
279
+ | **LoRA alpha** | {lora_alpha} |
280
+ | **LoRA dropout** | {lora_dropout} |
281
+ | **Target modules** | {modules_str} |
282
+ | **Training steps** | 50 |
283
+ | **Final loss** | ~0.48 |
284
+ | **Library** | PEFT |
285
+ | **License** | MIT |
286
+
287
+ ---
288
+
289
+ ## Training Pipeline
290
+
291
+ This adapter was produced by a **4-stage fine-tuning pipeline** applied
292
+ to the Qwen2.5 family:
293
+
294
+ | Stage | Method | Purpose |
295
+ |---|---|---|
296
+ | 1 | SFT | Supervised fine-tuning on 71 curated algorithmic coding problems |
297
+ | 2 | Reward-SFT | Continued SFT with heuristic reward signals for code correctness and formatting |
298
+ | 3 | DPO | Direct Preference Optimization on handcrafted chosen/rejected pairs |
299
+ | 4 | LoRA SFT (this adapter) | Final LoRA fine-tune (r={lora_r}) on 99 samples; base model Qwen2.5-0.5B |
300
+
301
+ ### Training Dataset (199 total samples)
302
+
303
+ | Subset | Count | Source | License |
304
+ |---|---|---|---|
305
+ | Curated algorithmic problems | 71 | Custom (local) β€” arrays, strings, trees, DP, graphs | MIT |
306
+ | MetaMathQA samples | 100 | [`meta-math/MetaMathQA`](https://huggingface.co/datasets/meta-math/MetaMathQA) | CC BY 4.0 |
307
+ | Python/JavaScript patterns | 28 | Custom (local) β€” decorators, context managers, data classes | MIT |
308
+ | **Total** | **199** | | |
309
+
310
+ ---
311
+
312
+ ## Usage
313
+
314
+ ### Load with PEFT
315
+
316
+ ```python
317
+ from peft import PeftModel
318
+ from transformers import AutoModelForCausalLM, AutoTokenizer
319
+
320
+ base_model_id = "{base_model}"
321
+ adapter_repo = "{repo_id}"
322
+
323
+ tokenizer = AutoTokenizer.from_pretrained(base_model_id, trust_remote_code=True)
324
+ model = AutoModelForCausalLM.from_pretrained(
325
+ base_model_id,
326
+ torch_dtype="auto",
327
+ device_map="auto",
328
+ trust_remote_code=True,
329
+ )
330
+ model = PeftModel.from_pretrained(model, adapter_repo)
331
+ model.eval()
332
+ ```
333
+
334
+ ### Generate code
335
+
336
+ ```python
337
+ prompt = (
338
+ "<|im_start|>system\\n"
339
+ "You are BlitzKode, a precise AI coding assistant created by Sajad.\\n"
340
+ "<|im_end|>\\n"
341
+ "<|im_start|>user\\n"
342
+ "Write a Python function for binary search with full edge-case handling.\\n"
343
+ "<|im_end|>\\n"
344
+ "<|im_start|>assistant\\n"
345
+ )
346
+
347
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
348
+ outputs = model.generate(
349
+ **inputs,
350
+ max_new_tokens=300,
351
+ temperature=0.7,
352
+ do_sample=True,
353
+ repetition_penalty=1.1,
354
+ )
355
+ print(tokenizer.decode(outputs[0], skip_special_tokens=True))
356
+ ```
357
+
358
+ ### Merge adapter into base model (for export)
359
+
360
+ ```python
361
+ merged = model.merge_and_unload()
362
+ merged.save_pretrained("blitzkode-0.5b-merged")
363
+ tokenizer.save_pretrained("blitzkode-0.5b-merged")
364
+ ```
365
+
366
+ ---
367
+
368
+ ## Prompt Format
369
+
370
+ BlitzKode uses the **ChatML** template standard for Qwen models:
371
+
372
+ ```
373
+ <|im_start|>system
374
+ You are BlitzKode, a precise AI coding assistant created by Sajad.<|im_end|>
375
+ <|im_start|>user
376
+ {{your question}}<|im_end|>
377
+ <|im_start|>assistant
378
+ ```
379
+
380
+ ---
381
+
382
+ ## Limitations
383
+
384
+ - **Text-only** β€” no image/multimodal support.
385
+ - **0.5B parameters** β€” smaller and faster than the 1.5B GGUF variant; may be
386
+ less accurate on complex algorithmic tasks.
387
+ - **2048-token context** β€” not suitable for long repository-level analysis.
388
+ - **Review all outputs** β€” generated code must be tested before use in production.
389
+ - **Not security-audited** β€” do not use for cryptographic or safety-critical code
390
+ without thorough expert review.
391
+ - **Math reasoning** β€” MetaMathQA training improves basic reasoning but does not
392
+ substitute a dedicated math model.
393
+
394
+ ---
395
+
396
+ ## Relation to the Production Model
397
+
398
+ | Variant | Repo | Size | Runtime | Use case |
399
+ |---|---|---|---|---|
400
+ | GGUF (1.5B, F16) | [`neuralbroker/blitzkode`](https://huggingface.co/neuralbroker/blitzkode) | ~3 GB | llama.cpp / llama-cpp-python | Production; CPU/GPU, no Python ML stack needed |
401
+ | LoRA adapter (0.5B) | `{repo_id}` (this repo) | ~100 MB | PEFT + Transformers | Research; merging, further fine-tuning, quantization |
402
+
403
+ ---
404
+
405
+ ## License
406
+
407
+ **MIT** β€” see [LICENSE](https://github.com/neuralbroker/blitzkode/blob/main/LICENSE).
408
+
409
+ You must also comply with the upstream
410
+ [{base_model}](https://huggingface.co/{base_model}) license
411
+ when redistributing any derived weights.
412
+
413
+ ---
414
+
415
+ ## Citation
416
+
417
+ ```bibtex
418
+ @software{{blitzkode2025,
419
+ author = {{Sajad}},
420
+ title = {{BlitzKode: A Local AI Coding Assistant}},
421
+ year = {{2025}},
422
+ url = {{https://github.com/neuralbroker/blitzkode}}
423
+ }}
424
+ ```
425
+ """)
426
+
427
+ return frontmatter + "\n" + body
428
+
429
+
430
+ # ---------------------------------------------------------------------------
431
+ # Main push routine
432
+ # ---------------------------------------------------------------------------
433
+ def push(args: argparse.Namespace) -> None: # noqa: C901
434
+ check_huggingface_hub()
435
+
436
+ # Import here so the check above can give a clean error first.
437
+ from huggingface_hub import HfApi # type: ignore[import]
438
+ from huggingface_hub.utils import HfHubHTTPError # type: ignore[import]
439
+
440
+ sep = "=" * 70
441
+ print(sep)
442
+ print("BlitzKode β€” Push LoRA Adapter to Hugging Face Hub")
443
+ if args.dry_run:
444
+ print("(DRY RUN β€” nothing will be pushed)")
445
+ print(sep)
446
+
447
+ # ------------------------------------------------------------------
448
+ # Step 1: Validate checkpoint
449
+ # ------------------------------------------------------------------
450
+ print(f"\n[1/5] Validating checkpoint directory …")
451
+ print(f" Path: {args.checkpoint}")
452
+ adapter_config = validate_checkpoint(args.checkpoint)
453
+
454
+ base_model = adapter_config.get("base_model_name_or_path", "unknown")
455
+ lora_r = adapter_config.get("r", "?")
456
+ lora_alpha = adapter_config.get("lora_alpha", "?")
457
+ target_modules = adapter_config.get("target_modules", [])
458
+ files_found = sorted(p.name for p in args.checkpoint.iterdir() if p.is_file())
459
+
460
+ print(f" base_model : {base_model}")
461
+ print(f" lora r / alpha : {lora_r} / {lora_alpha}")
462
+ print(f" target_modules : {target_modules}")
463
+ print(f" files : {files_found}")
464
+ print(" [OK] Checkpoint is valid.")
465
+
466
+ # ------------------------------------------------------------------
467
+ # Step 2: Resolve token
468
+ # ------------------------------------------------------------------
469
+ print("\n[2/5] Resolving HuggingFace token …")
470
+ token = resolve_token(args.token, dry_run=args.dry_run)
471
+ if args.dry_run:
472
+ print(" [OK] Token check skipped (dry run).")
473
+ else:
474
+ masked = token[:8] + "..." if len(token) > 8 else "***"
475
+ print(f" [OK] Token resolved (starts with: {masked})")
476
+
477
+ # ------------------------------------------------------------------
478
+ # Dry-run exit
479
+ # ------------------------------------------------------------------
480
+ if args.dry_run:
481
+ print()
482
+ print(sep)
483
+ print("DRY RUN COMPLETE β€” all validations passed, nothing was pushed.")
484
+ print(f" Checkpoint : {args.checkpoint}")
485
+ print(f" Target repo : https://huggingface.co/{args.repo_id}")
486
+ print(f" Repo type : {args.repo_type}")
487
+ print(f" Private : {args.private}")
488
+ print(f" Files ready : {files_found}")
489
+ print(sep)
490
+ return
491
+
492
+ api = HfApi(token=token)
493
+
494
+ # ------------------------------------------------------------------
495
+ # Step 3: Create repo (if requested)
496
+ # ------------------------------------------------------------------
497
+ if args.create_repo:
498
+ print(f"\n[3/5] Creating / verifying repo: {args.repo_id} …")
499
+ try:
500
+ repo_url = api.create_repo(
501
+ repo_id=args.repo_id,
502
+ repo_type=args.repo_type,
503
+ private=args.private,
504
+ exist_ok=True, # silently succeed if repo already exists
505
+ )
506
+ print(f" [OK] Repo ready: {repo_url}")
507
+ except HfHubHTTPError as exc:
508
+ print(
509
+ f"\n[ERROR] Failed to create / access repo '{args.repo_id}':\n"
510
+ f" {exc}\n"
511
+ "Check that your token has write access and the repo name is correct.\n",
512
+ file=sys.stderr,
513
+ )
514
+ sys.exit(1)
515
+ else:
516
+ print("\n[3/5] Skipping repo creation (--no-create-repo).")
517
+
518
+ # ------------------------------------------------------------------
519
+ # Step 4: Upload checkpoint folder
520
+ # ------------------------------------------------------------------
521
+ print(f"\n[4/5] Uploading checkpoint folder β†’ {args.repo_id} …")
522
+ print(f" Commit message: \"{args.commit_message}\"")
523
+ try:
524
+ commit_info = api.upload_folder(
525
+ folder_path=str(args.checkpoint),
526
+ repo_id=args.repo_id,
527
+ repo_type=args.repo_type,
528
+ commit_message=args.commit_message,
529
+ )
530
+ commit_ref = getattr(commit_info, "oid", None) or str(commit_info)
531
+ print(f" [OK] Folder uploaded. Commit: {commit_ref}")
532
+ except HfHubHTTPError as exc:
533
+ print(
534
+ f"\n[ERROR] Folder upload failed:\n {exc}\n",
535
+ file=sys.stderr,
536
+ )
537
+ sys.exit(1)
538
+
539
+ # ------------------------------------------------------------------
540
+ # Step 5: Upload model card README.md
541
+ # ------------------------------------------------------------------
542
+ print("\n[5/5] Uploading model card (README.md) …")
543
+ readme_content = build_model_card(adapter_config, args.repo_id)
544
+ try:
545
+ api.upload_file(
546
+ path_or_fileobj=readme_content.encode("utf-8"),
547
+ path_in_repo="README.md",
548
+ repo_id=args.repo_id,
549
+ repo_type=args.repo_type,
550
+ commit_message="Update model card README.md",
551
+ )
552
+ print(" [OK] README.md uploaded.")
553
+ except HfHubHTTPError as exc:
554
+ # Non-fatal: the adapter files are already uploaded.
555
+ print(
556
+ f"\n[WARN] Could not upload README.md (adapter files were uploaded OK):\n"
557
+ f" {exc}\n"
558
+ "You can upload the model card manually from the Hub web interface.\n",
559
+ file=sys.stderr,
560
+ )
561
+
562
+ # ------------------------------------------------------------------
563
+ # Summary
564
+ # ------------------------------------------------------------------
565
+ repo_url = f"https://huggingface.co/{args.repo_id}"
566
+ print()
567
+ print(sep)
568
+ print("PUSH COMPLETE")
569
+ print(f" Repo URL : {repo_url}")
570
+ print(f" Checkpoint : {args.checkpoint}")
571
+ print(f" Files pushed : {files_found}")
572
+ print(f" Base model : {base_model}")
573
+ print(f" LoRA r/alpha : {lora_r}/{lora_alpha}")
574
+ print(f" Commit msg : {args.commit_message}")
575
+ print(sep)
576
+
577
+
578
+ # ---------------------------------------------------------------------------
579
+ # Entry point
580
+ # ---------------------------------------------------------------------------
581
+ def main() -> None:
582
+ args = parse_args()
583
+ push(args)
584
+
585
+
586
+ if __name__ == "__main__":
587
+ main()