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
2,516 changes: 41 additions & 2,475 deletions backend/generate_pptx.py

Large diffs are not rendered by default.

20 changes: 12 additions & 8 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def get_result(job_id: str):

class PPTXRequest(BaseModel):
content: dict
language: str | None = "en"


def validate_and_transform_content(content: dict) -> dict:
Expand All @@ -154,7 +155,7 @@ def validate_and_transform_content(content: dict) -> dict:
"""
# Ensure required keys exist with default values if missing
transformed_content = {
"title": content.get("title", "Untitled Presentation"),
"title": content.get("title", ""),
"contentType": content.get("contentType", "lecture"),
"difficultyLevel": content.get("difficultyLevel", "intermediate"),
"slides": content.get("slides", []),
Expand All @@ -167,13 +168,13 @@ def validate_and_transform_content(content: dict) -> dict:

# Validate slides structure
for slide in transformed_content["slides"]:
slide.setdefault("title", "Untitled Slide")
slide.setdefault("title", "")
slide.setdefault("content", [])
slide.setdefault("notes", "")

# Validate activities structure
for activity in transformed_content["activities"]:
activity.setdefault("title", "Untitled Activity")
activity.setdefault("title", "")
activity.setdefault("description", "")
activity.setdefault("type", "Exercise")
activity.setdefault("duration", "20 minutes")
Expand All @@ -192,13 +193,13 @@ def validate_and_transform_content(content: dict) -> dict:

# Validate key terms structure
for term in transformed_content["keyTerms"]:
term.setdefault("term", "Untitled Term")
term.setdefault("definition", "No definition provided.")
term.setdefault("term", "")
term.setdefault("definition", "")

# Validate further readings structure
for reading in transformed_content["furtherReadings"]:
reading.setdefault("title", "Untitled Reading")
reading.setdefault("author", "Unknown Author")
reading.setdefault("title", "")
reading.setdefault("author", "")
reading.setdefault("readingDescription", "")

return transformed_content
Expand All @@ -222,7 +223,10 @@ async def generate_pptx(request: PPTXRequest):
print(temp_pptx_path)

# Generate the PPTX file
create_pptx(transformed_content, temp_pptx_path)
lang = (request.language or "en").lower()
if lang not in ["en", "id"]:
lang = "en"
create_pptx(transformed_content, temp_pptx_path, lang)
print(f"Temporary PPTX file created at: {temp_pptx_path}")

if not os.path.exists(temp_pptx_path):
Expand Down
1 change: 1 addition & 0 deletions backend/pptx_builder/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# pptx_builder package initialization
64 changes: 64 additions & 0 deletions backend/pptx_builder/builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import os
import json
import tempfile
from pptx import Presentation
from pptx.util import Inches
from .constants import SLIDE_WIDTH, SLIDE_HEIGHT
from .localization import set_language, t
from .slide_counter import calculate_total_slides
from .sections import (
create_title_slide,
create_agenda_slide,
create_learning_outcomes_slide,
create_key_terms_slide,
create_content_slides,
create_activity_slides,
create_quiz_slides,
create_discussion_slides,
create_further_readings_slides,
create_facilitation_notes_slide,
create_closing_slide,
)


def build_full_presentation(content, language="en"):
set_language(language)
total_slides = calculate_total_slides(content)
prs = Presentation()
prs.slide_width = Inches(SLIDE_WIDTH)
prs.slide_height = Inches(SLIDE_HEIGHT)
create_title_slide(prs, content)
create_agenda_slide(prs, content, total_slides)
create_learning_outcomes_slide(prs, content, total_slides)
create_key_terms_slide(prs, content, total_slides)
create_content_slides(prs, content, total_slides)
create_activity_slides(prs, content, total_slides)
create_quiz_slides(prs, content, total_slides)
create_discussion_slides(prs, content, total_slides)
create_further_readings_slides(prs, content, total_slides)
facilitation_slide = create_facilitation_notes_slide(prs, content, total_slides)
if facilitation_slide:
total_slides += 1 # update for closing slide numbering if needed
create_closing_slide(prs, content, total_slides, total_slides)
return prs


def create_pptx(content: dict, output_path: str, language: str = "en"):
base_dir = os.path.dirname(os.path.abspath(__file__))
normalized_output_path = os.path.abspath(output_path)
allowed_output = os.path.abspath(os.path.join(base_dir, "..", "output"))
parent = os.path.dirname(normalized_output_path)
is_temp = parent.startswith(os.path.abspath(tempfile.gettempdir()))
is_out = normalized_output_path.startswith(allowed_output)
if not (is_temp or is_out):
raise ValueError(
"Security violation: Output path must be in allowed directories"
)
prs = build_full_presentation(content, language)
prs.save(normalized_output_path)


def cli_build(content_path, output_path, language="en"):
with open(content_path, "r") as f:
content = json.load(f)
create_pptx(content, output_path, language)
177 changes: 177 additions & 0 deletions backend/pptx_builder/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
from pptx.dml.color import RGBColor

# Dimensions & layout
SLIDE_WIDTH = 10 # inches
SLIDE_HEIGHT = 5.625 # inches
FOOTER_Y = 5.3
CONTENT_START_Y = 1.5
AVAILABLE_CONTENT_HEIGHT = FOOTER_Y - CONTENT_START_Y
MAIN_BULLET_INDENT = 0.5
SUB_BULLET_INDENT = 1.0
SUB_SUB_BULLET_INDENT = 1.5

THEME = {
"use_gradients": True,
"corner_accent": True,
"slide_border": False,
"content_box_shadow": True,
"modern_bullets": True,
"footer_style": "modern", # "modern" or "classic"
}

COLORS = {
"primary": RGBColor(37, 99, 235),
"primary_light": RGBColor(96, 165, 250),
"primary_dark": RGBColor(30, 64, 175),
"secondary": RGBColor(79, 70, 229),
"secondary_light": RGBColor(139, 92, 246),
"secondary_dark": RGBColor(67, 56, 202),
"accent1": RGBColor(139, 92, 246),
"accent2": RGBColor(16, 185, 129),
"accent3": RGBColor(245, 158, 11),
"accent4": RGBColor(239, 68, 68),
"light": RGBColor(243, 244, 246),
"light_alt": RGBColor(249, 250, 251),
"dark": RGBColor(31, 41, 55),
"dark_alt": RGBColor(17, 24, 39),
"text": RGBColor(17, 24, 39),
"text_light": RGBColor(255, 255, 255),
"text_muted": RGBColor(107, 114, 128),
"success": RGBColor(16, 185, 129),
"warning": RGBColor(245, 158, 11),
"error": RGBColor(239, 68, 68),
"info": RGBColor(59, 130, 246),
"background": RGBColor(255, 255, 255),
"background_alt": RGBColor(249, 250, 251),
"royal_blue": RGBColor(65, 105, 225),
"medium_purple": RGBColor(147, 112, 219),
"dark_blue": RGBColor(26, 43, 60),
"teal": RGBColor(20, 184, 166),
"emerald": RGBColor(16, 185, 129),
"gradient_start": RGBColor(65, 105, 225),
"gradient_end": RGBColor(147, 112, 219),
"activity_purple": RGBColor(139, 92, 246),
"activity_blue": RGBColor(37, 99, 235),
"activity_green": RGBColor(16, 185, 129),
"activity_orange": RGBColor(249, 115, 22),
}

GLOBAL_LANG = "en"

LABELS = {
"en": {
"learningOutcomes": "Learning Outcomes",
"byTheEnd": "By the end of this {contentType}, you will be able to:",
"keyTerms": "Key Terms & Concepts",
"agenda": "Agenda",
"continued": " (continued)",
"agendaContinued": " (continued {idx}/{total})",
"introduction": "Introduction",
"mainContent": "Main Content",
"activities": "Activities",
"testYourKnowledge": "Test Your Knowledge",
"quizQuestions": "Quiz Questions ({count})",
"discussionQuestions": "Discussion Questions ({count})",
"additionalResources": "Additional Resources",
"furtherReadings": "Further Readings & Resources",
"activity": "Activity {num}: {title}",
"materialsSuffix": "(Materials)",
"type": "Type",
"duration": "Duration",
"instructions": "Instructions:",
"materialsNeeded": "Materials needed:",
"notesAvailable": "Notes Available",
"quizQuestion": "Quiz Question {num}",
"quizAnswer": "Quiz Answer {num}",
"question": "Question: {text}",
"correctAnswer": "Correct Answer:",
"explanation": "Explanation:",
"groupDiscussion": "Group Discussion:",
"groupInstruction": "Discuss this question with your group and prepare to share your thoughts with the class.",
"facilitatorGuidance": "Facilitator Guidance: Question {num}",
"author": "Author:",
"term": "Term",
"definition": "Definition",
"discussionQuestion": "Discussion Question {num}",
"thankYou": "Thank You!",
"presentation": "Presentation:",
"facilitationNotesSummary": "Facilitation Notes Summary",
"continuedNextSlide": "Continued on next slide...",
"facilitationNotesLabel": "Facilitation Notes:",
"learningObjectiveLabel": "Learning Objective:",
"successCriteriaLabel": "Success criteria:",
"untitledPresentation": "Untitled Presentation",
"untitledReading": "Untitled Reading",
"untitledActivity": "Untitled Activity",
"unknownAuthor": "Unknown Author",
"contentTypeNames": {
"lecture": "Lecture",
"tutorial": "Tutorial",
"workshop": "Workshop",
},
"difficultyNames": {
"introductory": "Introductory Level",
"intermediate": "Intermediate Level",
"advanced": "Advanced Level",
},
},
"id": {
"learningOutcomes": "Capaian Pembelajaran",
"byTheEnd": "Di akhir {contentType} ini, Anda akan dapat:",
"keyTerms": "Istilah Kunci & Konsep",
"agenda": "Agenda",
"continued": " (lanjutan)",
"agendaContinued": " (lanjutan {idx}/{total})",
"introduction": "Pendahuluan",
"mainContent": "Konten Utama",
"activities": "Aktivitas",
"testYourKnowledge": "Uji Pengetahuan Anda",
"quizQuestions": "Soal Kuis ({count})",
"discussionQuestions": "Pertanyaan Diskusi ({count})",
"additionalResources": "Sumber Tambahan",
"furtherReadings": "Bacaan Lanjutan & Sumber",
"activity": "Aktivitas {num}: {title}",
"materialsSuffix": "(Perlengkapan)",
"type": "Jenis",
"duration": "Durasi",
"instructions": "Instruksi:",
"materialsNeeded": "Perlengkapan yang dibutuhkan:",
"notesAvailable": "Catatan Tersedia",
"quizQuestion": "Soal Kuis {num}",
"quizAnswer": "Jawaban Kuis {num}",
"question": "Pertanyaan: {text}",
"correctAnswer": "Jawaban Benar:",
"explanation": "Penjelasan:",
"groupDiscussion": "Diskusi Kelompok:",
"groupInstruction": "Diskusikan pertanyaan ini dengan kelompok Anda dan siapkan untuk berbagi pendapat dengan kelas.",
"facilitatorGuidance": "Panduan Fasilitator: Pertanyaan {num}",
"author": "Penulis:",
"term": "Istilah",
"definition": "Definisi",
"discussionQuestion": "Pertanyaan Diskusi {num}",
"thankYou": "Terima kasih!",
"presentation": "Presentasi:",
"facilitationNotesSummary": "Ringkasan Catatan Fasilitasi",
"continuedNextSlide": "Bersambung di slide berikutnya...",
"facilitationNotesLabel": "Catatan Fasilitasi:",
"learningObjectiveLabel": "Tujuan Pembelajaran:",
"successCriteriaLabel": "Kriteria keberhasilan:",
"untitledPresentation": "Presentasi Tanpa Judul",
"untitledReading": "Bacaan Tanpa Judul",
"untitledActivity": "Aktivitas Tanpa Judul",
"unknownAuthor": "Penulis Tidak Diketahui",
"contentTypeNames": {
"lecture": "Kuliah",
"tutorial": "Tutorial",
"workshop": "Lokakarya",
},
"difficultyNames": {
"introductory": "Tingkat Dasar",
"intermediate": "Tingkat Menengah",
"advanced": "Tingkat Lanjutan",
},
},
}

BULLET_MARKERS = ["•", "*", "-", "○", "◦", "▪", "▫", "◆", "◇", "►", "▻", "▶", "▷"]
SUB_BULLET_MARKERS = ["-", "○", "◦", "▪", "▫"]
13 changes: 13 additions & 0 deletions backend/pptx_builder/localization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from . import constants


def set_language(lang: str):
lang = (lang or "en").lower()
constants.GLOBAL_LANG = "id" if lang == "id" else "en"


def t(key: str, **kwargs):
label = constants.LABELS.get(constants.GLOBAL_LANG, {}).get(key, key)
if isinstance(label, dict):
return label
return label.format(**kwargs) if kwargs else label
25 changes: 25 additions & 0 deletions backend/pptx_builder/sections/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from .title import create_title_slide
from .agenda import create_agenda_slide
from .learning_outcomes import create_learning_outcomes_slide
from .key_terms import create_key_terms_slide
from .content import create_content_slides
from .activities import create_activity_slides
from .quiz import create_quiz_slides
from .discussion import create_discussion_slides
from .readings import create_further_readings_slides
from .facilitation import create_facilitation_notes_slide
from .closing import create_closing_slide

__all__ = [
"create_title_slide",
"create_agenda_slide",
"create_learning_outcomes_slide",
"create_key_terms_slide",
"create_content_slides",
"create_activity_slides",
"create_quiz_slides",
"create_discussion_slides",
"create_further_readings_slides",
"create_facilitation_notes_slide",
"create_closing_slide",
]
Loading