firgiawann commited on
Commit
ef7843b
·
verified ·
1 Parent(s): 9efd4ae

Enable HF Space auto-sync env defaults and docs

Browse files
Files changed (2) hide show
  1. README.md +10 -3
  2. app.py +185 -4
README.md CHANGED
@@ -43,7 +43,8 @@ Versi ini difokuskan untuk tahap awal:
43
  - **Menu Training** (accordion)
44
  - membaca dataset dari `dataset/train` dan `dataset/test`
45
  - jika label `.txt` YOLO belum ada, bisa aktifkan pseudo-label
46
- - output model terbaik disimpan sebagai `best.pt`
 
47
 
48
  Catatan:
49
  - input evaluasi batch dihapus.
@@ -59,8 +60,14 @@ Catatan:
59
 
60
  Opsional environment variable:
61
  - `MODEL_PATH` - path model lokal custom
62
- - `HF_MODEL_REPO` - repo model Hugging Face Hub
63
- - `HF_MODEL_FILE` - nama file model di Hub (default `yolov8n.pt`)
 
 
 
 
 
 
64
 
65
  ## Menjalankan Lokal
66
 
 
43
  - **Menu Training** (accordion)
44
  - membaca dataset dari `dataset/train` dan `dataset/test`
45
  - jika label `.txt` YOLO belum ada, bisa aktifkan pseudo-label
46
+ - output model terbaik selalu di-overwrite ke `best.pt` setiap training selesai
47
+ - jika env Hugging Face diisi, `best.pt` otomatis di-sync ke repo HF
48
 
49
  Catatan:
50
  - input evaluasi batch dihapus.
 
60
 
61
  Opsional environment variable:
62
  - `MODEL_PATH` - path model lokal custom
63
+ - `HF_MODEL_REPO` - repo target Hugging Face (contoh: `firgiawann/KLP2`)
64
+ - `HF_MODEL_FILE` - nama file model di Hub (disarankan: `best.pt`)
65
+ - `HF_MODEL_REPO_TYPE` - tipe repo HF: `space` (default), `model`, atau `dataset`
66
+ - `HF_TOKEN` - token untuk upload otomatis saat training selesai
67
+
68
+ Catatan sync otomatis setelah training:
69
+ - Jika `HF_MODEL_REPO` terisi, aplikasi akan upload `best.pt` ke `HF_MODEL_FILE`.
70
+ - Jika tidak terisi, training tetap jalan normal dan hanya menyimpan `best.pt` lokal.
71
 
72
  ## Menjalankan Lokal
73
 
app.py CHANGED
@@ -2,9 +2,12 @@ import io
2
  import os
3
  import shutil
4
  import asyncio
 
 
5
  from datetime import datetime
6
  from functools import lru_cache
7
  from pathlib import Path
 
8
  from typing import Any, Dict, Tuple
9
 
10
  import cv2
@@ -12,7 +15,7 @@ import gradio as gr
12
  import matplotlib.pyplot as plt
13
  import numpy as np
14
  import seaborn as sns
15
- from huggingface_hub import hf_hub_download
16
  from ultralytics import YOLO
17
 
18
  CM_INIT = {"tp": 0, "fp": 0, "fn": 0, "tn": 0}
@@ -60,8 +63,9 @@ def resolve_model_path(model_choice: str) -> str:
60
  hf_repo = os.getenv("HF_MODEL_REPO", "").strip()
61
  default_hf = "best.pt" if model_choice == "Best.pt" else "yolov8n.pt"
62
  hf_file = os.getenv("HF_MODEL_FILE", default_hf).strip()
 
63
  if hf_repo:
64
- return hf_hub_download(repo_id=hf_repo, filename=hf_file)
65
 
66
  raise FileNotFoundError(
67
  f"Model pilihan `{model_choice}` tidak ditemukan. Simpan 'yolov8n.pt' / 'best.pt' di root project, "
@@ -442,6 +446,178 @@ def _prepare_training_dataset(dataset_root: Path, use_pseudo_if_missing: bool, p
442
  return yaml_path, info
443
 
444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  def run_training(
446
  epochs: float,
447
  imgsz: float,
@@ -458,7 +634,7 @@ def run_training(
458
  pseudo_conf=float(pseudo_conf),
459
  )
460
 
461
- model_path = str(base_dir / "yolov8n.pt") if (base_dir / "yolov8n.pt").exists() else resolve_model_path()
462
  model = YOLO(model_path)
463
  run_name = f"classroom_train_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
464
  results = model.train(
@@ -476,14 +652,18 @@ def run_training(
476
  save_dir = Path(str(results.save_dir))
477
  best_pt = save_dir / "weights" / "best.pt"
478
  best_out = base_dir / "best.pt"
 
479
  if best_pt.exists():
480
  shutil.copy2(best_pt, best_out)
 
 
481
 
482
  msg = (
483
  "Training selesai.\n"
484
  f"- {prep_info}\n"
485
  f"- Run dir: {save_dir}\n"
486
- f"- best.pt: {best_out if best_out.exists() else best_pt}"
 
487
  )
488
  return msg, str(best_out) if best_out.exists() else ""
489
  except Exception as err:
@@ -627,4 +807,5 @@ with gr.Blocks(title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
627
 
628
 
629
  if __name__ == "__main__":
 
630
  demo.launch(css=CUSTOM_CSS, ssr_mode=False)
 
2
  import os
3
  import shutil
4
  import asyncio
5
+ import fnmatch
6
+ import time
7
  from datetime import datetime
8
  from functools import lru_cache
9
  from pathlib import Path
10
+ from threading import Thread
11
  from typing import Any, Dict, Tuple
12
 
13
  import cv2
 
15
  import matplotlib.pyplot as plt
16
  import numpy as np
17
  import seaborn as sns
18
+ from huggingface_hub import CommitOperationAdd, CommitOperationDelete, HfApi, hf_hub_download
19
  from ultralytics import YOLO
20
 
21
  CM_INIT = {"tp": 0, "fp": 0, "fn": 0, "tn": 0}
 
63
  hf_repo = os.getenv("HF_MODEL_REPO", "").strip()
64
  default_hf = "best.pt" if model_choice == "Best.pt" else "yolov8n.pt"
65
  hf_file = os.getenv("HF_MODEL_FILE", default_hf).strip()
66
+ hf_repo_type = os.getenv("HF_MODEL_REPO_TYPE", "space").strip() or "space"
67
  if hf_repo:
68
+ return hf_hub_download(repo_id=hf_repo, filename=hf_file, repo_type=hf_repo_type)
69
 
70
  raise FileNotFoundError(
71
  f"Model pilihan `{model_choice}` tidak ditemukan. Simpan 'yolov8n.pt' / 'best.pt' di root project, "
 
446
  return yaml_path, info
447
 
448
 
449
+ def sync_best_to_hf(best_model_path: Path) -> str:
450
+ hf_repo = os.getenv("HF_MODEL_REPO", "").strip()
451
+ hf_file = os.getenv("HF_MODEL_FILE", "best.pt").strip() or "best.pt"
452
+ hf_repo_type = os.getenv("HF_MODEL_REPO_TYPE", "space").strip() or "space"
453
+ hf_token = os.getenv("HF_TOKEN", "").strip() or None
454
+
455
+ if not hf_repo:
456
+ return "HF sync dilewati: set HF_MODEL_REPO untuk upload otomatis."
457
+
458
+ try:
459
+ api = HfApi(token=hf_token)
460
+ api.upload_file(
461
+ path_or_fileobj=str(best_model_path),
462
+ path_in_repo=hf_file,
463
+ repo_id=hf_repo,
464
+ repo_type=hf_repo_type,
465
+ commit_message=f"Auto-update {hf_file} ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})",
466
+ )
467
+ return f"HF sync sukses: {hf_repo} ({hf_repo_type}) -> {hf_file}"
468
+ except Exception as err:
469
+ return f"HF sync gagal: {err}"
470
+
471
+
472
+ def _env_true(var_name: str, default: bool = False) -> bool:
473
+ val = os.getenv(var_name)
474
+ if val is None:
475
+ return default
476
+ return val.strip().lower() in {"1", "true", "yes", "on"}
477
+
478
+
479
+ def _parse_globs(raw_value: str) -> list[str]:
480
+ return [g.strip() for g in raw_value.split(",") if g.strip()]
481
+
482
+
483
+ def _snapshot_repo_files(root: Path, ignore_globs: list[str]) -> Dict[str, Tuple[int, int]]:
484
+ snapshot: Dict[str, Tuple[int, int]] = {}
485
+ for p in root.rglob("*"):
486
+ if not p.is_file():
487
+ continue
488
+ rel = p.relative_to(root).as_posix()
489
+ if any(fnmatch.fnmatch(rel, pat) for pat in ignore_globs):
490
+ continue
491
+ st = p.stat()
492
+ snapshot[rel] = (int(st.st_mtime_ns), int(st.st_size))
493
+ return snapshot
494
+
495
+
496
+ def _commit_repo_deltas(
497
+ api: HfApi,
498
+ root: Path,
499
+ repo_id: str,
500
+ repo_type: str,
501
+ changed: set[str],
502
+ deleted: set[str],
503
+ max_file_mb: float,
504
+ ) -> str:
505
+ ops = []
506
+ skipped: list[str] = []
507
+ for rel in sorted(changed):
508
+ src = root / rel
509
+ if not src.exists():
510
+ continue
511
+ if src.stat().st_size > max_file_mb * 1024 * 1024:
512
+ skipped.append(rel)
513
+ continue
514
+ ops.append(CommitOperationAdd(path_in_repo=rel, path_or_fileobj=str(src)))
515
+
516
+ for rel in sorted(deleted):
517
+ ops.append(CommitOperationDelete(path_in_repo=rel))
518
+
519
+ if not ops:
520
+ if skipped:
521
+ return f"HF autosync: tidak ada file di-commit (skip terlalu besar: {len(skipped)})."
522
+ return "HF autosync: tidak ada perubahan untuk di-commit."
523
+
524
+ api.create_commit(
525
+ repo_id=repo_id,
526
+ repo_type=repo_type,
527
+ operations=ops,
528
+ commit_message=(
529
+ "Auto-sync repo updates "
530
+ f"({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})"
531
+ ),
532
+ )
533
+ return (
534
+ "HF autosync sukses: "
535
+ f"updated={len(changed) - len(skipped)}, deleted={len(deleted)}, skipped={len(skipped)}"
536
+ )
537
+
538
+
539
+ def _repo_autosync_loop(
540
+ root: Path,
541
+ api: HfApi,
542
+ repo_id: str,
543
+ repo_type: str,
544
+ interval_sec: int,
545
+ ignore_globs: list[str],
546
+ max_file_mb: float,
547
+ ) -> None:
548
+ prev = _snapshot_repo_files(root, ignore_globs)
549
+ print(
550
+ f"HF autosync aktif: repo={repo_id} type={repo_type} interval={interval_sec}s"
551
+ )
552
+ while True:
553
+ try:
554
+ time.sleep(interval_sec)
555
+ curr = _snapshot_repo_files(root, ignore_globs)
556
+ changed = {rel for rel, sig in curr.items() if prev.get(rel) != sig}
557
+ deleted = set(prev.keys()) - set(curr.keys())
558
+
559
+ if changed or deleted:
560
+ msg = _commit_repo_deltas(
561
+ api=api,
562
+ root=root,
563
+ repo_id=repo_id,
564
+ repo_type=repo_type,
565
+ changed=changed,
566
+ deleted=deleted,
567
+ max_file_mb=max_file_mb,
568
+ )
569
+ print(msg)
570
+ prev = curr
571
+ except Exception as err:
572
+ print(f"HF autosync error: {err}")
573
+
574
+
575
+ _repo_autosync_started = False
576
+
577
+
578
+ def start_repo_autosync(root: Path) -> str:
579
+ global _repo_autosync_started
580
+ if _repo_autosync_started:
581
+ return "HF autosync sudah aktif."
582
+
583
+ repo_id = os.getenv("HF_AUTOSYNC_REPO_ID", "").strip() or os.getenv("HF_MODEL_REPO", "").strip()
584
+ default_enabled = bool(repo_id)
585
+ enabled = _env_true("HF_AUTOSYNC_ENABLED", default=default_enabled)
586
+ if not enabled:
587
+ return "HF autosync nonaktif (HF_AUTOSYNC_ENABLED=false)."
588
+ if not repo_id:
589
+ return "HF autosync dilewati: set HF_AUTOSYNC_REPO_ID atau HF_MODEL_REPO."
590
+
591
+ repo_type = (
592
+ os.getenv("HF_AUTOSYNC_REPO_TYPE", "").strip()
593
+ or os.getenv("HF_MODEL_REPO_TYPE", "").strip()
594
+ or "space"
595
+ )
596
+ token = os.getenv("HF_AUTOSYNC_TOKEN", "").strip() or os.getenv("HF_TOKEN", "").strip() or None
597
+ interval_sec = max(3, int(float(os.getenv("HF_AUTOSYNC_INTERVAL_SEC", "15").strip() or "15")))
598
+ max_file_mb = max(1.0, float(os.getenv("HF_AUTOSYNC_MAX_FILE_MB", "200").strip() or "200"))
599
+ ignore_globs = _parse_globs(
600
+ os.getenv(
601
+ "HF_AUTOSYNC_IGNORE_GLOBS",
602
+ ".git/**,__pycache__/**,.venv/**,venv/**,*.pyc,*.pyo,*.tmp",
603
+ )
604
+ )
605
+
606
+ api = HfApi(token=token)
607
+ worker = Thread(
608
+ target=_repo_autosync_loop,
609
+ args=(root, api, repo_id, repo_type, interval_sec, ignore_globs, max_file_mb),
610
+ daemon=True,
611
+ name="hf-repo-autosync",
612
+ )
613
+ worker.start()
614
+ _repo_autosync_started = True
615
+ return (
616
+ "HF autosync dinyalakan: "
617
+ f"repo={repo_id}, type={repo_type}, interval={interval_sec}s"
618
+ )
619
+
620
+
621
  def run_training(
622
  epochs: float,
623
  imgsz: float,
 
634
  pseudo_conf=float(pseudo_conf),
635
  )
636
 
637
+ model_path = str(base_dir / "yolov8n.pt") if (base_dir / "yolov8n.pt").exists() else resolve_model_path("YOLOv8n")
638
  model = YOLO(model_path)
639
  run_name = f"classroom_train_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
640
  results = model.train(
 
652
  save_dir = Path(str(results.save_dir))
653
  best_pt = save_dir / "weights" / "best.pt"
654
  best_out = base_dir / "best.pt"
655
+ hf_sync_msg = "HF sync dilewati: best.pt tidak ditemukan di output training."
656
  if best_pt.exists():
657
  shutil.copy2(best_pt, best_out)
658
+ load_model.cache_clear()
659
+ hf_sync_msg = sync_best_to_hf(best_out)
660
 
661
  msg = (
662
  "Training selesai.\n"
663
  f"- {prep_info}\n"
664
  f"- Run dir: {save_dir}\n"
665
+ f"- best.pt: {best_out if best_out.exists() else best_pt}\n"
666
+ f"- {hf_sync_msg}"
667
  )
668
  return msg, str(best_out) if best_out.exists() else ""
669
  except Exception as err:
 
807
 
808
 
809
  if __name__ == "__main__":
810
+ print(start_repo_autosync(Path(__file__).resolve().parent))
811
  demo.launch(css=CUSTOM_CSS, ssr_mode=False)