Merge branch 'main' into feat/undo-redo

This commit is contained in:
shiva raj badu 2025-08-29 14:18:06 +05:45
commit d133d9df9b
No known key found for this signature in database
38 changed files with 520 additions and 224 deletions

View file

@ -5,9 +5,9 @@ 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
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("<br>", "\n")
with open(file_path, "w") as text_file:

View file

@ -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()

View file

@ -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)

View file

@ -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
@ -7,7 +8,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
@ -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)
@ -38,7 +39,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:
@ -49,15 +50,23 @@ 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)
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.tone,
presentation.verbosity,
presentation.instructions,
):
# Give control to the event loop
await asyncio.sleep(0)

View file

@ -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 = []

View file

@ -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 = []

View file

@ -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
@ -18,10 +18,13 @@ 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
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 +33,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
@ -40,8 +43,11 @@ 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.randomizers import get_random_uuid
from utils.process_slides import (
process_slide_add_placeholder_assets,
process_slide_and_fetch_assets,
)
import uuid
PRESENTATION_ROUTER = APIRouter(prefix="/presentation", tags=["Presentation"])
@ -49,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:
@ -67,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:
@ -104,20 +110,26 @@ 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,
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,
prompt=prompt,
content=content,
n_slides=n_slides,
language=language,
file_paths=file_paths,
tone=tone,
verbosity=verbosity,
instructions=instructions,
)
sql_session.add(presentation)
@ -128,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,
@ -153,6 +165,7 @@ async def prepare_presentation(
await generate_presentation_structure(
presentation_outline=presentation_outline_model,
presentation_layout=layout,
instructions=presentation.instructions,
)
)
@ -177,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:
@ -194,7 +207,6 @@ async def stream_presentation(
)
image_generation_service = ImageGenerationService(get_images_directory())
icon_finder_service = IconFinderService()
async def inner():
structure = presentation.get_structure()
@ -213,7 +225,12 @@ 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.tone,
presentation.verbosity,
presentation.instructions,
)
slide = SlideModel(
@ -226,11 +243,12 @@ 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(
image_generation_service, icon_finder_service, slide
)
process_slide_and_fetch_assets(image_generation_service, slide)
)
yield SSEResponse(
@ -273,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()
@ -301,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)
@ -313,31 +339,51 @@ 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
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(
request.prompt,
request.content,
request.n_slides,
request.language,
additional_context,
request.tone,
request.verbosity,
request.instructions,
request.web_search,
):
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)
@ -356,6 +402,7 @@ async def generate_presentation_api(
await generate_presentation_structure(
presentation_outlines,
layout_model,
request.instructions,
)
)
@ -371,16 +418,18 @@ 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(),
tone=request.tone,
verbosity=request.verbosity,
instructions=request.instructions,
)
image_generation_service = ImageGenerationService(get_images_directory())
icon_finder_service = IconFinderService()
async_asset_generation_tasks = []
# 7. Generate slide content and save slides
@ -390,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
slide_layout,
outlines[i],
request.language,
request.tone,
request.verbosity,
request.instructions,
)
slide = SlideModel(
presentation=presentation_id,
@ -401,9 +455,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)
@ -421,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(
@ -458,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(

View file

@ -1,19 +1,18 @@
import importlib
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
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
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"])
@ -21,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),
):
@ -42,18 +41,16 @@ 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,
)
# 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
@ -67,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),
@ -84,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

View file

@ -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,

View file

@ -1,10 +1,29 @@
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")
content: str = Field(..., description="The content for generating the presentation")
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")
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"
)

View file

@ -1,8 +1,9 @@
from pydantic import BaseModel
import uuid
class PresentationAndPath(BaseModel):
presentation_id: str
presentation_id: uuid.UUID
path: str

View file

@ -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"

View file

@ -1,5 +1,6 @@
from typing import List, Optional
from datetime import datetime
import uuid
from pydantic import BaseModel
@ -11,17 +12,38 @@ from models.sql.slide import SlideModel
class PresentationWithSlides(BaseModel):
id: str
prompt: 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]
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,
)

View file

@ -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)

View file

@ -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))

View file

@ -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))

View file

@ -1,31 +1,48 @@
from datetime import datetime
from typing import List, Optional
from sqlalchemy import JSON, Column, DateTime
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)
prompt: str
__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)
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(),
prompt=self.prompt,
id=uuid.uuid4(),
content=self.content,
n_slides=self.n_slides,
language=self.language,
title=self.title,
@ -33,6 +50,7 @@ class PresentationModel(SQLModel, table=True):
outlines=self.outlines,
layout=self.layout,
structure=self.structure,
instructions=self.instructions,
)
def get_presentation_outline(self):

View file

@ -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))
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,
),
)

View file

@ -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,

View file

@ -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))
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
),
)

View file

@ -1,4 +0,0 @@
from services.temp_file_service import TempFileService
TEMP_FILE_SERVICE = TempFileService()

View file

@ -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()

View file

@ -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)

View file

@ -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(

View file

@ -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
@ -65,3 +65,6 @@ class TempFileService:
def cleanup_base_dir(self):
self.cleanup_temp_dir(self.base_dir)
TEMP_FILE_SERVICE = TempFileService()

View file

@ -0,0 +1,5 @@
from datetime import datetime, timezone
def get_current_utc_datetime():
return datetime.now(timezone.utc)

View file

@ -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:

View file

@ -2,19 +2,20 @@ import json
import os
import aiohttp
from typing import Literal
import uuid
from fastapi import HTTPException
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
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()

View file

@ -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,
)

View file

@ -1,64 +1,106 @@
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
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(
tone: Optional[str] = None,
verbosity: Optional[str] = None,
instructions: Optional[str] = None,
):
return f"""
**Input:**
- Prompt: {prompt}
- Output Language: {language}
- Number of Slides: {n_slides}
- 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.
{"# 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.
"""
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,
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, 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,
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)
client = LLMClient()
tools = [SearchWebTool, GetCurrentDatetimeTool]
async for chunk in client.stream_structured(
model,
get_messages(prompt, n_slides, language, content),
get_messages(
content,
n_slides,
language,
additional_context,
tone,
verbosity,
instructions,
),
response_model.model_json_schema(),
strict=True,
tools=tools if client.enable_web_grounding() else None,
tools=(
[SearchWebTool] if (client.enable_web_grounding() and web_search) else None
),
):
yield chunk

View file

@ -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,
instructions: 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 instructions else ""}
{instructions 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,
instructions: 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(),
instructions,
),
response_format=response_model.model_json_schema(),
strict=True,

View file

@ -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.presentation_outline_model import SlideOutlineModel
@ -5,28 +7,55 @@ 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(
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.
# 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.**
"""
{"# 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.
3. Generate speaker note that is simple, clear, concise and to the point.
# 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,
}}
"""
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
@ -38,11 +67,17 @@ def get_user_prompt(outline: str, language: str):
"""
def get_messages(outline: str, language: str):
def get_messages(
outline: str,
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(outline, language),
@ -51,7 +86,12 @@ 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,
tone: Optional[str] = None,
verbosity: Optional[str] = None,
instructions: Optional[str] = None,
):
client = LLMClient()
model = get_model()
@ -77,6 +117,9 @@ async def get_slide_content_from_type_and_outline(
messages=get_messages(
outline.content,
language,
tone,
verbosity,
instructions,
),
response_format=response_schema,
strict=False,

View file

@ -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)
@ -170,3 +168,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)

View file

@ -1,5 +0,0 @@
import uuid
def get_random_uuid():
return str(uuid.uuid4())

View file

@ -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",
{

View file

@ -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,

View file

@ -138,7 +138,7 @@ const GroupLayoutPreview = () => {
const payload = {
layouts: [
{
presentation_id: presentationId,
presentation: presentationId,
layout_id: currentLayoutId,
layout_name: currentLayoutName,
layout_code: currentCode,

View file

@ -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 ?? "",