Update 37 run training preparation in background worker
Browse files- CHANGELOG.md +6 -0
- README.md +2 -0
- app.py +154 -183
- space_training_archive/-/training.log +0 -4
- space_training_archive/-/training_summary.json +0 -11
- training_outputs/training_summary_latest.json +0 -11
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
| 1 |
# Changelog
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
## Update ke-36 - 2026-05-01
|
| 4 |
- Reset alur training agar `best.pt` dan artefak training lama bisa dikosongkan tanpa menghapus opsi `Best.pt` dari UI.
|
| 5 |
- `Best.pt` tidak lagi fallback diam-diam ke `YOLOv8n`; jika belum ada, aplikasi menampilkan pesan model belum tersedia.
|
|
|
|
| 1 |
# Changelog
|
| 2 |
|
| 3 |
+
## Update ke-37 - 2026-05-01
|
| 4 |
+
- Perbaiki training yang berhenti pada tahap `preparing_dataset`.
|
| 5 |
+
- Persiapan dataset, pseudo-label, training YOLO, penyimpanan `best.pt`, grafik PNG, confusion matrix, log, dan sync artefak kini berjalan dalam background worker sejak awal.
|
| 6 |
+
- Tombol training sekarang hanya memulai worker dan langsung mengembalikan status, sehingga proses tidak bergantung pada koneksi browser/request Gradio.
|
| 7 |
+
- Minor update dinaikkan ke-37.
|
| 8 |
+
|
| 9 |
## Update ke-36 - 2026-05-01
|
| 10 |
- Reset alur training agar `best.pt` dan artefak training lama bisa dikosongkan tanpa menghapus opsi `Best.pt` dari UI.
|
| 11 |
- `Best.pt` tidak lagi fallback diam-diam ke `YOLOv8n`; jika belum ada, aplikasi menampilkan pesan model belum tersedia.
|
README.md
CHANGED
|
@@ -51,6 +51,7 @@ Versi ini difokuskan untuk tahap awal:
|
|
| 51 |
- file `training.log`, `training_curve_latest.png`, dan `confusion_matrix_latest.png` juga disimpan lokal
|
| 52 |
- jika env Hugging Face diisi, `best.pt` dan artefak training otomatis di-sync ke repo HF
|
| 53 |
- grafik training disimpan permanen sebagai gambar PNG di `training_outputs/training_curve_latest.png` dan `space_training_archive/<run>/training_curve.png`
|
|
|
|
| 54 |
- **Tahap 3 - Input Gambar**
|
| 55 |
- input gambar (upload/webcam)
|
| 56 |
- tombol proses
|
|
@@ -128,6 +129,7 @@ Catatan auto-save hasil training:
|
|
| 128 |
- File grafik terbaru juga disalin ke `training_outputs/training_curve_latest.png` agar tetap bisa ditampilkan ulang setelah Space rebuild.
|
| 129 |
- Proses training juga langsung menyalin grafik YOLO `results.png` menjadi PNG permanen saat training selesai, sehingga tidak bergantung pada refresh browser.
|
| 130 |
- Jika koneksi browser terputus saat training, artefak tetap aman setelah proses training selesai dan sync ke repo berhasil. Jika container berhenti sebelum training selesai, artefak final memang belum sempat dibuat.
|
|
|
|
| 131 |
|
| 132 |
Catatan auto-sync update file repo:
|
| 133 |
- Default `HF_AUTOSYNC_ENABLED=false`, jadi tidak ada sync repo saat startup/runtime.
|
|
|
|
| 51 |
- file `training.log`, `training_curve_latest.png`, dan `confusion_matrix_latest.png` juga disimpan lokal
|
| 52 |
- jika env Hugging Face diisi, `best.pt` dan artefak training otomatis di-sync ke repo HF
|
| 53 |
- grafik training disimpan permanen sebagai gambar PNG di `training_outputs/training_curve_latest.png` dan `space_training_archive/<run>/training_curve.png`
|
| 54 |
+
- persiapan dataset dan pseudo-label berjalan di background worker, sehingga training tidak berhenti hanya karena koneksi browser terputus
|
| 55 |
- **Tahap 3 - Input Gambar**
|
| 56 |
- input gambar (upload/webcam)
|
| 57 |
- tombol proses
|
|
|
|
| 129 |
- File grafik terbaru juga disalin ke `training_outputs/training_curve_latest.png` agar tetap bisa ditampilkan ulang setelah Space rebuild.
|
| 130 |
- Proses training juga langsung menyalin grafik YOLO `results.png` menjadi PNG permanen saat training selesai, sehingga tidak bergantung pada refresh browser.
|
| 131 |
- Jika koneksi browser terputus saat training, artefak tetap aman setelah proses training selesai dan sync ke repo berhasil. Jika container berhenti sebelum training selesai, artefak final memang belum sempat dibuat.
|
| 132 |
+
- Pseudo-label dari dataset tanpa label `.txt` dapat memakan waktu di CPU Space karena semua gambar harus diproses YOLOv8n sebelum epoch dimulai.
|
| 133 |
|
| 134 |
Catatan auto-sync update file repo:
|
| 135 |
- Default `HF_AUTOSYNC_ENABLED=false`, jadi tidak ada sync repo saat startup/runtime.
|
app.py
CHANGED
|
@@ -24,7 +24,7 @@ from ultralytics import YOLO
|
|
| 24 |
CM_INIT = {"tp": 0, "fp": 0, "fn": 0, "tn": 0, "person": 0, "not_person": 0}
|
| 25 |
EVAL_CLASSES = ["Person", "Not Person/Background"]
|
| 26 |
APP_VERSION = os.getenv("APP_VERSION", "v0.9.0")
|
| 27 |
-
APP_MINOR_UPDATE = os.getenv("APP_MINOR_UPDATE", "
|
| 28 |
|
| 29 |
|
| 30 |
# Gradio/Spaces can trigger noisy asyncio cleanup traces ("Invalid file descriptor: -1")
|
|
@@ -1662,6 +1662,144 @@ def poll_training_status() -> Tuple[str, Any, Any, Any, Any, np.ndarray, Any, np
|
|
| 1662 |
)
|
| 1663 |
|
| 1664 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1665 |
def run_training(
|
| 1666 |
train_model_choice: str,
|
| 1667 |
epochs: float,
|
|
@@ -1681,198 +1819,27 @@ def run_training(
|
|
| 1681 |
return poll_training_status()
|
| 1682 |
|
| 1683 |
try:
|
| 1684 |
-
_write_status(
|
| 1685 |
-
paths["status"],
|
| 1686 |
-
{
|
| 1687 |
-
"status": "running",
|
| 1688 |
-
"stage": "preparing_dataset",
|
| 1689 |
-
"run_name": current.get("run_name", "-"),
|
| 1690 |
-
"started_at": datetime.now().isoformat(),
|
| 1691 |
-
},
|
| 1692 |
-
)
|
| 1693 |
-
with paths["log"].open("a", encoding="utf-8") as lf:
|
| 1694 |
-
lf.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Preparing dataset...\n")
|
| 1695 |
-
|
| 1696 |
-
dataset_root = base_dir / "dataset"
|
| 1697 |
-
data_yaml, prep_info = _prepare_training_dataset(
|
| 1698 |
-
dataset_root=dataset_root,
|
| 1699 |
-
use_pseudo_if_missing=use_pseudo_if_missing,
|
| 1700 |
-
pseudo_conf=float(pseudo_conf),
|
| 1701 |
-
log_file=paths["log"],
|
| 1702 |
-
)
|
| 1703 |
-
|
| 1704 |
-
if train_model_choice == "Best.pt":
|
| 1705 |
-
model_path = resolve_model_path("Best.pt")
|
| 1706 |
-
else:
|
| 1707 |
-
model_path = str(base_dir / "yolov8n.pt") if (base_dir / "yolov8n.pt").exists() else resolve_model_path("YOLOv8n")
|
| 1708 |
run_name = f"classroom_train_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
| 1709 |
project_dir = base_dir / "runs_train"
|
| 1710 |
save_dir = project_dir / run_name
|
| 1711 |
results_csv = save_dir / "results.csv"
|
| 1712 |
-
|
| 1713 |
-
with paths["log"].open("a", encoding="utf-8") as lf:
|
| 1714 |
-
lf.write(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Start training {run_name}\n")
|
| 1715 |
-
lf.write(f"Model awal training: {train_model_choice} ({model_path})\n")
|
| 1716 |
-
lf.write(f"{prep_info}\n")
|
| 1717 |
-
|
| 1718 |
-
train_code = r"""
|
| 1719 |
-
import os, json, shutil
|
| 1720 |
-
from datetime import datetime
|
| 1721 |
-
from pathlib import Path
|
| 1722 |
-
from ultralytics import YOLO
|
| 1723 |
-
from huggingface_hub import HfApi
|
| 1724 |
-
|
| 1725 |
-
def write_status(path, payload):
|
| 1726 |
-
p = Path(path)
|
| 1727 |
-
tmp = p.with_suffix(".tmp")
|
| 1728 |
-
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 1729 |
-
tmp.replace(p)
|
| 1730 |
-
|
| 1731 |
-
status_path = os.environ["TRAIN_STATUS_PATH"]
|
| 1732 |
-
base_dir = Path(os.environ["TRAIN_BASE_DIR"])
|
| 1733 |
-
run_name = os.environ["TRAIN_RUN_NAME"]
|
| 1734 |
-
save_dir = Path(os.environ["TRAIN_SAVE_DIR"])
|
| 1735 |
-
results_csv = save_dir / "results.csv"
|
| 1736 |
-
|
| 1737 |
-
state = {
|
| 1738 |
-
"status": "running",
|
| 1739 |
-
"run_name": run_name,
|
| 1740 |
-
"pid": os.getpid(),
|
| 1741 |
-
"save_dir": str(save_dir),
|
| 1742 |
-
"results_csv": str(results_csv),
|
| 1743 |
-
"started_at": datetime.now().isoformat(),
|
| 1744 |
-
}
|
| 1745 |
-
write_status(status_path, state)
|
| 1746 |
-
|
| 1747 |
-
try:
|
| 1748 |
-
model = YOLO(os.environ["TRAIN_MODEL_PATH"])
|
| 1749 |
-
res = model.train(
|
| 1750 |
-
data=os.environ["TRAIN_DATA_YAML"],
|
| 1751 |
-
epochs=int(os.environ["TRAIN_EPOCHS"]),
|
| 1752 |
-
imgsz=int(os.environ["TRAIN_IMGSZ"]),
|
| 1753 |
-
batch=int(os.environ["TRAIN_BATCH"]),
|
| 1754 |
-
device="cpu",
|
| 1755 |
-
project=os.environ["TRAIN_PROJECT_DIR"],
|
| 1756 |
-
name=run_name,
|
| 1757 |
-
exist_ok=False,
|
| 1758 |
-
verbose=True,
|
| 1759 |
-
)
|
| 1760 |
-
final_save = Path(str(res.save_dir))
|
| 1761 |
-
best_pt = final_save / "weights" / "best.pt"
|
| 1762 |
-
best_out = base_dir / "best.pt"
|
| 1763 |
-
artifact_dir = base_dir / "training_outputs"
|
| 1764 |
-
artifact_dir.mkdir(parents=True, exist_ok=True)
|
| 1765 |
-
chart_out = artifact_dir / "training_curve_latest.png"
|
| 1766 |
-
cm_out = artifact_dir / "confusion_matrix_latest.png"
|
| 1767 |
-
summary_out = artifact_dir / "training_summary_latest.json"
|
| 1768 |
-
|
| 1769 |
-
chart_src = final_save / "results.png"
|
| 1770 |
-
if chart_src.exists():
|
| 1771 |
-
shutil.copy2(chart_src, chart_out)
|
| 1772 |
-
|
| 1773 |
-
for cm_src in (final_save / "confusion_matrix.png", final_save / "confusion_matrix_normalized.png"):
|
| 1774 |
-
if cm_src.exists():
|
| 1775 |
-
shutil.copy2(cm_src, cm_out)
|
| 1776 |
-
break
|
| 1777 |
-
|
| 1778 |
-
hf_sync_msg = "HF sync dilewati: best.pt tidak ditemukan."
|
| 1779 |
-
if best_pt.exists():
|
| 1780 |
-
shutil.copy2(best_pt, best_out)
|
| 1781 |
-
hf_repo = os.getenv("HF_MODEL_REPO", "").strip()
|
| 1782 |
-
hf_file = os.getenv("HF_MODEL_FILE", "best.pt").strip() or "best.pt"
|
| 1783 |
-
hf_repo_type = os.getenv("HF_MODEL_REPO_TYPE", "space").strip() or "space"
|
| 1784 |
-
hf_token = os.getenv("HF_TOKEN", "").strip() or None
|
| 1785 |
-
if hf_repo:
|
| 1786 |
-
api = HfApi(token=hf_token)
|
| 1787 |
-
api.upload_file(
|
| 1788 |
-
path_or_fileobj=str(best_out),
|
| 1789 |
-
path_in_repo=hf_file,
|
| 1790 |
-
repo_id=hf_repo,
|
| 1791 |
-
repo_type=hf_repo_type,
|
| 1792 |
-
commit_message=f"Auto-update {hf_file} ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})",
|
| 1793 |
-
)
|
| 1794 |
-
hf_sync_msg = f"HF sync sukses: {hf_repo} ({hf_repo_type}) -> {hf_file}"
|
| 1795 |
-
|
| 1796 |
-
artifact_sync_msg = "HF training artifact sync dilewati: set HF_MODEL_REPO."
|
| 1797 |
-
try:
|
| 1798 |
-
hf_repo = os.getenv("HF_MODEL_REPO", "").strip()
|
| 1799 |
-
hf_repo_type = os.getenv("HF_MODEL_REPO_TYPE", "space").strip() or "space"
|
| 1800 |
-
hf_token = os.getenv("HF_TOKEN", "").strip() or None
|
| 1801 |
-
repo_dir = os.getenv("HF_TRAINING_DIR", "space_training_archive").strip().strip("/") or "space_training_archive"
|
| 1802 |
-
if hf_repo:
|
| 1803 |
-
api = HfApi(token=hf_token)
|
| 1804 |
-
summary_payload = {
|
| 1805 |
-
"saved_at": datetime.now().isoformat(),
|
| 1806 |
-
"status": "done",
|
| 1807 |
-
"run_name": run_name,
|
| 1808 |
-
"save_dir": str(final_save),
|
| 1809 |
-
"results_csv": str(final_save / "results.csv"),
|
| 1810 |
-
"best_path": str(best_out if best_out.exists() else best_pt),
|
| 1811 |
-
"hf_sync_msg": hf_sync_msg,
|
| 1812 |
-
}
|
| 1813 |
-
summary_out.write_text(json.dumps(summary_payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 1814 |
-
upload_items = [
|
| 1815 |
-
(final_save / "results.csv", f"{repo_dir}/{run_name}/results.csv"),
|
| 1816 |
-
(Path(os.environ["TRAIN_LOG_PATH"]), f"{repo_dir}/{run_name}/training.log"),
|
| 1817 |
-
(chart_out, f"{repo_dir}/{run_name}/training_curve.png"),
|
| 1818 |
-
(cm_out, f"{repo_dir}/{run_name}/confusion_matrix.png"),
|
| 1819 |
-
(summary_out, f"{repo_dir}/{run_name}/training_summary.json"),
|
| 1820 |
-
(chart_out, "training_outputs/training_curve_latest.png"),
|
| 1821 |
-
(cm_out, "training_outputs/confusion_matrix_latest.png"),
|
| 1822 |
-
(summary_out, "training_outputs/training_summary_latest.json"),
|
| 1823 |
-
]
|
| 1824 |
-
uploaded = 0
|
| 1825 |
-
for local_path, repo_path in upload_items:
|
| 1826 |
-
if local_path.exists() and local_path.is_file():
|
| 1827 |
-
api.upload_file(
|
| 1828 |
-
path_or_fileobj=str(local_path),
|
| 1829 |
-
path_in_repo=repo_path,
|
| 1830 |
-
repo_id=hf_repo,
|
| 1831 |
-
repo_type=hf_repo_type,
|
| 1832 |
-
commit_message=f"Save permanent training artifact {run_name}",
|
| 1833 |
-
)
|
| 1834 |
-
uploaded += 1
|
| 1835 |
-
artifact_sync_msg = f"HF training artifact sync sukses: {uploaded} file -> {repo_dir}/{run_name}"
|
| 1836 |
-
except Exception as sync_err:
|
| 1837 |
-
artifact_sync_msg = f"HF training artifact sync gagal: {sync_err}"
|
| 1838 |
-
|
| 1839 |
-
state.update({
|
| 1840 |
-
"status": "done",
|
| 1841 |
-
"ended_at": datetime.now().isoformat(),
|
| 1842 |
-
"save_dir": str(final_save),
|
| 1843 |
-
"results_csv": str(final_save / "results.csv"),
|
| 1844 |
-
"best_path": str(best_out if best_out.exists() else best_pt),
|
| 1845 |
-
"hf_sync_msg": hf_sync_msg,
|
| 1846 |
-
"artifacts_sync_attempted": True,
|
| 1847 |
-
"artifacts_sync_msg": artifact_sync_msg,
|
| 1848 |
-
})
|
| 1849 |
-
write_status(status_path, state)
|
| 1850 |
-
print("TRAIN_DONE", flush=True)
|
| 1851 |
-
except Exception as e:
|
| 1852 |
-
state.update({
|
| 1853 |
-
"status": "failed",
|
| 1854 |
-
"ended_at": datetime.now().isoformat(),
|
| 1855 |
-
"error": str(e),
|
| 1856 |
-
})
|
| 1857 |
-
write_status(status_path, state)
|
| 1858 |
-
print("TRAIN_FAILED: " + str(e), flush=True)
|
| 1859 |
-
"""
|
| 1860 |
env = os.environ.copy()
|
| 1861 |
env["TRAIN_BASE_DIR"] = str(base_dir)
|
| 1862 |
-
env["
|
| 1863 |
-
env["TRAIN_DATA_YAML"] = str(data_yaml)
|
| 1864 |
env["TRAIN_EPOCHS"] = str(int(epochs))
|
| 1865 |
env["TRAIN_IMGSZ"] = str(int(imgsz))
|
| 1866 |
env["TRAIN_BATCH"] = str(int(batch))
|
|
|
|
|
|
|
| 1867 |
env["TRAIN_PROJECT_DIR"] = str(project_dir)
|
| 1868 |
env["TRAIN_RUN_NAME"] = run_name
|
| 1869 |
env["TRAIN_SAVE_DIR"] = str(save_dir)
|
| 1870 |
env["TRAIN_STATUS_PATH"] = str(paths["status"])
|
| 1871 |
-
env["TRAIN_LOG_PATH"] = str(paths["log"])
|
| 1872 |
|
| 1873 |
with paths["log"].open("a", encoding="utf-8") as lf:
|
|
|
|
| 1874 |
proc = subprocess.Popen(
|
| 1875 |
-
[sys.executable, "-u", "-
|
| 1876 |
stdout=lf,
|
| 1877 |
stderr=subprocess.STDOUT,
|
| 1878 |
cwd=str(base_dir),
|
|
@@ -1884,7 +1851,7 @@ def run_training(
|
|
| 1884 |
paths["status"],
|
| 1885 |
{
|
| 1886 |
"status": "running",
|
| 1887 |
-
"stage": "
|
| 1888 |
"run_name": run_name,
|
| 1889 |
"pid": proc.pid,
|
| 1890 |
"save_dir": str(save_dir),
|
|
@@ -1895,7 +1862,7 @@ def run_training(
|
|
| 1895 |
return poll_training_status()
|
| 1896 |
except Exception as err:
|
| 1897 |
with paths["log"].open("a", encoding="utf-8") as lf:
|
| 1898 |
-
lf.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Training gagal: {err}\n")
|
| 1899 |
_write_status(
|
| 1900 |
paths["status"],
|
| 1901 |
{
|
|
@@ -2251,10 +2218,14 @@ with gr.Blocks(title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
|
|
| 2251 |
)
|
| 2252 |
|
| 2253 |
|
| 2254 |
-
if __name__ == "__main__":
|
| 2255 |
-
if
|
| 2256 |
-
|
| 2257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2258 |
print("HF autosync nonaktif (HF_AUTOSYNC_ENABLED=false).")
|
| 2259 |
demo.queue()
|
| 2260 |
demo.launch(css=CUSTOM_CSS, ssr_mode=False)
|
|
|
|
| 24 |
CM_INIT = {"tp": 0, "fp": 0, "fn": 0, "tn": 0, "person": 0, "not_person": 0}
|
| 25 |
EVAL_CLASSES = ["Person", "Not Person/Background"]
|
| 26 |
APP_VERSION = os.getenv("APP_VERSION", "v0.9.0")
|
| 27 |
+
APP_MINOR_UPDATE = os.getenv("APP_MINOR_UPDATE", "37")
|
| 28 |
|
| 29 |
|
| 30 |
# Gradio/Spaces can trigger noisy asyncio cleanup traces ("Invalid file descriptor: -1")
|
|
|
|
| 1662 |
)
|
| 1663 |
|
| 1664 |
|
| 1665 |
+
def _append_training_log(log_path: Path, message: str) -> None:
|
| 1666 |
+
with log_path.open("a", encoding="utf-8") as lf:
|
| 1667 |
+
lf.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}\n")
|
| 1668 |
+
|
| 1669 |
+
|
| 1670 |
+
def training_worker_main() -> None:
|
| 1671 |
+
base_dir = Path(os.environ["TRAIN_BASE_DIR"]).resolve()
|
| 1672 |
+
paths = _train_paths(base_dir)
|
| 1673 |
+
run_name = os.environ["TRAIN_RUN_NAME"]
|
| 1674 |
+
project_dir = Path(os.environ["TRAIN_PROJECT_DIR"])
|
| 1675 |
+
save_dir = Path(os.environ["TRAIN_SAVE_DIR"])
|
| 1676 |
+
results_csv = save_dir / "results.csv"
|
| 1677 |
+
train_model_choice = os.environ.get("TRAIN_MODEL_CHOICE", "YOLOv8n")
|
| 1678 |
+
|
| 1679 |
+
state: Dict[str, Any] = {
|
| 1680 |
+
"status": "running",
|
| 1681 |
+
"stage": "preparing_dataset",
|
| 1682 |
+
"run_name": run_name,
|
| 1683 |
+
"pid": os.getpid(),
|
| 1684 |
+
"save_dir": str(save_dir),
|
| 1685 |
+
"results_csv": str(results_csv),
|
| 1686 |
+
"started_at": datetime.now().isoformat(),
|
| 1687 |
+
}
|
| 1688 |
+
_write_status(paths["status"], state)
|
| 1689 |
+
|
| 1690 |
+
try:
|
| 1691 |
+
_append_training_log(paths["log"], f"Worker training aktif. PID={os.getpid()}")
|
| 1692 |
+
_append_training_log(paths["log"], "Preparing dataset dan pseudo-label di background...")
|
| 1693 |
+
data_yaml, prep_info = _prepare_training_dataset(
|
| 1694 |
+
dataset_root=base_dir / "dataset",
|
| 1695 |
+
use_pseudo_if_missing=_env_true("TRAIN_USE_PSEUDO", default=True),
|
| 1696 |
+
pseudo_conf=float(os.environ.get("TRAIN_PSEUDO_CONF", "0.30")),
|
| 1697 |
+
log_file=paths["log"],
|
| 1698 |
+
)
|
| 1699 |
+
|
| 1700 |
+
model_path = resolve_model_path("Best.pt" if train_model_choice == "Best.pt" else "YOLOv8n")
|
| 1701 |
+
state.update(
|
| 1702 |
+
{
|
| 1703 |
+
"stage": "training",
|
| 1704 |
+
"data_yaml": str(data_yaml),
|
| 1705 |
+
"model_path": str(model_path),
|
| 1706 |
+
}
|
| 1707 |
+
)
|
| 1708 |
+
_write_status(paths["status"], state)
|
| 1709 |
+
_append_training_log(paths["log"], f"Dataset siap: {prep_info}")
|
| 1710 |
+
_append_training_log(paths["log"], f"Start training {run_name}")
|
| 1711 |
+
_append_training_log(paths["log"], f"Model awal training: {train_model_choice} ({model_path})")
|
| 1712 |
+
|
| 1713 |
+
model = YOLO(str(model_path))
|
| 1714 |
+
res = model.train(
|
| 1715 |
+
data=str(data_yaml),
|
| 1716 |
+
epochs=int(os.environ["TRAIN_EPOCHS"]),
|
| 1717 |
+
imgsz=int(os.environ["TRAIN_IMGSZ"]),
|
| 1718 |
+
batch=int(os.environ["TRAIN_BATCH"]),
|
| 1719 |
+
device="cpu",
|
| 1720 |
+
project=str(project_dir),
|
| 1721 |
+
name=run_name,
|
| 1722 |
+
exist_ok=False,
|
| 1723 |
+
verbose=True,
|
| 1724 |
+
)
|
| 1725 |
+
|
| 1726 |
+
final_save = Path(str(res.save_dir))
|
| 1727 |
+
final_results_csv = final_save / "results.csv"
|
| 1728 |
+
best_pt = final_save / "weights" / "best.pt"
|
| 1729 |
+
best_out = base_dir / "best.pt"
|
| 1730 |
+
hf_sync_msg = "HF sync dilewati: best.pt tidak ditemukan."
|
| 1731 |
+
if best_pt.exists() and best_pt.is_file():
|
| 1732 |
+
shutil.copy2(best_pt, best_out)
|
| 1733 |
+
hf_sync_msg = sync_best_to_hf(best_out)
|
| 1734 |
+
|
| 1735 |
+
chart_file: str | None = None
|
| 1736 |
+
chart_src = final_save / "results.png"
|
| 1737 |
+
if chart_src.exists() and chart_src.is_file():
|
| 1738 |
+
paths["chart"].parent.mkdir(parents=True, exist_ok=True)
|
| 1739 |
+
shutil.copy2(chart_src, paths["chart"])
|
| 1740 |
+
chart_file = str(paths["chart"])
|
| 1741 |
+
elif final_results_csv.exists() and final_results_csv.is_file():
|
| 1742 |
+
chart_file = save_training_curve_image(plot_training_curves(final_results_csv), paths["chart"])
|
| 1743 |
+
|
| 1744 |
+
cm_file = save_training_confusion_matrix({"save_dir": str(final_save)}, paths["train_cm"])
|
| 1745 |
+
rows = _read_results_rows(final_results_csv if final_results_csv.exists() else None)
|
| 1746 |
+
final_state = {
|
| 1747 |
+
**state,
|
| 1748 |
+
"status": "done",
|
| 1749 |
+
"stage": "done",
|
| 1750 |
+
"ended_at": datetime.now().isoformat(),
|
| 1751 |
+
"save_dir": str(final_save),
|
| 1752 |
+
"results_csv": str(final_results_csv),
|
| 1753 |
+
"best_path": str(best_out if best_out.exists() else best_pt),
|
| 1754 |
+
"hf_sync_msg": hf_sync_msg,
|
| 1755 |
+
}
|
| 1756 |
+
summary_file = save_training_summary(paths, final_state, len(rows))
|
| 1757 |
+
artifact_sync_msg = sync_training_artifacts_to_hf(
|
| 1758 |
+
run_name=run_name,
|
| 1759 |
+
results_csv=final_results_csv if final_results_csv.exists() else None,
|
| 1760 |
+
log_file=paths["log"] if paths["log"].exists() else None,
|
| 1761 |
+
chart_file=Path(chart_file) if chart_file else None,
|
| 1762 |
+
confusion_matrix_file=Path(cm_file) if cm_file else None,
|
| 1763 |
+
summary_file=Path(summary_file),
|
| 1764 |
+
)
|
| 1765 |
+
final_state.update(
|
| 1766 |
+
{
|
| 1767 |
+
"artifacts_sync_attempted": True,
|
| 1768 |
+
"artifacts_sync_msg": artifact_sync_msg,
|
| 1769 |
+
}
|
| 1770 |
+
)
|
| 1771 |
+
save_training_summary(paths, final_state, len(rows))
|
| 1772 |
+
_write_status(paths["status"], final_state)
|
| 1773 |
+
_append_training_log(paths["log"], "TRAIN_DONE")
|
| 1774 |
+
except Exception as err:
|
| 1775 |
+
_append_training_log(paths["log"], f"TRAIN_FAILED: {err}")
|
| 1776 |
+
failed_state = {
|
| 1777 |
+
**state,
|
| 1778 |
+
"status": "failed",
|
| 1779 |
+
"stage": "failed",
|
| 1780 |
+
"ended_at": datetime.now().isoformat(),
|
| 1781 |
+
"error": str(err),
|
| 1782 |
+
}
|
| 1783 |
+
summary_file = save_training_summary(paths, failed_state, 0)
|
| 1784 |
+
artifact_sync_msg = sync_training_artifacts_to_hf(
|
| 1785 |
+
run_name=run_name,
|
| 1786 |
+
results_csv=None,
|
| 1787 |
+
log_file=paths["log"] if paths["log"].exists() else None,
|
| 1788 |
+
chart_file=paths["chart"] if paths["chart"].exists() else None,
|
| 1789 |
+
confusion_matrix_file=paths["train_cm"] if paths["train_cm"].exists() else None,
|
| 1790 |
+
summary_file=Path(summary_file),
|
| 1791 |
+
)
|
| 1792 |
+
failed_state.update(
|
| 1793 |
+
{
|
| 1794 |
+
"artifacts_sync_attempted": True,
|
| 1795 |
+
"artifacts_sync_msg": artifact_sync_msg,
|
| 1796 |
+
}
|
| 1797 |
+
)
|
| 1798 |
+
save_training_summary(paths, failed_state, 0)
|
| 1799 |
+
_write_status(paths["status"], failed_state)
|
| 1800 |
+
raise
|
| 1801 |
+
|
| 1802 |
+
|
| 1803 |
def run_training(
|
| 1804 |
train_model_choice: str,
|
| 1805 |
epochs: float,
|
|
|
|
| 1819 |
return poll_training_status()
|
| 1820 |
|
| 1821 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1822 |
run_name = f"classroom_train_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
| 1823 |
project_dir = base_dir / "runs_train"
|
| 1824 |
save_dir = project_dir / run_name
|
| 1825 |
results_csv = save_dir / "results.csv"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1826 |
env = os.environ.copy()
|
| 1827 |
env["TRAIN_BASE_DIR"] = str(base_dir)
|
| 1828 |
+
env["TRAIN_MODEL_CHOICE"] = str(train_model_choice)
|
|
|
|
| 1829 |
env["TRAIN_EPOCHS"] = str(int(epochs))
|
| 1830 |
env["TRAIN_IMGSZ"] = str(int(imgsz))
|
| 1831 |
env["TRAIN_BATCH"] = str(int(batch))
|
| 1832 |
+
env["TRAIN_USE_PSEUDO"] = "true" if use_pseudo_if_missing else "false"
|
| 1833 |
+
env["TRAIN_PSEUDO_CONF"] = str(float(pseudo_conf))
|
| 1834 |
env["TRAIN_PROJECT_DIR"] = str(project_dir)
|
| 1835 |
env["TRAIN_RUN_NAME"] = run_name
|
| 1836 |
env["TRAIN_SAVE_DIR"] = str(save_dir)
|
| 1837 |
env["TRAIN_STATUS_PATH"] = str(paths["status"])
|
|
|
|
| 1838 |
|
| 1839 |
with paths["log"].open("a", encoding="utf-8") as lf:
|
| 1840 |
+
lf.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Worker background dimulai: {run_name}\n")
|
| 1841 |
proc = subprocess.Popen(
|
| 1842 |
+
[sys.executable, "-u", __file__, "--train-worker"],
|
| 1843 |
stdout=lf,
|
| 1844 |
stderr=subprocess.STDOUT,
|
| 1845 |
cwd=str(base_dir),
|
|
|
|
| 1851 |
paths["status"],
|
| 1852 |
{
|
| 1853 |
"status": "running",
|
| 1854 |
+
"stage": "preparing_dataset",
|
| 1855 |
"run_name": run_name,
|
| 1856 |
"pid": proc.pid,
|
| 1857 |
"save_dir": str(save_dir),
|
|
|
|
| 1862 |
return poll_training_status()
|
| 1863 |
except Exception as err:
|
| 1864 |
with paths["log"].open("a", encoding="utf-8") as lf:
|
| 1865 |
+
lf.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Training gagal sebelum worker aktif: {err}\n")
|
| 1866 |
_write_status(
|
| 1867 |
paths["status"],
|
| 1868 |
{
|
|
|
|
| 2218 |
)
|
| 2219 |
|
| 2220 |
|
| 2221 |
+
if __name__ == "__main__":
|
| 2222 |
+
if len(sys.argv) > 1 and sys.argv[1] == "--train-worker":
|
| 2223 |
+
training_worker_main()
|
| 2224 |
+
raise SystemExit(0)
|
| 2225 |
+
|
| 2226 |
+
if _env_true("HF_AUTOSYNC_ENABLED", default=False):
|
| 2227 |
+
print(start_repo_autosync(Path(__file__).resolve().parent))
|
| 2228 |
+
else:
|
| 2229 |
print("HF autosync nonaktif (HF_AUTOSYNC_ENABLED=false).")
|
| 2230 |
demo.queue()
|
| 2231 |
demo.launch(css=CUSTOM_CSS, ssr_mode=False)
|
space_training_archive/-/training.log
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
[2026-05-01 16:09:37] Trigger training request
|
| 3 |
-
[2026-05-01 16:09:37] Preparing dataset...
|
| 4 |
-
[2026-05-01 16:09:37] Prepare split `train`: total gambar=80
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
space_training_archive/-/training_summary.json
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"saved_at": "2026-05-01T16:09:38.448561",
|
| 3 |
-
"status": "failed",
|
| 4 |
-
"stage": "preparing_dataset",
|
| 5 |
-
"run_name": "-",
|
| 6 |
-
"rows_count": 0,
|
| 7 |
-
"results_csv": null,
|
| 8 |
-
"best_path": null,
|
| 9 |
-
"hf_sync_msg": null,
|
| 10 |
-
"error": "Proses berhenti tanpa status akhir."
|
| 11 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training_outputs/training_summary_latest.json
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"saved_at": "2026-05-01T16:09:38.448561",
|
| 3 |
-
"status": "failed",
|
| 4 |
-
"stage": "preparing_dataset",
|
| 5 |
-
"run_name": "-",
|
| 6 |
-
"rows_count": 0,
|
| 7 |
-
"results_csv": null,
|
| 8 |
-
"best_path": null,
|
| 9 |
-
"hf_sync_msg": null,
|
| 10 |
-
"error": "Proses berhenti tanpa status akhir."
|
| 11 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|