fix terminal and runs dir
This commit is contained in:
parent
1a8d1678d2
commit
212d03da67
9 changed files with 35 additions and 163 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<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">
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -377,17 +375,6 @@
|
|||
</button>
|
||||
</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 -->
|
||||
<div class="log-card">
|
||||
<div class="log-header">
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue