commit
45f7db89af
|
|
@ -0,0 +1,9 @@
|
||||||
|
# MySQL Database Configuration
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=deteksi_pmk
|
||||||
|
|
||||||
|
# Flask Configuration
|
||||||
|
FLASK_SECRET=deteksi-pmk-secret-key-2026
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# MySQL Database Configuration
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=deteksi_pmk
|
||||||
|
|
||||||
|
# Flask Configuration
|
||||||
|
FLASK_SECRET=your-secret-key-here
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
__pycache__
|
__pycache__
|
||||||
|
.vscode
|
||||||
models/*
|
models/*
|
||||||
results/*
|
results/*
|
||||||
|
dataset/*
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
# MySQL Database Integration - Deteksi PMK
|
||||||
|
|
||||||
|
## ✅ Apa yang Sudah Diimplementasikan
|
||||||
|
|
||||||
|
### 1. **MySQL Database Integration**
|
||||||
|
File: `utils/mysql_db.py`
|
||||||
|
|
||||||
|
Fitur:
|
||||||
|
- Simpan hasil prediksi ke database MySQL
|
||||||
|
- Simpan hasil diagnosis dari expert system
|
||||||
|
- Query data dari database
|
||||||
|
- JSON storage untuk fitur-fitur ekstraksi
|
||||||
|
|
||||||
|
Tabel Database:
|
||||||
|
- `predictions` - Menyimpan semua hasil prediksi dengan fitur-fitur
|
||||||
|
- `diagnosis_history` - Menyimpan hasil diagnosis dari sistem pakar
|
||||||
|
|
||||||
|
### 2. **Environment Configuration**
|
||||||
|
File: `.env`
|
||||||
|
|
||||||
|
Konfigurasi MySQL:
|
||||||
|
```
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=deteksi_pmk
|
||||||
|
```
|
||||||
|
|
||||||
|
Gunakan file `.env.example` sebagai template.
|
||||||
|
|
||||||
|
### 3. **Automatic Database Setup**
|
||||||
|
File: `setup_db.py`
|
||||||
|
|
||||||
|
Menjalankan:
|
||||||
|
```bash
|
||||||
|
python setup_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Fitur:
|
||||||
|
- Membuat database otomatis
|
||||||
|
- Membuat tabel-tabel yang diperlukan
|
||||||
|
- Validasi koneksi
|
||||||
|
|
||||||
|
### 4. **Update Preference: MySQL First**
|
||||||
|
|
||||||
|
Priority penyimpanan data:
|
||||||
|
1. **MySQL Database** (Utama) - Real-time, queryable
|
||||||
|
2. **CSV Files** (Backup) - Fallback jika MySQL error
|
||||||
|
|
||||||
|
Priority load data:
|
||||||
|
1. **MySQL Database** (Cepat, terstruktur)
|
||||||
|
2. **CSV Files** (Fallback)
|
||||||
|
|
||||||
|
### 5. **Detail Riwayat Deteksi Page**
|
||||||
|
File: `templates/detail_deteksi.html`
|
||||||
|
Route: `/detail-deteksi/<id>`
|
||||||
|
|
||||||
|
Fitur:
|
||||||
|
- Lihat hasil lengkap satu prediksi
|
||||||
|
- Tampilkan semua fitur yang dianalisis
|
||||||
|
- Confidence score dengan progress bar
|
||||||
|
- Informasi file dan timestamp
|
||||||
|
- Link navigasi kembali
|
||||||
|
|
||||||
|
### 6. **Updated Riwayat Deteksi Page**
|
||||||
|
File: `templates/riwayat_deteksi.html`
|
||||||
|
|
||||||
|
Updated:
|
||||||
|
- Tombol "Detail" untuk setiap item riwayat
|
||||||
|
- Link ke halaman detail prediksi
|
||||||
|
- Priority load dari database
|
||||||
|
|
||||||
|
## 🚀 Cara Menggunakan
|
||||||
|
|
||||||
|
### Setup Pertama Kali
|
||||||
|
|
||||||
|
1. **Install dependencies** (jika belum):
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Setup database**:
|
||||||
|
```bash
|
||||||
|
python setup_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Jalankan aplikasi**:
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Akses aplikasi**:
|
||||||
|
```
|
||||||
|
http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow Penggunaan
|
||||||
|
|
||||||
|
1. **Upload Gambar**
|
||||||
|
- Ke halaman "Upload Gambar"
|
||||||
|
- Hasil langsung disimpan ke MySQL database
|
||||||
|
|
||||||
|
2. **Lihat Riwayat**
|
||||||
|
- Halaman "Riwayat Deteksi"
|
||||||
|
- Menampilkan 10 deteksi terbaru dari database
|
||||||
|
- Statistik otomatis dari data MySQL
|
||||||
|
|
||||||
|
3. **Detail Riwayat**
|
||||||
|
- Klik "Detail" di setiap item
|
||||||
|
- Lihat hasil lengkap prediksi
|
||||||
|
- Lihat fitur-fitur yang dianalisis
|
||||||
|
|
||||||
|
## 📊 Data Structure
|
||||||
|
|
||||||
|
### Predictions Table
|
||||||
|
```
|
||||||
|
id | original_filename | filename | image_path | prediction | confidence | features (JSON) | timestamp
|
||||||
|
```
|
||||||
|
|
||||||
|
Features JSON example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"area": 12345.67,
|
||||||
|
"perimeter": 456.78,
|
||||||
|
"circularity": 0.8765,
|
||||||
|
"solidity": 0.9123,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diagnosis History Table
|
||||||
|
```
|
||||||
|
id | original_filename | filename | image_path | diagnosis (JSON) | severity | confidence | timestamp
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
Edit `.env` untuk customize:
|
||||||
|
- MySQL host, port, user, password
|
||||||
|
- Database name
|
||||||
|
- Flask secret key
|
||||||
|
|
||||||
|
Default configuration sudah optimal untuk development.
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
Cek koneksi database:
|
||||||
|
```bash
|
||||||
|
python test_env.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Files yang Ditambahkan/Diubah
|
||||||
|
|
||||||
|
### Ditambahkan:
|
||||||
|
- `utils/mysql_db.py` - MySQL operations
|
||||||
|
- `setup_db.py` - Database initialization
|
||||||
|
- `test_env.py` - Environment test
|
||||||
|
- `.env` - Environment variables
|
||||||
|
- `.env.example` - Environment template
|
||||||
|
- `DATABASE_SETUP.md` - Setup documentation
|
||||||
|
- `templates/detail_deteksi.html` - Detail page
|
||||||
|
|
||||||
|
### Diubah:
|
||||||
|
- `app.py` - Add dotenv loading, update MySQL imports, add detail route
|
||||||
|
- `requirements.txt` - Add python-dotenv
|
||||||
|
- `templates/riwayat_deteksi.html` - Update detail links
|
||||||
|
|
||||||
|
## ⚠️ Troubleshooting
|
||||||
|
|
||||||
|
### Database connection failed?
|
||||||
|
1. Pastikan MySQL server running
|
||||||
|
2. Cek `.env` configuration
|
||||||
|
3. Jalankan `python setup_db.py` lagi
|
||||||
|
|
||||||
|
### MYSQL_AVAILABLE = False?
|
||||||
|
Fallback ke CSV storage. Periksa terminal output untuk error detail.
|
||||||
|
|
||||||
|
### Data tidak ada di database?
|
||||||
|
- Pastikan database sudah di-setup dengan `setup_db.py`
|
||||||
|
- Cek MySQL server status
|
||||||
|
- Refresh halaman aplikasi
|
||||||
|
|
||||||
|
## 🎯 Next Steps (Optional)
|
||||||
|
|
||||||
|
1. **Backup automation** - Backup database secara berkala
|
||||||
|
2. **Export data** - Export hasil riwayat ke Excel/PDF
|
||||||
|
3. **Advanced analytics** - Dashboard dengan chart dan statistik
|
||||||
|
4. **User authentication** - Login system untuk multi-user
|
||||||
|
5. **API endpoints** - REST API untuk integrasi dengan sistem lain
|
||||||
|
|
||||||
|
## 📖 Documentation
|
||||||
|
|
||||||
|
- `DATABASE_SETUP.md` - Panduan setup database lengkap
|
||||||
|
- `app.py` - Kode aplikasi dengan comments
|
||||||
|
- `utils/mysql_db.py` - Dokumentasi function database
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Aplikasi siap! MySQL database sudah integrated dengan priority utama. Data akan otomatis tersimpan ke database saat upload gambar.**
|
||||||
|
|
||||||
|
Untuk pertanyaan lebih lanjut, cek dokumentasi atau jalankan `python setup_db.py` untuk re-initialize database.
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
# Database Setup untuk Deteksi PMK
|
||||||
|
|
||||||
|
## Persyaratan
|
||||||
|
- MySQL Server berjalan (versi 5.7 atau lebih tinggi)
|
||||||
|
- Python 3.12.5 dengan semua dependencies terinstall
|
||||||
|
|
||||||
|
## Langkah Setup
|
||||||
|
|
||||||
|
### 1. Install MySQL Server (Jika belum)
|
||||||
|
|
||||||
|
#### Windows:
|
||||||
|
- Download dari https://dev.mysql.com/downloads/mysql/
|
||||||
|
- Ikuti installer wizard
|
||||||
|
- Default Port: 3306
|
||||||
|
- Default User: root
|
||||||
|
|
||||||
|
#### Linux (Ubuntu/Debian):
|
||||||
|
```bash
|
||||||
|
sudo apt-get install mysql-server
|
||||||
|
```
|
||||||
|
|
||||||
|
#### macOS:
|
||||||
|
```bash
|
||||||
|
brew install mysql
|
||||||
|
brew services start mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Environment Variables
|
||||||
|
|
||||||
|
Copy `.env.example` ke `.env` dan update konfigurasi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` dengan kredensial MySQL Anda:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# MySQL Database Configuration
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=your_password_here
|
||||||
|
DB_NAME=deteksi_pmk
|
||||||
|
|
||||||
|
# Flask Configuration
|
||||||
|
FLASK_SECRET=your-secret-key-here
|
||||||
|
```
|
||||||
|
|
||||||
|
**Penting:**
|
||||||
|
- `DB_HOST`: Alamat server MySQL (default: localhost)
|
||||||
|
- `DB_USER`: Username MySQL (default: root)
|
||||||
|
- `DB_PASSWORD`: Password MySQL (kosongkan jika tidak ada password)
|
||||||
|
- `DB_NAME`: Nama database yang akan dibuat
|
||||||
|
|
||||||
|
### 3. Setup Database
|
||||||
|
|
||||||
|
Jalankan script setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python setup_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Script ini akan:
|
||||||
|
- Membuat database `deteksi_pmk`
|
||||||
|
- Membuat tabel-tabel yang diperlukan:
|
||||||
|
- `predictions` - Menyimpan hasil prediksi
|
||||||
|
- `diagnosis_history` - Menyimpan hasil diagnosis
|
||||||
|
|
||||||
|
### 4. Verify Database Connection
|
||||||
|
|
||||||
|
Anda bisa test koneksi dengan menjalankan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "from utils.mysql_db import get_engine; engine = get_engine(); print('✓ Database connected successfully!')"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Jalankan Aplikasi
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Aplikasi akan berjalan di: http://localhost:5000
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### Tabel: predictions
|
||||||
|
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| id | INT PRIMARY KEY AUTO_INCREMENT | ID unik prediksi |
|
||||||
|
| original_filename | VARCHAR(255) | Nama file asli yang diupload |
|
||||||
|
| filename | VARCHAR(255) | Nama file yang disimpan |
|
||||||
|
| image_path | VARCHAR(500) | Path lengkap file |
|
||||||
|
| prediction | VARCHAR(50) | Hasil prediksi ('sehat' atau 'sakit') |
|
||||||
|
| confidence | FLOAT | Tingkat kepercayaan (0-100) |
|
||||||
|
| features | TEXT | JSON string dari fitur-fitur |
|
||||||
|
| timestamp | DATETIME | Waktu prediksi |
|
||||||
|
|
||||||
|
### Tabel: diagnosis_history
|
||||||
|
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| id | INT PRIMARY KEY AUTO_INCREMENT | ID unik diagnosis |
|
||||||
|
| original_filename | VARCHAR(255) | Nama file asli yang diupload |
|
||||||
|
| filename | VARCHAR(255) | Nama file yang disimpan |
|
||||||
|
| image_path | VARCHAR(500) | Path lengkap file |
|
||||||
|
| diagnosis | TEXT | JSON string dari diagnosis details |
|
||||||
|
| severity | VARCHAR(50) | Tingkat keparahan ('ringan', 'sedang', 'berat') |
|
||||||
|
| confidence | FLOAT | Tingkat kepercayaan (0-100) |
|
||||||
|
| timestamp | DATETIME | Waktu diagnosis |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "MySQL not available at startup"
|
||||||
|
- Pastikan MySQL Server berjalan
|
||||||
|
- Periksa konfigurasi di file `.env`
|
||||||
|
- Pastikan password MySQL benar
|
||||||
|
|
||||||
|
### "Cannot import 'setuptools.build_meta'"
|
||||||
|
- Jalankan: `pip install --upgrade setuptools`
|
||||||
|
|
||||||
|
### "NumPy 2.x incompatibility"
|
||||||
|
- Sudah fixed dengan numpy<2 di requirements.txt
|
||||||
|
- Jalankan: `pip install "numpy<2"`
|
||||||
|
|
||||||
|
### Database tidak tersimpan di MySQL
|
||||||
|
- Periksa apakah MYSQL_AVAILABLE = True di terminal saat startup
|
||||||
|
- Cek file `.env` konfigurasi
|
||||||
|
- Jalankan `python setup_db.py` lagi untuk pastikan database terbuat
|
||||||
|
|
||||||
|
## Features yang Tersedia
|
||||||
|
|
||||||
|
### 1. Prediksi dengan Machine Learning
|
||||||
|
- Upload gambar (JPG, PNG, BMP)
|
||||||
|
- Deteksi PMK otomatis
|
||||||
|
- Confidence score tinggi
|
||||||
|
|
||||||
|
### 2. Riwayat Deteksi
|
||||||
|
- Lihat semua hasil deteksi yang tersimpan di database
|
||||||
|
- Filter berdasarkan hasil (Sehat/Sakit)
|
||||||
|
- Statistik real-time
|
||||||
|
|
||||||
|
### 3. Detail Riwayat
|
||||||
|
- Klik "Detail" di riwayat untuk melihat:
|
||||||
|
- Hasil lengkap prediksi
|
||||||
|
- Confidence score
|
||||||
|
- Fitur-fitur yang dianalisis
|
||||||
|
- Informasi file
|
||||||
|
|
||||||
|
### 4. Diagnosis Expertise System
|
||||||
|
- Berbasis rule-based system
|
||||||
|
- Memberikan diagnosis detail berdasarkan gejala
|
||||||
|
- Simpan hasil diagnosis ke database
|
||||||
|
|
||||||
|
## Data Storage Priority
|
||||||
|
|
||||||
|
1. **MySQL Database** (Utama)
|
||||||
|
- Real-time
|
||||||
|
- Queryable
|
||||||
|
- Backup terstruktur
|
||||||
|
|
||||||
|
2. **CSV Files** (Backup)
|
||||||
|
- Fallback jika MySQL error
|
||||||
|
- Di folder `results/`
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- Pastikan MySQL Server running sebelum startup aplikasi
|
||||||
|
- Reguler backup database MySQL Anda
|
||||||
|
- Gunakan password yang kuat untuk MySQL di production
|
||||||
|
- Ubah FLASK_SECRET di `.env` untuk production
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Jika ada error, cek:
|
||||||
|
1. `setup_db.py` untuk initialize database
|
||||||
|
2. File `.env` konfigurasi
|
||||||
|
3. MySQL Server status
|
||||||
|
4. Requirements sudah terinstall dengan benar
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
# 🚀 QUICK START - Database Integration
|
||||||
|
|
||||||
|
## Langkah 1: Setup Database (First Time Only)
|
||||||
|
|
||||||
|
Jalankan script setup database:
|
||||||
|
```bash
|
||||||
|
python setup_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Output yang diharapkan:
|
||||||
|
```
|
||||||
|
✓ Database 'deteksi_pmk' created/verified
|
||||||
|
✓ Database tables created/verified
|
||||||
|
✓ Database setup completed successfully!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Langkah 2: Jalankan Aplikasi
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Buka browser ke: **http://localhost:5000**
|
||||||
|
|
||||||
|
## Langkah 3: Upload Gambar & Lihat Riwayat
|
||||||
|
|
||||||
|
1. **Upload**: Klik "Upload Gambar" → Pilih file → Upload
|
||||||
|
2. **Hasil disimpan otomatis ke database MySQL**
|
||||||
|
3. **Lihat Riwayat**: Klik "Riwayat Deteksi"
|
||||||
|
4. **Detail**: Klik tombol "Detail" untuk melihat hasil lengkap
|
||||||
|
|
||||||
|
## 📋 Fitur Baru
|
||||||
|
|
||||||
|
✅ **MySQL Database** - Penyimpanan data terstruktur
|
||||||
|
✅ **Detail Riwayat** - Lihat hasil lengkap setiap deteksi
|
||||||
|
✅ **Feature Display** - Lihat fitur-fitur yang dianalisis
|
||||||
|
✅ **Automatic Backup** - CSV sebagai fallback
|
||||||
|
✅ **Real-time Stats** - Statistik dari database
|
||||||
|
|
||||||
|
## ⚙️ Konfigurasi
|
||||||
|
|
||||||
|
Edit `.env` jika ingin ubah MySQL settings:
|
||||||
|
```
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=deteksi_pmk
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔗 Useful Routes
|
||||||
|
|
||||||
|
| URL | Deskripsi |
|
||||||
|
|-----|-----------|
|
||||||
|
| `/` | Halaman Beranda |
|
||||||
|
| `/upload` | Upload Gambar |
|
||||||
|
| `/riwayat_deteksi` | Riwayat Deteksi (List) |
|
||||||
|
| `/detail-deteksi/1` | Detail Riwayat (ID=1) |
|
||||||
|
| `/expert-system` | Sistem Pakar |
|
||||||
|
|
||||||
|
## 🆘 Jika Ada Error
|
||||||
|
|
||||||
|
**Error: "MySQL not available"**
|
||||||
|
- Jalankan: `python setup_db.py`
|
||||||
|
- Pastikan MySQL server running
|
||||||
|
|
||||||
|
**Error: "Cannot connect to database"**
|
||||||
|
- Cek `.env` configuration
|
||||||
|
- Pastikan DB_HOST, DB_USER, DB_password benar
|
||||||
|
|
||||||
|
**Error: "Unknown database"**
|
||||||
|
- Jalankan: `python setup_db.py`
|
||||||
|
|
||||||
|
**Memory load error?**
|
||||||
|
- Restart Python: `python app.py`
|
||||||
|
|
||||||
|
## 📁 File Penting
|
||||||
|
|
||||||
|
- `app.py` - Main application
|
||||||
|
- `.env` - Database configuration
|
||||||
|
- `utils/mysql_db.py` - Database operations
|
||||||
|
- `setup_db.py` - Database setup script
|
||||||
|
- `templates/detail_deteksi.html` - Detail page
|
||||||
|
|
||||||
|
## ✨ Done!
|
||||||
|
|
||||||
|
Database integration sudah selesai. Aplikasi siap digunakan dengan penyimpanan data MySQL yang aman dan terstruktur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Untuk dokumentasi lengkap, lihat:
|
||||||
|
- `DATABASE_SETUP.md` - Setup guide lengkap
|
||||||
|
- `DATABASE_INTEGRATION_SUMMARY.md` - Summary fitur baru
|
||||||
412
app.py
412
app.py
|
|
@ -6,11 +6,15 @@ from werkzeug.utils import secure_filename
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
from utils.helpers import load_model
|
from utils.helpers import load_model
|
||||||
from utils.preprocessing import preprocess_image
|
from utils.preprocessing import preprocess_image
|
||||||
from utils.feature_extraction import FeatureExtractor
|
from utils.feature_extraction import FeatureExtractor
|
||||||
from expert_system import ForwardChaining # Import sistem pakar
|
from expert_system import ForwardChaining, KnowledgeBase # Import sistem pakar
|
||||||
|
|
||||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'bmp'}
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'bmp'}
|
||||||
|
|
||||||
|
|
@ -24,17 +28,27 @@ app.secret_key = os.environ.get('FLASK_SECRET', 'change-me')
|
||||||
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
|
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
|
||||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
# Inisialisasi sistem pakar
|
# Inisialisasi sistem pakar dan knowledge base
|
||||||
expert_system = ForwardChaining()
|
expert_system = ForwardChaining()
|
||||||
|
kb = KnowledgeBase()
|
||||||
|
|
||||||
|
# Jinja filter untuk mendapatkan deskripsi gejala dari kode
|
||||||
|
@app.template_filter('get_symptom_desc')
|
||||||
|
def get_symptom_desc(symptom_code):
|
||||||
|
"""Convert symptom code to description"""
|
||||||
|
return kb.gejala.get(symptom_code, symptom_code)
|
||||||
|
|
||||||
# Attempt MySQL integration (optional). If env var not set, fall back to CSV storage.
|
# Attempt MySQL integration (optional). If env var not set, fall back to CSV storage.
|
||||||
try:
|
try:
|
||||||
from utils.mysql_db import (
|
from utils.mysql_db import (
|
||||||
save_prediction_mysql,
|
save_prediction_mysql,
|
||||||
get_recent_predictions_mysql,
|
get_recent_predictions_mysql,
|
||||||
|
get_prediction_by_id,
|
||||||
init_mysql_tables,
|
init_mysql_tables,
|
||||||
save_diagnosis_mysql,
|
save_diagnosis_mysql,
|
||||||
get_diagnosis_history_mysql,
|
get_diagnosis_history_mysql,
|
||||||
|
get_diagnosis_by_id,
|
||||||
|
get_diagnosis_by_prediction_id,
|
||||||
get_engine,
|
get_engine,
|
||||||
)
|
)
|
||||||
# Test DB connection now; only enable MySQL features if connect succeeds
|
# Test DB connection now; only enable MySQL features if connect succeeds
|
||||||
|
|
@ -94,26 +108,15 @@ def index():
|
||||||
except Exception:
|
except Exception:
|
||||||
model_info = None
|
model_info = None
|
||||||
|
|
||||||
# Prefer CSV (written synchronously on predict) so recent detection appears immediately;
|
# Get recent predictions from MySQL database
|
||||||
# fallback to MySQL only if CSV not present.
|
|
||||||
history = []
|
history = []
|
||||||
csv_path = os.path.join(BASE_DIR, 'results', 'predictions.csv')
|
if MYSQL_AVAILABLE:
|
||||||
if os.path.exists(csv_path):
|
|
||||||
try:
|
|
||||||
df = pd.read_csv(csv_path)
|
|
||||||
if not df.empty:
|
|
||||||
df_recent = df.sort_values('timestamp', ascending=False).head(5)
|
|
||||||
history = df_recent.to_dict(orient='records')
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error reading history for index: {e}")
|
|
||||||
|
|
||||||
if not history and MYSQL_AVAILABLE:
|
|
||||||
try:
|
try:
|
||||||
rows = get_recent_predictions_mysql(limit=5)
|
rows = get_recent_predictions_mysql(limit=5)
|
||||||
if rows:
|
if rows:
|
||||||
history = rows
|
history = rows
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"MySQL read failed for index: {e}")
|
print(f"✗ Error reading recent predictions from MySQL: {e}")
|
||||||
|
|
||||||
return render_template('index.html',
|
return render_template('index.html',
|
||||||
model_loaded=bool(model_loaded),
|
model_loaded=bool(model_loaded),
|
||||||
|
|
@ -128,155 +131,55 @@ def uploaded_file(filename):
|
||||||
|
|
||||||
@app.route('/dashboard')
|
@app.route('/dashboard')
|
||||||
@app.route('/riwayat-deteksi')
|
@app.route('/riwayat-deteksi')
|
||||||
|
@app.route('/riwayat_deteksi')
|
||||||
def riwayat_deteksi():
|
def riwayat_deteksi():
|
||||||
recent = []
|
recent = []
|
||||||
stats = {'total': 0, 'positif': 0, 'negatif': 0}
|
stats = {'total': 0, 'positif': 0, 'negatif': 0}
|
||||||
|
|
||||||
# Prefer CSV so new predictions are visible immediately; fallback to MySQL if CSV absent
|
# Get data from MySQL database only
|
||||||
csv_path = os.path.join(BASE_DIR, 'results', 'predictions.csv')
|
|
||||||
if os.path.exists(csv_path):
|
|
||||||
try:
|
|
||||||
# Robust CSV parsing: handle rows with extra/missing columns (old format variations)
|
|
||||||
import csv as _csv
|
|
||||||
from collections import Counter
|
|
||||||
|
|
||||||
def _robust_read(path):
|
|
||||||
with open(path, newline='', encoding='utf-8') as fh:
|
|
||||||
reader = _csv.reader(fh)
|
|
||||||
rows = [r for r in reader]
|
|
||||||
if not rows:
|
|
||||||
return pd.DataFrame()
|
|
||||||
header = rows[0]
|
|
||||||
data_rows = rows[1:]
|
|
||||||
if not data_rows:
|
|
||||||
return pd.DataFrame(columns=header)
|
|
||||||
lengths = [len(r) for r in data_rows]
|
|
||||||
cnt = Counter(lengths)
|
|
||||||
most_common_len, _ = cnt.most_common(1)[0]
|
|
||||||
|
|
||||||
# If header matches most common row length, use it
|
|
||||||
if len(header) == most_common_len:
|
|
||||||
return pd.DataFrame(data_rows, columns=header)
|
|
||||||
|
|
||||||
# If rows commonly have one extra column, assume missing 'filename' header at index 1
|
|
||||||
if most_common_len == len(header) + 1:
|
|
||||||
new_header = header.copy()
|
|
||||||
new_header.insert(1, 'filename')
|
|
||||||
norm_rows = []
|
|
||||||
for r in data_rows:
|
|
||||||
if len(r) == len(header):
|
|
||||||
r2 = r.copy()
|
|
||||||
r2.insert(1, '')
|
|
||||||
norm_rows.append(r2)
|
|
||||||
elif len(r) >= len(new_header):
|
|
||||||
norm_rows.append(r[:len(new_header)])
|
|
||||||
else:
|
|
||||||
r2 = r + [''] * (len(new_header) - len(r))
|
|
||||||
norm_rows.append(r2)
|
|
||||||
return pd.DataFrame(norm_rows, columns=new_header)
|
|
||||||
|
|
||||||
# Fallback to pandas (will create unnamed columns for extras)
|
|
||||||
return pd.read_csv(path, low_memory=False)
|
|
||||||
|
|
||||||
df = _robust_read(csv_path)
|
|
||||||
|
|
||||||
# Normalize/match columns even if CSV format changed over time
|
|
||||||
# 1) Find timestamp-like column
|
|
||||||
timestamp_col = None
|
|
||||||
best_ts_count = 0
|
|
||||||
for col in df.columns:
|
|
||||||
try:
|
|
||||||
parsed = pd.to_datetime(df[col], errors='coerce')
|
|
||||||
cnt = parsed.notna().sum()
|
|
||||||
if cnt > best_ts_count:
|
|
||||||
best_ts_count = cnt
|
|
||||||
timestamp_col = col
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if timestamp_col is not None and best_ts_count > 0:
|
|
||||||
df['timestamp'] = pd.to_datetime(df[timestamp_col], errors='coerce')
|
|
||||||
else:
|
|
||||||
df['timestamp'] = pd.NaT
|
|
||||||
|
|
||||||
# 2) Find prediction column by matching common labels
|
|
||||||
prediction_col = None
|
|
||||||
best_pred_count = 0
|
|
||||||
for col in df.columns:
|
|
||||||
try:
|
|
||||||
vals = df[col].astype(str).str.lower()
|
|
||||||
cnt = vals.isin(['sakit', 'sehat']).sum()
|
|
||||||
if cnt > best_pred_count:
|
|
||||||
best_pred_count = cnt
|
|
||||||
prediction_col = col
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if prediction_col:
|
|
||||||
df['prediction'] = df[prediction_col].astype(str).str.lower()
|
|
||||||
|
|
||||||
# 3) Ensure filename and image_path exist
|
|
||||||
if 'filename' not in df.columns:
|
|
||||||
# Heuristic: second column often contains filename when present
|
|
||||||
if len(df.columns) >= 2:
|
|
||||||
possible = df.columns[1]
|
|
||||||
# if many values look like a uuid or end with image ext, use it
|
|
||||||
vals = df[possible].astype(str)
|
|
||||||
score = vals.str.contains(r"\.(jpg|jpeg|png|bmp)$", case=False, regex=True).sum()
|
|
||||||
if score > 0:
|
|
||||||
df['filename'] = df[possible]
|
|
||||||
else:
|
|
||||||
df['filename'] = ''
|
|
||||||
else:
|
|
||||||
df['filename'] = ''
|
|
||||||
|
|
||||||
if 'image_path' not in df.columns:
|
|
||||||
# find a column containing file paths starting with drive letter or slash
|
|
||||||
img_col = None
|
|
||||||
for col in df.columns:
|
|
||||||
vals = df[col].astype(str)
|
|
||||||
if vals.str.contains(r'^[A-Za-z]:\\|^/|\\').any():
|
|
||||||
img_col = col
|
|
||||||
break
|
|
||||||
df['image_path'] = df[img_col] if img_col else ''
|
|
||||||
|
|
||||||
# Ensure confidence is numeric so templates can round it safely
|
|
||||||
if 'confidence' in df.columns:
|
|
||||||
try:
|
|
||||||
df['confidence'] = pd.to_numeric(df['confidence'], errors='coerce')
|
|
||||||
except Exception:
|
|
||||||
df['confidence'] = pd.NA
|
|
||||||
|
|
||||||
# Compute stats safely
|
|
||||||
stats['total'] = len(df)
|
|
||||||
stats['positif'] = int((df.get('prediction', '') == 'sakit').sum()) if 'prediction' in df else 0
|
|
||||||
stats['negatif'] = int((df.get('prediction', '') == 'sehat').sum()) if 'prediction' in df else 0
|
|
||||||
|
|
||||||
# Sort by parsed timestamp (NaT go to end)
|
|
||||||
if 'timestamp' in df.columns:
|
|
||||||
df_recent = df.sort_values('timestamp', ascending=False, na_position='last').head(10)
|
|
||||||
else:
|
|
||||||
df_recent = df.tail(10)
|
|
||||||
|
|
||||||
recent = df_recent.to_dict(orient='records')
|
|
||||||
return render_template('riwayat_deteksi.html', recent=recent, stats=stats, data_source='csv')
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error reading CSV for dashboard: {e}")
|
|
||||||
|
|
||||||
# CSV not available or failed; try MySQL
|
|
||||||
if MYSQL_AVAILABLE:
|
if MYSQL_AVAILABLE:
|
||||||
try:
|
try:
|
||||||
rows = get_recent_predictions_mysql(limit=10)
|
rows = get_recent_predictions_mysql(limit=10)
|
||||||
if rows:
|
# Query successful - return dengan data (bisa kosong)
|
||||||
recent = rows
|
recent = rows if rows else []
|
||||||
stats['total'] = len(rows)
|
stats['total'] = len(recent)
|
||||||
stats['positif'] = int(sum(1 for r in rows if (str(r.get('prediction') or '').lower() == 'sakit')))
|
stats['positif'] = int(sum(1 for r in recent if (str(r.get('prediction') or '').lower() == 'sakit')))
|
||||||
stats['negatif'] = int(sum(1 for r in rows if (str(r.get('prediction') or '').lower() == 'sehat')))
|
stats['negatif'] = int(sum(1 for r in recent if (str(r.get('prediction') or '').lower() == 'sehat')))
|
||||||
return render_template('riwayat_deteksi.html', recent=recent, stats=stats, data_source='mysql')
|
return render_template('riwayat_deteksi.html', recent=recent, stats=stats, data_source='mysql')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"MySQL read failed for dashboard: {e}")
|
print(f"✗ Error reading from MySQL: {e}")
|
||||||
|
flash('Error memuat riwayat deteksi dari database', 'danger')
|
||||||
|
return render_template('riwayat_deteksi.html', recent=recent, stats=stats, data_source='error')
|
||||||
|
else:
|
||||||
|
flash('Database tidak tersedia. Silakan setup database terlebih dahulu.', 'warning')
|
||||||
|
return render_template('riwayat_deteksi.html', recent=recent, stats=stats, data_source='database_error')
|
||||||
|
|
||||||
return render_template('riwayat_deteksi.html', recent=recent, stats=stats, data_source='none')
|
|
||||||
|
@app.route('/detail-deteksi/<int:pred_id>')
|
||||||
|
def detail_deteksi(pred_id):
|
||||||
|
"""Halaman detail riwayat deteksi"""
|
||||||
|
prediction = None
|
||||||
|
diagnosis = None
|
||||||
|
|
||||||
|
# Get from MySQL database only
|
||||||
|
if MYSQL_AVAILABLE:
|
||||||
|
try:
|
||||||
|
prediction = get_prediction_by_id(pred_id)
|
||||||
|
if prediction:
|
||||||
|
# Also get related diagnosis if exists
|
||||||
|
diagnosis = get_diagnosis_by_prediction_id(pred_id)
|
||||||
|
return render_template('detail_deteksi.html',
|
||||||
|
prediction=prediction,
|
||||||
|
diagnosis=diagnosis,
|
||||||
|
data_source='mysql')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error getting prediction from MySQL: {e}")
|
||||||
|
else:
|
||||||
|
flash('Database tidak tersedia. Silakan setup database terlebih dahulu.', 'danger')
|
||||||
|
|
||||||
|
# Not found or database error
|
||||||
|
flash(f"Prediksi dengan ID {pred_id} tidak ditemukan", 'danger')
|
||||||
|
return redirect(url_for('riwayat_deteksi'))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/upload')
|
@app.route('/upload')
|
||||||
|
|
@ -318,7 +221,27 @@ def predict():
|
||||||
probabilities = model.predict_proba(features_scaled)[0]
|
probabilities = model.predict_proba(features_scaled)[0]
|
||||||
confidence = float(max(probabilities) * 100)
|
confidence = float(max(probabilities) * 100)
|
||||||
|
|
||||||
# Save to CSV (ensure consistent columns, migrate old files if needed)
|
# Prepare feature dictionary for database
|
||||||
|
features_dict = {name: float(features[i]) for i, name in enumerate(extractor.feature_names)}
|
||||||
|
|
||||||
|
# Try to save to MySQL first (primary storage)
|
||||||
|
rowid = None
|
||||||
|
if MYSQL_AVAILABLE:
|
||||||
|
try:
|
||||||
|
rowid = save_prediction_mysql(original_filename, filename, filepath, prediction, confidence, features_dict)
|
||||||
|
print(f"✓ Saved prediction to MySQL, id={rowid}")
|
||||||
|
# store DB id in session so result page can link to the new history row
|
||||||
|
try:
|
||||||
|
session.setdefault('last_prediction', {})
|
||||||
|
session['last_prediction']['db_id'] = int(rowid) if rowid is not None else None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(f"✗ Gagal menyimpan ke MySQL: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# Also save to CSV as fallback/backup (ensure consistent columns, migrate old files if needed)
|
||||||
try:
|
try:
|
||||||
os.makedirs(os.path.join(BASE_DIR, 'results'), exist_ok=True)
|
os.makedirs(os.path.join(BASE_DIR, 'results'), exist_ok=True)
|
||||||
data = {
|
data = {
|
||||||
|
|
@ -361,25 +284,7 @@ def predict():
|
||||||
else:
|
else:
|
||||||
df.to_csv(csv_path, index=False)
|
df.to_csv(csv_path, index=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Gagal menyimpan prediksi: {e}")
|
print(f"Gagal menyimpan prediksi ke CSV: {e}")
|
||||||
|
|
||||||
# Also attempt to save to MySQL when available (non-fatal)
|
|
||||||
if MYSQL_AVAILABLE:
|
|
||||||
try:
|
|
||||||
features_dict = {name: float(features[i]) for i, name in enumerate(extractor.feature_names)}
|
|
||||||
# save_prediction_mysql(original_filename, filename, image_path, prediction, confidence, features, timestamp=None)
|
|
||||||
rowid = save_prediction_mysql(original_filename, filename, filepath, prediction, confidence, features_dict)
|
|
||||||
print(f"Saved prediction to MySQL, id={rowid}")
|
|
||||||
# store DB id in session so result page can link to the new history row
|
|
||||||
try:
|
|
||||||
session.setdefault('last_prediction', {})
|
|
||||||
session['last_prediction']['db_id'] = int(rowid) if rowid is not None else None
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
import traceback
|
|
||||||
print(f"Gagal menyimpan ke MySQL: {e}")
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
# Compact features table for session (round values to reduce size)
|
# Compact features table for session (round values to reduce size)
|
||||||
features_table = list(zip(extractor.feature_names, [round(float(x), 4) for x in features]))
|
features_table = list(zip(extractor.feature_names, [round(float(x), 4) for x in features]))
|
||||||
|
|
@ -520,6 +425,55 @@ def expert_system_page():
|
||||||
# Dapatkan diagnosis
|
# Dapatkan diagnosis
|
||||||
diagnosis = expert_system.get_diagnosis()
|
diagnosis = expert_system.get_diagnosis()
|
||||||
|
|
||||||
|
# Get prediction_id dari session untuk Foreign Key
|
||||||
|
prediction_id = session.get('last_prediction', {}).get('db_id')
|
||||||
|
|
||||||
|
# Save diagnosis ke database dengan FK ke predictions
|
||||||
|
if MYSQL_AVAILABLE and prediction_id:
|
||||||
|
try:
|
||||||
|
if diagnosis.get('status') == 'terdiagnosis' and diagnosis.get('diagnosis'):
|
||||||
|
diag_list = diagnosis['diagnosis']
|
||||||
|
main_diag = diag_list[0]
|
||||||
|
|
||||||
|
# Determine severity from CF
|
||||||
|
cf = main_diag.get('cf', 0)
|
||||||
|
if cf >= 70:
|
||||||
|
severity = 'berat'
|
||||||
|
elif cf >= 50:
|
||||||
|
severity = 'sedang'
|
||||||
|
else:
|
||||||
|
severity = 'ringan'
|
||||||
|
|
||||||
|
# Prepare diagnosis details
|
||||||
|
diagnosis_details = {
|
||||||
|
'nama': main_diag.get('nama', ''),
|
||||||
|
'deskripsi': main_diag.get('deskripsi', ''),
|
||||||
|
'solusi': main_diag.get('solusi', []),
|
||||||
|
'cf': float(cf),
|
||||||
|
'gejala_teramati': gejala_terpilih,
|
||||||
|
'semua_diagnosis': [
|
||||||
|
{
|
||||||
|
'nama': d.get('nama', ''),
|
||||||
|
'cf': float(d.get('cf', 0))
|
||||||
|
}
|
||||||
|
for d in diag_list
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save diagnosis dengan FK ke predictions
|
||||||
|
diag_id = save_diagnosis_mysql(
|
||||||
|
prediction_id=prediction_id,
|
||||||
|
diagnosis_dict=diagnosis_details,
|
||||||
|
severity=severity,
|
||||||
|
confidence=float(cf),
|
||||||
|
timestamp=datetime.datetime.utcnow()
|
||||||
|
)
|
||||||
|
print(f"✓ Diagnosis saved to database, id={diag_id}, linked to prediction_id={prediction_id}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error saving diagnosis to MySQL: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
# Jika ada hasil prediksi sebelumnya, tambahkan ke konteks
|
# Jika ada hasil prediksi sebelumnya, tambahkan ke konteks
|
||||||
image_info = None
|
image_info = None
|
||||||
if 'last_prediction' in session:
|
if 'last_prediction' in session:
|
||||||
|
|
@ -529,50 +483,11 @@ def expert_system_page():
|
||||||
'confidence': session['last_prediction']['confidence']
|
'confidence': session['last_prediction']['confidence']
|
||||||
}
|
}
|
||||||
|
|
||||||
# Persist diagnosis to CSV and MySQL (if available)
|
|
||||||
try:
|
|
||||||
os.makedirs(os.path.join(BASE_DIR, 'results'), exist_ok=True)
|
|
||||||
# Normalize fields for storage
|
|
||||||
if diagnosis.get('status') == 'terdiagnosis' and diagnosis.get('diagnosis'):
|
|
||||||
top = diagnosis['diagnosis'][0]
|
|
||||||
diag_text = top.get('nama')
|
|
||||||
confidence = float(top.get('cf', 0.0))
|
|
||||||
rekom = top.get('solusi', [])
|
|
||||||
else:
|
|
||||||
diag_text = diagnosis.get('message', str(diagnosis))
|
|
||||||
confidence = 0.0
|
|
||||||
rekom = []
|
|
||||||
|
|
||||||
gejala_str = ','.join(diagnosis.get('gejala_teramati', []))
|
|
||||||
|
|
||||||
row = {
|
|
||||||
'timestamp': pd.Timestamp.now(),
|
|
||||||
'gejala': gejala_str,
|
|
||||||
'diagnosis': diag_text,
|
|
||||||
'confidence': confidence,
|
|
||||||
'rekomendasi': '|'.join(rekom)
|
|
||||||
}
|
|
||||||
csv_path = os.path.join(BASE_DIR, 'results', 'diagnosis_history.csv')
|
|
||||||
df_row = pd.DataFrame([row])
|
|
||||||
if os.path.exists(csv_path):
|
|
||||||
df_row.to_csv(csv_path, mode='a', header=False, index=False)
|
|
||||||
else:
|
|
||||||
df_row.to_csv(csv_path, index=False)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Gagal menyimpan diagnosis ke CSV: {e}")
|
|
||||||
|
|
||||||
if MYSQL_AVAILABLE:
|
|
||||||
try:
|
|
||||||
save_diagnosis_mysql(diagnosis.get('gejala_teramati', []), diag_text, confidence=confidence, rekomendasi=rekom, related_prediction_id=None)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Gagal menyimpan diagnosis ke MySQL: {e}")
|
|
||||||
|
|
||||||
return render_template('expert_system.html',
|
return render_template('expert_system.html',
|
||||||
gejala_list=expert_system.get_gejala_list(),
|
gejala_list=expert_system.get_gejala_list(),
|
||||||
diagnosis=diagnosis,
|
diagnosis=diagnosis,
|
||||||
selected_gejala=gejala_terpilih,
|
selected_gejala=gejala_terpilih,
|
||||||
image_info=image_info,
|
image_info=image_info)
|
||||||
data_source=('mysql' if MYSQL_AVAILABLE else 'csv'))
|
|
||||||
|
|
||||||
# GET request - tampilkan form
|
# GET request - tampilkan form
|
||||||
# Ambil informasi gambar dari session jika ada
|
# Ambil informasi gambar dari session jika ada
|
||||||
|
|
@ -598,14 +513,63 @@ def expert_system_from_prediction():
|
||||||
|
|
||||||
@app.route('/api/diagnosis', methods=['POST'])
|
@app.route('/api/diagnosis', methods=['POST'])
|
||||||
def api_diagnosis():
|
def api_diagnosis():
|
||||||
"""API endpoint untuk diagnosis"""
|
"""API endpoint untuk diagnosis - Process dan Save ke database"""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
gejala = data.get('gejala', [])
|
gejala = data.get('gejala', [])
|
||||||
|
prediction_id = data.get('prediction_id', None) # FK dari predictions table
|
||||||
|
|
||||||
expert_system.reset()
|
expert_system.reset()
|
||||||
expert_system.tambah_gejala(gejala)
|
expert_system.tambah_gejala(gejala)
|
||||||
diagnosis = expert_system.get_diagnosis()
|
diagnosis = expert_system.get_diagnosis()
|
||||||
|
|
||||||
|
# Save diagnosis ke database jika MYSQL_AVAILABLE dan ada prediction_id
|
||||||
|
if MYSQL_AVAILABLE and prediction_id:
|
||||||
|
try:
|
||||||
|
# Prepare diagnosis data
|
||||||
|
if diagnosis.get('status') == 'terdiagnosis' and diagnosis.get('diagnosis'):
|
||||||
|
diag_list = diagnosis['diagnosis']
|
||||||
|
# Save first (main) diagnosis
|
||||||
|
main_diag = diag_list[0]
|
||||||
|
|
||||||
|
# Determine severity from CF (Certainty Factor)
|
||||||
|
cf = main_diag.get('cf', 0)
|
||||||
|
if cf >= 70:
|
||||||
|
severity = 'berat'
|
||||||
|
elif cf >= 50:
|
||||||
|
severity = 'sedang'
|
||||||
|
else:
|
||||||
|
severity = 'ringan'
|
||||||
|
|
||||||
|
# Prepare diagnosis details for storage
|
||||||
|
diagnosis_details = {
|
||||||
|
'nama': main_diag.get('nama', ''),
|
||||||
|
'deskripsi': main_diag.get('deskripsi', ''),
|
||||||
|
'solusi': main_diag.get('solusi', []),
|
||||||
|
'cf': float(main_diag.get('cf', 0)),
|
||||||
|
'gejala_teramati': gejala,
|
||||||
|
'semua_diagnosis': [
|
||||||
|
{
|
||||||
|
'nama': d.get('nama', ''),
|
||||||
|
'cf': float(d.get('cf', 0))
|
||||||
|
}
|
||||||
|
for d in diag_list
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save to database dengan FK ke predictions table
|
||||||
|
diag_id = save_diagnosis_mysql(
|
||||||
|
prediction_id=prediction_id,
|
||||||
|
diagnosis_dict=diagnosis_details,
|
||||||
|
severity=severity,
|
||||||
|
confidence=float(cf),
|
||||||
|
timestamp=datetime.datetime.utcnow()
|
||||||
|
)
|
||||||
|
print(f"✓ Diagnosis saved to database, id={diag_id}, linked to prediction_id={prediction_id}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error saving diagnosis to database: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
return diagnosis # Flask akan otomatis mengkonversi dict ke JSON
|
return diagnosis # Flask akan otomatis mengkonversi dict ke JSON
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -626,15 +590,21 @@ def riwayat_diagnosis():
|
||||||
try:
|
try:
|
||||||
rows = get_diagnosis_history_mysql(limit=50)
|
rows = get_diagnosis_history_mysql(limit=50)
|
||||||
if rows:
|
if rows:
|
||||||
# normalize to expected template keys
|
# Extract data from diagnosis JSON and map to template keys
|
||||||
for r in rows:
|
for r in rows:
|
||||||
|
diagnosis_obj = r.get('diagnosis', {})
|
||||||
|
gejala_list = diagnosis_obj.get('gejala_teramati', [])
|
||||||
|
solusi_list = diagnosis_obj.get('solusi', [])
|
||||||
|
|
||||||
diagnosis_history.append({
|
diagnosis_history.append({
|
||||||
'id': r.get('id'),
|
'id': r.get('id'),
|
||||||
|
'prediction_id': r.get('prediction_id'),
|
||||||
'timestamp': r.get('timestamp'),
|
'timestamp': r.get('timestamp'),
|
||||||
'gejala': ','.join(r.get('gejala') or []),
|
'gejala': ','.join(gejala_list) if isinstance(gejala_list, list) else str(gejala_list),
|
||||||
'diagnosis': r.get('diagnosis'),
|
'diagnosis': diagnosis_obj.get('nama', 'Tidak diketahui'),
|
||||||
|
'severity': r.get('severity', 'sedang'),
|
||||||
'confidence': r.get('confidence') or 0.0,
|
'confidence': r.get('confidence') or 0.0,
|
||||||
'rekomendasi': '|'.join(r.get('rekomendasi') or [])
|
'rekomendasi': '|'.join(solusi_list) if isinstance(solusi_list, list) else str(solusi_list)
|
||||||
})
|
})
|
||||||
data_source = 'mysql'
|
data_source = 'mysql'
|
||||||
return render_template('diagnosis_history.html', history=diagnosis_history, data_source=data_source)
|
return render_template('diagnosis_history.html', history=diagnosis_history, data_source=data_source)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
opencv-python==4.8.1.78
|
opencv-python==4.9.0.80
|
||||||
numpy==1.24.3
|
numpy<2
|
||||||
pandas==2.0.3
|
pandas>=2.1.0
|
||||||
scikit-learn==1.3.0
|
scikit-learn>=1.3.2
|
||||||
scikit-image==0.21.0
|
scikit-image>=0.22.0
|
||||||
matplotlib==3.7.2
|
matplotlib>=3.8.0
|
||||||
pillow==10.0.0
|
pillow>=10.1.0
|
||||||
joblib==1.3.2
|
joblib>=1.3.2
|
||||||
tkinter
|
Flask>=2.3.3
|
||||||
Flask==2.3.2
|
SQLAlchemy>=2.0.0
|
||||||
SQLAlchemy
|
PyMySQL>=1.1.0
|
||||||
PyMySQL
|
python-dotenv>=1.0.0
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
"""
|
||||||
|
Setup script untuk initialize MySQL Database
|
||||||
|
Jalankan: python setup_db.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import pymysql
|
||||||
|
from pymysql import MySQLError
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
DB_HOST = os.environ.get('DB_HOST', 'localhost')
|
||||||
|
DB_PORT = int(os.environ.get('DB_PORT', '3306'))
|
||||||
|
DB_USER = os.environ.get('DB_USER', 'root')
|
||||||
|
DB_PASSWORD = os.environ.get('DB_PASSWORD', '')
|
||||||
|
DB_NAME = os.environ.get('DB_NAME', 'deteksi_pmk')
|
||||||
|
|
||||||
|
|
||||||
|
def create_database():
|
||||||
|
"""Create database if not exists"""
|
||||||
|
try:
|
||||||
|
# Connect to MySQL server (without specifying database)
|
||||||
|
if DB_PASSWORD:
|
||||||
|
conn = pymysql.connect(
|
||||||
|
host=DB_HOST,
|
||||||
|
port=DB_PORT,
|
||||||
|
user=DB_USER,
|
||||||
|
password=DB_PASSWORD,
|
||||||
|
charset='utf8mb4'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
conn = pymysql.connect(
|
||||||
|
host=DB_HOST,
|
||||||
|
port=DB_PORT,
|
||||||
|
user=DB_USER,
|
||||||
|
charset='utf8mb4'
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Create database
|
||||||
|
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
|
||||||
|
print(f"✓ Database '{DB_NAME}' created/verified")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except MySQLError as e:
|
||||||
|
print(f"✗ Error creating database: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def init_tables():
|
||||||
|
"""Initialize database tables using SQLAlchemy"""
|
||||||
|
try:
|
||||||
|
# Import after ensuring database exists
|
||||||
|
from utils.mysql_db import init_mysql_tables
|
||||||
|
|
||||||
|
init_mysql_tables()
|
||||||
|
print("✓ Database tables created/verified")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error initializing tables: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 60)
|
||||||
|
print("MySQL Database Setup for Deteksi PMK")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
print(f"Configuration:")
|
||||||
|
print(f" Host: {DB_HOST}")
|
||||||
|
print(f" Port: {DB_PORT}")
|
||||||
|
print(f" User: {DB_USER}")
|
||||||
|
print(f" Database: {DB_NAME}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Step 1: Create database
|
||||||
|
print("Step 1: Creating database...")
|
||||||
|
if not create_database():
|
||||||
|
print("\n✗ Setup failed. Please check your MySQL connection settings in .env")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Step 2: Initialize tables
|
||||||
|
print("Step 2: Initializing tables...")
|
||||||
|
if not init_tables():
|
||||||
|
print("\n✗ Setup failed. Please check your database configuration.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("✓ Database setup completed successfully!")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
print("Next steps:")
|
||||||
|
print("1. Make sure database is running")
|
||||||
|
print("2. Run: python app.py")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Detail Riwayat Deteksi{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-lg mt-5 mb-5">
|
||||||
|
{% if prediction %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 offset-lg-2">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="card border-success mb-4">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h4 class="mb-0"><i class="fas fa-search"></i> Detail Riwayat Deteksi</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-muted mb-2">Nama File Asli</h6>
|
||||||
|
<p class="fw-bold">{{ prediction.original_filename or 'N/A' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-muted mb-2">Waktu Deteksi</h6>
|
||||||
|
<p class="fw-bold">
|
||||||
|
{% if prediction.timestamp %}
|
||||||
|
{{ prediction.timestamp[:19] | replace('T', ' ') }}
|
||||||
|
{% else %}
|
||||||
|
N/A
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Display -->
|
||||||
|
{% if prediction.image_path %}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-image"></i> Foto yang Dianalisis</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
{% set image_url = '/uploads/' + prediction.filename %}
|
||||||
|
<img src="{{ image_url }}" alt="Uploaded Image" class="img-fluid rounded" style="max-height: 400px;">
|
||||||
|
<p class="text-muted mt-3 small">Gambar yang digunakan untuk analisis</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Prediction Result -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-stethoscope"></i> Hasil Prediksi</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="text-center p-4">
|
||||||
|
{% if prediction.prediction.lower() == 'sehat' %}
|
||||||
|
<div class="badge bg-success p-3 mb-3" style="font-size: 1.2rem;">
|
||||||
|
<i class="fas fa-check-circle"></i> SEHAT
|
||||||
|
</div>
|
||||||
|
<h5 class="text-success fw-bold">Kesimpulan: Organ Sehat</h5>
|
||||||
|
{% else %}
|
||||||
|
<div class="badge bg-danger p-3 mb-3" style="font-size: 1.2rem;">
|
||||||
|
<i class="fas fa-exclamation-circle"></i> SAKIT
|
||||||
|
</div>
|
||||||
|
<h5 class="text-danger fw-bold">Kesimpulan: PMK Terdeteksi</h5>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6 class="text-muted mb-2">Confidence Score</h6>
|
||||||
|
<div class="progress mb-2" style="height: 25px;">
|
||||||
|
<div
|
||||||
|
class="progress-bar {% if prediction.prediction.lower() == 'sehat' %}bg-success{% else %}bg-danger{% endif %}"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: {{ prediction.confidence or 0 }}%"
|
||||||
|
aria-valuenow="{{ prediction.confidence or 0 }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
<strong>{{ prediction.confidence or 0 }}%</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Tingkat kepercayaan model terhadap hasil prediksi</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Diagnosis Result (if exists) -->
|
||||||
|
{% if diagnosis %}
|
||||||
|
<div class="card mb-4 border-primary">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0"><i class="fas fa-stethoscope"></i> Hasil Diagnosis Sistem Pakar</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Diagnosis Name and Severity -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-muted mb-2">Diagnosis</h6>
|
||||||
|
<h5 class="fw-bold">
|
||||||
|
{% set diag_name = diagnosis.diagnosis.nama or 'Tidak diketahui' %}
|
||||||
|
{{ diag_name }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-muted mb-2">Tingkat Keparahan</h6>
|
||||||
|
<span class="badge p-2" style="font-size: 1rem;
|
||||||
|
{% if diagnosis.severity == 'ringan' %}background-color: #28a745;
|
||||||
|
{% elif diagnosis.severity == 'sedang' %}background-color: #ffc107;
|
||||||
|
{% else %}background-color: #dc3545;{% endif %}">
|
||||||
|
{{ diagnosis.severity|upper }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
{% if diagnosis.diagnosis.deskripsi %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-2">Deskripsi</h6>
|
||||||
|
<p class="card-text">{{ diagnosis.diagnosis.deskripsi }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Observed Symptoms -->
|
||||||
|
{% if diagnosis.diagnosis.gejala_teramati %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-3">Gejala yang Teramati</h6>
|
||||||
|
<div class="row">
|
||||||
|
{% for symptom_code in diagnosis.diagnosis.gejala_teramati %}
|
||||||
|
<div class="col-md-12 mb-2">
|
||||||
|
<div class="card bg-light">
|
||||||
|
<div class="card-body py-2 px-3">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="badge bg-info">{{ symptom_code }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<p class="mb-0">{{ symptom_code|get_symptom_desc }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Recommended Solutions -->
|
||||||
|
{% if diagnosis.diagnosis.solusi %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-2">Solusi dan Rekomendasi</h6>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for solution in diagnosis.diagnosis.solusi %}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<i class="fas fa-check-circle text-success me-2"></i>{{ solution }}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Confidence and All Diagnoses -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-muted mb-2">Tingkat Keyakinan Diagnosis</h6>
|
||||||
|
<div class="progress mb-2" style="height: 25px;">
|
||||||
|
<div class="progress-bar bg-primary" role="progressbar"
|
||||||
|
style="width: {{ (diagnosis.confidence * 100) or 0 }}%"
|
||||||
|
aria-valuenow="{{ (diagnosis.confidence * 100) or 0 }}"
|
||||||
|
aria-valuemin="0" aria-valuemax="100">
|
||||||
|
<strong>{{ "%.1f"|format((diagnosis.confidence * 100) or 0) }}%</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-muted mb-2">Waktu Diagnosis</h6>
|
||||||
|
<p class="fw-bold">
|
||||||
|
{% if diagnosis.timestamp %}
|
||||||
|
{{ diagnosis.timestamp[:19] | replace('T', ' ') }}
|
||||||
|
{% else %}
|
||||||
|
N/A
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- All Possible Diagnoses -->
|
||||||
|
{% if diagnosis.diagnosis.semua_diagnosis %}
|
||||||
|
<div class="mt-4">
|
||||||
|
<h6 class="text-muted mb-3">Semua Kemungkinan Diagnosis</h6>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Diagnosis</th>
|
||||||
|
<th>Confidence</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for alt_diag in diagnosis.diagnosis.semua_diagnosis %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ alt_diag.nama }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="progress" style="height: 20px;">
|
||||||
|
<div class="progress-bar" role="progressbar"
|
||||||
|
style="width: {{ (alt_diag.cf * 100) or 0 }}%"
|
||||||
|
aria-valuenow="{{ (alt_diag.cf * 100) or 0 }}"
|
||||||
|
aria-valuemin="0" aria-valuemax="100">
|
||||||
|
{{ "%.1f"|format((alt_diag.cf * 100) or 0) }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
<strong>Belum Ada Diagnosis.</strong> Prediction ini belum didiagnosis oleh sistem pakar.
|
||||||
|
<a href="{{ url_for('expert_system_page') }}" class="alert-link">Lakukan diagnosis sekarang.</a>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="d-grid gap-2 d-sm-flex justify-content-sm-between mt-4">
|
||||||
|
<a href="{{ url_for('riwayat_deteksi') }}" class="btn btn-secondary btn-lg">
|
||||||
|
<i class="fas fa-arrow-left"></i> Kembali ke Riwayat
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('index') }}" class="btn btn-success btn-lg">
|
||||||
|
<i class="fas fa-home"></i> Ke Halaman Beranda
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<h5 class="alert-heading"><i class="fas fa-exclamation-triangle"></i> Data Tidak Ditemukan</h5>
|
||||||
|
<p>Maaf, riwayat deteksi dengan ID yang diminta tidak dapat ditemukan.</p>
|
||||||
|
<hr>
|
||||||
|
<a href="{{ url_for('riwayat_deteksi') }}" class="btn btn-primary">Kembali ke Riwayat Deteksi</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
background-color: rgba(40, 167, 69, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -7,11 +7,7 @@
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow">
|
<div class="card shadow">
|
||||||
<div class="card-header bg-success text-white">
|
<div class="card-header bg-success text-white">
|
||||||
<h4 class="mb-0"><i class="fas fa-history me-2"></i>Riwayat Diagnosis Sistem Pakar
|
<h4 class="mb-0"><i class="fas fa-history me-2"></i>Riwayat Diagnosis Sistem Pakar</h4>
|
||||||
{% if data_source %}
|
|
||||||
<span class="badge bg-secondary ms-2">Sumber: {{ data_source|upper }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</h4>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,10 @@
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-0"><strong>Tingkat Keyakinan:</strong> {{ "%.2f"|format(image_info.confidence) }}%</p>
|
<p class="mb-0"><strong>Tingkat Keyakinan:</strong> {{ "%.2f"|format(image_info.confidence) }}%</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<!-- <div class="col-md-4 text-center">
|
||||||
<i class="fas fa-arrow-right fa-2x text-success"></i>
|
<i class="fas fa-arrow-right fa-2x text-success"></i>
|
||||||
<p class="mb-0"><small>Lanjutkan diagnosis</small></p>
|
<p class="mb-0"><small>Lanjutkan diagnosis</small></p>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h2 class="mb-4"><i class="fas fa-history me-2"></i>Riwayat Deteksi
|
<h2 class="mb-4"><i class="fas fa-history me-2"></i>Riwayat Deteksi</h2>
|
||||||
{% if data_source %}
|
|
||||||
<span class="badge bg-secondary ms-2">Sumber: {{ data_source|upper }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -124,9 +120,9 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-info" onclick="viewDetails('{{ item.filename or item.image_path }}')">
|
<a href="{{ url_for('detail_deteksi', pred_id=item.id if item.id is defined else loop.index0) }}" class="btn btn-sm btn-info">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i> Detail
|
||||||
</button>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
@ -134,11 +130,23 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
{% if data_source == 'database_error' %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
<strong>Error:</strong> Database tidak tersedia. Pastikan MySQL server berjalan dan jalankan <code>python setup_db.py</code>.
|
||||||
|
</div>
|
||||||
|
{% elif data_source == 'error' %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-circle me-2"></i>
|
||||||
|
<strong>Error:</strong> Gagal mengambil data dari database. Cek koneksi MySQL dan coba lagi.
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
Belum ada data deteksi. Silakan upload gambar terlebih dahulu.
|
Belum ada data deteksi. Silakan upload gambar terlebih dahulu.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Quick test untuk environment variables dan MySQL connection"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("ENVIRONMENT VARIABLES TEST")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
variables = {
|
||||||
|
'DB_HOST': 'localhost',
|
||||||
|
'DB_PORT': '3306',
|
||||||
|
'DB_USER': 'root',
|
||||||
|
'DB_PASSWORD': '(hidden)',
|
||||||
|
'DB_NAME': 'deteksi_pmk',
|
||||||
|
'FLASK_SECRET': '(hidden)'
|
||||||
|
}
|
||||||
|
|
||||||
|
for var, default in variables.items():
|
||||||
|
value = os.environ.get(var)
|
||||||
|
if value:
|
||||||
|
if 'PASSWORD' in var or 'SECRET' in var:
|
||||||
|
print(f"✓ {var}: Set (hidden)")
|
||||||
|
else:
|
||||||
|
print(f"✓ {var}: {value}")
|
||||||
|
else:
|
||||||
|
print(f"✗ {var}: Not set (using implicit defaults)")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("MYSQL CONNECTION TEST")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from utils.mysql_db import get_engine
|
||||||
|
engine = get_engine()
|
||||||
|
with engine.connect() as conn:
|
||||||
|
print("✓ Database connection: SUCCESS")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Database connection: FAILED")
|
||||||
|
print(f" Error: {e}")
|
||||||
|
|
||||||
|
print()
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
|
|
@ -0,0 +1,426 @@
|
||||||
|
"""
|
||||||
|
MySQL Database Integration for Deteksi PMK
|
||||||
|
Handles saving and retrieving predictions and diagnosis history
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, Text, Boolean, ForeignKey
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
# Database configuration from environment variables
|
||||||
|
DB_HOST = os.environ.get('DB_HOST', 'localhost')
|
||||||
|
DB_PORT = os.environ.get('DB_PORT', '3306')
|
||||||
|
DB_USER = os.environ.get('DB_USER', 'root')
|
||||||
|
DB_PASSWORD = os.environ.get('DB_PASSWORD', '')
|
||||||
|
DB_NAME = os.environ.get('DB_NAME', 'deteksi_pmk')
|
||||||
|
|
||||||
|
# Create database URI
|
||||||
|
if DB_PASSWORD:
|
||||||
|
DATABASE_URI = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
||||||
|
else:
|
||||||
|
DATABASE_URI = f"mysql+pymysql://{DB_USER}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
||||||
|
|
||||||
|
# Create engine
|
||||||
|
engine = create_engine(DATABASE_URI, echo=False, pool_pre_ping=True)
|
||||||
|
Base = declarative_base()
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
class Prediction(Base):
|
||||||
|
"""Model untuk menyimpan hasil prediksi"""
|
||||||
|
__tablename__ = 'predictions'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
original_filename = Column(String(255))
|
||||||
|
filename = Column(String(255))
|
||||||
|
image_path = Column(String(500))
|
||||||
|
prediction = Column(String(50)) # 'sehat' or 'sakit'
|
||||||
|
confidence = Column(Float)
|
||||||
|
features = Column(Text) # JSON string of features
|
||||||
|
timestamp = Column(DateTime, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
class DiagnosisHistory(Base):
|
||||||
|
"""Model untuk menyimpan hasil diagnosis dari expert system"""
|
||||||
|
__tablename__ = 'diagnosis_history'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
prediction_id = Column(Integer, ForeignKey('predictions.id'), nullable=True) # FK ke predictions table
|
||||||
|
diagnosis = Column(Text) # JSON string of diagnosis details
|
||||||
|
severity = Column(String(50)) # 'ringan', 'sedang', 'berat'
|
||||||
|
confidence = Column(Float)
|
||||||
|
timestamp = Column(DateTime, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine():
|
||||||
|
"""Return SQLAlchemy engine"""
|
||||||
|
return engine
|
||||||
|
|
||||||
|
|
||||||
|
def init_mysql_tables():
|
||||||
|
"""Initialize database tables"""
|
||||||
|
try:
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
print("Database tables initialized successfully")
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Error initializing database tables: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def save_prediction_mysql(original_filename, filename, image_path, prediction, confidence, features_dict, timestamp=None):
|
||||||
|
"""
|
||||||
|
Save prediction result to MySQL database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
original_filename: Original filename uploaded
|
||||||
|
filename: Saved filename
|
||||||
|
image_path: Full path to saved image
|
||||||
|
prediction: 'sehat' or 'sakit'
|
||||||
|
confidence: Confidence score (0-1)
|
||||||
|
features_dict: Dictionary of extracted features
|
||||||
|
timestamp: Optional datetime, defaults to now
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ID of saved prediction
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
# Convert features dict to JSON string
|
||||||
|
features_json = json.dumps(features_dict, default=str)
|
||||||
|
|
||||||
|
# Create prediction record
|
||||||
|
pred = Prediction(
|
||||||
|
original_filename=original_filename,
|
||||||
|
filename=filename,
|
||||||
|
image_path=image_path,
|
||||||
|
prediction=prediction.lower(),
|
||||||
|
confidence=float(confidence),
|
||||||
|
features=features_json,
|
||||||
|
timestamp=timestamp or datetime.datetime.utcnow()
|
||||||
|
)
|
||||||
|
|
||||||
|
session.add(pred)
|
||||||
|
session.commit()
|
||||||
|
pred_id = pred.id
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
return pred_id
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Error saving prediction to database: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_recent_predictions_mysql(limit=10):
|
||||||
|
"""
|
||||||
|
Get recent predictions from database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: Number of recent predictions to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dictionaries containing prediction data
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
# Query recent predictions, ordered by timestamp descending
|
||||||
|
predictions = session.query(Prediction).order_by(
|
||||||
|
Prediction.timestamp.desc()
|
||||||
|
).limit(limit).all()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for pred in predictions:
|
||||||
|
try:
|
||||||
|
features = json.loads(pred.features) if pred.features else {}
|
||||||
|
except:
|
||||||
|
features = {}
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
'id': pred.id,
|
||||||
|
'original_filename': pred.original_filename,
|
||||||
|
'filename': pred.filename,
|
||||||
|
'image_path': pred.image_path,
|
||||||
|
'prediction': pred.prediction,
|
||||||
|
'confidence': round(float(pred.confidence), 4),
|
||||||
|
'features': features,
|
||||||
|
'timestamp': pred.timestamp.isoformat() if pred.timestamp else None
|
||||||
|
})
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Error retrieving predictions from database: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_prediction_by_id(pred_id):
|
||||||
|
"""
|
||||||
|
Get specific prediction by ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pred_id: Prediction ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with prediction details or None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
pred = session.query(Prediction).filter(Prediction.id == pred_id).first()
|
||||||
|
|
||||||
|
if not pred:
|
||||||
|
session.close()
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
features = json.loads(pred.features) if pred.features else {}
|
||||||
|
except:
|
||||||
|
features = {}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'id': pred.id,
|
||||||
|
'original_filename': pred.original_filename,
|
||||||
|
'filename': pred.filename,
|
||||||
|
'image_path': pred.image_path,
|
||||||
|
'prediction': pred.prediction,
|
||||||
|
'confidence': round(float(pred.confidence), 4),
|
||||||
|
'features': features,
|
||||||
|
'timestamp': pred.timestamp.isoformat() if pred.timestamp else None
|
||||||
|
}
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Error retrieving prediction by ID: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def save_diagnosis_mysql(prediction_id, diagnosis_dict, severity='sedang', confidence=None, timestamp=None):
|
||||||
|
"""
|
||||||
|
Save diagnosis result from expert system to database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prediction_id: Foreign Key ke predictions table (prediction yang di-diagnosa)
|
||||||
|
diagnosis_dict: Dictionary of diagnosis details from expert system
|
||||||
|
severity: 'ringan', 'sedang', or 'berat'
|
||||||
|
confidence: Confidence score (optional)
|
||||||
|
timestamp: Optional datetime, defaults to now
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ID of saved diagnosis
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
# Convert diagnosis dict to JSON string
|
||||||
|
diagnosis_json = json.dumps(diagnosis_dict, default=str)
|
||||||
|
|
||||||
|
# Create diagnosis record with FK to predictions
|
||||||
|
diag = DiagnosisHistory(
|
||||||
|
prediction_id=prediction_id,
|
||||||
|
diagnosis=diagnosis_json,
|
||||||
|
severity=severity,
|
||||||
|
confidence=float(confidence) if confidence else None,
|
||||||
|
timestamp=timestamp or datetime.datetime.utcnow()
|
||||||
|
)
|
||||||
|
|
||||||
|
session.add(diag)
|
||||||
|
session.commit()
|
||||||
|
diag_id = diag.id
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
return diag_id
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Error saving diagnosis to database: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_diagnosis_history_mysql(limit=50, order_by='timestamp'):
|
||||||
|
"""
|
||||||
|
Get diagnosis history from database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: Number of records to retrieve
|
||||||
|
order_by: Column to order by
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dictionaries containing diagnosis data
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
# Query diagnosis history, ordered by timestamp descending
|
||||||
|
diagnoses = session.query(DiagnosisHistory).order_by(
|
||||||
|
DiagnosisHistory.timestamp.desc()
|
||||||
|
).limit(limit).all()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for diag in diagnoses:
|
||||||
|
try:
|
||||||
|
diagnosis = json.loads(diag.diagnosis) if diag.diagnosis else {}
|
||||||
|
except:
|
||||||
|
diagnosis = {}
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
'id': diag.id,
|
||||||
|
'prediction_id': diag.prediction_id,
|
||||||
|
'diagnosis': diagnosis,
|
||||||
|
'severity': diag.severity,
|
||||||
|
'confidence': round(float(diag.confidence), 4) if diag.confidence else None,
|
||||||
|
'timestamp': diag.timestamp.isoformat() if diag.timestamp else None
|
||||||
|
})
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Error retrieving diagnosis history from database: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_diagnosis_by_id(diag_id):
|
||||||
|
"""
|
||||||
|
Get specific diagnosis by ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
diag_id: Diagnosis ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with diagnosis details or None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
diag = session.query(DiagnosisHistory).filter(DiagnosisHistory.id == diag_id).first()
|
||||||
|
|
||||||
|
if not diag:
|
||||||
|
session.close()
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
diagnosis = json.loads(diag.diagnosis) if diag.diagnosis else {}
|
||||||
|
except:
|
||||||
|
diagnosis = {}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'id': diag.id,
|
||||||
|
'prediction_id': diag.prediction_id,
|
||||||
|
'diagnosis': diagnosis,
|
||||||
|
'severity': diag.severity,
|
||||||
|
'confidence': round(float(diag.confidence), 4) if diag.confidence else None,
|
||||||
|
'timestamp': diag.timestamp.isoformat() if diag.timestamp else None
|
||||||
|
}
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Error retrieving diagnosis by ID: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_diagnosis_by_prediction_id(pred_id):
|
||||||
|
"""
|
||||||
|
Get diagnosis for a specific prediction (by prediction_id FK)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pred_id: Prediction ID (Foreign Key)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with most recent diagnosis or None if no diagnosis exists
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
# Get most recent diagnosis for this prediction
|
||||||
|
diag = session.query(DiagnosisHistory).filter(
|
||||||
|
DiagnosisHistory.prediction_id == pred_id
|
||||||
|
).order_by(DiagnosisHistory.timestamp.desc()).first()
|
||||||
|
|
||||||
|
if not diag:
|
||||||
|
session.close()
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
diagnosis = json.loads(diag.diagnosis) if diag.diagnosis else {}
|
||||||
|
except:
|
||||||
|
diagnosis = {}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'id': diag.id,
|
||||||
|
'prediction_id': diag.prediction_id,
|
||||||
|
'diagnosis': diagnosis,
|
||||||
|
'severity': diag.severity,
|
||||||
|
'confidence': round(float(diag.confidence), 4) if diag.confidence else None,
|
||||||
|
'timestamp': diag.timestamp.isoformat() if diag.timestamp else None
|
||||||
|
}
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Error retrieving diagnosis by prediction ID: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def delete_prediction_mysql(pred_id):
|
||||||
|
"""Delete a prediction by ID"""
|
||||||
|
try:
|
||||||
|
session = Session()
|
||||||
|
session.query(Prediction).filter(Prediction.id == pred_id).delete()
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
return True
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Error deleting prediction: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def delete_diagnosis_mysql(diag_id):
|
||||||
|
"""Delete a diagnosis by ID"""
|
||||||
|
try:
|
||||||
|
session = Session()
|
||||||
|
session.query(DiagnosisHistory).filter(DiagnosisHistory.id == diag_id).delete()
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
return True
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Error deleting diagnosis: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_statistics_mysql():
|
||||||
|
"""Get statistics from database"""
|
||||||
|
try:
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
# Count predictions
|
||||||
|
total_predictions = session.query(Prediction).count()
|
||||||
|
sakit_predictions = session.query(Prediction).filter(Prediction.prediction == 'sakit').count()
|
||||||
|
sehat_predictions = session.query(Prediction).filter(Prediction.prediction == 'sehat').count()
|
||||||
|
|
||||||
|
# Count diagnoses
|
||||||
|
total_diagnoses = session.query(DiagnosisHistory).count()
|
||||||
|
|
||||||
|
# Average confidence
|
||||||
|
avg_confidence = None
|
||||||
|
result = session.query(Prediction).first()
|
||||||
|
if result:
|
||||||
|
from sqlalchemy import func
|
||||||
|
avg_conf = session.query(func.avg(Prediction.confidence)).scalar()
|
||||||
|
avg_confidence = round(float(avg_conf), 4) if avg_conf else None
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_predictions': total_predictions,
|
||||||
|
'sakit_predictions': sakit_predictions,
|
||||||
|
'sehat_predictions': sehat_predictions,
|
||||||
|
'total_diagnoses': total_diagnoses,
|
||||||
|
'average_confidence': avg_confidence
|
||||||
|
}
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Error getting statistics: {e}")
|
||||||
|
return {}
|
||||||
Loading…
Reference in New Issue