182 lines
5.3 KiB
Python
182 lines
5.3 KiB
Python
from pathlib import Path
|
|
import tempfile
|
|
|
|
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel, Field
|
|
|
|
from audio_utils_api import SUPPORTED_AUDIO_EXTENSIONS, cleanup_temp_files
|
|
from database import (
|
|
authenticate_user,
|
|
create_user,
|
|
get_database_label,
|
|
init_db,
|
|
list_prediction_results,
|
|
save_prediction_result,
|
|
)
|
|
from predict_api import MODEL_NOT_FOUND_MESSAGE, get_model_info, predict_audio
|
|
|
|
|
|
app = FastAPI(title="ConfiVoice Prediction API")
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
class PredictionSaveRequest(BaseModel):
|
|
user_id: int | None = None
|
|
student_name: str
|
|
student_gender: str | None = None
|
|
predicted_label: str | None = None
|
|
label: str | None = None
|
|
description: str | None = None
|
|
confidence: float = 0
|
|
probability_pd: float = 0
|
|
probability_tpd: float = 0
|
|
is_valid_audio: bool = True
|
|
error_message: str | None = None
|
|
audio_quality: dict = Field(default_factory=dict)
|
|
voice_indicators: dict = Field(default_factory=dict)
|
|
|
|
|
|
class AuthRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
|
|
class RegisterRequest(AuthRequest):
|
|
full_name: str
|
|
|
|
|
|
@app.on_event("startup")
|
|
def startup():
|
|
init_db()
|
|
|
|
|
|
@app.get("/")
|
|
def read_root():
|
|
return {
|
|
"name": "ConfiVoice Prediction API",
|
|
"status": "ok",
|
|
"predict_endpoint": "/predict",
|
|
"predictions_endpoint": "/predictions",
|
|
"database": get_database_label(),
|
|
}
|
|
|
|
|
|
@app.get("/model")
|
|
def read_model_info():
|
|
try:
|
|
return get_model_info()
|
|
except FileNotFoundError as error:
|
|
raise HTTPException(status_code=404, detail=MODEL_NOT_FOUND_MESSAGE) from error
|
|
except Exception as error:
|
|
raise HTTPException(status_code=500, detail=str(error)) from error
|
|
|
|
|
|
@app.get("/predictions")
|
|
def read_predictions(limit: int = 200, user_id: int | None = None):
|
|
return {
|
|
"database": get_database_label(),
|
|
"data": list_prediction_results(limit=limit, user_id=user_id),
|
|
}
|
|
|
|
|
|
@app.post("/register")
|
|
def register(payload: RegisterRequest):
|
|
full_name = " ".join(payload.full_name.strip().split())
|
|
username = payload.username.strip().lower()
|
|
password = payload.password
|
|
|
|
if not full_name:
|
|
raise HTTPException(status_code=400, detail="Nama lengkap wajib diisi.")
|
|
if len(username) < 3:
|
|
raise HTTPException(status_code=400, detail="Username minimal 3 karakter.")
|
|
if len(password) < 6:
|
|
raise HTTPException(status_code=400, detail="Password minimal 6 karakter.")
|
|
|
|
try:
|
|
user = create_user(full_name, username, password)
|
|
except Exception as error:
|
|
message = str(error).lower()
|
|
if "duplicate" in message or "unique" in message:
|
|
raise HTTPException(status_code=409, detail="Username sudah terdaftar.") from error
|
|
raise HTTPException(status_code=500, detail=str(error)) from error
|
|
|
|
return {"status": "registered", "user": user}
|
|
|
|
|
|
@app.post("/login")
|
|
def login(payload: AuthRequest):
|
|
username = payload.username.strip().lower()
|
|
user = authenticate_user(username, payload.password)
|
|
if not user:
|
|
raise HTTPException(status_code=401, detail="Username atau password salah.")
|
|
return {"status": "logged_in", "user": user}
|
|
|
|
|
|
@app.post("/predictions")
|
|
def create_prediction(payload: PredictionSaveRequest):
|
|
student_name = payload.student_name.strip()
|
|
if not student_name:
|
|
raise HTTPException(status_code=400, detail="Nama siswa/i wajib diisi.")
|
|
|
|
result = payload.model_dump()
|
|
prediction_id = save_prediction_result(student_name, result)
|
|
return {
|
|
"status": "saved",
|
|
"prediction_id": prediction_id,
|
|
"student_name": student_name,
|
|
}
|
|
|
|
|
|
@app.post("/save")
|
|
def save_prediction(payload: PredictionSaveRequest):
|
|
return create_prediction(payload)
|
|
|
|
|
|
@app.post("/predict")
|
|
async def predict(
|
|
file: UploadFile = File(...),
|
|
student_name: str = Form("Tanpa Nama"),
|
|
student_gender: str = Form(""),
|
|
):
|
|
suffix = Path(file.filename or "").suffix.lower()
|
|
if suffix not in SUPPORTED_AUDIO_EXTENSIONS:
|
|
allowed = ", ".join(sorted(SUPPORTED_AUDIO_EXTENSIONS))
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Format audio tidak didukung. Format yang didukung: {allowed}",
|
|
)
|
|
|
|
temp_input = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
|
|
temp_input_path = Path(temp_input.name)
|
|
|
|
try:
|
|
content = await file.read()
|
|
temp_input.write(content)
|
|
temp_input.close()
|
|
|
|
if not content:
|
|
raise HTTPException(status_code=400, detail="File audio kosong.")
|
|
|
|
clean_student_name = student_name.strip() or "Tanpa Nama"
|
|
result = predict_audio(temp_input_path)
|
|
result["student_name"] = clean_student_name
|
|
result["student_gender"] = student_gender.strip() or None
|
|
return result
|
|
except HTTPException:
|
|
raise
|
|
except FileNotFoundError as error:
|
|
raise HTTPException(status_code=404, detail=MODEL_NOT_FOUND_MESSAGE) from error
|
|
except Exception as error:
|
|
raise HTTPException(status_code=500, detail=str(error)) from error
|
|
finally:
|
|
temp_input.close()
|
|
cleanup_temp_files(temp_input_path)
|