// ───────────────────────────────────────────────────────────────────────────── // src/lib/api/aiApi.ts // Direct integration with the Quran AI analysis service. // ───────────────────────────────────────────────────────────────────────────── const AI_BASE_URL = 'https://johanne-totipotent-gaylene.ngrok-free.dev'; // ─── AI Response Types ──────────────────────────────────────────────────────── export interface TashkeelCompare { true_char_tashkeel: string; false_char_tashkeel: string; reason: string; } export interface TashkeelStageItem { true_word_tashkeel: string; false_word_tashkeel: string; tashkeel_compare: TashkeelCompare[]; } export interface AyahResult { ayah_number: number; ayah_text: string; words_stage: { extra_words: string[]; missed_words: string[]; }; tashkeel_stage: TashkeelStageItem[]; } export interface AiAnalysisResponse { surah_info: { surah_number: number; surah_name: string; start_ayah: number; end_ayah: number; }; overall_stats: { word_accuracy_percentage: number; tashkeel_accuracy_percentage: number; }; model_used: string; user_text: string; quran_text: string; ayahs: AyahResult[]; } // ─── Health Check ───────────────────────────────────────────────────────────── export async function checkAiHealth(): Promise { try { const res = await fetch(`${AI_BASE_URL}/`, { method: 'GET', headers: { 'ngrok-skip-browser-warning': 'true' }, }); return res.ok; } catch { return false; } } // ─── Analyze Recitation ─────────────────────────────────────────────────────── export async function analyzeRecitation( audio: Blob | File, surahNumber: number, ): Promise { const form = new FormData(); // Determine filename/extension from mime type const mime = audio instanceof File ? audio.type : audio.type; const ext = mime.includes('wav') ? 'wav' : mime.includes('mpeg') || mime.includes('mp3') ? 'mp3' : mime.includes('m4a') ? 'm4a' : 'webm'; const filename = audio instanceof File ? audio.name : `recording.${ext}`; form.append('audio', audio, filename); form.append('surah_number', String(surahNumber)); const res = await fetch(`${AI_BASE_URL}/analyze`, { method: 'POST', headers: { 'ngrok-skip-browser-warning': 'true' }, body: form, }); if (!res.ok) { const msg = await res.text().catch(() => res.statusText); throw new Error(`AI analysis failed (${res.status}): ${msg}`); } return res.json() as Promise; } // ─── Map AI response → RecitationDetailDto ─────────────────────────────────── // Derives all display fields from the AI JSON so the UI doesn't need to change. import type { RecitationDetailDto } from './studentApi'; export function mapAiResponseToDto( ai: AiAnalysisResponse, recitationId = 0, ): RecitationDetailDto { // Collect incorrect (missed) words across all ayahs const incorrectWords: string[] = ai.ayahs.flatMap((a) => a.words_stage.missed_words); // Collect tashkeel mistakes as human-readable strings const tashkeelMistakes: string[] = ai.ayahs.flatMap((a) => a.tashkeel_stage.flatMap((ts) => ts.tashkeel_compare.map( (tc) => `${tc.false_char_tashkeel} ← ${tc.true_char_tashkeel}: ${tc.reason}`, ), ), ); // Scores come directly from the model (already 0-100) const wordScore = ai.overall_stats.word_accuracy_percentage; const tashkeelScore = ai.overall_stats.tashkeel_accuracy_percentage; const overallScore = Math.round((wordScore + tashkeelScore) / 2 * 100) / 100; return { recitationId, surahName: ai.surah_info.surah_name, startAyah: ai.surah_info.start_ayah, endAyah: ai.surah_info.end_ayah, recitationText: ai.user_text, userText: ai.user_text, wordScore, tashkeelScore, totalScore: overallScore, overallScore, wordMistakesCount: incorrectWords.length, tashkeelMistakesCount: tashkeelMistakes.length, tajweedMistakesCount: 0, incorrectWords, tashkeelMistakes, tajweedMistakes: [], feedback: undefined, createdAt: new Date().toISOString(), }; } // ─── Save AI result to backend DB ──────────────────────────────────────────── // The backend POST /api/student/recitations only accepts multipart/form-data, // not JSON. We build a FormData payload and attach the auth token manually. import { getApiBaseUrl } from './config'; const BACKEND_BASE_URL = getApiBaseUrl(); export async function saveRecitationToBackend( ai: AiAnalysisResponse, ): Promise { const form = new FormData(); form.append('surahName', ai.surah_info.surah_name); form.append('surahNumber', String(ai.surah_info.surah_number)); form.append('ayahStart', String(ai.surah_info.start_ayah)); form.append('ayahEnd', String(ai.surah_info.end_ayah)); form.append('userText', ai.user_text); form.append('wordScore', String(ai.overall_stats.word_accuracy_percentage)); form.append('tashkeelScore', String(ai.overall_stats.tashkeel_accuracy_percentage)); const token = localStorage.getItem('authToken') ?? ''; const res = await fetch(`${BACKEND_BASE_URL}/api/student/recitations`, { method: 'POST', headers: token ? { Authorization: `Bearer ${token}` } : {}, body: form, }); if (!res.ok) return null; const text = await res.text(); if (!text) return null; return JSON.parse(text) as RecitationDetailDto; }