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:
1 parent
51e74a253e
commit
d170cff0e4
14 files changed
+166
-32
No files matched your search
@@ -151,6 +151,7 @@ erDiagram
|
||||
integer id PK "AUTOINCREMENT"
|
||||
integer project_id FK "References projects(id)"
|
||||
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 parent_model_path "Path to base model used as starting point"
|
||||
text metrics "Trained model evaluation metrics JSON"
|
||||
|
||||
@@ -562,6 +562,7 @@ data/
|
||||
│ ├── images/{train,val}/ # Immutable frame images
|
||||
│ └── labels/{train,val}/ # YOLO format bounding box annotations (.txt)
|
||||
└── models/<n>/ # Training runs (weights/best.pt, metrics.json, args.yaml)
|
||||
# Named: {arch}-{labelType}-{epochs}ep-{classNames}-{date}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -46,7 +46,7 @@ def available_models(project_id: int) -> dict:
|
||||
for version in training.listing(project_id):
|
||||
if version.get("weights_path") and os.path.isfile(version["weights_path"]):
|
||||
out.append({
|
||||
"label": f"v{version['version']}",
|
||||
"label": version.get("name") or f"v{version['version']}",
|
||||
"path": version["weights_path"],
|
||||
"version_id": version["id"],
|
||||
})
|
||||
|
||||
+17
-1
@@ -57,8 +57,9 @@ def download_weights(model_id: int):
|
||||
version = training.get_version(model_id)
|
||||
if version is None or not os.path.isfile(version["weights_path"]):
|
||||
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",
|
||||
filename=f"v{version['version']}-best.pt")
|
||||
filename=f"{name}-best.pt")
|
||||
|
||||
|
||||
@router.post("/api/models/{model_id}/promote")
|
||||
@@ -67,3 +68,18 @@ def promote_model(model_id: int) -> dict:
|
||||
return training.promote(model_id)
|
||||
except training.TrainingError as 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))
|
||||
@@ -270,6 +270,8 @@ def migrate() -> None:
|
||||
# REQ-113: and what augmentation it trained under.
|
||||
if "augment" not in version_cols:
|
||||
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
|
||||
# at merge time. Editing project rules afterwards must not rewrite what
|
||||
@@ -284,6 +286,7 @@ def migrate() -> None:
|
||||
_migrate_job_types(cur)
|
||||
_migrate_clock_column(cur)
|
||||
_migrate_truck_columns(cur)
|
||||
_backfill_model_names(cur)
|
||||
|
||||
|
||||
def _migrate_dataset_items(cur) -> None:
|
||||
@@ -391,6 +394,49 @@ def _migrate_job_types(cur) -> None:
|
||||
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:
|
||||
"""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
|
||||
|
||||
+29
-3
@@ -12,6 +12,21 @@ import shutil
|
||||
import time
|
||||
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,
|
||||
hardware, jobs, projects)
|
||||
|
||||
@@ -149,6 +164,16 @@ def promote(model_id: int) -> dict:
|
||||
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:
|
||||
cur.execute(
|
||||
"SELECT COALESCE(MAX(version), 0) + 1 FROM model_versions WHERE project_id = ?",
|
||||
@@ -262,11 +287,12 @@ def _run_train(job) -> None:
|
||||
cur.execute(
|
||||
"""INSERT INTO model_versions (project_id, version, weights_path,
|
||||
parent_model_path, metrics, base_metrics,
|
||||
created_at, augment)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
created_at, augment, name)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(project["id"], version, weights, project["base_model_path"],
|
||||
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"]
|
||||
|
||||
@@ -151,6 +151,7 @@ erDiagram
|
||||
integer id PK "AUTOINCREMENT"
|
||||
integer project_id FK "References projects(id)"
|
||||
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 parent_model_path "Path to base model used as starting point"
|
||||
text metrics "Trained model evaluation metrics JSON"
|
||||
|
||||
@@ -229,7 +229,7 @@ Parameter pada form pembuatan proyek:
|
||||
*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\%$).
|
||||
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
|
||||
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).
|
||||
4. Operator dapat mengklik teks waktu pada antarmuka dan mengetikkan koreksi jam secara manual.
|
||||
|
||||
## 4.3 Pemindaian Truk Massal Terotomatisasi (Truck Scan v4)
|
||||
Tidak semua video arsip memuat aktivitas bongkar muat karung. Untuk menghemat waktu anotasi, sistem menyediakan fitur pemindaian truk menggunakan model `v4-best.pt`.
|
||||
## 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 terlatih terbaru.
|
||||
|
||||
Langkah operasional:
|
||||
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).
|
||||
4. Kolom `Truk` pada tabel akan menampilkan rasio keberadaan truk:
|
||||
- **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
|
||||
|
||||

|
||||
|
||||
*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)*
|
||||
|
||||
Opsi engine pelabelan:
|
||||
- **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.
|
||||
|
||||
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)*
|
||||
|
||||
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.
|
||||
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:
|
||||
@@ -647,20 +647,20 @@ Tabel metrik evaluasi model:
|
||||
|
||||
| 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 |
|
||||
| `v2` | 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 |
|
||||
| `v4` | **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-50ep-sack+box-20260909` | Arsip | 0.884 | 0.612 | 0.891 | 0.875 | Basis Awal | Model Awal |
|
||||
| `yolo11n-bbox-100ep-sack+box-20260910` | Arsip | 0.912 | 0.654 | 0.920 | 0.898 | `+0.028` (Hijau) | Ditingkatkan |
|
||||
| `yolo11n-bbox-100ep-sack+box-20260911` | Arsip | 0.938 | 0.701 | 0.942 | 0.925 | `+0.026` (Hijau) | Ditingkatkan |
|
||||
| `yolo11n-bbox-200ep-sack+box-20260912` | **Base Aktif** | 0.965 | 0.748 | 0.968 | 0.952 | `+0.027` (Hijau) | **Standar Produksi** |
|
||||
| `yolo11n-bbox-200ep-sack+box-20260913` | Kandidat | 0.978 | 0.772 | 0.981 | 0.969 | `+0.013` (Hijau) | Siap Dipromosikan |
|
||||
|
||||
Penjelasan nilai Delta $\Delta$:
|
||||
- **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.
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
# 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). |
|
||||
| `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. |
|
||||
| `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). |
|
||||
| `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. |
|
||||
@@ -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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `POST` | `/api/counting-bench/run` | `backend/counting_bench.py`| Menjalankan evaluasi headless counting pada rekaman video arsip. |
|
||||
|
||||
|
||||
+4
-2
@@ -79,7 +79,7 @@ dataset_items( -- master dataset membership (REQ-052
|
||||
split CHECK(train|val), image_rel, label_rel, added_at)
|
||||
|
||||
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,
|
||||
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/models/{id}/weights # download best.pt
|
||||
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
|
||||
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
|
||||
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
|
||||
`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:
|
||||
|
||||
|
||||
@@ -336,7 +336,8 @@ changes.
|
||||
delta.
|
||||
- **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
|
||||
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
|
||||
the SAM3 model before training starts.
|
||||
|
||||
|
||||
+4
-4
@@ -153,14 +153,14 @@ Serves REQ-060…065.
|
||||
|
||||
- `backend/hardware.py`: VRAM detection → `batch`/`imgsz`/`device` defaults.
|
||||
- `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
|
||||
`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
|
||||
models, `best.pt` downloads, promoting the version swaps the project's base model and a
|
||||
second training run starts from it.
|
||||
models with a descriptive name, `best.pt` downloads as `{name}-best.pt`, promoting the version
|
||||
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
|
||||
second run logged `Fine-tuning model.pt`. The mAP figures are zero because those labels are
|
||||
|
||||
+7
-4
@@ -685,18 +685,20 @@ only subtract from them.
|
||||
- **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
|
||||
"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
|
||||
metrics table:
|
||||
- **Trained Model Versions (N)** — one card per version: the model name (auto-generated
|
||||
`{arch}-{labelType}-{epochs}ep-{classNames}-{YYYYMMDD}`, clickable to rename inline),
|
||||
the created timestamp, and a metrics table:
|
||||
|
||||
| Metric | Base | This version | Δ |
|
||||
|---|---|---|---|
|
||||
| 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
|
||||
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`,
|
||||
`/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 }
|
||||
GET /api/projects/{id}/models
|
||||
POST /api/models/{id}/promote
|
||||
PATCH /api/models/{id}/rename { name }
|
||||
GET /api/models/{id}/weights
|
||||
```
|
||||
|
||||
|
||||
@@ -229,6 +229,8 @@ export const api = {
|
||||
request(`/projects/${projectId}/train`, { method: 'POST', body }),
|
||||
listModels: (projectId) => request(`/projects/${projectId}/models`),
|
||||
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`,
|
||||
|
||||
listJobs: (projectId) => request(`/jobs${projectId ? `?project_id=${projectId}` : ''}`),
|
||||
|
||||
@@ -20,6 +20,8 @@ function Metric({ label, base, next, delta }) {
|
||||
|
||||
function VersionCard({ version, onPromote, onError }) {
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [nameValue, setNameValue] = useState(version.name || '')
|
||||
const metrics = version.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 (
|
||||
<div className="panel side-panel">
|
||||
<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="faint mono">
|
||||
{new Date(version.created_at * 1000).toLocaleString()}
|
||||
|
||||
Reference in new issue
Block a user