Merged with main

This commit is contained in:
shiva raj badu 2025-07-31 11:46:08 +05:45
commit 4ebd480d46
No known key found for this signature in database
30 changed files with 284 additions and 261 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
from typing import Optional
import uuid
from sqlmodel import SQLModel, Field, Column, JSON
from utils.randomizers import get_random_uuid

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
}

View file

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

View file

@ -1,5 +1,5 @@
{
"description": "Default layout for presentations",
"ordered": false,
"default": true
"default": false
}

View file

@ -1,5 +1,5 @@
{
"description": "General purpose layouts for common presentation elements",
"ordered": false,
"default": false
"default": true
}