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,338 @@
1
+ """Ingest helper functions — extraction, conversion, download runners."""
2
+
3
+ import os
4
+ import streamlit as st
5
+
6
+ from flashstudio.constants import IMG_EXTENSIONS, DEFAULT_DATA_DIR, INFER_NUM_CLASSES
7
+ from flashstudio.pages.data._common import (
8
+ _HAS_FLASHDET, _FLASHDET_INSTALL_MSG,
9
+ _auto_detect_classes, _check_network, _dataset_already_downloaded,
10
+ )
11
+
12
+ try:
13
+ from flashdet import download_dataset
14
+ from flashdet.data import convert_dataset, detect_dataset_format
15
+ except ImportError:
16
+ pass
17
+
18
+
19
+ def _extract_upload(uploaded_file, split):
20
+ import zipfile
21
+ import tarfile
22
+ import tempfile
23
+ import shutil
24
+ name = uploaded_file.name
25
+ out = os.path.join(DEFAULT_DATA_DIR, "uploaded", split)
26
+ os.makedirs(out, exist_ok=True)
27
+ try:
28
+ if name.endswith(".zip"):
29
+ tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
30
+ tmp.write(uploaded_file.read()); tmp.flush(); tmp.close()
31
+ uploaded_file.seek(0)
32
+ if not zipfile.is_zipfile(tmp.name):
33
+ os.unlink(tmp.name)
34
+ st.error(f"❌ **Invalid ZIP file:** `{name}` is not a valid zip archive. Please upload a correct file.")
35
+ return
36
+ with zipfile.ZipFile(tmp.name, "r") as zf:
37
+ members = zf.namelist()
38
+ total = len(members)
39
+ if total == 0:
40
+ os.unlink(tmp.name)
41
+ st.error(f"❌ **Empty archive:** `{name}` contains no files.")
42
+ return
43
+ progress = st.progress(0, text=f"Extracting {name} — 0 / {total} files")
44
+ img_count = 0
45
+ for i, member in enumerate(members, 1):
46
+ zf.extract(member, out)
47
+ if member.lower().endswith(IMG_EXTENSIONS):
48
+ img_count += 1
49
+ progress.progress(i / total, text=f"Extracting {name} — {i} / {total} files · {img_count} images")
50
+ progress.progress(1.0, text=f"✓ Extracted {total} files · {img_count} images")
51
+ os.unlink(tmp.name)
52
+ elif name.endswith((".tar", ".tar.gz", ".tgz")) or (name.endswith(".gz") and ".tar." in name):
53
+ tmp = tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False)
54
+ tmp.write(uploaded_file.read()); tmp.flush(); tmp.close()
55
+ uploaded_file.seek(0)
56
+ try:
57
+ with tarfile.open(tmp.name, "r:*") as tf:
58
+ members = tf.getmembers()
59
+ total = len(members)
60
+ if total == 0:
61
+ os.unlink(tmp.name)
62
+ st.error(f"❌ **Empty archive:** `{name}` contains no files.")
63
+ return
64
+ progress = st.progress(0, text=f"Extracting {name} — 0 / {total} files")
65
+ img_count = 0
66
+ for i, member in enumerate(members, 1):
67
+ tf.extract(member, out)
68
+ if member.name.lower().endswith(IMG_EXTENSIONS):
69
+ img_count += 1
70
+ progress.progress(i / total, text=f"Extracting {name} — {i} / {total} files · {img_count} images")
71
+ progress.progress(1.0, text=f"✓ Extracted {total} files · {img_count} images")
72
+ except tarfile.TarError:
73
+ os.unlink(tmp.name)
74
+ st.error(f"❌ **Invalid TAR file:** `{name}` is not a valid tar archive. Please upload a correct file.")
75
+ return
76
+ os.unlink(tmp.name)
77
+ else:
78
+ st.error(f"Unsupported: {name}")
79
+ return
80
+
81
+ # --- Post-extraction: validate, detect format, find images ---
82
+ if not os.listdir(out):
83
+ shutil.rmtree(out, ignore_errors=True)
84
+ st.error(f"❌ **Empty archive:** `{name}` contains no files. Please upload a valid dataset zip.")
85
+ return
86
+
87
+ root = _find_root(out)
88
+ st.session_state["dataset_output_path"] = root
89
+
90
+ fmt = "unknown"
91
+ if _HAS_FLASHDET:
92
+ fmt = detect_dataset_format(root)
93
+ st.session_state["detected_format"] = fmt
94
+
95
+ if fmt in ("txt", "voc"):
96
+ _convert_to_coco(root, split, fmt)
97
+ return
98
+
99
+ img_dir = _find_images(root)
100
+ if img_dir:
101
+ st.session_state[f"{split}_img_path"] = img_dir
102
+ ann = _find_ann(root)
103
+ if ann:
104
+ st.session_state[f"{split}_ann_path"] = ann
105
+ label_dir = _find_labels(root)
106
+ if label_dir:
107
+ st.session_state[f"{split}_label_path"] = label_dir
108
+ st.session_state["dataset_name"] = name.rsplit(".", 1)[0]
109
+
110
+ parent = os.path.dirname(img_dir)
111
+ for sibling, key in [("valid", "val"), ("val", "val"), ("test", "test"), ("train", "train")]:
112
+ sib_path = os.path.join(parent, sibling)
113
+ if os.path.isdir(sib_path) and key != split:
114
+ sk = "val_img_path" if key in ("val",) else f"{key}_img_path"
115
+ if not st.session_state.get(sk):
116
+ st.session_state[sk] = sib_path
117
+
118
+ detected_cls = _auto_detect_classes()
119
+ _existing_cls = st.session_state.get("class_names", "")
120
+ if isinstance(_existing_cls, list):
121
+ _existing_cls = "\n".join(_existing_cls)
122
+ if detected_cls and not _existing_cls.strip():
123
+ st.session_state["class_names"] = "\n".join(detected_cls)
124
+ st.session_state["num_classes"] = len(detected_cls)
125
+
126
+ from flashstudio.utils import flash
127
+ flash(f"Extracted `{name}` to {img_dir}", "success")
128
+ st.rerun()
129
+ else:
130
+ st.session_state[f"{split}_img_path"] = root
131
+ st.session_state["dataset_name"] = name.rsplit(".", 1)[0]
132
+ from flashstudio.utils import flash
133
+ flash(f"Extracted `{name}` (no images dir found, using root)", "warning")
134
+ st.rerun()
135
+ except Exception as e:
136
+ st.error(f"Extract failed: {e}")
137
+
138
+
139
+ def _find_root(base):
140
+ entries = os.listdir(base)
141
+ if len(entries) == 1:
142
+ s = os.path.join(base, entries[0])
143
+ if os.path.isdir(s):
144
+ return s
145
+ for e in entries:
146
+ full = os.path.join(base, e)
147
+ if os.path.isdir(full) and _HAS_FLASHDET and detect_dataset_format(full) != "unknown":
148
+ return full
149
+ return base
150
+
151
+
152
+ def _convert_to_coco(src, split, src_fmt):
153
+ if not _HAS_FLASHDET:
154
+ st.error(_FLASHDET_INSTALL_MSG)
155
+ return
156
+ out = os.path.join(DEFAULT_DATA_DIR, "uploaded", f"{split}_coco")
157
+ cls_names = None
158
+ raw = st.session_state.get("class_names", "")
159
+ if isinstance(raw, list):
160
+ raw = "\n".join(raw)
161
+ if raw.strip():
162
+ cls_names = [c.strip() for c in raw.strip().replace(",", "\n").split("\n") if c.strip()]
163
+ with st.spinner("Converting to COCO..."):
164
+ try:
165
+ stats = convert_dataset(source_dir=src, output_dir=out, target_format="coco", class_names=cls_names)
166
+ if stats.get("status") == "already_in_target_format":
167
+ out = src
168
+ st.session_state["dataset_output_path"] = out
169
+ st.session_state["ann_format"] = "COCO JSON"
170
+ t = os.path.join(out, "train")
171
+ v = os.path.join(out, "valid")
172
+ if not os.path.isdir(v):
173
+ v = os.path.join(out, "val")
174
+ if os.path.isdir(t):
175
+ st.session_state[f"{split}_img_path"] = t
176
+ if os.path.isdir(v):
177
+ st.session_state["val_img_path"] = v
178
+ from flashstudio.utils import flash
179
+ flash("Converted to COCO format", "success")
180
+ st.rerun()
181
+ except Exception as e:
182
+ st.error(f"Conversion failed: {e}")
183
+
184
+
185
+ def _find_images(base):
186
+ best = None
187
+ best_count = 0
188
+ for root, dirs, files in os.walk(base):
189
+ img_count = len([f for f in files if f.lower().endswith(IMG_EXTENSIONS)])
190
+ if img_count > best_count:
191
+ best = root
192
+ best_count = img_count
193
+ for d in dirs:
194
+ if d.lower() in ("images", "imgs"):
195
+ c = os.path.join(root, d)
196
+ for sub_root, _, sub_files in os.walk(c):
197
+ sc = len([f for f in sub_files if f.lower().endswith(IMG_EXTENSIONS)])
198
+ if sc > best_count:
199
+ best = sub_root
200
+ best_count = sc
201
+ return best if best_count >= 1 else None
202
+
203
+
204
+ def _find_ann(base):
205
+ for root, _, files in os.walk(base):
206
+ for f in files:
207
+ if f.endswith(".json") and ("annotation" in f.lower() or "coco" in f.lower()):
208
+ return os.path.join(root, f)
209
+ return None
210
+
211
+
212
+ def _find_labels(base):
213
+ """Find YOLO-format label directory (folder named 'labels' with .txt files)."""
214
+ for root, dirs, files in os.walk(base):
215
+ for d in dirs:
216
+ if d.lower() == "labels":
217
+ lbl_dir = os.path.join(root, d)
218
+ for sub_root, _, sub_files in os.walk(lbl_dir):
219
+ if any(f.endswith(".txt") for f in sub_files):
220
+ return sub_root
221
+ return None
222
+
223
+
224
+ def _use_existing(path, ds_info):
225
+ from flashstudio.utils import flash
226
+ st.session_state["dataset_output_path"] = path
227
+ name = ds_info.get("name", os.path.basename(path))
228
+ st.session_state["dataset_name"] = name
229
+ st.session_state["dataset_classes"] = ds_info.get("classes", ds_info.get("cls", INFER_NUM_CLASSES))
230
+ if ds_info.get("id"):
231
+ st.session_state["dataset_id"] = ds_info["id"]
232
+ t = os.path.join(path, "train")
233
+ v = os.path.join(path, "valid")
234
+ if not os.path.isdir(v):
235
+ v = os.path.join(path, "val")
236
+ if os.path.isdir(t):
237
+ st.session_state["train_img_path"] = t
238
+ if os.path.isdir(v):
239
+ st.session_state["val_img_path"] = v
240
+
241
+ detected_cls = _auto_detect_classes()
242
+ if detected_cls:
243
+ st.session_state["class_names"] = "\n".join(detected_cls)
244
+ st.session_state["num_classes"] = len(detected_cls)
245
+
246
+ flash(f"Dataset `{name}` loaded", "success")
247
+ st.rerun()
248
+
249
+
250
+ def _run_flashdet_download(dataset_id):
251
+ if not _HAS_FLASHDET:
252
+ st.error(_FLASHDET_INSTALL_MSG)
253
+ return
254
+ out = st.session_state.get("download_output_dir") or os.path.join(DEFAULT_DATA_DIR, dataset_id)
255
+ existing = _dataset_already_downloaded(dataset_id, out)
256
+ if existing:
257
+ st.session_state["dataset_output_path"] = existing
258
+ for d, k in [("train", "train_img_path"), ("valid", "val_img_path")]:
259
+ p = os.path.join(existing, d)
260
+ if os.path.isdir(p):
261
+ st.session_state[k] = p
262
+ detected_cls = _auto_detect_classes()
263
+ if detected_cls:
264
+ st.session_state["class_names"] = "\n".join(detected_cls)
265
+ st.session_state["num_classes"] = len(detected_cls)
266
+ st.rerun()
267
+ return
268
+ if not _check_network():
269
+ st.error("Network unavailable.")
270
+ return
271
+ with st.spinner(f"Downloading {dataset_id}..."):
272
+ try:
273
+ result = download_dataset(dataset_id=dataset_id, output_dir=out)
274
+ st.session_state["dataset_output_path"] = result
275
+ for d, k in [("train", "train_img_path"), ("valid", "val_img_path")]:
276
+ p = os.path.join(result, d)
277
+ if os.path.isdir(p):
278
+ st.session_state[k] = p
279
+ detected_cls = _auto_detect_classes()
280
+ if detected_cls:
281
+ st.session_state["class_names"] = "\n".join(detected_cls)
282
+ st.session_state["num_classes"] = len(detected_cls)
283
+ from flashstudio.utils import flash
284
+ flash(f"Downloaded `{dataset_id}` successfully", "success")
285
+ st.rerun()
286
+ except Exception as e:
287
+ st.error(f"Download failed: {e}")
288
+
289
+
290
+ def _run_external_download(ds):
291
+ import urllib.request
292
+ import urllib.error
293
+ import zipfile
294
+ import tempfile
295
+ url, name = ds.get("url", ""), ds.get("name", "dataset")
296
+ if not url or "VisDrone" in url:
297
+ st.info(f"Manual: [{url}]({url})")
298
+ return
299
+ out = os.path.join(DEFAULT_DATA_DIR, name.lower().replace(" ", "_").replace("(", "").replace(")", ""))
300
+ if os.path.isdir(out) and os.listdir(out):
301
+ st.session_state.update({"dataset_output_path": out, "dataset_name": name})
302
+ st.rerun()
303
+ return
304
+ with st.spinner(f"Downloading {name}..."):
305
+ tmp_path = None
306
+ try:
307
+ tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
308
+ tmp_path = tmp.name; tmp.close()
309
+ urllib.request.urlretrieve(url, tmp_path)
310
+ if os.path.getsize(tmp_path) < 1000 or not zipfile.is_zipfile(tmp_path):
311
+ st.error(f"Invalid download. Manual: [{url}]({url})")
312
+ return
313
+ os.makedirs(out, exist_ok=True)
314
+ with zipfile.ZipFile(tmp_path, "r") as zf:
315
+ members = zf.namelist()
316
+ total = len(members)
317
+ progress = st.progress(0, text=f"Extracting {name} — 0 / {total}")
318
+ img_count = 0
319
+ for i, member in enumerate(members, 1):
320
+ zf.extract(member, out)
321
+ if member.lower().endswith(IMG_EXTENSIONS):
322
+ img_count += 1
323
+ progress.progress(i / total, text=f"Extracting {name} — {i} / {total} files · {img_count} images")
324
+ progress.progress(1.0, text=f"Extracted {total} files · {img_count} images")
325
+ st.session_state.update({"dataset_output_path": out, "dataset_name": name})
326
+ for d, k in [("train", "train_img_path"), ("valid", "val_img_path")]:
327
+ p = os.path.join(out, d)
328
+ if os.path.isdir(p):
329
+ st.session_state[k] = p
330
+ st.rerun()
331
+ except Exception as e:
332
+ st.error(f"Error: {e}")
333
+ finally:
334
+ if tmp_path and os.path.exists(tmp_path):
335
+ try:
336
+ os.unlink(tmp_path)
337
+ except OSError:
338
+ pass
@@ -0,0 +1,50 @@
1
+ """FlashStudio — Data Page (ultra-compact, no-scroll)."""
2
+
3
+ import os
4
+ import streamlit as st
5
+
6
+ from flashstudio.constants import IMG_EXTENSIONS
7
+ from flashstudio.pages.data.upload.tab import _render_upload
8
+ from flashstudio.pages.data.download.tab import _render_download
9
+ from flashstudio.pages.data.preview.tab import _render_preview
10
+ from flashstudio.pages.data.verify.tab import _render_verify
11
+
12
+
13
+ def render_data_page():
14
+ from flashstudio.components.styles import render_page_header
15
+ from flashstudio.utils import show_flashes
16
+ render_page_header("", "Data")
17
+ show_flashes()
18
+
19
+ # Status banner
20
+ dataset = st.session_state.get("dataset_name")
21
+ if dataset:
22
+ train_path = st.session_state.get("train_img_path", "")
23
+ val_path = st.session_state.get("val_img_path", "")
24
+ tc = len([f for f in os.listdir(train_path) if f.lower().endswith(IMG_EXTENSIONS)]) if train_path and os.path.isdir(train_path) else 0
25
+ vc = len([f for f in os.listdir(val_path) if f.lower().endswith(IMG_EXTENSIONS)]) if val_path and os.path.isdir(val_path) else 0
26
+ dc1, dc2 = st.columns([8, 1])
27
+ with dc1:
28
+ st.success(f"**{dataset}** — {tc} train / {vc} val images")
29
+ with dc2:
30
+ if st.button("Clear", key="clear_dataset_btn", use_container_width=True):
31
+ from flashstudio.utils import flash
32
+ for k in ["dataset_name", "train_img_path", "val_img_path",
33
+ "train_label_path", "val_label_path",
34
+ "dataset_output_path", "dataset_id", "dataset_classes",
35
+ "class_names", "num_classes",
36
+ "detected_format", "_last_verify_ok"]:
37
+ st.session_state.pop(k, None)
38
+ flash("Dataset cleared", "info")
39
+ st.rerun()
40
+
41
+ tab_upload, tab_download, tab_preview, tab_verify = st.tabs(["Upload", "Download", "Preview", "Verify"])
42
+
43
+ with tab_upload:
44
+ _render_upload()
45
+ with tab_download:
46
+ _render_download()
47
+ with tab_preview:
48
+ _render_preview()
49
+ with tab_verify:
50
+ _render_verify()
@@ -0,0 +1,3 @@
1
+ """Preview tab sub-package."""
2
+
3
+ from flashstudio.pages.data.preview.tab import _render_preview # noqa: F401
@@ -0,0 +1,139 @@
1
+ """Preview tab — image display with COCO/YOLO bbox overlay."""
2
+
3
+ import os
4
+ import json
5
+ import streamlit as st
6
+
7
+ from flashstudio.constants import IMG_EXTENSIONS, BBOX_COLORS_RGB, MAX_PREVIEW_COLS
8
+
9
+
10
+ def _render_preview():
11
+ train_path = st.session_state.get("train_img_path", "")
12
+ val_path = st.session_state.get("val_img_path", "")
13
+ if not train_path or not os.path.isdir(train_path):
14
+ st.info("No dataset loaded yet.")
15
+ return
16
+
17
+ c1, c2, c3 = st.columns([2, 1, 1])
18
+ with c1:
19
+ split = st.radio("Split", ["Train", "Val"], horizontal=True, key="prev_split")
20
+ preview_path = train_path if split == "Train" else val_path
21
+ if not preview_path or not os.path.isdir(preview_path):
22
+ st.warning("Path not set.")
23
+ return
24
+
25
+ all_imgs = sorted([f for f in os.listdir(preview_path) if f.lower().endswith(IMG_EXTENSIONS)])
26
+ if not all_imgs:
27
+ st.warning("No images.")
28
+ return
29
+
30
+ with c2:
31
+ n = st.slider("Count", 1, min(12, len(all_imgs)), min(4, len(all_imgs)), key="prev_n")
32
+ with c3:
33
+ bbox = st.checkbox("Boxes", value=True, key="prev_bb")
34
+
35
+ if "prev_start" not in st.session_state:
36
+ st.session_state["prev_start"] = 0
37
+ if st.session_state["prev_start"] >= len(all_imgs):
38
+ st.session_state["prev_start"] = 0
39
+ start = st.session_state["prev_start"]
40
+
41
+ nav_prev, nav_info, nav_next = st.columns([1, 3, 1])
42
+ with nav_prev:
43
+ if st.button("Prev", disabled=(start <= 0), key="prev_btn"):
44
+ st.session_state["prev_start"] = max(0, start - n)
45
+ st.rerun()
46
+ with nav_info:
47
+ end_idx = min(start + n, len(all_imgs))
48
+ st.markdown(f'<p style="text-align:center;margin:0;padding:0.3rem 0;color:#6B7280;">'
49
+ f'{start + 1}–{end_idx} of {len(all_imgs)}</p>', unsafe_allow_html=True)
50
+ with nav_next:
51
+ if st.button("Next", disabled=(start + n >= len(all_imgs)), key="next_btn"):
52
+ st.session_state["prev_start"] = min(start + n, max(len(all_imgs) - n, 0))
53
+ st.rerun()
54
+
55
+ # Load COCO annotations if available
56
+ ann_file = os.path.join(preview_path, "_annotations.coco.json")
57
+ ann_data = None
58
+ if os.path.isfile(ann_file):
59
+ try:
60
+ with open(ann_file) as f:
61
+ ann_data = json.load(f)
62
+ except Exception:
63
+ pass
64
+
65
+ img_id_map, ann_by_img, cat_map = {}, {}, {}
66
+ if ann_data:
67
+ for c in ann_data.get("categories", []):
68
+ cat_map[c["id"]] = c["name"]
69
+ for im in ann_data.get("images", []):
70
+ img_id_map[im["file_name"]] = im["id"]
71
+ for a in ann_data.get("annotations", []):
72
+ ann_by_img.setdefault(a["image_id"], []).append(a)
73
+
74
+ # YOLO label directory — check sibling 'labels' folder or session state
75
+ yolo_label_dir = st.session_state.get(f"{split.lower()}_label_path", "")
76
+ if not yolo_label_dir or not os.path.isdir(yolo_label_dir):
77
+ candidate = preview_path.replace("/images/", "/labels/").replace("\\images\\", "\\labels\\")
78
+ if os.path.isdir(candidate):
79
+ yolo_label_dir = candidate
80
+ else:
81
+ yolo_label_dir = ""
82
+
83
+ from PIL import Image, ImageDraw
84
+ selected = all_imgs[start:start + n]
85
+
86
+ total_boxes = 0
87
+ for row in range(0, len(selected), MAX_PREVIEW_COLS):
88
+ cols = st.columns(MAX_PREVIEW_COLS)
89
+ for j, col in enumerate(cols):
90
+ idx = row + j
91
+ if idx >= len(selected):
92
+ break
93
+ img_name = selected[idx]
94
+ with col:
95
+ try:
96
+ img = Image.open(os.path.join(preview_path, img_name)).convert("RGB")
97
+ w_img, h_img = img.size
98
+ if bbox:
99
+ draw = ImageDraw.Draw(img)
100
+ drawn = False
101
+ if ann_data and img_name in img_id_map:
102
+ for a in ann_by_img.get(img_id_map[img_name], []):
103
+ b = a.get("bbox", [])
104
+ if len(b) == 4:
105
+ x, y, w, h = b
106
+ color = BBOX_COLORS_RGB[a.get("category_id", 0) % len(BBOX_COLORS_RGB)]
107
+ draw.rectangle([x, y, x+w, y+h], outline=color, width=2)
108
+ total_boxes += 1
109
+ drawn = True
110
+ if not drawn and yolo_label_dir:
111
+ lbl_name = os.path.splitext(img_name)[0] + ".txt"
112
+ lbl_path = os.path.join(yolo_label_dir, lbl_name)
113
+ if os.path.isfile(lbl_path):
114
+ with open(lbl_path) as lf:
115
+ for line in lf:
116
+ parts = line.strip().split()
117
+ if len(parts) >= 5:
118
+ cls_id = int(parts[0])
119
+ cx, cy, bw, bh = float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])
120
+ x1 = int((cx - bw / 2) * w_img)
121
+ y1 = int((cy - bh / 2) * h_img)
122
+ x2 = int((cx + bw / 2) * w_img)
123
+ y2 = int((cy + bh / 2) * h_img)
124
+ color = BBOX_COLORS_RGB[cls_id % len(BBOX_COLORS_RGB)]
125
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=2)
126
+ total_boxes += 1
127
+ st.image(img, caption=f"{img_name[:15]} {w_img}x{h_img}", use_container_width=True)
128
+ except Exception as e:
129
+ st.error(str(e)[:40])
130
+
131
+ m1, m2, m3 = st.columns(3)
132
+ with m1:
133
+ st.metric("Images", len(all_imgs))
134
+ with m2:
135
+ ann_count = len(ann_data.get("annotations", [])) if ann_data else total_boxes
136
+ st.metric("Annotations", ann_count)
137
+ with m3:
138
+ cat_count = len(cat_map) if cat_map else "—"
139
+ st.metric("Categories", cat_count)
@@ -0,0 +1,3 @@
1
+ """Upload tab sub-package."""
2
+
3
+ from flashstudio.pages.data.upload.tab import _render_upload # noqa: F401