aboalaa147's picture
Initial deployment
eb6a2f9
Raw
History Blame Contribute Delete
6.33 kB
// ─────────────────────────────────────────────────────────────────────────────
// 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<boolean> {
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<AiAnalysisResponse> {
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<AiAnalysisResponse>;
}
// ─── 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<RecitationDetailDto | null> {
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;
}