337 lines
10 KiB
Python
337 lines
10 KiB
Python
import os
|
|
import uuid
|
|
import logging
|
|
from functools import wraps
|
|
from flask import Flask, request, jsonify, send_file
|
|
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
|
|
from services.embedding_service import (
|
|
compute_embedding_from_frames,
|
|
compute_embedding_from_image,
|
|
compute_embedding_from_video
|
|
)
|
|
|
|
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"Test: {result['test_accuracy']*100:.2f}%"
|
|
)
|
|
|
|
return jsonify({
|
|
"status": "success",
|
|
"message": (
|
|
f"Model SVM berhasil dilatih. "
|
|
f"{result['total_users']} user, "
|
|
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)
|
|
|
|
|
|
@app.route('/extract-and-embed', methods=['POST'])
|
|
@require_api_key
|
|
def api_extract_and_embed():
|
|
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)
|
|
|
|
embedding = compute_embedding_from_frames(output_dir)
|
|
|
|
logger.info(
|
|
f"Extract & embed berhasil untuk user {user_id}. "
|
|
f"Frames: {result['total_extracted']}, "
|
|
f"Embedding dim: {len(embedding)}"
|
|
)
|
|
|
|
return jsonify({
|
|
"status": "success",
|
|
"message": f"Berhasil mengekstrak {result['total_extracted']} frame dan menghitung embedding",
|
|
"embedding": embedding,
|
|
**result
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Extract & embed 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('/get-embedding', methods=['POST'])
|
|
@require_api_key
|
|
def api_get_embedding():
|
|
if 'file' not in request.files:
|
|
return jsonify({"status": "error", "message": "File tidak ditemukan."}), 400
|
|
|
|
is_video = request.form.get('is_video', 'false').lower() == 'true'
|
|
|
|
uploaded_file = request.files['file']
|
|
ext = 'mp4' if is_video else 'jpg'
|
|
temp_file = os.path.join(TEMP_DIR, f"embed_{uuid.uuid4().hex[:8]}.{ext}")
|
|
|
|
try:
|
|
uploaded_file.save(temp_file)
|
|
|
|
if is_video:
|
|
embedding, blur_score = compute_embedding_from_video(temp_file, target_frames=10)
|
|
else:
|
|
embedding, blur_score = compute_embedding_from_image(temp_file)
|
|
|
|
logger.info(
|
|
f"Get embedding berhasil. "
|
|
f"Dim: {len(embedding)}, blur: {round(blur_score, 1)}"
|
|
)
|
|
|
|
return jsonify({
|
|
"status": "success",
|
|
"embedding": embedding,
|
|
"blur_score": round(blur_score, 1),
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Get embedding GAGAL: {str(e)}")
|
|
return jsonify({"status": "error", "message": str(e)}), 500
|
|
|
|
finally:
|
|
if os.path.exists(temp_file):
|
|
os.remove(temp_file)
|
|
|
|
|
|
@app.route('/list-frames/<user_id>', methods=['GET'])
|
|
@require_api_key
|
|
def api_list_frames(user_id):
|
|
user_dir = os.path.join(DATASETS_DIR, str(user_id))
|
|
|
|
if not os.path.exists(user_dir):
|
|
return jsonify({
|
|
"status": "success",
|
|
"user_id": user_id,
|
|
"total_frames": 0,
|
|
"frames": []
|
|
})
|
|
|
|
frames = sorted([
|
|
f for f in os.listdir(user_dir)
|
|
if f.startswith('frame_') and f.endswith('.jpg')
|
|
])
|
|
|
|
return jsonify({
|
|
"status": "success",
|
|
"user_id": user_id,
|
|
"total_frames": len(frames),
|
|
"frames": frames
|
|
})
|
|
|
|
|
|
@app.route('/get-frame/<user_id>/<frame_index>', methods=['GET'])
|
|
@require_api_key
|
|
def api_get_frame(user_id, frame_index):
|
|
idx = str(frame_index).zfill(3)
|
|
|
|
raw_path = os.path.join(DATASETS_DIR, str(user_id), f"raw_frame_{idx}.jpg")
|
|
frame_path = os.path.join(DATASETS_DIR, str(user_id), f"frame_{idx}.jpg")
|
|
|
|
if os.path.exists(raw_path):
|
|
return send_file(raw_path, mimetype='image/jpeg')
|
|
elif os.path.exists(frame_path):
|
|
return send_file(frame_path, mimetype='image/jpeg')
|
|
|
|
return jsonify({"status": "error", "message": "Frame tidak ditemukan."}), 404
|
|
|
|
|
|
@app.route('/delete-frames/<user_id>', methods=['DELETE'])
|
|
@require_api_key
|
|
def api_delete_frames(user_id):
|
|
user_dir = os.path.join(DATASETS_DIR, str(user_id))
|
|
|
|
if not os.path.exists(user_dir):
|
|
return jsonify({
|
|
"status": "success",
|
|
"message": f"Folder user {user_id} tidak ditemukan (sudah bersih).",
|
|
"deleted": 0
|
|
})
|
|
|
|
import shutil
|
|
shutil.rmtree(user_dir)
|
|
|
|
logger.info(f"Deleted frames for user {user_id}: {user_dir}")
|
|
|
|
return jsonify({
|
|
"status": "success",
|
|
"message": f"Data frame user {user_id} berhasil dihapus.",
|
|
"user_id": user_id
|
|
})
|
|
|
|
|
|
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)
|