Compare commits
6 commits
fix_runs_a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c05cd91cde | |||
| 77a85bcb02 | |||
| 3640dff6d7 | |||
| ec017268bf | |||
| fed058d4c8 | |||
| d91eca366f |
7 changed files with 62 additions and 62 deletions
|
|
@ -23,9 +23,8 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
RUN uv pip install --system --no-deps --no-build-isolation .
|
RUN uv pip install --system --no-deps --no-build-isolation .
|
||||||
|
|
||||||
# Expose Web UI port and MLflow port
|
# Expose Web UI port
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
# Start Web UI using the system entry point
|
# Start Web UI using the system entry point
|
||||||
CMD ["yolo-train-webui", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["yolo-train-webui", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,19 @@ services:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
image: yolo-train-webui:latest
|
image: yolo-train-webui:latest
|
||||||
|
network_mode: bridge
|
||||||
ports:
|
ports:
|
||||||
# The training API has no built-in user accounts, so expose it locally only.
|
- "8000:8000"
|
||||||
- "127.0.0.1:8000:8000"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./datasets:/workspace/datasets
|
- ./datasets:/workspace/datasets
|
||||||
- ./runs:/workspace/runs
|
- ./runs:/workspace/runs
|
||||||
- ./models:/workspace/models
|
- ./models:/workspace/models
|
||||||
- ./models/.config:/root/.config/Ultralytics
|
- ./models/.config:/root/.config/Ultralytics
|
||||||
environment:
|
environment:
|
||||||
# Use the Compose MLflow service so metadata and artifacts survive container recreation.
|
# Tracking URI for external MLflow server. Change host/IP if running MLflow on another machine/container.
|
||||||
YOLO_WEBUI_MLFLOW_TRACKING_URI: http://mlflow:5000
|
YOLO_WEBUI_MLFLOW_TRACKING_URI: http://host.docker.internal:5000
|
||||||
depends_on:
|
extra_hosts:
|
||||||
- mlflow
|
- "host.docker.internal:host-gateway"
|
||||||
# Uncomment the block below on Linux with NVIDIA GPU to pass the graphics card into the container:
|
# Uncomment the block below on Linux with NVIDIA GPU to pass the graphics card into the container:
|
||||||
# deploy:
|
# deploy:
|
||||||
# resources:
|
# resources:
|
||||||
|
|
@ -26,28 +26,3 @@ services:
|
||||||
# capabilities: [gpu]
|
# capabilities: [gpu]
|
||||||
ipc: host
|
ipc: host
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
mlflow:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
image: yolo-train-webui:latest
|
|
||||||
command:
|
|
||||||
- mlflow
|
|
||||||
- server
|
|
||||||
- --backend-store-uri
|
|
||||||
- sqlite:////mlflow/mlflow.db
|
|
||||||
- --artifacts-destination
|
|
||||||
- /mlflow/artifacts
|
|
||||||
- --host
|
|
||||||
- 0.0.0.0
|
|
||||||
- --port
|
|
||||||
- "5000"
|
|
||||||
- --workers
|
|
||||||
- "1"
|
|
||||||
- --allowed-hosts
|
|
||||||
- localhost:5000,127.0.0.1:5000,mlflow:5000
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:5000:5000"
|
|
||||||
volumes:
|
|
||||||
- ./mlflow:/mlflow
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const mlflowInputs = document.querySelectorAll('.mlflow-fields input');
|
const mlflowInputs = document.querySelectorAll('.mlflow-fields input');
|
||||||
const trackingUriInput = document.getElementById('tracking-uri');
|
const trackingUriInput = document.getElementById('tracking-uri');
|
||||||
const mlflowHeaderLink = document.getElementById('mlflow-header-link');
|
const mlflowHeaderLink = document.getElementById('mlflow-header-link');
|
||||||
|
const taskSelect = document.getElementById('task');
|
||||||
|
|
||||||
// --- Dynamic Model Selection ---
|
// --- Dynamic Model Selection ---
|
||||||
const standardModels = {
|
const standardModels = {
|
||||||
|
|
@ -34,6 +35,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
obb: ['yolo11n-obb.pt', 'yolo11s-obb.pt', 'yolo11m-obb.pt', 'yolo11l-obb.pt', 'yolo11x-obb.pt']
|
obb: ['yolo11n-obb.pt', 'yolo11s-obb.pt', 'yolo11m-obb.pt', 'yolo11l-obb.pt', 'yolo11x-obb.pt']
|
||||||
};
|
};
|
||||||
let discoveredModels = [];
|
let discoveredModels = [];
|
||||||
|
let discoveredDatasets = [];
|
||||||
|
|
||||||
function getDiscoveredModelValue(model) {
|
function getDiscoveredModelValue(model) {
|
||||||
return model.path || model.name;
|
return model.path || model.name;
|
||||||
|
|
@ -99,7 +101,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskSelect = document.getElementById('task');
|
|
||||||
|
|
||||||
// Session Controls
|
// Session Controls
|
||||||
const sessionSelect = document.getElementById('session-select');
|
const sessionSelect = document.getElementById('session-select');
|
||||||
|
|
@ -249,6 +250,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
mlflowEnabled.addEventListener('change', updateMlflowFields);
|
mlflowEnabled.addEventListener('change', updateMlflowFields);
|
||||||
|
|
||||||
// --- Logger ---
|
// --- Logger ---
|
||||||
|
function scrollToLogBottom(container) {
|
||||||
|
if (!container) return;
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
function addLogLine(message, level = 'info') {
|
function addLogLine(message, level = 'info') {
|
||||||
const line = document.createElement('div');
|
const line = document.createElement('div');
|
||||||
line.className = `log-line log-level-${level.toLowerCase()}`;
|
line.className = `log-line log-level-${level.toLowerCase()}`;
|
||||||
|
|
@ -256,7 +262,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
logContainer.appendChild(line);
|
logContainer.appendChild(line);
|
||||||
|
|
||||||
if (autoscrollCheck.checked) {
|
if (autoscrollCheck.checked) {
|
||||||
logContainer.scrollTop = logContainer.scrollHeight;
|
scrollToLogBottom(logContainer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -380,7 +386,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
addLogLine(data.message, data.level);
|
addLogLine(data.message, data.level);
|
||||||
}
|
}
|
||||||
if (autoscrollCheck && autoscrollCheck.checked) {
|
if (autoscrollCheck && autoscrollCheck.checked) {
|
||||||
logContainer.scrollTop = logContainer.scrollHeight;
|
scrollToLogBottom(logContainer);
|
||||||
}
|
}
|
||||||
} 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);
|
||||||
|
|
@ -701,7 +707,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Datasets Auto-Discovery ---
|
// --- Datasets Auto-Discovery ---
|
||||||
let discoveredDatasets = [];
|
|
||||||
|
|
||||||
async function loadDatasetsList() {
|
async function loadDatasetsList() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -932,6 +938,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configForm) {
|
if (configForm) {
|
||||||
|
configForm.addEventListener('submit', (e) => e.preventDefault());
|
||||||
configForm.addEventListener('input', () => {
|
configForm.addEventListener('input', () => {
|
||||||
const config = getFormConfig();
|
const config = getFormConfig();
|
||||||
localStorage.setItem('draft_config', JSON.stringify(config));
|
localStorage.setItem('draft_config', JSON.stringify(config));
|
||||||
|
|
@ -1022,7 +1029,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
exportLogContainer.appendChild(line);
|
exportLogContainer.appendChild(line);
|
||||||
|
|
||||||
if (exportAutoscrollCheck.checked) {
|
if (exportAutoscrollCheck.checked) {
|
||||||
exportLogContainer.scrollTop = exportLogContainer.scrollHeight;
|
scrollToLogBottom(exportLogContainer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1249,5 +1256,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadSessionsList();
|
loadSessionsList();
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
connectExportWebSocket();
|
connectExportWebSocket();
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Initial load error:', err);
|
||||||
|
addLogLine('Ошибка загрузки конфигурации: ' + err.message, 'warning');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
5
src/yolo_webui/static/favicon.svg
Normal file
5
src/yolo_webui/static/favicon.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#f97316" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||||
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
||||||
|
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 397 B |
|
|
@ -7,8 +7,8 @@
|
||||||
<!-- Google Fonts -->
|
<!-- Google Fonts -->
|
||||||
<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 rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css?v=6">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
|
|
@ -536,6 +536,6 @@
|
||||||
</section>
|
</section>
|
||||||
</div> <!-- End export-view -->
|
</div> <!-- End export-view -->
|
||||||
</main>
|
</main>
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js?v=6"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,11 @@ body {
|
||||||
background-color: var(--bg-primary);
|
background-color: var(--bg-primary);
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
|
max-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-x: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header Styles */
|
/* Header Styles */
|
||||||
|
|
@ -136,14 +137,18 @@ body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
height: calc(100vh - 57px);
|
height: calc(100vh - 57px);
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-view {
|
.workspace-view {
|
||||||
display: none;
|
display: none;
|
||||||
grid-template-columns: 380px 1fr;
|
grid-template-columns: 420px 1fr;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-view.active {
|
.workspace-view.active {
|
||||||
|
|
@ -169,6 +174,7 @@ body {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#config-pane,
|
#config-pane,
|
||||||
|
|
@ -183,8 +189,9 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
|
height: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Scrollbars */
|
/* Custom Scrollbars */
|
||||||
|
|
@ -343,20 +350,26 @@ body {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.25rem;
|
||||||
gap: 0.25rem;
|
gap: 0.2rem;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn {
|
.tab-btn {
|
||||||
flex: 1;
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
font-size: 0.9rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0.6rem;
|
padding: 0.55rem 0.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
transition: var(--transition-fast);
|
transition: var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -762,7 +775,7 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 480px;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -809,16 +822,18 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-body {
|
.log-body {
|
||||||
flex: 1;
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.6rem 0.85rem;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 0.85rem;
|
font-size: 0.75rem;
|
||||||
line-height: 1.5;
|
line-height: 1.35;
|
||||||
|
letter-spacing: -0.015em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.15rem;
|
||||||
color: #e4e4e7;
|
color: #e4e4e7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,12 @@ def test_compose_is_local_and_does_not_require_nvidia() -> None:
|
||||||
compose = yaml.safe_load(
|
compose = yaml.safe_load(
|
||||||
(PROJECT_ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
(PROJECT_ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||||
)
|
)
|
||||||
|
assert set(compose["services"].keys()) == {"webui"}
|
||||||
webui = compose["services"]["webui"]
|
webui = compose["services"]["webui"]
|
||||||
|
|
||||||
assert webui["ports"] == ["127.0.0.1:8000:8000"]
|
assert webui["ports"] == ["8000:8000"]
|
||||||
assert "deploy" not in webui
|
assert "deploy" not in webui
|
||||||
|
assert "YOLO_WEBUI_MLFLOW_TRACKING_URI" in webui["environment"]
|
||||||
mlflow = compose["services"]["mlflow"]
|
|
||||||
assert mlflow["ports"] == ["127.0.0.1:5000:5000"]
|
|
||||||
assert mlflow["volumes"] == ["./mlflow:/mlflow"]
|
|
||||||
assert "server" in mlflow["command"]
|
|
||||||
assert webui["environment"]["YOLO_WEBUI_MLFLOW_TRACKING_URI"] == "http://mlflow:5000"
|
|
||||||
|
|
||||||
|
|
||||||
def test_docker_dependencies_are_exported_from_lock_file() -> None:
|
def test_docker_dependencies_are_exported_from_lock_file() -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue