175 lines
5.5 KiB
Python
175 lines
5.5 KiB
Python
"""
|
|
Flask API Server untuk Aplikasi Penerjemah Bahasa Isyarat Indonesia.
|
|
|
|
Menyediakan REST API endpoint untuk frontend React:
|
|
- POST /init : Inisialisasi session inferensi berdasarkan kategori (angka, abjad_bisindo, abjad_sibi, kata)
|
|
- POST /detect : Memproses frame kamera (base64) dari frontend dan mengembalikan hasil prediksi
|
|
"""
|
|
|
|
import sys
|
|
import uuid
|
|
import time
|
|
import base64
|
|
import numpy as np
|
|
import cv2
|
|
from pathlib import Path
|
|
from flask import Flask, request, jsonify
|
|
from flask_cors import CORS
|
|
|
|
# Menambahkan root folder backend ke sys.path
|
|
BASE_DIR = Path(__file__).parent
|
|
sys.path.insert(0, str(BASE_DIR))
|
|
|
|
from utils.inference import GestureRecognizer
|
|
from utils.config import MODEL_DIR, WINDOW_SIZE, CATEGORIES
|
|
|
|
app = Flask(__name__)
|
|
|
|
# Mengizinkan Cross-Origin Requests (CORS) agar Frontend React (port 3000) bisa terhubung
|
|
CORS(app, resources={r"/*": {"origins": "*"}})
|
|
|
|
# Dictionary penyimpanan session inferensi
|
|
# Format: { session_id: { "category": str, "recognizer": GestureRecognizer, "last_accessed": float } }
|
|
sessions = {}
|
|
|
|
# Timeout session (10 menit tanpa aktivitas -> cleanup)
|
|
SESSION_TIMEOUT_SECONDS = 600
|
|
|
|
|
|
def cleanup_expired_sessions():
|
|
"""Hapus session yang sudah tidak aktif untuk menghemat memory."""
|
|
now = time.time()
|
|
expired = [
|
|
sid for sid, sess in sessions.items()
|
|
if now - sess.get("last_accessed", 0) > SESSION_TIMEOUT_SECONDS
|
|
]
|
|
for sid in expired:
|
|
try:
|
|
sessions[sid]["recognizer"].close()
|
|
except Exception:
|
|
pass
|
|
del sessions[sid]
|
|
|
|
|
|
@app.route("/", methods=["GET"])
|
|
def index():
|
|
return jsonify({
|
|
"status": "online",
|
|
"message": "Backend Penerjemah Bahasa Isyarat Indonesia (TCN) Siap!",
|
|
"categories": CATEGORIES,
|
|
"endpoints": {
|
|
"/init": "POST - Inisialisasi session model",
|
|
"/detect": "POST - Kirim frame base64 dan dapatkan prediksi"
|
|
}
|
|
})
|
|
|
|
|
|
@app.route("/init", methods=["POST"])
|
|
def init_session():
|
|
"""Inisialisasi session inferensi untuk frontend."""
|
|
cleanup_expired_sessions()
|
|
|
|
data = request.get_json() or {}
|
|
category = data.get("category", "kata")
|
|
|
|
if category not in CATEGORIES:
|
|
return jsonify({
|
|
"error": f"Kategori '{category}' tidak valid. Pilihan kategori: {CATEGORIES}"
|
|
}), 400
|
|
|
|
model_path = MODEL_DIR / category / f"model_{category}.pt"
|
|
encoder_path = MODEL_DIR / category / "label_encoder.json"
|
|
|
|
if not model_path.exists():
|
|
return jsonify({
|
|
"error": f"Model untuk kategori '{category}' tidak ditemukan di {model_path}"
|
|
}), 404
|
|
|
|
if not encoder_path.exists():
|
|
return jsonify({
|
|
"error": f"Label encoder untuk kategori '{category}' tidak ditemukan di {encoder_path}"
|
|
}), 404
|
|
|
|
try:
|
|
recognizer = GestureRecognizer(
|
|
model_path=model_path,
|
|
label_encoder_path=encoder_path,
|
|
window_size=WINDOW_SIZE,
|
|
draw_landmarks=False
|
|
)
|
|
session_id = str(uuid.uuid4())
|
|
sessions[session_id] = {
|
|
"category": category,
|
|
"recognizer": recognizer,
|
|
"last_accessed": time.time()
|
|
}
|
|
|
|
print(f"[INIT] Session {session_id} dibuat untuk kategori: {category}")
|
|
return jsonify({
|
|
"session_id": session_id,
|
|
"category": category,
|
|
"status": "ready"
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
print(f"[ERROR] Gagal inisialisasi session: {e}")
|
|
return jsonify({"error": f"Gagal inisialisasi model: {str(e)}"}), 500
|
|
|
|
|
|
@app.route("/detect", methods=["POST"])
|
|
def detect_frame():
|
|
"""Memproses 1 frame JPEG base64 dari frontend dan mengembalikan hasil prediksi."""
|
|
data = request.get_json() or {}
|
|
session_id = data.get("session_id")
|
|
frame_data = data.get("frame")
|
|
|
|
if not session_id or session_id not in sessions:
|
|
return jsonify({"error": "Session ID tidak ditemukan atau expired. Silakan muat ulang halaman."}), 400
|
|
|
|
if not frame_data:
|
|
return jsonify({"error": "Data frame gambar tidak boleh kosong."}), 400
|
|
|
|
session = sessions[session_id]
|
|
session["last_accessed"] = time.time()
|
|
recognizer = session["recognizer"]
|
|
|
|
try:
|
|
# Decode base64 image dari dataURL format (data:image/jpeg;base64,...)
|
|
if "," in frame_data:
|
|
frame_data = frame_data.split(",")[1]
|
|
|
|
image_bytes = base64.b64decode(frame_data)
|
|
nparr = np.frombuffer(image_bytes, np.uint8)
|
|
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
|
|
if frame is None:
|
|
return jsonify({"error": "Gagal mendekode gambar dari frame data."}), 400
|
|
|
|
# Process frame
|
|
label, confidence = recognizer.process_frame(frame)
|
|
|
|
buffer_size = len(recognizer.buffer)
|
|
window_size = recognizer.window_size
|
|
|
|
# Print log untuk debug
|
|
if buffer_size > 0:
|
|
print(f"[{session['category'].upper()}] Buffer: {buffer_size}/{window_size} | Prediksi: {label if label else '...'}")
|
|
|
|
return jsonify({
|
|
"label": label if label is not None else None,
|
|
"confidence": float(confidence) if confidence is not None else 0.0,
|
|
"buffer_size": buffer_size,
|
|
"window_size": window_size
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
print(f"[ERROR] Exception saat memproses frame: {e}")
|
|
return jsonify({"error": f"Error memproses frame: {str(e)}"}), 500
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("=" * 60)
|
|
print(" STARTING BACKEND FLASK SERVER (PORT 5000)")
|
|
print("=" * 60)
|
|
app.run(host="0.0.0.0", port=5000, debug=True)
|