flashstudio 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flashstudio might be problematic. Click here for more details.
- flashstudio/__init__.py +5 -0
- flashstudio/app.py +194 -0
- flashstudio/cli.py +18 -0
- flashstudio/components/__init__.py +0 -0
- flashstudio/components/project_manager.py +490 -0
- flashstudio/components/sidebar.py +87 -0
- flashstudio/components/styles.py +346 -0
- flashstudio/components/wizard.py +45 -0
- flashstudio/components/zone_drawer/__init__.py +177 -0
- flashstudio/constants.py +245 -0
- flashstudio/core/__init__.py +3 -0
- flashstudio/core/constants.py +3 -0
- flashstudio/launcher.py +288 -0
- flashstudio/pages/__init__.py +8 -0
- flashstudio/pages/dashboard/__init__.py +3 -0
- flashstudio/pages/dashboard/_common.py +10 -0
- flashstudio/pages/dashboard/overview/__init__.py +3 -0
- flashstudio/pages/dashboard/overview/metrics.py +33 -0
- flashstudio/pages/dashboard/overview/pipeline_status.py +35 -0
- flashstudio/pages/dashboard/overview/project_banner.py +23 -0
- flashstudio/pages/dashboard/overview/quick_actions.py +28 -0
- flashstudio/pages/dashboard/overview/recent_runs.py +99 -0
- flashstudio/pages/dashboard/overview/tab.py +35 -0
- flashstudio/pages/dashboard/page.py +17 -0
- flashstudio/pages/data/__init__.py +3 -0
- flashstudio/pages/data/_common.py +107 -0
- flashstudio/pages/data/download/__init__.py +3 -0
- flashstudio/pages/data/download/tab.py +75 -0
- flashstudio/pages/data/helpers.py +338 -0
- flashstudio/pages/data/page.py +50 -0
- flashstudio/pages/data/preview/__init__.py +3 -0
- flashstudio/pages/data/preview/tab.py +139 -0
- flashstudio/pages/data/upload/__init__.py +3 -0
- flashstudio/pages/data/upload/tab.py +156 -0
- flashstudio/pages/data/verify/__init__.py +3 -0
- flashstudio/pages/data/verify/tab.py +108 -0
- flashstudio/pages/export/__init__.py +3 -0
- flashstudio/pages/export/page.py +54 -0
- flashstudio/pages/export/weights/__init__.py +4 -0
- flashstudio/pages/export/weights/exporter.py +60 -0
- flashstudio/pages/export/weights/settings.py +86 -0
- flashstudio/pages/inference/__init__.py +3 -0
- flashstudio/pages/inference/_common.py +76 -0
- flashstudio/pages/inference/data/__init__.py +3 -0
- flashstudio/pages/inference/data/tab.py +121 -0
- flashstudio/pages/inference/model/__init__.py +3 -0
- flashstudio/pages/inference/model/tab.py +62 -0
- flashstudio/pages/inference/page.py +42 -0
- flashstudio/pages/inference/run/__init__.py +3 -0
- flashstudio/pages/inference/run/detection.py +133 -0
- flashstudio/pages/inference/run/tab.py +161 -0
- flashstudio/pages/inference/run/video.py +195 -0
- flashstudio/pages/inference/solution/__init__.py +3 -0
- flashstudio/pages/inference/solution/tab.py +121 -0
- flashstudio/pages/model/__init__.py +3 -0
- flashstudio/pages/model/_common.py +3 -0
- flashstudio/pages/model/advanced/__init__.py +3 -0
- flashstudio/pages/model/advanced/tab.py +54 -0
- flashstudio/pages/model/architecture/__init__.py +3 -0
- flashstudio/pages/model/architecture/tab.py +90 -0
- flashstudio/pages/model/augment/__init__.py +3 -0
- flashstudio/pages/model/augment/tab.py +18 -0
- flashstudio/pages/model/hyperparams/__init__.py +4 -0
- flashstudio/pages/model/hyperparams/tab.py +89 -0
- flashstudio/pages/model/page.py +30 -0
- flashstudio/pages/model/utils/__init__.py +3 -0
- flashstudio/pages/model/utils/config.py +27 -0
- flashstudio/pages/training/__init__.py +3 -0
- flashstudio/pages/training/_common.py +24 -0
- flashstudio/pages/training/launch/__init__.py +3 -0
- flashstudio/pages/training/launch/controls.py +84 -0
- flashstudio/pages/training/launch/dialogs.py +247 -0
- flashstudio/pages/training/launch/preflight.py +77 -0
- flashstudio/pages/training/launch/runner.py +440 -0
- flashstudio/pages/training/launch/tab.py +134 -0
- flashstudio/pages/training/monitor/__init__.py +3 -0
- flashstudio/pages/training/monitor/checkpoints.py +96 -0
- flashstudio/pages/training/monitor/curves.py +113 -0
- flashstudio/pages/training/monitor/dashboard.py +129 -0
- flashstudio/pages/training/monitor/gt_verification.py +122 -0
- flashstudio/pages/training/monitor/log_viewer.py +43 -0
- flashstudio/pages/training/monitor/parsers.py +253 -0
- flashstudio/pages/training/monitor/run_meta.py +165 -0
- flashstudio/pages/training/monitor/tab.py +142 -0
- flashstudio/pages/training/monitor/visualizations.py +52 -0
- flashstudio/pages/training/page.py +134 -0
- flashstudio/utils/__init__.py +152 -0
- flashstudio/utils/config.py +104 -0
- flashstudio/utils/device.py +67 -0
- flashstudio/utils/filesystem.py +67 -0
- flashstudio/utils/jobs.py +95 -0
- flashstudio/utils/training_hooks.py +266 -0
- flashstudio-0.0.1.dist-info/METADATA +107 -0
- flashstudio-0.0.1.dist-info/RECORD +97 -0
- flashstudio-0.0.1.dist-info/WHEEL +5 -0
- flashstudio-0.0.1.dist-info/entry_points.txt +2 -0
- flashstudio-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"""FlashStudio — Project Management System.
|
|
2
|
+
|
|
3
|
+
Provides project isolation: each project has its own dataset, model config,
|
|
4
|
+
training runs, exports, and inference results. Users can create, switch,
|
|
5
|
+
duplicate, and delete projects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import shutil
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import streamlit as st
|
|
15
|
+
|
|
16
|
+
PROJECTS_ROOT = os.path.join(os.path.expanduser("~"), ".flashstudio", "projects")
|
|
17
|
+
PROJECTS_INDEX = os.path.join(os.path.expanduser("~"), ".flashstudio", "projects.json")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _ensure_dirs():
|
|
21
|
+
os.makedirs(PROJECTS_ROOT, exist_ok=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load_index() -> dict:
|
|
25
|
+
"""Load the global project index."""
|
|
26
|
+
_ensure_dirs()
|
|
27
|
+
if os.path.isfile(PROJECTS_INDEX):
|
|
28
|
+
try:
|
|
29
|
+
with open(PROJECTS_INDEX, "r") as f:
|
|
30
|
+
data = json.load(f)
|
|
31
|
+
if isinstance(data, dict) and "projects" in data:
|
|
32
|
+
return data
|
|
33
|
+
except (json.JSONDecodeError, OSError):
|
|
34
|
+
pass
|
|
35
|
+
return {"projects": [], "active": None}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _save_index(index: dict):
|
|
39
|
+
_ensure_dirs()
|
|
40
|
+
with open(PROJECTS_INDEX, "w") as f:
|
|
41
|
+
json.dump(index, f, indent=2)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_project_dir(project_id: str) -> str:
|
|
45
|
+
return os.path.join(PROJECTS_ROOT, project_id)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def list_projects() -> list:
|
|
49
|
+
"""Return all projects with metadata."""
|
|
50
|
+
index = _load_index()
|
|
51
|
+
projects = []
|
|
52
|
+
for p in index.get("projects", []):
|
|
53
|
+
proj_dir = get_project_dir(p["id"])
|
|
54
|
+
p["exists"] = os.path.isdir(proj_dir)
|
|
55
|
+
if p["exists"]:
|
|
56
|
+
p["size"] = _dir_size_str(proj_dir)
|
|
57
|
+
runs_dir = os.path.join(proj_dir, "runs")
|
|
58
|
+
p["num_runs"] = len(os.listdir(runs_dir)) if os.path.isdir(runs_dir) else 0
|
|
59
|
+
projects.append(p)
|
|
60
|
+
return projects
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_active_project() -> Optional[dict]:
|
|
64
|
+
"""Get the currently active project, or None."""
|
|
65
|
+
index = _load_index()
|
|
66
|
+
active_id = index.get("active")
|
|
67
|
+
if not active_id:
|
|
68
|
+
return None
|
|
69
|
+
for p in index.get("projects", []):
|
|
70
|
+
if p["id"] == active_id:
|
|
71
|
+
return p
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def create_project(name: str, description: str = "") -> dict:
|
|
76
|
+
"""Create a new project and set it as active."""
|
|
77
|
+
index = _load_index()
|
|
78
|
+
project_id = _slugify(name) + "_" + datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
79
|
+
proj_dir = get_project_dir(project_id)
|
|
80
|
+
|
|
81
|
+
os.makedirs(proj_dir, exist_ok=True)
|
|
82
|
+
os.makedirs(os.path.join(proj_dir, "data"), exist_ok=True)
|
|
83
|
+
os.makedirs(os.path.join(proj_dir, "runs"), exist_ok=True)
|
|
84
|
+
os.makedirs(os.path.join(proj_dir, "exports"), exist_ok=True)
|
|
85
|
+
os.makedirs(os.path.join(proj_dir, "inference_results"), exist_ok=True)
|
|
86
|
+
|
|
87
|
+
project = {
|
|
88
|
+
"id": project_id,
|
|
89
|
+
"name": name,
|
|
90
|
+
"description": description,
|
|
91
|
+
"created": datetime.now().isoformat(),
|
|
92
|
+
"last_modified": datetime.now().isoformat(),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Save project config
|
|
96
|
+
with open(os.path.join(proj_dir, "project.json"), "w") as f:
|
|
97
|
+
json.dump(project, f, indent=2)
|
|
98
|
+
|
|
99
|
+
index["projects"].append(project)
|
|
100
|
+
index["active"] = project_id
|
|
101
|
+
_save_index(index)
|
|
102
|
+
|
|
103
|
+
return project
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def switch_project(project_id: str):
|
|
107
|
+
"""Switch active project and load its state."""
|
|
108
|
+
_clear_session_for_new_project()
|
|
109
|
+
index = _load_index()
|
|
110
|
+
index["active"] = project_id
|
|
111
|
+
_save_index(index)
|
|
112
|
+
load_project_state(project_id)
|
|
113
|
+
st.session_state["_project_state_loaded"] = True
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def delete_project(project_id: str):
|
|
117
|
+
"""Delete a project and all its data."""
|
|
118
|
+
index = _load_index()
|
|
119
|
+
index["projects"] = [p for p in index["projects"] if p["id"] != project_id]
|
|
120
|
+
if index.get("active") == project_id:
|
|
121
|
+
index["active"] = index["projects"][0]["id"] if index["projects"] else None
|
|
122
|
+
_save_index(index)
|
|
123
|
+
|
|
124
|
+
proj_dir = get_project_dir(project_id)
|
|
125
|
+
if os.path.isdir(proj_dir):
|
|
126
|
+
shutil.rmtree(proj_dir)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def delete_all_projects():
|
|
130
|
+
"""Delete all projects and reset everything."""
|
|
131
|
+
index = _load_index()
|
|
132
|
+
for p in index.get("projects", []):
|
|
133
|
+
proj_dir = get_project_dir(p["id"])
|
|
134
|
+
if os.path.isdir(proj_dir):
|
|
135
|
+
shutil.rmtree(proj_dir)
|
|
136
|
+
_save_index({"projects": [], "active": None})
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def save_project_state():
|
|
140
|
+
"""Save current session state to the active project."""
|
|
141
|
+
index = _load_index()
|
|
142
|
+
active_id = index.get("active")
|
|
143
|
+
if not active_id:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
proj_dir = get_project_dir(active_id)
|
|
147
|
+
if not os.path.isdir(proj_dir):
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
# Save relevant session state keys
|
|
151
|
+
state_keys = [
|
|
152
|
+
"dataset_name", "dataset_classes", "dataset_id", "dataset_output_path",
|
|
153
|
+
"train_img_path", "val_img_path", "train_ann_path", "val_ann_path",
|
|
154
|
+
"ann_format", "class_names", "upload_num_classes", "num_classes",
|
|
155
|
+
"model_arch", "arch_family", "pico_backbone",
|
|
156
|
+
"epochs", "batch_size", "lr", "img_size",
|
|
157
|
+
"optimizer", "weight_decay", "val_interval", "lr_final_ratio",
|
|
158
|
+
"finetune_strategy", "pretrain_option", "custom_weights",
|
|
159
|
+
"lora_variant", "lora_rank", "lora_alpha", "lora_dropout", "lora_targets",
|
|
160
|
+
"qlora", "qlora_dtype", "warmup_epochs", "patience", "num_workers",
|
|
161
|
+
"amp", "grad_clip", "multiscale",
|
|
162
|
+
"aug_mosaic", "aug_mixup", "aug_copypaste",
|
|
163
|
+
"activation_checkpointing", "activation_offloading", "optimizer_in_bwd",
|
|
164
|
+
"use_8bit_optimizer", "compile_model", "ddp", "grad_accum",
|
|
165
|
+
"chunked_loss", "chunk_size", "class_file",
|
|
166
|
+
"save_dir", "save_best", "run_name", "resume_training", "resume_path",
|
|
167
|
+
"training_status",
|
|
168
|
+
"infer_model_arch", "infer_conf", "infer_nms", "infer_img_size",
|
|
169
|
+
"selected_solution",
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
state = {}
|
|
173
|
+
for key in state_keys:
|
|
174
|
+
if key in st.session_state:
|
|
175
|
+
val = st.session_state[key]
|
|
176
|
+
if isinstance(val, (str, int, float, bool, list, dict, type(None))):
|
|
177
|
+
state[key] = val
|
|
178
|
+
|
|
179
|
+
state_file = os.path.join(proj_dir, "session_state.json")
|
|
180
|
+
with open(state_file, "w") as f:
|
|
181
|
+
json.dump(state, f, indent=2)
|
|
182
|
+
|
|
183
|
+
# Update last_modified
|
|
184
|
+
for p in index["projects"]:
|
|
185
|
+
if p["id"] == active_id:
|
|
186
|
+
p["last_modified"] = datetime.now().isoformat()
|
|
187
|
+
break
|
|
188
|
+
_save_index(index)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def load_project_state(project_id: str):
|
|
192
|
+
"""Load a project's saved state into session."""
|
|
193
|
+
proj_dir = get_project_dir(project_id)
|
|
194
|
+
state_file = os.path.join(proj_dir, "session_state.json")
|
|
195
|
+
|
|
196
|
+
if not os.path.isfile(state_file):
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
with open(state_file, "r") as f:
|
|
200
|
+
state = json.load(f)
|
|
201
|
+
|
|
202
|
+
for key, val in state.items():
|
|
203
|
+
st.session_state[key] = val
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def duplicate_project(project_id: str, new_name: str) -> dict:
|
|
207
|
+
"""Duplicate an existing project with a new name."""
|
|
208
|
+
index = _load_index()
|
|
209
|
+
prev_active = index.get("active")
|
|
210
|
+
src_dir = get_project_dir(project_id)
|
|
211
|
+
new_project = create_project(new_name)
|
|
212
|
+
dst_dir = get_project_dir(new_project["id"])
|
|
213
|
+
|
|
214
|
+
# Copy data and config (not runs — those are large)
|
|
215
|
+
for subdir in ("data",):
|
|
216
|
+
src = os.path.join(src_dir, subdir)
|
|
217
|
+
dst = os.path.join(dst_dir, subdir)
|
|
218
|
+
if os.path.isdir(src):
|
|
219
|
+
shutil.rmtree(dst, ignore_errors=True)
|
|
220
|
+
shutil.copytree(src, dst)
|
|
221
|
+
|
|
222
|
+
# Copy session state
|
|
223
|
+
src_state = os.path.join(src_dir, "session_state.json")
|
|
224
|
+
if os.path.isfile(src_state):
|
|
225
|
+
shutil.copy2(src_state, os.path.join(dst_dir, "session_state.json"))
|
|
226
|
+
|
|
227
|
+
# Restore previously active project so duplication doesn't switch context
|
|
228
|
+
if prev_active and prev_active != new_project["id"]:
|
|
229
|
+
idx = _load_index()
|
|
230
|
+
idx["active"] = prev_active
|
|
231
|
+
_save_index(idx)
|
|
232
|
+
|
|
233
|
+
return new_project
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def get_project_stats(project_id: str) -> dict:
|
|
237
|
+
"""Get statistics for a project."""
|
|
238
|
+
import re
|
|
239
|
+
import csv
|
|
240
|
+
import glob
|
|
241
|
+
proj_dir = get_project_dir(project_id)
|
|
242
|
+
stats = {"runs": 0, "best_map": None, "total_size": "0 KB", "has_export": False}
|
|
243
|
+
|
|
244
|
+
# Check both project/runs/ and workspace/
|
|
245
|
+
from flashstudio.constants import DEFAULT_SAVE_DIR
|
|
246
|
+
runs_dirs = [os.path.join(proj_dir, "runs")]
|
|
247
|
+
if os.path.isdir(DEFAULT_SAVE_DIR):
|
|
248
|
+
runs_dirs.append(DEFAULT_SAVE_DIR)
|
|
249
|
+
|
|
250
|
+
best_map = 0
|
|
251
|
+
total_runs = 0
|
|
252
|
+
for runs_dir in runs_dirs:
|
|
253
|
+
if not os.path.isdir(runs_dir):
|
|
254
|
+
continue
|
|
255
|
+
runs = [d for d in os.listdir(runs_dir) if os.path.isdir(os.path.join(runs_dir, d))]
|
|
256
|
+
total_runs += len(runs)
|
|
257
|
+
|
|
258
|
+
for run in runs:
|
|
259
|
+
rd = os.path.join(runs_dir, run)
|
|
260
|
+
from flashstudio.constants import TRAINING_LOG_CSV
|
|
261
|
+
csv_path = os.path.join(rd, TRAINING_LOG_CSV)
|
|
262
|
+
if os.path.isfile(csv_path):
|
|
263
|
+
try:
|
|
264
|
+
with open(csv_path, "r") as f:
|
|
265
|
+
rows = list(csv.DictReader(f))
|
|
266
|
+
vals = [float(r.get("mAP@0.5", 0)) for r in rows if r.get("mAP@0.5", "").strip()]
|
|
267
|
+
if vals and max(vals) > best_map:
|
|
268
|
+
best_map = max(vals)
|
|
269
|
+
except Exception:
|
|
270
|
+
pass
|
|
271
|
+
if best_map == 0:
|
|
272
|
+
from flashstudio.constants import TRAINING_LOG_GLOB
|
|
273
|
+
logs = glob.glob(os.path.join(rd, TRAINING_LOG_GLOB))
|
|
274
|
+
for lf in logs:
|
|
275
|
+
try:
|
|
276
|
+
with open(lf, "r", encoding="utf-8", errors="replace") as f:
|
|
277
|
+
for ln in f.readlines()[-10:]:
|
|
278
|
+
bm = re.search(r"Best mAP@0\.5:\s*([\d.]+)", ln)
|
|
279
|
+
if bm and float(bm.group(1)) > best_map:
|
|
280
|
+
best_map = float(bm.group(1))
|
|
281
|
+
except OSError:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
stats["runs"] = total_runs
|
|
285
|
+
if best_map > 0:
|
|
286
|
+
stats["best_map"] = best_map
|
|
287
|
+
|
|
288
|
+
exports_dir = os.path.join(proj_dir, "exports")
|
|
289
|
+
if os.path.isdir(exports_dir) and os.listdir(exports_dir):
|
|
290
|
+
stats["has_export"] = True
|
|
291
|
+
if not stats["has_export"]:
|
|
292
|
+
for runs_dir in runs_dirs:
|
|
293
|
+
if os.path.isdir(runs_dir):
|
|
294
|
+
for d in os.listdir(runs_dir):
|
|
295
|
+
rd = os.path.join(runs_dir, d)
|
|
296
|
+
if os.path.isdir(rd) and any(f.endswith(".onnx") for f in os.listdir(rd)):
|
|
297
|
+
stats["has_export"] = True
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
stats["total_size"] = _dir_size_str(proj_dir)
|
|
301
|
+
return stats
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def render_project_selector():
|
|
305
|
+
"""Render the project management UI in the sidebar or top of page."""
|
|
306
|
+
index = _load_index()
|
|
307
|
+
active_id = index.get("active")
|
|
308
|
+
projects = index.get("projects", [])
|
|
309
|
+
|
|
310
|
+
if not projects:
|
|
311
|
+
return False # No projects exist — show creation UI
|
|
312
|
+
|
|
313
|
+
# Quick selector
|
|
314
|
+
active_idx = 0
|
|
315
|
+
for i, p in enumerate(projects):
|
|
316
|
+
if p["id"] == active_id:
|
|
317
|
+
active_idx = i
|
|
318
|
+
break
|
|
319
|
+
|
|
320
|
+
selected_idx = st.selectbox(
|
|
321
|
+
"Active Project",
|
|
322
|
+
range(len(projects)),
|
|
323
|
+
index=active_idx,
|
|
324
|
+
format_func=lambda i: projects[i]['name'],
|
|
325
|
+
key="project_selector",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if projects[selected_idx]["id"] != active_id:
|
|
329
|
+
save_project_state()
|
|
330
|
+
switch_project(projects[selected_idx]["id"])
|
|
331
|
+
st.rerun()
|
|
332
|
+
|
|
333
|
+
return True
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def render_project_manager_page():
|
|
337
|
+
"""Full project management page — create, switch, delete, manage projects."""
|
|
338
|
+
st.markdown(
|
|
339
|
+
"<div style='text-align:center; padding:0.4rem 0;'>"
|
|
340
|
+
"<b style='font-size:1.2rem;'>Project Manager</b>"
|
|
341
|
+
"</div>",
|
|
342
|
+
unsafe_allow_html=True,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
index = _load_index()
|
|
346
|
+
projects = index.get("projects", [])
|
|
347
|
+
active_id = index.get("active")
|
|
348
|
+
|
|
349
|
+
# ─── Create New Project ───
|
|
350
|
+
with st.container(border=True):
|
|
351
|
+
st.markdown("#### Create New Project")
|
|
352
|
+
col_name, col_desc, col_btn = st.columns([2, 3, 1])
|
|
353
|
+
with col_name:
|
|
354
|
+
new_name = st.text_input("Project Name", placeholder="e.g. PPE Detection v2",
|
|
355
|
+
key="new_project_name")
|
|
356
|
+
with col_desc:
|
|
357
|
+
new_desc = st.text_input("Description (optional)",
|
|
358
|
+
placeholder="Safety equipment detection for construction sites",
|
|
359
|
+
key="new_project_desc")
|
|
360
|
+
with col_btn:
|
|
361
|
+
st.markdown("<div style='height:28px'></div>", unsafe_allow_html=True)
|
|
362
|
+
if st.button("Create", type="primary", key="create_project_btn",
|
|
363
|
+
use_container_width=True, disabled=not new_name):
|
|
364
|
+
create_project(new_name, new_desc)
|
|
365
|
+
st.success(f"Project **{new_name}** created!")
|
|
366
|
+
_clear_session_for_new_project()
|
|
367
|
+
st.rerun()
|
|
368
|
+
|
|
369
|
+
# ─── Project List ───
|
|
370
|
+
if not projects:
|
|
371
|
+
st.info("No projects yet. Create your first project above to get started.")
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
st.markdown(f"#### Your Projects ({len(projects)})")
|
|
375
|
+
|
|
376
|
+
for p in sorted(projects, key=lambda x: x.get("last_modified", ""), reverse=True):
|
|
377
|
+
is_active = p["id"] == active_id
|
|
378
|
+
stats = get_project_stats(p["id"])
|
|
379
|
+
|
|
380
|
+
with st.container(border=True):
|
|
381
|
+
col_info, col_stats, col_actions = st.columns([3, 2, 2])
|
|
382
|
+
|
|
383
|
+
with col_info:
|
|
384
|
+
active_badge = " **[ACTIVE]**" if is_active else ""
|
|
385
|
+
st.markdown(f"**{p['name']}**{active_badge}")
|
|
386
|
+
if p.get("description"):
|
|
387
|
+
st.caption(p["description"])
|
|
388
|
+
created = p.get("created", "")[:10]
|
|
389
|
+
modified = p.get("last_modified", "")[:10]
|
|
390
|
+
st.caption(f"Created: {created} · Modified: {modified}")
|
|
391
|
+
|
|
392
|
+
with col_stats:
|
|
393
|
+
sc1, sc2, sc3 = st.columns(3)
|
|
394
|
+
with sc1:
|
|
395
|
+
st.metric("Runs", stats["runs"])
|
|
396
|
+
with sc2:
|
|
397
|
+
map_str = f"{stats['best_map']:.3f}" if stats["best_map"] else "—"
|
|
398
|
+
st.metric("Best mAP", map_str)
|
|
399
|
+
with sc3:
|
|
400
|
+
st.metric("Size", stats["total_size"])
|
|
401
|
+
|
|
402
|
+
with col_actions:
|
|
403
|
+
ac1, ac2, ac3 = st.columns(3)
|
|
404
|
+
with ac1:
|
|
405
|
+
if not is_active:
|
|
406
|
+
if st.button("Open", key=f"switch_{p['id']}", help="Switch to this project"):
|
|
407
|
+
save_project_state()
|
|
408
|
+
switch_project(p["id"])
|
|
409
|
+
st.rerun()
|
|
410
|
+
with ac2:
|
|
411
|
+
if st.button("Copy", key=f"dup_{p['id']}", help="Duplicate project"):
|
|
412
|
+
dup = duplicate_project(p["id"], f"{p['name']} (copy)")
|
|
413
|
+
st.success(f"Duplicated as '{dup['name']}'")
|
|
414
|
+
st.rerun()
|
|
415
|
+
with ac3:
|
|
416
|
+
if st.button("Del", key=f"del_{p['id']}", help="Delete project"):
|
|
417
|
+
st.session_state[f"confirm_delete_{p['id']}"] = True
|
|
418
|
+
|
|
419
|
+
# Delete confirmation
|
|
420
|
+
if st.session_state.get(f"confirm_delete_{p['id']}"):
|
|
421
|
+
st.error(f"Delete project **{p['name']}** and ALL its data? This cannot be undone.")
|
|
422
|
+
dc1, dc2, dc3 = st.columns([1, 1, 3])
|
|
423
|
+
with dc1:
|
|
424
|
+
if st.button("Yes, Delete", key=f"confirm_del_{p['id']}", type="primary"):
|
|
425
|
+
delete_project(p["id"])
|
|
426
|
+
st.session_state.pop(f"confirm_delete_{p['id']}", None)
|
|
427
|
+
st.rerun()
|
|
428
|
+
with dc2:
|
|
429
|
+
if st.button("Cancel", key=f"cancel_del_{p['id']}"):
|
|
430
|
+
st.session_state.pop(f"confirm_delete_{p['id']}", None)
|
|
431
|
+
st.rerun()
|
|
432
|
+
|
|
433
|
+
# ─── Danger Zone ───
|
|
434
|
+
st.divider()
|
|
435
|
+
with st.expander("Danger Zone", expanded=False):
|
|
436
|
+
st.error("These actions are irreversible!")
|
|
437
|
+
col_d1, col_d2 = st.columns(2)
|
|
438
|
+
with col_d1:
|
|
439
|
+
if st.button("Delete ALL Projects", key="delete_all_projects"):
|
|
440
|
+
st.session_state["confirm_delete_all"] = True
|
|
441
|
+
|
|
442
|
+
if st.session_state.get("confirm_delete_all"):
|
|
443
|
+
st.warning(f"This will delete **{len(projects)} projects** and all training data!")
|
|
444
|
+
if st.button("CONFIRM: Delete Everything", type="primary", key="confirm_delete_all_btn"):
|
|
445
|
+
delete_all_projects()
|
|
446
|
+
_clear_session_for_new_project()
|
|
447
|
+
st.session_state.pop("confirm_delete_all", None)
|
|
448
|
+
st.rerun()
|
|
449
|
+
if st.button("Cancel", key="cancel_delete_all"):
|
|
450
|
+
st.session_state.pop("confirm_delete_all", None)
|
|
451
|
+
st.rerun()
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _clear_session_for_new_project():
|
|
455
|
+
"""Clear session state keys when switching to a new/blank project."""
|
|
456
|
+
keys_to_clear = [
|
|
457
|
+
"dataset_name", "dataset_classes", "dataset_id", "dataset_output_path",
|
|
458
|
+
"train_img_path", "val_img_path", "train_ann_path", "val_ann_path",
|
|
459
|
+
"training_active", "training_status", "training_pid",
|
|
460
|
+
"infer_img_results", "video_results", "exported_files",
|
|
461
|
+
"run_name", "detected_format",
|
|
462
|
+
]
|
|
463
|
+
for key in keys_to_clear:
|
|
464
|
+
st.session_state.pop(key, None)
|
|
465
|
+
st.session_state["current_step"] = 0
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _slugify(text: str) -> str:
|
|
469
|
+
"""Convert text to a safe directory name."""
|
|
470
|
+
import re
|
|
471
|
+
text = text.lower().strip()
|
|
472
|
+
text = re.sub(r'[^\w\s-]', '', text)
|
|
473
|
+
text = re.sub(r'[\s-]+', '_', text)
|
|
474
|
+
return text[:30]
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _dir_size_str(path: str) -> str:
|
|
478
|
+
"""Get human-readable directory size."""
|
|
479
|
+
total = 0
|
|
480
|
+
for dirpath, _dirnames, filenames in os.walk(path):
|
|
481
|
+
for f in filenames:
|
|
482
|
+
try:
|
|
483
|
+
total += os.path.getsize(os.path.join(dirpath, f))
|
|
484
|
+
except OSError:
|
|
485
|
+
pass
|
|
486
|
+
if total > 1024 * 1024 * 1024:
|
|
487
|
+
return f"{total / (1024**3):.1f} GB"
|
|
488
|
+
elif total > 1024 * 1024:
|
|
489
|
+
return f"{total / (1024**2):.0f} MB"
|
|
490
|
+
return f"{total / 1024:.0f} KB"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""FlashStudio sidebar — clean icon-based navigation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import streamlit as st
|
|
5
|
+
from flashstudio.utils.device import get_gpu_info
|
|
6
|
+
from flashstudio.components.project_manager import (
|
|
7
|
+
get_active_project, save_project_state,
|
|
8
|
+
)
|
|
9
|
+
from flashstudio.constants import (
|
|
10
|
+
COLOR_SUCCESS, COLOR_WARNING, COLOR_ERROR,
|
|
11
|
+
COLOR_TEXT_PRIMARY, COLOR_TEXT_BODY, COLOR_TEXT_SECONDARY,
|
|
12
|
+
SIDEBAR_LABEL_TRUNCATE,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
NAV = [
|
|
16
|
+
("1", "Dashboard"),
|
|
17
|
+
("2", "Data"),
|
|
18
|
+
("3", "Model"),
|
|
19
|
+
("4", "Training"),
|
|
20
|
+
("5", "Export"),
|
|
21
|
+
("6", "Inference"),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_logo_b64():
|
|
26
|
+
"""Load logo as base64 for inline HTML rendering."""
|
|
27
|
+
import base64
|
|
28
|
+
logo_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "..", "assets", "logo.png")
|
|
29
|
+
if not os.path.isfile(logo_path):
|
|
30
|
+
logo_path = os.path.join(os.getcwd(), "assets", "logo.png")
|
|
31
|
+
if os.path.isfile(logo_path):
|
|
32
|
+
with open(logo_path, "rb") as f:
|
|
33
|
+
return base64.b64encode(f.read()).decode()
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def render_sidebar():
|
|
38
|
+
with st.sidebar:
|
|
39
|
+
logo_b64 = _get_logo_b64()
|
|
40
|
+
if logo_b64:
|
|
41
|
+
st.markdown(
|
|
42
|
+
f'<div style="display:flex;flex-direction:column;align-items:center;gap:0.3rem;padding:0.6rem 0.2rem 0.5rem;">'
|
|
43
|
+
f'<img src="data:image/png;base64,{logo_b64}" style="width:56px;height:56px;">'
|
|
44
|
+
f'<b style="font-size:1.1rem;color:{COLOR_TEXT_PRIMARY};">FlashStudio</b>'
|
|
45
|
+
f'</div>',
|
|
46
|
+
unsafe_allow_html=True,
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
st.markdown(
|
|
50
|
+
f'<div style="display:flex;flex-direction:column;align-items:center;gap:0.2rem;padding:0.5rem 0 0.3rem;">'
|
|
51
|
+
f'<b style="font-size:1.5rem;">FS</b>'
|
|
52
|
+
f'<b style="font-size:1rem;color:{COLOR_TEXT_PRIMARY};">FlashStudio</b>'
|
|
53
|
+
f'</div>',
|
|
54
|
+
unsafe_allow_html=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
project = get_active_project()
|
|
58
|
+
if project:
|
|
59
|
+
if st.button(project['name'][:SIDEBAR_LABEL_TRUNCATE], key="sb_project", use_container_width=True):
|
|
60
|
+
save_project_state()
|
|
61
|
+
st.session_state["show_project_manager"] = True
|
|
62
|
+
st.rerun()
|
|
63
|
+
|
|
64
|
+
current = st.session_state.get("current_step", 0)
|
|
65
|
+
for i, (icon, label) in enumerate(NAV):
|
|
66
|
+
btn_type = "primary" if i == current else "secondary"
|
|
67
|
+
if st.button(f"{icon} {label}", key=f"sb_{i}", type=btn_type, use_container_width=True):
|
|
68
|
+
st.session_state["current_step"] = i
|
|
69
|
+
st.rerun()
|
|
70
|
+
|
|
71
|
+
st.divider()
|
|
72
|
+
|
|
73
|
+
gpu = get_gpu_info()
|
|
74
|
+
from flashstudio.constants import GPU_NAME_TRUNCATE, DATASET_NAME_TRUNCATE
|
|
75
|
+
train_status = st.session_state.get("training_status", "Idle")
|
|
76
|
+
status_color = COLOR_SUCCESS if train_status == "Complete" else (
|
|
77
|
+
COLOR_WARNING if train_status == "Running" else (
|
|
78
|
+
COLOR_ERROR if train_status == "Failed" else COLOR_TEXT_SECONDARY))
|
|
79
|
+
st.markdown(
|
|
80
|
+
f'<div style="font-size:0.82rem;color:{COLOR_TEXT_BODY};padding:0.1rem 0.5rem;line-height:1.9;">'
|
|
81
|
+
f'GPU: {gpu["name"][:GPU_NAME_TRUNCATE] if gpu["available"] else "CPU"}<br>'
|
|
82
|
+
f'Data: {(st.session_state.get("dataset_name") or "—")[:DATASET_NAME_TRUNCATE]}<br>'
|
|
83
|
+
f'Model: {st.session_state.get("model_arch", "Pico").replace("FlashDet-", "")}<br>'
|
|
84
|
+
f'Status: <span style="color:{status_color};font-weight:600;">{train_status[:12]}</span>'
|
|
85
|
+
f'</div>',
|
|
86
|
+
unsafe_allow_html=True,
|
|
87
|
+
)
|