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

Admin ConfiVoice

Dashboard hasil analisis percaya diri siswa/i dari aplikasi Flutter.

Total data{total}
Percaya diri{pd_count}
Tidak ercaya 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"""
{body}
ID Waktu Nama siswa/i Label Keterangan Confidence PD TPD Durasi Status audio Aksi
""" def _html_page(title, content): return f""" {escape(title)} {content} """