fix terminal and runs dir

This commit is contained in:
malvm 2026-08-05 09:32:44 +04:00
parent 1a8d1678d2
commit 212d03da67
9 changed files with 35 additions and 163 deletions

View file

@ -28,6 +28,17 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(me
logger = logging.getLogger("yolo_webui") logger = logging.getLogger("yolo_webui")
SESSION_NAME_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,64}$") 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 @dataclass
class LiveState: class LiveState:

View file

@ -364,6 +364,10 @@ class TrainingConfig:
def train_kwargs(self) -> dict[str, str | int | float | bool]: def train_kwargs(self) -> dict[str, str | int | float | bool]:
"""Convert the form values to arguments accepted by YOLO.train().""" """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] = { values: dict[str, str | int | float | bool] = {
"data": self.dataset.strip(), "data": self.dataset.strip(),
"epochs": self.epochs, "epochs": self.epochs,
@ -371,7 +375,7 @@ class TrainingConfig:
"batch": self.batch_size, "batch": self.batch_size,
"workers": self.workers, "workers": self.workers,
"patience": self.patience, "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. # Enable verbose output so users see active progress and losses in the log console.
"verbose": True, "verbose": True,
} }

View file

@ -125,10 +125,6 @@ document.addEventListener('DOMContentLoaded', () => {
const autoscrollCheck = document.getElementById('autoscroll'); const autoscrollCheck = document.getElementById('autoscroll');
const clearLogBtn = document.getElementById('clear-log-btn'); const clearLogBtn = document.getElementById('clear-log-btn');
// Chart
const ctx = document.getElementById('metricsChart').getContext('2d');
let metricsChart = null;
// State variables // State variables
let trainingTimer = null; let trainingTimer = null;
let secondsElapsed = 0; let secondsElapsed = 0;
@ -268,78 +264,6 @@ document.addEventListener('DOMContentLoaded', () => {
logContainer.innerHTML = ''; 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 --- // --- Timer UI ---
function startTimer() { function startTimer() {
stopTimer(); stopTimer();
@ -427,14 +351,6 @@ document.addEventListener('DOMContentLoaded', () => {
addLogLine(msg, level); 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 // Sync progress
if (data.status === 'training' || data.status === 'stopping') { if (data.status === 'training' || data.status === 'stopping') {
updateProgress(data.epoch, data.total_epochs); updateProgress(data.epoch, data.total_epochs);
@ -468,9 +384,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} else if (data.type === 'progress') { } else if (data.type === 'progress') {
updateProgress(data.epoch, data.total_epochs, data.message); 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; startBtn.disabled = true;
stopBtn.disabled = false; stopBtn.disabled = false;
startTimer(); startTimer();
initChart();
break; break;
case 'training': case 'training':
statusTitle.textContent = 'ОБУЧЕНИЕ'; statusTitle.textContent = 'ОБУЧЕНИЕ';
@ -1337,6 +1249,5 @@ document.addEventListener('DOMContentLoaded', () => {
loadSessionsList(); loadSessionsList();
connectWebSocket(); connectWebSocket();
connectExportWebSocket(); connectExportWebSocket();
initChart();
}); });
}); });

View file

@ -8,8 +8,6 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
</head> </head>
<body> <body>
@ -377,17 +375,6 @@
</button> </button>
</div> </div>
<!-- Live Chart -->
<div class="chart-container-card">
<div class="chart-header">
<h3>График обучения (Live)</h3>
<div class="chart-legend" id="chart-legend"></div>
</div>
<div class="chart-canvas-wrapper">
<canvas id="metricsChart"></canvas>
</div>
</div>
<!-- Logs Box --> <!-- Logs Box -->
<div class="log-card"> <div class="log-card">
<div class="log-header"> <div class="log-header">

View file

@ -140,7 +140,7 @@ body {
.workspace-view { .workspace-view {
display: none; display: none;
grid-template-columns: 46% 1fr; grid-template-columns: 380px 1fr;
gap: 1.5rem; gap: 1.5rem;
flex: 1; flex: 1;
height: 100%; height: 100%;
@ -754,40 +754,6 @@ body {
box-shadow: none !important; 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 */ /* Console Logs Box */
.log-card { .log-card {
background-color: #0d0d0f; background-color: #0d0d0f;
@ -795,9 +761,8 @@ body {
border-radius: 8px; border-radius: 8px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 280px; flex: 1;
min-height: 280px; min-height: 480px;
flex: none;
overflow: hidden; overflow: hidden;
} }
@ -845,6 +810,7 @@ body {
.log-body { .log-body {
flex: 1; flex: 1;
overflow-x: auto;
overflow-y: auto; overflow-y: auto;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
font-family: var(--font-mono); font-family: var(--font-mono);
@ -857,8 +823,8 @@ body {
} }
.log-line { .log-line {
white-space: pre-wrap; white-space: pre;
word-break: break-all; word-break: normal;
} }
.log-level-info { color: var(--text-muted); } .log-level-info { color: var(--text-muted); }

View file

@ -191,6 +191,16 @@ class TrainingRunner:
from ultralytics import YOLO, settings from ultralytics import YOLO, settings
from .ultralytics_trainers import trainer_for_task 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"] previous_mlflow_setting = settings["mlflow"]
setting_changed = previous_mlflow_setting is not config.mlflow.enabled setting_changed = previous_mlflow_setting is not config.mlflow.enabled
if setting_changed: if setting_changed:

View file

@ -96,19 +96,7 @@ global.clearTimeout = id => {
if (timer) timer.cancelled = true; 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 { class FakeWebSocket {
static instances = []; 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();
socket.onclose(); socket.onclose();
let activeReconnectTimers = scheduledTimers.filter( let activeReconnectTimers = scheduledTimers.filter(

View file

@ -15,6 +15,7 @@ def test_train_kwargs_omit_optional_empty_values() -> None:
augmentation=AugmentationConfig(enabled=False), augmentation=AugmentationConfig(enabled=False),
) )
from pathlib import Path
assert config.train_kwargs() == { assert config.train_kwargs() == {
"data": "dataset.yaml", "data": "dataset.yaml",
"epochs": 100, "epochs": 100,
@ -22,7 +23,7 @@ def test_train_kwargs_omit_optional_empty_values() -> None:
"batch": 16, "batch": 16,
"workers": 8, "workers": 8,
"patience": 100, "patience": 100,
"project": "runs/train", "project": str(Path("runs/train").resolve()),
"verbose": True, "verbose": True,
} }

View file

@ -106,7 +106,7 @@ def test_runner_wires_yolo_callbacks_and_returns_output(
assert output == tmp_path / "run" assert output == tmp_path / "run"
assert constructed == [("models/model.pt", "pose")] 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]["data"] == "dataset.yaml"
assert train_arguments[0]["verbose"] is True assert train_arguments[0]["verbose"] is True
assert train_arguments[0]["trainer"] is FakeTrainer assert train_arguments[0]["trainer"] is FakeTrainer