Enable HF Space auto-sync env defaults and docs
Browse files
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
|
|
|
|
| 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
|
| 63 |
-
- `HF_MODEL_FILE` - nama file model di Hub (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|