ta-azis-pendeteksi-nominal-.../project/templates/training.html

658 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html>
<head>
<title>MIRA - Training</title>
<link href="https://fonts.googleapis.com/css2?family=Dangrek&family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Poppins', sans-serif;
background: #6D60B4;
min-height: 100vh;
display: flex;
align-items: center;
padding: 20px;
}
.wrapper {
display: flex;
width: 100%;
min-height: calc(100vh - 40px);
}
.sidebar {
width: 170px;
display: flex;
flex-direction: column;
align-items: center;
padding: 32px 14px 24px;
gap: 40px;
flex-shrink: 0;
}
.brand { text-align: center; color: white; }
.brand h2 {
font-family: 'Poppins', sans-serif;
font-size: 26px; font-weight: 700; line-height: 1; letter-spacing: 2px;
}
.brand p {
font-size: 10px; line-height: 1.5;
margin-top: 7px; opacity: 0.9;
}
.nav { display: flex; flex-direction: column; gap: 10px; width: 100%; }
.nav-item {
display: flex; align-items: center; gap: 10px;
padding: 10px 10px 10px 16px;
color: rgba(255,255,255,0.6);
font-size: 14px; font-weight: 600;
text-decoration: none; position: relative;
}
.nav-item.active { color: white; }
.nav-item.active::before {
content: ''; position: absolute; left: 0;
top: 50%; transform: translateY(-50%);
width: 4px; height: 28px;
background: white; border-radius: 0 3px 3px 0;
}
.icon-grid {
width: 22px; height: 22px; display: inline-block; flex-shrink: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Crect x='3' y='3' width='8' height='8' rx='1'/%3E%3Crect x='13' y='3' width='8' height='8' rx='1'/%3E%3Crect x='3' y='13' width='8' height='8' rx='1'/%3E%3Crect x='13' y='13' width='8' height='8' rx='1'/%3E%3C/svg%3E");
background-size: contain; background-repeat: no-repeat;
}
.icon-money {
width: 22px; height: 22px; display: inline-block; flex-shrink: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/%3E%3C/svg%3E");
background-size: contain; background-repeat: no-repeat;
}
.icon-train {
width: 22px; height: 22px; display: inline-block; flex-shrink: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M19 3H5c-1.1 0-2 .9-2 2v14l4-4h12c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 9l-3-2.25V12H7V6h4v2.25L14 6l5 3-5 3z'/%3E%3C/svg%3E");
background-size: contain; background-repeat: no-repeat;
}
.main-wrap { flex: 1; display: flex; }
.content-area {
flex: 1; background: #FFFFFF;
border-radius: 24px;
padding: 32px 36px 36px;
display: flex; flex-direction: column;
overflow-y: auto;
max-height: calc(100vh - 40px);
}
.page-title {
font-size: 24px; font-weight: 700;
color: #6D60B4; margin-bottom: 28px;
}
.step-card {
background: #F4F2FC;
border-radius: 18px;
border: 1.5px solid #D4CFEE;
padding: 22px 26px;
margin-bottom: 18px;
}
.step-header {
display: flex; align-items: center; gap: 12px;
margin-bottom: 14px;
}
.step-badge {
background: #6D60B4; color: white;
font-size: 12px; font-weight: 700;
width: 28px; height: 28px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.step-title { font-size: 15px; font-weight: 700; color: #4a3a9a; }
.step-desc { font-size: 12px; color: #7a7a9a; margin-top: 2px; }
.step-status {
margin-left: auto;
font-size: 11px; font-weight: 600;
padding: 4px 12px; border-radius: 20px;
}
.status-wait { background: #e8e6f6; color: #8880c4; }
.status-ok { background: #d4f4e2; color: #2e7d52; }
.status-err { background: #fde8e8; color: #b94040; }
.status-run { background: #fff3cd; color: #8a6400; }
.form-row {
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
}
.btn {
background: #6D60B4; color: white;
border: none; border-radius: 9px;
padding: 9px 22px;
font-family: 'Poppins', sans-serif;
font-size: 13px; font-weight: 600;
cursor: pointer; white-space: nowrap;
}
.btn:hover { background: #5c51a0; }
.btn:disabled { background: #b0aad4; cursor: not-allowed; }
.btn-green { background: #3a7d52; }
.btn-green:hover { background: #2e6442; }
.file-label {
background: #6D60B4; color: white;
font-size: 12px; padding: 8px 18px;
border-radius: 9px; cursor: pointer; font-weight: 600;
}
.file-label:hover { background: #5c51a0; }
input[type="file"] { display: none; }
.file-name { font-size: 12px; color: #4a3a9a; }
.input-small {
font-family: 'Poppins', sans-serif;
border: 1.5px solid #C4BFDF;
border-radius: 8px;
padding: 7px 12px;
font-size: 13px;
color: #4a3a9a;
width: 90px;
background: white;
}
.input-label { font-size: 12px; color: #6a6a9a; font-weight: 600; }
.log-box {
background: #1e1e2e;
color: #c8c0ff;
font-family: 'Courier New', monospace;
font-size: 12px;
border-radius: 12px;
padding: 16px;
height: 280px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
line-height: 1.6;
display: none;
margin-top: 14px;
}
.log-box.visible { display: block; }
.result-info {
font-size: 13px; color: #4a3a9a;
background: #eceaf6; border-radius: 10px;
padding: 10px 16px; margin-top: 10px;
display: none;
}
.result-info.visible { display: block; }
.model-note {
font-size: 11px; color: #9a9abf;
margin-top: 8px; font-style: italic;
}
/* === AUTOCOMPLETE === */
.model-input-wrap { position: relative; }
.model-input-wrap input[type="text"] {
font-family: 'Poppins', sans-serif;
border: 1.5px solid #C4BFDF;
border-radius: 8px;
padding: 7px 12px;
font-size: 13px;
color: #4a3a9a;
width: 210px;
background: white;
outline: none;
}
.model-input-wrap input[type="text"]:focus { border-color: #6D60B4; }
.suggestion-box {
display: none;
position: absolute;
top: calc(100% + 4px); left: 0;
background: white;
border: 1.5px solid #C4BFDF;
border-radius: 12px;
width: 290px;
max-height: 260px;
overflow-y: auto;
z-index: 999;
box-shadow: 0 6px 20px rgba(109,96,180,0.15);
padding: 6px 0;
}
.sg-group {
font-size: 10px; font-weight: 700;
color: #9a90d4; padding: 8px 14px 4px;
letter-spacing: 0.5px;
}
.sg-item {
font-size: 12px; font-weight: 600;
color: #4a3a9a; padding: 8px 16px;
cursor: pointer;
display: flex; justify-content: space-between;
align-items: center; gap: 8px;
}
.sg-item:hover { background: #eceaf6; }
.sg-item span { font-weight: 400; color: #9a90c0; font-size: 11px; white-space: nowrap; }
.sg-item.sg-hidden { display: none; }
</style>
</head>
<body>
<div class="wrapper">
<div class="sidebar">
<div class="brand">
<h2>MIRA</h2>
<p>Money Identification and<br>Recognition Assistant</p>
</div>
<nav class="nav">
<a href="/monitoring" class="nav-item">
<span class="icon-grid"></span> Monitoring
</a>
<a href="/testing" class="nav-item">
<span class="icon-money"></span> Testing
</a>
<a href="/training" class="nav-item active">
<span class="icon-train"></span> Training
</a>
</nav>
</div>
<div class="main-wrap">
<div class="content-area">
<div class="page-title">Training</div>
<!-- STEP 1: Install Ultralytics -->
<div class="step-card">
<div class="step-header">
<div class="step-badge">1</div>
<div>
<div class="step-title">Install Ultralytics</div>
<div class="step-desc">Install library ultralytics yang dibutuhkan untuk proses training YOLO</div>
</div>
<span class="step-status status-wait" id="status0">Menunggu</span>
</div>
<div class="form-row">
<button class="btn" id="btnInstall" onclick="installUltralytics()">Install Ultralytics</button>
</div>
<div class="log-box" id="logBox0"></div>
</div>
<!-- STEP 2: Upload Dataset -->
<div class="step-card">
<div class="step-header">
<div class="step-badge">2</div>
<div>
<div class="step-title">Upload Dataset</div>
<div class="step-desc">Upload file dataset ".zip"</div>
</div>
<span class="step-status status-wait" id="status1">Menunggu</span>
</div>
<div class="form-row">
<label class="file-label" for="datasetFile">Pilih File .zip</label>
<input type="file" id="datasetFile" accept=".zip" onchange="updateFileName(this, 'fname1')">
<span class="file-name" id="fname1">Belum ada file</span>
<button class="btn" onclick="uploadDataset()">Upload & Ekstrak</button>
</div>
<div class="result-info" id="info1"></div>
</div>
<!-- STEP 3: Split Dataset -->
<div class="step-card">
<div class="step-header">
<div class="step-badge">3</div>
<div>
<div class="step-title">Bagi Dataset (Train / Validasi)</div>
<div class="step-desc">Membagi dataset menjadi data training dan validasi</div>
</div>
<span class="step-status status-wait" id="status2">Menunggu</span>
</div>
<div class="form-row">
<span class="input-label">Persentase Training:</span>
<input type="number" class="input-small" id="trainPct" value="90" min="50" max="95"> %
<button class="btn" onclick="splitDataset()">Bagi Dataset</button>
</div>
<div class="result-info" id="info2"></div>
</div>
<!-- STEP 4: Buat data.yaml -->
<div class="step-card">
<div class="step-header">
<div class="step-badge">4</div>
<div>
<div class="step-title">Konfigurasi Training</div>
<div class="step-desc">Membaca classes.txt dan membuat file konfigurasi training</div>
</div>
<span class="step-status status-wait" id="status3">Menunggu</span>
</div>
<div class="form-row">
<button class="btn" onclick="createYaml()">Baca data</button>
</div>
<div class="result-info" id="info3"></div>
</div>
<!-- STEP 5: Training -->
<div class="step-card">
<div class="step-header">
<div class="step-badge">5</div>
<div>
<div class="step-title">Mulai Training YOLO</div>
<div class="step-desc">Proses training model — log akan muncul secara real-time di bawah</div>
</div>
<span class="step-status status-wait" id="status4">Menunggu</span>
</div>
<div class="form-row">
<span class="input-label">Model:</span>
<div class="model-input-wrap">
<input
type="text"
id="modelSize"
value="yolov8n.pt"
placeholder="cth: yolov8n.pt"
autocomplete="off"
oninput="filterSuggestions(this.value)"
onfocus="showSuggestions()"
onblur="setTimeout(hideSuggestions, 200)"
>
<div class="suggestion-box" id="suggestionBox">
<div class="sg-item" onclick="selectModel('yolov8n.pt')">yolov8n.pt <span>Nano — Tercepat, CPU ok</span></div>
</div>
</div>
<span class="input-label">Epochs:</span>
<input type="number" class="input-small" id="epochs" value="60" min="1" max="500">
<span class="input-label">Imgsz:</span>
<input type="number" class="input-small" id="imgsz" value="640" min="320" max="1280" step="32">
<button class="btn" id="btnTrain" onclick="startTraining()">▶ Mulai Training</button>
</div>
<p class="model-note">* Untuk CPU: gunakan Nano atau Small dengan epochs 3060.</p>
<p class="model-note">* Hasil training: <strong>best.pt</strong> (akurasi terbaik) dan <strong>last.pt</strong> (epoch terakhir) akan tersimpan otomatis.</p>
<div class="log-box" id="logBox"></div>
</div>
<!-- STEP 6: Kelola Model -->
<div class="step-card">
<div class="step-header">
<div class="step-badge">6</div>
<div>
<div class="step-title">Kelola Model</div>
<div class="step-desc">Lihat semua model tersimpan, terapkan, atau download model yang diinginkan</div>
</div>
<button onclick="loadModels()" style="margin-left:auto; background:white; color:#6D60B4; border:1.5px solid #6D60B4; border-radius:9px; padding:7px 16px; font-family:'Poppins',sans-serif; font-size:12px; font-weight:600; cursor:pointer;">🔄 Refresh</button>
</div>
<div id="modelLibrary">
<div style="font-size:12px; color:#9a9abf; font-style:italic;">Klik Refresh untuk memuat daftar model.</div>
</div>
<div class="result-info" id="info5"></div>
</div>
</div>
</div>
</div>
<script>
function updateFileName(input, targetId) {
document.getElementById(targetId).textContent = input.files[0] ? input.files[0].name : "Belum ada file";
}
function setStatus(stepNum, type, text) {
const el = document.getElementById('status' + stepNum);
el.className = 'step-status status-' + type;
el.textContent = text;
}
function showInfo(stepNum, text, isError) {
const el = document.getElementById('info' + stepNum);
el.textContent = text;
el.style.background = isError ? '#fde8e8' : '#eceaf6';
el.style.color = isError ? '#b94040' : '#4a3a9a';
el.classList.add('visible');
}
function appendLog(boxId, text) {
const box = document.getElementById(boxId);
box.classList.add('visible');
box.textContent += text + '\n';
box.scrollTop = box.scrollHeight;
}
// === AUTOCOMPLETE ===
function showSuggestions() {
filterSuggestions(document.getElementById('modelSize').value);
document.getElementById('suggestionBox').style.display = 'block';
}
function hideSuggestions() {
document.getElementById('suggestionBox').style.display = 'none';
}
function selectModel(val) {
document.getElementById('modelSize').value = val;
hideSuggestions();
}
function filterSuggestions(query) {
const items = document.querySelectorAll('.sg-item');
const q = query.toLowerCase().trim();
items.forEach(item => {
const text = item.textContent.toLowerCase();
item.classList.toggle('sg-hidden', q.length > 0 && !text.includes(q));
});
document.getElementById('suggestionBox').style.display = 'block';
}
// STEP 0 - Install Ultralytics
function installUltralytics() {
const logBox = document.getElementById('logBox0');
const btn = document.getElementById('btnInstall');
logBox.textContent = '';
logBox.classList.add('visible');
btn.disabled = true;
setStatus(0, 'run', 'Installing...');
fetch('/install_ultralytics', { method: 'POST' })
.then(res => res.json())
.then(() => {
const evtSource = new EventSource('/training_log');
evtSource.onmessage = function(e) {
if (e.data === '__DONE__') {
evtSource.close();
btn.disabled = false;
if (logBox.textContent.includes('❌')) {
setStatus(0, 'err', 'Gagal');
} else {
setStatus(0, 'ok', 'Selesai ✓');
}
return;
}
if (e.data.trim()) appendLog('logBox0', e.data);
};
evtSource.onerror = function() {
evtSource.close();
btn.disabled = false;
setStatus(0, 'err', 'Koneksi terputus');
};
}).catch(e => {
btn.disabled = false;
setStatus(0, 'err', 'Error');
appendLog('logBox0', '❌ ' + e);
});
}
// STEP 1 - Upload Dataset
async function uploadDataset() {
const file = document.getElementById('datasetFile').files[0];
if (!file) { alert('Pilih file .zip terlebih dahulu!'); return; }
setStatus(1, 'run', 'Mengupload...');
const formData = new FormData();
formData.append('dataset', file);
try {
const res = await fetch('/upload_dataset', { method: 'POST', body: formData });
const data = await res.json();
if (data.error) { setStatus(1, 'err', 'Gagal'); showInfo(1, '❌ ' + data.error, true); }
else { setStatus(1, 'ok', 'Selesai ✓'); showInfo(1, '✅ ' + data.message, false); }
} catch(e) {
setStatus(1, 'err', 'Error'); showInfo(1, '❌ Gagal upload: ' + e, true);
}
}
// STEP 2 - Split Dataset
async function splitDataset() {
const pct = parseFloat(document.getElementById('trainPct').value) / 100;
setStatus(2, 'run', 'Memproses...');
try {
const res = await fetch('/split_dataset', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ train_pct: pct })
});
const data = await res.json();
if (data.error) { setStatus(2, 'err', 'Gagal'); showInfo(2, '❌ ' + data.error, true); }
else { setStatus(2, 'ok', 'Selesai ✓'); showInfo(2, '✅ ' + data.message, false); }
} catch(e) {
setStatus(2, 'err', 'Error'); showInfo(2, '❌ ' + e, true);
}
}
// STEP 3 - Buat data.yaml
async function createYaml() {
setStatus(3, 'run', 'Memproses...');
try {
const res = await fetch('/create_yaml', { method: 'POST' });
const data = await res.json();
if (data.error) { setStatus(3, 'err', 'Gagal'); showInfo(3, '❌ ' + data.error, true); }
else { setStatus(3, 'ok', 'Selesai ✓'); showInfo(3, '✅ ' + data.message, false); }
} catch(e) {
setStatus(3, 'err', 'Error'); showInfo(3, '❌ ' + e, true);
}
}
// STEP 4 - Training
function startTraining() {
const epochs = document.getElementById('epochs').value;
const imgsz = document.getElementById('imgsz').value;
const modelSize = document.getElementById('modelSize').value.trim();
const logBox = document.getElementById('logBox');
const btn = document.getElementById('btnTrain');
if (!modelSize) { alert('Masukkan nama model terlebih dahulu!'); return; }
logBox.textContent = '';
logBox.classList.add('visible');
btn.disabled = true;
setStatus(4, 'run', 'Training...');
fetch('/start_training', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ epochs, imgsz, model: modelSize })
})
.then(res => res.json())
.then(data => {
if (data.error) {
setStatus(4, 'err', 'Gagal');
appendLog('logBox', '❌ ' + data.error);
btn.disabled = false;
return;
}
const evtSource = new EventSource('/training_log');
evtSource.onmessage = function(e) {
if (e.data === '__DONE__') {
evtSource.close();
btn.disabled = false;
if (logBox.textContent.includes('❌')) {
setStatus(4, 'err', 'Gagal');
} else {
setStatus(4, 'ok', 'Selesai ✓');
loadModels();
}
return;
}
if (e.data.trim()) appendLog('logBox', e.data);
};
evtSource.onerror = function() {
evtSource.close();
setStatus(4, 'err', 'Koneksi terputus');
btn.disabled = false;
};
})
.catch(e => {
setStatus(4, 'err', 'Error');
appendLog('logBox', '❌ ' + e);
btn.disabled = false;
});
}
// STEP 6 - MODEL LIBRARY
async function loadModels() {
const container = document.getElementById('modelLibrary');
container.innerHTML = '<div style="font-size:12px;color:#9a9abf;font-style:italic;">Memuat...</div>';
try {
const res = await fetch('/list_models');
const data = await res.json();
const models = data.models;
if (models.length === 0) {
container.innerHTML = '<div style="font-size:12px;color:#9a9abf;font-style:italic;">Belum ada model tersimpan. Jalankan training terlebih dahulu.</div>';
return;
}
container.innerHTML = '';
models.forEach(m => {
const card = document.createElement('div');
card.style.cssText = `
background:${m.active ? '#f0eef9' : 'white'};
border:1.5px solid ${m.active ? '#6D60B4' : '#D4CFEE'};
border-radius:12px; padding:12px 16px; margin-bottom:10px;
display:flex; align-items:center; gap:12px; flex-wrap:wrap;
`;
card.innerHTML = `
<div style="flex:1; min-width:0;">
<div style="font-size:13px; font-weight:700; color:#4a3a9a; display:flex; align-items:center; gap:8px;">
${m.name}
${m.active ? '<span style="background:#6D60B4;color:white;font-size:9px;font-weight:700;padding:2px 8px;border-radius:20px;">AKTIF</span>' : ''}
</div>
<div style="font-size:11px; color:#9a90c0; margin-top:3px;">
Base: ${m.base_model} &nbsp;|&nbsp; Epochs: ${m.epochs} &nbsp;|&nbsp; Imgsz: ${m.imgsz} &nbsp;|&nbsp; ${m.created}
</div>
</div>
<div style="display:flex; gap:8px; flex-shrink:0;">
${!m.active ? `<button class="btn" style="padding:7px 16px;font-size:12px;" onclick="applyModel('${m.name}')">✅ Terapkan</button>` : ''}
<a href="/download_model/${m.name}">
<button style="background:white;color:#6D60B4;border:1.5px solid #6D60B4;border-radius:9px;padding:7px 16px;font-family:'Poppins',sans-serif;font-size:12px;font-weight:600;cursor:pointer;">⬇ Download</button>
</a>
</div>
`;
container.appendChild(card);
});
} catch(e) {
container.innerHTML = `<div style="font-size:12px;color:#b94040;">❌ Gagal memuat: ${e}</div>`;
}
}
async function applyModel(name) {
if (!confirm(`Terapkan model "${name}"?\nModel ini akan langsung digunakan di Monitoring & Testing.`)) return;
try {
const res = await fetch('/apply_model', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({name}) });
const data = await res.json();
if (data.error) { alert('❌ ' + data.error); return; }
const el = document.getElementById('info5');
el.textContent = '✅ ' + data.message;
el.style.background = '#eceaf6'; el.style.color = '#4a3a9a';
el.classList.add('visible');
loadModels();
} catch(e) { alert('❌ Gagal: ' + e); }
}
loadModels();
</script>
</body>
</html>