Skip to content
Merged

pull #11

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ JWT_SECRET=
# ─── AI / PDF Processing ────────────────────────────────────────────────────
# Anthropic API key for Claude-powered PDF parsing
# Get from: https://console.anthropic.com → API Keys
ANTHROPIC_API_KEY=
ANTHROPIC_API_KEY=

# Google Gemini API key for free multimodal PDF parsing
# Get from: https://aistudio.google.com/
GEMINI_API_KEY=
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

A full-stack, responsive GPA Calculator and Student Portal built using **Next.js 14 (App Router & TypeScript)**, **Tailwind CSS**, **shadcn/ui**, and **Neon PostgreSQL with Drizzle ORM**. It features an automated OCR pipeline for parsing results sheets, student management, and smart recommendations.

---
---

## 🚀 Features
Expand Down
95 changes: 94 additions & 1 deletion app/api/admin/results/analyze/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,100 @@ export async function POST(request: NextRequest) {
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);

// Extract text digitally using pdf-parse
// ── 1. Try Google Gemini API (Multimodal Vision OCR) if Configured ─────────
if (process.env.GEMINI_API_KEY) {
try {
console.log("Attempting Gemini API PDF parsing...");
const base64Data = buffer.toString("base64");

const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${process.env.GEMINI_API_KEY}`;
Comment on lines +37 to +40

const payload = {
contents: [
{
parts: [
{
inlineData: {
mimeType: "application/pdf",
data: base64Data,
},
},
{
text: "Analyze this university result sheet PDF. Extract the subject code, subject name, semester description, and all student index numbers with their corresponding grades. Make sure to capture every single student result listed in the table or sheet. Ensure index numbers (e.g. 22CIS0123) and grades (e.g. A+, A, A-, B+, B, B-, C+, C, C-, D+, D, E, AB) are extracted with 100% accuracy. If you see a grade like 'B-' or 'A-', make sure you extract the '-' symbol and do not shorten it to 'B' or 'A'. Return a JSON object matching the schema.",
},
],
},
],
generationConfig: {
responseMimeType: "application/json",
responseSchema: {
type: "OBJECT",
properties: {
subjectCodeFromHeader: { type: "STRING" },
subjectNameFromHeader: { type: "STRING" },
semesterFromHeader: { type: "STRING" },
results: {
type: "ARRAY",
items: {
type: "OBJECT",
properties: {
indexNumber: { type: "STRING" },
grade: { type: "STRING" },
},
required: ["indexNumber", "grade"],
},
},
},
required: ["results"],
},
},
};

const geminiRes = await fetch(geminiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});

if (geminiRes.ok) {
const geminiData = await geminiRes.json();
const textResponse = geminiData.candidates?.[0]?.content?.parts?.[0]?.text;
if (textResponse) {
const parsedData = JSON.parse(textResponse);
// Standardize grades to uppercase and validate them
const validGrades = new Set([
"A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D+", "D", "E", "AB"
]);
const filteredResults = (parsedData.results || [])
.map((r: any) => ({
indexNumber: String(r.indexNumber).trim().toUpperCase(),
grade: String(r.grade).trim().toUpperCase(),
}))
.filter((r: any) => r.indexNumber && validGrades.has(r.grade));
Comment on lines +98 to +103

console.log(`Gemini parsed successfully! Found ${filteredResults.length} results.`);
return NextResponse.json({
success: true,
method: "gemini",
data: {
subjectCodeFromHeader: parsedData.subjectCodeFromHeader || null,
subjectNameFromHeader: parsedData.subjectNameFromHeader || null,
semesterFromHeader: parsedData.semesterFromHeader || null,
results: filteredResults,
totalFound: filteredResults.length,
},
});
}
} else {
const errorText = await geminiRes.text();
console.error("Gemini API error response:", errorText);
}
} catch (geminiErr) {
console.error("Failed to run Gemini analysis:", geminiErr);
}
}

// ── 2. Fallback: Local Digital Text Extraction using pdf-parse ───────────
let text = "";
try {
const pdfData = await pdf(buffer);
Expand Down
4 changes: 2 additions & 2 deletions lib/hooks/usePDFOCR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ export function usePDFOCR() {

if (response.ok) {
const resData = await response.json();
if (resData.success && resData.method === "digital") {
if (resData.success && (resData.method === "digital" || resData.method === "gemini")) {
const parsed = resData.data;
setResult(parsed);
setProgress({
status: "done",
percent: 100,
currentPage: 1,
totalPages: 1,
statusMessage: `Success! Found ${parsed.totalFound} student results digitally.`,
statusMessage: `Success! Found ${parsed.totalFound} student results using ${resData.method} analysis.`,
});
skipOCR = true;
}
Expand Down