From 325ef1e66a874d1ccc7371e4f1c44de88e724b61 Mon Sep 17 00:00:00 2001 From: sauravniraula Date: Tue, 29 Jul 2025 09:30:58 +0545 Subject: [PATCH 1/2] fix(fastapi): fix /generate enpoint to take form data, reduced database calls and makes assets fetch more efficient --- .../api/v1/ppt/endpoints/presentation.py | 90 +++++++++--------- servers/fastapi/chroma/chroma.sqlite3 | Bin 4329472 -> 4329472 bytes .../models/generate_presentation_api.py | 19 ---- .../fastapi/models/presentation_and_path.py | 10 ++ servers/fastapi/utils/export_utils.py | 2 +- servers/fastapi/utils/validators.py | 11 ++- 6 files changed, 63 insertions(+), 69 deletions(-) delete mode 100644 servers/fastapi/models/generate_presentation_api.py create mode 100644 servers/fastapi/models/presentation_and_path.py diff --git a/servers/fastapi/api/v1/ppt/endpoints/presentation.py b/servers/fastapi/api/v1/ppt/endpoints/presentation.py index 812f17c4..9c3a0dae 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/presentation.py +++ b/servers/fastapi/api/v1/ppt/endpoints/presentation.py @@ -2,12 +2,15 @@ import asyncio import json import os import random -from typing import Annotated, List, Optional -import uuid, aiohttp -from fastapi import APIRouter, Body, HTTPException +from typing import Annotated, List, Literal, Optional +import uuid +from annotated_types import Len +from fastapi import APIRouter, Body, File, HTTPException, UploadFile from fastapi.responses import StreamingResponse from sqlalchemy import delete from sqlmodel import select +from constants.documents import UPLOAD_ACCEPTED_FILE_TYPES +from models.presentation_and_path import PresentationPathAndEditPath from models.presentation_from_template import GetPresentationUsingTemplateRequest from models.presentation_outline_model import ( PresentationOutlineModel, @@ -17,11 +20,6 @@ from models.pptx_models import PptxPresentationModel from models.presentation_layout import PresentationLayoutModel from models.presentation_structure_model import PresentationStructureModel from models.presentation_with_slides import PresentationWithSlides -from models.generate_presentation_api import ( - GeneratePresentationRequest, - PresentationAndPath, - PresentationPathAndEditPath, -) from services.get_layout_by_name import get_layout_by_name from services.icon_finder_service import IconFinderService from services.image_generation_service import ImageGenerationService @@ -45,6 +43,7 @@ from utils.llm_calls.generate_slide_content import ( ) from utils.process_slides import process_slide_and_fetch_assets from utils.randomizers import get_random_uuid +from utils.validators import validate_files PRESENTATION_ROUTER = APIRouter(prefix="/presentation", tags=["Presentation"]) @@ -322,18 +321,22 @@ async def create_pptx(pptx_model: Annotated[PptxPresentationModel, Body()]): @PRESENTATION_ROUTER.post("/generate", response_model=PresentationPathAndEditPath) async def generate_presentation_api( - data: Annotated[GeneratePresentationRequest, Body()], + prompt: Annotated[str, Body()], + n_slides: Annotated[int, Body()] = 8, + language: Annotated[str, Body()] = "English", + layout: Annotated[str, Body()] = "general", + files: Annotated[Optional[List[UploadFile]], File()] = None, + export_as: Annotated[Literal["pptx", "pdf"], Body()] = "pptx", ): - presentation_id = str(uuid.uuid4()) - print("**" * 40) - print(f"Generating presentation with ID: {presentation_id}") - print(f"Received Body as JSON: {data.model_dump_json(indent=2)}") + validate_files(files, True, True, 50, UPLOAD_ACCEPTED_FILE_TYPES) + + presentation_id = get_random_uuid() # 1. Save uploaded files file_paths = [] - if data.documents: + if files: temp_dir = TEMP_FILE_SERVICE.create_temp_dir() - for upload in data.documents: + for upload in files: file_path = os.path.join(temp_dir, upload.filename) with open(file_path, "wb") as f: f.write(await upload.read()) @@ -350,25 +353,23 @@ async def generate_presentation_api( # 3. Generate Outlines presentation_content_text = "" async for chunk in generate_ppt_outline( - data.prompt, - data.n_slides, - data.language, + prompt, + n_slides, + language, summary, ): presentation_content_text += chunk presentation_content_json = json.loads(presentation_content_text) presentation_content = PresentationOutlineModel(**presentation_content_json) - outlines = presentation_content.slides[: data.n_slides] + outlines = presentation_content.slides[:n_slides] total_outlines = len(outlines) print("-" * 40) - print("Generated Presentation Content:", presentation_content_text) print(f"Generated {total_outlines} outlines for the presentation") - print(f"Presentation Title: {presentation_content.title}") # 4. Parse Layouts - layout = await get_layout_by_name(data.layout) + layout = await get_layout_by_name(layout) total_slide_layouts = len(layout.slides) # 5. Generate Structure @@ -395,12 +396,12 @@ async def generate_presentation_api( if presentation_structure.slides[index] >= total_slide_layouts: presentation_structure.slides[index] = random_slide_index - # 6. Create and Save PresentationModel + # 6. Create PresentationModel presentation = PresentationModel( id=presentation_id, - prompt=data.prompt, - n_slides=data.n_slides, - language=data.language, + prompt=prompt, + n_slides=n_slides, + language=language, title=presentation_content.title, summary=summary, outlines=[each.model_dump() for each in outlines], @@ -408,10 +409,10 @@ async def generate_presentation_api( layout=layout.model_dump(), structure=presentation_structure.model_dump(), ) - with get_sql_session() as sql_session: - sql_session.add(presentation) - sql_session.commit() - sql_session.refresh(presentation) + + image_generation_service = ImageGenerationService(get_images_directory()) + icon_finder_service = IconFinderService() + async_asset_generation_tasks = [] # 7. Generate slide content and save slides slides: List[SlideModel] = [] @@ -422,7 +423,6 @@ async def generate_presentation_api( slide_content = await get_slide_content_from_type_and_outline( slide_layout, outlines[i] ) - print(f"Generated content for slide {i}: {json.dumps(slide_content, indent=2)}") slide = SlideModel( presentation=presentation_id, layout_group=layout.name, @@ -430,29 +430,29 @@ async def generate_presentation_api( index=i, content=slide_content, ) + async_asset_generation_tasks.append( + process_slide_and_fetch_assets( + image_generation_service, icon_finder_service, slide + ) + ) slides.append(slide) slide_contents.append(slide_content) - # Process slides to fetch assets (images, icons, etc.) - print("Processing slides to fetch assets") - image_generation_service = ImageGenerationService(get_images_directory()) - icon_finder_service = IconFinderService() - for slide in slides: - try: - await process_slide_and_fetch_assets( - image_generation_service, icon_finder_service, slide - ) - print(f"Processed slide {slide.index} successfully") - except Exception as e: - print(f"Error processing slide {slide.index}: {e}") + generated_assets_lists = await asyncio.gather(*async_asset_generation_tasks) + generated_assets = [] + for assets_list in generated_assets_lists: + generated_assets.extend(assets_list) + # 8. Save PresentationModel and Slides with get_sql_session() as sql_session: + sql_session.add(presentation) sql_session.add_all(slides) + sql_session.add_all(generated_assets) sql_session.commit() - # 8. Export + # 9. Export presentation_and_path = await export_presentation( - presentation_id, presentation_content.title, data.export_as + presentation_id, presentation_content.title, export_as ) return PresentationPathAndEditPath( diff --git a/servers/fastapi/chroma/chroma.sqlite3 b/servers/fastapi/chroma/chroma.sqlite3 index b0a57e47caa641dc91b45e73449d71f3c4305c88..e26f38dcee8dbf2136df5be7f375d8133188a107 100644 GIT binary patch delta 333 zcmWm9J5s^`7yw~H2!xkE#DGAeJX8<`Q4tjc0>ec(N@gselp9!h0ms6YBUq+!hM}=Z zLt{hZruepB_iY(=@oFi?%AP64$Narx>f(iDFMqNVUw&mPKjM96H)}CfT@A(6SdeHC zjlB5I!gmyep&|keQN*Aljs%iOA&mnFGRVTfAxz|u#}NuBq67;zj&XuADyX7{Q=H)( tbu@5+CN6P>7TUN*2RFFI9lE$j4-a_66Z#n78AFUPM({rQ@|~&q{SQ#&i}L^g delta 304 zcmWm9Id%bI0Kjo$4aR&XCM0I;YiwglVmI+z#8JGbkdhlHa)CUBYdCY6u9Am7e4qAz!QQ9Aq*7} zXb^}($1`Fu@ParJc*Pr%FkvBuG&0B{hdc@>qJ%OksNx+p)X_i_E!b$IgD!gbz$d=Y V#{ff&FvbK^%<#B>AEh%lPX8WchwA_U diff --git a/servers/fastapi/models/generate_presentation_api.py b/servers/fastapi/models/generate_presentation_api.py deleted file mode 100644 index e34b7f1f..00000000 --- a/servers/fastapi/models/generate_presentation_api.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import List, Optional, Literal -from pydantic import BaseModel, Field -from fastapi import UploadFile - -class GeneratePresentationRequest(BaseModel): - prompt: str - n_slides: int = Field(default=8, ge=5, le=20) - language: str = Field(default="English") - layout: str = Field(default="general") - documents: Optional[List[UploadFile]] = None - export_as: Literal["pptx", "pdf"] = Field(default="pptx") - - -class PresentationAndPath(BaseModel): - presentation_id: str - path: str - -class PresentationPathAndEditPath(PresentationAndPath): - edit_path: str diff --git a/servers/fastapi/models/presentation_and_path.py b/servers/fastapi/models/presentation_and_path.py new file mode 100644 index 00000000..e85c6c3d --- /dev/null +++ b/servers/fastapi/models/presentation_and_path.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class PresentationAndPath(BaseModel): + presentation_id: str + path: str + + +class PresentationPathAndEditPath(PresentationAndPath): + edit_path: str diff --git a/servers/fastapi/utils/export_utils.py b/servers/fastapi/utils/export_utils.py index 2e4915ec..1c5db64d 100644 --- a/servers/fastapi/utils/export_utils.py +++ b/servers/fastapi/utils/export_utils.py @@ -6,7 +6,7 @@ from fastapi import HTTPException from pathvalidate import sanitize_filename from models.pptx_models import PptxPresentationModel -from models.generate_presentation_api import PresentationAndPath +from models.presentation_and_path import PresentationAndPath from services.pptx_presentation_creator import PptxPresentationCreator from services import TEMP_FILE_SERVICE from utils.asset_directory_utils import get_exports_directory diff --git a/servers/fastapi/utils/validators.py b/servers/fastapi/utils/validators.py index e96fc845..6b405ccf 100644 --- a/servers/fastapi/utils/validators.py +++ b/servers/fastapi/utils/validators.py @@ -1,5 +1,5 @@ -from http.client import HTTPException from typing import List +from fastapi import HTTPException from fastapi import UploadFile @@ -18,10 +18,13 @@ def validate_files( if (max_size * 1024 * 1024) < each_file.size: raise HTTPException( 400, - f"File '{each_file.filename}' exceeded max upload size of {max_size} MB", + detail=f"File '{each_file.filename}' exceeded max upload size of {max_size} MB", ) elif each_file.content_type not in accepted_types: - raise HTTPException(400, f"File '{each_file.filename}' not accepted.") + raise HTTPException( + 400, + detail=f"File '{each_file.filename}' not accepted. Accepted types: {accepted_types}", + ) elif not (field or nullable): - raise HTTPException(400, "File must be provided.") + raise HTTPException(400, detail="File must be provided.") From 725da454f394fec79a760e104e14fcdfd4939976 Mon Sep 17 00:00:00 2001 From: sauravniraula Date: Tue, 29 Jul 2025 09:33:17 +0545 Subject: [PATCH 2/2] docs(readme): corrects api endpoint and body --- README.md | 3 +-- servers/fastapi/chroma/chroma.sqlite3 | Bin 4329472 -> 4329472 bytes 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f27ef92..9652f23b 100644 --- a/README.md +++ b/README.md @@ -155,12 +155,11 @@ Content-Type: `multipart/form-data` #### Example Request ```bash -curl -X POST http://localhost:5000/api/v1/ppt/generate/presentation \ +curl -X POST http://localhost:5000/api/v1/ppt/presentation/generate \ -F "prompt=Introduction to Machine Learning" \ -F "n_slides=5" \ -F "language=English" \ -F "layout=general" \ - -F "theme=light" \ -F "export_as=pptx" ``` diff --git a/servers/fastapi/chroma/chroma.sqlite3 b/servers/fastapi/chroma/chroma.sqlite3 index e26f38dcee8dbf2136df5be7f375d8133188a107..41cef45e5aec98ba3167e582ca976a011a6e43b4 100644 GIT binary patch delta 320 zcmWm9J8}U59Kc~~y_WFDio`qKLK4JVuuLxEC}vV5rQAT_0vUyI3p1K+GGjDpG-)&% zKgD-|Z?^cd&M=QD`giuvZ9OPF_*{uTmjo4L9wP+uv@o5?g ztWaTt9S%6*f(AD{xPccw2>b{j2pu8ZB8)rSBZ4Smh$Dd{Qg}cb8Dx<|9t9Lp!XwJ4 l;0e!oK@~OB@rnkTXrYY`y6B;gHw-Yu2-o{~Z5or(_5wfihZ5XtKqO7U1G&teH1uo%+ z2Rgj);R@IABLG1VAs7fFf*V8;!!7O*M*>Ntkj6bS$RdY43Mit4GAgK|hB_K(q6HIe fJm3)>bn%2|yx