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.

Files changed (97) hide show
  1. flashstudio/__init__.py +5 -0
  2. flashstudio/app.py +194 -0
  3. flashstudio/cli.py +18 -0
  4. flashstudio/components/__init__.py +0 -0
  5. flashstudio/components/project_manager.py +490 -0
  6. flashstudio/components/sidebar.py +87 -0
  7. flashstudio/components/styles.py +346 -0
  8. flashstudio/components/wizard.py +45 -0
  9. flashstudio/components/zone_drawer/__init__.py +177 -0
  10. flashstudio/constants.py +245 -0
  11. flashstudio/core/__init__.py +3 -0
  12. flashstudio/core/constants.py +3 -0
  13. flashstudio/launcher.py +288 -0
  14. flashstudio/pages/__init__.py +8 -0
  15. flashstudio/pages/dashboard/__init__.py +3 -0
  16. flashstudio/pages/dashboard/_common.py +10 -0
  17. flashstudio/pages/dashboard/overview/__init__.py +3 -0
  18. flashstudio/pages/dashboard/overview/metrics.py +33 -0
  19. flashstudio/pages/dashboard/overview/pipeline_status.py +35 -0
  20. flashstudio/pages/dashboard/overview/project_banner.py +23 -0
  21. flashstudio/pages/dashboard/overview/quick_actions.py +28 -0
  22. flashstudio/pages/dashboard/overview/recent_runs.py +99 -0
  23. flashstudio/pages/dashboard/overview/tab.py +35 -0
  24. flashstudio/pages/dashboard/page.py +17 -0
  25. flashstudio/pages/data/__init__.py +3 -0
  26. flashstudio/pages/data/_common.py +107 -0
  27. flashstudio/pages/data/download/__init__.py +3 -0
  28. flashstudio/pages/data/download/tab.py +75 -0
  29. flashstudio/pages/data/helpers.py +338 -0
  30. flashstudio/pages/data/page.py +50 -0
  31. flashstudio/pages/data/preview/__init__.py +3 -0
  32. flashstudio/pages/data/preview/tab.py +139 -0
  33. flashstudio/pages/data/upload/__init__.py +3 -0
  34. flashstudio/pages/data/upload/tab.py +156 -0
  35. flashstudio/pages/data/verify/__init__.py +3 -0
  36. flashstudio/pages/data/verify/tab.py +108 -0
  37. flashstudio/pages/export/__init__.py +3 -0
  38. flashstudio/pages/export/page.py +54 -0
  39. flashstudio/pages/export/weights/__init__.py +4 -0
  40. flashstudio/pages/export/weights/exporter.py +60 -0
  41. flashstudio/pages/export/weights/settings.py +86 -0
  42. flashstudio/pages/inference/__init__.py +3 -0
  43. flashstudio/pages/inference/_common.py +76 -0
  44. flashstudio/pages/inference/data/__init__.py +3 -0
  45. flashstudio/pages/inference/data/tab.py +121 -0
  46. flashstudio/pages/inference/model/__init__.py +3 -0
  47. flashstudio/pages/inference/model/tab.py +62 -0
  48. flashstudio/pages/inference/page.py +42 -0
  49. flashstudio/pages/inference/run/__init__.py +3 -0
  50. flashstudio/pages/inference/run/detection.py +133 -0
  51. flashstudio/pages/inference/run/tab.py +161 -0
  52. flashstudio/pages/inference/run/video.py +195 -0
  53. flashstudio/pages/inference/solution/__init__.py +3 -0
  54. flashstudio/pages/inference/solution/tab.py +121 -0
  55. flashstudio/pages/model/__init__.py +3 -0
  56. flashstudio/pages/model/_common.py +3 -0
  57. flashstudio/pages/model/advanced/__init__.py +3 -0
  58. flashstudio/pages/model/advanced/tab.py +54 -0
  59. flashstudio/pages/model/architecture/__init__.py +3 -0
  60. flashstudio/pages/model/architecture/tab.py +90 -0
  61. flashstudio/pages/model/augment/__init__.py +3 -0
  62. flashstudio/pages/model/augment/tab.py +18 -0
  63. flashstudio/pages/model/hyperparams/__init__.py +4 -0
  64. flashstudio/pages/model/hyperparams/tab.py +89 -0
  65. flashstudio/pages/model/page.py +30 -0
  66. flashstudio/pages/model/utils/__init__.py +3 -0
  67. flashstudio/pages/model/utils/config.py +27 -0
  68. flashstudio/pages/training/__init__.py +3 -0
  69. flashstudio/pages/training/_common.py +24 -0
  70. flashstudio/pages/training/launch/__init__.py +3 -0
  71. flashstudio/pages/training/launch/controls.py +84 -0
  72. flashstudio/pages/training/launch/dialogs.py +247 -0
  73. flashstudio/pages/training/launch/preflight.py +77 -0
  74. flashstudio/pages/training/launch/runner.py +440 -0
  75. flashstudio/pages/training/launch/tab.py +134 -0
  76. flashstudio/pages/training/monitor/__init__.py +3 -0
  77. flashstudio/pages/training/monitor/checkpoints.py +96 -0
  78. flashstudio/pages/training/monitor/curves.py +113 -0
  79. flashstudio/pages/training/monitor/dashboard.py +129 -0
  80. flashstudio/pages/training/monitor/gt_verification.py +122 -0
  81. flashstudio/pages/training/monitor/log_viewer.py +43 -0
  82. flashstudio/pages/training/monitor/parsers.py +253 -0
  83. flashstudio/pages/training/monitor/run_meta.py +165 -0
  84. flashstudio/pages/training/monitor/tab.py +142 -0
  85. flashstudio/pages/training/monitor/visualizations.py +52 -0
  86. flashstudio/pages/training/page.py +134 -0
  87. flashstudio/utils/__init__.py +152 -0
  88. flashstudio/utils/config.py +104 -0
  89. flashstudio/utils/device.py +67 -0
  90. flashstudio/utils/filesystem.py +67 -0
  91. flashstudio/utils/jobs.py +95 -0
  92. flashstudio/utils/training_hooks.py +266 -0
  93. flashstudio-0.0.1.dist-info/METADATA +107 -0
  94. flashstudio-0.0.1.dist-info/RECORD +97 -0
  95. flashstudio-0.0.1.dist-info/WHEEL +5 -0
  96. flashstudio-0.0.1.dist-info/entry_points.txt +2 -0
  97. 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
+ )