Merge pull request #274 from presenton/refactor/images-and-ppt-endpoints
refactor/images and ppt endpoints
This commit is contained in:
commit
64daae1065
19 changed files with 596 additions and 249 deletions
|
|
@ -9,9 +9,8 @@ from services.database import get_async_session
|
|||
from services.image_generation_service import ImageGenerationService
|
||||
from utils.asset_directory_utils import get_images_directory
|
||||
import os
|
||||
from utils.asset_directory_utils import get_uploads_directory
|
||||
import uuid
|
||||
from services.image_upload_service import ImageUploadService
|
||||
from utils.file_utils import get_file_name_with_random_uuid
|
||||
|
||||
IMAGES_ROUTER = APIRouter(prefix="/images", tags=["Images"])
|
||||
|
||||
|
|
@ -38,51 +37,69 @@ async def generate_image(
|
|||
async def get_generated_images(sql_session: AsyncSession = Depends(get_async_session)):
|
||||
try:
|
||||
images = await sql_session.scalars(
|
||||
select(ImageAsset).where(ImageAsset.is_uploaded == False).order_by(ImageAsset.created_at.desc())
|
||||
select(ImageAsset)
|
||||
.where(ImageAsset.is_uploaded == False)
|
||||
.order_by(ImageAsset.created_at.desc())
|
||||
)
|
||||
return images
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to retrieve generated images: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to retrieve generated images: {str(e)}"
|
||||
)
|
||||
|
||||
@IMAGES_ROUTER.post("/upload-image")
|
||||
async def upload_image(file: UploadFile = File(...), sql_session: AsyncSession = Depends(get_async_session)):
|
||||
|
||||
@IMAGES_ROUTER.post("/upload")
|
||||
async def upload_image(
|
||||
file: UploadFile = File(...), sql_session: AsyncSession = Depends(get_async_session)
|
||||
):
|
||||
try:
|
||||
service = ImageUploadService(get_uploads_directory())
|
||||
image_asset = await service.upload_image(file)
|
||||
new_filename = get_file_name_with_random_uuid(file)
|
||||
image_path = os.path.join(
|
||||
get_images_directory(), os.path.basename(new_filename)
|
||||
)
|
||||
|
||||
with open(image_path, "wb") as f:
|
||||
f.write(await file.read())
|
||||
|
||||
image_asset = ImageAsset(path=image_path, is_uploaded=True)
|
||||
|
||||
sql_session.add(image_asset)
|
||||
await sql_session.commit()
|
||||
return {
|
||||
"message": "Image uploaded successfully",
|
||||
"path": image_asset.path,
|
||||
"id": str(image_asset.id),
|
||||
}
|
||||
|
||||
return image_asset
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to upload image: {str(e)}")
|
||||
|
||||
|
||||
@IMAGES_ROUTER.get("/uploaded", response_model=List[ImageAsset])
|
||||
async def get_uploaded_images(sql_session: AsyncSession = Depends(get_async_session)):
|
||||
try:
|
||||
images = await sql_session.scalars(
|
||||
select(ImageAsset).where(ImageAsset.is_uploaded == True).order_by(ImageAsset.created_at.desc())
|
||||
select(ImageAsset)
|
||||
.where(ImageAsset.is_uploaded == True)
|
||||
.order_by(ImageAsset.created_at.desc())
|
||||
)
|
||||
return images
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to retrieve uploaded images: {str(e)}")
|
||||
|
||||
|
||||
@IMAGES_ROUTER.delete("/uploaded-image/{image_id}")
|
||||
async def delete_image(image_id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session)):
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to retrieve uploaded images: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@IMAGES_ROUTER.delete("/{id}", status_code=204)
|
||||
async def delete_uploaded_image_by_id(
|
||||
id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session)
|
||||
):
|
||||
try:
|
||||
# Fetch the asset to get its actual file path
|
||||
image = await sql_session.get(ImageAsset, image_id)
|
||||
image = await sql_session.get(ImageAsset, id)
|
||||
if not image:
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
|
||||
service = ImageUploadService(get_uploads_directory())
|
||||
await service.delete_image(image.path)
|
||||
os.remove(image.path)
|
||||
|
||||
await sql_session.delete(image)
|
||||
await sql_session.commit()
|
||||
return {"message": "Image deleted successfully"}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete image: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
|
@ -16,17 +17,17 @@ from models.sse_response import (
|
|||
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
|
||||
from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
|
||||
from utils.ppt_utils import get_presentation_title_from_outlines
|
||||
|
||||
OUTLINES_ROUTER = APIRouter(prefix="/outlines", tags=["Outlines"])
|
||||
|
||||
|
||||
@OUTLINES_ROUTER.get("/stream")
|
||||
@OUTLINES_ROUTER.get("/stream/{id}")
|
||||
async def stream_outlines(
|
||||
presentation_id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session)
|
||||
id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session)
|
||||
):
|
||||
presentation = await sql_session.get(PresentationModel, presentation_id)
|
||||
presentation = await sql_session.get(PresentationModel, id)
|
||||
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
|
|
@ -38,79 +39,63 @@ async def stream_outlines(
|
|||
status="Generating presentation outlines..."
|
||||
).to_string()
|
||||
|
||||
presentation_outlines = None
|
||||
additional_context = ""
|
||||
if presentation.file_paths:
|
||||
documents_loader = DocumentsLoader(file_paths=presentation.file_paths)
|
||||
await documents_loader.load_documents(temp_dir)
|
||||
documents = documents_loader.documents
|
||||
if documents and len(documents) == 1:
|
||||
additional_context = documents[0]
|
||||
chunker = ScoreBasedChunker()
|
||||
try:
|
||||
chunks = await chunker.get_n_chunks(
|
||||
documents[0], presentation.n_slides
|
||||
)
|
||||
presentation_outlines = PresentationOutlineModel(
|
||||
slides=[chunk.to_slide_outline() for chunk in chunks]
|
||||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
elif documents:
|
||||
if documents:
|
||||
additional_context = "\n\n".join(documents)
|
||||
|
||||
if not presentation_outlines:
|
||||
presentation_outlines_text = ""
|
||||
async for chunk in generate_ppt_outline(
|
||||
presentation.content,
|
||||
presentation.n_slides,
|
||||
presentation.language,
|
||||
additional_context,
|
||||
presentation.tone,
|
||||
presentation.verbosity,
|
||||
presentation.instructions,
|
||||
True,
|
||||
):
|
||||
# Give control to the event loop
|
||||
await asyncio.sleep(0)
|
||||
presentation_outlines_text = ""
|
||||
|
||||
if isinstance(chunk, HTTPException):
|
||||
yield SSEErrorResponse(detail=chunk.detail).to_string()
|
||||
return
|
||||
|
||||
yield SSEResponse(
|
||||
event="response",
|
||||
data=json.dumps({"type": "chunk", "chunk": chunk}),
|
||||
).to_string()
|
||||
|
||||
presentation_outlines_text += chunk
|
||||
|
||||
try:
|
||||
presentation_outlines_json = json.loads(presentation_outlines_text)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Failed to generate presentation outlines. Please try again.",
|
||||
)
|
||||
|
||||
presentation_outlines = PresentationOutlineModel(
|
||||
**presentation_outlines_json
|
||||
n_slides_to_generate = presentation.n_slides
|
||||
if presentation.include_table_of_contents:
|
||||
needed_toc_count = math.ceil((presentation.n_slides - 1) / 10)
|
||||
n_slides_to_generate -= math.ceil(
|
||||
(presentation.n_slides - needed_toc_count) / 10
|
||||
)
|
||||
|
||||
async for chunk in generate_ppt_outline(
|
||||
presentation.content,
|
||||
n_slides_to_generate,
|
||||
presentation.language,
|
||||
additional_context,
|
||||
presentation.tone,
|
||||
presentation.verbosity,
|
||||
presentation.instructions,
|
||||
True,
|
||||
):
|
||||
# Give control to the event loop
|
||||
await asyncio.sleep(0)
|
||||
|
||||
if isinstance(chunk, HTTPException):
|
||||
yield SSEErrorResponse(detail=chunk.detail).to_string()
|
||||
return
|
||||
|
||||
yield SSEResponse(
|
||||
event="response",
|
||||
data=json.dumps({"type": "chunk", "chunk": chunk}),
|
||||
).to_string()
|
||||
|
||||
presentation_outlines_text += chunk
|
||||
|
||||
try:
|
||||
presentation_outlines_json = json.loads(presentation_outlines_text)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Failed to generate presentation outlines. Please try again.",
|
||||
)
|
||||
|
||||
presentation_outlines = PresentationOutlineModel(**presentation_outlines_json)
|
||||
|
||||
presentation_outlines.slides = presentation_outlines.slides[
|
||||
: presentation.n_slides
|
||||
:n_slides_to_generate
|
||||
]
|
||||
|
||||
presentation.outlines = presentation_outlines.model_dump()
|
||||
presentation.title = (
|
||||
presentation_outlines.slides[0]
|
||||
.content[:50]
|
||||
.replace("#", "")
|
||||
.replace("/", "")
|
||||
.replace("\\", "")
|
||||
.replace("\n", "")
|
||||
)
|
||||
presentation.title = get_presentation_title_from_outlines(presentation_outlines)
|
||||
|
||||
sql_session.add(presentation)
|
||||
await sql_session.commit()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
from typing import Annotated, List, Optional
|
||||
from typing import Annotated, List, Literal, Optional
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import delete
|
||||
|
|
@ -10,11 +11,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from sqlmodel import select
|
||||
from models.generate_presentation_request import GeneratePresentationRequest
|
||||
from models.presentation_and_path import PresentationPathAndEditPath
|
||||
from models.presentation_from_template import GetPresentationUsingTemplateRequest
|
||||
from models.presentation_from_template import EditPresentationRequest
|
||||
from models.presentation_outline_model import (
|
||||
PresentationOutlineModel,
|
||||
SlideOutlineModel,
|
||||
)
|
||||
from enums.tone import Tone
|
||||
from enums.verbosity import Verbosity
|
||||
from models.pptx_models import PptxPresentationModel
|
||||
from models.presentation_layout import PresentationLayoutModel
|
||||
from models.presentation_structure_model import PresentationStructureModel
|
||||
|
|
@ -23,7 +26,6 @@ from models.presentation_with_slides import (
|
|||
)
|
||||
|
||||
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
|
||||
|
|
@ -43,6 +45,7 @@ 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.ppt_utils import select_toc_or_list_slide_layout_index
|
||||
from utils.process_slides import (
|
||||
process_slide_add_placeholder_assets,
|
||||
process_slide_and_fetch_assets,
|
||||
|
|
@ -53,7 +56,32 @@ import uuid
|
|||
PRESENTATION_ROUTER = APIRouter(prefix="/presentation", tags=["Presentation"])
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.get("", response_model=PresentationWithSlides)
|
||||
@PRESENTATION_ROUTER.get("/all", response_model=List[PresentationWithSlides])
|
||||
async def get_all_presentations(sql_session: AsyncSession = Depends(get_async_session)):
|
||||
presentations_with_slides = []
|
||||
|
||||
query = (
|
||||
select(PresentationModel, SlideModel)
|
||||
.join(
|
||||
SlideModel,
|
||||
(SlideModel.presentation == PresentationModel.id) & (SlideModel.index == 0),
|
||||
)
|
||||
.order_by(PresentationModel.created_at.desc())
|
||||
)
|
||||
|
||||
results = await sql_session.execute(query)
|
||||
rows = results.all()
|
||||
presentations_with_slides = [
|
||||
PresentationWithSlides(
|
||||
**presentation.model_dump(),
|
||||
slides=[first_slide],
|
||||
)
|
||||
for presentation, first_slide in rows
|
||||
]
|
||||
return presentations_with_slides
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.get("/{id}", response_model=PresentationWithSlides)
|
||||
async def get_presentation(
|
||||
id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session)
|
||||
):
|
||||
|
|
@ -71,7 +99,7 @@ async def get_presentation(
|
|||
)
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.delete("", status_code=204)
|
||||
@PRESENTATION_ROUTER.delete("/{id}", status_code=204)
|
||||
async def delete_presentation(
|
||||
id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session)
|
||||
):
|
||||
|
|
@ -83,41 +111,27 @@ async def delete_presentation(
|
|||
await sql_session.commit()
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.get("/all", response_model=List[PresentationWithSlides])
|
||||
async def get_all_presentations(sql_session: AsyncSession = Depends(get_async_session)):
|
||||
presentations_with_slides = []
|
||||
presentations = await sql_session.scalars(select(PresentationModel))
|
||||
|
||||
async def inner(presentation: PresentationModel, sql_session: AsyncSession):
|
||||
first_slide = await sql_session.scalar(
|
||||
select(SlideModel)
|
||||
.where(SlideModel.presentation == presentation.id)
|
||||
.where(SlideModel.index == 0)
|
||||
)
|
||||
if not first_slide:
|
||||
return None
|
||||
return PresentationWithSlides(
|
||||
**presentation.model_dump(),
|
||||
slides=[first_slide],
|
||||
)
|
||||
|
||||
tasks = [inner(p, sql_session) for p in presentations]
|
||||
results = await asyncio.gather(*tasks)
|
||||
presentations_with_slides = [r for r in results if r is not None]
|
||||
return presentations_with_slides
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.post("/create", response_model=PresentationModel)
|
||||
async def create_presentation(
|
||||
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,
|
||||
tone: Annotated[Tone, Body()] = Tone.DEFAULT,
|
||||
verbosity: Annotated[Verbosity, Body()] = Verbosity.STANDARD,
|
||||
instructions: Annotated[Optional[str], Body()] = None,
|
||||
include_table_of_contents: Annotated[bool, Body()] = False,
|
||||
include_title_slide: Annotated[bool, Body()] = True,
|
||||
web_search: Annotated[bool, Body()] = False,
|
||||
sql_session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
|
||||
if include_table_of_contents and n_slides < 3:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Number of slides cannot be less than 3 if table of contents is included",
|
||||
)
|
||||
|
||||
presentation_id = uuid.uuid4()
|
||||
|
||||
presentation = PresentationModel(
|
||||
|
|
@ -126,9 +140,12 @@ async def create_presentation(
|
|||
n_slides=n_slides,
|
||||
language=language,
|
||||
file_paths=file_paths,
|
||||
tone=tone,
|
||||
verbosity=verbosity,
|
||||
tone=tone.value,
|
||||
verbosity=verbosity.value,
|
||||
instructions=instructions,
|
||||
include_table_of_contents=include_table_of_contents,
|
||||
include_title_slide=include_title_slide,
|
||||
web_search=web_search,
|
||||
)
|
||||
|
||||
sql_session.add(presentation)
|
||||
|
|
@ -177,6 +194,42 @@ async def prepare_presentation(
|
|||
if presentation_structure.slides[index] >= total_slide_layouts:
|
||||
presentation_structure.slides[index] = random_slide_index
|
||||
|
||||
if presentation.include_table_of_contents:
|
||||
n_toc_slides = presentation.n_slides - total_outlines
|
||||
toc_slide_layout_index = select_toc_or_list_slide_layout_index(layout)
|
||||
if toc_slide_layout_index != -1:
|
||||
outline_index = 1 if presentation.include_title_slide else 0
|
||||
for i in range(n_toc_slides):
|
||||
outlines_to = outline_index + 10
|
||||
if total_outlines == outlines_to:
|
||||
outlines_to -= 1
|
||||
|
||||
presentation_structure.slides.insert(
|
||||
i + 1 if presentation.include_title_slide else i,
|
||||
toc_slide_layout_index,
|
||||
)
|
||||
toc_outline = f"Table of Contents\n\n"
|
||||
|
||||
for outline in presentation_outline_model.slides[
|
||||
outline_index:outlines_to
|
||||
]:
|
||||
page_number = (
|
||||
outline_index - i + n_toc_slides + 1
|
||||
if presentation.include_title_slide
|
||||
else outline_index - i + n_toc_slides
|
||||
)
|
||||
toc_outline += f"Slide page number: {page_number}\n Slide Content: {outline.content[:100]}\n\n"
|
||||
outline_index += 1
|
||||
|
||||
outline_index += 1
|
||||
|
||||
presentation_outline_model.slides.insert(
|
||||
i + 1 if presentation.include_title_slide else i,
|
||||
SlideOutlineModel(
|
||||
content=toc_outline,
|
||||
),
|
||||
)
|
||||
|
||||
sql_session.add(presentation)
|
||||
presentation.outlines = presentation_outline_model.model_dump(mode="json")
|
||||
presentation.title = title or presentation.title
|
||||
|
|
@ -187,11 +240,11 @@ async def prepare_presentation(
|
|||
return presentation
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.get("/stream", response_model=PresentationWithSlides)
|
||||
@PRESENTATION_ROUTER.get("/stream/{id}", response_model=PresentationWithSlides)
|
||||
async def stream_presentation(
|
||||
presentation_id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session)
|
||||
id: uuid.UUID, sql_session: AsyncSession = Depends(get_async_session)
|
||||
):
|
||||
presentation = await sql_session.get(PresentationModel, presentation_id)
|
||||
presentation = await sql_session.get(PresentationModel, id)
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
if not presentation.structure:
|
||||
|
|
@ -237,7 +290,7 @@ async def stream_presentation(
|
|||
return
|
||||
|
||||
slide = SlideModel(
|
||||
presentation=presentation_id,
|
||||
presentation=id,
|
||||
layout_group=layout.name,
|
||||
layout=slide_layout.id,
|
||||
index=i,
|
||||
|
|
@ -271,7 +324,7 @@ async def stream_presentation(
|
|||
|
||||
# Moved this here to make sure new slides are generated before deleting the old ones
|
||||
await sql_session.execute(
|
||||
delete(SlideModel).where(SlideModel.presentation == presentation_id)
|
||||
delete(SlideModel).where(SlideModel.presentation == id)
|
||||
)
|
||||
await sql_session.commit()
|
||||
|
||||
|
|
@ -334,7 +387,7 @@ async def update_presentation(
|
|||
|
||||
|
||||
@PRESENTATION_ROUTER.post("/export/pptx", response_model=str)
|
||||
async def create_pptx(
|
||||
async def export_presentation_as_pptx(
|
||||
pptx_model: Annotated[PptxPresentationModel, Body()],
|
||||
):
|
||||
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
|
||||
|
|
@ -351,6 +404,31 @@ async def create_pptx(
|
|||
return pptx_path
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.post("/export", response_model=PresentationPathAndEditPath)
|
||||
async def export_presentation_as_pptx_or_pdf(
|
||||
id: Annotated[uuid.UUID, Body(description="Presentation ID to export")],
|
||||
export_as: Annotated[
|
||||
Literal["pptx", "pdf"], Body(description="Format to export the presentation as")
|
||||
] = "pptx",
|
||||
sql_session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
presentation = await sql_session.get(PresentationModel, id)
|
||||
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
|
||||
presentation_and_path = await export_presentation(
|
||||
id,
|
||||
presentation.title or str(uuid.uuid4()),
|
||||
export_as,
|
||||
)
|
||||
|
||||
return PresentationPathAndEditPath(
|
||||
**presentation_and_path.model_dump(),
|
||||
edit_path=f"/presentation?id={id}",
|
||||
)
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.post("/generate", response_model=PresentationPathAndEditPath)
|
||||
async def generate_presentation_api(
|
||||
request: GeneratePresentationRequest,
|
||||
|
|
@ -358,39 +436,47 @@ async def generate_presentation_api(
|
|||
):
|
||||
presentation_id = uuid.uuid4()
|
||||
|
||||
# 3. Generate Outlines
|
||||
presentation_outlines = None
|
||||
additional_context = ""
|
||||
using_slides_markdown = False
|
||||
|
||||
# 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()
|
||||
try:
|
||||
chunks = await chunker.get_n_chunks(documents[0], request.n_slides)
|
||||
presentation_outlines = PresentationOutlineModel(
|
||||
slides=[chunk.to_slide_outline() for chunk in chunks]
|
||||
if request.slides_markdown:
|
||||
using_slides_markdown = True
|
||||
request.n_slides = len(request.slides_markdown)
|
||||
|
||||
if not using_slides_markdown:
|
||||
additional_context = ""
|
||||
|
||||
if request.files:
|
||||
documents_loader = DocumentsLoader(file_paths=request.files)
|
||||
await documents_loader.load_documents()
|
||||
documents = documents_loader.documents
|
||||
if documents:
|
||||
additional_context = "\n\n".join(documents)
|
||||
|
||||
# Finding number of slides to generate by considering table of contents
|
||||
n_slides_to_generate = request.n_slides
|
||||
if request.include_table_of_contents:
|
||||
needed_toc_count = math.ceil(
|
||||
(
|
||||
(request.n_slides - 1)
|
||||
if request.include_title_slide
|
||||
else request.n_slides
|
||||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
/ 10
|
||||
)
|
||||
n_slides_to_generate -= math.ceil(
|
||||
(request.n_slides - needed_toc_count) / 10
|
||||
)
|
||||
|
||||
elif documents:
|
||||
additional_context = "\n\n".join(documents)
|
||||
|
||||
if not presentation_outlines:
|
||||
presentation_outlines_text = ""
|
||||
async for chunk in generate_ppt_outline(
|
||||
request.content,
|
||||
request.n_slides,
|
||||
n_slides_to_generate,
|
||||
request.language,
|
||||
additional_context,
|
||||
request.tone,
|
||||
request.verbosity,
|
||||
request.tone.value,
|
||||
request.verbosity.value,
|
||||
request.instructions,
|
||||
request.include_title_slide,
|
||||
request.web_search,
|
||||
):
|
||||
|
||||
|
|
@ -408,18 +494,25 @@ async def generate_presentation_api(
|
|||
detail="Failed to generate presentation outlines. Please try again.",
|
||||
)
|
||||
presentation_outlines = PresentationOutlineModel(**presentation_outlines_json)
|
||||
total_outlines = n_slides_to_generate
|
||||
|
||||
outlines = presentation_outlines.slides[: request.n_slides]
|
||||
total_outlines = len(outlines)
|
||||
else:
|
||||
# Setting outlines to slides markdown
|
||||
presentation_outlines = PresentationOutlineModel(
|
||||
slides=[
|
||||
SlideOutlineModel(content=slide) for slide in request.slides_markdown
|
||||
]
|
||||
)
|
||||
total_outlines = len(request.slides_markdown)
|
||||
|
||||
print("-" * 40)
|
||||
print(f"Generated {total_outlines} outlines for the presentation")
|
||||
|
||||
# 4. Parse Layouts
|
||||
# Parse Layouts
|
||||
layout_model = await get_layout_by_name(request.template)
|
||||
total_slide_layouts = len(layout_model.slides)
|
||||
|
||||
# 5. Generate Structure
|
||||
# Generate Structure
|
||||
if layout_model.ordered:
|
||||
presentation_structure = layout_model.to_presentation_structure()
|
||||
else:
|
||||
|
|
@ -428,6 +521,7 @@ async def generate_presentation_api(
|
|||
presentation_outlines,
|
||||
layout_model,
|
||||
request.instructions,
|
||||
using_slides_markdown,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -440,7 +534,42 @@ async def generate_presentation_api(
|
|||
if presentation_structure.slides[index] >= total_slide_layouts:
|
||||
presentation_structure.slides[index] = random_slide_index
|
||||
|
||||
# 6. Create PresentationModel
|
||||
# Injecting table of contents to the presentation structure and outlines
|
||||
if request.include_table_of_contents and not using_slides_markdown:
|
||||
n_toc_slides = request.n_slides - total_outlines
|
||||
toc_slide_layout_index = select_toc_or_list_slide_layout_index(layout_model)
|
||||
if toc_slide_layout_index != -1:
|
||||
outline_index = 1 if request.include_title_slide else 0
|
||||
for i in range(n_toc_slides):
|
||||
outlines_to = outline_index + 10
|
||||
if total_outlines == outlines_to:
|
||||
outlines_to -= 1
|
||||
|
||||
presentation_structure.slides.insert(
|
||||
i + 1 if request.include_title_slide else i,
|
||||
toc_slide_layout_index,
|
||||
)
|
||||
toc_outline = f"Table of Contents\n\n"
|
||||
|
||||
for outline in presentation_outlines.slides[outline_index:outlines_to]:
|
||||
page_number = (
|
||||
outline_index - i + n_toc_slides + 1
|
||||
if request.include_title_slide
|
||||
else outline_index - i + n_toc_slides
|
||||
)
|
||||
toc_outline += f"Slide page number: {page_number}\n Slide Content: {outline.content[:100]}\n\n"
|
||||
outline_index += 1
|
||||
|
||||
outline_index += 1
|
||||
|
||||
presentation_outlines.slides.insert(
|
||||
i + 1 if request.include_title_slide else i,
|
||||
SlideOutlineModel(
|
||||
content=toc_outline,
|
||||
),
|
||||
)
|
||||
|
||||
# Create PresentationModel
|
||||
presentation = PresentationModel(
|
||||
id=presentation_id,
|
||||
content=request.content,
|
||||
|
|
@ -474,10 +603,10 @@ async def generate_presentation_api(
|
|||
content_tasks = [
|
||||
get_slide_content_from_type_and_outline(
|
||||
slide_layouts[i],
|
||||
outlines[i],
|
||||
presentation_outlines.slides[i],
|
||||
request.language,
|
||||
request.tone,
|
||||
request.verbosity,
|
||||
request.tone.value,
|
||||
request.verbosity.value,
|
||||
request.instructions,
|
||||
)
|
||||
for i in range(start, end)
|
||||
|
|
@ -530,14 +659,57 @@ async def generate_presentation_api(
|
|||
)
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.post("/from-template", response_model=PresentationPathAndEditPath)
|
||||
async def from_template(
|
||||
data: Annotated[GetPresentationUsingTemplateRequest, Body()],
|
||||
@PRESENTATION_ROUTER.post("/edit", response_model=PresentationPathAndEditPath)
|
||||
async def edit_presentation_with_new_content(
|
||||
data: Annotated[EditPresentationRequest, Body()],
|
||||
sql_session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
presentation = await sql_session.get(PresentationModel, data.presentation_id)
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
|
||||
slides = await sql_session.scalars(
|
||||
select(SlideModel).where(SlideModel.presentation == data.presentation_id)
|
||||
)
|
||||
|
||||
new_slides = []
|
||||
slides_to_delete = []
|
||||
for each_slide in slides:
|
||||
updated_content = None
|
||||
new_slide_data = list(filter(lambda x: x.index == each_slide.index, data.data))
|
||||
if new_slide_data:
|
||||
updated_content = deep_update(each_slide.content, new_slide_data[0].content)
|
||||
new_slides.append(
|
||||
each_slide.get_new_slide(presentation.id, updated_content)
|
||||
)
|
||||
slides_to_delete.append(each_slide.id)
|
||||
|
||||
await sql_session.execute(
|
||||
delete(SlideModel).where(SlideModel.id.in_(slides_to_delete))
|
||||
)
|
||||
|
||||
sql_session.add_all(new_slides)
|
||||
await sql_session.commit()
|
||||
|
||||
presentation_and_path = await export_presentation(
|
||||
presentation.id, presentation.title or str(uuid.uuid4()), data.export_as
|
||||
)
|
||||
|
||||
return PresentationPathAndEditPath(
|
||||
**presentation_and_path.model_dump(),
|
||||
edit_path=f"/presentation?id={presentation.id}",
|
||||
)
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.post("/derive", response_model=PresentationPathAndEditPath)
|
||||
async def derive_presentation_from_existing_one(
|
||||
data: Annotated[EditPresentationRequest, Body()],
|
||||
sql_session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
presentation = await sql_session.get(PresentationModel, data.presentation_id)
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
|
||||
slides = await sql_session.scalars(
|
||||
select(SlideModel).where(SlideModel.presentation == data.presentation_id)
|
||||
)
|
||||
|
|
|
|||
11
servers/fastapi/enums/tone.py
Normal file
11
servers/fastapi/enums/tone.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class Tone(str, Enum):
|
||||
DEFAULT = "default"
|
||||
CASUAL = "casual"
|
||||
PROFESSIONAL = "professional"
|
||||
FUNNY = "funny"
|
||||
EDUCATIONAL = "educational"
|
||||
SALES_PITCH = "sales_pitch"
|
||||
|
||||
8
servers/fastapi/enums/verbosity.py
Normal file
8
servers/fastapi/enums/verbosity.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class Verbosity(str, Enum):
|
||||
CONCISE = "concise"
|
||||
STANDARD = "standard"
|
||||
TEXT_HEAVY = "text-heavy"
|
||||
|
||||
|
|
@ -1,17 +1,21 @@
|
|||
from typing import List, Literal, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from enums.tone import Tone
|
||||
from enums.verbosity import Verbosity
|
||||
|
||||
|
||||
class GeneratePresentationRequest(BaseModel):
|
||||
content: str = Field(..., description="The content for generating the presentation")
|
||||
slides_markdown: Optional[List[str]] = Field(
|
||||
default=None, description="The markdown for the slides"
|
||||
)
|
||||
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"
|
||||
tone: Tone = Field(default=Tone.DEFAULT, description="The tone to use for the text")
|
||||
verbosity: Verbosity = Field(
|
||||
default=Verbosity.STANDARD, description="How verbose the presentation should be"
|
||||
)
|
||||
web_search: bool = Field(default=False, description="Whether to enable web search")
|
||||
n_slides: int = Field(default=8, description="Number of slides to generate")
|
||||
|
|
@ -21,6 +25,12 @@ class GeneratePresentationRequest(BaseModel):
|
|||
template: str = Field(
|
||||
default="general", description="Template to use for the presentation"
|
||||
)
|
||||
include_table_of_contents: bool = Field(
|
||||
default=False, description="Whether to include a table of contents"
|
||||
)
|
||||
include_title_slide: bool = Field(
|
||||
default=True, description="Whether to include a title slide"
|
||||
)
|
||||
files: Optional[List[str]] = Field(
|
||||
default=None, description="Files to use for the presentation"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class SlideContentUpdate(BaseModel):
|
|||
content: dict
|
||||
|
||||
|
||||
class GetPresentationUsingTemplateRequest(BaseModel):
|
||||
class EditPresentationRequest(BaseModel):
|
||||
presentation_id: uuid.UUID
|
||||
data: List[SlideContentUpdate]
|
||||
export_as: Literal["pptx", "pdf"] = "pptx"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from models.sql.slide import SlideModel
|
|||
|
||||
class PresentationWithSlides(BaseModel):
|
||||
id: uuid.UUID
|
||||
user: uuid.UUID
|
||||
content: str
|
||||
n_slides: int
|
||||
language: str
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from datetime import datetime
|
|||
from typing import List, Optional
|
||||
import uuid
|
||||
from sqlalchemy import JSON, Column, DateTime, String
|
||||
from sqlmodel import Field, SQLModel
|
||||
from sqlmodel import Boolean, Field, SQLModel
|
||||
|
||||
from models.presentation_layout import PresentationLayoutModel
|
||||
from models.presentation_outline_model import PresentationOutlineModel
|
||||
|
|
@ -38,6 +38,9 @@ class PresentationModel(SQLModel, table=True):
|
|||
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)
|
||||
include_table_of_contents: bool = Field(sa_column=Column(Boolean), default=False)
|
||||
include_title_slide: bool = Field(sa_column=Column(Boolean), default=True)
|
||||
web_search: bool = Field(sa_column=Column(Boolean), default=False)
|
||||
|
||||
def get_new_presentation(self):
|
||||
return PresentationModel(
|
||||
|
|
@ -51,6 +54,10 @@ class PresentationModel(SQLModel, table=True):
|
|||
layout=self.layout,
|
||||
structure=self.structure,
|
||||
instructions=self.instructions,
|
||||
tone=self.tone,
|
||||
verbosity=self.verbosity,
|
||||
include_table_of_contents=self.include_table_of_contents,
|
||||
include_title_slide=self.include_title_slide,
|
||||
)
|
||||
|
||||
def get_presentation_outline(self):
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
from fastapi import UploadFile
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from models.sql.image_asset import ImageAsset
|
||||
from utils.asset_directory_utils import get_uploads_directory
|
||||
|
||||
|
||||
class ImageUploadService:
|
||||
"""Handles saving uploaded images to disk and returning ImageAsset models."""
|
||||
|
||||
def __init__(self, output_directory: str | None = None):
|
||||
# Prefer provided directory, otherwise resolve from app data directory
|
||||
self.uploads_directory = output_directory or get_uploads_directory()
|
||||
os.makedirs(self.uploads_directory, exist_ok=True)
|
||||
|
||||
async def upload_image(self, file: UploadFile) -> ImageAsset:
|
||||
"""Save the uploaded file to disk and return an ImageAsset (not committed).
|
||||
|
||||
The caller is responsible for adding the returned ImageAsset to the
|
||||
database session and committing.
|
||||
"""
|
||||
file_extension = os.path.splitext(file.filename)[1] if file.filename else ""
|
||||
unique_filename = f"{uuid.uuid4()}{file_extension}"
|
||||
file_path = os.path.join(self.uploads_directory, unique_filename)
|
||||
|
||||
content = await file.read()
|
||||
with open(file_path, "wb") as buffer:
|
||||
buffer.write(content)
|
||||
|
||||
image_asset = ImageAsset(
|
||||
path=file_path,
|
||||
is_uploaded=True,
|
||||
extras={
|
||||
"original_filename": file.filename,
|
||||
"content_type": file.content_type,
|
||||
"file_size": len(content),
|
||||
},
|
||||
)
|
||||
|
||||
return image_asset
|
||||
|
||||
async def delete_image(self, file_path: str) -> bool:
|
||||
"""Delete an image file from disk by its absolute path."""
|
||||
if not file_path:
|
||||
return False
|
||||
if not os.path.isabs(file_path):
|
||||
# Ensure we only operate on absolute paths that we generated
|
||||
file_path = os.path.join(self.uploads_directory, file_path)
|
||||
if not os.path.exists(file_path):
|
||||
return False
|
||||
os.remove(file_path)
|
||||
return True
|
||||
44
servers/fastapi/utils/file_utils.py
Normal file
44
servers/fastapi/utils/file_utils.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import os
|
||||
from typing import BinaryIO
|
||||
import uuid
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
|
||||
def replace_file_name(filename: str, new_stem: str) -> str:
|
||||
_, ext = os.path.splitext(filename)
|
||||
return f"{new_stem}{ext}"
|
||||
|
||||
|
||||
def get_file_name_with_random_uuid(file: str | UploadFile | BinaryIO) -> str:
|
||||
filename = None
|
||||
if getattr(file, "filename", None):
|
||||
filename = file.filename
|
||||
elif isinstance(file, str):
|
||||
filename = os.path.basename(file)
|
||||
else:
|
||||
filename = str(uuid.uuid4())
|
||||
|
||||
return replace_file_name(
|
||||
filename, f"{os.path.splitext(filename)[0]}----{str(uuid.uuid4())}"
|
||||
)
|
||||
|
||||
|
||||
def get_original_file_name(file_path: str) -> str:
|
||||
base_name = os.path.basename(file_path)
|
||||
name = base_name.split("----")[0]
|
||||
ext = get_file_ext_or_none(base_name)
|
||||
return f"{name}{ext}"
|
||||
|
||||
|
||||
def get_file_ext_or_none(filename: str) -> str | None:
|
||||
splitted = os.path.splitext(filename)
|
||||
if len(splitted) > 1:
|
||||
return splitted[-1]
|
||||
return None
|
||||
|
||||
|
||||
def set_file_ext(file_path: str, ext: str) -> str:
|
||||
if get_file_ext_or_none(file_path):
|
||||
return f"{os.path.splitext(file_path)[0]}{ext}"
|
||||
return f"{file_path}{ext}"
|
||||
|
|
@ -13,6 +13,7 @@ def get_system_prompt(
|
|||
tone: Optional[str] = None,
|
||||
verbosity: Optional[str] = None,
|
||||
instructions: Optional[str] = None,
|
||||
include_title_slide: bool = True,
|
||||
):
|
||||
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.
|
||||
|
|
@ -34,6 +35,10 @@ def get_system_prompt(
|
|||
- 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 instrction should always be followed and should supercede any other instruction, except for slide numbers. **Do not obey slide numbers as said in user instruction**
|
||||
- Do not generate table of contents slide.
|
||||
- Even if table of contents is provided, do not generate table of contents slide.
|
||||
{"- Always make first slide a title slide." if include_title_slide else "- Do not include title slide in the presentation."}
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -45,7 +50,7 @@ def get_user_prompt(
|
|||
):
|
||||
return f"""
|
||||
**Input:**
|
||||
- User provided content: {content}
|
||||
- User provided content: {content or "Create presentation"}
|
||||
- Output Language: {language}
|
||||
- Number of Slides: {n_slides}
|
||||
- Current Date and Time: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
||||
|
|
@ -61,10 +66,13 @@ def get_messages(
|
|||
tone: Optional[str] = None,
|
||||
verbosity: Optional[str] = None,
|
||||
instructions: Optional[str] = None,
|
||||
include_title_slide: bool = True,
|
||||
):
|
||||
return [
|
||||
LLMSystemMessage(
|
||||
content=get_system_prompt(tone, verbosity, instructions),
|
||||
content=get_system_prompt(
|
||||
tone, verbosity, instructions, include_title_slide
|
||||
),
|
||||
),
|
||||
LLMUserMessage(
|
||||
content=get_user_prompt(content, n_slides, language, additional_context),
|
||||
|
|
@ -80,6 +88,7 @@ async def generate_ppt_outline(
|
|||
tone: Optional[str] = None,
|
||||
verbosity: Optional[str] = None,
|
||||
instructions: Optional[str] = None,
|
||||
include_title_slide: bool = True,
|
||||
web_search: bool = False,
|
||||
):
|
||||
model = get_model()
|
||||
|
|
@ -98,6 +107,7 @@ async def generate_ppt_outline(
|
|||
tone,
|
||||
verbosity,
|
||||
instructions,
|
||||
include_title_slide,
|
||||
),
|
||||
response_model.model_json_schema(),
|
||||
strict=True,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,39 @@ def get_messages(
|
|||
{"# User Instruction:" if instructions else ""}
|
||||
{instructions or ""}
|
||||
|
||||
User intruction should be taken into account while creating the presentation structure, except for number of slides.
|
||||
|
||||
Select layout index for each of the {n_slides} slides based on what will best serve the presentation's goals.
|
||||
""",
|
||||
),
|
||||
LLMUserMessage(
|
||||
content=f"""
|
||||
{data}
|
||||
""",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def get_messages_for_slides_markdown(
|
||||
presentation_layout: PresentationLayoutModel,
|
||||
n_slides: int,
|
||||
data: str,
|
||||
instructions: Optional[str] = None,
|
||||
):
|
||||
return [
|
||||
LLMSystemMessage(
|
||||
content=f"""
|
||||
You're a professional presentation designer with creative freedom to design engaging presentations.
|
||||
|
||||
{"# User Instruction:" if instructions else ""}
|
||||
{instructions or ""}
|
||||
|
||||
{presentation_layout.to_string()}
|
||||
|
||||
Select layout that best matches the content of the slides.
|
||||
|
||||
User intruction should be taken into account while creating the presentation structure, except for number of slides.
|
||||
|
||||
Select layout index for each of the {n_slides} slides based on what will best serve the presentation's goals.
|
||||
""",
|
||||
),
|
||||
|
|
@ -66,6 +99,7 @@ async def generate_presentation_structure(
|
|||
presentation_outline: PresentationOutlineModel,
|
||||
presentation_layout: PresentationLayoutModel,
|
||||
instructions: Optional[str] = None,
|
||||
using_slides_markdown: bool = False,
|
||||
) -> PresentationStructureModel:
|
||||
|
||||
client = LLMClient()
|
||||
|
|
@ -77,11 +111,20 @@ async def generate_presentation_structure(
|
|||
try:
|
||||
response = await client.generate_structured(
|
||||
model=model,
|
||||
messages=get_messages(
|
||||
presentation_layout,
|
||||
len(presentation_outline.slides),
|
||||
presentation_outline.to_string(),
|
||||
instructions,
|
||||
messages=(
|
||||
get_messages_for_slides_markdown(
|
||||
presentation_layout,
|
||||
len(presentation_outline.slides),
|
||||
presentation_outline.to_string(),
|
||||
instructions,
|
||||
)
|
||||
if using_slides_markdown
|
||||
else get_messages(
|
||||
presentation_layout,
|
||||
len(presentation_outline.slides),
|
||||
presentation_outline.to_string(),
|
||||
instructions,
|
||||
)
|
||||
),
|
||||
response_format=response_model.model_json_schema(),
|
||||
strict=True,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,18 @@ def get_system_prompt(
|
|||
- 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.
|
||||
- Generate content as per the given tone.
|
||||
- Be very careful with number of words to generate for given field. As generating more than max characters will overflow in the design. So, analyze early and never generate more characters than allowed.
|
||||
- Do not add emoji in the content.
|
||||
- Metrics should be in abbreviated form with least possible characters. Do not add long sequence of words for metrics.
|
||||
- For verbosity:
|
||||
- If verbosity is 'concise', then generate description as 1/3 or lower of the max character limit. Don't worry if you miss content or context.
|
||||
- If verbosity is 'standard', then generate description as 2/3 of the max character limit.
|
||||
- If verbosity is 'text-heavy', then generate description as 3/4 or higher of the max character limit. Make sure it does not exceed the max character limit.
|
||||
|
||||
User instructions, tone and verbosity should always be followed and should supercede any other instruction, except for max and min character limit, slide schema and number of items.
|
||||
|
||||
- Provide output in json format and **don't include <parameters> tags**.
|
||||
|
||||
# Image and Icon Output Format
|
||||
image: {{
|
||||
|
|
|
|||
82
servers/fastapi/utils/ppt_utils.py
Normal file
82
servers/fastapi/utils/ppt_utils.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
from models.presentation_layout import PresentationLayoutModel
|
||||
from models.presentation_outline_model import PresentationOutlineModel
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
from models.presentation_structure_model import PresentationStructureModel
|
||||
|
||||
|
||||
def get_presentation_title_from_outlines(
|
||||
presentation_outlines: PresentationOutlineModel,
|
||||
) -> str:
|
||||
if not presentation_outlines.slides:
|
||||
return "Untitled Presentation"
|
||||
|
||||
first_content = presentation_outlines.slides[0].content or ""
|
||||
|
||||
if re.match(r"^\s*#{1,6}\s*Page\s+\d+\b", first_content):
|
||||
first_content = re.sub(
|
||||
r"^\s*#{1,6}\s*Page\s+\d+\b[\s,:\-]*",
|
||||
"",
|
||||
first_content,
|
||||
count=1,
|
||||
)
|
||||
|
||||
return (
|
||||
first_content[:100]
|
||||
.replace("#", "")
|
||||
.replace("/", "")
|
||||
.replace("\\", "")
|
||||
.replace("\n", " ")
|
||||
)
|
||||
|
||||
|
||||
def find_slide_layout_index_by_regex(
|
||||
layout: PresentationLayoutModel, patterns: List[str]
|
||||
) -> int:
|
||||
def _find_index(pattern: str) -> int:
|
||||
regex = re.compile(pattern, re.IGNORECASE)
|
||||
for index, slide_layout in enumerate(layout.slides):
|
||||
candidates = [
|
||||
slide_layout.id or "",
|
||||
(slide_layout.name or ""),
|
||||
(slide_layout.description or ""),
|
||||
(slide_layout.json_schema.get("title") if slide_layout.json_schema else ""),
|
||||
]
|
||||
for text in candidates:
|
||||
if text and regex.search(text):
|
||||
return index
|
||||
return -1
|
||||
|
||||
for pattern in patterns:
|
||||
match_index = _find_index(pattern)
|
||||
if match_index != -1:
|
||||
return match_index
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
def select_toc_or_list_slide_layout_index(
|
||||
layout: PresentationLayoutModel,
|
||||
) -> int:
|
||||
toc_patterns = [
|
||||
r"\btable\s*of\s*contents\b",
|
||||
r"\btable[- ]?of[- ]?contents\b",
|
||||
r"\bagenda\b",
|
||||
r"\bcontents\b",
|
||||
r"\boutline\b",
|
||||
r"\bindex\b",
|
||||
r"\btoc\b",
|
||||
]
|
||||
|
||||
list_patterns = [
|
||||
r"\b(bullet(ed)?\s*list|bullets?)\b",
|
||||
r"\b(numbered\s*list|ordered\s*list|unordered\s*list)\b",
|
||||
r"\blist\b",
|
||||
]
|
||||
|
||||
toc_index = find_slide_layout_index_by_regex(layout, toc_patterns)
|
||||
if toc_index != -1:
|
||||
return toc_index
|
||||
|
||||
return find_slide_layout_index_by_regex(layout, list_patterns)
|
||||
|
|
@ -29,7 +29,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
|
|||
setIsLoading(true)
|
||||
try {
|
||||
eventSource = new EventSource(
|
||||
`/api/v1/ppt/outlines/stream?presentation_id=${presentationId}`
|
||||
`/api/v1/ppt/outlines/stream/${presentationId}`
|
||||
);
|
||||
|
||||
eventSource.addEventListener("response", (event) => {
|
||||
|
|
@ -42,7 +42,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
|
|||
try {
|
||||
const repairedJson = jsonrepair(accumulatedChunks);
|
||||
const partialData = JSON.parse(repairedJson);
|
||||
|
||||
|
||||
if (partialData.slides) {
|
||||
const nextSlides: { content: string }[] = partialData.slides || [];
|
||||
// Determine which slide index changed to minimize live parsing
|
||||
|
|
@ -70,7 +70,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
|
|||
highestIndexRef.current = nextActive;
|
||||
setHighestActiveIndex(nextActive);
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
|
||||
prevSlidesRef.current = nextSlides;
|
||||
dispatch(setOutlines(nextSlides));
|
||||
|
|
@ -82,18 +82,18 @@ export const useOutlineStreaming = (presentationId: string | null) => {
|
|||
break;
|
||||
|
||||
case "complete":
|
||||
|
||||
|
||||
try {
|
||||
const outlinesData: { content: string }[] = data.presentation.outlines.slides;
|
||||
dispatch(setOutlines(outlinesData));
|
||||
setIsStreaming(false)
|
||||
setIsLoading(false)
|
||||
setActiveSlideIndex(null)
|
||||
setHighestActiveIndex(-1)
|
||||
prevSlidesRef.current = outlinesData;
|
||||
activeIndexRef.current = -1;
|
||||
highestIndexRef.current = -1;
|
||||
eventSource.close();
|
||||
setIsStreaming(false)
|
||||
setIsLoading(false)
|
||||
setActiveSlideIndex(null)
|
||||
setHighestActiveIndex(-1)
|
||||
prevSlidesRef.current = outlinesData;
|
||||
activeIndexRef.current = -1;
|
||||
highestIndexRef.current = -1;
|
||||
eventSource.close();
|
||||
} catch (error) {
|
||||
console.error("Error parsing accumulated chunks:", error);
|
||||
toast.error("Failed to parse presentation data");
|
||||
|
|
@ -103,7 +103,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
|
|||
break;
|
||||
|
||||
case "closing":
|
||||
|
||||
|
||||
setIsStreaming(false)
|
||||
setIsLoading(false)
|
||||
setActiveSlideIndex(null)
|
||||
|
|
@ -113,7 +113,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
|
|||
eventSource.close();
|
||||
break;
|
||||
case "error":
|
||||
|
||||
|
||||
setIsStreaming(false)
|
||||
setIsLoading(false)
|
||||
setActiveSlideIndex(null)
|
||||
|
|
@ -131,7 +131,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
|
|||
});
|
||||
|
||||
eventSource.onerror = () => {
|
||||
|
||||
|
||||
setIsStreaming(false)
|
||||
setIsLoading(false)
|
||||
setActiveSlideIndex(null)
|
||||
|
|
@ -142,7 +142,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
|
|||
toast.error("Failed to connect to the server. Please try again.");
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
|
||||
setIsStreaming(false)
|
||||
setIsLoading(false)
|
||||
setActiveSlideIndex(null)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const usePresentationStreaming = (
|
|||
trackEvent(MixpanelEvent.Presentation_Stream_API_Call);
|
||||
|
||||
eventSource = new EventSource(
|
||||
`/api/v1/ppt/presentation/stream?presentation_id=${presentationId}`
|
||||
`/api/v1/ppt/presentation/stream/${presentationId}`
|
||||
);
|
||||
|
||||
eventSource.addEventListener("response", (event) => {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export class DashboardApi {
|
|||
static async getPresentation(id: string) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/v1/ppt/presentation?id=${id}`,
|
||||
`/api/v1/ppt/presentation/${id}`,
|
||||
{
|
||||
method: "GET",
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ export class DashboardApi {
|
|||
static async deletePresentation(presentation_id: string) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/v1/ppt/presentation?id=${presentation_id}`,
|
||||
`/api/v1/ppt/presentation/${presentation_id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: getHeader(),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export class ImagesApi {
|
|||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const response = await fetch(`/api/v1/ppt/images/upload-image`, {
|
||||
const response = await fetch(`/api/v1/ppt/images/upload`, {
|
||||
method: "POST",
|
||||
headers: getHeaderForFormData(),
|
||||
body: formData,
|
||||
|
|
@ -33,7 +33,7 @@ export class ImagesApi {
|
|||
|
||||
static async deleteImage(image_id: string): Promise<{success: boolean, message?: string}> {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/ppt/images/uploaded-image/${image_id}`, {
|
||||
const response = await fetch(`/api/v1/ppt/images/${image_id}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
return await ApiResponseHandler.handleResponse(response, "Failed to delete image") as {success: boolean, message?: string};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue