from html import escape
from pathlib import Path
import sys
from fastapi import FastAPI, Form, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
BASE_DIR = Path(__file__).resolve().parent
PROJECT_DIR = BASE_DIR.parent
ML_DIR = PROJECT_DIR / "ml"
if str(ML_DIR) not in sys.path:
sys.path.append(str(ML_DIR))
from database import ( # noqa: E402
delete_prediction_result,
get_database_label,
get_prediction_result,
init_db,
list_prediction_results,
save_prediction_result,
update_prediction_result,
)
app = FastAPI(title="ConfiVoice Admin Web")
@app.on_event("startup")
def startup():
init_db()
@app.get("/")
def read_root():
return {
"name": "ConfiVoice Admin Web",
"status": "ok",
"admin_page": "/admin",
"database": get_database_label(),
}
@app.get("/predictions")
def read_predictions(limit: int = 500):
return {
"database": get_database_label(),
"data": list_prediction_results(limit=limit),
}
@app.post("/admin/predictions")
def create_prediction_from_admin(
student_name: str = Form(...),
predicted_label: str = Form("PD"),
description: str = Form(""),
confidence_percent: float = Form(0),
probability_pd_percent: float = Form(0),
probability_tpd_percent: float = Form(0),
is_valid_audio: str = Form("1"),
error_message: str = Form(""),
audio_duration: float = Form(0),
):
student_name = student_name.strip()
if not student_name:
raise HTTPException(status_code=400, detail="Nama siswa/i wajib diisi.")
save_prediction_result(
student_name,
_result_payload(
predicted_label=predicted_label,
description=description,
confidence_percent=confidence_percent,
probability_pd_percent=probability_pd_percent,
probability_tpd_percent=probability_tpd_percent,
is_valid_audio=is_valid_audio,
error_message=error_message,
audio_duration=audio_duration,
),
)
return _redirect_admin()
@app.get("/admin/predictions/{prediction_id}/edit", response_class=HTMLResponse)
def read_edit_prediction_page(prediction_id: int):
row = get_prediction_result(prediction_id)
if row is None:
raise HTTPException(status_code=404, detail="Data tidak ditemukan.")
return _html_page(
title=f"Edit Data #{prediction_id}",
content=f"""
Edit Data #{prediction_id}
Ubah hasil analisis yang tersimpan di admin.
Kembali
{_prediction_form(row=row, action=f"/admin/predictions/{prediction_id}/edit", submit_label="Simpan Perubahan")}
""",
)
@app.post("/admin/predictions/{prediction_id}/edit")
def update_prediction_from_admin(
prediction_id: int,
student_name: str = Form(...),
predicted_label: str = Form("PD"),
description: str = Form(""),
confidence_percent: float = Form(0),
probability_pd_percent: float = Form(0),
probability_tpd_percent: float = Form(0),
is_valid_audio: str = Form("1"),
error_message: str = Form(""),
audio_duration: float = Form(0),
):
student_name = student_name.strip()
if not student_name:
raise HTTPException(status_code=400, detail="Nama siswa/i wajib diisi.")
updated = update_prediction_result(
prediction_id,
_db_values(
student_name=student_name,
predicted_label=predicted_label,
description=description,
confidence_percent=confidence_percent,
probability_pd_percent=probability_pd_percent,
probability_tpd_percent=probability_tpd_percent,
is_valid_audio=is_valid_audio,
error_message=error_message,
audio_duration=audio_duration,
),
)
if not updated:
raise HTTPException(status_code=404, detail="Data tidak ditemukan.")
return _redirect_admin()
@app.post("/admin/predictions/{prediction_id}/delete")
def delete_prediction_from_admin(prediction_id: int):
delete_prediction_result(prediction_id)
return _redirect_admin()
@app.get("/admin", response_class=HTMLResponse)
def read_admin_page():
rows = list_prediction_results(limit=500)
total = len(rows)
pd_count = sum(1 for row in rows if row["predicted_label"] == "PD")
tpd_count = sum(1 for row in rows if row["predicted_label"] == "TPD")
avg_pd = sum(float(row["probability_pd"] or 0) for row in rows) / total if total else 0
return _html_page(
title="Admin ConfiVoice",
content=f"""
Total Data{total}
Percaya Diri{pd_count}
Tidak Percaya Diri{tpd_count}
Rata-rata Percaya Diri{avg_pd * 100:.1f}%
Tambah Data
Gunakan form ini jika perlu menambahkan hasil analisis secara manual.
{_prediction_form(action="/admin/predictions", submit_label="Tambah Data")}
Data Analisis
Kelola data yang sudah tersimpan.
{_prediction_table(rows)}
""",
)
def _redirect_admin():
return RedirectResponse(url="/admin", status_code=303)
def _result_payload(
*,
predicted_label,
description,
confidence_percent,
probability_pd_percent,
probability_tpd_percent,
is_valid_audio,
error_message,
audio_duration,
):
return {
"predicted_label": predicted_label,
"description": description,
"confidence": _percent_to_ratio(confidence_percent),
"probability_pd": _percent_to_ratio(probability_pd_percent),
"probability_tpd": _percent_to_ratio(probability_tpd_percent),
"is_valid_audio": is_valid_audio == "1",
"error_message": error_message.strip() or None,
"audio_quality": {"duration": audio_duration},
"voice_indicators": {},
}
def _db_values(
*,
student_name,
predicted_label,
description,
confidence_percent,
probability_pd_percent,
probability_tpd_percent,
is_valid_audio,
error_message,
audio_duration,
):
return (
student_name,
predicted_label.strip() or None,
description.strip() or None,
_percent_to_ratio(confidence_percent),
_percent_to_ratio(probability_pd_percent),
_percent_to_ratio(probability_tpd_percent),
1 if is_valid_audio == "1" else 0,
error_message.strip() or None,
float(audio_duration or 0),
0,
0,
0,
0,
0,
)
def _percent_to_ratio(value):
return max(0, min(float(value or 0), 100)) / 100
def _ratio_to_percent(value):
return f"{float(value or 0) * 100:.2f}"
def _prediction_form(row=None, action="/admin/predictions", submit_label="Simpan"):
row = row or {}
student_name = escape(str(row.get("student_name") or ""))
predicted_label = str(row.get("predicted_label") or "PD")
description = escape(str(row.get("description") or ""))
confidence = _ratio_to_percent(row.get("confidence"))
probability_pd = _ratio_to_percent(row.get("probability_pd"))
probability_tpd = _ratio_to_percent(row.get("probability_tpd"))
audio_duration = float(row.get("audio_duration") or 0)
error_message = escape(str(row.get("error_message") or ""))
valid_audio = str(int(row.get("is_valid_audio", 1))) == "1"
pd_selected = "selected" if predicted_label == "PD" else ""
tpd_selected = "selected" if predicted_label == "TPD" else ""
valid_selected = "selected" if valid_audio else ""
invalid_selected = "selected" if not valid_audio else ""
return f"""
"""
def _prediction_table(rows):
table_rows = []
for row in rows:
valid_text = "Valid" if row["is_valid_audio"] else "Tidak valid"
table_rows.append(
""
f"| {row['id']} | "
f"{escape(str(row['created_at']))} | "
f"{escape(row['student_name'])} | "
f"{escape(row['predicted_label'] or '-')} | "
f"{escape(row['description'] or '-')} | "
f"{float(row['confidence'] or 0) * 100:.2f}% | "
f"{float(row['probability_pd'] or 0) * 100:.2f}% | "
f"{float(row['probability_tpd'] or 0) * 100:.2f}% | "
f"{float(row['audio_duration'] or 0):.2f} dtk | "
f"{valid_text} | "
""
f"Edit"
f""
" | "
"
"
)
body = "\n".join(table_rows) or (
'| Belum ada hasil prediksi. |
'
)
return f"""
| ID |
Waktu |
Nama siswa/i |
Label |
Keterangan |
Confidence |
PD |
TPD |
Durasi |
Status audio |
Aksi |
{body}
"""
def _html_page(title, content):
return f"""
{escape(title)}
{content}
"""