Upload source code tugas akhir
|
|
@ -0,0 +1,727 @@
|
||||||
|
#include "esp_camera.h"
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WiFiClient.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
#include <SPIFFS.h>
|
||||||
|
#include <driver/i2s.h>
|
||||||
|
|
||||||
|
// LED FLASH bawaan ESP32-CAM = GPIO 4
|
||||||
|
#define LED_PIN 4
|
||||||
|
|
||||||
|
// VIBRATION MOTOR = GPIO 12
|
||||||
|
#define VIBRO_PIN 12
|
||||||
|
|
||||||
|
// I2S AUDIO (MAX98357A)
|
||||||
|
#define I2S_BCLK 14
|
||||||
|
#define I2S_LRC 15
|
||||||
|
#define I2S_DOUT 13
|
||||||
|
|
||||||
|
// PIN AI THINKER
|
||||||
|
#define PWDN_GPIO_NUM 32
|
||||||
|
#define RESET_GPIO_NUM -1
|
||||||
|
#define XCLK_GPIO_NUM 0
|
||||||
|
#define SIOD_GPIO_NUM 26
|
||||||
|
#define SIOC_GPIO_NUM 27
|
||||||
|
#define Y9_GPIO_NUM 35
|
||||||
|
#define Y8_GPIO_NUM 34
|
||||||
|
#define Y7_GPIO_NUM 39
|
||||||
|
#define Y6_GPIO_NUM 36
|
||||||
|
#define Y5_GPIO_NUM 21
|
||||||
|
#define Y4_GPIO_NUM 19
|
||||||
|
#define Y3_GPIO_NUM 18
|
||||||
|
#define Y2_GPIO_NUM 5
|
||||||
|
#define VSYNC_GPIO_NUM 25
|
||||||
|
#define HREF_GPIO_NUM 23
|
||||||
|
#define PCLK_GPIO_NUM 22
|
||||||
|
|
||||||
|
// Global config (dari form, tidak disimpan ke EEPROM)
|
||||||
|
String savedSSID = "";
|
||||||
|
String savedPassword = "";
|
||||||
|
String savedServer = ""; // format: "192.168.x.x"
|
||||||
|
int savedPort = 5000;
|
||||||
|
String savedPath = "/detect";
|
||||||
|
|
||||||
|
WebServer configServer(80);
|
||||||
|
|
||||||
|
// Forward declaration
|
||||||
|
void connectWiFi();
|
||||||
|
void startCamera();
|
||||||
|
void startConfigPortal();
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// INIT I2S (on-demand)
|
||||||
|
// ======================
|
||||||
|
void startI2S() {
|
||||||
|
i2s_config_t i2s_config = {
|
||||||
|
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
|
||||||
|
.sample_rate = 8000,
|
||||||
|
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
||||||
|
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
|
||||||
|
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
||||||
|
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL3,
|
||||||
|
.dma_buf_count = 4,
|
||||||
|
.dma_buf_len = 32,
|
||||||
|
.use_apll = false,
|
||||||
|
.tx_desc_auto_clear = true
|
||||||
|
};
|
||||||
|
|
||||||
|
i2s_pin_config_t pin_config = {
|
||||||
|
.bck_io_num = I2S_BCLK,
|
||||||
|
.ws_io_num = I2S_LRC,
|
||||||
|
.data_out_num = I2S_DOUT,
|
||||||
|
.data_in_num = I2S_PIN_NO_CHANGE
|
||||||
|
};
|
||||||
|
|
||||||
|
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
|
||||||
|
i2s_set_pin(I2S_NUM_0, &pin_config);
|
||||||
|
i2s_zero_dma_buffer(I2S_NUM_0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopI2S() {
|
||||||
|
i2s_zero_dma_buffer(I2S_NUM_0);
|
||||||
|
i2s_driver_uninstall(I2S_NUM_0);
|
||||||
|
}
|
||||||
|
// ======================
|
||||||
|
// DOWNLOAD WAV DARI SERVER & PLAY
|
||||||
|
// Alur: HTTP GET /audio/<filename> → stream ke I2S
|
||||||
|
// ======================
|
||||||
|
void playWAVFromServer(const char* filename) {
|
||||||
|
if (savedServer == "" || WiFi.status() != WL_CONNECTED) {
|
||||||
|
Serial.println("⚠️ Skip audio: WiFi tidak konek");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFiClient client;
|
||||||
|
client.setTimeout(10000);
|
||||||
|
|
||||||
|
Serial.printf("🔊 Download audio: /audio/%s\n", filename);
|
||||||
|
|
||||||
|
if (!client.connect(savedServer.c_str(), savedPort)) {
|
||||||
|
Serial.println("❌ Gagal konek ke server untuk audio");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP GET request
|
||||||
|
client.printf("GET /audio/%s HTTP/1.1\r\n", filename);
|
||||||
|
client.printf("Host: %s:%d\r\n", savedServer.c_str(), savedPort);
|
||||||
|
client.print("Connection: close\r\n\r\n");
|
||||||
|
|
||||||
|
// Tunggu response header
|
||||||
|
unsigned long t = millis();
|
||||||
|
while (client.available() == 0) {
|
||||||
|
if (millis() - t > 10000) {
|
||||||
|
Serial.println("❌ Timeout tunggu audio response");
|
||||||
|
client.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delay(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baca & buang HTTP header (sampai \r\n\r\n)
|
||||||
|
bool headerDone = false;
|
||||||
|
String headerBuf = "";
|
||||||
|
while (client.connected() || client.available()) {
|
||||||
|
if (client.available()) {
|
||||||
|
char c = client.read();
|
||||||
|
headerBuf += c;
|
||||||
|
if (headerBuf.endsWith("\r\n\r\n")) {
|
||||||
|
headerDone = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!headerDone) {
|
||||||
|
Serial.println("❌ Header audio tidak lengkap");
|
||||||
|
client.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cek status 200
|
||||||
|
if (headerBuf.indexOf("200 OK") == -1) {
|
||||||
|
Serial.println("❌ Server tidak kirim 200 OK untuk audio");
|
||||||
|
Serial.println(" Header: " + headerBuf.substring(0, 100));
|
||||||
|
client.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip 44 byte WAV header dari body
|
||||||
|
// Baca dulu 44 byte pertama
|
||||||
|
uint8_t wavHeader[44];
|
||||||
|
int hRead = 0;
|
||||||
|
unsigned long ht = millis();
|
||||||
|
while (hRead < 44 && (millis() - ht < 3000)) {
|
||||||
|
if (client.available()) {
|
||||||
|
wavHeader[hRead++] = client.read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suspend kamera & init I2S
|
||||||
|
esp_camera_deinit();
|
||||||
|
delay(50);
|
||||||
|
startI2S();
|
||||||
|
|
||||||
|
Serial.println("▶ Streaming audio ke I2S...");
|
||||||
|
|
||||||
|
// Stream body langsung ke I2S dalam chunk
|
||||||
|
const int bufSize = 512;
|
||||||
|
uint8_t buf[bufSize];
|
||||||
|
size_t bytesWritten = 0;
|
||||||
|
unsigned long lastData = millis();
|
||||||
|
|
||||||
|
while (client.connected() || client.available()) {
|
||||||
|
int available = client.available();
|
||||||
|
if (available > 0) {
|
||||||
|
int toRead = min(available, bufSize);
|
||||||
|
int bytesRead = client.read(buf, toRead);
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
i2s_write(I2S_NUM_0, buf, bytesRead, &bytesWritten, portMAX_DELAY);
|
||||||
|
lastData = millis();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (millis() - lastData > 2000) break; // tidak ada data 2 detik → selesai
|
||||||
|
delay(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.stop();
|
||||||
|
|
||||||
|
// Stop I2S & restart kamera
|
||||||
|
stopI2S();
|
||||||
|
delay(50);
|
||||||
|
startCamera();
|
||||||
|
|
||||||
|
Serial.println("🔊 Audio selesai");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// MAPPING NOMINAL → PLAY AUDIO DARI SERVER
|
||||||
|
// ======================
|
||||||
|
void playNominal(String response) {
|
||||||
|
int idx = response.indexOf("\"nominal\"");
|
||||||
|
if (idx == -1) {
|
||||||
|
Serial.println("⚠️ Key nominal tidak ditemukan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int start = response.indexOf(":", idx) + 1;
|
||||||
|
int q1 = response.indexOf("\"", start) + 1;
|
||||||
|
int q2 = response.indexOf("\"", q1);
|
||||||
|
String nominal = response.substring(q1, q2);
|
||||||
|
nominal.trim();
|
||||||
|
nominal.toLowerCase();
|
||||||
|
|
||||||
|
Serial.println("🎵 Nominal: " + nominal);
|
||||||
|
|
||||||
|
if (nominal == "seribu" || nominal == "1000") playWAVFromServer("seribu.wav");
|
||||||
|
else if (nominal == "dua ribu" || nominal == "2000") playWAVFromServer("duaribu.wav");
|
||||||
|
else if (nominal == "lima ribu" || nominal == "5000") playWAVFromServer("limaribu.wav");
|
||||||
|
else if (nominal == "sepuluh ribu" || nominal == "10000") playWAVFromServer("sepuluhribu.wav");
|
||||||
|
else if (nominal == "dua puluh ribu" || nominal == "20000") playWAVFromServer("duapuluhribu.wav");
|
||||||
|
else if (nominal == "lima puluh ribu" || nominal == "50000") playWAVFromServer("limapuluhribu.wav");
|
||||||
|
else if (nominal == "seratus ribu" || nominal == "100000") playWAVFromServer("seratusribu.wav");
|
||||||
|
else if (nominal == "tidak terdeteksi") Serial.println("⚠️ Uang tidak terdeteksi");
|
||||||
|
else Serial.println("⚠️ Nominal tidak dikenali: " + nominal);
|
||||||
|
}
|
||||||
|
// ======================
|
||||||
|
// PARSE SERVER STRING
|
||||||
|
// Input: "192.168.x.x" atau "http://192.168.x.x:5000/detect"
|
||||||
|
// Hasil disimpan ke savedServer (host), savedPort, savedPath
|
||||||
|
// ======================
|
||||||
|
void parseServerString(String raw) {
|
||||||
|
raw.trim();
|
||||||
|
|
||||||
|
// Hapus prefix http://
|
||||||
|
if (raw.startsWith("http://")) {
|
||||||
|
raw = raw.substring(7);
|
||||||
|
} else if (raw.startsWith("https://")) {
|
||||||
|
raw = raw.substring(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pisah host:port/path
|
||||||
|
int slashIdx = raw.indexOf('/');
|
||||||
|
String hostPort;
|
||||||
|
if (slashIdx != -1) {
|
||||||
|
savedPath = raw.substring(slashIdx); // "/detect"
|
||||||
|
hostPort = raw.substring(0, slashIdx);
|
||||||
|
} else {
|
||||||
|
savedPath = "/detect";
|
||||||
|
hostPort = raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pisah host:port
|
||||||
|
int colonIdx = hostPort.indexOf(':');
|
||||||
|
if (colonIdx != -1) {
|
||||||
|
savedServer = hostPort.substring(0, colonIdx);
|
||||||
|
savedPort = hostPort.substring(colonIdx + 1).toInt();
|
||||||
|
} else {
|
||||||
|
savedServer = hostPort;
|
||||||
|
savedPort = 5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println(" Host : " + savedServer);
|
||||||
|
Serial.println(" Port : " + String(savedPort));
|
||||||
|
Serial.println(" Path : " + savedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// KIRIM JPEG VIA WiFiClient (raw HTTP/1.1)
|
||||||
|
// Return: body response atau "" jika gagal
|
||||||
|
// - Retry koneksi hingga 5x jika gagal
|
||||||
|
// - Timeout tunggu response: 30 detik (untuk inferensi AI yang lambat)
|
||||||
|
// - Baca response hingga koneksi benar-benar tutup
|
||||||
|
// ======================
|
||||||
|
String postImageWiFiClient(uint8_t* buf, size_t len) {
|
||||||
|
WiFiClient client;
|
||||||
|
client.setTimeout(30000); // TCP read timeout 30 detik
|
||||||
|
|
||||||
|
// --- Retry koneksi hingga 5x ---
|
||||||
|
const int MAX_RETRY = 5;
|
||||||
|
bool connected = false;
|
||||||
|
for (int attempt = 1; attempt <= MAX_RETRY; attempt++) {
|
||||||
|
Serial.printf("🔌 Konek ke %s:%d (percobaan %d/%d)\n",
|
||||||
|
savedServer.c_str(), savedPort, attempt, MAX_RETRY);
|
||||||
|
if (client.connect(savedServer.c_str(), savedPort)) {
|
||||||
|
connected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Serial.println(" ⚠️ Gagal, coba lagi 1 detik...");
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
Serial.println("❌ Gagal koneksi setelah " + String(MAX_RETRY) + "x percobaan!");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("✅ Terhubung ke server");
|
||||||
|
|
||||||
|
// --- Kirim HTTP POST header ---
|
||||||
|
client.print("POST " + savedPath + " HTTP/1.1\r\n");
|
||||||
|
client.print("Host: " + savedServer + ":" + String(savedPort) + "\r\n");
|
||||||
|
client.print("Content-Type: image/jpeg\r\n");
|
||||||
|
client.print("Content-Length: " + String(len) + "\r\n");
|
||||||
|
client.print("Connection: close\r\n");
|
||||||
|
client.print("\r\n");
|
||||||
|
|
||||||
|
// --- Kirim body JPEG dalam chunk 1024 byte ---
|
||||||
|
const size_t chunkSize = 1024;
|
||||||
|
size_t sent = 0;
|
||||||
|
while (sent < len) {
|
||||||
|
size_t toSend = min(chunkSize, len - sent);
|
||||||
|
size_t written = client.write(buf + sent, toSend);
|
||||||
|
if (written == 0) {
|
||||||
|
Serial.println("❌ Koneksi putus saat kirim data!");
|
||||||
|
client.stop();
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
sent += written;
|
||||||
|
}
|
||||||
|
Serial.println("📤 Data terkirim (" + String(len) + " bytes), menunggu response...");
|
||||||
|
|
||||||
|
// --- Tunggu response dengan timeout 30 detik ---
|
||||||
|
unsigned long waitStart = millis();
|
||||||
|
while (client.available() == 0) {
|
||||||
|
if (!client.connected()) {
|
||||||
|
Serial.println("❌ Server menutup koneksi sebelum kirim response!");
|
||||||
|
client.stop();
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (millis() - waitStart > 30000) {
|
||||||
|
Serial.println("❌ Timeout 30 detik menunggu response!");
|
||||||
|
client.stop();
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Baca response sampai koneksi tutup ---
|
||||||
|
// Strategi: baca semua dulu, lalu cari body setelah "\r\n\r\n"
|
||||||
|
String fullResponse = "";
|
||||||
|
unsigned long readStart = millis();
|
||||||
|
|
||||||
|
while (client.connected() || client.available()) {
|
||||||
|
if (client.available()) {
|
||||||
|
char c = client.read();
|
||||||
|
fullResponse += c;
|
||||||
|
readStart = millis(); // reset timeout jika masih ada data
|
||||||
|
} else {
|
||||||
|
if (millis() - readStart > 3000) break; // tidak ada data 3 detik → selesai
|
||||||
|
delay(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.stop();
|
||||||
|
|
||||||
|
// --- Pisahkan header dan body ---
|
||||||
|
int bodyIdx = fullResponse.indexOf("\r\n\r\n");
|
||||||
|
String body = "";
|
||||||
|
if (bodyIdx != -1) {
|
||||||
|
body = fullResponse.substring(bodyIdx + 4);
|
||||||
|
} else {
|
||||||
|
// Fallback: cari \n\n
|
||||||
|
bodyIdx = fullResponse.indexOf("\n\n");
|
||||||
|
if (bodyIdx != -1) {
|
||||||
|
body = fullResponse.substring(bodyIdx + 2);
|
||||||
|
} else {
|
||||||
|
body = fullResponse; // tidak ada header sama sekali
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.trim();
|
||||||
|
|
||||||
|
if (body.length() == 0) {
|
||||||
|
Serial.println("⚠️ Response kosong");
|
||||||
|
Serial.println(" Raw: " + fullResponse.substring(0, 200)); // debug 200 char pertama
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("📩 Response: " + body);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// HALAMAN CONFIG (HTML)
|
||||||
|
// ======================
|
||||||
|
const char CONFIG_PAGE[] PROGMEM = R"rawliteral(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>MIRA - Wi-Fi Setting</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?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;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.brand { text-align: center; color: white; }
|
||||||
|
.brand h2 { font-size: 28px; font-weight: 700; letter-spacing: 2px; line-height: 1; }
|
||||||
|
.brand p { font-size: 11px; opacity: 0.9; margin-top: 6px; line-height: 1.5; font-weight: 400; }
|
||||||
|
.card { background: #FFFFFF; border-radius: 24px; padding: 32px 28px 28px; width: 100%; }
|
||||||
|
.card h2 { font-size: 20px; font-weight: 700; color: #6D60B4; text-align: center; margin-bottom: 28px; }
|
||||||
|
.field { margin-bottom: 18px; }
|
||||||
|
.field label { display: block; font-size: 12px; color: #555; margin-bottom: 6px; font-weight: 400; }
|
||||||
|
.field input {
|
||||||
|
width: 100%; padding: 12px 16px;
|
||||||
|
border: 2px solid #C4BFDF; border-radius: 40px;
|
||||||
|
background: #ECEAF6; font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 14px; color: #6D60B4; outline: none; transition: border 0.2s;
|
||||||
|
}
|
||||||
|
.field input:focus { border-color: #6D60B4; }
|
||||||
|
.field input::placeholder { color: #B0AAD4; }
|
||||||
|
.btn {
|
||||||
|
width: 100%; padding: 13px; background: #6D60B4; color: white;
|
||||||
|
border: none; border-radius: 40px; font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 8px; transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.btn:hover { background: #5c50a0; }
|
||||||
|
.btn:active { background: #4e4490; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="brand">
|
||||||
|
<h2>MIRA</h2>
|
||||||
|
<p>Money Identification and<br>Recognition Assistant</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Wi-Fi Setting</h2>
|
||||||
|
<form method="POST" action="/save">
|
||||||
|
<div class="field">
|
||||||
|
<label>SSID WiFi</label>
|
||||||
|
<input type="text" name="ssid" placeholder="Nama WiFi" required />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Password WiFi</label>
|
||||||
|
<div style="position:relative;">
|
||||||
|
<input type="password" name="pass" id="passInput" placeholder="Password WiFi"
|
||||||
|
style="width:100%;padding:12px 44px 12px 16px;border:2px solid #C4BFDF;
|
||||||
|
border-radius:40px;background:#ECEAF6;font-family:'Poppins',sans-serif;
|
||||||
|
font-size:14px;color:#6D60B4;outline:none;" />
|
||||||
|
<span onclick="togglePass()" id="eyeBtn"
|
||||||
|
style="position:absolute;right:16px;top:50%;transform:translateY(-50%);
|
||||||
|
cursor:pointer;font-size:18px;user-select:none;">👁️</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>IP Server</label>
|
||||||
|
<input type="text" name="server" placeholder="192.168.x.x" required />
|
||||||
|
</div>
|
||||||
|
<button class="btn" type="submit">Connect</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function togglePass() {
|
||||||
|
var input = document.getElementById("passInput");
|
||||||
|
var btn = document.getElementById("eyeBtn");
|
||||||
|
if (input.type === "password") {
|
||||||
|
input.type = "text";
|
||||||
|
btn.textContent = "🙈";
|
||||||
|
} else {
|
||||||
|
input.type = "password";
|
||||||
|
btn.textContent = "👁️";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)rawliteral";
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// CONFIG PORTAL (SOFTAP)
|
||||||
|
// ======================
|
||||||
|
void startConfigPortal() {
|
||||||
|
Serial.println("📡 Hotspot config aktif: MIRA WIFI CONFIG");
|
||||||
|
|
||||||
|
WiFi.softAP("MIRA WIFI CONFIG", "");
|
||||||
|
Serial.println("🌐 Buka 192.168.4.1 di browser");
|
||||||
|
|
||||||
|
configServer.on("/", HTTP_GET, []() {
|
||||||
|
configServer.send(200, "text/html", CONFIG_PAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
configServer.on("/save", HTTP_POST, []() {
|
||||||
|
savedSSID = configServer.arg("ssid");
|
||||||
|
savedPassword = configServer.arg("pass");
|
||||||
|
String rawServer = configServer.arg("server");
|
||||||
|
|
||||||
|
// Parse host, port, path dari input user
|
||||||
|
parseServerString(rawServer);
|
||||||
|
|
||||||
|
Serial.println("💾 Config diterima:");
|
||||||
|
Serial.println(" SSID : " + savedSSID);
|
||||||
|
|
||||||
|
configServer.send(200, "text/html", R"(
|
||||||
|
<!DOCTYPE html><html><head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;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;justify-content:center;}
|
||||||
|
.card{background:white;border-radius:24px;padding:40px 32px;text-align:center;max-width:320px;width:100%;}
|
||||||
|
.icon{font-size:48px;margin-bottom:16px;}
|
||||||
|
h2{color:#6D60B4;font-size:20px;font-weight:700;margin-bottom:10px;}
|
||||||
|
p{color:#888;font-size:13px;line-height:1.7;}
|
||||||
|
</style></head>
|
||||||
|
<body><div class="card">
|
||||||
|
<div class="icon">🔗</div>
|
||||||
|
<h2>Menghubungkan...</h2>
|
||||||
|
<p>ESP32-CAM sedang konek ke WiFi.<br>Tunggu sebentar.</p>
|
||||||
|
</div></body></html>
|
||||||
|
)");
|
||||||
|
|
||||||
|
delay(500);
|
||||||
|
configServer.stop();
|
||||||
|
WiFi.softAPdisconnect(true);
|
||||||
|
connectWiFi();
|
||||||
|
});
|
||||||
|
|
||||||
|
configServer.begin();
|
||||||
|
|
||||||
|
while (savedSSID == "") {
|
||||||
|
configServer.handleClient();
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// KONEK WIFI
|
||||||
|
// ======================
|
||||||
|
void connectWiFi() {
|
||||||
|
Serial.print("📶 Konek ke: " + savedSSID);
|
||||||
|
WiFi.begin(savedSSID.c_str(), savedPassword.c_str());
|
||||||
|
|
||||||
|
int timeout = 0;
|
||||||
|
while (WiFi.status() != WL_CONNECTED && timeout < 20) {
|
||||||
|
delay(500);
|
||||||
|
Serial.print(".");
|
||||||
|
timeout++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
Serial.println("\n✅ WiFi Connected!");
|
||||||
|
Serial.println(" IP ESP32 : " + WiFi.localIP().toString());
|
||||||
|
Serial.println(" Server : " + savedServer + ":" + String(savedPort) + savedPath);
|
||||||
|
playWAVFromServer("wificonnected.wav");
|
||||||
|
} else {
|
||||||
|
Serial.println("\n❌ Gagal konek WiFi!");
|
||||||
|
playWAVFromServer("wifidisconnect.wav");
|
||||||
|
savedSSID = "";
|
||||||
|
startConfigPortal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// INIT CAMERA
|
||||||
|
// ======================
|
||||||
|
void startCamera() {
|
||||||
|
camera_config_t config;
|
||||||
|
|
||||||
|
config.ledc_channel = LEDC_CHANNEL_0;
|
||||||
|
config.ledc_timer = LEDC_TIMER_0;
|
||||||
|
|
||||||
|
config.pin_d0 = Y2_GPIO_NUM;
|
||||||
|
config.pin_d1 = Y3_GPIO_NUM;
|
||||||
|
config.pin_d2 = Y4_GPIO_NUM;
|
||||||
|
config.pin_d3 = Y5_GPIO_NUM;
|
||||||
|
config.pin_d4 = Y6_GPIO_NUM;
|
||||||
|
config.pin_d5 = Y7_GPIO_NUM;
|
||||||
|
config.pin_d6 = Y8_GPIO_NUM;
|
||||||
|
config.pin_d7 = Y9_GPIO_NUM;
|
||||||
|
|
||||||
|
config.pin_xclk = XCLK_GPIO_NUM;
|
||||||
|
config.pin_pclk = PCLK_GPIO_NUM;
|
||||||
|
config.pin_vsync = VSYNC_GPIO_NUM;
|
||||||
|
config.pin_href = HREF_GPIO_NUM;
|
||||||
|
|
||||||
|
config.pin_sscb_sda = SIOD_GPIO_NUM;
|
||||||
|
config.pin_sscb_scl = SIOC_GPIO_NUM;
|
||||||
|
|
||||||
|
config.pin_pwdn = PWDN_GPIO_NUM;
|
||||||
|
config.pin_reset = RESET_GPIO_NUM;
|
||||||
|
|
||||||
|
config.xclk_freq_hz = 20000000;
|
||||||
|
config.pixel_format = PIXFORMAT_JPEG;
|
||||||
|
config.frame_size = FRAMESIZE_VGA;
|
||||||
|
config.jpeg_quality = 12;
|
||||||
|
config.fb_count = 2;
|
||||||
|
|
||||||
|
esp_err_t err = esp_camera_init(&config);
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
Serial.println("❌ Camera init FAILED");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("✅ Camera OK");
|
||||||
|
|
||||||
|
sensor_t *s = esp_camera_sensor_get();
|
||||||
|
s->set_hmirror(s, 0);
|
||||||
|
s->set_vflip(s, 1);
|
||||||
|
|
||||||
|
Serial.println("✅ Mirror & Flip OK");
|
||||||
|
|
||||||
|
// Matikan SD card controller agar pin I2S bebas
|
||||||
|
periph_module_disable(PERIPH_SDMMC_MODULE);
|
||||||
|
delay(50);
|
||||||
|
pinMode(2, INPUT_PULLUP);
|
||||||
|
pinMode(13, INPUT_PULLUP);
|
||||||
|
pinMode(14, INPUT_PULLUP);
|
||||||
|
pinMode(15, INPUT_PULLUP);
|
||||||
|
Serial.println("✅ SD card controller dimatikan");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// AMBIL FRAME SEGAR
|
||||||
|
// ======================
|
||||||
|
camera_fb_t* getFreshFrame() {
|
||||||
|
camera_fb_t* fb = esp_camera_fb_get();
|
||||||
|
if (fb) esp_camera_fb_return(fb);
|
||||||
|
fb = esp_camera_fb_get();
|
||||||
|
if (fb) esp_camera_fb_return(fb);
|
||||||
|
return esp_camera_fb_get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// SETUP
|
||||||
|
// ======================
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
// LED nyala 60% kecerahan
|
||||||
|
ledcSetup(2, 5000, 8);
|
||||||
|
ledcAttachPin(LED_PIN, 2);
|
||||||
|
ledcWrite(2, 153);
|
||||||
|
Serial.println("💡 LED ON 60%");
|
||||||
|
|
||||||
|
// Vibration motor: getar sesaat saat boot
|
||||||
|
pinMode(VIBRO_PIN, OUTPUT);
|
||||||
|
digitalWrite(VIBRO_PIN, HIGH);
|
||||||
|
delay(400);
|
||||||
|
digitalWrite(VIBRO_PIN, LOW);
|
||||||
|
Serial.println("📳 Vibrate OK");
|
||||||
|
|
||||||
|
Serial.println("🚀 START PROGRAM - MIRA ESP32-CAM");
|
||||||
|
|
||||||
|
// Init SPIFFS
|
||||||
|
if (!SPIFFS.begin(true)) {
|
||||||
|
Serial.println("❌ SPIFFS gagal mount!");
|
||||||
|
} else {
|
||||||
|
Serial.println("✅ OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init kamera dulu sebelum I2S
|
||||||
|
startCamera();
|
||||||
|
|
||||||
|
// Selalu buka config portal setiap nyala
|
||||||
|
startConfigPortal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// LOOP
|
||||||
|
// ======================
|
||||||
|
void loop() {
|
||||||
|
Serial.println("📸 Ambil gambar...");
|
||||||
|
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
|
||||||
|
camera_fb_t* fb = getFreshFrame();
|
||||||
|
|
||||||
|
if (!fb) {
|
||||||
|
Serial.println("❌ Camera capture failed");
|
||||||
|
delay(1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kirim gambar ke server via WiFiClient
|
||||||
|
String response = postImageWiFiClient(fb->buf, fb->len);
|
||||||
|
|
||||||
|
esp_camera_fb_return(fb);
|
||||||
|
|
||||||
|
if (response.length() > 0) {
|
||||||
|
playNominal(response);
|
||||||
|
} else {
|
||||||
|
Serial.println("❌ Tidak ada response dari server");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// WiFi putus → coba reconnect
|
||||||
|
Serial.println("⚠️ WiFi putus, reconnecting...");
|
||||||
|
WiFi.begin(savedSSID.c_str(), savedPassword.c_str());
|
||||||
|
|
||||||
|
int t = 0;
|
||||||
|
while (WiFi.status() != WL_CONNECTED && t < 10) {
|
||||||
|
delay(500);
|
||||||
|
t++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
Serial.println("❌ Reconnect gagal. Kembali ke config...");
|
||||||
|
playWAVFromServer("wifidisconnect.wav");
|
||||||
|
savedSSID = "";
|
||||||
|
startConfigPortal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(5000);
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
|
@ -0,0 +1,110 @@
|
||||||
|
task: detect
|
||||||
|
mode: train
|
||||||
|
model: yolov8n.pt
|
||||||
|
data: /content/data.yaml
|
||||||
|
epochs: 60
|
||||||
|
time: null
|
||||||
|
patience: 100
|
||||||
|
batch: 16
|
||||||
|
imgsz: 640
|
||||||
|
save: true
|
||||||
|
save_period: -1
|
||||||
|
cache: false
|
||||||
|
device: null
|
||||||
|
workers: 8
|
||||||
|
project: null
|
||||||
|
name: train
|
||||||
|
exist_ok: false
|
||||||
|
pretrained: true
|
||||||
|
optimizer: auto
|
||||||
|
verbose: true
|
||||||
|
seed: 0
|
||||||
|
deterministic: true
|
||||||
|
single_cls: false
|
||||||
|
rect: false
|
||||||
|
cos_lr: false
|
||||||
|
close_mosaic: 10
|
||||||
|
resume: false
|
||||||
|
amp: true
|
||||||
|
fraction: 1.0
|
||||||
|
profile: false
|
||||||
|
freeze: null
|
||||||
|
multi_scale: 0.0
|
||||||
|
compile: false
|
||||||
|
overlap_mask: true
|
||||||
|
mask_ratio: 4
|
||||||
|
dropout: 0.0
|
||||||
|
val: true
|
||||||
|
split: val
|
||||||
|
save_json: false
|
||||||
|
conf: null
|
||||||
|
iou: 0.7
|
||||||
|
max_det: 300
|
||||||
|
half: false
|
||||||
|
dnn: false
|
||||||
|
plots: true
|
||||||
|
end2end: null
|
||||||
|
source: null
|
||||||
|
vid_stride: 1
|
||||||
|
stream_buffer: false
|
||||||
|
visualize: false
|
||||||
|
augment: false
|
||||||
|
agnostic_nms: false
|
||||||
|
classes: null
|
||||||
|
retina_masks: false
|
||||||
|
embed: null
|
||||||
|
show: false
|
||||||
|
save_frames: false
|
||||||
|
save_txt: false
|
||||||
|
save_conf: false
|
||||||
|
save_crop: false
|
||||||
|
show_labels: true
|
||||||
|
show_conf: true
|
||||||
|
show_boxes: true
|
||||||
|
line_width: null
|
||||||
|
format: torchscript
|
||||||
|
keras: false
|
||||||
|
optimize: false
|
||||||
|
int8: false
|
||||||
|
dynamic: false
|
||||||
|
simplify: true
|
||||||
|
opset: null
|
||||||
|
workspace: null
|
||||||
|
nms: false
|
||||||
|
lr0: 0.01
|
||||||
|
lrf: 0.01
|
||||||
|
momentum: 0.937
|
||||||
|
weight_decay: 0.0005
|
||||||
|
warmup_epochs: 3.0
|
||||||
|
warmup_momentum: 0.8
|
||||||
|
warmup_bias_lr: 0.1
|
||||||
|
box: 7.5
|
||||||
|
cls: 0.5
|
||||||
|
cls_pw: 0.0
|
||||||
|
dfl: 1.5
|
||||||
|
pose: 12.0
|
||||||
|
kobj: 1.0
|
||||||
|
rle: 1.0
|
||||||
|
angle: 1.0
|
||||||
|
nbs: 64
|
||||||
|
hsv_h: 0.015
|
||||||
|
hsv_s: 0.7
|
||||||
|
hsv_v: 0.4
|
||||||
|
degrees: 0.0
|
||||||
|
translate: 0.1
|
||||||
|
scale: 0.5
|
||||||
|
shear: 0.0
|
||||||
|
perspective: 0.0
|
||||||
|
flipud: 0.0
|
||||||
|
fliplr: 0.5
|
||||||
|
bgr: 0.0
|
||||||
|
mosaic: 1.0
|
||||||
|
mixup: 0.0
|
||||||
|
cutmix: 0.0
|
||||||
|
copy_paste: 0.0
|
||||||
|
copy_paste_mode: flip
|
||||||
|
auto_augment: randaugment
|
||||||
|
erasing: 0.4
|
||||||
|
cfg: null
|
||||||
|
tracker: botsort.yaml
|
||||||
|
save_dir: /content/runs/detect/train
|
||||||
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
|
@ -0,0 +1,61 @@
|
||||||
|
epoch,time,train/box_loss,train/cls_loss,train/dfl_loss,metrics/precision(B),metrics/recall(B),metrics/mAP50(B),metrics/mAP50-95(B),val/box_loss,val/cls_loss,val/dfl_loss,lr/pg0,lr/pg1,lr/pg2
|
||||||
|
1,23.5987,0.56775,3.07803,1.13398,0.00407,1,0.26955,0.23338,0.49248,2.71731,1.10912,0.000296413,0.000296413,0.000296413
|
||||||
|
2,43.4543,0.46226,2.00984,1.02307,0.61801,0.85806,0.81228,0.687,0.53877,1.43912,1.15181,0.000589523,0.000589523,0.000589523
|
||||||
|
3,64.2127,0.44702,1.32038,1.003,0.63638,0.6476,0.72283,0.59244,0.64426,2.7419,1.22905,0.000872633,0.000872633,0.000872633
|
||||||
|
4,85.5646,0.44927,1.08363,1.00397,0.78223,0.73283,0.77022,0.63425,0.57312,1.37018,1.13899,0.000864004,0.000864004,0.000864004
|
||||||
|
5,107.483,0.42333,1.00282,0.98683,0.87708,0.91268,0.9638,0.81199,0.5749,0.76178,1.16741,0.000849006,0.000849006,0.000849006
|
||||||
|
6,128.04,0.43471,0.92302,1.00023,0.70543,0.81579,0.88937,0.78734,0.48334,1.19508,1.05447,0.000834007,0.000834007,0.000834007
|
||||||
|
7,150.164,0.41756,0.8213,0.98819,0.67741,0.80716,0.91849,0.79924,0.51072,1.24918,1.07745,0.000819009,0.000819009,0.000819009
|
||||||
|
8,170.356,0.42253,0.75249,0.98362,0.84482,0.9516,0.96688,0.87948,0.44547,0.70966,0.99428,0.00080401,0.00080401,0.00080401
|
||||||
|
9,192.19,0.39343,0.70184,0.96906,0.9437,0.96403,0.99413,0.90372,0.4561,0.56456,1.01693,0.000789012,0.000789012,0.000789012
|
||||||
|
10,213.655,0.39368,0.66582,0.97089,0.91835,0.9815,0.99343,0.91534,0.41141,0.4505,0.99596,0.000774013,0.000774013,0.000774013
|
||||||
|
11,234.954,0.39306,0.65676,0.97705,0.96494,0.98492,0.995,0.91062,0.44327,0.46675,0.99698,0.000759015,0.000759015,0.000759015
|
||||||
|
12,256.292,0.38015,0.58921,0.96798,0.96552,0.98766,0.98897,0.92254,0.36902,0.42228,0.93015,0.000744016,0.000744016,0.000744016
|
||||||
|
13,276.365,0.37748,0.54548,0.95787,0.97752,0.99558,0.995,0.91134,0.40507,0.43711,0.97149,0.000729018,0.000729018,0.000729018
|
||||||
|
14,297.709,0.36603,0.54605,0.95927,0.95732,0.97246,0.995,0.90436,0.41202,0.41029,0.95718,0.000714019,0.000714019,0.000714019
|
||||||
|
15,318.208,0.36069,0.53659,0.95795,0.99137,1,0.995,0.91453,0.41606,0.37028,0.97318,0.000699021,0.000699021,0.000699021
|
||||||
|
16,339.518,0.3601,0.50683,0.95127,0.94019,0.99524,0.995,0.90625,0.46517,0.42066,1.04481,0.000684022,0.000684022,0.000684022
|
||||||
|
17,362.129,0.35448,0.49302,0.96129,0.99344,1,0.995,0.93157,0.37921,0.33386,0.93655,0.000669024,0.000669024,0.000669024
|
||||||
|
18,383.414,0.36011,0.49654,0.9532,0.94739,0.96849,0.9946,0.91832,0.39362,0.45317,0.93946,0.000654025,0.000654025,0.000654025
|
||||||
|
19,404.881,0.35675,0.48153,0.9525,0.96781,0.97431,0.99,0.91629,0.36713,0.3564,0.93727,0.000639027,0.000639027,0.000639027
|
||||||
|
20,425.589,0.35226,0.44836,0.94997,0.9939,0.99786,0.995,0.93022,0.37843,0.31009,0.95305,0.000624028,0.000624028,0.000624028
|
||||||
|
21,447.153,0.35963,0.4362,0.94971,0.99017,1,0.995,0.92384,0.39193,0.3189,0.94547,0.00060903,0.00060903,0.00060903
|
||||||
|
22,468.982,0.35262,0.42538,0.94757,0.98639,0.99714,0.995,0.92716,0.36112,0.31048,0.9175,0.000594031,0.000594031,0.000594031
|
||||||
|
23,490.336,0.34499,0.41519,0.94893,0.99256,1,0.995,0.93248,0.35024,0.3062,0.92001,0.000579033,0.000579033,0.000579033
|
||||||
|
24,512.15,0.33571,0.396,0.94286,0.99135,0.97159,0.995,0.94324,0.35043,0.2919,0.90388,0.000564034,0.000564034,0.000564034
|
||||||
|
25,534.597,0.3301,0.38113,0.93667,0.98589,0.99779,0.995,0.94266,0.3542,0.29198,0.92023,0.000549036,0.000549036,0.000549036
|
||||||
|
26,555.046,0.33444,0.39744,0.94136,0.99251,1,0.995,0.93447,0.36479,0.29234,0.92809,0.000534037,0.000534037,0.000534037
|
||||||
|
27,576.452,0.32986,0.38709,0.93638,0.98883,1,0.995,0.92523,0.36926,0.27624,0.9349,0.000519039,0.000519039,0.000519039
|
||||||
|
28,596.917,0.32153,0.3821,0.93416,0.98595,1,0.995,0.93647,0.35348,0.27479,0.90623,0.00050404,0.00050404,0.00050404
|
||||||
|
29,617.562,0.31231,0.37336,0.93255,0.98169,0.97784,0.995,0.92839,0.34092,0.27472,0.909,0.000489042,0.000489042,0.000489042
|
||||||
|
30,638.574,0.31301,0.35371,0.92996,0.99351,1,0.995,0.93088,0.34402,0.27627,0.89746,0.000474043,0.000474043,0.000474043
|
||||||
|
31,660.613,0.31366,0.33084,0.93134,0.97514,0.98765,0.995,0.93899,0.34682,0.299,0.90286,0.000459045,0.000459045,0.000459045
|
||||||
|
32,682.549,0.31376,0.32962,0.9315,0.99007,1,0.995,0.93998,0.33462,0.26922,0.90362,0.000444046,0.000444046,0.000444046
|
||||||
|
33,703.344,0.31362,0.33264,0.92939,0.99153,1,0.995,0.94137,0.34127,0.25437,0.89927,0.000429048,0.000429048,0.000429048
|
||||||
|
34,725.681,0.31963,0.31389,0.93847,0.99375,1,0.995,0.93954,0.33684,0.25281,0.89773,0.000414049,0.000414049,0.000414049
|
||||||
|
35,747.291,0.31974,0.32704,0.93238,0.99495,1,0.995,0.93629,0.33972,0.24074,0.8983,0.000399051,0.000399051,0.000399051
|
||||||
|
36,768.329,0.30826,0.30259,0.9273,0.9945,1,0.995,0.93513,0.35498,0.24272,0.91019,0.000384052,0.000384052,0.000384052
|
||||||
|
37,790.078,0.30423,0.30533,0.93535,0.99221,1,0.995,0.9417,0.34522,0.24637,0.90284,0.000369054,0.000369054,0.000369054
|
||||||
|
38,810.77,0.30261,0.31911,0.93068,0.99466,1,0.995,0.9438,0.319,0.22484,0.88496,0.000354055,0.000354055,0.000354055
|
||||||
|
39,833.014,0.29944,0.29314,0.92939,0.99399,1,0.995,0.94978,0.32843,0.22661,0.88758,0.000339057,0.000339057,0.000339057
|
||||||
|
40,854.979,0.30314,0.29308,0.92547,0.99275,1,0.995,0.93705,0.3388,0.26593,0.89406,0.000324058,0.000324058,0.000324058
|
||||||
|
41,876.271,0.30158,0.30171,0.92695,0.9944,1,0.995,0.9321,0.34916,0.23713,0.90987,0.00030906,0.00030906,0.00030906
|
||||||
|
42,898.715,0.30321,0.28277,0.92936,0.99449,1,0.995,0.92748,0.36614,0.24057,0.9325,0.000294061,0.000294061,0.000294061
|
||||||
|
43,919.495,0.29282,0.28211,0.92175,0.99494,1,0.995,0.95585,0.31322,0.22617,0.88235,0.000279063,0.000279063,0.000279063
|
||||||
|
44,941.722,0.29117,0.2847,0.92475,0.99339,1,0.995,0.94525,0.32771,0.22543,0.88932,0.000264064,0.000264064,0.000264064
|
||||||
|
45,963.932,0.29404,0.27877,0.92829,0.99441,1,0.995,0.95236,0.3072,0.22235,0.86861,0.000249066,0.000249066,0.000249066
|
||||||
|
46,984.507,0.2859,0.26662,0.92208,0.99396,1,0.995,0.95183,0.32424,0.23672,0.87856,0.000234068,0.000234068,0.000234068
|
||||||
|
47,1006.63,0.28645,0.27411,0.91942,0.99374,1,0.995,0.95487,0.31166,0.20915,0.87008,0.000219069,0.000219069,0.000219069
|
||||||
|
48,1027.55,0.27696,0.26363,0.91514,0.99468,1,0.995,0.94573,0.29992,0.21049,0.86498,0.00020407,0.00020407,0.00020407
|
||||||
|
49,1050.01,0.28712,0.26586,0.91623,0.995,1,0.995,0.96567,0.29589,0.20576,0.8606,0.000189072,0.000189072,0.000189072
|
||||||
|
50,1071.76,0.27448,0.2569,0.91217,0.99384,1,0.995,0.95153,0.3016,0.21041,0.86318,0.000174074,0.000174074,0.000174074
|
||||||
|
51,1096.37,0.24454,0.27353,0.90708,0.9931,1,0.995,0.94781,0.31723,0.21284,0.87421,0.000159075,0.000159075,0.000159075
|
||||||
|
52,1117.72,0.23813,0.25866,0.90893,0.99341,1,0.995,0.95297,0.30285,0.21154,0.87651,0.000144077,0.000144077,0.000144077
|
||||||
|
53,1139.37,0.23287,0.24002,0.8973,0.99463,1,0.995,0.95298,0.30491,0.19608,0.87328,0.000129078,0.000129078,0.000129078
|
||||||
|
54,1159.65,0.22664,0.23081,0.90271,0.99499,1,0.995,0.95707,0.3028,0.19075,0.87182,0.00011408,0.00011408,0.00011408
|
||||||
|
55,1180.75,0.22792,0.22646,0.89321,0.99293,1,0.995,0.94881,0.30661,0.20937,0.87424,9.9081e-05,9.9081e-05,9.9081e-05
|
||||||
|
56,1200.5,0.22643,0.22389,0.89645,0.99444,1,0.995,0.95894,0.29503,0.19394,0.86518,8.40825e-05,8.40825e-05,8.40825e-05
|
||||||
|
57,1222.48,0.21807,0.2238,0.889,0.99404,1,0.995,0.9554,0.29928,0.18871,0.86646,6.9084e-05,6.9084e-05,6.9084e-05
|
||||||
|
58,1244.57,0.21641,0.19949,0.87792,0.9946,1,0.995,0.95124,0.29435,0.19182,0.86436,5.40855e-05,5.40855e-05,5.40855e-05
|
||||||
|
59,1265.51,0.21277,0.2038,0.88393,0.99482,1,0.995,0.94917,0.29288,0.18379,0.86309,3.9087e-05,3.9087e-05,3.9087e-05
|
||||||
|
60,1286.49,0.20824,0.1987,0.89503,0.99507,1,0.995,0.94969,0.29104,0.18803,0.86317,2.40885e-05,2.40885e-05,2.40885e-05
|
||||||
|
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 581 KiB |
|
After Width: | Height: | Size: 565 KiB |
|
After Width: | Height: | Size: 596 KiB |
|
After Width: | Height: | Size: 484 KiB |
|
After Width: | Height: | Size: 506 KiB |
|
After Width: | Height: | Size: 515 KiB |
|
After Width: | Height: | Size: 627 KiB |
|
After Width: | Height: | Size: 633 KiB |
|
After Width: | Height: | Size: 560 KiB |
|
After Width: | Height: | Size: 564 KiB |
|
After Width: | Height: | Size: 555 KiB |
|
After Width: | Height: | Size: 555 KiB |
|
|
@ -0,0 +1,574 @@
|
||||||
|
from flask import Flask, request, jsonify, render_template, Response, send_file
|
||||||
|
from ultralytics import YOLO
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import cv2
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
import zipfile
|
||||||
|
import yaml
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
import glob
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# INIT APP
|
||||||
|
# =========================
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
model = YOLO("best.pt")
|
||||||
|
|
||||||
|
latest_result = {
|
||||||
|
"image": "/static/latest.jpg",
|
||||||
|
"nominal": "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
if not os.path.exists("static"):
|
||||||
|
os.makedirs("static")
|
||||||
|
|
||||||
|
if not os.path.exists("uploads"):
|
||||||
|
os.makedirs("uploads")
|
||||||
|
|
||||||
|
# Folder audio WAV
|
||||||
|
if not os.path.exists("static/audio"):
|
||||||
|
os.makedirs("static/audio")
|
||||||
|
|
||||||
|
# Folder untuk menyimpan semua model
|
||||||
|
if not os.path.exists("models"):
|
||||||
|
os.makedirs("models")
|
||||||
|
|
||||||
|
# File untuk menyimpan nama model yang aktif
|
||||||
|
active_model_file = "models/active_model.txt"
|
||||||
|
if not os.path.exists(active_model_file):
|
||||||
|
with open(active_model_file, "w") as f:
|
||||||
|
f.write("original")
|
||||||
|
|
||||||
|
# Simpan best.pt awal sebagai model "original" jika belum ada
|
||||||
|
_original_dir = os.path.join("models", "original")
|
||||||
|
if not os.path.exists(_original_dir) and os.path.exists("best.pt"):
|
||||||
|
os.makedirs(_original_dir, exist_ok=True)
|
||||||
|
shutil.copy("best.pt", os.path.join(_original_dir, "best.pt"))
|
||||||
|
_info = {
|
||||||
|
"name": "original",
|
||||||
|
"base_model": "best.pt",
|
||||||
|
"epochs": "-",
|
||||||
|
"imgsz": "-",
|
||||||
|
"timestamp": "000000_000000",
|
||||||
|
"created": "Model Awal"
|
||||||
|
}
|
||||||
|
with open(os.path.join(_original_dir, "info.yaml"), "w") as f:
|
||||||
|
yaml.dump(_info, f)
|
||||||
|
|
||||||
|
# Queue untuk streaming log training
|
||||||
|
training_log_queue = queue.Queue()
|
||||||
|
training_running = False
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# HALAMAN MONITORING
|
||||||
|
# =========================
|
||||||
|
@app.route('/')
|
||||||
|
def home():
|
||||||
|
return render_template('monitoring.html')
|
||||||
|
|
||||||
|
@app.route('/monitoring')
|
||||||
|
def monitoring():
|
||||||
|
return render_template('monitoring.html')
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# HALAMAN TESTING
|
||||||
|
# =========================
|
||||||
|
@app.route('/testing')
|
||||||
|
def test_page():
|
||||||
|
return render_template('testing.html')
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# HALAMAN TRAINING
|
||||||
|
# =========================
|
||||||
|
@app.route('/training')
|
||||||
|
def training_page():
|
||||||
|
return render_template('training.html')
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# STEP 0 - INSTALL ULTRALYTICS
|
||||||
|
# =========================
|
||||||
|
@app.route('/install_ultralytics', methods=['POST'])
|
||||||
|
def install_ultralytics():
|
||||||
|
def run_install():
|
||||||
|
while not training_log_queue.empty():
|
||||||
|
training_log_queue.get()
|
||||||
|
|
||||||
|
training_log_queue.put("▶ Menginstall ultralytics...\n")
|
||||||
|
|
||||||
|
process = subprocess.Popen(
|
||||||
|
[sys.executable, "-m", "pip", "install", "ultralytics"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1
|
||||||
|
)
|
||||||
|
for line in process.stdout:
|
||||||
|
training_log_queue.put(line)
|
||||||
|
|
||||||
|
process.wait()
|
||||||
|
if process.returncode == 0:
|
||||||
|
training_log_queue.put("\n✅ Ultralytics berhasil diinstall!\n")
|
||||||
|
else:
|
||||||
|
training_log_queue.put("\n❌ Gagal install ultralytics.\n")
|
||||||
|
training_log_queue.put("__DONE__")
|
||||||
|
|
||||||
|
thread = threading.Thread(target=run_install, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# STEP 1 - UPLOAD DATASET
|
||||||
|
# =========================
|
||||||
|
@app.route('/upload_dataset', methods=['POST'])
|
||||||
|
def upload_dataset():
|
||||||
|
if 'dataset' not in request.files:
|
||||||
|
return jsonify({"error": "No file uploaded"}), 400
|
||||||
|
|
||||||
|
file = request.files['dataset']
|
||||||
|
if not file.filename.endswith('.zip'):
|
||||||
|
return jsonify({"error": "File harus berupa .zip"}), 400
|
||||||
|
|
||||||
|
zip_path = "uploads/data.zip"
|
||||||
|
os.makedirs("uploads", exist_ok=True)
|
||||||
|
file.save(zip_path)
|
||||||
|
|
||||||
|
extract_path = "uploads/custom_data"
|
||||||
|
if os.path.exists(extract_path):
|
||||||
|
shutil.rmtree(extract_path)
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as z:
|
||||||
|
z.extractall(extract_path)
|
||||||
|
|
||||||
|
return jsonify({"status": "ok", "message": "Dataset berhasil diupload dan diekstrak"})
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# STEP 2 - SPLIT DATASET
|
||||||
|
# =========================
|
||||||
|
@app.route('/split_dataset', methods=['POST'])
|
||||||
|
def split_dataset():
|
||||||
|
import random
|
||||||
|
|
||||||
|
src = "uploads/custom_data"
|
||||||
|
train_pct = float(request.json.get("train_pct", 0.9))
|
||||||
|
|
||||||
|
images = glob.glob(f"{src}/**/*.jpg", recursive=True) + \
|
||||||
|
glob.glob(f"{src}/**/*.jpeg", recursive=True) + \
|
||||||
|
glob.glob(f"{src}/**/*.png", recursive=True)
|
||||||
|
|
||||||
|
if len(images) == 0:
|
||||||
|
return jsonify({"error": "Tidak ada gambar ditemukan di dataset"}), 400
|
||||||
|
|
||||||
|
random.shuffle(images)
|
||||||
|
split_idx = int(len(images) * train_pct)
|
||||||
|
train_imgs = images[:split_idx]
|
||||||
|
val_imgs = images[split_idx:]
|
||||||
|
|
||||||
|
for split, imgs in [("train", train_imgs), ("validation", val_imgs)]:
|
||||||
|
os.makedirs(f"uploads/data/{split}/images", exist_ok=True)
|
||||||
|
os.makedirs(f"uploads/data/{split}/labels", exist_ok=True)
|
||||||
|
for img_path in imgs:
|
||||||
|
shutil.copy(img_path, f"uploads/data/{split}/images/")
|
||||||
|
label_path = img_path.rsplit(".", 1)[0] + ".txt"
|
||||||
|
label_path = label_path.replace("\\images\\", "\\labels\\").replace("/images/", "/labels/")
|
||||||
|
if os.path.exists(label_path):
|
||||||
|
shutil.copy(label_path, f"uploads/data/{split}/labels/")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"status": "ok",
|
||||||
|
"train": len(train_imgs),
|
||||||
|
"validation": len(val_imgs),
|
||||||
|
"message": f"Dataset dibagi: {len(train_imgs)} train, {len(val_imgs)} validasi"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# STEP 3 - BUAT data.yaml
|
||||||
|
# =========================
|
||||||
|
@app.route('/create_yaml', methods=['POST'])
|
||||||
|
def create_yaml():
|
||||||
|
classes_txt = "uploads/custom_data/classes.txt"
|
||||||
|
|
||||||
|
if not os.path.exists(classes_txt):
|
||||||
|
found = glob.glob("uploads/custom_data/**/classes.txt", recursive=True)
|
||||||
|
if found:
|
||||||
|
classes_txt = found[0]
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "classes.txt tidak ditemukan di dalam dataset"}), 400
|
||||||
|
|
||||||
|
with open(classes_txt, 'r') as f:
|
||||||
|
classes = [line.strip() for line in f.readlines() if line.strip()]
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'path': os.path.abspath("uploads/data").replace("\\", "/"),
|
||||||
|
'train': 'train/images',
|
||||||
|
'val': 'validation/images',
|
||||||
|
'nc': len(classes),
|
||||||
|
'names': classes
|
||||||
|
}
|
||||||
|
|
||||||
|
yaml_path = "uploads/data.yaml"
|
||||||
|
with open(yaml_path, 'w') as f:
|
||||||
|
yaml.dump(data, f, sort_keys=False)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"status": "ok",
|
||||||
|
"classes": classes,
|
||||||
|
"message": f"data.yaml berhasil dibuat dengan {len(classes)} kelas: {', '.join(classes)}"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# STEP 4 - MULAI TRAINING
|
||||||
|
# =========================
|
||||||
|
@app.route('/start_training', methods=['POST'])
|
||||||
|
def start_training():
|
||||||
|
global training_running
|
||||||
|
|
||||||
|
if training_running:
|
||||||
|
return jsonify({"error": "Training sedang berjalan"}), 400
|
||||||
|
|
||||||
|
data = request.json or {}
|
||||||
|
epochs = int(data.get("epochs", 60))
|
||||||
|
imgsz = int(data.get("imgsz", 640))
|
||||||
|
model_size = data.get("model", "yolov8n.pt")
|
||||||
|
model_name = data.get("model_name", "").strip()
|
||||||
|
|
||||||
|
yaml_path = os.path.abspath("uploads/data.yaml")
|
||||||
|
if not os.path.exists(yaml_path):
|
||||||
|
return jsonify({"error": "data.yaml belum dibuat, jalankan Step 4 dulu"}), 400
|
||||||
|
|
||||||
|
def run_training():
|
||||||
|
global training_running
|
||||||
|
training_running = True
|
||||||
|
|
||||||
|
while not training_log_queue.empty():
|
||||||
|
training_log_queue.get()
|
||||||
|
|
||||||
|
project_path = os.path.abspath("uploads/runs").replace("\\", "/")
|
||||||
|
train_script = f"""
|
||||||
|
from ultralytics import YOLO
|
||||||
|
model = YOLO("{model_size}")
|
||||||
|
model.train(
|
||||||
|
data=r"{yaml_path}",
|
||||||
|
epochs={epochs},
|
||||||
|
imgsz={imgsz},
|
||||||
|
project=r"{project_path}",
|
||||||
|
name="train",
|
||||||
|
exist_ok=True
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
script_path = "uploads/_train_runner.py"
|
||||||
|
with open(script_path, "w") as f:
|
||||||
|
f.write(train_script)
|
||||||
|
|
||||||
|
cmd = [sys.executable, script_path]
|
||||||
|
training_log_queue.put(f"▶ Memulai training: {model_size}, epochs={epochs}, imgsz={imgsz}\n")
|
||||||
|
training_log_queue.put(f"▶ Python: {sys.executable}\n\n")
|
||||||
|
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
encoding='utf-8',
|
||||||
|
errors='replace'
|
||||||
|
)
|
||||||
|
for line in process.stdout:
|
||||||
|
training_log_queue.put(line)
|
||||||
|
|
||||||
|
process.wait()
|
||||||
|
|
||||||
|
if process.returncode == 0:
|
||||||
|
best_src = os.path.join("uploads", "runs", "train", "weights", "best.pt")
|
||||||
|
last_src = os.path.join("uploads", "runs", "train", "weights", "last.pt")
|
||||||
|
|
||||||
|
# Buat nama folder model
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
base_model = model_size.replace(".pt", "")
|
||||||
|
display_name = model_name if model_name else f"{base_model}_{timestamp}"
|
||||||
|
save_dir = os.path.join("models", display_name)
|
||||||
|
os.makedirs(save_dir, exist_ok=True)
|
||||||
|
|
||||||
|
msg = "\n"
|
||||||
|
if os.path.exists(best_src):
|
||||||
|
shutil.copy(best_src, os.path.join(save_dir, "best.pt"))
|
||||||
|
msg += f"✅ best.pt disimpan ke models/{display_name}/\n"
|
||||||
|
else:
|
||||||
|
msg += "⚠️ best.pt tidak ditemukan.\n"
|
||||||
|
|
||||||
|
if os.path.exists(last_src):
|
||||||
|
shutil.copy(last_src, os.path.join(save_dir, "last.pt"))
|
||||||
|
msg += f"✅ last.pt disimpan ke models/{display_name}/\n"
|
||||||
|
else:
|
||||||
|
msg += "⚠️ last.pt tidak ditemukan.\n"
|
||||||
|
|
||||||
|
# Simpan info model
|
||||||
|
info = {
|
||||||
|
"name": display_name,
|
||||||
|
"base_model": model_size,
|
||||||
|
"epochs": epochs,
|
||||||
|
"imgsz": imgsz,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"created": datetime.now().strftime("%d %b %Y, %H:%M")
|
||||||
|
}
|
||||||
|
with open(os.path.join(save_dir, "info.yaml"), "w") as f:
|
||||||
|
yaml.dump(info, f)
|
||||||
|
|
||||||
|
msg += f"\n💾 Model tersimpan sebagai: {display_name}"
|
||||||
|
training_log_queue.put(msg)
|
||||||
|
else:
|
||||||
|
training_log_queue.put(f"\n❌ Training gagal dengan kode: {process.returncode}\n")
|
||||||
|
|
||||||
|
training_log_queue.put("__DONE__")
|
||||||
|
training_running = False
|
||||||
|
|
||||||
|
thread = threading.Thread(target=run_training, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify({"status": "ok", "message": "Training dimulai"})
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# STREAM LOG TRAINING (SSE)
|
||||||
|
# =========================
|
||||||
|
# Kata kunci baris yang TIDAK perlu ditampilkan
|
||||||
|
_LOG_SKIP = [
|
||||||
|
"Scanning", "images,", "corrupt", "it/s",
|
||||||
|
"backgrounds", "labels...", "cache"
|
||||||
|
]
|
||||||
|
|
||||||
|
@app.route('/training_log')
|
||||||
|
def training_log():
|
||||||
|
def generate():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = training_log_queue.get(timeout=2)
|
||||||
|
if line == "__DONE__":
|
||||||
|
yield "data: __DONE__\n\n"
|
||||||
|
break
|
||||||
|
|
||||||
|
clean = line.rstrip()
|
||||||
|
if not clean:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip baris yang tidak penting (scanning, progress bar, dll)
|
||||||
|
if any(skip in clean for skip in _LOG_SKIP):
|
||||||
|
continue
|
||||||
|
|
||||||
|
safe = clean.replace("\n", " ")
|
||||||
|
yield f"data: {safe}\n\n"
|
||||||
|
|
||||||
|
except queue.Empty:
|
||||||
|
yield ": keepalive\n\n"
|
||||||
|
|
||||||
|
return Response(generate(), mimetype='text/event-stream',
|
||||||
|
headers={
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
|
"Connection": "keep-alive"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# DAFTAR SEMUA MODEL
|
||||||
|
# =========================
|
||||||
|
@app.route('/list_models')
|
||||||
|
def list_models():
|
||||||
|
models_list = []
|
||||||
|
|
||||||
|
active = "best.pt"
|
||||||
|
if os.path.exists(active_model_file):
|
||||||
|
with open(active_model_file, "r") as f:
|
||||||
|
active = f.read().strip()
|
||||||
|
|
||||||
|
if not os.path.exists("models"):
|
||||||
|
return jsonify({"models": [], "active": active})
|
||||||
|
|
||||||
|
for folder in sorted(os.listdir("models"), reverse=True):
|
||||||
|
folder_path = os.path.join("models", folder)
|
||||||
|
if not os.path.isdir(folder_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
best_path = os.path.join(folder_path, "best.pt")
|
||||||
|
if not os.path.exists(best_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
info_path = os.path.join(folder_path, "info.yaml")
|
||||||
|
info = {}
|
||||||
|
if os.path.exists(info_path):
|
||||||
|
with open(info_path, "r") as f:
|
||||||
|
info = yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
models_list.append({
|
||||||
|
"name": folder,
|
||||||
|
"base_model": info.get("base_model", "-"),
|
||||||
|
"epochs": info.get("epochs", "-"),
|
||||||
|
"imgsz": info.get("imgsz", "-"),
|
||||||
|
"created": info.get("created", "-"),
|
||||||
|
"active": (active == folder)
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({"models": models_list, "active": active})
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# TERAPKAN MODEL
|
||||||
|
# =========================
|
||||||
|
@app.route('/apply_model', methods=['POST'])
|
||||||
|
def apply_model():
|
||||||
|
global model
|
||||||
|
|
||||||
|
data = request.json or {}
|
||||||
|
model_name = data.get("name", "")
|
||||||
|
best_path = os.path.join("models", model_name, "best.pt")
|
||||||
|
|
||||||
|
if not os.path.exists(best_path):
|
||||||
|
return jsonify({"error": f"Model '{model_name}' tidak ditemukan"}), 404
|
||||||
|
|
||||||
|
shutil.copy(best_path, "best.pt")
|
||||||
|
model = YOLO("best.pt")
|
||||||
|
|
||||||
|
with open(active_model_file, "w") as f:
|
||||||
|
f.write(model_name)
|
||||||
|
|
||||||
|
return jsonify({"status": "ok", "message": f"Model '{model_name}' berhasil diterapkan!"})
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# DOWNLOAD MODEL TERTENTU
|
||||||
|
# =========================
|
||||||
|
@app.route('/download_model/<model_name>')
|
||||||
|
def download_model(model_name):
|
||||||
|
folder_path = os.path.join("models", model_name)
|
||||||
|
best_src = os.path.join(folder_path, "best.pt")
|
||||||
|
last_src = os.path.join(folder_path, "last.pt")
|
||||||
|
|
||||||
|
if not os.path.exists(best_src):
|
||||||
|
return jsonify({"error": "Model tidak ditemukan"}), 404
|
||||||
|
|
||||||
|
zip_path = os.path.join("uploads", f"{model_name}.zip")
|
||||||
|
with zipfile.ZipFile(zip_path, 'w') as z:
|
||||||
|
z.write(best_src, "best.pt")
|
||||||
|
if os.path.exists(last_src):
|
||||||
|
z.write(last_src, "last.pt")
|
||||||
|
|
||||||
|
return send_file(os.path.abspath(zip_path), as_attachment=True,
|
||||||
|
download_name=f"{model_name}.zip")
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# STATUS TRAINING
|
||||||
|
# =========================
|
||||||
|
@app.route('/training_status')
|
||||||
|
def training_status():
|
||||||
|
return jsonify({"running": training_running})
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# DETEKSI (ESP32 + POSTMAN)
|
||||||
|
# =========================
|
||||||
|
@app.route('/detect', methods=['POST'])
|
||||||
|
def detect():
|
||||||
|
filepath = "temp.jpg"
|
||||||
|
|
||||||
|
if request.data:
|
||||||
|
with open(filepath, "wb") as f:
|
||||||
|
f.write(request.data)
|
||||||
|
elif 'image' in request.files:
|
||||||
|
file = request.files['image']
|
||||||
|
file.save(filepath)
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "No image uploaded"})
|
||||||
|
|
||||||
|
results = model(filepath, conf=0.6)
|
||||||
|
boxes = results[0].boxes
|
||||||
|
|
||||||
|
if len(boxes) > 0:
|
||||||
|
best_idx = boxes.conf.argmax()
|
||||||
|
cls_id = int(boxes.cls[best_idx])
|
||||||
|
label = results[0].names[cls_id]
|
||||||
|
else:
|
||||||
|
label = "Tidak terdeteksi"
|
||||||
|
|
||||||
|
shutil.copy(filepath, "static/latest.jpg")
|
||||||
|
latest_result["nominal"] = label
|
||||||
|
|
||||||
|
return jsonify({"nominal": label})
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# DATA MONITORING
|
||||||
|
# =========================
|
||||||
|
@app.route('/latest')
|
||||||
|
def latest():
|
||||||
|
return jsonify(latest_result)
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# SERVE AUDIO WAV
|
||||||
|
# =========================
|
||||||
|
@app.route('/audio/<filename>')
|
||||||
|
def serve_audio(filename):
|
||||||
|
# Hanya izinkan ekstensi .wav
|
||||||
|
if not filename.endswith('.wav'):
|
||||||
|
return jsonify({"error": "Hanya file .wav yang diizinkan"}), 400
|
||||||
|
|
||||||
|
audio_path = os.path.join("static", "audio", filename)
|
||||||
|
|
||||||
|
if not os.path.exists(audio_path):
|
||||||
|
return jsonify({"error": f"File {filename} tidak ditemukan"}), 404
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
os.path.abspath(audio_path),
|
||||||
|
mimetype='audio/wav',
|
||||||
|
as_attachment=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# TESTING UPLOAD + BOUNDING BOX
|
||||||
|
# =========================
|
||||||
|
@app.route('/upload_test', methods=['POST'])
|
||||||
|
def upload_test():
|
||||||
|
file = request.files['image']
|
||||||
|
filepath = "static/test.jpg"
|
||||||
|
file.save(filepath)
|
||||||
|
|
||||||
|
results = model(filepath, conf=0.6)
|
||||||
|
result_img = results[0].plot()
|
||||||
|
output_path = "static/result.jpg"
|
||||||
|
cv2.imwrite(output_path, result_img)
|
||||||
|
|
||||||
|
boxes = results[0].boxes
|
||||||
|
|
||||||
|
if len(boxes) > 0:
|
||||||
|
best_idx = boxes.conf.argmax()
|
||||||
|
conf = float(boxes.conf[best_idx])
|
||||||
|
cls_id = int(boxes.cls[best_idx])
|
||||||
|
label = results[0].names[cls_id]
|
||||||
|
else:
|
||||||
|
label = "Tidak terdeteksi"
|
||||||
|
conf = 0
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"image": "/" + output_path,
|
||||||
|
"nominal": label,
|
||||||
|
"confidence": round(conf, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# RUN SERVER
|
||||||
|
# =========================
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=False, threaded=True)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
original
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
base_model: best.pt
|
||||||
|
created: Model Awal
|
||||||
|
epochs: '-'
|
||||||
|
imgsz: '-'
|
||||||
|
name: original
|
||||||
|
timestamp: '000000_000000'
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
base_model: yolov8n.pt
|
||||||
|
created: 02 Jun 2026, 02:50
|
||||||
|
epochs: 5
|
||||||
|
imgsz: 640
|
||||||
|
name: yolov8n_20260602_025018
|
||||||
|
timestamp: '20260602_025018'
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
base_model: yolov8n.pt
|
||||||
|
created: 02 Jun 2026, 03:01
|
||||||
|
epochs: 10
|
||||||
|
imgsz: 640
|
||||||
|
name: yolov8n_20260602_030101
|
||||||
|
timestamp: '20260602_030101'
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
base_model: yolov8n.pt
|
||||||
|
created: 04 Jun 2026, 01:29
|
||||||
|
epochs: 30
|
||||||
|
imgsz: 640
|
||||||
|
name: yolov8n_20260604_012950
|
||||||
|
timestamp: '20260604_012950'
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -0,0 +1,234 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>MIRA - Monitoring</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-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-top: 7px;
|
||||||
|
opacity: 0.9;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-money {
|
||||||
|
width: 22px; height: 22px;
|
||||||
|
display: inline-block;
|
||||||
|
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;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6D60B4;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-box {
|
||||||
|
background: #C4BFDF;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 520px;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-box img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nominal-label {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6D60B4;
|
||||||
|
margin: 24px 0 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nominal-display {
|
||||||
|
background: #ECEAF6;
|
||||||
|
border: 2px solid #C4BFDF;
|
||||||
|
border-radius: 40px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6D60B4;
|
||||||
|
width: 520px;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</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 active">
|
||||||
|
<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">
|
||||||
|
<span class="icon-train"></span> Training
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-wrap">
|
||||||
|
<div class="content-area">
|
||||||
|
<div class="page-title">Monitoring</div>
|
||||||
|
<div class="camera-box">
|
||||||
|
<img id="image" src="" alt="camera feed" />
|
||||||
|
</div>
|
||||||
|
<div class="nominal-label">Nominal</div>
|
||||||
|
<div class="nominal-display" id="nominal"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadData() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/latest");
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById("image").src = data.image + "?t=" + new Date().getTime();
|
||||||
|
document.getElementById("nominal").innerText = data.nominal;
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Error:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInterval(loadData, 1000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,303 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>MIRA - Testing</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-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-top: 7px;
|
||||||
|
opacity: 0.9;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-money {
|
||||||
|
width: 22px; height: 22px;
|
||||||
|
display: inline-block;
|
||||||
|
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;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6D60B4;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-card-wrap {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-card {
|
||||||
|
background: #C4BFDF;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 28px 32px;
|
||||||
|
width: 420px;
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-title {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #4a3a9a;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-label {
|
||||||
|
background: #6D60B4;
|
||||||
|
color: white;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 7px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-label:hover { background: #5c51a0; }
|
||||||
|
|
||||||
|
input[type="file"] { display: none; }
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #4a3a9a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detect-btn {
|
||||||
|
background: #6D60B4;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 28px;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detect-btn:hover { background: #5c51a0; }
|
||||||
|
|
||||||
|
.preview-box { width: 100%; }
|
||||||
|
|
||||||
|
.preview-box img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-box {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #4a3a9a;
|
||||||
|
line-height: 2;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</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 active">
|
||||||
|
<span class="icon-money"></span> Testing
|
||||||
|
</a>
|
||||||
|
<a href="/training" class="nav-item">
|
||||||
|
<span class="icon-train"></span> Training
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-wrap">
|
||||||
|
<div class="content-area">
|
||||||
|
<div class="page-title">Testing</div>
|
||||||
|
<div class="test-card-wrap">
|
||||||
|
<div class="test-card">
|
||||||
|
<div class="test-title">Test Deteksi Uang</div>
|
||||||
|
<div class="file-row">
|
||||||
|
<label class="file-label" for="fileInput">Choose File</label>
|
||||||
|
<input type="file" id="fileInput" onchange="updateFileName(this)" />
|
||||||
|
<span class="file-name" id="fileName">No File Chosen</span>
|
||||||
|
</div>
|
||||||
|
<button class="detect-btn" onclick="uploadImage()">Detect</button>
|
||||||
|
<div class="preview-box">
|
||||||
|
<img id="preview" src="" alt="" style="display:none;" />
|
||||||
|
</div>
|
||||||
|
<div class="result-box">
|
||||||
|
Nominal : <span id="nominal">-</span><br>
|
||||||
|
Convidance : <span id="conf">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function updateFileName(input) {
|
||||||
|
const name = input.files[0] ? input.files[0].name : "No File Chosen";
|
||||||
|
document.getElementById("fileName").textContent = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadImage() {
|
||||||
|
let fileInput = document.getElementById("fileInput");
|
||||||
|
let file = fileInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append("image", file);
|
||||||
|
|
||||||
|
fetch("/upload_test", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
const preview = document.getElementById("preview");
|
||||||
|
preview.src = data.image + "?t=" + new Date().getTime();
|
||||||
|
preview.style.display = "block";
|
||||||
|
document.getElementById("nominal").innerText = data.nominal;
|
||||||
|
document.getElementById("conf").innerText = data.confidence;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,657 @@
|
||||||
|
<!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 30–60.</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} | Epochs: ${m.epochs} | Imgsz: ${m.imgsz} | ${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>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
from ultralytics import YOLO
|
||||||
|
model = YOLO("yolov8n.pt")
|
||||||
|
model.train(
|
||||||
|
data=r"c:\ta\project\uploads\data.yaml",
|
||||||
|
epochs=30,
|
||||||
|
imgsz=640,
|
||||||
|
project=r"c:/ta/project/uploads/runs",
|
||||||
|
name="train",
|
||||||
|
exist_ok=True
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
dua ribu
|
||||||
|
seribu
|
||||||
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 313 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
2 0.49123303167420823 0.5090497737556561 0.9349547511312218 0.6696832579185522
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0 0.49971719457013575 0.5622171945701357 0.9451357466063349 0.6493212669683258
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0 0.5580693815987933 0.5022624434389141 0.6696832579185519 0.9411764705882354
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0 0.5233785822021112 0.5011312217194565 0.6847662141779781 0.9570135746606327
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
2 0.5173453996983408 0.47171945701357465 0.8355957767722473 0.9434389140271493
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0 0.5005656108597286 0.5169683257918551 0.973981900452489 0.6809954751131221
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
2 0.5082013574660634 0.48868778280542985 0.9383484162895929 0.6742081447963801
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
2 0.5098039215686275 0.505656108597285 0.6757164404223228 0.9389140271493213
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
2 0.5233785822021116 0.4966063348416289 0.6787330316742082 0.9434389140271493
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0 0.4494720965309201 0.4457013574660634 0.7903469079939669 0.8914027149321267
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0 0.4766214177978884 0.5022624434389141 0.6696832579185521 0.9773755656108598
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
0 0.46455505279034703 0.47398190045248867 0.81447963800905 0.9479638009049773
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
2 0.5113122171945702 0.4615384615384615 0.799396681749623 0.923076923076923
|
||||||