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,27 @@
|
|
|
1
|
+
"""FlashStudio — Config building/applying helpers."""
|
|
2
|
+
|
|
3
|
+
import streamlit as st
|
|
4
|
+
from flashstudio.utils.config import apply_training_config
|
|
5
|
+
from flashstudio.constants import (
|
|
6
|
+
ARCH_FAMILIES, DEFAULT_MODEL_ARCH,
|
|
7
|
+
TRAIN_EPOCHS, TRAIN_BATCH_SIZE, TRAIN_LR, TRAIN_IMG_SIZE,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _build_cfg():
|
|
12
|
+
return {
|
|
13
|
+
"model": {"family": st.session_state.get("arch_family", ARCH_FAMILIES[0]), "variant": st.session_state.get("model_arch", DEFAULT_MODEL_ARCH)},
|
|
14
|
+
"training": {"epochs": st.session_state.get("epochs", TRAIN_EPOCHS), "batch_size": st.session_state.get("batch_size", TRAIN_BATCH_SIZE),
|
|
15
|
+
"lr": st.session_state.get("lr", TRAIN_LR), "img_size": st.session_state.get("img_size", TRAIN_IMG_SIZE)},
|
|
16
|
+
"augmentation": {"mosaic": st.session_state.get("aug_mosaic", True), "mixup": st.session_state.get("aug_mixup", False)},
|
|
17
|
+
"advanced": {"amp": st.session_state.get("amp", True), "compile": st.session_state.get("compile_model", False)},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _apply_cfg(config):
|
|
22
|
+
if "model" in config:
|
|
23
|
+
m = config["model"]
|
|
24
|
+
for k, sk in [("family", "arch_family"), ("variant", "model_arch"), ("pretrained", "pretrain_option")]:
|
|
25
|
+
if k in m:
|
|
26
|
+
st.session_state[sk] = m[k]
|
|
27
|
+
apply_training_config(config)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Shared helpers for the training page."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import glob as glob_module
|
|
5
|
+
import streamlit as st
|
|
6
|
+
from flashstudio.constants import TRAINING_LOG_GLOB
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_save_dir():
|
|
10
|
+
"""Single source of truth: Model > Advanced > Save Dir. Used across the whole project."""
|
|
11
|
+
from flashstudio.utils import get_state
|
|
12
|
+
val = get_state("save_dir")
|
|
13
|
+
if not val:
|
|
14
|
+
from flashstudio.utils import DEFAULTS
|
|
15
|
+
val = DEFAULTS["save_dir"]
|
|
16
|
+
return val
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _find_log_file(run_dir: str):
|
|
20
|
+
"""Find the best training log file — prefer the largest (stdout-redirected has more data)."""
|
|
21
|
+
logs = glob_module.glob(os.path.join(run_dir, TRAINING_LOG_GLOB))
|
|
22
|
+
if not logs:
|
|
23
|
+
return None
|
|
24
|
+
return max(logs, key=lambda p: os.path.getsize(p))
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Training process controls — stop, pause, resume."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _stop_training():
|
|
9
|
+
"""Stop the training subprocess by killing its process tree."""
|
|
10
|
+
from flashstudio.utils import flash
|
|
11
|
+
|
|
12
|
+
pid = st.session_state.get("training_pid")
|
|
13
|
+
if pid:
|
|
14
|
+
try:
|
|
15
|
+
os.kill(pid, signal.SIGTERM)
|
|
16
|
+
flash(f"Training stopped (PID {pid})", "success")
|
|
17
|
+
except ProcessLookupError:
|
|
18
|
+
flash("Training process already finished", "info")
|
|
19
|
+
except PermissionError:
|
|
20
|
+
try:
|
|
21
|
+
os.kill(pid, signal.SIGKILL)
|
|
22
|
+
flash(f"Training force-killed (PID {pid})", "warning")
|
|
23
|
+
except Exception as e:
|
|
24
|
+
flash(f"Could not kill process {pid}: {e}", "error")
|
|
25
|
+
except Exception as e:
|
|
26
|
+
flash(f"Error stopping training: {e}", "error")
|
|
27
|
+
else:
|
|
28
|
+
flash("No training process to stop", "warning")
|
|
29
|
+
|
|
30
|
+
st.session_state["training_active"] = False
|
|
31
|
+
st.session_state["training_status"] = "Stopped"
|
|
32
|
+
st.session_state["training_paused"] = False
|
|
33
|
+
st.session_state["training_pid"] = None
|
|
34
|
+
fh = st.session_state.pop("_log_file_handle", None)
|
|
35
|
+
if fh and not fh.closed:
|
|
36
|
+
try:
|
|
37
|
+
fh.close()
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
st.rerun()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _pause_training():
|
|
44
|
+
"""Pause the training subprocess by sending SIGSTOP."""
|
|
45
|
+
from flashstudio.utils import flash
|
|
46
|
+
|
|
47
|
+
pid = st.session_state.get("training_pid")
|
|
48
|
+
if pid:
|
|
49
|
+
try:
|
|
50
|
+
os.kill(pid, signal.SIGSTOP)
|
|
51
|
+
st.session_state["training_paused"] = True
|
|
52
|
+
st.session_state["training_status"] = "Paused"
|
|
53
|
+
flash(f"Training paused (PID {pid}). GPU memory still held.", "success")
|
|
54
|
+
except ProcessLookupError:
|
|
55
|
+
flash("Training process already finished", "info")
|
|
56
|
+
st.session_state["training_active"] = False
|
|
57
|
+
st.session_state["training_paused"] = False
|
|
58
|
+
except Exception as e:
|
|
59
|
+
flash(f"Could not pause training: {e}", "error")
|
|
60
|
+
else:
|
|
61
|
+
flash("No training process found to pause", "warning")
|
|
62
|
+
st.rerun()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _resume_active_training():
|
|
66
|
+
"""Resume a paused training subprocess by sending SIGCONT."""
|
|
67
|
+
from flashstudio.utils import flash
|
|
68
|
+
|
|
69
|
+
pid = st.session_state.get("training_pid")
|
|
70
|
+
if pid:
|
|
71
|
+
try:
|
|
72
|
+
os.kill(pid, signal.SIGCONT)
|
|
73
|
+
st.session_state["training_paused"] = False
|
|
74
|
+
st.session_state["training_status"] = "Running"
|
|
75
|
+
flash(f"Training resumed (PID {pid})", "success")
|
|
76
|
+
except ProcessLookupError:
|
|
77
|
+
flash("Training process already finished", "info")
|
|
78
|
+
st.session_state["training_active"] = False
|
|
79
|
+
st.session_state["training_paused"] = False
|
|
80
|
+
except Exception as e:
|
|
81
|
+
flash(f"Could not resume training: {e}", "error")
|
|
82
|
+
else:
|
|
83
|
+
flash("No training process found to resume", "warning")
|
|
84
|
+
st.rerun()
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Training launch dialogs — clean workspace, resume, folder management, config."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import yaml
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
from flashstudio.utils.filesystem import dir_size_str
|
|
8
|
+
from flashstudio.utils.config import build_training_config, apply_training_config
|
|
9
|
+
from flashstudio.constants import (
|
|
10
|
+
CKPT_BEST, CKPT_BEST_INFERENCE, CKPT_BEST_FP16,
|
|
11
|
+
CKPT_FINAL_INFERENCE, CKPT_FINAL_FP16, CKPT_LAST, CKPT_LAST_INFERENCE,
|
|
12
|
+
TRAINING_LOG_CSV,
|
|
13
|
+
)
|
|
14
|
+
from flashstudio.pages.training._common import _get_save_dir
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_build_config_dict = build_training_config
|
|
18
|
+
_apply_config_dict = apply_training_config
|
|
19
|
+
_get_folder_size_str = dir_size_str
|
|
20
|
+
_get_dir_size = dir_size_str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _render_folder_dialogs(runs_root: str, existing_folders: list):
|
|
24
|
+
"""Render rename/delete dialogs for folder management."""
|
|
25
|
+
import shutil
|
|
26
|
+
from flashstudio.utils import flash
|
|
27
|
+
|
|
28
|
+
if st.session_state.get("wf_rename_target"):
|
|
29
|
+
target = st.session_state["wf_rename_target"]
|
|
30
|
+
with st.container(border=True):
|
|
31
|
+
rename_val = st.text_input("New name", value=target, key="rename_wf_input")
|
|
32
|
+
rc1, rc2 = st.columns(2)
|
|
33
|
+
with rc1:
|
|
34
|
+
if st.button("Rename", type="primary", key="do_rename_wf"):
|
|
35
|
+
if rename_val and rename_val != target:
|
|
36
|
+
old_path = os.path.join(runs_root, target)
|
|
37
|
+
new_path = os.path.join(runs_root, rename_val)
|
|
38
|
+
if os.path.exists(new_path):
|
|
39
|
+
flash(f"Rename failed: `{rename_val}` already exists", "error")
|
|
40
|
+
else:
|
|
41
|
+
os.rename(old_path, new_path)
|
|
42
|
+
if st.session_state.get("active_workflow_folder") == target:
|
|
43
|
+
st.session_state["active_workflow_folder"] = rename_val
|
|
44
|
+
st.session_state["run_name"] = rename_val
|
|
45
|
+
flash(f"Renamed `{target}` → `{rename_val}`", "success")
|
|
46
|
+
st.session_state.pop("wf_rename_target", None)
|
|
47
|
+
st.rerun()
|
|
48
|
+
with rc2:
|
|
49
|
+
if st.button("Cancel", key="cancel_rename_wf"):
|
|
50
|
+
st.session_state.pop("wf_rename_target", None)
|
|
51
|
+
st.rerun()
|
|
52
|
+
|
|
53
|
+
if st.session_state.get("wf_delete_target"):
|
|
54
|
+
target = st.session_state["wf_delete_target"]
|
|
55
|
+
with st.container(border=True):
|
|
56
|
+
st.error(f"Delete **{target}** and all contents?")
|
|
57
|
+
dc1, dc2 = st.columns(2)
|
|
58
|
+
with dc1:
|
|
59
|
+
if st.button("Delete", type="primary", key="confirm_delete_wf"):
|
|
60
|
+
shutil.rmtree(os.path.join(runs_root, target), ignore_errors=True)
|
|
61
|
+
if st.session_state.get("active_workflow_folder") == target:
|
|
62
|
+
st.session_state.pop("active_workflow_folder", None)
|
|
63
|
+
st.session_state.pop("wf_delete_target", None)
|
|
64
|
+
flash(f"Deleted `{target}`", "success")
|
|
65
|
+
st.rerun()
|
|
66
|
+
with dc2:
|
|
67
|
+
if st.button("Cancel", key="cancel_delete_wf"):
|
|
68
|
+
st.session_state.pop("wf_delete_target", None)
|
|
69
|
+
st.rerun()
|
|
70
|
+
|
|
71
|
+
if st.session_state.get("show_delete_all_wf_folders"):
|
|
72
|
+
with st.container(border=True):
|
|
73
|
+
st.error(f"Delete ALL {len(existing_folders)} folders? This is permanent!")
|
|
74
|
+
dc1, dc2 = st.columns(2)
|
|
75
|
+
with dc1:
|
|
76
|
+
if st.button("DELETE ALL", type="primary", key="confirm_delete_all_wf"):
|
|
77
|
+
for f in existing_folders:
|
|
78
|
+
shutil.rmtree(os.path.join(runs_root, f), ignore_errors=True)
|
|
79
|
+
st.session_state.pop("active_workflow_folder", None)
|
|
80
|
+
st.session_state.pop("show_delete_all_wf_folders", None)
|
|
81
|
+
st.rerun()
|
|
82
|
+
with dc2:
|
|
83
|
+
if st.button("Cancel", key="cancel_delete_all_wf"):
|
|
84
|
+
st.session_state.pop("show_delete_all_wf_folders", None)
|
|
85
|
+
st.rerun()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _render_config_file_dialog(run_path: str):
|
|
89
|
+
"""Compact save/load config dialog."""
|
|
90
|
+
with st.container(border=True):
|
|
91
|
+
tab_save, tab_load = st.tabs(["Save", "Load"])
|
|
92
|
+
with tab_save:
|
|
93
|
+
config = _build_config_dict()
|
|
94
|
+
yaml_str = yaml.dump(config, default_flow_style=False, sort_keys=False)
|
|
95
|
+
with st.expander("YAML Preview"):
|
|
96
|
+
st.code(yaml_str, language="yaml")
|
|
97
|
+
sc1, sc2 = st.columns(2)
|
|
98
|
+
with sc1:
|
|
99
|
+
st.download_button("Download", yaml_str, file_name="config.yaml", mime="text/yaml",
|
|
100
|
+
use_container_width=True, type="primary")
|
|
101
|
+
with sc2:
|
|
102
|
+
if st.button("Save to Run", use_container_width=True, key="save_config_to_run"):
|
|
103
|
+
os.makedirs(run_path, exist_ok=True)
|
|
104
|
+
with open(os.path.join(run_path, "config.yaml"), "w") as f:
|
|
105
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
106
|
+
st.success("Saved")
|
|
107
|
+
with tab_load:
|
|
108
|
+
load_method = st.radio("From", ["Upload", "Path", "Runs"], key="config_load_method", horizontal=True)
|
|
109
|
+
config_to_load = None
|
|
110
|
+
if load_method == "Upload":
|
|
111
|
+
uploaded = st.file_uploader("YAML", type=["yaml", "yml"], key="config_file_upload")
|
|
112
|
+
if uploaded:
|
|
113
|
+
try:
|
|
114
|
+
config_to_load = yaml.safe_load(uploaded.read().decode("utf-8"))
|
|
115
|
+
except Exception as e:
|
|
116
|
+
st.error(str(e)[:40])
|
|
117
|
+
elif load_method == "Path":
|
|
118
|
+
cp = st.text_input("Path", placeholder="/path/config.yaml", key="config_path_input")
|
|
119
|
+
if cp and os.path.isfile(cp):
|
|
120
|
+
try:
|
|
121
|
+
with open(cp) as f:
|
|
122
|
+
config_to_load = yaml.safe_load(f.read())
|
|
123
|
+
except Exception as e:
|
|
124
|
+
st.error(str(e)[:40])
|
|
125
|
+
else:
|
|
126
|
+
sd = _get_save_dir()
|
|
127
|
+
parent = sd if os.path.isdir(sd) else ""
|
|
128
|
+
found = [(d, os.path.join(parent, d, "config.yaml"))
|
|
129
|
+
for d in (os.listdir(parent) if parent and os.path.isdir(parent) else [])
|
|
130
|
+
if os.path.isfile(os.path.join(parent, d, "config.yaml"))]
|
|
131
|
+
if found:
|
|
132
|
+
sel = st.selectbox("Run", range(len(found)), format_func=lambda i: found[i][0], key="config_from_run_select")
|
|
133
|
+
with open(found[sel][1]) as f:
|
|
134
|
+
config_to_load = yaml.safe_load(f.read())
|
|
135
|
+
if config_to_load:
|
|
136
|
+
if st.button("Apply", type="primary", key="apply_loaded_config", use_container_width=True):
|
|
137
|
+
_apply_config_dict(config_to_load)
|
|
138
|
+
st.rerun()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _render_clean_workspace_dialog():
|
|
142
|
+
"""Compact workspace cleanup."""
|
|
143
|
+
with st.container(border=True):
|
|
144
|
+
parent_dir = _get_save_dir()
|
|
145
|
+
|
|
146
|
+
if not os.path.isdir(parent_dir):
|
|
147
|
+
st.info("Nothing to clean.")
|
|
148
|
+
if st.button("Close", key="close_clean"):
|
|
149
|
+
st.session_state["show_clean_dialog"] = False
|
|
150
|
+
st.rerun()
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
runs = [{"name": d, "path": os.path.join(parent_dir, d), "size": _get_dir_size(os.path.join(parent_dir, d)),
|
|
154
|
+
"has_best": (os.path.isfile(os.path.join(parent_dir, d, CKPT_BEST))
|
|
155
|
+
or os.path.isfile(os.path.join(parent_dir, d, CKPT_BEST_INFERENCE)))}
|
|
156
|
+
for d in sorted(os.listdir(parent_dir)) if os.path.isdir(os.path.join(parent_dir, d))]
|
|
157
|
+
if not runs:
|
|
158
|
+
st.info("No runs."); return
|
|
159
|
+
|
|
160
|
+
clean_mode = st.radio("Mode", ["Selected", "Keep best only", "Delete incomplete", "Full reset"],
|
|
161
|
+
key="clean_mode", horizontal=True)
|
|
162
|
+
|
|
163
|
+
if clean_mode == "Selected":
|
|
164
|
+
selected = st.multiselect("Runs", [r["name"] for r in runs], key="runs_to_delete")
|
|
165
|
+
if selected and st.button("Delete", type="primary", key="do_delete_selected"):
|
|
166
|
+
import shutil
|
|
167
|
+
for n in selected:
|
|
168
|
+
shutil.rmtree(os.path.join(parent_dir, n), ignore_errors=True)
|
|
169
|
+
st.session_state["show_clean_dialog"] = False; st.rerun()
|
|
170
|
+
elif clean_mode == "Keep best only":
|
|
171
|
+
if st.button("Clean", type="primary", key="do_keep_best"):
|
|
172
|
+
for r in runs:
|
|
173
|
+
_cleanup_run_keep_best(r["path"])
|
|
174
|
+
st.session_state["show_clean_dialog"] = False; st.rerun()
|
|
175
|
+
elif clean_mode == "Delete incomplete":
|
|
176
|
+
incomplete = [r for r in runs if not r["has_best"]]
|
|
177
|
+
st.caption(f"{len(incomplete)} incomplete")
|
|
178
|
+
if incomplete and st.button("Delete", type="primary", key="do_delete_incomplete"):
|
|
179
|
+
import shutil
|
|
180
|
+
for r in incomplete:
|
|
181
|
+
shutil.rmtree(r["path"])
|
|
182
|
+
st.session_state["show_clean_dialog"] = False; st.rerun()
|
|
183
|
+
elif clean_mode == "Full reset":
|
|
184
|
+
if st.button("DELETE ALL", type="primary", key="do_delete_all"):
|
|
185
|
+
import shutil
|
|
186
|
+
shutil.rmtree(parent_dir); os.makedirs(parent_dir, exist_ok=True)
|
|
187
|
+
st.session_state["show_clean_dialog"] = False; st.rerun()
|
|
188
|
+
|
|
189
|
+
if st.button("Cancel", key="cancel_clean"):
|
|
190
|
+
st.session_state["show_clean_dialog"] = False; st.rerun()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _render_resume_dialog():
|
|
194
|
+
"""Compact resume dialog."""
|
|
195
|
+
with st.container(border=True):
|
|
196
|
+
parent_dir = _get_save_dir()
|
|
197
|
+
resumable = []
|
|
198
|
+
if os.path.isdir(parent_dir):
|
|
199
|
+
for d in sorted(os.listdir(parent_dir), reverse=True):
|
|
200
|
+
full = os.path.join(parent_dir, d)
|
|
201
|
+
ckpt = os.path.join(full, CKPT_LAST)
|
|
202
|
+
if not os.path.isfile(ckpt):
|
|
203
|
+
ckpt = os.path.join(full, CKPT_LAST_INFERENCE)
|
|
204
|
+
if os.path.isdir(full) and os.path.isfile(ckpt):
|
|
205
|
+
resumable.append({"name": d, "path": full, "ckpt": ckpt})
|
|
206
|
+
if not resumable:
|
|
207
|
+
st.info("No resumable runs.")
|
|
208
|
+
if st.button("Close", key="close_resume"):
|
|
209
|
+
st.session_state["show_resume_dialog"] = False; st.rerun()
|
|
210
|
+
return
|
|
211
|
+
rc1, rc2, rc3 = st.columns([4, 1, 1])
|
|
212
|
+
with rc1:
|
|
213
|
+
selected = st.selectbox("Run", [r["name"] for r in resumable], key="resume_run_select", label_visibility="collapsed")
|
|
214
|
+
with rc2:
|
|
215
|
+
if st.button("Resume", type="primary", key="do_resume"):
|
|
216
|
+
from flashstudio.pages.training.launch.runner import _start_training
|
|
217
|
+
for r in resumable:
|
|
218
|
+
if r["name"] == selected:
|
|
219
|
+
st.session_state.update({"resume_training": True, "resume_path": r["ckpt"],
|
|
220
|
+
"active_run_path": r["path"], "training_active": True,
|
|
221
|
+
"training_status": "Resuming", "show_resume_dialog": False})
|
|
222
|
+
_start_training(); break
|
|
223
|
+
with rc3:
|
|
224
|
+
if st.button("Cancel", key="cancel_resume"):
|
|
225
|
+
st.session_state["show_resume_dialog"] = False; st.rerun()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _cleanup_run_keep_best(run_dir: str) -> int:
|
|
229
|
+
"""Remove non-essential files from a run, keeping final/best models and results."""
|
|
230
|
+
from flashstudio.constants import ONNX_MODEL_FILE, ONNX_DATA_FILE, VIS_DIR_NAMES, GT_VERIFICATION_DIR
|
|
231
|
+
keep = {
|
|
232
|
+
CKPT_BEST, TRAINING_LOG_CSV,
|
|
233
|
+
CKPT_BEST_INFERENCE, CKPT_BEST_FP16,
|
|
234
|
+
CKPT_FINAL_INFERENCE, CKPT_FINAL_FP16,
|
|
235
|
+
ONNX_MODEL_FILE, ONNX_DATA_FILE,
|
|
236
|
+
}
|
|
237
|
+
removed = 0
|
|
238
|
+
for f in os.listdir(run_dir):
|
|
239
|
+
fpath = os.path.join(run_dir, f)
|
|
240
|
+
if os.path.isfile(fpath) and f not in keep and not f.endswith(".log"):
|
|
241
|
+
os.remove(fpath)
|
|
242
|
+
removed += 1
|
|
243
|
+
elif os.path.isdir(fpath) and f in (*VIS_DIR_NAMES, GT_VERIFICATION_DIR):
|
|
244
|
+
import shutil
|
|
245
|
+
shutil.rmtree(fpath)
|
|
246
|
+
removed += 1
|
|
247
|
+
return removed
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Pre-flight validation checks before training."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import streamlit as st
|
|
5
|
+
|
|
6
|
+
from flashstudio.utils.device import has_cuda
|
|
7
|
+
from flashstudio.pages.training._common import _get_save_dir
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run_preflight_checks() -> list:
|
|
11
|
+
"""Run pre-flight validation checks before training. Returns list of (label, ok, msg)."""
|
|
12
|
+
checks = []
|
|
13
|
+
|
|
14
|
+
train_path = st.session_state.get("train_img_path", "")
|
|
15
|
+
if train_path and os.path.isdir(train_path):
|
|
16
|
+
checks.append(("Dataset", True, ""))
|
|
17
|
+
else:
|
|
18
|
+
checks.append(("Dataset", False, "No train data path"))
|
|
19
|
+
|
|
20
|
+
if train_path and os.path.isdir(train_path):
|
|
21
|
+
ann_found = False
|
|
22
|
+
# Check in train_img_path itself
|
|
23
|
+
ann_file = os.path.join(train_path, "_annotations.coco.json")
|
|
24
|
+
json_files = [f for f in os.listdir(train_path) if f.endswith(".json")]
|
|
25
|
+
if os.path.isfile(ann_file) or json_files:
|
|
26
|
+
ann_found = True
|
|
27
|
+
# Check parent directory (common layout: train/images/ + train/_annotations.coco.json)
|
|
28
|
+
if not ann_found:
|
|
29
|
+
parent = os.path.dirname(train_path)
|
|
30
|
+
parent_ann = os.path.join(parent, "_annotations.coco.json")
|
|
31
|
+
parent_jsons = [f for f in os.listdir(parent) if f.endswith(".json")] if os.path.isdir(parent) else []
|
|
32
|
+
if os.path.isfile(parent_ann) or parent_jsons:
|
|
33
|
+
ann_found = True
|
|
34
|
+
# Check session state annotation path
|
|
35
|
+
if not ann_found and st.session_state.get("train_ann_path"):
|
|
36
|
+
if os.path.isfile(st.session_state["train_ann_path"]):
|
|
37
|
+
ann_found = True
|
|
38
|
+
checks.append(("Annotations", ann_found, "" if ann_found else "No COCO JSON found"))
|
|
39
|
+
else:
|
|
40
|
+
checks.append(("Annotations", False, "No data path"))
|
|
41
|
+
|
|
42
|
+
nc = st.session_state.get("num_classes", 0)
|
|
43
|
+
cls_names = st.session_state.get("class_names", "")
|
|
44
|
+
if isinstance(cls_names, list):
|
|
45
|
+
cls_names = "\n".join(cls_names)
|
|
46
|
+
if nc and nc > 0:
|
|
47
|
+
checks.append(("Classes", True, f"{nc} classes"))
|
|
48
|
+
elif isinstance(cls_names, str) and cls_names.strip():
|
|
49
|
+
n = len([c for c in cls_names.strip().split("\n") if c.strip()])
|
|
50
|
+
checks.append(("Classes", True, f"{n} classes"))
|
|
51
|
+
else:
|
|
52
|
+
checks.append(("Classes", False, "No classes — go to Data → Upload"))
|
|
53
|
+
|
|
54
|
+
from flashstudio.utils import get_state
|
|
55
|
+
arch = get_state("model_arch") or ""
|
|
56
|
+
if arch:
|
|
57
|
+
checks.append(("Model", True, ""))
|
|
58
|
+
else:
|
|
59
|
+
checks.append(("Model", False, "No model selected"))
|
|
60
|
+
|
|
61
|
+
if has_cuda():
|
|
62
|
+
checks.append(("GPU", True, "CUDA"))
|
|
63
|
+
else:
|
|
64
|
+
checks.append(("GPU", True, "CPU (slow)"))
|
|
65
|
+
|
|
66
|
+
save_dir = _get_save_dir()
|
|
67
|
+
try:
|
|
68
|
+
stat = os.statvfs(os.path.dirname(save_dir) if not os.path.isdir(save_dir) else save_dir)
|
|
69
|
+
free_gb = (stat.f_bavail * stat.f_frsize) / (1024**3)
|
|
70
|
+
if free_gb > 1:
|
|
71
|
+
checks.append(("Disk", True, ""))
|
|
72
|
+
else:
|
|
73
|
+
checks.append(("Disk", False, f"Only {free_gb:.1f} GB free"))
|
|
74
|
+
except (OSError, AttributeError):
|
|
75
|
+
checks.append(("Disk", True, ""))
|
|
76
|
+
|
|
77
|
+
return checks
|