Upload source code tugas akhir

This commit is contained in:
nauval 2026-06-15 21:02:13 +07:00
commit 26fa148c14
2143 changed files with 3869 additions and 0 deletions

View File

@ -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);
}

BIN
my_model/my_model.pt Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

110
my_model/train/args.yaml Normal file
View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
my_model/train/labels.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

View File

@ -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
1 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
2 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
3 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
4 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
5 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
6 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
7 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
8 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
9 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
10 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
11 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
12 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
13 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
14 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
15 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
16 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
17 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
18 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
19 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
20 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
21 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
22 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
23 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
24 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
25 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
26 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
27 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
28 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
29 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
30 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
31 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
32 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
33 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
34 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
35 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
36 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
37 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
38 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
39 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
40 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
41 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
42 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
43 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
44 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
45 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
46 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
47 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
48 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
49 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
50 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
51 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
52 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
53 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
54 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
55 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
56 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
57 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
58 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
59 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
60 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
61 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

BIN
my_model/train/results.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

Binary file not shown.

574
project/app.py Normal file
View File

@ -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)

BIN
project/best.pt Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
original

Binary file not shown.

View File

@ -0,0 +1,6 @@
base_model: best.pt
created: Model Awal
epochs: '-'
imgsz: '-'
name: original
timestamp: '000000_000000'

Binary file not shown.

View File

@ -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'

Binary file not shown.

Binary file not shown.

View File

@ -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'

Binary file not shown.

Binary file not shown.

View File

@ -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'

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
project/static/latest.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
project/static/result.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

BIN
project/static/test.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

BIN
project/temp.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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 3060.</p>
<p class="model-note">* Hasil training: <strong>best.pt</strong> (akurasi terbaik) dan <strong>last.pt</strong> (epoch terakhir) akan tersimpan otomatis.</p>
<div class="log-box" id="logBox"></div>
</div>
<!-- STEP 6: Kelola Model -->
<div class="step-card">
<div class="step-header">
<div class="step-badge">6</div>
<div>
<div class="step-title">Kelola Model</div>
<div class="step-desc">Lihat semua model tersimpan, terapkan, atau download model yang diinginkan</div>
</div>
<button onclick="loadModels()" style="margin-left:auto; background:white; color:#6D60B4; border:1.5px solid #6D60B4; border-radius:9px; padding:7px 16px; font-family:'Poppins',sans-serif; font-size:12px; font-weight:600; cursor:pointer;">🔄 Refresh</button>
</div>
<div id="modelLibrary">
<div style="font-size:12px; color:#9a9abf; font-style:italic;">Klik Refresh untuk memuat daftar model.</div>
</div>
<div class="result-info" id="info5"></div>
</div>
</div>
</div>
</div>
<script>
function updateFileName(input, targetId) {
document.getElementById(targetId).textContent = input.files[0] ? input.files[0].name : "Belum ada file";
}
function setStatus(stepNum, type, text) {
const el = document.getElementById('status' + stepNum);
el.className = 'step-status status-' + type;
el.textContent = text;
}
function showInfo(stepNum, text, isError) {
const el = document.getElementById('info' + stepNum);
el.textContent = text;
el.style.background = isError ? '#fde8e8' : '#eceaf6';
el.style.color = isError ? '#b94040' : '#4a3a9a';
el.classList.add('visible');
}
function appendLog(boxId, text) {
const box = document.getElementById(boxId);
box.classList.add('visible');
box.textContent += text + '\n';
box.scrollTop = box.scrollHeight;
}
// === AUTOCOMPLETE ===
function showSuggestions() {
filterSuggestions(document.getElementById('modelSize').value);
document.getElementById('suggestionBox').style.display = 'block';
}
function hideSuggestions() {
document.getElementById('suggestionBox').style.display = 'none';
}
function selectModel(val) {
document.getElementById('modelSize').value = val;
hideSuggestions();
}
function filterSuggestions(query) {
const items = document.querySelectorAll('.sg-item');
const q = query.toLowerCase().trim();
items.forEach(item => {
const text = item.textContent.toLowerCase();
item.classList.toggle('sg-hidden', q.length > 0 && !text.includes(q));
});
document.getElementById('suggestionBox').style.display = 'block';
}
// STEP 0 - Install Ultralytics
function installUltralytics() {
const logBox = document.getElementById('logBox0');
const btn = document.getElementById('btnInstall');
logBox.textContent = '';
logBox.classList.add('visible');
btn.disabled = true;
setStatus(0, 'run', 'Installing...');
fetch('/install_ultralytics', { method: 'POST' })
.then(res => res.json())
.then(() => {
const evtSource = new EventSource('/training_log');
evtSource.onmessage = function(e) {
if (e.data === '__DONE__') {
evtSource.close();
btn.disabled = false;
if (logBox.textContent.includes('❌')) {
setStatus(0, 'err', 'Gagal');
} else {
setStatus(0, 'ok', 'Selesai ✓');
}
return;
}
if (e.data.trim()) appendLog('logBox0', e.data);
};
evtSource.onerror = function() {
evtSource.close();
btn.disabled = false;
setStatus(0, 'err', 'Koneksi terputus');
};
}).catch(e => {
btn.disabled = false;
setStatus(0, 'err', 'Error');
appendLog('logBox0', '❌ ' + e);
});
}
// STEP 1 - Upload Dataset
async function uploadDataset() {
const file = document.getElementById('datasetFile').files[0];
if (!file) { alert('Pilih file .zip terlebih dahulu!'); return; }
setStatus(1, 'run', 'Mengupload...');
const formData = new FormData();
formData.append('dataset', file);
try {
const res = await fetch('/upload_dataset', { method: 'POST', body: formData });
const data = await res.json();
if (data.error) { setStatus(1, 'err', 'Gagal'); showInfo(1, '❌ ' + data.error, true); }
else { setStatus(1, 'ok', 'Selesai ✓'); showInfo(1, '✅ ' + data.message, false); }
} catch(e) {
setStatus(1, 'err', 'Error'); showInfo(1, '❌ Gagal upload: ' + e, true);
}
}
// STEP 2 - Split Dataset
async function splitDataset() {
const pct = parseFloat(document.getElementById('trainPct').value) / 100;
setStatus(2, 'run', 'Memproses...');
try {
const res = await fetch('/split_dataset', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ train_pct: pct })
});
const data = await res.json();
if (data.error) { setStatus(2, 'err', 'Gagal'); showInfo(2, '❌ ' + data.error, true); }
else { setStatus(2, 'ok', 'Selesai ✓'); showInfo(2, '✅ ' + data.message, false); }
} catch(e) {
setStatus(2, 'err', 'Error'); showInfo(2, '❌ ' + e, true);
}
}
// STEP 3 - Buat data.yaml
async function createYaml() {
setStatus(3, 'run', 'Memproses...');
try {
const res = await fetch('/create_yaml', { method: 'POST' });
const data = await res.json();
if (data.error) { setStatus(3, 'err', 'Gagal'); showInfo(3, '❌ ' + data.error, true); }
else { setStatus(3, 'ok', 'Selesai ✓'); showInfo(3, '✅ ' + data.message, false); }
} catch(e) {
setStatus(3, 'err', 'Error'); showInfo(3, '❌ ' + e, true);
}
}
// STEP 4 - Training
function startTraining() {
const epochs = document.getElementById('epochs').value;
const imgsz = document.getElementById('imgsz').value;
const modelSize = document.getElementById('modelSize').value.trim();
const logBox = document.getElementById('logBox');
const btn = document.getElementById('btnTrain');
if (!modelSize) { alert('Masukkan nama model terlebih dahulu!'); return; }
logBox.textContent = '';
logBox.classList.add('visible');
btn.disabled = true;
setStatus(4, 'run', 'Training...');
fetch('/start_training', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ epochs, imgsz, model: modelSize })
})
.then(res => res.json())
.then(data => {
if (data.error) {
setStatus(4, 'err', 'Gagal');
appendLog('logBox', '❌ ' + data.error);
btn.disabled = false;
return;
}
const evtSource = new EventSource('/training_log');
evtSource.onmessage = function(e) {
if (e.data === '__DONE__') {
evtSource.close();
btn.disabled = false;
if (logBox.textContent.includes('❌')) {
setStatus(4, 'err', 'Gagal');
} else {
setStatus(4, 'ok', 'Selesai ✓');
loadModels();
}
return;
}
if (e.data.trim()) appendLog('logBox', e.data);
};
evtSource.onerror = function() {
evtSource.close();
setStatus(4, 'err', 'Koneksi terputus');
btn.disabled = false;
};
})
.catch(e => {
setStatus(4, 'err', 'Error');
appendLog('logBox', '❌ ' + e);
btn.disabled = false;
});
}
// STEP 6 - MODEL LIBRARY
async function loadModels() {
const container = document.getElementById('modelLibrary');
container.innerHTML = '<div style="font-size:12px;color:#9a9abf;font-style:italic;">Memuat...</div>';
try {
const res = await fetch('/list_models');
const data = await res.json();
const models = data.models;
if (models.length === 0) {
container.innerHTML = '<div style="font-size:12px;color:#9a9abf;font-style:italic;">Belum ada model tersimpan. Jalankan training terlebih dahulu.</div>';
return;
}
container.innerHTML = '';
models.forEach(m => {
const card = document.createElement('div');
card.style.cssText = `
background:${m.active ? '#f0eef9' : 'white'};
border:1.5px solid ${m.active ? '#6D60B4' : '#D4CFEE'};
border-radius:12px; padding:12px 16px; margin-bottom:10px;
display:flex; align-items:center; gap:12px; flex-wrap:wrap;
`;
card.innerHTML = `
<div style="flex:1; min-width:0;">
<div style="font-size:13px; font-weight:700; color:#4a3a9a; display:flex; align-items:center; gap:8px;">
${m.name}
${m.active ? '<span style="background:#6D60B4;color:white;font-size:9px;font-weight:700;padding:2px 8px;border-radius:20px;">AKTIF</span>' : ''}
</div>
<div style="font-size:11px; color:#9a90c0; margin-top:3px;">
Base: ${m.base_model} &nbsp;|&nbsp; Epochs: ${m.epochs} &nbsp;|&nbsp; Imgsz: ${m.imgsz} &nbsp;|&nbsp; ${m.created}
</div>
</div>
<div style="display:flex; gap:8px; flex-shrink:0;">
${!m.active ? `<button class="btn" style="padding:7px 16px;font-size:12px;" onclick="applyModel('${m.name}')">✅ Terapkan</button>` : ''}
<a href="/download_model/${m.name}">
<button style="background:white;color:#6D60B4;border:1.5px solid #6D60B4;border-radius:9px;padding:7px 16px;font-family:'Poppins',sans-serif;font-size:12px;font-weight:600;cursor:pointer;">⬇ Download</button>
</a>
</div>
`;
container.appendChild(card);
});
} catch(e) {
container.innerHTML = `<div style="font-size:12px;color:#b94040;">❌ Gagal memuat: ${e}</div>`;
}
}
async function applyModel(name) {
if (!confirm(`Terapkan model "${name}"?\nModel ini akan langsung digunakan di Monitoring & Testing.`)) return;
try {
const res = await fetch('/apply_model', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({name}) });
const data = await res.json();
if (data.error) { alert('❌ ' + data.error); return; }
const el = document.getElementById('info5');
el.textContent = '✅ ' + data.message;
el.style.background = '#eceaf6'; el.style.color = '#4a3a9a';
el.classList.add('visible');
loadModels();
} catch(e) { alert('❌ Gagal: ' + e); }
}
loadModels();
</script>
</body>
</html>

View File

@ -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
)

View File

@ -0,0 +1,2 @@
dua ribu
seribu

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View File

@ -0,0 +1 @@
2 0.49123303167420823 0.5090497737556561 0.9349547511312218 0.6696832579185522

View File

@ -0,0 +1 @@
0 0.49971719457013575 0.5622171945701357 0.9451357466063349 0.6493212669683258

View File

@ -0,0 +1 @@
0 0.5580693815987933 0.5022624434389141 0.6696832579185519 0.9411764705882354

View File

@ -0,0 +1 @@
0 0.5233785822021112 0.5011312217194565 0.6847662141779781 0.9570135746606327

View File

@ -0,0 +1 @@
2 0.5173453996983408 0.47171945701357465 0.8355957767722473 0.9434389140271493

View File

@ -0,0 +1 @@
0 0.5005656108597286 0.5169683257918551 0.973981900452489 0.6809954751131221

View File

@ -0,0 +1 @@
2 0.5082013574660634 0.48868778280542985 0.9383484162895929 0.6742081447963801

View File

@ -0,0 +1 @@
2 0.5098039215686275 0.505656108597285 0.6757164404223228 0.9389140271493213

View File

@ -0,0 +1 @@
2 0.5233785822021116 0.4966063348416289 0.6787330316742082 0.9434389140271493

View File

@ -0,0 +1 @@
0 0.4494720965309201 0.4457013574660634 0.7903469079939669 0.8914027149321267

View File

@ -0,0 +1 @@
0 0.4766214177978884 0.5022624434389141 0.6696832579185521 0.9773755656108598

View File

@ -0,0 +1 @@
0 0.46455505279034703 0.47398190045248867 0.81447963800905 0.9479638009049773

View File

@ -0,0 +1 @@
2 0.5113122171945702 0.4615384615384615 0.799396681749623 0.923076923076923

Some files were not shown because too many files have changed in this diff Show More