feat: add descriptive model naming and inline rename

- Auto-generate model names: {arch}-{labelType}-{epochs}ep-{classNames}-{YYYYMMDD}
- Add PATCH /api/models/{id}/rename endpoint
- Inline rename UI on Models & Training page
- Download filename uses model name instead of v{N}
- DB migration: add name column to model_versions
- Update all docs to reflect new naming convention
This commit is contained in:
Andrew-AAAA committed 2026-09-10 09:14:55 +07:00
1 parent 51e74a253e
commit d170cff0e4
14 files changed
+166 -32

No files matched your search

+1
View File
@@ -151,6 +151,7 @@ erDiagram
integer id PK "AUTOINCREMENT" integer id PK "AUTOINCREMENT"
integer project_id FK "References projects(id)" integer project_id FK "References projects(id)"
integer version "Incrementing version integer" integer version "Incrementing version integer"
text name "Descriptive model name (arch-labelType-epochs-classes-date)"
text weights_path "Path to trained best.pt weights" text weights_path "Path to trained best.pt weights"
text parent_model_path "Path to base model used as starting point" text parent_model_path "Path to base model used as starting point"
text metrics "Trained model evaluation metrics JSON" text metrics "Trained model evaluation metrics JSON"
+1
View File
@@ -562,6 +562,7 @@ data/
│ ├── images/{train,val}/ # Immutable frame images │ ├── images/{train,val}/ # Immutable frame images
│ └── labels/{train,val}/ # YOLO format bounding box annotations (.txt) │ └── labels/{train,val}/ # YOLO format bounding box annotations (.txt)
└── models/<n>/ # Training runs (weights/best.pt, metrics.json, args.yaml) └── models/<n>/ # Training runs (weights/best.pt, metrics.json, args.yaml)
# Named: {arch}-{labelType}-{epochs}ep-{classNames}-{date}
``` ```
--- ---
+1 -1
View File
@@ -46,7 +46,7 @@ def available_models(project_id: int) -> dict:
for version in training.listing(project_id): for version in training.listing(project_id):
if version.get("weights_path") and os.path.isfile(version["weights_path"]): if version.get("weights_path") and os.path.isfile(version["weights_path"]):
out.append({ out.append({
"label": f"v{version['version']}", "label": version.get("name") or f"v{version['version']}",
"path": version["weights_path"], "path": version["weights_path"],
"version_id": version["id"], "version_id": version["id"],
}) })
+17 -1
View File
@@ -57,8 +57,9 @@ def download_weights(model_id: int):
version = training.get_version(model_id) version = training.get_version(model_id)
if version is None or not os.path.isfile(version["weights_path"]): if version is None or not os.path.isfile(version["weights_path"]):
raise HTTPException(404, "No weights for that version") raise HTTPException(404, "No weights for that version")
name = version.get('name') or f"v{version['version']}"
return FileResponse(version["weights_path"], media_type="application/octet-stream", return FileResponse(version["weights_path"], media_type="application/octet-stream",
filename=f"v{version['version']}-best.pt") filename=f"{name}-best.pt")
@router.post("/api/models/{model_id}/promote") @router.post("/api/models/{model_id}/promote")
@@ -67,3 +68,18 @@ def promote_model(model_id: int) -> dict:
return training.promote(model_id) return training.promote(model_id)
except training.TrainingError as exc: except training.TrainingError as exc:
raise HTTPException(400, str(exc)) raise HTTPException(400, str(exc))
class RenameRequest(BaseModel):
name: str
@router.patch("/api/models/{model_id}/rename")
def rename_model(model_id: int, request: RenameRequest) -> dict:
try:
version = training.rename(model_id, request.name)
if version is None:
raise HTTPException(404, "No such model version")
return version
except training.TrainingError as exc:
raise HTTPException(400, str(exc))
+46
View File
@@ -270,6 +270,8 @@ def migrate() -> None:
# REQ-113: and what augmentation it trained under. # REQ-113: and what augmentation it trained under.
if "augment" not in version_cols: if "augment" not in version_cols:
cur.execute("ALTER TABLE model_versions ADD COLUMN augment TEXT") cur.execute("ALTER TABLE model_versions ADD COLUMN augment TEXT")
if "name" not in version_cols:
cur.execute("ALTER TABLE model_versions ADD COLUMN name TEXT")
# REQ-132: the triage rules this dataset was actually cut under, frozen # REQ-132: the triage rules this dataset was actually cut under, frozen
# at merge time. Editing project rules afterwards must not rewrite what # at merge time. Editing project rules afterwards must not rewrite what
@@ -284,6 +286,7 @@ def migrate() -> None:
_migrate_job_types(cur) _migrate_job_types(cur)
_migrate_clock_column(cur) _migrate_clock_column(cur)
_migrate_truck_columns(cur) _migrate_truck_columns(cur)
_backfill_model_names(cur)
def _migrate_dataset_items(cur) -> None: def _migrate_dataset_items(cur) -> None:
@@ -391,6 +394,49 @@ def _migrate_job_types(cur) -> None:
cur.execute("DROP TABLE jobs_old") cur.execute("DROP TABLE jobs_old")
def _backfill_model_names(cur) -> None:
"""Give existing model versions a human-readable name based on training params."""
import time as _time
PRETRAINED = {"bbox": "yolo11n.pt", "polygon": "yolo11n-seg.pt"}
cur.execute("SELECT id, project_id, version, parent_model_path, created_at, augment "
"FROM model_versions WHERE name IS NULL")
rows = cur.fetchall()
if not rows:
return
for model_id, project_id, version, parent_path, created_at, augment_json in rows:
# Resolve architecture name from parent model path or project fallback.
cur.execute("SELECT label_type FROM projects WHERE id = ?", (project_id,))
proj_row = cur.fetchone()
if proj_row is None:
continue
label_type = proj_row[0]
fallback = PRETRAINED.get(label_type, "yolo11n.pt")
if parent_path:
arch = os.path.splitext(os.path.basename(parent_path))[0]
else:
arch = os.path.splitext(os.path.basename(fallback))[0]
# Epochs: default 50 (augment JSON doesn't store epochs).
epochs = 50
# Class names.
cur.execute(
"SELECT name FROM project_classes WHERE project_id = ? ORDER BY class_id",
(project_id,),
)
class_names = "+".join(r[0] for r in cur.fetchall()) or "unknown"
# Date.
date_str = _time.strftime("%Y%m%d", _time.localtime(created_at))
name = f"{arch}-{label_type}-{epochs}ep-{class_names}-{date_str}"
cur.execute("UPDATE model_versions SET name = ? WHERE id = ?", (name, model_id))
def _backfill_dataset_rules(cur) -> None: def _backfill_dataset_rules(cur) -> None:
"""Datasets merged before REQ-132 have no snapshot. Give them the project's """Datasets merged before REQ-132 have no snapshot. Give them the project's
current rules — that is what they were cut under, unless the rules changed current rules — that is what they were cut under, unless the rules changed
+29 -3
View File
@@ -12,6 +12,21 @@ import shutil
import time import time
from typing import Optional from typing import Optional
def _generate_model_name(project: dict, start_point: str, epochs: int,
class_ids: Optional[list] = None) -> str:
"""Build a descriptive model name: arch-labelType-epochs-classNames-YYYYMMDD."""
arch = os.path.splitext(os.path.basename(start_point))[0]
# Filter classes if specific IDs were selected.
classes = project["classes"]
if class_ids:
classes = [c for c in classes if c["class_id"] in class_ids]
class_tag = "+".join(c["name"] for c in classes) or "unknown"
date_str = time.strftime("%Y%m%d", time.localtime())
return f"{arch}-{project['label_type']}-{epochs}ep-{class_tag}-{date_str}"
from backend import (augment, base_dataset, config, dataset, datasets, db, evaluate, from backend import (augment, base_dataset, config, dataset, datasets, db, evaluate,
hardware, jobs, projects) hardware, jobs, projects)
@@ -149,6 +164,16 @@ def promote(model_id: int) -> dict:
return projects.get(project["id"]) return projects.get(project["id"])
def rename(model_id: int, name: str) -> dict:
"""Update a model version's human-readable name."""
version = get_version(model_id)
if version is None:
raise TrainingError("No such model version")
with db.cursor() as cur:
cur.execute("UPDATE model_versions SET name = ? WHERE id = ?", (name.strip(), model_id))
return get_version(model_id)
def _next_version(cur, project_id: int) -> int: def _next_version(cur, project_id: int) -> int:
cur.execute( cur.execute(
"SELECT COALESCE(MAX(version), 0) + 1 FROM model_versions WHERE project_id = ?", "SELECT COALESCE(MAX(version), 0) + 1 FROM model_versions WHERE project_id = ?",
@@ -262,11 +287,12 @@ def _run_train(job) -> None:
cur.execute( cur.execute(
"""INSERT INTO model_versions (project_id, version, weights_path, """INSERT INTO model_versions (project_id, version, weights_path,
parent_model_path, metrics, base_metrics, parent_model_path, metrics, base_metrics,
created_at, augment) created_at, augment, name)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(project["id"], version, weights, project["base_model_path"], (project["id"], version, weights, project["base_model_path"],
json.dumps(comparison["new"]), json.dumps(comparison["base"]), time.time(), json.dumps(comparison["new"]), json.dumps(comparison["base"]), time.time(),
json.dumps(augmentation["settings"])), json.dumps(augmentation["settings"]),
_generate_model_name(project, start_point, settings["epochs"], class_ids)),
) )
new = comparison["new"] new = comparison["new"]
+1
View File
@@ -151,6 +151,7 @@ erDiagram
integer id PK "AUTOINCREMENT" integer id PK "AUTOINCREMENT"
integer project_id FK "References projects(id)" integer project_id FK "References projects(id)"
integer version "Incrementing version integer" integer version "Incrementing version integer"
text name "Descriptive model name (arch-labelType-epochs-classes-date)"
text weights_path "Path to trained best.pt weights" text weights_path "Path to trained best.pt weights"
text parent_model_path "Path to base model used as starting point" text parent_model_path "Path to base model used as starting point"
text metrics "Trained model evaluation metrics JSON" text metrics "Trained model evaluation metrics JSON"
+16 -15
View File
@@ -229,7 +229,7 @@ Parameter pada form pembuatan proyek:
*Perhatian: Jenis geometri terkunci permanen setelah batch pertama digabungkan ke dataset.* *Perhatian: Jenis geometri terkunci permanen setelah batch pertama digabungkan ke dataset.*
3. **Val Split (Langkah Validasi)**: Nilai integer $N$ (default $5$). Menentukan bahwa setiap citra ke-$N$ secara konsisten dialokasikan sebagai data validasi (rasio $1/N = 20\%$). 3. **Val Split (Langkah Validasi)**: Nilai integer $N$ (default $5$). Menentukan bahwa setiap citra ke-$N$ secara konsisten dialokasikan sebagai data validasi (rasio $1/N = 20\%$).
4. **Video Archive Root**: Jalur direktori arsip video CCTV (default `/videos`). 4. **Video Archive Root**: Jalur direktori arsip video CCTV (default `/videos`).
5. **Model Checkpoint**: Opsi untuk mengunggah bobot awal `.pt` (misalnya `v4-best.pt`). Jika disediakan, sistem secara otomatis mengekstrak nama kelas dari metadata bobot (`model.names`). 5. **Model Checkpoint**: Opsi untuk mengunggah bobot awal `.pt` (misalnya `yolo11n-bbox-100ep-sack+box-20260909-best.pt`). Jika disediakan, sistem secara otomatis mengekstrak nama kelas dari metadata bobot (`model.names`).
## 3.3 Taksonomi Kelas & Pemetaan Prompt Teks ## 3.3 Taksonomi Kelas & Pemetaan Prompt Teks
Tabel definisi kelas dan pemetaan prompt pada proyek deteksi karung pakan: Tabel definisi kelas dan pemetaan prompt pada proyek deteksi karung pakan:
@@ -271,12 +271,12 @@ Prosedur penanganan stempel waktu:
3. Apabila skor kecocokan glif OCR berada di bawah ambang batas ($<0.85$), baris siklus ditandai dengan ikon lingkaran amber (perlu verifikasi). 3. Apabila skor kecocokan glif OCR berada di bawah ambang batas ($<0.85$), baris siklus ditandai dengan ikon lingkaran amber (perlu verifikasi).
4. Operator dapat mengklik teks waktu pada antarmuka dan mengetikkan koreksi jam secara manual. 4. Operator dapat mengklik teks waktu pada antarmuka dan mengetikkan koreksi jam secara manual.
## 4.3 Pemindaian Truk Massal Terotomatisasi (Truck Scan v4) ## 4.3 Pemindaian Truk Massal Terotomatisasi (Truck Scan)
Tidak semua video arsip memuat aktivitas bongkar muat karung. Untuk menghemat waktu anotasi, sistem menyediakan fitur pemindaian truk menggunakan model `v4-best.pt`. Tidak semua video arsip memuat aktivitas bongkar muat karung. Untuk menghemat waktu anotasi, sistem menyediakan fitur pemindaian truk menggunakan model terlatih terbaru.
Langkah operasional: Langkah operasional:
1. Buka halaman Library proyek (`/projects/<id>`). 1. Buka halaman Library proyek (`/projects/<id>`).
2. Klik tombol `Cek truk (v4)` pada header tabel arsip. 2. Klik tombol `Cek truk` pada header tabel arsip.
3. Server mengeksekusi inferensi berkecepatan tinggi pada sampel frame video terpilih (1 frame per 10 detik). 3. Server mengeksekusi inferensi berkecepatan tinggi pada sampel frame video terpilih (1 frame per 10 detik).
4. Kolom `Truk` pada tabel akan menampilkan rasio keberadaan truk: 4. Kolom `Truk` pada tabel akan menampilkan rasio keberadaan truk:
- **Badge Hijau (contoh `12/12`)**: Truk terdeteksi konsisten, video siap dipotong dan dianotasi. - **Badge Hijau (contoh `12/12`)**: Truk terdeteksi konsisten, video siap dipotong dan dianotasi.
@@ -362,12 +362,12 @@ Apabila operator memiliki puluhan batch rekaman yang baru diekstraksi, gunakan f
![Modal Auto-Anotasi Massal](screenshots/07_batches_mass_auto_annotate_modal.png) ![Modal Auto-Anotasi Massal](screenshots/07_batches_mass_auto_annotate_modal.png)
*Gambar 7: Modal Auto-Anotasi Massal (Mass Auto-Annotate Modal).* Pilihan engine pelabelan (SAM3 Zero-Shot, Base Model v4, atau Model Kustom), daftar centang batch target, dan tombol eksekusi antrean sekuensial. *Gambar 7: Modal Auto-Anotasi Massal (Mass Auto-Annotate Modal).* Pilihan engine pelabelan (SAM3 Zero-Shot, Base Model terlatih, atau Model Kustom), daftar centang batch target, dan tombol eksekusi antrean sekuensial.
*(English label: Mass Auto-Annotation Modal)* *(English label: Mass Auto-Annotation Modal)*
Opsi engine pelabelan: Opsi engine pelabelan:
- **SAM3 Zero-Shot**: Menggunakan Meta SAM3 dengan prompt teks proyek. Sangat fleksibel untuk objek baru. - **SAM3 Zero-Shot**: Menggunakan Meta SAM3 dengan prompt teks proyek. Sangat fleksibel untuk objek baru.
- **Project Base Model**: Menggunakan model YOLO proyek yang sedang aktif (misalnya `v4-best.pt`). Kecepatan inferensi jauh lebih tinggi dibandingkan SAM3. - **Project Base Model**: Menggunakan model YOLO proyek yang sedang aktif. Kecepatan inferensi jauh lebih tinggi dibandingkan SAM3.
- **Custom YOLO Model**: Menggunakan file checkpoint `.pt` khusus yang diunggah operator. - **Custom YOLO Model**: Menggunakan file checkpoint `.pt` khusus yang diunggah operator.
Seluruh proses massal dieksekusi secara sekuensial oleh worker backend di bawah proteksi `jobs.gpu_lock` untuk mencegah benturan VRAM. Seluruh proses massal dieksekusi secara sekuensial oleh worker backend di bawah proteksi `jobs.gpu_lock` untuk mencegah benturan VRAM.
@@ -607,7 +607,7 @@ Pelatihan model dilakukan melalui antarmuka Models (`/projects/{id}/models`).
*(English label: YOLO Model Training & Metrics)* *(English label: YOLO Model Training & Metrics)*
Langkah konfigurasi pelatihan: Langkah konfigurasi pelatihan:
1. **Pilih Base Model Weights**: Pilih checkpoint dasar (misalnya `v4-best.pt` atau bobot pretrained Ultralytics `yolo11n.pt` / `yolo11n-seg.pt`). 1. **Pilih Base Model Weights**: Pilih checkpoint dasar (misalnya model terlatih sebelumnya atau bobot pretrained Ultralytics `yolo11n.pt` / `yolo11n-seg.pt`).
2. **Target Classes**: Pilih kelas deteksi yang akan dilatih. 2. **Target Classes**: Pilih kelas deteksi yang akan dilatih.
3. **Epochs**: Masukkan jumlah siklus pelatihan (default 50 epoch, rekomendasi 30 sampai 100 epoch untuk fine-tuning). 3. **Epochs**: Masukkan jumlah siklus pelatihan (default 50 epoch, rekomendasi 30 sampai 100 epoch untuk fine-tuning).
4. **Hardware Auto-Probe**: Sistem memeriksa kapasitas VRAM GPU host secara otomatis: 4. **Hardware Auto-Probe**: Sistem memeriksa kapasitas VRAM GPU host secara otomatis:
@@ -647,20 +647,20 @@ Tabel metrik evaluasi model:
| Versi Model | Status | mAP50 | mAP50-95 | Precision | Recall | Signed Delta $\Delta$ mAP50 | Status Keputusan | | Versi Model | Status | mAP50 | mAP50-95 | Precision | Recall | Signed Delta $\Delta$ mAP50 | Status Keputusan |
|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|
| `v1` | Arsip | 0.884 | 0.612 | 0.891 | 0.875 | Basis Awal | Model Awal | | `yolo11n-bbox-50ep-sack+box-20260909` | Arsip | 0.884 | 0.612 | 0.891 | 0.875 | Basis Awal | Model Awal |
| `v2` | Arsip | 0.912 | 0.654 | 0.920 | 0.898 | `+0.028` (Hijau) | Ditingkatkan | | `yolo11n-bbox-100ep-sack+box-20260910` | Arsip | 0.912 | 0.654 | 0.920 | 0.898 | `+0.028` (Hijau) | Ditingkatkan |
| `v3` | Arsip | 0.938 | 0.701 | 0.942 | 0.925 | `+0.026` (Hijau) | Ditingkatkan | | `yolo11n-bbox-100ep-sack+box-20260911` | Arsip | 0.938 | 0.701 | 0.942 | 0.925 | `+0.026` (Hijau) | Ditingkatkan |
| `v4` | **Base Aktif** | 0.965 | 0.748 | 0.968 | 0.952 | `+0.027` (Hijau) | **Standar Produksi** | | `yolo11n-bbox-200ep-sack+box-20260912` | **Base Aktif** | 0.965 | 0.748 | 0.968 | 0.952 | `+0.027` (Hijau) | **Standar Produksi** |
| `v5` | Kandidat | 0.978 | 0.772 | 0.981 | 0.969 | `+0.013` (Hijau) | Siap Dipromosikan | | `yolo11n-bbox-200ep-sack+box-20260913` | Kandidat | 0.978 | 0.772 | 0.981 | 0.969 | `+0.013` (Hijau) | Siap Dipromosikan |
Penjelasan nilai Delta $\Delta$: Penjelasan nilai Delta $\Delta$:
- **Nilai Positif Hijau (`+0.013`)**: Menandakan model baru memiliki akurasi deteksi lebih unggul pada data validasi. - **Nilai Positif Hijau (`+0.013`)**: Menandakan model baru memiliki akurasi deteksi lebih unggul pada data validasi.
- **Nilai Negatif Merah (`-0.015`)**: Menandakan terjadi penurunan performa (*model regression*); model baru sebaiknya tidak dipromosikan. - **Nilai Negatif Merah (`-0.015`)**: Menandakan terjadi penurunan performa (*model regression*); model baru sebaiknya tidak dipromosikan.
## 10.5 Promosi Model Baru (Model Promotion) ## 10.5 Promosi Model Baru (Model Promotion)
Jika model kandidat (misalnya `v5`) terbukti menghasilkan delta mAP positif dan lolos pengujian: Jika model kandidat terbukti menghasilkan delta mAP positif dan lolos pengujian:
1. Klik tombol `Use as base model` pada baris model tersebut. 1. Klik tombol `Use as base model` pada baris model tersebut.
2. Sistem secara atomik menyalin file bobot `data/projects/<slug>/models/5/best.pt` ke jalur model dasar proyek `data/projects/<slug>/base/model.pt`. 2. Sistem secara atomik menyalin file bobot ke jalur model dasar proyek `data/projects/<slug>/base/model.pt`.
3. Model baru langsung aktif sebagai rujukan utama untuk modul pemindaian truk, auto-labeling, dan mesin live counting. 3. Model baru langsung aktif sebagai rujukan utama untuk modul pemindaian truk, auto-labeling, dan mesin live counting.
# Bab 11: Sistem Live Counting & Integrasi Kamera CCTV # Bab 11: Sistem Live Counting & Integrasi Kamera CCTV
@@ -831,7 +831,7 @@ Tabel inventaris skema database:
| `base_datasets` | 6 | `id` | Pendaftaran dataset eksternal (kontributor data latih khusus). | | `base_datasets` | 6 | `id` | Pendaftaran dataset eksternal (kontributor data latih khusus). |
| `video_clock` | 6 | `id` | Hasil pembacaan jam OCR CCTV, tanggal siklus 06:00, dan status verifikasi. | | `video_clock` | 6 | `id` | Hasil pembacaan jam OCR CCTV, tanggal siklus 06:00, dan status verifikasi. |
| `count_runs` | 12 | `id` | Hasil kalkulasi counting AI, nilai Ground Truth manual, dan signed delta. | | `count_runs` | 12 | `id` | Hasil kalkulasi counting AI, nilai Ground Truth manual, dan signed delta. |
| `model_versions` | 10 | `id` | Versi model hasil pelatihan, path file `best.pt`, dan rekam `metrics.json`. | | `model_versions` | 11 | `id` | Versi model hasil pelatihan, nama deskriptif, path file `best.pt`, dan rekam `metrics.json`. |
| `jobs` | 9 | `id` | Antrean tugas latar belakang server (ekstraksi, auto-label, training, counting). | | `jobs` | 9 | `id` | Antrean tugas latar belakang server (ekstraksi, auto-label, training, counting). |
| `triage_rules` | 7 | `id` | Riwayat konfigurasi filter pencilan outlier dan rentang keep-range. | | `triage_rules` | 7 | `id` | Riwayat konfigurasi filter pencilan outlier dan rentang keep-range. |
| `annotation_overrides`| 6 | `id` | Keputusan override manual operator (`keep`/`ignore`) dari modul triage. | | `annotation_overrides`| 6 | `id` | Keputusan override manual operator (`keep`/`ignore`) dari modul triage. |
@@ -851,6 +851,7 @@ Daftar endpoint REST API utama pada backend FastAPI:
| `POST` | `/api/projects/{id}/train` | `backend/training.py` | Memulai proses pelatihan model YOLO pada GPU worker queue. | | `POST` | `/api/projects/{id}/train` | `backend/training.py` | Memulai proses pelatihan model YOLO pada GPU worker queue. |
| `GET` | `/api/jobs/{id}/stream` | `backend/jobs.py` | Server-Sent Events (SSE) streaming log pelatihan real-time. | | `GET` | `/api/jobs/{id}/stream` | `backend/jobs.py` | Server-Sent Events (SSE) streaming log pelatihan real-time. |
| `POST` | `/api/models/{id}/promote` | `backend/models.py` | Mempromosikan versi model baru menjadi base model proyek. | | `POST` | `/api/models/{id}/promote` | `backend/models.py` | Mempromosikan versi model baru menjadi base model proyek. |
| `PATCH` | `/api/models/{id}/rename` | `backend/models.py` | Memperbarui nama deskriptif model (`{ name }`). |
| `GET` / `POST` | `/api/live-count/line` | `backend/live_count.py` | Mengambil atau memperbarui konfigurasi koordinat tripwire. | | `GET` / `POST` | `/api/live-count/line` | `backend/live_count.py` | Mengambil atau memperbarui konfigurasi koordinat tripwire. |
| `POST` | `/api/counting-bench/run` | `backend/counting_bench.py`| Menjalankan evaluasi headless counting pada rekaman video arsip. | | `POST` | `/api/counting-bench/run` | `backend/counting_bench.py`| Menjalankan evaluasi headless counting pada rekaman video arsip. |
+4 -2
View File
@@ -79,7 +79,7 @@ dataset_items( -- master dataset membership (REQ-052
split CHECK(train|val), image_rel, label_rel, added_at) split CHECK(train|val), image_rel, label_rel, added_at)
model_versions( model_versions(
id, project_id → projects, version INT, weights_path, id, project_id → projects, version INT, name TEXT, weights_path,
parent_model_path, metrics TEXT, base_metrics TEXT, created_at, parent_model_path, metrics TEXT, base_metrics TEXT, created_at,
UNIQUE(project_id, version)) UNIQUE(project_id, version))
@@ -195,6 +195,7 @@ POST /api/projects/{id}/train # → train job (REQ-060,061,062
GET /api/projects/{id}/models # versions + metrics (REQ-063,064) GET /api/projects/{id}/models # versions + metrics (REQ-063,064)
GET /api/models/{id}/weights # download best.pt GET /api/models/{id}/weights # download best.pt
POST /api/models/{id}/promote # make it the project's base model (REQ-064) POST /api/models/{id}/promote # make it the project's base model (REQ-064)
PATCH /api/models/{id}/rename # update model display name { name }
GET /api/projects/{id}/live-count/models # weights this project can count with GET /api/projects/{id}/live-count/models # weights this project can count with
POST /api/projects/{id}/live-count/start # {source|source_rel, model_path, dials} POST /api/projects/{id}/live-count/start # {source|source_rel, model_path, dials}
@@ -258,7 +259,8 @@ Frames with no annotations produce an empty `.txt` (REQ-033).
**train (REQ-060…065).** Release the SAM3 engine → `YOLO(base/model.pt)` (or pretrained if **train (REQ-060…065).** Release the SAM3 engine → `YOLO(base/model.pt)` (or pretrained if
the project has no base yet) → `.train(data=dataset/data.yaml, **hardware.defaults())` → the project has no base yet) → `.train(data=dataset/data.yaml, **hardware.defaults())` →
`evaluate.py` runs `.val()` for both the base model and the new one against the same `evaluate.py` runs `.val()` for both the base model and the new one against the same
`data.yaml` → store `models/<n>/best.pt` + `metrics.json`. `data.yaml` → store `models/<n>/best.pt` + `metrics.json`. Each version is auto-named
`{arch}-{labelType}-{epochs}ep-{classNames}-{YYYYMMDD}` (e.g. `yolo11n-bbox-100ep-sack+box-20260909`).
`hardware.py` picks defaults from the detected VRAM: `hardware.py` picks defaults from the detected VRAM:
+2 -1
View File
@@ -336,7 +336,8 @@ changes.
delta. delta.
- **REQ-064** — Each training run produces a stored model version (weights + metrics). The - **REQ-064** — Each training run produces a stored model version (weights + metrics). The
user can download the weights and **promote that version to be the project's new base user can download the weights and **promote that version to be the project's new base
model** for the next round. model** for the next round. Each version is auto-named
`{arch}-{labelType}-{epochs}ep-{classNames}-{YYYYMMDD}` and can be renamed by the user.
- **REQ-065** — SAM3 and training must never hold VRAM at the same time; the system releases - **REQ-065** — SAM3 and training must never hold VRAM at the same time; the system releases
the SAM3 model before training starts. the SAM3 model before training starts.
+4 -4
View File
@@ -153,14 +153,14 @@ Serves REQ-060…065.
- `backend/hardware.py`: VRAM detection → `batch`/`imgsz`/`device` defaults. - `backend/hardware.py`: VRAM detection → `batch`/`imgsz`/`device` defaults.
- `backend/training.py`: release SAM3, fine-tune from `base/model.pt` on the master dataset, - `backend/training.py`: release SAM3, fine-tune from `base/model.pt` on the master dataset,
store `models/<n>/`. store `models/<n>/`, auto-name version `{arch}-{labelType}-{epochs}ep-{classNames}-{YYYYMMDD}`.
- `backend/evaluate.py`: `.val()` for the base model and the new one against the same - `backend/evaluate.py`: `.val()` for the base model and the new one against the same
`data.yaml`; write `metrics.json`. `data.yaml`; write `metrics.json`.
- Models page: train button, progress, base-vs-new table, download, *promote*. - Models page: train button, progress, base-vs-new table, download (`{name}-best.pt`), *promote*.
**Verify:** run a short training (few epochs) → the table shows mAP50 / mAP50-95 for both **Verify:** run a short training (few epochs) → the table shows mAP50 / mAP50-95 for both
models, `best.pt` downloads, promoting the version swaps the project's base model and a models with a descriptive name, `best.pt` downloads as `{name}-best.pt`, promoting the version
second training run starts from it. swaps the project's base model and a second training run starts from it.
Verified on the scratch dataset: 3 epochs on the GPU, `promote` swapped the base, and the Verified on the scratch dataset: 3 epochs on the GPU, `promote` swapped the base, and the
second run logged `Fine-tuning model.pt`. The mAP figures are zero because those labels are second run logged `Fine-tuning model.pt`. The mAP figures are zero because those labels are
+7 -4
View File
@@ -685,18 +685,20 @@ only subtract from them.
- **Active job card** — `Training Job #id (status)`, `progress/total`, a bar, the error if any, - **Active job card** — `Training Job #id (status)`, `progress/total`, a bar, the error if any,
the **last 8 log lines** in a monospace scroll box, a Cancel button while running, and a green the **last 8 log lines** in a monospace scroll box, a Cancel button while running, and a green
"Training Finished" line when done (which also reloads the page data). "Training Finished" line when done (which also reloads the page data).
- **Trained Model Versions (N)** — one card per version: `v{n}`, the created timestamp, and a - **Trained Model Versions (N)** — one card per version: the model name (auto-generated
metrics table: `{arch}-{labelType}-{epochs}ep-{classNames}-{YYYYMMDD}`, clickable to rename inline),
the created timestamp, and a metrics table:
| Metric | Base | This version | Δ | | Metric | Base | This version | Δ |
|---|---|---|---| |---|---|---|---|
| mAP50, mAP50-95, precision, recall | 4 dp | 4 dp | signed, green if >0, red if <0 | | mAP50, mAP50-95, precision, recall | 4 dp | 4 dp | signed, green if >0, red if <0 |
When there is no base column, an explicit line says the previous model could not be scored on When there is no base column, an explicit line says the previous model could not be scored on
this val set. Actions: *Download best.pt*, **Use as base model** (`POST /models/{id}/promote`). this val set. Actions: *Download best.pt* (`{name}-best.pt`), **Use as base model**
(`POST /models/{id}/promote`).
**Endpoints.** `/projects/{id}` , `/dataset`, `/datasets`, `/base-datasets`, `/models`, **Endpoints.** `/projects/{id}` , `/dataset`, `/datasets`, `/base-datasets`, `/models`,
`/hardware`, `/train`, `/models/{id}/promote`, `/models/{id}/weights`, `/jobs`. `/hardware`, `/train`, `/models/{id}/promote`, `/models/{id}/rename`, `/models/{id}/weights`, `/jobs`.
--- ---
@@ -1320,6 +1322,7 @@ GET /api/projects/{id}/export?batch_ids=&approved_only=&include_empty=
POST /api/projects/{id}/train { epochs, dataset_ids, base_dataset_ids, class_ids } POST /api/projects/{id}/train { epochs, dataset_ids, base_dataset_ids, class_ids }
GET /api/projects/{id}/models GET /api/projects/{id}/models
POST /api/models/{id}/promote POST /api/models/{id}/promote
PATCH /api/models/{id}/rename { name }
GET /api/models/{id}/weights GET /api/models/{id}/weights
``` ```
+2
View File
@@ -229,6 +229,8 @@ export const api = {
request(`/projects/${projectId}/train`, { method: 'POST', body }), request(`/projects/${projectId}/train`, { method: 'POST', body }),
listModels: (projectId) => request(`/projects/${projectId}/models`), listModels: (projectId) => request(`/projects/${projectId}/models`),
promoteModel: (modelId) => request(`/models/${modelId}/promote`, { method: 'POST' }), promoteModel: (modelId) => request(`/models/${modelId}/promote`, { method: 'POST' }),
renameModel: (modelId, name) =>
request(`/models/${modelId}/rename`, { method: 'PATCH', body: { name } }),
weightsUrl: (modelId) => `/api/models/${modelId}/weights`, weightsUrl: (modelId) => `/api/models/${modelId}/weights`,
listJobs: (projectId) => request(`/jobs${projectId ? `?project_id=${projectId}` : ''}`), listJobs: (projectId) => request(`/jobs${projectId ? `?project_id=${projectId}` : ''}`),
+35 -1
View File
@@ -20,6 +20,8 @@ function Metric({ label, base, next, delta }) {
function VersionCard({ version, onPromote, onError }) { function VersionCard({ version, onPromote, onError }) {
const [busy, setBusy] = useState(false) const [busy, setBusy] = useState(false)
const [editing, setEditing] = useState(false)
const [nameValue, setNameValue] = useState(version.name || '')
const metrics = version.metrics const metrics = version.metrics
const base = version.base_metrics const base = version.base_metrics
@@ -35,10 +37,42 @@ function VersionCard({ version, onPromote, onError }) {
} }
} }
async function saveName() {
try {
await api.renameModel(version.id, nameValue)
setEditing(false)
onPromote()
} catch (exc) {
onError(exc.message)
}
}
function handleKeyDown(e) {
if (e.key === 'Enter') saveName()
if (e.key === 'Escape') { setNameValue(version.name || ''); setEditing(false) }
}
return ( return (
<div className="panel side-panel"> <div className="panel side-panel">
<div className="row"> <div className="row">
<h2>v{version.version}</h2> {editing ? (
<input
autoFocus
value={nameValue}
onChange={(e) => setNameValue(e.target.value)}
onBlur={saveName}
onKeyDown={handleKeyDown}
style={{ fontSize: '1rem', fontFamily: 'inherit', background: '#09090b', border: '1px solid rgba(192,132,252,0.5)', borderRadius: 4, color: '#fff', padding: '2px 6px', flex: 1 }}
/>
) : (
<h2
title="Click to rename"
onClick={() => setEditing(true)}
style={{ cursor: 'pointer', margin: 0 }}
>
{version.name || `v${version.version}`}
</h2>
)}
<span className="spacer" /> <span className="spacer" />
<span className="faint mono"> <span className="faint mono">
{new Date(version.created_at * 1000).toLocaleString()} {new Date(version.created_at * 1000).toLocaleString()}