182 lines
5.5 KiB
Python
182 lines
5.5 KiB
Python
import os
|
|
import uuid
|
|
import logging
|
|
from functools import wraps
|
|
from flask import Flask, request, jsonify
|
|
from flask_cors import CORS
|
|
|
|
from config import DATASETS_DIR, MODELS_DIR, TEMP_DIR, MAX_CONTENT_LENGTH, API_KEY
|
|
from services.extract_service import extract_frames
|
|
from services.train_service import train_model
|
|
from services.verify_service import verify_face
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='[%(asctime)s] %(levelname)s: %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
app = Flask(__name__)
|
|
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
|
|
CORS(app)
|
|
|
|
|
|
def require_api_key(f):
|
|
@wraps(f)
|
|
def decorated(*args, **kwargs):
|
|
key = request.headers.get('X-API-Key', '')
|
|
if key != API_KEY:
|
|
return jsonify({"status": "error", "message": "API key tidak valid."}), 401
|
|
return f(*args, **kwargs)
|
|
return decorated
|
|
|
|
|
|
@app.route('/health', methods=['GET'])
|
|
def health():
|
|
model_exists = os.path.exists(os.path.join(MODELS_DIR, 'face_model.pkl'))
|
|
return jsonify({
|
|
"status": "ok",
|
|
"model_loaded": model_exists,
|
|
"datasets_dir": DATASETS_DIR,
|
|
"models_dir": MODELS_DIR,
|
|
})
|
|
|
|
|
|
@app.route('/extract-frames', methods=['POST'])
|
|
@require_api_key
|
|
def api_extract_frames():
|
|
if 'video' not in request.files:
|
|
return jsonify({"status": "error", "message": "File video tidak ditemukan."}), 400
|
|
|
|
user_id = request.form.get('user_id')
|
|
target_frames = int(request.form.get('target_frames', 200))
|
|
|
|
if not user_id:
|
|
return jsonify({"status": "error", "message": "user_id wajib diisi."}), 400
|
|
|
|
video_file = request.files['video']
|
|
temp_video = os.path.join(TEMP_DIR, f"enroll_{user_id}_{uuid.uuid4().hex[:8]}.mp4")
|
|
|
|
try:
|
|
video_file.save(temp_video)
|
|
|
|
output_dir = os.path.join(DATASETS_DIR, str(user_id))
|
|
|
|
result = extract_frames(temp_video, output_dir, target_frames=target_frames)
|
|
|
|
logger.info(
|
|
f"Extract frames berhasil untuk user {user_id}. "
|
|
f"Frames: {result['total_extracted']}"
|
|
)
|
|
|
|
return jsonify({
|
|
"status": "success",
|
|
"message": f"Berhasil mengekstrak {result['total_extracted']} frame wajah frontal",
|
|
**result
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Extract frames GAGAL untuk user {user_id}: {str(e)}")
|
|
return jsonify({"status": "error", "message": str(e)}), 500
|
|
|
|
finally:
|
|
if os.path.exists(temp_video):
|
|
os.remove(temp_video)
|
|
|
|
|
|
@app.route('/train-model', methods=['POST'])
|
|
@require_api_key
|
|
def api_train_model():
|
|
data = request.get_json(silent=True) or {}
|
|
approved_user_ids = data.get('approved_user_ids')
|
|
|
|
if not approved_user_ids or not isinstance(approved_user_ids, list):
|
|
return jsonify({
|
|
"status": "error",
|
|
"message": "approved_user_ids wajib diisi sebagai array."
|
|
}), 400
|
|
|
|
approved_str = [str(uid) for uid in approved_user_ids]
|
|
|
|
try:
|
|
logger.info(
|
|
f"Memulai training model SVM. "
|
|
f"Approved users: {approved_str}"
|
|
)
|
|
|
|
result = train_model(DATASETS_DIR, MODELS_DIR, approved_str)
|
|
|
|
logger.info(
|
|
f"Training selesai. Users: {result['total_users']}, "
|
|
f"CV: {result['cv_score']*100:.2f}%, "
|
|
f"Test: {result['test_accuracy']*100:.2f}%"
|
|
)
|
|
|
|
return jsonify({
|
|
"status": "success",
|
|
"message": (
|
|
f"Model SVM berhasil dilatih. "
|
|
f"{result['total_users']} user, "
|
|
f"CV={result['cv_score']*100:.2f}%, "
|
|
f"Test={result['test_accuracy']*100:.2f}%"
|
|
),
|
|
**result
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Training model GAGAL: {str(e)}")
|
|
return jsonify({"status": "error", "message": str(e)}), 500
|
|
|
|
|
|
@app.route('/verify-face', methods=['POST'])
|
|
@require_api_key
|
|
def api_verify_face():
|
|
if 'file' not in request.files:
|
|
return jsonify({"status": "error", "message": "File tidak ditemukan."}), 400
|
|
|
|
user_id = request.form.get('user_id')
|
|
is_video = request.form.get('is_video', 'false').lower() == 'true'
|
|
|
|
if not user_id:
|
|
return jsonify({"status": "error", "message": "user_id wajib diisi."}), 400
|
|
|
|
uploaded_file = request.files['file']
|
|
ext = 'mp4' if is_video else 'jpg'
|
|
temp_file = os.path.join(TEMP_DIR, f"verify_{user_id}_{uuid.uuid4().hex[:8]}.{ext}")
|
|
|
|
try:
|
|
uploaded_file.save(temp_file)
|
|
|
|
result = verify_face(MODELS_DIR, user_id, temp_file, is_video=is_video)
|
|
|
|
logger.info(
|
|
f"Verifikasi user {user_id}: "
|
|
f"status={result.get('verification_status')}, "
|
|
f"match={result.get('match')}, "
|
|
f"svm_df={result.get('svm_df')}, "
|
|
f"confidence={result.get('confidence')}, "
|
|
f"predicted={result.get('predicted_user')}, "
|
|
f"blur={result.get('blur_score')}, "
|
|
f"frames_approved={result.get('frames_approved')}/{result.get('frames_total')}"
|
|
)
|
|
|
|
return jsonify(result)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Verifikasi GAGAL untuk user {user_id}: {str(e)}")
|
|
return jsonify({"status": "error", "message": str(e)}), 500
|
|
|
|
finally:
|
|
if os.path.exists(temp_file):
|
|
os.remove(temp_file)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
logger.info("=" * 50)
|
|
logger.info("MPG HRIS - Flask ML API Server")
|
|
logger.info(f"Datasets: {DATASETS_DIR}")
|
|
logger.info(f"Models: {MODELS_DIR}")
|
|
logger.info("=" * 50)
|
|
|
|
app.run(host='0.0.0.0', port=5000, debug=True)
|