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