Skip to content
Merged
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
88 changes: 56 additions & 32 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

from fastapi import FastAPI, HTTPException, File, UploadFile
from fastapi import BackgroundTasks, FastAPI, HTTPException, File, UploadFile
from pydantic import BaseModel
import fitz # PyMuPDF
from pathlib import Path
Expand All @@ -12,11 +11,15 @@
from generate_image_embedding import generate_image_embedding
from fastapi.responses import FileResponse, JSONResponse
from generate_pptx import create_pptx
from generate_pptx import create_pptx
from starlette.background import BackgroundTask
import tempfile
import imagehash
from PIL import Image
import io
import uuid
from typing import Dict
import json

app = FastAPI()

Expand All @@ -26,22 +29,10 @@
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)


@app.post("/parse")
async def parse_pdf(file: UploadFile = File(...)):
"""
Endpoint to parse a PDF file uploaded via multipart/form-data.
Extracts images, generates captions and embeddings, and returns the data.
"""
temp_file_path = None
def process_pdf_to_file(job_id: str, pdf_path: str, filename: str):
try:
# Create temp file with delete=False to avoid Windows file locking issues
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
temp_file.write(await file.read())
temp_file_path = temp_file.name

print(f"DEBUG : Temporary PDF file created at: {temp_file_path}")
# Open the PDF file using PyMuPDF (now works on Windows since file is closed)
pdf_file = fitz.open(str(temp_file_path))
print(f"Processing job {job_id}")
pdf_file = fitz.open(str(pdf_path))
image_data = []
image_order = 1
seen_hashes = set()
Expand Down Expand Up @@ -88,29 +79,62 @@ async def parse_pdf(file: UploadFile = File(...)):

# Prepare the response data
response_data = {
"name": file.filename,
"name": filename,
"details": f"Extracted {len(image_data)} images from the PDF.",
"images": image_data,
"text": extracted_text,
}

return JSONResponse(content=response_data)
temp_dir = tempfile.gettempdir()
result_path = os.path.join(temp_dir, f"{job_id}.json")
with open(result_path, "w") as f:
json.dump(response_data, f)

except Exception as e:
print(f"Error processing PDF: {e}")
raise HTTPException(
status_code=500, detail=f"An error occurred while processing the PDF: {e}"
)
print(f"Error in processing pdf job_id: {job_id}: {e}")

finally:
# Clean up temporary file on Windows
if temp_file_path and os.path.exists(temp_file_path):
try:
os.unlink(temp_file_path)
print(f"DEBUG: Cleaned up temporary file: {temp_file_path}")
except Exception as cleanup_error:
print(
f"Warning: Failed to clean up temporary file {temp_file_path}: {cleanup_error}"
)
try:
if os.path.exists(pdf_path):
os.remove(pdf_path)
except Exception as cleanup_err:
print(f"Warning: Failed to remove temporary PDF {pdf_path}: {cleanup_err}")


@app.post("/upload")
async def upload_file(
file: UploadFile = File(...), background_tasks: BackgroundTasks = None
):
try:
# Generate job ID
job_id = str(uuid.uuid4())
tmp_dir = tempfile.gettempdir()
tmp_path = os.path.join(tmp_dir, f"{job_id}_{file.filename}")

# Save uploaded file to /tmp
with open(tmp_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)

# Schedule background PDF processing
background_tasks.add_task(process_pdf_to_file, job_id, tmp_path, file.filename)

return {"jobID": job_id}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error uploading file: {e}")


@app.get("/result/{job_id}")
def get_result(job_id: str):
temp_dir = tempfile.gettempdir()
result_path = os.path.join(temp_dir, f"{job_id}.json")
if not os.path.exists(result_path):
return JSONResponse(
status_code=202, content={"message": "PDF processing not complete yet."}
)

with open(result_path, "r") as f:
result = json.load(f)
return result


class PPTXRequest(BaseModel):
Expand Down
70 changes: 55 additions & 15 deletions frontend/src/app/api/slide/content-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { createOllama } from 'ollama-ai-provider'
import { type CoreMessage, generateText } from 'ai'
import type { ClientSource } from '@/lib/types/client-source'
import type { CourseInfo } from '@/lib/types/course-info-types'
import type {
LectureContent,
AssessmentQuestion,
Expand Down Expand Up @@ -42,6 +43,7 @@ export async function generateCourseContent(
sessionLength: number,
difficultyLevel: string,
topicName: string,
courseInfo?: CourseInfo,
): Promise<LectureContent> {
try {
// Check for required environment variables
Expand All @@ -55,8 +57,11 @@ export async function generateCourseContent(

// Prepare source content
console.log('Preparing source content...')
const { content: assistantContent, metadata: sourceMetadata } =
await prepareSourceContent(selectedSources)
const { content: assistantContent, metadata: sourceMetadata } = await prepareSourceContent(
selectedSources,
topicName,
courseInfo,
)

// Ensure assistant content fits within context window
const assistantMessage: CoreMessage = {
Expand Down Expand Up @@ -112,7 +117,9 @@ export async function generateCourseContent(

const metadataUserMessage: CoreMessage = {
role: 'user',
content: `Generate the title, learning outcomes, and at least 5-10 key terms for a ${difficultyLevel} level ${contentType} on "${topicName}" based STRICTLY on the provided source materials above.`,
content: sourceMetadata.usingCourseContext
? `Generate the title, learning outcomes, and at least 5-10 key terms for a ${difficultyLevel} level ${contentType} on "${topicName}" based on standard academic knowledge and best practices for this subject area.`
: `Generate the title, learning outcomes, and at least 5-10 key terms for a ${difficultyLevel} level ${contentType} on "${topicName}" based STRICTLY on the provided source materials above.`,
}

const metadataMessages = [metadataSystemMessage, assistantMessage, metadataUserMessage]
Expand All @@ -137,10 +144,17 @@ export async function generateCourseContent(
const introSystemPrompt = `You are an expert educational content developer. Continue creating a ${difficultyLevel} level ${contentType} on "${topicName}" designed for a ${sessionLength}-minute session.

IMPORTANT INSTRUCTIONS:
1. You MUST base your content ENTIRELY on the source materials provided.
${
sourceMetadata.usingCourseContext
? `1. Since no specific source materials were provided, base your content on standard academic knowledge for the topic.
2. Draw from established educational practices and common curriculum content for this subject area.
3. Create content appropriate for the specified difficulty level and session length.
4. Ensure the introduction provides context and importance of the topic based on general knowledge.`
: `1. You MUST base your content ENTIRELY on the source materials provided.
2. Extract key concepts, terminology, examples, and explanations directly from the source materials.
3. Do not introduce concepts or information that is not present in the source materials.
4. Create an engaging introduction that provides context and importance of the topic.
4. Create an engaging introduction that provides context and importance of the topic.`
}

RESPONSE FORMAT:
Your response MUST be a valid JSON object with EXACTLY these fields:
Expand All @@ -157,7 +171,9 @@ CRITICAL: Your response MUST be valid JSON only. Do not include any text, markdo

const introUserMessage: CoreMessage = {
role: 'user',
content: `Generate an engaging introduction for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based STRICTLY on the provided source materials above.`,
content: sourceMetadata.usingCourseContext
? `Generate an engaging introduction for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based on standard academic knowledge and best practices for this subject area.`
: `Generate an engaging introduction for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based STRICTLY on the provided source materials above.`,
}

const introMessages = [introSystemMessage, assistantMessage, introUserMessage]
Expand All @@ -179,9 +195,15 @@ CRITICAL: Your response MUST be valid JSON only. Do not include any text, markdo
const specialSlidesSystemPrompt = `You are an expert educational content developer. Continue creating a ${difficultyLevel} level ${contentType} on "${topicName}" designed for a ${sessionLength}-minute session.

IMPORTANT INSTRUCTIONS:
1. You MUST base your content ENTIRELY on the source materials provided.
${
sourceMetadata.usingCourseContext
? `1. Since no specific source materials were provided, base your content on standard academic knowledge for the topic.
2. Draw from established educational practices and common curriculum content for this subject area.
3. Create content appropriate for the specified difficulty level and session length.`
: `1. You MUST base your content ENTIRELY on the source materials provided.
2. Extract key concepts, terminology, examples, and explanations directly from the source materials.
3. Do not introduce concepts or information that is not present in the source materials.
3. Do not introduce concepts or information that is not present in the source materials.`
}
4. Create ONLY the following special slides:
- Introduction slide (first slide that introduces the topic)
- Agenda/Overview slide (outlines what will be covered)
Expand Down Expand Up @@ -228,7 +250,9 @@ CRITICAL: Your response MUST be valid JSON only. Do not include any text, markdo

const specialSlidesUserMessage: CoreMessage = {
role: 'user',
content: `Generate the introduction, agenda, assessment, and conclusion slides for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based STRICTLY on the provided source materials above.`,
content: sourceMetadata.usingCourseContext
? `Generate the introduction, agenda, assessment, and conclusion slides for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based on standard academic knowledge and best practices for this subject area.`
: `Generate the introduction, agenda, assessment, and conclusion slides for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based STRICTLY on the provided source materials above.`,
}

const specialSlidesMessages = [
Expand Down Expand Up @@ -291,9 +315,15 @@ CRITICAL: Your response MUST be valid JSON only. Do not include any text, markdo
const contentSlidesSystemPrompt = `You are generating content slides ${startSlideNum} through ${endSlideNum} of a total of ${totalContentSlidesNeeded} content slides. Ensure all slides are unique.

IMPORTANT INSTRUCTIONS:
1. You MUST base your content ENTIRELY on the source materials provided.
${
sourceMetadata.usingCourseContext
? `1. Since no specific source materials were provided, base your content on standard academic knowledge for the topic.
2. Draw from established educational practices and common curriculum content for this subject area.
3. Create content appropriate for the specified difficulty level and session length.`
: `1. You MUST base your content ENTIRELY on the source materials provided.
2. Extract key concepts, terminology, examples, and explanations directly from the source materials.
3. Do not introduce concepts or information that is not present in the source materials.
3. Do not introduce concepts or information that is not present in the source materials.`
}
4. Create detailed teaching slides with substantial content on each slide.
5. Focus ONLY on core teaching content slides.
6. Each slide should have comprehensive speaker notes with additional details and examples.
Expand Down Expand Up @@ -324,7 +354,11 @@ CRITICAL: Your response MUST be valid JSON only. Do not include any text, markdo

const contentSlidesUserMessage: CoreMessage = {
role: 'user',
content: `Generate content slides ${startSlideNum} through ${endSlideNum} for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based STRICTLY on the provided source materials above.
content: sourceMetadata.usingCourseContext
? `Generate content slides ${startSlideNum} through ${endSlideNum} for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based on standard academic knowledge and best practices for this subject area.

DO NOT create introduction, agenda, assessment, or conclusion slides. Focus ONLY on core teaching content slides.`
: `Generate content slides ${startSlideNum} through ${endSlideNum} for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based STRICTLY on the provided source materials above.

DO NOT create introduction, agenda, assessment, or conclusion slides. Focus ONLY on core teaching content slides.`,
}
Expand Down Expand Up @@ -381,7 +415,9 @@ DO NOT create introduction, agenda, assessment, or conclusion slides. Focus ONLY

const activitiesUserMessage: CoreMessage = {
role: 'user',
content: `Generate the activities for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based STRICTLY on the provided source materials above.`,
content: sourceMetadata.usingCourseContext
? `Generate the activities for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based on standard academic knowledge and best practices for this subject area.`
: `Generate the activities for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based STRICTLY on the provided source materials above.`,
}

const activitiesMessages = [activitiesSystemMessage, assistantMessage, activitiesUserMessage]
Expand Down Expand Up @@ -417,7 +453,9 @@ DO NOT create introduction, agenda, assessment, or conclusion slides. Focus ONLY

const assessmentUserMessage: CoreMessage = {
role: 'user',
content: `Generate assessment ideas (without example questions) for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based STRICTLY on the provided source materials above.`,
content: sourceMetadata.usingCourseContext
? `Generate assessment ideas (without example questions) for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based on standard academic knowledge and best practices for this subject area.`
: `Generate assessment ideas (without example questions) for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based STRICTLY on the provided source materials above.`,
}

const assessmentMessages = [assessmentSystemMessage, assistantMessage, assessmentUserMessage]
Expand Down Expand Up @@ -479,7 +517,9 @@ DO NOT create introduction, agenda, assessment, or conclusion slides. Focus ONLY

const readingsUserMessage: CoreMessage = {
role: 'user',
content: `Generate further reading suggestions for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based STRICTLY on the provided source materials above.`,
content: sourceMetadata.usingCourseContext
? `Generate further reading suggestions for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based on standard academic knowledge and best practices for this subject area.`
: `Generate further reading suggestions for a ${difficultyLevel} level ${contentType} on "${topicName}" with title "${metadataResponse.title}" based STRICTLY on the provided source materials above.`,
}

const readingsMessages = [readingsSystemMessage, assistantMessage, readingsUserMessage]
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/app/api/slide/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export async function POST(req: Request) {
sessionLength,
difficultyLevel,
topicName,
courseInfo,
} = requestData

console.log('Data from request:', {
Expand All @@ -149,6 +150,7 @@ export async function POST(req: Request) {
sessionLength,
difficultyLevel,
topicName,
courseInfo,
})

// Generate course content
Expand All @@ -160,6 +162,7 @@ export async function POST(req: Request) {
sessionLength,
difficultyLevel,
topicName,
courseInfo,
)

return NextResponse.json(generatedContent)
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/api/slide/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// Type definitions for course content generation

import { ClientSource } from '@/lib/types/client-source'
import type { CourseInfo } from '@/lib/types/course-info-types'

export interface LectureSlide {
title: string
Expand Down Expand Up @@ -120,6 +121,7 @@ export interface CourseContentRequest {
sessionLength: number
difficultyLevel: string
topicName: string
courseInfo?: CourseInfo
action?: string
content?: LectureContent
}
Loading