Merged with main
This commit is contained in:
commit
4ebd480d46
30 changed files with 284 additions and 261 deletions
19
Dockerfile
19
Dockerfile
|
|
@ -2,24 +2,15 @@ FROM python:3.11-slim-bookworm
|
|||
|
||||
# Install Node.js and npm
|
||||
RUN apt-get update && apt-get install -y \
|
||||
nodejs \
|
||||
npm \
|
||||
|
||||
nginx \
|
||||
curl \
|
||||
redis-server \
|
||||
default-libmysqlclient-dev \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libreoffice \
|
||||
fontconfig \
|
||||
imagemagick
|
||||
redis-server
|
||||
|
||||
# Install Node.js 20 using NodeSource repository
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
RUN sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml
|
||||
|
||||
# Create fonts directory and set permissions
|
||||
RUN mkdir -p /usr/share/fonts/truetype && \
|
||||
chmod 755 /usr/share/fonts/truetype
|
||||
|
||||
# Create a working directory
|
||||
WORKDIR /app
|
||||
|
|
|
|||
|
|
@ -2,23 +2,15 @@ FROM python:3.11-slim-bookworm
|
|||
|
||||
# Install Node.js and npm
|
||||
RUN apt-get update && apt-get install -y \
|
||||
nodejs \
|
||||
npm \
|
||||
nginx \
|
||||
curl \
|
||||
redis-server \
|
||||
default-libmysqlclient-dev \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libreoffice \
|
||||
fontconfig \
|
||||
imagemagick
|
||||
redis-server
|
||||
|
||||
RUN sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml
|
||||
|
||||
# Create fonts directory and set permissions
|
||||
RUN mkdir -p /usr/share/fonts/truetype && \
|
||||
chmod 755 /usr/share/fonts/truetype
|
||||
# Install Node.js 20 using NodeSource repository
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
|
||||
# Change working directory
|
||||
WORKDIR /app
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -2,11 +2,12 @@ from contextlib import asynccontextmanager
|
|||
import os
|
||||
|
||||
from fastapi import FastAPI
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from services import SQL_ENGINE
|
||||
from services.database import create_db_and_tables
|
||||
from utils.get_env import get_app_data_directory_env
|
||||
from utils.model_availability import check_llm_and_image_provider_api_or_model_availability
|
||||
from utils.model_availability import (
|
||||
check_llm_and_image_provider_api_or_model_availability,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
|
|
@ -14,9 +15,9 @@ async def app_lifespan(_: FastAPI):
|
|||
"""
|
||||
Lifespan context manager for FastAPI application.
|
||||
Initializes the application data directory and checks LLM model availability.
|
||||
|
||||
|
||||
"""
|
||||
os.makedirs(get_app_data_directory_env(), exist_ok=True)
|
||||
SQLModel.metadata.create_all(SQL_ENGINE)
|
||||
await create_db_and_tables()
|
||||
await check_llm_and_image_provider_api_or_model_availability()
|
||||
yield
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
from typing import List
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import select
|
||||
|
||||
from models.image_prompt import ImagePrompt
|
||||
from models.sql.image_asset import ImageAsset
|
||||
from services.database import get_sql_session
|
||||
from services.database import get_async_session
|
||||
from services.image_generation_service import ImageGenerationService
|
||||
from utils.asset_directory_utils import get_images_directory
|
||||
|
||||
|
|
@ -12,7 +13,9 @@ IMAGES_ROUTER = APIRouter(prefix="/images", tags=["Images"])
|
|||
|
||||
|
||||
@IMAGES_ROUTER.get("/generate")
|
||||
async def generate_image(prompt: str):
|
||||
async def generate_image(
|
||||
prompt: str, sql_session: AsyncSession = Depends(get_async_session)
|
||||
):
|
||||
images_directory = get_images_directory()
|
||||
image_prompt = ImagePrompt(prompt=prompt)
|
||||
image_generation_service = ImageGenerationService(images_directory)
|
||||
|
|
@ -21,21 +24,18 @@ async def generate_image(prompt: str):
|
|||
if not isinstance(image, ImageAsset):
|
||||
return image
|
||||
|
||||
with get_sql_session() as sql_session:
|
||||
sql_session.add(image)
|
||||
sql_session.commit()
|
||||
image_path = image.path
|
||||
sql_session.add(image)
|
||||
await sql_session.commit()
|
||||
|
||||
return image_path
|
||||
return image.path
|
||||
|
||||
|
||||
@IMAGES_ROUTER.get("/generated", response_model=List[ImageAsset])
|
||||
async def get_generated_images():
|
||||
async def get_generated_images(sql_session: AsyncSession = Depends(get_async_session)):
|
||||
try:
|
||||
with get_sql_session() as sql_session:
|
||||
images = sql_session.exec(
|
||||
select(ImageAsset).order_by(ImageAsset.created_at.desc())
|
||||
).all()
|
||||
images = await sql_session.scalars(
|
||||
select(ImageAsset).order_by(ImageAsset.created_at.desc())
|
||||
)
|
||||
return images
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to retrieve generated images: {str(e)}"}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
import asyncio
|
||||
import json
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
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.database import get_sql_session
|
||||
from services.database import get_async_session
|
||||
from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
|
||||
|
||||
OUTLINES_ROUTER = APIRouter(prefix="/outlines", tags=["Outlines"])
|
||||
|
||||
|
||||
@OUTLINES_ROUTER.get("/stream")
|
||||
async def stream_outlines(presentation_id: str):
|
||||
with get_sql_session() as sql_session:
|
||||
presentation = sql_session.get(PresentationModel, presentation_id)
|
||||
async def stream_outlines(
|
||||
presentation_id: str, sql_session: AsyncSession = Depends(get_async_session)
|
||||
):
|
||||
presentation = await sql_session.get(PresentationModel, presentation_id)
|
||||
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
|
|
@ -54,10 +56,8 @@ async def stream_outlines(presentation_id: str):
|
|||
]
|
||||
presentation.notes = presentation_content.notes
|
||||
|
||||
with get_sql_session() as sql_session:
|
||||
sql_session.add(presentation)
|
||||
sql_session.commit()
|
||||
sql_session.refresh(presentation)
|
||||
sql_session.add(presentation)
|
||||
await sql_session.commit()
|
||||
|
||||
yield SSECompleteResponse(
|
||||
key="presentation", value=presentation.model_dump(mode="json")
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ import json
|
|||
import os
|
||||
import random
|
||||
from typing import Annotated, List, Literal, Optional
|
||||
import uuid
|
||||
from annotated_types import Len
|
||||
from fastapi import APIRouter, Body, File, HTTPException, UploadFile
|
||||
from fastapi import APIRouter, Body, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy import String, cast, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import select
|
||||
from constants.documents import UPLOAD_ACCEPTED_FILE_TYPES
|
||||
from models.presentation_and_path import PresentationPathAndEditPath
|
||||
|
|
@ -29,7 +28,7 @@ from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
|
|||
from models.sql.slide import SlideModel
|
||||
from models.sse_response import SSECompleteResponse, SSEResponse
|
||||
from services import TEMP_FILE_SERVICE
|
||||
from services.database import get_sql_session
|
||||
from services.database import get_async_session
|
||||
from services.documents_loader import DocumentsLoader
|
||||
from models.sql.presentation import PresentationModel
|
||||
from services.pptx_presentation_creator import PptxPresentationCreator
|
||||
|
|
@ -49,57 +48,58 @@ PRESENTATION_ROUTER = APIRouter(prefix="/presentation", tags=["Presentation"])
|
|||
|
||||
|
||||
@PRESENTATION_ROUTER.get("", response_model=PresentationWithSlides)
|
||||
def get_presentation(id: str):
|
||||
with get_sql_session() as sql_session:
|
||||
presentation = sql_session.get(PresentationModel, id)
|
||||
if not presentation:
|
||||
raise HTTPException(404, "Presentation not found")
|
||||
slides = sql_session.exec(
|
||||
select(SlideModel)
|
||||
.where(SlideModel.presentation == id)
|
||||
.order_by(SlideModel.index)
|
||||
)
|
||||
return PresentationWithSlides(
|
||||
**presentation.model_dump(),
|
||||
slides=slides,
|
||||
)
|
||||
async def get_presentation(
|
||||
id: str, sql_session: AsyncSession = Depends(get_async_session)
|
||||
):
|
||||
presentation = await sql_session.get(PresentationModel, id)
|
||||
if not presentation:
|
||||
raise HTTPException(404, "Presentation not found")
|
||||
slides = await sql_session.scalars(
|
||||
select(SlideModel)
|
||||
.where(SlideModel.presentation == id)
|
||||
.order_by(SlideModel.index)
|
||||
)
|
||||
return PresentationWithSlides(
|
||||
**presentation.model_dump(),
|
||||
slides=slides,
|
||||
)
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.delete("", status_code=204)
|
||||
def delete_presentation(id: str):
|
||||
with get_sql_session() as sql_session:
|
||||
presentation = sql_session.get(PresentationModel, id)
|
||||
if not presentation:
|
||||
raise HTTPException(404, "Presentation not found")
|
||||
slides = sql_session.exec(
|
||||
select(SlideModel).where(SlideModel.presentation == id)
|
||||
).all()
|
||||
for slide in slides:
|
||||
sql_session.delete(slide)
|
||||
sql_session.delete(presentation)
|
||||
sql_session.commit()
|
||||
async def delete_presentation(
|
||||
id: str, sql_session: AsyncSession = Depends(get_async_session)
|
||||
):
|
||||
presentation = await sql_session.get(PresentationModel, id)
|
||||
if not presentation:
|
||||
raise HTTPException(404, "Presentation not found")
|
||||
|
||||
await sql_session.execute(delete(SlideModel).where(SlideModel.presentation == id))
|
||||
await sql_session.delete(presentation)
|
||||
await sql_session.commit()
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.get("/all", response_model=List[PresentationWithSlides])
|
||||
def get_all_presentations():
|
||||
with get_sql_session() as sql_session:
|
||||
presentations_with_slides = []
|
||||
presentations = sql_session.exec(select(PresentationModel))
|
||||
for presentation in presentations:
|
||||
slides = sql_session.exec(
|
||||
select(SlideModel)
|
||||
.where(SlideModel.presentation == presentation.id)
|
||||
.where(SlideModel.index == 0)
|
||||
).all()
|
||||
if not slides:
|
||||
continue
|
||||
presentations_with_slides.append(
|
||||
PresentationWithSlides(
|
||||
**presentation.model_dump(),
|
||||
slides=slides,
|
||||
)
|
||||
)
|
||||
return presentations_with_slides
|
||||
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)
|
||||
|
|
@ -108,8 +108,9 @@ async def create_presentation(
|
|||
n_slides: Annotated[int, Body()],
|
||||
language: Annotated[str, Body()],
|
||||
file_paths: Annotated[Optional[List[str]], Body()] = None,
|
||||
sql_session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
presentation_id = str(uuid.uuid4())
|
||||
presentation_id = get_random_uuid()
|
||||
|
||||
summary = None
|
||||
if file_paths:
|
||||
|
|
@ -127,10 +128,8 @@ async def create_presentation(
|
|||
summary=summary,
|
||||
)
|
||||
|
||||
with get_sql_session() as sql_session:
|
||||
sql_session.add(presentation)
|
||||
sql_session.commit()
|
||||
sql_session.refresh(presentation)
|
||||
sql_session.add(presentation)
|
||||
await sql_session.commit()
|
||||
|
||||
return presentation
|
||||
|
||||
|
|
@ -141,12 +140,14 @@ async def prepare_presentation(
|
|||
outlines: Annotated[List[SlideOutlineModel], Body()],
|
||||
layout: Annotated[PresentationLayoutModel, Body()],
|
||||
title: Annotated[Optional[str], Body()] = None,
|
||||
sql_session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
if not outlines:
|
||||
raise HTTPException(status_code=400, detail="Outlines are required")
|
||||
|
||||
with get_sql_session() as sql_session:
|
||||
presentation = sql_session.get(PresentationModel, presentation_id)
|
||||
presentation = await sql_session.get(PresentationModel, presentation_id)
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
|
||||
total_slide_layouts = len(layout.slides)
|
||||
total_outlines = len(outlines)
|
||||
|
|
@ -170,34 +171,33 @@ async def prepare_presentation(
|
|||
if presentation_structure.slides[index] >= total_slide_layouts:
|
||||
presentation_structure.slides[index] = random_slide_index
|
||||
|
||||
with get_sql_session() as sql_session:
|
||||
sql_session.add(presentation)
|
||||
presentation.outlines = [each.model_dump() for each in outlines]
|
||||
presentation.title = title or presentation.title
|
||||
presentation.set_layout(layout)
|
||||
presentation.set_structure(presentation_structure)
|
||||
sql_session.commit()
|
||||
sql_session.refresh(presentation)
|
||||
sql_session.add(presentation)
|
||||
presentation.outlines = [each.model_dump() for each in outlines]
|
||||
presentation.title = title or presentation.title
|
||||
presentation.set_layout(layout)
|
||||
presentation.set_structure(presentation_structure)
|
||||
await sql_session.commit()
|
||||
|
||||
return presentation
|
||||
|
||||
|
||||
@PRESENTATION_ROUTER.get("/stream", response_model=PresentationWithSlides)
|
||||
async def stream_presentation(presentation_id: str):
|
||||
with get_sql_session() as sql_session:
|
||||
presentation = sql_session.get(PresentationModel, presentation_id)
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
if not presentation.structure:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Presentation not prepared for stream",
|
||||
)
|
||||
if not presentation.outlines:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Outlines can not be empty",
|
||||
)
|
||||
async def stream_presentation(
|
||||
presentation_id: str, sql_session: AsyncSession = Depends(get_async_session)
|
||||
):
|
||||
presentation = await sql_session.get(PresentationModel, presentation_id)
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
if not presentation.structure:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Presentation not prepared for stream",
|
||||
)
|
||||
if not presentation.outlines:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Outlines can not be empty",
|
||||
)
|
||||
|
||||
image_generation_service = ImageGenerationService(get_images_directory())
|
||||
icon_finder_service = IconFinderService()
|
||||
|
|
@ -218,7 +218,7 @@ async def stream_presentation(presentation_id: str):
|
|||
for i, slide_layout_index in enumerate(structure.slides):
|
||||
slide_layout = layout.slides[slide_layout_index]
|
||||
slide_content = await get_slide_content_from_type_and_outline(
|
||||
slide_layout, outline.slides[i]
|
||||
slide_layout, outline.slides[i], presentation.language
|
||||
)
|
||||
slide = SlideModel(
|
||||
presentation=presentation_id,
|
||||
|
|
@ -254,14 +254,10 @@ async def stream_presentation(presentation_id: str):
|
|||
for assets_list in generated_assets_lists:
|
||||
generated_assets.extend(assets_list)
|
||||
|
||||
with get_sql_session() as sql_session:
|
||||
sql_session.add(presentation)
|
||||
sql_session.add_all(slides)
|
||||
sql_session.add_all(generated_assets)
|
||||
sql_session.commit()
|
||||
sql_session.refresh(presentation)
|
||||
for each_slide in slides:
|
||||
sql_session.refresh(each_slide)
|
||||
sql_session.add(presentation)
|
||||
sql_session.add_all(slides)
|
||||
sql_session.add_all(generated_assets)
|
||||
await sql_session.commit()
|
||||
|
||||
response = PresentationWithSlides(
|
||||
**presentation.model_dump(),
|
||||
|
|
@ -277,25 +273,22 @@ async def stream_presentation(presentation_id: str):
|
|||
|
||||
|
||||
@PRESENTATION_ROUTER.put("/update", response_model=PresentationWithSlides)
|
||||
def update_presentation(
|
||||
async def update_presentation(
|
||||
presentation_with_slides: Annotated[PresentationWithSlides, Body()],
|
||||
sql_session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
updated_presentation = presentation_with_slides.to_presentation_model()
|
||||
updated_slides = presentation_with_slides.slides
|
||||
with get_sql_session() as sql_session:
|
||||
presentation = sql_session.get(PresentationModel, updated_presentation.id)
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
presentation.sqlmodel_update(updated_presentation)
|
||||
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)
|
||||
|
||||
sql_session.exec(
|
||||
delete(SlideModel).where(SlideModel.presentation == updated_presentation.id)
|
||||
)
|
||||
sql_session.add_all(updated_slides)
|
||||
sql_session.commit()
|
||||
sql_session.refresh(presentation)
|
||||
for slide in updated_slides:
|
||||
sql_session.refresh(slide)
|
||||
await sql_session.execute(
|
||||
delete(SlideModel).where(SlideModel.presentation == updated_presentation.id)
|
||||
)
|
||||
sql_session.add_all(updated_slides)
|
||||
await sql_session.commit()
|
||||
|
||||
return PresentationWithSlides(
|
||||
**presentation.model_dump(),
|
||||
|
|
@ -304,7 +297,9 @@ def update_presentation(
|
|||
|
||||
|
||||
@PRESENTATION_ROUTER.post("/export/pptx", response_model=str)
|
||||
async def create_pptx(pptx_model: Annotated[PptxPresentationModel, Body()]):
|
||||
async def create_pptx(
|
||||
pptx_model: Annotated[PptxPresentationModel, Body()],
|
||||
):
|
||||
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
|
||||
|
||||
pptx_creator = PptxPresentationCreator(pptx_model, temp_dir)
|
||||
|
|
@ -327,6 +322,7 @@ async def generate_presentation_api(
|
|||
layout: Annotated[str, Body()] = "general",
|
||||
files: Annotated[Optional[List[UploadFile]], File()] = None,
|
||||
export_as: Annotated[Literal["pptx", "pdf"], Body()] = "pptx",
|
||||
sql_session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
validate_files(files, True, True, 50, UPLOAD_ACCEPTED_FILE_TYPES)
|
||||
|
||||
|
|
@ -369,12 +365,12 @@ async def generate_presentation_api(
|
|||
print(f"Generated {total_outlines} outlines for the presentation")
|
||||
|
||||
# 4. Parse Layouts
|
||||
layout = await get_layout_by_name(layout)
|
||||
total_slide_layouts = len(layout.slides)
|
||||
layout_model = await get_layout_by_name(layout)
|
||||
total_slide_layouts = len(layout_model.slides)
|
||||
|
||||
# 5. Generate Structure
|
||||
if layout.ordered:
|
||||
presentation_structure = layout.to_presentation_structure()
|
||||
if layout_model.ordered:
|
||||
presentation_structure = layout_model.to_presentation_structure()
|
||||
else:
|
||||
presentation_structure: PresentationStructureModel = (
|
||||
await generate_presentation_structure(
|
||||
|
|
@ -383,7 +379,7 @@ async def generate_presentation_api(
|
|||
slides=outlines,
|
||||
notes=presentation_content.notes,
|
||||
),
|
||||
presentation_layout=layout,
|
||||
presentation_layout=layout_model,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -406,7 +402,7 @@ async def generate_presentation_api(
|
|||
summary=summary,
|
||||
outlines=[each.model_dump() for each in outlines],
|
||||
notes=presentation_content.notes,
|
||||
layout=layout.model_dump(),
|
||||
layout=layout_model.model_dump(),
|
||||
structure=presentation_structure.model_dump(),
|
||||
)
|
||||
|
||||
|
|
@ -418,14 +414,14 @@ async def generate_presentation_api(
|
|||
slides: List[SlideModel] = []
|
||||
slide_contents: List[dict] = []
|
||||
for i, slide_layout_index in enumerate(presentation_structure.slides):
|
||||
slide_layout = layout.slides[slide_layout_index]
|
||||
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]
|
||||
slide_layout, outlines[i], language
|
||||
)
|
||||
slide = SlideModel(
|
||||
presentation=presentation_id,
|
||||
layout_group=layout.name,
|
||||
layout_group=layout_model.name,
|
||||
layout=slide_layout.id,
|
||||
index=i,
|
||||
content=slide_content,
|
||||
|
|
@ -444,11 +440,10 @@ async def generate_presentation_api(
|
|||
generated_assets.extend(assets_list)
|
||||
|
||||
# 8. Save PresentationModel and Slides
|
||||
with get_sql_session() as sql_session:
|
||||
sql_session.add(presentation)
|
||||
sql_session.add_all(slides)
|
||||
sql_session.add_all(generated_assets)
|
||||
sql_session.commit()
|
||||
sql_session.add(presentation)
|
||||
sql_session.add_all(slides)
|
||||
sql_session.add_all(generated_assets)
|
||||
await sql_session.commit()
|
||||
|
||||
# 9. Export
|
||||
presentation_and_path = await export_presentation(
|
||||
|
|
@ -464,14 +459,14 @@ async def generate_presentation_api(
|
|||
@PRESENTATION_ROUTER.post("/from-template", response_model=PresentationPathAndEditPath)
|
||||
async def from_template(
|
||||
data: Annotated[GetPresentationUsingTemplateRequest, Body()],
|
||||
sql_session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
with get_sql_session() as sql_session:
|
||||
presentation = sql_session.get(PresentationModel, data.presentation_id)
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
slides = sql_session.exec(
|
||||
select(SlideModel).where(SlideModel.presentation == data.presentation_id)
|
||||
).all()
|
||||
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_presentation = presentation.get_new_presentation()
|
||||
new_slides = []
|
||||
|
|
@ -485,11 +480,9 @@ async def from_template(
|
|||
each_slide.get_new_slide(new_presentation.id, updated_content)
|
||||
)
|
||||
|
||||
with get_sql_session() as sql_session:
|
||||
sql_session.add(new_presentation)
|
||||
sql_session.add_all(new_slides)
|
||||
sql_session.commit()
|
||||
sql_session.refresh(new_presentation)
|
||||
sql_session.add(new_presentation)
|
||||
sql_session.add_all(new_slides)
|
||||
await sql_session.commit()
|
||||
|
||||
presentation_and_path = await export_presentation(
|
||||
new_presentation.id, new_presentation.title, data.export_as
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from typing import Annotated, Optional
|
||||
from fastapi import APIRouter, Body, HTTPException
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.sql.presentation import PresentationModel
|
||||
from models.sql.slide import SlideModel
|
||||
from services.database import get_sql_session
|
||||
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
|
||||
|
|
@ -18,15 +19,17 @@ SLIDE_ROUTER = APIRouter(prefix="/slide", tags=["Slide"])
|
|||
|
||||
|
||||
@SLIDE_ROUTER.post("/edit")
|
||||
async def edit_slide(id: Annotated[str, Body()], prompt: Annotated[str, Body()]):
|
||||
|
||||
with get_sql_session() as sql_session:
|
||||
slide = sql_session.get(SlideModel, id)
|
||||
if not slide:
|
||||
raise HTTPException(status_code=404, detail="Slide not found")
|
||||
presentation = sql_session.get(PresentationModel, slide.presentation)
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
async def edit_slide(
|
||||
id: Annotated[str, Body()],
|
||||
prompt: Annotated[str, Body()],
|
||||
sql_session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
slide = await sql_session.get(SlideModel, id)
|
||||
if not slide:
|
||||
raise HTTPException(status_code=404, detail="Slide not found")
|
||||
presentation = await sql_session.get(PresentationModel, slide.presentation)
|
||||
if not presentation:
|
||||
raise HTTPException(status_code=404, detail="Presentation not found")
|
||||
|
||||
presentation_layout = presentation.get_layout()
|
||||
|
||||
|
|
@ -51,13 +54,11 @@ async def edit_slide(id: Annotated[str, Body()], prompt: Annotated[str, Body()])
|
|||
# Always assign a new unique id to the slide
|
||||
slide.id = get_random_uuid()
|
||||
|
||||
with get_sql_session() as sql_session:
|
||||
sql_session.add(slide)
|
||||
slide.content = edited_slide_content
|
||||
slide.layout = slide_layout.id
|
||||
sql_session.add_all(new_assets)
|
||||
sql_session.commit()
|
||||
sql_session.refresh(slide)
|
||||
sql_session.add(slide)
|
||||
slide.content = edited_slide_content
|
||||
slide.layout = slide_layout.id
|
||||
sql_session.add_all(new_assets)
|
||||
await sql_session.commit()
|
||||
|
||||
return slide
|
||||
|
||||
|
|
@ -67,11 +68,11 @@ async def edit_slide_html(
|
|||
id: Annotated[str, Body()],
|
||||
prompt: Annotated[str, Body()],
|
||||
html: Annotated[Optional[str], Body()] = None,
|
||||
sql_session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
with get_sql_session() as sql_session:
|
||||
slide = sql_session.get(SlideModel, id)
|
||||
if not slide:
|
||||
raise HTTPException(status_code=404, detail="Slide not found")
|
||||
slide = await sql_session.get(SlideModel, id)
|
||||
if not slide:
|
||||
raise HTTPException(status_code=404, detail="Slide not found")
|
||||
|
||||
html_to_edit = html or slide.html_content
|
||||
if not html_to_edit:
|
||||
|
|
@ -83,10 +84,8 @@ async def edit_slide_html(
|
|||
# This is to ensure that the nextjs can track slide updates
|
||||
slide.id = get_random_uuid()
|
||||
|
||||
with get_sql_session() as sql_session:
|
||||
sql_session.add(slide)
|
||||
slide.html_content = edited_slide_html
|
||||
sql_session.commit()
|
||||
sql_session.refresh(slide)
|
||||
sql_session.add(slide)
|
||||
slide.html_content = edited_slide_html
|
||||
await sql_session.commit()
|
||||
|
||||
return slide
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -21,7 +21,7 @@ class PresentationWithSlides(BaseModel):
|
|||
summary: Optional[str]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
layout: PresentationLayoutModel
|
||||
layout: Optional[PresentationLayoutModel]
|
||||
structure: Optional[PresentationStructureModel]
|
||||
slides: List[SlideModel]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
from sqlalchemy import JSON, Column, DateTime
|
||||
from sqlmodel import SQLModel, Field
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from typing import Optional
|
||||
import uuid
|
||||
from sqlmodel import SQLModel, Field, Column, JSON
|
||||
|
||||
from utils.randomizers import get_random_uuid
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
aiohappyeyeballs==2.6.1
|
||||
aiohttp==3.12.14
|
||||
aiomysql==0.2.0
|
||||
aiosignal==1.4.0
|
||||
aiosqlite==0.21.0
|
||||
annotated-types==0.7.0
|
||||
anthropic==0.60.0
|
||||
anyio==4.9.0
|
||||
async-timeout==5.0.1
|
||||
asyncpg==0.30.0
|
||||
attrs==25.3.0
|
||||
backoff==2.2.1
|
||||
bcrypt==4.3.0
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
from services.redis_service import RedisService
|
||||
from services.temp_file_service import TempFileService
|
||||
from services.database import sql_engine
|
||||
|
||||
|
||||
TEMP_FILE_SERVICE = TempFileService()
|
||||
SQL_ENGINE = sql_engine
|
||||
REDIS_SERVICE = RedisService()
|
||||
|
|
|
|||
|
|
@ -1,25 +1,42 @@
|
|||
from contextlib import contextmanager
|
||||
from collections.abc import AsyncGenerator
|
||||
import os
|
||||
from sqlalchemy import create_engine
|
||||
from sqlmodel import Session
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncEngine,
|
||||
create_async_engine,
|
||||
async_sessionmaker,
|
||||
AsyncSession,
|
||||
)
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from utils.get_env import get_app_data_directory_env, get_database_url_env
|
||||
|
||||
|
||||
database_url = get_database_url_env() or "sqlite:///" + os.path.join(
|
||||
raw_database_url = get_database_url_env() or "sqlite:///" + os.path.join(
|
||||
get_app_data_directory_env() or "/tmp/presenton", "fastapi.db"
|
||||
)
|
||||
|
||||
if raw_database_url.startswith("sqlite://"):
|
||||
database_url = raw_database_url.replace("sqlite://", "sqlite+aiosqlite://", 1)
|
||||
elif raw_database_url.startswith("postgresql://"):
|
||||
database_url = raw_database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
elif raw_database_url.startswith("mysql://"):
|
||||
database_url = raw_database_url.replace("mysql://", "mysql+aiomysql://", 1)
|
||||
else:
|
||||
database_url = raw_database_url
|
||||
|
||||
connect_args = {}
|
||||
if "sqlite" in database_url:
|
||||
connect_args["check_same_thread"] = False
|
||||
|
||||
sql_engine = create_engine(database_url, connect_args=connect_args)
|
||||
sql_engine: AsyncEngine = create_async_engine(database_url, connect_args=connect_args)
|
||||
async_session_maker = async_sessionmaker(sql_engine, expire_on_commit=False)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_sql_session():
|
||||
session = Session(sql_engine)
|
||||
try:
|
||||
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
async def create_db_and_tables():
|
||||
async with sql_engine.begin() as conn:
|
||||
await conn.run_sync(SQLModel.metadata.create_all)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import asyncio
|
||||
import os
|
||||
import uuid
|
||||
import aiohttp
|
||||
from google import genai
|
||||
from google.genai.types import GenerateContentConfig
|
||||
|
|
@ -16,6 +15,7 @@ from utils.image_provider import (
|
|||
is_gemini_flash_selected,
|
||||
is_dalle3_selected,
|
||||
)
|
||||
from utils.randomizers import get_random_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"{str(uuid.uuid4())}.jpg")
|
||||
image_path = os.path.join(output_directory, f"{get_random_uuid()}.jpg")
|
||||
with open(image_path, "wb") as f:
|
||||
f.write(part.inline_data.data)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
from lxml import etree
|
||||
|
||||
from pptx import Presentation
|
||||
|
|
@ -41,6 +40,7 @@ from utils.image_utils import (
|
|||
round_image_corners,
|
||||
set_image_opacity,
|
||||
)
|
||||
from utils.randomizers import get_random_uuid
|
||||
|
||||
BLANK_SLIDE_LAYOUT = 6
|
||||
|
||||
|
|
@ -74,9 +74,9 @@ class PptxPresentationCreator:
|
|||
image_path = each_shape.picture.path
|
||||
if image_path.startswith("http"):
|
||||
if "app_data/" in image_path:
|
||||
relative_path = image_path.split("/app_data/")[1]
|
||||
relative_path = image_path.split("app_data/")[1]
|
||||
each_shape.picture.path = os.path.join(
|
||||
"app_data", relative_path
|
||||
"/app_data", relative_path
|
||||
)
|
||||
each_shape.picture.is_network = False
|
||||
continue
|
||||
|
|
@ -89,9 +89,9 @@ class PptxPresentationCreator:
|
|||
image_path = each_shape.picture.path
|
||||
if image_path.startswith("http"):
|
||||
if "app_data" in image_path:
|
||||
relative_path = image_path.split("/app_data/")[1]
|
||||
relative_path = image_path.split("app_data/")[1]
|
||||
each_shape.picture.path = os.path.join(
|
||||
"app_data", relative_path
|
||||
"/app_data", relative_path
|
||||
)
|
||||
each_shape.picture.is_network = False
|
||||
continue
|
||||
|
|
@ -210,7 +210,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"{str(uuid.uuid4())}.png")
|
||||
image_path = os.path.join(self._temp_dir, f"{get_random_uuid()}.png")
|
||||
image.save(image_path)
|
||||
|
||||
margined_position = self.get_margined_position(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import os
|
||||
import uuid
|
||||
from typing import Optional, Union
|
||||
|
||||
from utils.get_env import get_temp_directory_env
|
||||
from utils.randomizers import get_random_uuid
|
||||
|
||||
|
||||
class TempFileService:
|
||||
|
|
@ -14,7 +14,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 str(uuid.uuid4()))
|
||||
temp_dir = os.path.join(base_dir, dir_name if dir_name else get_random_uuid())
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
return temp_dir
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import asyncio
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from models.presentation_layout import SlideLayoutModel
|
||||
from models.sql.slide import SlideModel
|
||||
|
|
@ -21,24 +20,32 @@ system_prompt = """
|
|||
- The goal is to change Slide data based on the provided prompt.
|
||||
- Do not change **Image prompts** and **Icon queries** if not asked for in prompt.
|
||||
- Generate **Image prompts** and **Icon queries** if asked to generate or change in prompt.
|
||||
- Make sure to follow language guidelines.
|
||||
|
||||
**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: Optional[str] = None):
|
||||
def get_user_prompt(prompt: str, slide_data: dict, language: str):
|
||||
return f"""
|
||||
- Prompt: {prompt}
|
||||
- Output Language: {language}
|
||||
- Image Prompts and Icon Queries Language: English
|
||||
- Slide data: {slide_data}
|
||||
## Icon Query And Image Prompt Language
|
||||
English
|
||||
|
||||
## Slide Content Language
|
||||
{language}
|
||||
|
||||
## Prompt
|
||||
{prompt}
|
||||
|
||||
## Slide data
|
||||
{slide_data}
|
||||
"""
|
||||
|
||||
|
||||
def get_prompt_to_edit_slide_content(
|
||||
prompt: str,
|
||||
slide_data: dict,
|
||||
language: Optional[str] = None,
|
||||
language: str,
|
||||
):
|
||||
return [
|
||||
{
|
||||
|
|
@ -56,7 +63,7 @@ async def get_edited_slide_content(
|
|||
prompt: str,
|
||||
slide_layout: SlideLayoutModel,
|
||||
slide: SlideModel,
|
||||
language: Optional[str] = None,
|
||||
language: str,
|
||||
):
|
||||
model = get_large_model()
|
||||
response_schema = remove_fields_from_schema(
|
||||
|
|
|
|||
|
|
@ -24,12 +24,19 @@ system_prompt = """
|
|||
- Provide prompt to generate image on "__image_prompt__" property.
|
||||
- Provide query to search icon on "__icon_query__" property.
|
||||
- Do not use markdown formatting in slide body.
|
||||
- **Strictly follow the max and min character limit for every property in the slide.**
|
||||
- Make sure to follow language guidelines.
|
||||
**Strictly follow the max and min character limit for every property in the slide.**
|
||||
"""
|
||||
|
||||
|
||||
def get_user_prompt(title: str, outline: str):
|
||||
def get_user_prompt(title: str, outline: str, language: str):
|
||||
return f"""
|
||||
## Icon Query And Image Prompt Language
|
||||
English
|
||||
|
||||
## Slide Content Language
|
||||
{language}
|
||||
|
||||
## Slide Title
|
||||
{title}
|
||||
|
||||
|
|
@ -38,7 +45,7 @@ def get_user_prompt(title: str, outline: str):
|
|||
"""
|
||||
|
||||
|
||||
def get_prompt_to_generate_slide_content(title: str, outline: str):
|
||||
def get_prompt_to_generate_slide_content(title: str, outline: str, language: str):
|
||||
|
||||
return [
|
||||
{
|
||||
|
|
@ -47,13 +54,13 @@ def get_prompt_to_generate_slide_content(title: str, outline: str):
|
|||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": get_user_prompt(title, outline),
|
||||
"content": get_user_prompt(title, outline, language),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def get_slide_content_from_type_and_outline(
|
||||
slide_layout: SlideLayoutModel, outline: SlideOutlineModel
|
||||
slide_layout: SlideLayoutModel, outline: SlideOutlineModel, language: str
|
||||
):
|
||||
model = get_large_model()
|
||||
|
||||
|
|
@ -68,6 +75,7 @@ async def get_slide_content_from_type_and_outline(
|
|||
messages=get_prompt_to_generate_slide_content(
|
||||
outline.title,
|
||||
outline.body,
|
||||
language,
|
||||
),
|
||||
response_format={
|
||||
"type": "json_schema",
|
||||
|
|
@ -83,7 +91,7 @@ async def get_slide_content_from_type_and_outline(
|
|||
response = await asyncio.to_thread(
|
||||
client.models.generate_content,
|
||||
model=model,
|
||||
contents=[get_user_prompt(outline.title, outline.body)],
|
||||
contents=[get_user_prompt(outline.title, outline.body, language)],
|
||||
config=GenerateContentConfig(
|
||||
system_instruction=system_prompt,
|
||||
response_mime_type="application/json",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ def set_ollama_url_env(value):
|
|||
|
||||
|
||||
def set_custom_llm_url_env(value):
|
||||
os.environ["CUSTOM_URL"] = value
|
||||
os.environ["CUSTOM_LLM_URL"] = value
|
||||
|
||||
|
||||
def set_openai_api_key_env(value):
|
||||
|
|
@ -44,9 +44,10 @@ def set_custom_model_env(value):
|
|||
def set_pexels_api_key_env(value):
|
||||
os.environ["PEXELS_API_KEY"] = value
|
||||
|
||||
|
||||
def set_image_provider_env(value):
|
||||
os.environ["IMAGE_PROVIDER"] = value
|
||||
|
||||
|
||||
def set_pixabay_api_key_env(value):
|
||||
os.environ["PIXABAY_API_KEY"] = value
|
||||
os.environ["PIXABAY_API_KEY"] = value
|
||||
|
|
|
|||
|
|
@ -1,14 +1,30 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { marked } from "marked";
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content }) => {
|
||||
const markdownContent = marked.parse(content);
|
||||
const [markdownContent, setMarkdownContent] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const parseMarkdown = async () => {
|
||||
try {
|
||||
const parsed = await marked.parse(content);
|
||||
setMarkdownContent(parsed);
|
||||
} catch (error) {
|
||||
console.error("Error parsing markdown:", error);
|
||||
setMarkdownContent("");
|
||||
}
|
||||
};
|
||||
|
||||
parseMarkdown();
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="prose prose-slate max-w-none mb-10"
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
|
|||
for (const childElementHandle of directChildElementHandles) {
|
||||
const attributes = await getElementAttributes(childElementHandle);
|
||||
|
||||
if (attributes.tagName === "style") {
|
||||
if (['style', 'script', 'link', 'meta', 'path'].includes(attributes.tagName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -268,8 +268,8 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
|
|||
|
||||
allResults.push({ attributes, depth });
|
||||
|
||||
//? If the element is a svg, canvas, or table, we don't need to go deeper
|
||||
if (attributes.should_screenshot) {
|
||||
//? If the element is a canvas, or table, we don't need to go deeper
|
||||
if (attributes.should_screenshot && attributes.tagName !== 'svg') {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@
|
|||
"@tiptap/extension-underline": "^2.0.0",
|
||||
"@tiptap/react": "^2.11.5",
|
||||
"@tiptap/starter-kit": "^2.11.5",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"description": "Default layout for presentations",
|
||||
"ordered": false,
|
||||
"default": true
|
||||
"default": false
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"description": "General purpose layouts for common presentation elements",
|
||||
"ordered": false,
|
||||
"default": false
|
||||
"default": true
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue