diff --git a/src/yolo_webui/app.py b/src/yolo_webui/app.py index 9b198ad..ae5454f 100755 --- a/src/yolo_webui/app.py +++ b/src/yolo_webui/app.py @@ -28,6 +28,17 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(me logger = logging.getLogger("yolo_webui") SESSION_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,64}$") +try: + from ultralytics import settings as _ultralytics_settings + _workspace_root = Path.cwd().resolve() + _ultralytics_settings.update({ + "runs_dir": str((_workspace_root / "runs").resolve()), + "datasets_dir": str((_workspace_root / "datasets").resolve()), + "weights_dir": str((_workspace_root / "models").resolve()), + }) +except Exception as _exc: + logger.debug(f"Ultralytics settings init skipped: {_exc}") + @dataclass class LiveState: diff --git a/src/yolo_webui/config.py b/src/yolo_webui/config.py index 4476028..cfa4a50 100755 --- a/src/yolo_webui/config.py +++ b/src/yolo_webui/config.py @@ -364,6 +364,10 @@ class TrainingConfig: def train_kwargs(self) -> dict[str, str | int | float | bool]: """Convert the form values to arguments accepted by YOLO.train().""" + project_path = Path(self.project.strip() or "runs/train") + if not project_path.is_absolute(): + project_path = (Path.cwd() / project_path).resolve() + values: dict[str, str | int | float | bool] = { "data": self.dataset.strip(), "epochs": self.epochs, @@ -371,7 +375,7 @@ class TrainingConfig: "batch": self.batch_size, "workers": self.workers, "patience": self.patience, - "project": self.project.strip() or "runs/train", + "project": str(project_path), # Enable verbose output so users see active progress and losses in the log console. "verbose": True, } diff --git a/src/yolo_webui/static/app.js b/src/yolo_webui/static/app.js index dc97f64..9251863 100755 --- a/src/yolo_webui/static/app.js +++ b/src/yolo_webui/static/app.js @@ -125,10 +125,6 @@ document.addEventListener('DOMContentLoaded', () => { const autoscrollCheck = document.getElementById('autoscroll'); const clearLogBtn = document.getElementById('clear-log-btn'); - // Chart - const ctx = document.getElementById('metricsChart').getContext('2d'); - let metricsChart = null; - // State variables let trainingTimer = null; let secondsElapsed = 0; @@ -268,78 +264,6 @@ document.addEventListener('DOMContentLoaded', () => { logContainer.innerHTML = ''; }); - // --- Chart.js Integration --- - function initChart(datasets = []) { - if (metricsChart) { - metricsChart.destroy(); - } - - metricsChart = new Chart(ctx, { - type: 'line', - data: { - labels: [], - datasets: datasets - }, - options: { - responsive: true, - maintainAspectRatio: false, - scales: { - x: { - title: { display: true, text: 'Эпоха', color: '#a1a1aa' }, - grid: { color: '#27272a' }, - ticks: { color: '#a1a1aa' } - }, - y: { - title: { display: true, text: 'Значение', color: '#a1a1aa' }, - grid: { color: '#27272a' }, - ticks: { color: '#a1a1aa' } - } - }, - plugins: { - legend: { - labels: { color: '#f4f4f5', font: { family: 'Outfit' } } - } - } - } - }); - } - - function updateChart(epoch, metrics) { - if (!metricsChart) { - initChart(); - } - - let labelIndex = metricsChart.data.labels.indexOf(epoch); - if (labelIndex === -1) { - metricsChart.data.labels.push(epoch); - labelIndex = metricsChart.data.labels.length - 1; - metricsChart.data.datasets.forEach(dataset => dataset.data.push(null)); - } - - const colors = ['#f97316', '#10b981', '#3b82f6', '#eab308', '#a855f7']; - Object.entries(metrics).forEach(([key, value]) => { - if (key === 'epoch') return; - - let dataset = metricsChart.data.datasets.find(item => item.label === key); - if (!dataset) { - const color = colors[metricsChart.data.datasets.length % colors.length]; - dataset = { - label: key, - data: Array(metricsChart.data.labels.length).fill(null), - borderColor: color, - backgroundColor: color + '22', - tension: 0.15, - fill: false - }; - metricsChart.data.datasets.push(dataset); - } - - dataset.data[labelIndex] = value; - }); - - metricsChart.update(); - } - // --- Timer UI --- function startTimer() { stopTimer(); @@ -427,14 +351,6 @@ document.addEventListener('DOMContentLoaded', () => { addLogLine(msg, level); }); - // Draw initial chart points - initChart(); - if (Array.isArray(data.metrics) && data.metrics.length > 0) { - data.metrics.forEach(m => { - updateChart(m.epoch, m); - }); - } - // Sync progress if (data.status === 'training' || data.status === 'stopping') { updateProgress(data.epoch, data.total_epochs); @@ -468,9 +384,6 @@ document.addEventListener('DOMContentLoaded', () => { } } else if (data.type === 'progress') { updateProgress(data.epoch, data.total_epochs, data.message); - if (data.metrics) { - updateChart(data.epoch, data.metrics); - } } }; } @@ -496,7 +409,6 @@ document.addEventListener('DOMContentLoaded', () => { startBtn.disabled = true; stopBtn.disabled = false; startTimer(); - initChart(); break; case 'training': statusTitle.textContent = 'ОБУЧЕНИЕ'; @@ -1337,6 +1249,5 @@ document.addEventListener('DOMContentLoaded', () => { loadSessionsList(); connectWebSocket(); connectExportWebSocket(); - initChart(); }); }); diff --git a/src/yolo_webui/static/index.html b/src/yolo_webui/static/index.html index 6bf2507..d99981e 100755 --- a/src/yolo_webui/static/index.html +++ b/src/yolo_webui/static/index.html @@ -8,8 +8,6 @@ - - @@ -377,17 +375,6 @@ - -
-
-

График обучения (Live)

-
-
-
- -
-
-
diff --git a/src/yolo_webui/static/style.css b/src/yolo_webui/static/style.css index 5ebfc95..c12625a 100755 --- a/src/yolo_webui/static/style.css +++ b/src/yolo_webui/static/style.css @@ -140,7 +140,7 @@ body { .workspace-view { display: none; - grid-template-columns: 46% 1fr; + grid-template-columns: 380px 1fr; gap: 1.5rem; flex: 1; height: 100%; @@ -754,40 +754,6 @@ body { box-shadow: none !important; } -/* Charts Card */ -.chart-container-card { - background-color: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 1rem; - height: 320px; - min-height: 320px; - flex: none; - display: flex; - flex-direction: column; -} - -.chart-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.75rem; -} - -.chart-header h3 { - font-size: 0.9rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); -} - -.chart-canvas-wrapper { - position: relative; - width: 100%; - height: 240px; -} - /* Console Logs Box */ .log-card { background-color: #0d0d0f; @@ -795,9 +761,8 @@ body { border-radius: 8px; display: flex; flex-direction: column; - height: 280px; - min-height: 280px; - flex: none; + flex: 1; + min-height: 480px; overflow: hidden; } @@ -845,6 +810,7 @@ body { .log-body { flex: 1; + overflow-x: auto; overflow-y: auto; padding: 0.75rem 1rem; font-family: var(--font-mono); @@ -857,8 +823,8 @@ body { } .log-line { - white-space: pre-wrap; - word-break: break-all; + white-space: pre; + word-break: normal; } .log-level-info { color: var(--text-muted); } diff --git a/src/yolo_webui/trainer.py b/src/yolo_webui/trainer.py index 442a050..0dc4a44 100755 --- a/src/yolo_webui/trainer.py +++ b/src/yolo_webui/trainer.py @@ -191,6 +191,16 @@ class TrainingRunner: from ultralytics import YOLO, settings from .ultralytics_trainers import trainer_for_task + workspace_root = Path.cwd().resolve() + try: + settings.update({ + "runs_dir": str((workspace_root / "runs").resolve()), + "datasets_dir": str((workspace_root / "datasets").resolve()), + "weights_dir": str((workspace_root / "models").resolve()), + }) + except Exception: + pass + previous_mlflow_setting = settings["mlflow"] setting_changed = previous_mlflow_setting is not config.mlflow.enabled if setting_changed: diff --git a/tests/frontend_smoke.js b/tests/frontend_smoke.js index 83ef24f..0a094b7 100755 --- a/tests/frontend_smoke.js +++ b/tests/frontend_smoke.js @@ -96,19 +96,7 @@ global.clearTimeout = id => { if (timer) timer.cancelled = true; }; -class FakeChart { - static instances = []; - constructor(_context, config) { - this.data = config.data; - this.options = config.options; - FakeChart.instances.push(this); - } - - destroy() {} - update() {} -} -global.Chart = FakeChart; class FakeWebSocket { static instances = []; @@ -236,12 +224,6 @@ function runTimer(timer) { }) }); - const chart = FakeChart.instances.at(-1); - assert.deepEqual(chart.data.labels, [1, 2]); - assert.deepEqual(chart.data.datasets.map(item => item.label), ['mAP50', 'loss']); - assert.deepEqual(chart.data.datasets[0].data, [0.5, null]); - assert.deepEqual(chart.data.datasets[1].data, [null, 0.2]); - socket.onclose(); socket.onclose(); let activeReconnectTimers = scheduledTimers.filter( diff --git a/tests/test_config.py b/tests/test_config.py index d8c0faf..9d9d745 100755 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -15,6 +15,7 @@ def test_train_kwargs_omit_optional_empty_values() -> None: augmentation=AugmentationConfig(enabled=False), ) + from pathlib import Path assert config.train_kwargs() == { "data": "dataset.yaml", "epochs": 100, @@ -22,7 +23,7 @@ def test_train_kwargs_omit_optional_empty_values() -> None: "batch": 16, "workers": 8, "patience": 100, - "project": "runs/train", + "project": str(Path("runs/train").resolve()), "verbose": True, } diff --git a/tests/test_trainer.py b/tests/test_trainer.py index fc02c38..3892457 100755 --- a/tests/test_trainer.py +++ b/tests/test_trainer.py @@ -106,7 +106,7 @@ def test_runner_wires_yolo_callbacks_and_returns_output( assert output == tmp_path / "run" assert constructed == [("models/model.pt", "pose")] - assert settings_updates == [{"mlflow": False}, {"mlflow": True}] + assert [u for u in settings_updates if "mlflow" in u] == [{"mlflow": False}, {"mlflow": True}] assert train_arguments[0]["data"] == "dataset.yaml" assert train_arguments[0]["verbose"] is True assert train_arguments[0]["trainer"] is FakeTrainer