From c5b255a3c86acf5c4732e9b8e42fc0e73ef0ef5b Mon Sep 17 00:00:00 2001 From: sauravniraula Date: Thu, 28 Aug 2025 14:01:12 +0545 Subject: [PATCH 1/8] fix: removes datetime tool and provide it directly --- .../llm_calls/generate_presentation_outlines.py | 8 ++++---- .../utils/llm_calls/generate_slide_content.py | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py b/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py index 9f330a3c..9212edc1 100644 --- a/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py +++ b/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py @@ -1,7 +1,8 @@ +from datetime import datetime from typing import Optional from models.llm_message import LLMSystemMessage, LLMUserMessage -from models.llm_tools import GetCurrentDatetimeTool, SearchWebTool +from models.llm_tools import SearchWebTool from services.llm_client import LLMClient from utils.get_dynamic_models import get_presentation_outline_model_with_n_slides from utils.llm_provider import get_model @@ -26,6 +27,7 @@ def get_user_prompt(prompt: str, n_slides: int, language: str, content: str): - Prompt: {prompt} - Output Language: {language} - Number of Slides: {n_slides} + - Current Date and Time: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - Additional Information: {content} """ @@ -52,13 +54,11 @@ async def generate_ppt_outline( client = LLMClient() - tools = [SearchWebTool, GetCurrentDatetimeTool] - async for chunk in client.stream_structured( model, get_messages(prompt, n_slides, language, content), response_model.model_json_schema(), strict=True, - tools=tools if client.enable_web_grounding() else None, + tools=[SearchWebTool] if client.enable_web_grounding() else None, ): yield chunk diff --git a/servers/fastapi/utils/llm_calls/generate_slide_content.py b/servers/fastapi/utils/llm_calls/generate_slide_content.py index e8f695a0..771620a8 100644 --- a/servers/fastapi/utils/llm_calls/generate_slide_content.py +++ b/servers/fastapi/utils/llm_calls/generate_slide_content.py @@ -1,3 +1,4 @@ +from datetime import datetime from models.llm_message import LLMSystemMessage, LLMUserMessage from models.presentation_layout import SlideLayoutModel from models.presentation_outline_model import SlideOutlineModel @@ -16,17 +17,26 @@ system_prompt = """ # Notes - Slide body should not use words like "This slide", "This presentation". - Rephrase the slide body to make it flow naturally. - - Provide prompt to generate image on "__image_prompt__" property. - - Provide query to search icon on "__icon_query__" property. - Only use markdown to highlight important points. - Make sure to follow language guidelines. - Speaker note should be normal text, not markdown. **Strictly follow the max and min character limit for every property in the slide.** + + # Image and Icon Output Format + image: { + __image_prompt__: string, + } + icon: { + __icon_query__: string, + } """ def get_user_prompt(outline: str, language: str): return f""" + ## Current Date and Time + {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} + ## Icon Query And Image Prompt Language English From 0d1de4a28e79b37443e72180141c2d8775de8762 Mon Sep 17 00:00:00 2001 From: sauravniraula Date: Thu, 28 Aug 2025 14:11:04 +0545 Subject: [PATCH 2/8] feat: adds placeholder image and icons while generating --- .../fastapi/api/v1/ppt/endpoints/presentation.py | 8 +++++++- .../utils/llm_calls/generate_slide_content.py | 4 +++- servers/fastapi/utils/process_slides.py | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/servers/fastapi/api/v1/ppt/endpoints/presentation.py b/servers/fastapi/api/v1/ppt/endpoints/presentation.py index 0c0f4565..6ae10f8b 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/presentation.py +++ b/servers/fastapi/api/v1/ppt/endpoints/presentation.py @@ -40,7 +40,10 @@ from utils.llm_calls.generate_presentation_structure import ( from utils.llm_calls.generate_slide_content import ( get_slide_content_from_type_and_outline, ) -from utils.process_slides import process_slide_and_fetch_assets +from utils.process_slides import ( + process_slide_add_placeholder_assets, + process_slide_and_fetch_assets, +) from utils.randomizers import get_random_uuid @@ -226,6 +229,9 @@ async def stream_presentation( ) slides.append(slide) + # This will mutate slide and add placeholder assets + process_slide_add_placeholder_assets(slide) + # This will mutate slide async_assets_generation_tasks.append( process_slide_and_fetch_assets( diff --git a/servers/fastapi/utils/llm_calls/generate_slide_content.py b/servers/fastapi/utils/llm_calls/generate_slide_content.py index 771620a8..33bdd993 100644 --- a/servers/fastapi/utils/llm_calls/generate_slide_content.py +++ b/servers/fastapi/utils/llm_calls/generate_slide_content.py @@ -20,7 +20,9 @@ system_prompt = """ - Only use markdown to highlight important points. - Make sure to follow language guidelines. - Speaker note should be normal text, not markdown. - **Strictly follow the max and min character limit for every property in the slide.** + - Strictly follow the max and min character limit for every property in the slide. + - Never ever go over the max character limit. Limit your narration to make sure you never go over the max character limit. + - Number of items should not be more than max number of items specified in slide schema. If you have to put multiple points then merge them to obey max numebr of items. # Image and Icon Output Format image: { diff --git a/servers/fastapi/utils/process_slides.py b/servers/fastapi/utils/process_slides.py index 81a0a6b5..91c2d829 100644 --- a/servers/fastapi/utils/process_slides.py +++ b/servers/fastapi/utils/process_slides.py @@ -170,3 +170,19 @@ async def process_old_and_new_slides_and_fetch_assets( set_dict_at_path(new_slide_content, new_icon_dict_paths[i], new_icon_dict) return new_assets + + +def process_slide_add_placeholder_assets(slide: SlideModel): + + image_paths = get_dict_paths_with_key(slide.content, "__image_prompt__") + icon_paths = get_dict_paths_with_key(slide.content, "__icon_query__") + + for image_path in image_paths: + image_dict = get_dict_at_path(slide.content, image_path) + image_dict["__image_url__"] = "/static/images/placeholder.jpg" + set_dict_at_path(slide.content, image_path, image_dict) + + for icon_path in icon_paths: + icon_dict = get_dict_at_path(slide.content, icon_path) + icon_dict["__icon_url__"] = "/static/icons/placeholder.png" + set_dict_at_path(slide.content, icon_path, icon_dict) From b33b44de99b9ae4b29da1b1d220e0daa57284823 Mon Sep 17 00:00:00 2001 From: sauravniraula Date: Thu, 28 Aug 2025 14:55:12 +0545 Subject: [PATCH 3/8] fix: icon finder services reinitiating in every request --- servers/fastapi/api/v1/ppt/endpoints/files.py | 2 +- servers/fastapi/api/v1/ppt/endpoints/icons.py | 5 ++--- servers/fastapi/api/v1/ppt/endpoints/outlines.py | 2 +- .../fastapi/api/v1/ppt/endpoints/presentation.py | 15 ++++----------- servers/fastapi/api/v1/ppt/endpoints/slide.py | 4 ---- servers/fastapi/services/__init__.py | 4 ---- servers/fastapi/services/icon_finder_service.py | 7 +++++-- servers/fastapi/services/temp_file_service.py | 3 +++ servers/fastapi/utils/export_utils.py | 2 +- servers/fastapi/utils/process_slides.py | 8 +++----- 10 files changed, 20 insertions(+), 32 deletions(-) diff --git a/servers/fastapi/api/v1/ppt/endpoints/files.py b/servers/fastapi/api/v1/ppt/endpoints/files.py index 25936e6a..c63b6689 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/files.py +++ b/servers/fastapi/api/v1/ppt/endpoints/files.py @@ -5,7 +5,7 @@ from fastapi import APIRouter, Body, File, UploadFile from constants.documents import UPLOAD_ACCEPTED_FILE_TYPES from models.decomposed_file_info import DecomposedFileInfo -from services import TEMP_FILE_SERVICE +from services.temp_file_service import TEMP_FILE_SERVICE from services.documents_loader import DocumentsLoader from utils.randomizers import get_random_uuid from utils.validators import validate_files diff --git a/servers/fastapi/api/v1/ppt/endpoints/icons.py b/servers/fastapi/api/v1/ppt/endpoints/icons.py index 36116f53..5d71791e 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/icons.py +++ b/servers/fastapi/api/v1/ppt/endpoints/icons.py @@ -1,11 +1,10 @@ from typing import List from fastapi import APIRouter -from services.icon_finder_service import IconFinderService +from services.icon_finder_service import ICON_FINDER_SERVICE ICONS_ROUTER = APIRouter(prefix="/icons", tags=["Icons"]) @ICONS_ROUTER.get("/search", response_model=List[str]) async def search_icons(query: str, limit: int = 20): - icon_finder_service = IconFinderService() - return await icon_finder_service.search_icons(query, limit) + return await ICON_FINDER_SERVICE.search_icons(query, limit) diff --git a/servers/fastapi/api/v1/ppt/endpoints/outlines.py b/servers/fastapi/api/v1/ppt/endpoints/outlines.py index f53c8bab..d5a2b963 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/outlines.py +++ b/servers/fastapi/api/v1/ppt/endpoints/outlines.py @@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from models.presentation_outline_model import PresentationOutlineModel from models.sql.presentation import PresentationModel from models.sse_response import SSECompleteResponse, SSEResponse, SSEStatusResponse -from services import TEMP_FILE_SERVICE +from services.temp_file_service import TEMP_FILE_SERVICE from services.database import get_async_session from services.documents_loader import DocumentsLoader from services.score_based_chunker import ScoreBasedChunker diff --git a/servers/fastapi/api/v1/ppt/endpoints/presentation.py b/servers/fastapi/api/v1/ppt/endpoints/presentation.py index 6ae10f8b..e42c58a5 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/presentation.py +++ b/servers/fastapi/api/v1/ppt/endpoints/presentation.py @@ -2,7 +2,7 @@ import asyncio import json import os import random -from typing import Annotated, List, Literal, Optional +from typing import Annotated, List, Optional from fastapi import APIRouter, Body, Depends, HTTPException from fastapi.responses import StreamingResponse from sqlalchemy import delete @@ -21,7 +21,6 @@ from models.presentation_structure_model import PresentationStructureModel from models.presentation_with_slides import PresentationWithSlides from utils.get_layout_by_name import get_layout_by_name -from services.icon_finder_service import IconFinderService from services.image_generation_service import ImageGenerationService from utils.dict_utils import deep_update from utils.export_utils import export_presentation @@ -30,7 +29,7 @@ from models.sql.slide import SlideModel from models.sse_response import SSECompleteResponse, SSEResponse from services.database import get_async_session -from services import TEMP_FILE_SERVICE +from services.temp_file_service import TEMP_FILE_SERVICE from models.sql.presentation import PresentationModel from services.pptx_presentation_creator import PptxPresentationCreator from utils.asset_directory_utils import get_exports_directory, get_images_directory @@ -197,7 +196,6 @@ async def stream_presentation( ) image_generation_service = ImageGenerationService(get_images_directory()) - icon_finder_service = IconFinderService() async def inner(): structure = presentation.get_structure() @@ -234,9 +232,7 @@ async def stream_presentation( # This will mutate slide async_assets_generation_tasks.append( - process_slide_and_fetch_assets( - image_generation_service, icon_finder_service, slide - ) + process_slide_and_fetch_assets(image_generation_service, slide) ) yield SSEResponse( @@ -386,7 +382,6 @@ async def generate_presentation_api( ) image_generation_service = ImageGenerationService(get_images_directory()) - icon_finder_service = IconFinderService() async_asset_generation_tasks = [] # 7. Generate slide content and save slides @@ -407,9 +402,7 @@ async def generate_presentation_api( content=slide_content, ) async_asset_generation_tasks.append( - process_slide_and_fetch_assets( - image_generation_service, icon_finder_service, slide - ) + process_slide_and_fetch_assets(image_generation_service, slide) ) slides.append(slide) slide_contents.append(slide_content) diff --git a/servers/fastapi/api/v1/ppt/endpoints/slide.py b/servers/fastapi/api/v1/ppt/endpoints/slide.py index e1ec9e6b..7d42b884 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/slide.py +++ b/servers/fastapi/api/v1/ppt/endpoints/slide.py @@ -1,4 +1,3 @@ -import importlib from typing import Annotated, Optional from fastapi import APIRouter, Body, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession @@ -6,7 +5,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from models.sql.presentation import PresentationModel from models.sql.slide import SlideModel from services.database import get_async_session -from services.icon_finder_service import IconFinderService from services.image_generation_service import ImageGenerationService from utils.asset_directory_utils import get_images_directory from utils.llm_calls.edit_slide import get_edited_slide_content @@ -42,12 +40,10 @@ async def edit_slide( ) image_generation_service = ImageGenerationService(get_images_directory()) - icon_finder_service = IconFinderService() # This will mutate edited_slide_content new_assets = await process_old_and_new_slides_and_fetch_assets( image_generation_service, - icon_finder_service, slide.content, edited_slide_content, ) diff --git a/servers/fastapi/services/__init__.py b/servers/fastapi/services/__init__.py index a1d47d50..e69de29b 100644 --- a/servers/fastapi/services/__init__.py +++ b/servers/fastapi/services/__init__.py @@ -1,4 +0,0 @@ -from services.temp_file_service import TempFileService - - -TEMP_FILE_SERVICE = TempFileService() diff --git a/servers/fastapi/services/icon_finder_service.py b/servers/fastapi/services/icon_finder_service.py index 6d3c0398..3584f3af 100644 --- a/servers/fastapi/services/icon_finder_service.py +++ b/servers/fastapi/services/icon_finder_service.py @@ -11,9 +11,9 @@ class IconFinderService: self.client = chromadb.PersistentClient( path="chroma", settings=Settings(anonymized_telemetry=False) ) - print('Initializing icons collection...') + print("Initializing icons collection...") self._initialize_icons_collection() - print('Icons collection initialized.') + print("Icons collection initialized.") def _initialize_icons_collection(self): self.embedding_function = ONNXMiniLM_L6_V2() @@ -51,3 +51,6 @@ class IconFinderService: n_results=k, ) return [f"/static/icons/bold/{each}.png" for each in result["ids"][0]] + + +ICON_FINDER_SERVICE = IconFinderService() diff --git a/servers/fastapi/services/temp_file_service.py b/servers/fastapi/services/temp_file_service.py index eaa09e97..c687f708 100644 --- a/servers/fastapi/services/temp_file_service.py +++ b/servers/fastapi/services/temp_file_service.py @@ -65,3 +65,6 @@ class TempFileService: def cleanup_base_dir(self): self.cleanup_temp_dir(self.base_dir) + + +TEMP_FILE_SERVICE = TempFileService() diff --git a/servers/fastapi/utils/export_utils.py b/servers/fastapi/utils/export_utils.py index 1c5db64d..e75f49cc 100644 --- a/servers/fastapi/utils/export_utils.py +++ b/servers/fastapi/utils/export_utils.py @@ -8,7 +8,7 @@ from pathvalidate import sanitize_filename from models.pptx_models import PptxPresentationModel from models.presentation_and_path import PresentationAndPath from services.pptx_presentation_creator import PptxPresentationCreator -from services import TEMP_FILE_SERVICE +from services.temp_file_service import TEMP_FILE_SERVICE from utils.asset_directory_utils import get_exports_directory from utils.randomizers import get_random_uuid diff --git a/servers/fastapi/utils/process_slides.py b/servers/fastapi/utils/process_slides.py index 91c2d829..b3c605fa 100644 --- a/servers/fastapi/utils/process_slides.py +++ b/servers/fastapi/utils/process_slides.py @@ -3,7 +3,7 @@ from typing import List, Tuple from models.image_prompt import ImagePrompt from models.sql.image_asset import ImageAsset from models.sql.slide import SlideModel -from services.icon_finder_service import IconFinderService +from services.icon_finder_service import ICON_FINDER_SERVICE from services.image_generation_service import ImageGenerationService from utils.asset_directory_utils import get_images_directory from utils.dict_utils import get_dict_at_path, get_dict_paths_with_key, set_dict_at_path @@ -11,7 +11,6 @@ from utils.dict_utils import get_dict_at_path, get_dict_paths_with_key, set_dict async def process_slide_and_fetch_assets( image_generation_service: ImageGenerationService, - icon_finder_service: IconFinderService, slide: SlideModel, ) -> List[ImageAsset]: @@ -33,7 +32,7 @@ async def process_slide_and_fetch_assets( for icon_path in icon_paths: __icon_query__parent = get_dict_at_path(slide.content, icon_path) async_tasks.append( - icon_finder_service.search_icons(__icon_query__parent["__icon_query__"]) + ICON_FINDER_SERVICE.search_icons(__icon_query__parent["__icon_query__"]) ) results = await asyncio.gather(*async_tasks) @@ -60,7 +59,6 @@ async def process_slide_and_fetch_assets( async def process_old_and_new_slides_and_fetch_assets( image_generation_service: ImageGenerationService, - icon_finder_service: IconFinderService, old_slide_content: dict, new_slide_content: dict, ) -> List[ImageAsset]: @@ -138,7 +136,7 @@ async def process_old_and_new_slides_and_fetch_assets( continue async_icon_fetch_tasks.append( - icon_finder_service.search_icons(new_icon["__icon_query__"]) + ICON_FINDER_SERVICE.search_icons(new_icon["__icon_query__"]) ) new_icons_fetch_status.append(True) From 74dc67628d8f0d9d934b87d8a1a5d1840ef85c14 Mon Sep 17 00:00:00 2001 From: sauravniraula Date: Thu, 28 Aug 2025 18:25:13 +0545 Subject: [PATCH 4/8] feat: multiple document support in outlines generation --- servers/fastapi/api/v1/ppt/endpoints/outlines.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/servers/fastapi/api/v1/ppt/endpoints/outlines.py b/servers/fastapi/api/v1/ppt/endpoints/outlines.py index d5a2b963..5f485b7c 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/outlines.py +++ b/servers/fastapi/api/v1/ppt/endpoints/outlines.py @@ -38,7 +38,7 @@ async def stream_outlines( documents_loader = DocumentsLoader(file_paths=presentation.file_paths) await documents_loader.load_documents(temp_dir) documents = documents_loader.documents - if documents: + if documents and len(documents) == 1: additional_context = documents[0] chunker = ScoreBasedChunker() try: @@ -50,6 +50,8 @@ async def stream_outlines( ) except Exception as e: print(e) + else: + additional_context = "\n\n".join(documents) if not presentation_outlines: presentation_outlines_text = "" From 6fa63c8ece92abf45771d646f9b98b9c8be9cce1 Mon Sep 17 00:00:00 2001 From: sauravniraula Date: Thu, 28 Aug 2025 18:31:57 +0545 Subject: [PATCH 5/8] feat: documents support in API --- .../api/v1/ppt/endpoints/presentation.py | 36 ++++++++++++++----- .../models/generate_presentation_request.py | 17 ++++++--- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/servers/fastapi/api/v1/ppt/endpoints/presentation.py b/servers/fastapi/api/v1/ppt/endpoints/presentation.py index e42c58a5..440f7c89 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/presentation.py +++ b/servers/fastapi/api/v1/ppt/endpoints/presentation.py @@ -20,6 +20,8 @@ from models.presentation_layout import PresentationLayoutModel from models.presentation_structure_model import PresentationStructureModel from models.presentation_with_slides import PresentationWithSlides +from services.documents_loader import DocumentsLoader +from services.score_based_chunker import ScoreBasedChunker from utils.get_layout_by_name import get_layout_by_name from services.image_generation_service import ImageGenerationService from utils.dict_utils import deep_update @@ -321,6 +323,21 @@ async def generate_presentation_api( presentation_outlines = None additional_context = "" + # Process files + if request.files: + documents_loader = DocumentsLoader(file_paths=request.files) + await documents_loader.load_documents() + documents = documents_loader.documents + if documents and len(documents) == 1: + additional_context = documents[0] + chunker = ScoreBasedChunker() + chunks = await chunker.get_n_chunks(documents[0], request.n_slides) + presentation_outlines = PresentationOutlineModel( + slides=[chunk.to_slide_outline() for chunk in chunks] + ) + elif documents: + additional_context = "\n\n".join(documents) + if not presentation_outlines: presentation_outlines_text = "" async for chunk in generate_ppt_outline( @@ -331,15 +348,16 @@ async def generate_presentation_api( ): presentation_outlines_text += chunk - try: - presentation_outlines_json = json.loads(presentation_outlines_text) - except Exception as e: - print(e) - raise HTTPException( - status_code=400, - detail="Failed to generate presentation outlines. Please try again.", - ) - presentation_outlines = PresentationOutlineModel(**presentation_outlines_json) + try: + presentation_outlines_json = json.loads(presentation_outlines_text) + except Exception as e: + print(e) + raise HTTPException( + status_code=400, + detail="Failed to generate presentation outlines. Please try again.", + ) + presentation_outlines = PresentationOutlineModel(**presentation_outlines_json) + outlines = presentation_outlines.slides[: request.n_slides] total_outlines = len(outlines) diff --git a/servers/fastapi/models/generate_presentation_request.py b/servers/fastapi/models/generate_presentation_request.py index f8c65670..a38f6a2e 100644 --- a/servers/fastapi/models/generate_presentation_request.py +++ b/servers/fastapi/models/generate_presentation_request.py @@ -1,10 +1,19 @@ -from typing import Literal, Optional +from typing import List, Literal, Optional from pydantic import BaseModel, Field class GeneratePresentationRequest(BaseModel): prompt: str = Field(..., description="The prompt for generating the presentation") n_slides: int = Field(default=8, description="Number of slides to generate") - language: str = Field(default="English", description="Language for the presentation") - template: str = Field(default="general", description="Template to use for the presentation") - export_as: Literal["pptx", "pdf"] = Field(default="pptx", description="Export format") + language: str = Field( + default="English", description="Language for the presentation" + ) + template: str = Field( + default="general", description="Template to use for the presentation" + ) + files: Optional[List[str]] = Field( + default=None, description="Files to use for the presentation" + ) + export_as: Literal["pptx", "pdf"] = Field( + default="pptx", description="Export format" + ) From 5ec4144f9fcca3daefa4652d70f8a41ff9e21707 Mon Sep 17 00:00:00 2001 From: sauravniraula Date: Thu, 28 Aug 2025 20:35:27 +0545 Subject: [PATCH 6/8] feat: adds content and instruction on UI endpoints --- .../fastapi/api/v1/ppt/endpoints/outlines.py | 6 +- .../api/v1/ppt/endpoints/presentation.py | 21 ++++-- .../models/generate_presentation_request.py | 5 +- .../models/presentation_with_slides.py | 3 +- servers/fastapi/models/sql/presentation.py | 8 +- .../generate_presentation_outlines.py | 75 ++++++++++++------- .../generate_presentation_structure.py | 11 ++- .../utils/llm_calls/generate_slide_content.py | 62 ++++++++------- 8 files changed, 126 insertions(+), 65 deletions(-) diff --git a/servers/fastapi/api/v1/ppt/endpoints/outlines.py b/servers/fastapi/api/v1/ppt/endpoints/outlines.py index 5f485b7c..dc7f813d 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/outlines.py +++ b/servers/fastapi/api/v1/ppt/endpoints/outlines.py @@ -49,7 +49,10 @@ async def stream_outlines( slides=[chunk.to_slide_outline() for chunk in chunks] ) except Exception as e: - print(e) + raise HTTPException( + status_code=400, + detail="Failed to generate presentation outlines. Please try again.", + ) else: additional_context = "\n\n".join(documents) @@ -60,6 +63,7 @@ async def stream_outlines( presentation.n_slides, presentation.language, additional_context, + presentation.instruction, ): # Give control to the event loop await asyncio.sleep(0) diff --git a/servers/fastapi/api/v1/ppt/endpoints/presentation.py b/servers/fastapi/api/v1/ppt/endpoints/presentation.py index 440f7c89..da7c749b 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/presentation.py +++ b/servers/fastapi/api/v1/ppt/endpoints/presentation.py @@ -108,20 +108,22 @@ async def get_all_presentations(sql_session: AsyncSession = Depends(get_async_se @PRESENTATION_ROUTER.post("/create", response_model=PresentationModel) async def create_presentation( - prompt: Annotated[str, Body()], + content: Annotated[str, Body()], n_slides: Annotated[int, Body()], language: Annotated[str, Body()], file_paths: Annotated[Optional[List[str]], Body()] = None, + instruction: Annotated[Optional[str], Body()] = None, sql_session: AsyncSession = Depends(get_async_session), ): presentation_id = get_random_uuid() presentation = PresentationModel( id=presentation_id, - prompt=prompt, + content=content, n_slides=n_slides, language=language, file_paths=file_paths, + instruction=instruction, ) sql_session.add(presentation) @@ -157,6 +159,7 @@ async def prepare_presentation( await generate_presentation_structure( presentation_outline=presentation_outline_model, presentation_layout=layout, + instruction=presentation.instruction, ) ) @@ -216,7 +219,10 @@ async def stream_presentation( slide_layout = layout.slides[slide_layout_index] slide_content = await get_slide_content_from_type_and_outline( - slide_layout, outline.slides[i], presentation.language + slide_layout, + outline.slides[i], + presentation.language, + presentation.instruction, ) slide = SlideModel( @@ -341,10 +347,11 @@ async def generate_presentation_api( if not presentation_outlines: presentation_outlines_text = "" async for chunk in generate_ppt_outline( - request.prompt, + request.content, request.n_slides, request.language, additional_context, + request.instruction, ): presentation_outlines_text += chunk @@ -376,6 +383,7 @@ async def generate_presentation_api( await generate_presentation_structure( presentation_outlines, layout_model, + request.instruction, ) ) @@ -391,12 +399,13 @@ async def generate_presentation_api( # 6. Create PresentationModel presentation = PresentationModel( id=presentation_id, - prompt=request.prompt, + content=request.content, n_slides=request.n_slides, language=request.language, outlines=presentation_outlines.model_dump(), layout=layout_model.model_dump(), structure=presentation_structure.model_dump(), + instruction=request.instruction, ) image_generation_service = ImageGenerationService(get_images_directory()) @@ -409,7 +418,7 @@ async def generate_presentation_api( slide_layout = layout_model.slides[slide_layout_index] print(f"Generating content for slide {i} with layout {slide_layout.id}") slide_content = await get_slide_content_from_type_and_outline( - slide_layout, outlines[i], request.language + slide_layout, outlines[i], request.language, request.instruction ) slide = SlideModel( presentation=presentation_id, diff --git a/servers/fastapi/models/generate_presentation_request.py b/servers/fastapi/models/generate_presentation_request.py index a38f6a2e..d210c4a7 100644 --- a/servers/fastapi/models/generate_presentation_request.py +++ b/servers/fastapi/models/generate_presentation_request.py @@ -3,7 +3,10 @@ from pydantic import BaseModel, Field class GeneratePresentationRequest(BaseModel): - prompt: str = Field(..., description="The prompt for generating the presentation") + content: str = Field(..., description="The content for generating the presentation") + instruction: Optional[str] = Field( + default=None, description="The instruction for generating the presentation" + ) n_slides: int = Field(default=8, description="Number of slides to generate") language: str = Field( default="English", description="Language for the presentation" diff --git a/servers/fastapi/models/presentation_with_slides.py b/servers/fastapi/models/presentation_with_slides.py index 08bc6fdb..c6edde76 100644 --- a/servers/fastapi/models/presentation_with_slides.py +++ b/servers/fastapi/models/presentation_with_slides.py @@ -12,7 +12,7 @@ from models.sql.slide import SlideModel class PresentationWithSlides(BaseModel): id: str - prompt: str + content: str n_slides: int language: str title: Optional[str] = None @@ -21,6 +21,7 @@ class PresentationWithSlides(BaseModel): updated_at: datetime layout: Optional[PresentationLayoutModel] structure: Optional[PresentationStructureModel] + instruction: Optional[str] = None slides: List[SlideModel] def to_presentation_model(self) -> PresentationModel: diff --git a/servers/fastapi/models/sql/presentation.py b/servers/fastapi/models/sql/presentation.py index c0b65c64..e565d9a0 100644 --- a/servers/fastapi/models/sql/presentation.py +++ b/servers/fastapi/models/sql/presentation.py @@ -1,6 +1,6 @@ from datetime import datetime from typing import List, Optional -from sqlalchemy import JSON, Column, DateTime +from sqlalchemy import JSON, Column, DateTime, String from sqlmodel import Field, SQLModel from models.presentation_layout import PresentationLayoutModel @@ -11,7 +11,7 @@ from utils.randomizers import get_random_uuid class PresentationModel(SQLModel, table=True): id: str = Field(primary_key=True) - prompt: str + content: str n_slides: int language: str title: Optional[str] = None @@ -21,11 +21,12 @@ class PresentationModel(SQLModel, table=True): updated_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now)) layout: Optional[dict] = Field(sa_column=Column(JSON), default=None) structure: Optional[dict] = Field(sa_column=Column(JSON), default=None) + instruction: Optional[str] = Field(sa_column=Column(String), default=None) def get_new_presentation(self): return PresentationModel( id=get_random_uuid(), - prompt=self.prompt, + content=self.content, n_slides=self.n_slides, language=self.language, title=self.title, @@ -33,6 +34,7 @@ class PresentationModel(SQLModel, table=True): outlines=self.outlines, layout=self.layout, structure=self.structure, + instruction=self.instruction, ) def get_presentation_outline(self): diff --git a/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py b/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py index 9212edc1..7c57e734 100644 --- a/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py +++ b/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py @@ -7,47 +7,64 @@ from services.llm_client import LLMClient from utils.get_dynamic_models import get_presentation_outline_model_with_n_slides from utils.llm_provider import get_model -system_prompt = """ - You are an expert presentation creator. Generate structured presentations based on user requirements and format them according to the specified JSON schema with markdown content. - Try to use available tools for better results. - - - Provide content for each slide in markdown format. - - Make sure that flow of the presentation is logical and consistent. - - Place greater emphasis on numerical data. - - If Additional Information is provided, divide it into slides. - - Make sure no images are provided in the content. - - Make sure that content follows language guidelines. -""" - - -def get_user_prompt(prompt: str, n_slides: int, language: str, content: str): +def get_system_prompt(instruction: Optional[str] = None): return f""" - **Input:** - - Prompt: {prompt} - - Output Language: {language} - - Number of Slides: {n_slides} - - Current Date and Time: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - - Additional Information: {content} + You are an expert presentation creator. Generate structured presentations based on user requirements and format them according to the specified JSON schema with markdown content. + + Try to use available tools for better results. + + - Provide content for each slide in markdown format. + - Make sure that flow of the presentation is logical and consistent. + - Place greater emphasis on numerical data. + - If Additional Information is provided, divide it into slides. + - Make sure no images are provided in the content. + - Make sure that content follows language guidelines. + + {"# User Instruction:" if instruction else ""} + {instruction or ""} """ -def get_messages(prompt: str, n_slides: int, language: str, content: str): +def get_user_prompt( + content: str, + n_slides: int, + language: str, + additional_context: Optional[str] = None, +): + return f""" + **Input:** + - User provided content: {content} + - Output Language: {language} + - Number of Slides: {n_slides} + - Current Date and Time: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} + - Additional Information: {additional_context or ""} + """ + + +def get_messages( + content: str, + n_slides: int, + language: str, + additional_context: Optional[str] = None, + instruction: Optional[str] = None, +): return [ LLMSystemMessage( - content=system_prompt, + content=get_system_prompt(instruction), ), LLMUserMessage( - content=get_user_prompt(prompt, n_slides, language, content), + content=get_user_prompt(content, n_slides, language, additional_context), ), ] async def generate_ppt_outline( - prompt: Optional[str], + content: str, n_slides: int, language: Optional[str] = None, - content: Optional[str] = None, + additional_context: Optional[str] = None, + instruction: Optional[str] = None, ): model = get_model() response_model = get_presentation_outline_model_with_n_slides(n_slides) @@ -56,7 +73,13 @@ async def generate_ppt_outline( async for chunk in client.stream_structured( model, - get_messages(prompt, n_slides, language, content), + get_messages( + content, + n_slides, + language, + additional_context, + instruction, + ), response_model.model_json_schema(), strict=True, tools=[SearchWebTool] if client.enable_web_grounding() else None, diff --git a/servers/fastapi/utils/llm_calls/generate_presentation_structure.py b/servers/fastapi/utils/llm_calls/generate_presentation_structure.py index 1bfc0cd0..42a398fc 100644 --- a/servers/fastapi/utils/llm_calls/generate_presentation_structure.py +++ b/servers/fastapi/utils/llm_calls/generate_presentation_structure.py @@ -1,3 +1,4 @@ +from typing import Optional from models.llm_message import LLMSystemMessage, LLMUserMessage from models.presentation_layout import PresentationLayoutModel from models.presentation_outline_model import PresentationOutlineModel @@ -8,7 +9,10 @@ from models.presentation_structure_model import PresentationStructureModel def get_messages( - presentation_layout: PresentationLayoutModel, n_slides: int, data: str + presentation_layout: PresentationLayoutModel, + n_slides: int, + data: str, + instruction: Optional[str] = None, ): return [ LLMSystemMessage( @@ -43,6 +47,9 @@ def get_messages( **Trust your design instincts. Focus on creating the most effective presentation for the content and audience.** + {"# User Instruction:" if instruction else ""} + {instruction or ""} + Select layout index for each of the {n_slides} slides based on what will best serve the presentation's goals. """, ), @@ -57,6 +64,7 @@ def get_messages( async def generate_presentation_structure( presentation_outline: PresentationOutlineModel, presentation_layout: PresentationLayoutModel, + instruction: Optional[str] = None, ) -> PresentationStructureModel: client = LLMClient() @@ -71,6 +79,7 @@ async def generate_presentation_structure( presentation_layout, len(presentation_outline.slides), presentation_outline.to_string(), + instruction, ), response_format=response_model.model_json_schema(), strict=True, diff --git a/servers/fastapi/utils/llm_calls/generate_slide_content.py b/servers/fastapi/utils/llm_calls/generate_slide_content.py index 33bdd993..ce51baf4 100644 --- a/servers/fastapi/utils/llm_calls/generate_slide_content.py +++ b/servers/fastapi/utils/llm_calls/generate_slide_content.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Optional from models.llm_message import LLMSystemMessage, LLMUserMessage from models.presentation_layout import SlideLayoutModel from models.presentation_outline_model import SlideOutlineModel @@ -6,32 +7,37 @@ from services.llm_client import LLMClient from utils.llm_provider import get_model from utils.schema_utils import add_field_in_schema, remove_fields_from_schema -system_prompt = """ - Generate structured slide based on provided outline, follow mentioned steps and notes and provide structured output. - # Steps - 1. Analyze the outline. - 2. Generate structured slide based on the outline. - 3. Generate speaker note that is simple, clear, concise and to the point. +def get_system_prompt(instruction: Optional[str] = None): + return f""" + Generate structured slide based on provided outline, follow mentioned steps and notes and provide structured output. - # Notes - - Slide body should not use words like "This slide", "This presentation". - - Rephrase the slide body to make it flow naturally. - - Only use markdown to highlight important points. - - Make sure to follow language guidelines. - - Speaker note should be normal text, not markdown. - - Strictly follow the max and min character limit for every property in the slide. - - Never ever go over the max character limit. Limit your narration to make sure you never go over the max character limit. - - Number of items should not be more than max number of items specified in slide schema. If you have to put multiple points then merge them to obey max numebr of items. + # Steps + 1. Analyze the outline. + 2. Generate structured slide based on the outline. + 3. Generate speaker note that is simple, clear, concise and to the point. - # Image and Icon Output Format - image: { - __image_prompt__: string, - } - icon: { - __icon_query__: string, - } -""" + # Notes + - Slide body should not use words like "This slide", "This presentation". + - Rephrase the slide body to make it flow naturally. + - Only use markdown to highlight important points. + - Make sure to follow language guidelines. + - Speaker note should be normal text, not markdown. + - Strictly follow the max and min character limit for every property in the slide. + - Never ever go over the max character limit. Limit your narration to make sure you never go over the max character limit. + - Number of items should not be more than max number of items specified in slide schema. If you have to put multiple points then merge them to obey max numebr of items. + + # Image and Icon Output Format + image: {{ + __image_prompt__: string, + }} + icon: {{ + __icon_query__: string, + }} + + {"# User Instruction:" if instruction else ""} + {instruction or ""} + """ def get_user_prompt(outline: str, language: str): @@ -50,11 +56,11 @@ def get_user_prompt(outline: str, language: str): """ -def get_messages(outline: str, language: str): +def get_messages(outline: str, language: str, instruction: Optional[str] = None): return [ LLMSystemMessage( - content=system_prompt, + content=get_system_prompt(instruction), ), LLMUserMessage( content=get_user_prompt(outline, language), @@ -63,7 +69,10 @@ def get_messages(outline: str, language: str): async def get_slide_content_from_type_and_outline( - slide_layout: SlideLayoutModel, outline: SlideOutlineModel, language: str + slide_layout: SlideLayoutModel, + outline: SlideOutlineModel, + language: str, + instruction: Optional[str] = None, ): client = LLMClient() model = get_model() @@ -89,6 +98,7 @@ async def get_slide_content_from_type_and_outline( messages=get_messages( outline.content, language, + instruction, ), response_format=response_schema, strict=False, From 614948887f7c1776ca3981dff85454afac15f489 Mon Sep 17 00:00:00 2001 From: sauravniraula Date: Fri, 29 Aug 2025 10:52:34 +0545 Subject: [PATCH 7/8] feat: adds tone, verbosity and refactors tables --- servers/fastapi/api/v1/ppt/endpoints/files.py | 8 +-- servers/fastapi/api/v1/ppt/endpoints/fonts.py | 4 +- .../fastapi/api/v1/ppt/endpoints/outlines.py | 9 ++- .../api/v1/ppt/endpoints/pdf_slides.py | 6 +- .../api/v1/ppt/endpoints/pptx_slides.py | 6 +- .../api/v1/ppt/endpoints/presentation.py | 64 +++++++++++++------ servers/fastapi/api/v1/ppt/endpoints/slide.py | 11 ++-- .../api/v1/ppt/endpoints/slide_to_html.py | 43 +++++++------ .../models/generate_presentation_request.py | 9 ++- .../fastapi/models/presentation_and_path.py | 3 +- .../models/presentation_from_template.py | 3 +- .../models/presentation_with_slides.py | 31 +++++++-- servers/fastapi/models/sql/image_asset.py | 11 +++- servers/fastapi/models/sql/key_value.py | 5 +- .../fastapi/models/sql/ollama_pull_status.py | 3 +- servers/fastapi/models/sql/presentation.py | 30 +++++++-- .../models/sql/presentation_layout_code.py | 32 ++++++++-- servers/fastapi/models/sql/slide.py | 20 +++--- servers/fastapi/models/sql/template.py | 19 +++++- .../services/image_generation_service.py | 4 +- .../services/pptx_presentation_creator.py | 4 +- servers/fastapi/services/temp_file_service.py | 4 +- servers/fastapi/utils/datetime_utils.py | 5 ++ servers/fastapi/utils/download_helpers.py | 6 +- servers/fastapi/utils/export_utils.py | 9 +-- servers/fastapi/utils/llm_calls/edit_slide.py | 35 ++++++++-- .../generate_presentation_outlines.py | 37 ++++++++--- .../generate_presentation_structure.py | 10 +-- .../utils/llm_calls/generate_slide_content.py | 35 ++++++++-- servers/fastapi/utils/randomizers.py | 5 -- .../services/api/presentation-generation.ts | 6 +- .../template-preview/[slug]/page.tsx | 2 +- .../upload/components/UploadPage.tsx | 2 +- 33 files changed, 333 insertions(+), 148 deletions(-) create mode 100644 servers/fastapi/utils/datetime_utils.py delete mode 100644 servers/fastapi/utils/randomizers.py diff --git a/servers/fastapi/api/v1/ppt/endpoints/files.py b/servers/fastapi/api/v1/ppt/endpoints/files.py index c63b6689..5f7d88ce 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/files.py +++ b/servers/fastapi/api/v1/ppt/endpoints/files.py @@ -7,7 +7,7 @@ from constants.documents import UPLOAD_ACCEPTED_FILE_TYPES from models.decomposed_file_info import DecomposedFileInfo from services.temp_file_service import TEMP_FILE_SERVICE from services.documents_loader import DocumentsLoader -from utils.randomizers import get_random_uuid +import uuid from utils.validators import validate_files FILES_ROUTER = APIRouter(prefix="/files", tags=["Files"]) @@ -18,7 +18,7 @@ async def upload_files(files: Optional[List[UploadFile]]): if not files: raise HTTPException(400, "Documents are required") - temp_dir = TEMP_FILE_SERVICE.create_temp_dir(get_random_uuid()) + temp_dir = TEMP_FILE_SERVICE.create_temp_dir(str(uuid.uuid4())) validate_files(files, True, True, 100, UPLOAD_ACCEPTED_FILE_TYPES) @@ -39,7 +39,7 @@ async def upload_files(files: Optional[List[UploadFile]]): @FILES_ROUTER.post("/decompose", response_model=List[DecomposedFileInfo]) async def decompose_files(file_paths: Annotated[List[str], Body(embed=True)]): - temp_dir = TEMP_FILE_SERVICE.create_temp_dir(get_random_uuid()) + temp_dir = TEMP_FILE_SERVICE.create_temp_dir(str(uuid.uuid4())) txt_files = [] other_files = [] @@ -56,7 +56,7 @@ async def decompose_files(file_paths: Annotated[List[str], Body(embed=True)]): response = [] for index, parsed_doc in enumerate(parsed_documents): file_path = TEMP_FILE_SERVICE.create_temp_file_path( - f"{get_random_uuid()}.txt", temp_dir + f"{uuid.uuid4()}.txt", temp_dir ) parsed_doc = parsed_doc.replace("
", "\n") with open(file_path, "w") as text_file: diff --git a/servers/fastapi/api/v1/ppt/endpoints/fonts.py b/servers/fastapi/api/v1/ppt/endpoints/fonts.py index 9b14fe3a..ecf7ca83 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/fonts.py +++ b/servers/fastapi/api/v1/ppt/endpoints/fonts.py @@ -6,7 +6,7 @@ from typing import List, Dict, Any, Optional from fastapi import APIRouter, HTTPException, File, UploadFile from pydantic import BaseModel from utils.asset_directory_utils import get_app_data_directory_env -from utils.randomizers import get_random_uuid +import uuid try: from fontTools.ttLib import TTFont @@ -157,7 +157,7 @@ async def upload_font( # Generate unique filename to avoid conflicts file_ext = os.path.splitext(font_file.filename)[1].lower() base_name = os.path.splitext(font_file.filename)[0] - unique_filename = f"{base_name}_{get_random_uuid()[:8]}{file_ext}" + unique_filename = f"{base_name}_{str(uuid.uuid4())[:8]}{file_ext}" # Get fonts directory fonts_dir = get_fonts_directory() diff --git a/servers/fastapi/api/v1/ppt/endpoints/outlines.py b/servers/fastapi/api/v1/ppt/endpoints/outlines.py index dc7f813d..5102e7ad 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/outlines.py +++ b/servers/fastapi/api/v1/ppt/endpoints/outlines.py @@ -1,5 +1,6 @@ import asyncio import json +import uuid from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession @@ -18,7 +19,7 @@ OUTLINES_ROUTER = APIRouter(prefix="/outlines", tags=["Outlines"]) @OUTLINES_ROUTER.get("/stream") async def stream_outlines( - presentation_id: str, sql_session: AsyncSession = Depends(get_async_session) + presentation_id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session) ): presentation = await sql_session.get(PresentationModel, presentation_id) @@ -59,11 +60,13 @@ async def stream_outlines( if not presentation_outlines: presentation_outlines_text = "" async for chunk in generate_ppt_outline( - presentation.prompt, + presentation.content, presentation.n_slides, presentation.language, additional_context, - presentation.instruction, + presentation.tone, + presentation.verbosity, + presentation.instructions, ): # Give control to the event loop await asyncio.sleep(0) diff --git a/servers/fastapi/api/v1/ppt/endpoints/pdf_slides.py b/servers/fastapi/api/v1/ppt/endpoints/pdf_slides.py index 56df3c28..0c1173d9 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/pdf_slides.py +++ b/servers/fastapi/api/v1/ppt/endpoints/pdf_slides.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, UploadFile, File, HTTPException from pydantic import BaseModel from utils.asset_directory_utils import get_images_directory -from utils.randomizers import get_random_uuid +import uuid from constants.documents import PDF_MIME_TYPES @@ -68,8 +68,8 @@ async def process_pdf_slides( # Move screenshots to images directory and generate URLs images_dir = get_images_directory() - presentation_id = get_random_uuid() - presentation_images_dir = os.path.join(images_dir, presentation_id) + presentation_id = uuid.uuid4() + presentation_images_dir = os.path.join(images_dir, str(presentation_id)) os.makedirs(presentation_images_dir, exist_ok=True) slides_data = [] diff --git a/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py b/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py index 473c5dca..33e38aec 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py +++ b/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py @@ -13,7 +13,7 @@ import xml.etree.ElementTree as ET import re from utils.asset_directory_utils import get_images_directory -from utils.randomizers import get_random_uuid +import uuid from constants.documents import POWERPOINT_TYPES @@ -308,8 +308,8 @@ async def process_pptx_slides( # Move screenshots to images directory and generate URLs images_dir = get_images_directory() - presentation_id = get_random_uuid() - presentation_images_dir = os.path.join(images_dir, presentation_id) + presentation_id = uuid.uuid4() + presentation_images_dir = os.path.join(images_dir, str(presentation_id)) os.makedirs(presentation_images_dir, exist_ok=True) slides_data = [] diff --git a/servers/fastapi/api/v1/ppt/endpoints/presentation.py b/servers/fastapi/api/v1/ppt/endpoints/presentation.py index da7c749b..35293013 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/presentation.py +++ b/servers/fastapi/api/v1/ppt/endpoints/presentation.py @@ -18,7 +18,9 @@ from models.presentation_outline_model import ( 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.presentation_with_slides import ( + PresentationWithSlides, +) from services.documents_loader import DocumentsLoader from services.score_based_chunker import ScoreBasedChunker @@ -45,7 +47,7 @@ from utils.process_slides import ( process_slide_add_placeholder_assets, process_slide_and_fetch_assets, ) -from utils.randomizers import get_random_uuid +import uuid PRESENTATION_ROUTER = APIRouter(prefix="/presentation", tags=["Presentation"]) @@ -53,7 +55,7 @@ PRESENTATION_ROUTER = APIRouter(prefix="/presentation", tags=["Presentation"]) @PRESENTATION_ROUTER.get("", response_model=PresentationWithSlides) async def get_presentation( - id: str, sql_session: AsyncSession = Depends(get_async_session) + id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session) ): presentation = await sql_session.get(PresentationModel, id) if not presentation: @@ -71,7 +73,7 @@ async def get_presentation( @PRESENTATION_ROUTER.delete("", status_code=204) async def delete_presentation( - id: str, sql_session: AsyncSession = Depends(get_async_session) + id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session) ): presentation = await sql_session.get(PresentationModel, id) if not presentation: @@ -112,10 +114,12 @@ async def create_presentation( n_slides: Annotated[int, Body()], language: Annotated[str, Body()], file_paths: Annotated[Optional[List[str]], Body()] = None, - instruction: Annotated[Optional[str], Body()] = None, + tone: Annotated[Optional[str], Body()] = None, + verbosity: Annotated[Optional[str], Body()] = None, + instructions: Annotated[Optional[str], Body()] = None, sql_session: AsyncSession = Depends(get_async_session), ): - presentation_id = get_random_uuid() + presentation_id = uuid.uuid4() presentation = PresentationModel( id=presentation_id, @@ -123,7 +127,9 @@ async def create_presentation( n_slides=n_slides, language=language, file_paths=file_paths, - instruction=instruction, + tone=tone, + verbosity=verbosity, + instructions=instructions, ) sql_session.add(presentation) @@ -134,7 +140,7 @@ async def create_presentation( @PRESENTATION_ROUTER.post("/prepare", response_model=PresentationModel) async def prepare_presentation( - presentation_id: Annotated[str, Body()], + presentation_id: Annotated[uuid.UUID, Body()], outlines: Annotated[List[SlideOutlineModel], Body()], layout: Annotated[PresentationLayoutModel, Body()], title: Annotated[Optional[str], Body()] = None, @@ -159,7 +165,7 @@ async def prepare_presentation( await generate_presentation_structure( presentation_outline=presentation_outline_model, presentation_layout=layout, - instruction=presentation.instruction, + instructions=presentation.instructions, ) ) @@ -184,7 +190,7 @@ async def prepare_presentation( @PRESENTATION_ROUTER.get("/stream", response_model=PresentationWithSlides) async def stream_presentation( - presentation_id: str, sql_session: AsyncSession = Depends(get_async_session) + presentation_id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session) ): presentation = await sql_session.get(PresentationModel, presentation_id) if not presentation: @@ -222,7 +228,9 @@ async def stream_presentation( slide_layout, outline.slides[i], presentation.language, - presentation.instruction, + presentation.tone, + presentation.verbosity, + presentation.instructions, ) slide = SlideModel( @@ -283,14 +291,22 @@ async def update_presentation( ): updated_presentation = presentation_with_slides.to_presentation_model() updated_slides = presentation_with_slides.slides + presentation = await sql_session.get(PresentationModel, updated_presentation.id) if not presentation: raise HTTPException(status_code=404, detail="Presentation not found") + presentation.sqlmodel_update(updated_presentation) await sql_session.execute( delete(SlideModel).where(SlideModel.presentation == updated_presentation.id) ) + + # Just to make sure id is UUID + for slide in updated_slides: + slide.presentation = uuid.UUID(slide.presentation) + slide.id = uuid.UUID(slide.id) + sql_session.add_all(updated_slides) await sql_session.commit() @@ -311,7 +327,7 @@ async def create_pptx( export_directory = get_exports_directory() pptx_path = os.path.join( - export_directory, f"{pptx_model.name or get_random_uuid()}.pptx" + export_directory, f"{pptx_model.name or uuid.uuid4()}.pptx" ) pptx_creator.save(pptx_path) @@ -323,7 +339,7 @@ async def generate_presentation_api( request: GeneratePresentationRequest, sql_session: AsyncSession = Depends(get_async_session), ): - presentation_id = get_random_uuid() + presentation_id = uuid.uuid4() # 3. Generate Outlines presentation_outlines = None @@ -351,7 +367,10 @@ async def generate_presentation_api( request.n_slides, request.language, additional_context, - request.instruction, + request.tone, + request.verbosity, + request.instructions, + request.web_search, ): presentation_outlines_text += chunk @@ -383,7 +402,7 @@ async def generate_presentation_api( await generate_presentation_structure( presentation_outlines, layout_model, - request.instruction, + request.instructions, ) ) @@ -405,7 +424,9 @@ async def generate_presentation_api( outlines=presentation_outlines.model_dump(), layout=layout_model.model_dump(), structure=presentation_structure.model_dump(), - instruction=request.instruction, + tone=request.tone, + verbosity=request.verbosity, + instructions=request.instructions, ) image_generation_service = ImageGenerationService(get_images_directory()) @@ -418,7 +439,12 @@ async def generate_presentation_api( slide_layout = layout_model.slides[slide_layout_index] print(f"Generating content for slide {i} with layout {slide_layout.id}") slide_content = await get_slide_content_from_type_and_outline( - slide_layout, outlines[i], request.language, request.instruction + slide_layout, + outlines[i], + request.language, + request.tone, + request.verbosity, + request.instructions, ) slide = SlideModel( presentation=presentation_id, @@ -447,7 +473,7 @@ async def generate_presentation_api( # 9. Export presentation_and_path = await export_presentation( - presentation_id, presentation.title or get_random_uuid(), request.export_as + presentation_id, presentation.title or str(uuid.uuid4()), request.export_as ) return PresentationPathAndEditPath( @@ -484,7 +510,7 @@ async def from_template( await sql_session.commit() presentation_and_path = await export_presentation( - new_presentation.id, new_presentation.title or get_random_uuid(), data.export_as + new_presentation.id, new_presentation.title or str(uuid.uuid4()), data.export_as ) return PresentationPathAndEditPath( diff --git a/servers/fastapi/api/v1/ppt/endpoints/slide.py b/servers/fastapi/api/v1/ppt/endpoints/slide.py index 7d42b884..704c2eb6 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/slide.py +++ b/servers/fastapi/api/v1/ppt/endpoints/slide.py @@ -1,6 +1,7 @@ from typing import Annotated, Optional from fastapi import APIRouter, Body, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession +import uuid from models.sql.presentation import PresentationModel from models.sql.slide import SlideModel @@ -11,7 +12,7 @@ from utils.llm_calls.edit_slide import get_edited_slide_content from utils.llm_calls.edit_slide_html import get_edited_slide_html from utils.llm_calls.select_slide_type_on_edit import get_slide_layout_from_prompt from utils.process_slides import process_old_and_new_slides_and_fetch_assets -from utils.randomizers import get_random_uuid +import uuid SLIDE_ROUTER = APIRouter(prefix="/slide", tags=["Slide"]) @@ -19,7 +20,7 @@ SLIDE_ROUTER = APIRouter(prefix="/slide", tags=["Slide"]) @SLIDE_ROUTER.post("/edit") async def edit_slide( - id: Annotated[str, Body()], + id: Annotated[uuid.UUID, Body()], prompt: Annotated[str, Body()], sql_session: AsyncSession = Depends(get_async_session), ): @@ -49,7 +50,7 @@ async def edit_slide( ) # Always assign a new unique id to the slide - slide.id = get_random_uuid() + slide.id = uuid.uuid4() sql_session.add(slide) slide.content = edited_slide_content @@ -63,7 +64,7 @@ async def edit_slide( @SLIDE_ROUTER.post("/edit-html", response_model=SlideModel) async def edit_slide_html( - id: Annotated[str, Body()], + id: Annotated[uuid.UUID, Body()], prompt: Annotated[str, Body()], html: Annotated[Optional[str], Body()] = None, sql_session: AsyncSession = Depends(get_async_session), @@ -80,7 +81,7 @@ async def edit_slide_html( # Always assign a new unique id to the slide # This is to ensure that the nextjs can track slide updates - slide.id = get_random_uuid() + slide.id = uuid.uuid4() sql_session.add(slide) slide.html_content = edited_slide_html diff --git a/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py b/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py index 0ffc6a6a..dced58f0 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py +++ b/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py @@ -2,6 +2,7 @@ import os import base64 from datetime import datetime from typing import Optional, List, Dict +from uuid import UUID from fastapi import APIRouter, HTTPException, File, UploadFile, Form, Depends from pydantic import BaseModel from openai import OpenAI @@ -54,7 +55,7 @@ class HtmlToReactResponse(BaseModel): # Request/Response models for layout management endpoints class LayoutData(BaseModel): - presentation_id: str # UUID of the presentation + presentation: UUID # UUID of the presentation layout_id: str # Unique identifier for the layout layout_name: str # Display name of the layout layout_code: str # TSX/React component code for the layout @@ -80,7 +81,7 @@ class GetLayoutsResponse(BaseModel): class PresentationSummary(BaseModel): - presentation_id: str + presentation_id: UUID layout_count: int last_updated_at: Optional[datetime] = None template: Optional[dict] = None @@ -101,7 +102,7 @@ class ErrorResponse(BaseModel): class TemplateCreateRequest(BaseModel): - id: str + id: UUID name: str description: Optional[str] = None @@ -113,7 +114,7 @@ class TemplateCreateResponse(BaseModel): class TemplateInfo(BaseModel): - id: str + id: UUID name: Optional[str] = None description: Optional[str] = None created_at: Optional[datetime] = None @@ -688,7 +689,7 @@ async def save_layouts( for i, layout_data in enumerate(request.layouts): # Validate individual layout data - if not layout_data.presentation_id or not layout_data.presentation_id.strip(): + if not layout_data.presentation or not str(layout_data.presentation).strip(): raise HTTPException( status_code=400, detail=f"Layout {i+1}: presentation_id cannot be empty" @@ -714,7 +715,7 @@ async def save_layouts( # Check if layout already exists for this presentation and layout_id stmt = select(PresentationLayoutCodeModel).where( - PresentationLayoutCodeModel.presentation_id == layout_data.presentation_id, + PresentationLayoutCodeModel.presentation == layout_data.presentation, PresentationLayoutCodeModel.layout_id == layout_data.layout_id ) result = await session.execute(stmt) @@ -729,7 +730,7 @@ async def save_layouts( else: # Create new layout new_layout = PresentationLayoutCodeModel( - presentation_id=layout_data.presentation_id, + presentation=layout_data.presentation, layout_id=layout_data.layout_id, layout_name=layout_data.layout_name, layout_code=layout_data.layout_code, @@ -762,7 +763,7 @@ async def save_layouts( # ENDPOINT 5: Get layouts for a presentation @LAYOUT_MANAGEMENT_ROUTER.get( - "/get-templates/{presentation_id}", + "/get-templates/{presentation}", response_model=GetLayoutsResponse, responses={ 400: {"model": ErrorResponse, "description": "Invalid presentation ID"}, @@ -771,14 +772,14 @@ async def save_layouts( } ) async def get_layouts( - presentation_id: str, + presentation: UUID, session: AsyncSession = Depends(get_async_session) ): """ Retrieve all layouts for a specific presentation. Args: - presentation_id: UUID of the presentation + presentation: UUID of the presentation session: Database session Returns: @@ -789,7 +790,7 @@ async def get_layouts( """ try: # Validate presentation_id format (basic UUID check) - if not presentation_id or len(presentation_id.strip()) == 0: + if not presentation or len(str(presentation).strip()) == 0: raise HTTPException( status_code=400, detail="Presentation ID cannot be empty" @@ -797,7 +798,7 @@ async def get_layouts( # Query layouts for the given presentation_id stmt = select(PresentationLayoutCodeModel).where( - PresentationLayoutCodeModel.presentation_id == presentation_id + PresentationLayoutCodeModel.presentation == presentation ) result = await session.execute(stmt) layouts_db = result.scalars().all() @@ -806,13 +807,13 @@ async def get_layouts( if not layouts_db: raise HTTPException( status_code=404, - detail=f"No layouts found for presentation ID: {presentation_id}" + detail=f"No layouts found for presentation ID: {presentation}" ) # Convert to response format layouts = [ LayoutData( - presentation_id=layout.presentation_id, + presentation=layout.presentation, layout_id=layout.layout_id, layout_name=layout.layout_name, layout_code=layout.layout_code, @@ -829,7 +830,7 @@ async def get_layouts( fonts_list = sorted(list(aggregated_fonts)) if aggregated_fonts else None # Fetch template meta - template_meta = await session.get(TemplateModel, presentation_id) + template_meta = await session.get(TemplateModel, presentation) template = None if template_meta: template = { @@ -842,7 +843,7 @@ async def get_layouts( return GetLayoutsResponse( success=True, layouts=layouts, - message=f"Retrieved {len(layouts)} layout(s) for presentation {presentation_id}", + message=f"Retrieved {len(layouts)} layout(s) for presentation {presentation}", template=template, fonts=fonts_list, ) @@ -851,7 +852,7 @@ async def get_layouts( # Re-raise HTTP exceptions as-is raise except Exception as e: - print(f"Error retrieving layouts for presentation {presentation_id}: {str(e)}") + print(f"Error retrieving layouts for presentation {presentation}: {str(e)}") raise HTTPException( status_code=500, detail=f"Internal server error while retrieving layouts: {str(e)}" @@ -878,10 +879,10 @@ async def get_presentations_summary( try: # Query to get presentation_id, count of layouts, and MAX(updated_at) stmt = select( - PresentationLayoutCodeModel.presentation_id, + PresentationLayoutCodeModel.presentation, func.count(PresentationLayoutCodeModel.id).label('layout_count'), func.max(PresentationLayoutCodeModel.updated_at).label('last_updated_at') - ).group_by(PresentationLayoutCodeModel.presentation_id) + ).group_by(PresentationLayoutCodeModel.presentation) result = await session.execute(stmt) presentation_data = result.all() @@ -889,7 +890,7 @@ async def get_presentations_summary( # Convert to response format with template info if available presentations = [] for row in presentation_data: - template_meta = await session.get(TemplateModel, row.presentation_id) + template_meta = await session.get(TemplateModel, row.presentation) template = None if template_meta: template = { @@ -900,7 +901,7 @@ async def get_presentations_summary( } presentations.append( PresentationSummary( - presentation_id=row.presentation_id, + presentation=row.presentation, layout_count=row.layout_count, last_updated_at=row.last_updated_at, template=template, diff --git a/servers/fastapi/models/generate_presentation_request.py b/servers/fastapi/models/generate_presentation_request.py index d210c4a7..f3f0dc16 100644 --- a/servers/fastapi/models/generate_presentation_request.py +++ b/servers/fastapi/models/generate_presentation_request.py @@ -4,9 +4,16 @@ from pydantic import BaseModel, Field class GeneratePresentationRequest(BaseModel): content: str = Field(..., description="The content for generating the presentation") - instruction: Optional[str] = Field( + instructions: Optional[str] = Field( default=None, description="The instruction for generating the presentation" ) + tone: Optional[str] = Field( + default=None, description="The tone for the presentation" + ) + verbosity: Optional[str] = Field( + default=None, description="The verbosity for the presentation" + ) + web_search: bool = Field(default=False, description="Whether to enable web search") n_slides: int = Field(default=8, description="Number of slides to generate") language: str = Field( default="English", description="Language for the presentation" diff --git a/servers/fastapi/models/presentation_and_path.py b/servers/fastapi/models/presentation_and_path.py index e85c6c3d..3f2cd7ab 100644 --- a/servers/fastapi/models/presentation_and_path.py +++ b/servers/fastapi/models/presentation_and_path.py @@ -1,8 +1,9 @@ from pydantic import BaseModel +import uuid class PresentationAndPath(BaseModel): - presentation_id: str + presentation_id: uuid.UUID path: str diff --git a/servers/fastapi/models/presentation_from_template.py b/servers/fastapi/models/presentation_from_template.py index 9a51c12d..6ec7d462 100644 --- a/servers/fastapi/models/presentation_from_template.py +++ b/servers/fastapi/models/presentation_from_template.py @@ -1,5 +1,6 @@ from typing import List, Literal from pydantic import BaseModel +import uuid class SlideContentUpdate(BaseModel): @@ -8,6 +9,6 @@ class SlideContentUpdate(BaseModel): class GetPresentationUsingTemplateRequest(BaseModel): - presentation_id: str + presentation_id: uuid.UUID data: List[SlideContentUpdate] export_as: Literal["pptx", "pdf"] = "pptx" diff --git a/servers/fastapi/models/presentation_with_slides.py b/servers/fastapi/models/presentation_with_slides.py index c6edde76..481aaf8e 100644 --- a/servers/fastapi/models/presentation_with_slides.py +++ b/servers/fastapi/models/presentation_with_slides.py @@ -1,5 +1,6 @@ from typing import List, Optional from datetime import datetime +import uuid from pydantic import BaseModel @@ -11,18 +12,38 @@ from models.sql.slide import SlideModel class PresentationWithSlides(BaseModel): - id: str + id: uuid.UUID content: str n_slides: int language: str + file_paths: Optional[List[str]] title: Optional[str] = None - outlines: Optional[PresentationOutlineModel] + outlines: Optional[PresentationOutlineModel] = None created_at: datetime updated_at: datetime layout: Optional[PresentationLayoutModel] - structure: Optional[PresentationStructureModel] - instruction: Optional[str] = None + structure: Optional[PresentationStructureModel] = None + instructions: Optional[str] = None + tone: Optional[str] = None + verbosity: Optional[str] = None slides: List[SlideModel] def to_presentation_model(self) -> PresentationModel: - return PresentationModel(**self.model_dump()) + return PresentationModel( + id=self.id, + content=self.content, + n_slides=self.n_slides, + language=self.language, + file_paths=self.file_paths, + title=self.title, + outlines=self.outlines.model_dump(mode="json") if self.outlines else None, + created_at=self.created_at, + updated_at=self.updated_at, + layout=self.layout.model_dump(mode="json") if self.layout else None, + structure=( + self.structure.model_dump(mode="json") if self.structure else None + ), + instructions=self.instructions, + tone=self.tone, + verbosity=self.verbosity, + ) diff --git a/servers/fastapi/models/sql/image_asset.py b/servers/fastapi/models/sql/image_asset.py index 00939c87..4665e031 100644 --- a/servers/fastapi/models/sql/image_asset.py +++ b/servers/fastapi/models/sql/image_asset.py @@ -1,14 +1,19 @@ from datetime import datetime from typing import Optional +import uuid from sqlalchemy import JSON, Column, DateTime from sqlmodel import Field, SQLModel -from utils.randomizers import get_random_uuid +from utils.datetime_utils import get_current_utc_datetime class ImageAsset(SQLModel, table=True): - id: str = Field(default_factory=get_random_uuid, primary_key=True) - created_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now)) + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + created_at: datetime = Field( + sa_column=Column( + DateTime(timezone=True), nullable=False, default=get_current_utc_datetime + ), + ) path: str extras: Optional[dict] = Field(sa_column=Column(JSON), default=None) diff --git a/servers/fastapi/models/sql/key_value.py b/servers/fastapi/models/sql/key_value.py index 3ecabf39..c7296ac2 100644 --- a/servers/fastapi/models/sql/key_value.py +++ b/servers/fastapi/models/sql/key_value.py @@ -1,9 +1,8 @@ +import uuid from sqlmodel import Field, Column, JSON, SQLModel -from utils.randomizers import get_random_uuid - class KeyValueSqlModel(SQLModel, table=True): - id: str = Field(default_factory=get_random_uuid, primary_key=True) + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) key: str = Field(index=True) value: dict = Field(sa_column=Column(JSON)) diff --git a/servers/fastapi/models/sql/ollama_pull_status.py b/servers/fastapi/models/sql/ollama_pull_status.py index 59599cae..13c240df 100644 --- a/servers/fastapi/models/sql/ollama_pull_status.py +++ b/servers/fastapi/models/sql/ollama_pull_status.py @@ -1,8 +1,9 @@ from datetime import datetime +import uuid from sqlmodel import Field, Column, JSON, SQLModel, DateTime class OllamaPullStatus(SQLModel, table=True): - id: str = Field(primary_key=True) + id: uuid.UUID = Field(primary_key=True, default_factory=uuid.uuid4) last_updated: datetime = Field(sa_column=Column(DateTime, default=datetime.now)) status: dict = Field(sa_column=Column(JSON)) diff --git a/servers/fastapi/models/sql/presentation.py b/servers/fastapi/models/sql/presentation.py index e565d9a0..14d99736 100644 --- a/servers/fastapi/models/sql/presentation.py +++ b/servers/fastapi/models/sql/presentation.py @@ -1,31 +1,47 @@ from datetime import datetime from typing import List, Optional +import uuid from sqlalchemy import JSON, Column, DateTime, String from sqlmodel import Field, SQLModel from models.presentation_layout import PresentationLayoutModel from models.presentation_outline_model import PresentationOutlineModel from models.presentation_structure_model import PresentationStructureModel -from utils.randomizers import get_random_uuid +from utils.datetime_utils import get_current_utc_datetime class PresentationModel(SQLModel, table=True): - id: str = Field(primary_key=True) + __tablename__ = "presentations" + + id: uuid.UUID = Field(primary_key=True, default_factory=uuid.uuid4) content: str n_slides: int language: str title: Optional[str] = None file_paths: Optional[List[str]] = Field(sa_column=Column(JSON), default=None) outlines: Optional[dict] = Field(sa_column=Column(JSON), default=None) - created_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now)) - updated_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now)) + created_at: datetime = Field( + sa_column=Column( + DateTime(timezone=True), nullable=False, default=get_current_utc_datetime + ), + ) + updated_at: datetime = Field( + sa_column=Column( + DateTime(timezone=True), + nullable=False, + default=get_current_utc_datetime, + onupdate=get_current_utc_datetime, + ), + ) layout: Optional[dict] = Field(sa_column=Column(JSON), default=None) structure: Optional[dict] = Field(sa_column=Column(JSON), default=None) - instruction: Optional[str] = Field(sa_column=Column(String), default=None) + instructions: Optional[str] = Field(sa_column=Column(String), default=None) + tone: Optional[str] = Field(sa_column=Column(String), default=None) + verbosity: Optional[str] = Field(sa_column=Column(String), default=None) def get_new_presentation(self): return PresentationModel( - id=get_random_uuid(), + id=uuid.uuid4(), content=self.content, n_slides=self.n_slides, language=self.language, @@ -34,7 +50,7 @@ class PresentationModel(SQLModel, table=True): outlines=self.outlines, layout=self.layout, structure=self.structure, - instruction=self.instruction, + instructions=self.instructions, ) def get_presentation_outline(self): diff --git a/servers/fastapi/models/sql/presentation_layout_code.py b/servers/fastapi/models/sql/presentation_layout_code.py index 19089b47..fe57c01e 100644 --- a/servers/fastapi/models/sql/presentation_layout_code.py +++ b/servers/fastapi/models/sql/presentation_layout_code.py @@ -1,19 +1,37 @@ from datetime import datetime from typing import Optional, List +import uuid from sqlalchemy import Column, DateTime, Text, JSON from sqlmodel import SQLModel, Field +from utils.datetime_utils import get_current_utc_datetime + class PresentationLayoutCodeModel(SQLModel, table=True): """Model for storing presentation layout codes""" - + __tablename__ = "presentation_layout_codes" - + id: Optional[int] = Field(default=None, primary_key=True) - presentation_id: str = Field(index=True, description="UUID of the presentation") + presentation: uuid.UUID = Field(index=True, description="UUID of the presentation") layout_id: str = Field(description="Unique identifier for the layout") layout_name: str = Field(description="Display name of the layout") - layout_code: str = Field(sa_column=Column(Text), description="TSX/React component code for the layout") - fonts: Optional[List[str]] = Field(sa_column=Column(JSON), default=None, description="Optional list of font links") - created_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now)) - updated_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now, onupdate=datetime.now)) \ No newline at end of file + layout_code: str = Field( + sa_column=Column(Text), description="TSX/React component code for the layout" + ) + fonts: Optional[List[str]] = Field( + sa_column=Column(JSON), default=None, description="Optional list of font links" + ) + created_at: datetime = Field( + sa_column=Column( + DateTime(timezone=True), nullable=False, default=get_current_utc_datetime + ) + ) + updated_at: datetime = Field( + sa_column=Column( + DateTime(timezone=True), + nullable=False, + default=get_current_utc_datetime, + onupdate=get_current_utc_datetime, + ), + ) diff --git a/servers/fastapi/models/sql/slide.py b/servers/fastapi/models/sql/slide.py index 5d859d82..f91ba8cf 100644 --- a/servers/fastapi/models/sql/slide.py +++ b/servers/fastapi/models/sql/slide.py @@ -1,24 +1,28 @@ from typing import Optional +import uuid +from sqlalchemy import ForeignKey from sqlmodel import Field, Column, JSON, SQLModel -from utils.randomizers import get_random_uuid - class SlideModel(SQLModel, table=True): - id: str = Field(primary_key=True, default_factory=get_random_uuid) - presentation: str + __tablename__ = "slides" + + id: uuid.UUID = Field(primary_key=True, default_factory=uuid.uuid4) + presentation: uuid.UUID = Field( + sa_column=Column(ForeignKey("presentations.id", ondelete="CASCADE"), index=True) + ) layout_group: str layout: str index: int content: dict = Field(sa_column=Column(JSON)) html_content: Optional[str] - speaker_note: str + speaker_note: Optional[str] = None properties: Optional[dict] = Field(sa_column=Column(JSON)) - def get_new_slide(self, presentation_id: str, content: Optional[dict] = None): + def get_new_slide(self, presentation: uuid.UUID, content: Optional[dict] = None): return SlideModel( - id=get_random_uuid(), - presentation=presentation_id, + id=uuid.uuid4(), + presentation=presentation, layout_group=self.layout_group, layout=self.layout, index=self.index, diff --git a/servers/fastapi/models/sql/template.py b/servers/fastapi/models/sql/template.py index 8a1e7457..a5ca53fe 100644 --- a/servers/fastapi/models/sql/template.py +++ b/servers/fastapi/models/sql/template.py @@ -1,13 +1,26 @@ from datetime import datetime from typing import Optional +import uuid from sqlalchemy import Column, DateTime from sqlmodel import SQLModel, Field +from utils.datetime_utils import get_current_utc_datetime + class TemplateModel(SQLModel, table=True): __tablename__ = "templates" - id: str = Field(primary_key=True, description="UUID for the template (matches presentation_id)") + id: uuid.UUID = Field( + default_factory=uuid.uuid4, + primary_key=True, + description="UUID for the template (matches presentation_id)", + ) name: str = Field(description="Human friendly template name") - description: Optional[str] = Field(default=None, description="Optional template description") - created_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now)) \ No newline at end of file + description: Optional[str] = Field( + default=None, description="Optional template description" + ) + created_at: datetime = Field( + sa_column=Column( + DateTime(timezone=True), nullable=False, default=get_current_utc_datetime + ), + ) diff --git a/servers/fastapi/services/image_generation_service.py b/servers/fastapi/services/image_generation_service.py index 33af29b8..855ff2da 100644 --- a/servers/fastapi/services/image_generation_service.py +++ b/servers/fastapi/services/image_generation_service.py @@ -15,7 +15,7 @@ from utils.image_provider import ( is_gemini_flash_selected, is_dalle3_selected, ) -from utils.randomizers import get_random_uuid +import uuid class ImageGenerationService: @@ -104,7 +104,7 @@ class ImageGenerationService: if part.text is not None: print(part.text) elif part.inline_data is not None: - image_path = os.path.join(output_directory, f"{get_random_uuid()}.jpg") + image_path = os.path.join(output_directory, f"{uuid.uuid4()}.jpg") with open(image_path, "wb") as f: f.write(part.inline_data.data) diff --git a/servers/fastapi/services/pptx_presentation_creator.py b/servers/fastapi/services/pptx_presentation_creator.py index 6563bd89..83d4df61 100644 --- a/servers/fastapi/services/pptx_presentation_creator.py +++ b/servers/fastapi/services/pptx_presentation_creator.py @@ -43,7 +43,7 @@ from utils.image_utils import ( round_image_corners, set_image_opacity, ) -from utils.randomizers import get_random_uuid +import uuid BLANK_SLIDE_LAYOUT = 6 @@ -216,7 +216,7 @@ class PptxPresentationCreator: image = invert_image(image) if picture_model.opacity: image = set_image_opacity(image, picture_model.opacity) - image_path = os.path.join(self._temp_dir, f"{get_random_uuid()}.png") + image_path = os.path.join(self._temp_dir, f"{uuid.uuid4()}.png") image.save(image_path) margined_position = self.get_margined_position( diff --git a/servers/fastapi/services/temp_file_service.py b/servers/fastapi/services/temp_file_service.py index c687f708..c8a6651a 100644 --- a/servers/fastapi/services/temp_file_service.py +++ b/servers/fastapi/services/temp_file_service.py @@ -2,7 +2,7 @@ import os from typing import Optional, Union from utils.get_env import get_temp_directory_env -from utils.randomizers import get_random_uuid +import uuid class TempFileService: @@ -13,7 +13,7 @@ class TempFileService: os.makedirs(self.base_dir, exist_ok=True) def create_dir_in_dir(self, base_dir: str, dir_name: Optional[str] = None) -> str: - temp_dir = os.path.join(base_dir, dir_name if dir_name else get_random_uuid()) + temp_dir = os.path.join(base_dir, dir_name if dir_name else str(uuid.uuid4())) os.makedirs(temp_dir, exist_ok=True) return temp_dir diff --git a/servers/fastapi/utils/datetime_utils.py b/servers/fastapi/utils/datetime_utils.py new file mode 100644 index 00000000..16652e7e --- /dev/null +++ b/servers/fastapi/utils/datetime_utils.py @@ -0,0 +1,5 @@ +from datetime import datetime, timezone + + +def get_current_utc_datetime(): + return datetime.now(timezone.utc) diff --git a/servers/fastapi/utils/download_helpers.py b/servers/fastapi/utils/download_helpers.py index 697a9394..e5843927 100644 --- a/servers/fastapi/utils/download_helpers.py +++ b/servers/fastapi/utils/download_helpers.py @@ -6,7 +6,7 @@ from urllib.parse import urlparse import aiohttp -from utils.randomizers import get_random_uuid +import uuid async def download_file( @@ -36,9 +36,9 @@ async def download_file( content_type.split(";")[0] ) if extension: - filename = f"{get_random_uuid()}{extension}" + filename = f"{uuid.uuid4()}{extension}" - filename = filename or get_random_uuid() + filename = filename or str(uuid.uuid4()) save_path = os.path.join(save_directory, filename) async with aiohttp.ClientSession(trust_env=True) as session: diff --git a/servers/fastapi/utils/export_utils.py b/servers/fastapi/utils/export_utils.py index e75f49cc..f59c92aa 100644 --- a/servers/fastapi/utils/export_utils.py +++ b/servers/fastapi/utils/export_utils.py @@ -2,6 +2,7 @@ import json import os import aiohttp from typing import Literal +import uuid from fastapi import HTTPException from pathvalidate import sanitize_filename @@ -10,11 +11,11 @@ from models.presentation_and_path import PresentationAndPath from services.pptx_presentation_creator import PptxPresentationCreator from services.temp_file_service import TEMP_FILE_SERVICE from utils.asset_directory_utils import get_exports_directory -from utils.randomizers import get_random_uuid +import uuid async def export_presentation( - presentation_id: str, title: str, export_as: Literal["pptx", "pdf"] + presentation_id: uuid.UUID, title: str, export_as: Literal["pptx", "pdf"] ) -> PresentationAndPath: if export_as == "pptx": @@ -41,7 +42,7 @@ async def export_presentation( export_directory = get_exports_directory() pptx_path = os.path.join( export_directory, - f"{sanitize_filename(title or get_random_uuid())}.pptx", + f"{sanitize_filename(title or str(uuid.uuid4()))}.pptx", ) pptx_creator.save(pptx_path) @@ -55,7 +56,7 @@ async def export_presentation( "http://localhost/api/export-as-pdf", json={ "id": presentation_id, - "title": sanitize_filename(title or get_random_uuid()), + "title": sanitize_filename(title or str(uuid.uuid4())), }, ) as response: response_json = await response.json() diff --git a/servers/fastapi/utils/llm_calls/edit_slide.py b/servers/fastapi/utils/llm_calls/edit_slide.py index 5d91a607..6123e443 100644 --- a/servers/fastapi/utils/llm_calls/edit_slide.py +++ b/servers/fastapi/utils/llm_calls/edit_slide.py @@ -1,3 +1,5 @@ +from datetime import datetime +from typing import Optional from models.llm_message import LLMSystemMessage, LLMUserMessage from models.presentation_layout import SlideLayoutModel from models.sql.slide import SlideModel @@ -5,9 +7,23 @@ from services.llm_client import LLMClient from utils.llm_provider import get_model from utils.schema_utils import add_field_in_schema, remove_fields_from_schema -system_prompt = """ + +def get_system_prompt( + tone: Optional[str] = None, + verbosity: Optional[str] = None, + instructions: Optional[str] = None, +): + return f""" Edit Slide data and speaker note based on provided prompt, follow mentioned steps and notes and provide structured output. + {"# User Instruction:" if instructions else ""} + {instructions or ""} + + {"# Tone:" if tone else ""} + {tone or ""} + + {"# Verbosity:" if verbosity else ""} + {verbosity or ""} # Notes - Provide output in language mentioned in **Input**. @@ -19,7 +35,7 @@ system_prompt = """ - Speaker note should be simple, clear, concise and to the point. **Go through all notes and steps and make sure they are followed, including mentioned constraints** -""" + """ def get_user_prompt(prompt: str, slide_data: dict, language: str): @@ -27,6 +43,9 @@ def get_user_prompt(prompt: str, slide_data: dict, language: str): ## Icon Query And Image Prompt Language English + ## Current Date and Time + {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} + ## Slide Content Language {language} @@ -42,10 +61,13 @@ def get_messages( prompt: str, slide_data: dict, language: str, + tone: Optional[str] = None, + verbosity: Optional[str] = None, + instructions: Optional[str] = None, ): return [ LLMSystemMessage( - content=system_prompt, + content=get_system_prompt(tone, verbosity, instructions), ), LLMUserMessage( content=get_user_prompt(prompt, slide_data, language), @@ -58,6 +80,9 @@ async def get_edited_slide_content( slide: SlideModel, language: str, slide_layout: SlideLayoutModel, + tone: Optional[str] = None, + verbosity: Optional[str] = None, + instructions: Optional[str] = None, ): model = get_model() @@ -80,7 +105,9 @@ async def get_edited_slide_content( client = LLMClient() response = await client.generate_structured( model=model, - messages=get_messages(prompt, slide.content, language), + messages=get_messages( + prompt, slide.content, language, tone, verbosity, instructions + ), response_format=response_schema, strict=False, ) diff --git a/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py b/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py index 7c57e734..d70a05d4 100644 --- a/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py +++ b/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py @@ -8,21 +8,31 @@ from utils.get_dynamic_models import get_presentation_outline_model_with_n_slide from utils.llm_provider import get_model -def get_system_prompt(instruction: Optional[str] = None): +def get_system_prompt( + tone: Optional[str] = None, + verbosity: Optional[str] = None, + instructions: Optional[str] = None, +): return f""" You are an expert presentation creator. Generate structured presentations based on user requirements and format them according to the specified JSON schema with markdown content. Try to use available tools for better results. + {"# User Instruction:" if instructions else ""} + {instructions or ""} + + {"# Tone:" if tone else ""} + {tone or ""} + + {"# Verbosity:" if verbosity else ""} + {verbosity or ""} + - Provide content for each slide in markdown format. - Make sure that flow of the presentation is logical and consistent. - Place greater emphasis on numerical data. - If Additional Information is provided, divide it into slides. - Make sure no images are provided in the content. - Make sure that content follows language guidelines. - - {"# User Instruction:" if instruction else ""} - {instruction or ""} """ @@ -47,11 +57,13 @@ def get_messages( n_slides: int, language: str, additional_context: Optional[str] = None, - instruction: Optional[str] = None, + tone: Optional[str] = None, + verbosity: Optional[str] = None, + instructions: Optional[str] = None, ): return [ LLMSystemMessage( - content=get_system_prompt(instruction), + content=get_system_prompt(tone, verbosity, instructions), ), LLMUserMessage( content=get_user_prompt(content, n_slides, language, additional_context), @@ -64,7 +76,10 @@ async def generate_ppt_outline( n_slides: int, language: Optional[str] = None, additional_context: Optional[str] = None, - instruction: Optional[str] = None, + tone: Optional[str] = None, + verbosity: Optional[str] = None, + instructions: Optional[str] = None, + web_search: bool = False, ): model = get_model() response_model = get_presentation_outline_model_with_n_slides(n_slides) @@ -78,10 +93,14 @@ async def generate_ppt_outline( n_slides, language, additional_context, - instruction, + tone, + verbosity, + instructions, ), response_model.model_json_schema(), strict=True, - tools=[SearchWebTool] if client.enable_web_grounding() else None, + tools=( + [SearchWebTool] if (client.enable_web_grounding() and web_search) else None + ), ): yield chunk diff --git a/servers/fastapi/utils/llm_calls/generate_presentation_structure.py b/servers/fastapi/utils/llm_calls/generate_presentation_structure.py index 42a398fc..33464b4e 100644 --- a/servers/fastapi/utils/llm_calls/generate_presentation_structure.py +++ b/servers/fastapi/utils/llm_calls/generate_presentation_structure.py @@ -12,7 +12,7 @@ def get_messages( presentation_layout: PresentationLayoutModel, n_slides: int, data: str, - instruction: Optional[str] = None, + instructions: Optional[str] = None, ): return [ LLMSystemMessage( @@ -47,8 +47,8 @@ def get_messages( **Trust your design instincts. Focus on creating the most effective presentation for the content and audience.** - {"# User Instruction:" if instruction else ""} - {instruction or ""} + {"# User Instruction:" if instructions else ""} + {instructions or ""} Select layout index for each of the {n_slides} slides based on what will best serve the presentation's goals. """, @@ -64,7 +64,7 @@ def get_messages( async def generate_presentation_structure( presentation_outline: PresentationOutlineModel, presentation_layout: PresentationLayoutModel, - instruction: Optional[str] = None, + instructions: Optional[str] = None, ) -> PresentationStructureModel: client = LLMClient() @@ -79,7 +79,7 @@ async def generate_presentation_structure( presentation_layout, len(presentation_outline.slides), presentation_outline.to_string(), - instruction, + instructions, ), response_format=response_model.model_json_schema(), strict=True, diff --git a/servers/fastapi/utils/llm_calls/generate_slide_content.py b/servers/fastapi/utils/llm_calls/generate_slide_content.py index ce51baf4..3f702161 100644 --- a/servers/fastapi/utils/llm_calls/generate_slide_content.py +++ b/servers/fastapi/utils/llm_calls/generate_slide_content.py @@ -8,10 +8,23 @@ from utils.llm_provider import get_model from utils.schema_utils import add_field_in_schema, remove_fields_from_schema -def get_system_prompt(instruction: Optional[str] = None): +def get_system_prompt( + tone: Optional[str] = None, + verbosity: Optional[str] = None, + instructions: Optional[str] = None, +): return f""" Generate structured slide based on provided outline, follow mentioned steps and notes and provide structured output. + {"# User Instructions:" if instructions else ""} + {instructions or ""} + + {"# Tone:" if tone else ""} + {tone or ""} + + {"# Verbosity:" if verbosity else ""} + {verbosity or ""} + # Steps 1. Analyze the outline. 2. Generate structured slide based on the outline. @@ -35,8 +48,6 @@ def get_system_prompt(instruction: Optional[str] = None): __icon_query__: string, }} - {"# User Instruction:" if instruction else ""} - {instruction or ""} """ @@ -56,11 +67,17 @@ def get_user_prompt(outline: str, language: str): """ -def get_messages(outline: str, language: str, instruction: Optional[str] = None): +def get_messages( + outline: str, + language: str, + tone: Optional[str] = None, + verbosity: Optional[str] = None, + instructions: Optional[str] = None, +): return [ LLMSystemMessage( - content=get_system_prompt(instruction), + content=get_system_prompt(tone, verbosity, instructions), ), LLMUserMessage( content=get_user_prompt(outline, language), @@ -72,7 +89,9 @@ async def get_slide_content_from_type_and_outline( slide_layout: SlideLayoutModel, outline: SlideOutlineModel, language: str, - instruction: Optional[str] = None, + tone: Optional[str] = None, + verbosity: Optional[str] = None, + instructions: Optional[str] = None, ): client = LLMClient() model = get_model() @@ -98,7 +117,9 @@ async def get_slide_content_from_type_and_outline( messages=get_messages( outline.content, language, - instruction, + tone, + verbosity, + instructions, ), response_format=response_schema, strict=False, diff --git a/servers/fastapi/utils/randomizers.py b/servers/fastapi/utils/randomizers.py deleted file mode 100644 index 06c8dc27..00000000 --- a/servers/fastapi/utils/randomizers.py +++ /dev/null @@ -1,5 +0,0 @@ -import uuid - - -def get_random_uuid(): - return str(uuid.uuid4()) diff --git a/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts b/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts index ec27d9e3..3b3f3b0f 100644 --- a/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts +++ b/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts @@ -50,12 +50,12 @@ export class PresentationGenerationApi { } static async createPresentation({ - prompt, + content, n_slides, file_paths, language, }: { - prompt: string; + content: string; n_slides: number | null; file_paths?: string[]; language: string | null; @@ -67,7 +67,7 @@ export class PresentationGenerationApi { method: "POST", headers: getHeader(), body: JSON.stringify({ - prompt, + content, n_slides, file_paths, language, diff --git a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx index 96b519a8..7826d3ec 100644 --- a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx +++ b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx @@ -138,7 +138,7 @@ const GroupLayoutPreview = () => { const payload = { layouts: [ { - presentation_id: presentationId, + presentation: presentationId, layout_id: currentLayoutId, layout_name: currentLayoutName, layout_code: currentCode, diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx index e47f29cb..ace407b4 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx @@ -152,7 +152,7 @@ const UploadPage = () => { // Use the first available layout group for direct generation trackEvent(MixpanelEvent.Upload_Create_Presentation_API_Call); const createResponse = await PresentationGenerationApi.createPresentation({ - prompt: config?.prompt ?? "", + content: config?.prompt ?? "", n_slides: config?.slides ? parseInt(config.slides) : null, file_paths: [], language: config?.language ?? "", From 36bb437f6bc3d36d0e2a6d6df9fa0cab2b6a396b Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Fri, 29 Aug 2025 12:35:14 +0545 Subject: [PATCH 8/8] refactor: modify presentation ID handling in useLayoutSaving hook --- Dockerfile.dev | 2 +- .../custom-template/hooks/useLayoutSaving.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index 6de18da2..1dabde3e 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -27,7 +27,7 @@ ENV TEMP_DIRECTORY=/tmp/presenton # ENV PYTHONPATH="${PYTHONPATH}:/app/servers/fastapi" # Install ollama -RUN curl -fsSL http://ollama.com/install.sh | sh +# RUN curl -fsSL http://ollama.com/install.sh | sh # Install dependencies for FastAPI RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \ diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useLayoutSaving.ts b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useLayoutSaving.ts index f1667288..37767dd1 100644 --- a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useLayoutSaving.ts +++ b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useLayoutSaving.ts @@ -52,7 +52,7 @@ export const useLayoutSaving = ( ); return { - presentation_id: presentationId, + presentation: presentationId, layout_id: `${slide.slide_number}`, layout_name: `Slide${slide.slide_number}`, layout_code: data.react_component || data.component_code, @@ -144,7 +144,6 @@ export const useLayoutSaving = ( body: JSON.stringify({ id: presentationId, name: layoutName, description }), }); - // Save the layout components to the app_data/layouts folder const saveResponse = await fetch( "/api/v1/ppt/template-management/save-templates", {