feat(fastapi): adds image gen and icon search endpoints, chore(fastapi): seperates middlewares and lifespan from main.py

This commit is contained in:
sauravniraula 2025-07-16 04:36:19 +05:45
parent b048197f02
commit ffa22b1663
No known key found for this signature in database
GPG key ID: 60FCC1B5A5E83326
12 changed files with 631 additions and 62 deletions

View file

@ -0,0 +1,16 @@
from contextlib import asynccontextmanager
import os
from fastapi import FastAPI
from sqlmodel import SQLModel
from services import SQL_ENGINE
from utils.model_availability import check_llm_model_availability
@asynccontextmanager
async def app_lifespan(_: FastAPI):
os.makedirs(os.getenv("APP_DATA_DIRECTORY"), exist_ok=True)
SQLModel.metadata.create_all(SQL_ENGINE)
await check_llm_model_availability()
yield

View file

@ -1,41 +1,25 @@
from contextlib import asynccontextmanager
import os
from fastapi import FastAPI, Request
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from sqlmodel import SQLModel
from api.lifespan import app_lifespan
from api.middlewares import UserConfigEnvUpdateMiddleware
from api.v1.ppt.router import API_V1_PPT_ROUTER
from services import SQL_ENGINE
from utils.check_llm_model_availability import check_llm_model_availability
from utils.get_env import get_can_change_keys_env
from utils.user_config import update_env_with_user_config
# Lifespan
@asynccontextmanager
async def app_lifespan(_: FastAPI):
os.makedirs(os.getenv("APP_DATA_DIRECTORY"), exist_ok=True)
SQLModel.metadata.create_all(SQL_ENGINE)
await check_llm_model_availability()
yield
# App
APP = FastAPI(lifespan=app_lifespan)
# Static files
APP.mount("/static", StaticFiles(directory="static"), name="static")
# APP.mount("/static/app-data", StaticFiles(directory=get_app_data_directory_env()))
app = FastAPI(lifespan=app_lifespan)
# Routers
APP.include_router(API_V1_PPT_ROUTER)
app.include_router(API_V1_PPT_ROUTER)
# Static files
app.mount("/static", StaticFiles(directory="static"), name="static")
# APP.mount("/static/app-data", StaticFiles(directory=get_app_data_directory_env()))
# Middlewares
origins = ["*"]
APP.add_middleware(
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
@ -43,9 +27,4 @@ APP.add_middleware(
allow_headers=["*"],
)
@APP.middleware("http")
async def update_env_middleware(request: Request, call_next):
if get_can_change_keys_env() != "false":
update_env_with_user_config()
return await call_next(request)
app.add_middleware(UserConfigEnvUpdateMiddleware)

View file

@ -0,0 +1,13 @@
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
from utils.get_env import get_can_change_keys_env
from utils.user_config import update_env_with_user_config
class UserConfigEnvUpdateMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if get_can_change_keys_env() != "false":
update_env_with_user_config()
return await call_next(request)

View file

@ -0,0 +1,11 @@
from typing import List
from fastapi import APIRouter
from services.icon_finder_service import IconFinderService
ICONS_ROUTER = APIRouter(prefix="/icons", tags=["Icons"])
@ICONS_ROUTER.get("/search", response_model=List[str])
async def search_icons(query: str, limit: int = 20):
icon_finder_service = IconFinderService()
return await icon_finder_service.search_icons(query, limit)

View file

@ -1,9 +1,17 @@
from typing import Annotated, List
from fastapi import APIRouter, File, UploadFile
from typing import Annotated
from fastapi import APIRouter, Body
from models.image_prompt import ImagePrompt
from services import TEMP_FILE_SERVICE
from services.image_generation_service import ImageGenerationService
IMAGES_ROUTER = APIRouter(prefix="/images", tags=["Images"])
@IMAGES_ROUTER.post("/upload")
async def upload_images(images: Annotated[List[UploadFile], File()]):
pass
@IMAGES_ROUTER.get("/generate")
async def generate_image(prompt: str):
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
image_prompt = ImagePrompt(prompt=prompt)
image_generation_service = ImageGenerationService(temp_dir)
return await image_generation_service.generate_image(image_prompt)

View file

@ -1,6 +1,7 @@
from fastapi import APIRouter
from api.v1.ppt.endpoints.files import FILES_ROUTER
from api.v1.ppt.endpoints.icons import ICONS_ROUTER
from api.v1.ppt.endpoints.images import IMAGES_ROUTER
from api.v1.ppt.endpoints.outlines import OUTLINES_ROUTER
from api.v1.ppt.endpoints.presentation import PRESENTATION_ROUTER
@ -12,3 +13,4 @@ API_V1_PPT_ROUTER.include_router(FILES_ROUTER)
API_V1_PPT_ROUTER.include_router(OUTLINES_ROUTER)
API_V1_PPT_ROUTER.include_router(PRESENTATION_ROUTER)
API_V1_PPT_ROUTER.include_router(IMAGES_ROUTER)
API_V1_PPT_ROUTER.include_router(ICONS_ROUTER)

View file

@ -1,31 +1,560 @@
from typing import List
from pydantic import BaseModel, Field
from typing import List, Optional
from pydantic import BaseModel, Field, HttpUrl, EmailStr
from models.presentation_layout import PresentationLayoutModel, SlideLayoutModel
class TitleDescriptionSlide(BaseModel):
title: str = Field(min_length=10, max_length=100)
description: str = Field(min_length=50, max_length=200)
class ContactInfoModel(BaseModel):
email: Optional[EmailStr] = Field(None, description="Contact email")
phone: Optional[str] = Field(
None, min_length=5, max_length=50, description="Contact phone number"
)
website: Optional[HttpUrl] = Field(None, description="Website URL")
class ContentSlide(BaseModel):
title: str = Field(min_length=10, max_length=100)
content: List[str] = Field(min_length=1, max_length=5)
# First Slide Layout
class FirstSlideModel(BaseModel):
title: str = Field(
"Welcome to Our Presentation",
min_length=3,
max_length=100,
description="Main title of the presentation",
)
subtitle: Optional[str] = Field(
None, min_length=10, max_length=200, description="Optional subtitle or tagline"
)
author: Optional[str] = Field(
"John Doe",
min_length=2,
max_length=100,
description="Author or presenter name",
)
date: Optional[str] = Field(None, description="Presentation date")
company: Optional[str] = Field(
"Company Name",
min_length=2,
max_length=100,
description="Company or organization name",
)
backgroundImage: Optional[HttpUrl] = Field(
None, description="URL to background image for the slide"
)
# Bullet Point Slide Layout
class BulletPointSlideModel(BaseModel):
title: str = Field(
"Key Points",
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
None,
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
icon: Optional[str] = Field(None, description="Icon to display in the slide")
bulletPoints: List[str] = Field(
[
"First key point that highlights important information",
"Second bullet point with valuable insights",
"Third point demonstrating clear benefits",
"Fourth item showcasing key features",
],
min_length=2,
max_length=8,
description="List of bullet points (2-8 items)",
)
# Image Slide Layout
class ImageSlideModel(BaseModel):
title: str = Field(
"Image Showcase",
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
"Subtitle for the slide",
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
image: HttpUrl = Field(
"https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&h=600&fit=crop",
description="Main image URL",
)
imageCaption: Optional[str] = Field(
"Image caption",
min_length=5,
max_length=200,
description="Optional image caption or description",
)
content: Optional[str] = Field(
None,
min_length=10,
max_length=600,
description="Optional supporting content text",
)
backgroundImage: Optional[HttpUrl] = Field(
None, description="URL to background image for the slide"
)
# Statistics Slide Layout
class StatisticItemModel(BaseModel):
value: str = Field(
min_length=1,
max_length=20,
description="Statistical value (e.g., '250%', '$1.2M', '99.9%')",
)
label: str = Field(
min_length=3, max_length=100, description="Description of the statistic"
)
trend: Optional[str] = Field(
None, description="Trend direction indicator", pattern="^(up|down|neutral)$"
)
context: Optional[str] = Field(
None,
min_length=5,
max_length=200,
description="Additional context or time period",
)
class StatisticsSlideModel(BaseModel):
title: str = Field(
"Key Statistics",
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
None,
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
statistics: List[StatisticItemModel] = Field(
[
StatisticItemModel(
value="250%",
label="Revenue Growth",
trend="up",
context="Year over year increase",
),
StatisticItemModel(
value="50M+",
label="Active Users",
trend="up",
context="Global user base",
),
StatisticItemModel(
value="99.9%",
label="Uptime",
trend="neutral",
context="Service reliability",
),
StatisticItemModel(
value="24/7",
label="Support",
trend="neutral",
context="Customer service",
),
],
min_length=2,
max_length=6,
description="List of statistics (2-6 items)",
)
backgroundImage: Optional[HttpUrl] = Field(
None, description="URL to background image for the slide"
)
# Quote Slide Layout
class QuoteSlideModel(BaseModel):
title: str = Field(
"Testimonials",
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
None,
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
quote: str = Field(
"This solution has transformed our business operations and exceeded all expectations.",
min_length=10,
max_length=500,
description="The main quote or testimonial",
)
author: str = Field(
"John Smith",
min_length=2,
max_length=100,
description="Quote author name",
)
authorTitle: Optional[str] = Field(
None, min_length=2, max_length=100, description="Author job title or position"
)
company: Optional[str] = Field(
None, min_length=2, max_length=100, description="Author company or organization"
)
authorImage: Optional[HttpUrl] = Field(None, description="URL to author photo")
backgroundImage: Optional[HttpUrl] = Field(
None, description="URL to background image for the slide"
)
# Timeline Slide Layout
class TimelineItemModel(BaseModel):
date: str = Field(min_length=2, max_length=50, description="Date or time period")
title: str = Field(
min_length=3, max_length=100, description="Event or milestone title"
)
description: str = Field(
min_length=10, max_length=300, description="Event description"
)
status: str = Field(
"upcoming",
description="Timeline item status",
pattern="^(completed|current|upcoming)$",
)
class TimelineSlideModel(BaseModel):
title: str = Field(
"Project Timeline",
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
None,
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
timelineItems: List[TimelineItemModel] = Field(
[
TimelineItemModel(
date="Q1 2024",
title="Project Initiation",
description="Project planning, team assembly, and initial requirements gathering",
status="completed",
),
TimelineItemModel(
date="Q2 2024",
title="Development Phase",
description="Core development work, prototype creation, and testing implementation",
status="current",
),
TimelineItemModel(
date="Q3 2024",
title="Testing & QA",
description="Comprehensive testing, quality assurance, and user acceptance testing",
status="upcoming",
),
TimelineItemModel(
date="Q4 2024",
title="Launch & Deployment",
description="Final deployment, go-live activities, and post-launch monitoring",
status="upcoming",
),
],
min_length=2,
max_length=6,
description="Timeline events (2-6 items)",
)
backgroundImage: Optional[HttpUrl] = Field(
None, description="URL to background image for the slide"
)
# Team Slide Layout
class TeamMemberModel(BaseModel):
name: str = Field(min_length=2, max_length=100, description="Team member name")
title: str = Field(min_length=2, max_length=100, description="Job title or role")
image: Optional[HttpUrl] = Field(None, description="URL to team member photo")
bio: Optional[str] = Field(
None,
min_length=10,
max_length=300,
description="Brief biography or description",
)
email: Optional[EmailStr] = Field(None, description="Contact email")
linkedin: Optional[HttpUrl] = Field(None, description="LinkedIn profile URL")
class TeamSlideModel(BaseModel):
title: str = Field(
"Meet Our Team",
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
None,
min_length=3,
max_length=150,
description="Optional subtitle or team description",
)
teamMembers: List[TeamMemberModel] = Field(
[
TeamMemberModel(
name="Sarah Johnson",
title="Chief Executive Officer",
image="https://images.unsplash.com/photo-1494790108755-2616b612b786?w=300&h=300&fit=crop&crop=face",
bio="Strategic leader with 15+ years experience driving innovation and growth in technology companies.",
email="sarah@company.com",
linkedin="https://linkedin.com/in/sarahjohnson",
),
TeamMemberModel(
name="Michael Chen",
title="Chief Technology Officer",
image="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=300&h=300&fit=crop&crop=face",
bio="Technology visionary specializing in scalable architecture and emerging technologies.",
email="michael@company.com",
linkedin="https://linkedin.com/in/michaelchen",
),
TeamMemberModel(
name="Emma Rodriguez",
title="Head of Design",
image="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=300&h=300&fit=crop&crop=face",
bio="Creative director passionate about user-centered design and innovative digital experiences.",
email="emma@company.com",
linkedin="https://linkedin.com/in/emmarodriguez",
),
],
min_length=1,
max_length=6,
description="Team members (1-6 people)",
)
backgroundImage: Optional[HttpUrl] = Field(
None, description="URL to background image for the slide"
)
# Process Slide Layout
class ProcessStepModel(BaseModel):
step: int = Field(ge=1, le=10, description="Step number")
title: str = Field(min_length=3, max_length=100, description="Step title")
description: str = Field(
min_length=10, max_length=200, description="Step description"
)
class ProcessSlideModel(BaseModel):
title: str = Field(
"Our Process",
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
None,
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
processSteps: List[ProcessStepModel] = Field(
[
ProcessStepModel(
step=1,
title="Discovery",
description="Understanding requirements and gathering initial insights",
),
ProcessStepModel(
step=2,
title="Planning",
description="Strategic planning and roadmap development",
),
ProcessStepModel(
step=3,
title="Implementation",
description="Executing the plan with precision and quality",
),
ProcessStepModel(
step=4,
title="Delivery",
description="Final delivery and ongoing support",
),
],
min_length=2,
max_length=6,
description="Process steps (2-6 items)",
)
backgroundImage: Optional[HttpUrl] = Field(
None, description="URL to background image for the slide"
)
# Two Column Slide Layout
class ColumnContentModel(BaseModel):
title: str = Field(min_length=3, max_length=100, description="Column title")
content: str = Field(min_length=10, max_length=800, description="Column content")
class TwoColumnSlideModel(BaseModel):
title: str = Field(
"Two Column Layout",
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
None,
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
leftColumn: ColumnContentModel = Field(
ColumnContentModel(
title="Left Column",
content="Content for the left column goes here. This can include detailed information, explanations, or supporting details.",
),
description="Left column content",
)
rightColumn: ColumnContentModel = Field(
ColumnContentModel(
title="Right Column",
content="Content for the right column goes here. This can include additional information, comparisons, or contrasting details.",
),
description="Right column content",
)
backgroundImage: Optional[HttpUrl] = Field(
None, description="URL to background image for the slide"
)
# Conclusion Slide Layout
class ConclusionSlideModel(BaseModel):
title: str = Field(
"Conclusion",
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
None,
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
keyTakeaways: List[str] = Field(
[
"Successfully achieved our primary objectives",
"Demonstrated significant value and impact",
"Established clear next steps for continued success",
"Built strong foundation for future growth",
],
min_length=2,
max_length=6,
description="Key takeaways or summary points (2-6 items)",
)
callToAction: Optional[str] = Field(
None,
min_length=5,
max_length=150,
description="Optional call to action or next steps",
)
contactInfo: Optional[ContactInfoModel] = Field(
None, description="Optional contact information"
)
backgroundImage: Optional[HttpUrl] = Field(
None, description="URL to background image for the slide"
)
# Content Slide Layout
class ContentSlideModel(BaseModel):
title: str = Field(
"Slide Title",
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
None,
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
content: str = Field(
"Your slide content goes here. This is where you can add detailed information, explanations, or any other text content that supports your presentation.",
min_length=10,
max_length=1000,
description="Main content text",
)
backgroundImage: Optional[HttpUrl] = Field(
None, description="URL to background image for the slide"
)
# Create the presentation layout with all slide types
presentation_layout = PresentationLayoutModel(
name="Basic Presentation",
name="Complete Presentation Layout",
slides=[
SlideLayoutModel(
id="title_description",
name="Title Description",
json_schema=TitleDescriptionSlide.model_json_schema(),
id="first-slide",
name="First Slide",
json_schema=FirstSlideModel.model_json_schema(),
),
SlideLayoutModel(
id="content",
name="Content",
json_schema=ContentSlide.model_json_schema(),
id="bullet-point-slide",
name="Bullet Point Slide",
json_schema=BulletPointSlideModel.model_json_schema(),
),
SlideLayoutModel(
id="image-slide",
name="Image Slide",
json_schema=ImageSlideModel.model_json_schema(),
),
SlideLayoutModel(
id="statistics-slide",
name="Statistics Slide",
json_schema=StatisticsSlideModel.model_json_schema(),
),
SlideLayoutModel(
id="quote-slide",
name="Quote Slide",
json_schema=QuoteSlideModel.model_json_schema(),
),
SlideLayoutModel(
id="timeline-slide",
name="Timeline Slide",
json_schema=TimelineSlideModel.model_json_schema(),
),
SlideLayoutModel(
id="team-slide",
name="Team Slide",
json_schema=TeamSlideModel.model_json_schema(),
),
SlideLayoutModel(
id="process-slide",
name="Process Slide",
json_schema=ProcessSlideModel.model_json_schema(),
),
SlideLayoutModel(
id="two-column-slide",
name="Two Column Slide",
json_schema=TwoColumnSlideModel.model_json_schema(),
),
SlideLayoutModel(
id="conclusion-slide",
name="Conclusion Slide",
json_schema=ConclusionSlideModel.model_json_schema(),
),
SlideLayoutModel(
id="content-slide",
name="Content Slide",
json_schema=ContentSlideModel.model_json_schema(),
),
],
)

View file

@ -14,7 +14,7 @@ if __name__ == "__main__":
args = parser.parse_args()
uvicorn.run(
"api.main:APP",
"api.main:app",
host="0.0.0.0",
port=args.port,
log_level="info",

View file

@ -1,3 +1,4 @@
import asyncio
import json
import os
from fastembed_vectorstore import FastembedVectorstore, FastembedEmbeddingModel
@ -27,6 +28,8 @@ class IconFinderService:
return vector_store
def search_icons(self, query: str, k: int = 1):
result = self.vector_store.search(query, k)
return [ f"/static/icons/bold/{result[0].split("||")[0]}.png" for result in result]
async def search_icons(self, query: str, k: int = 1):
result = await asyncio.to_thread(self.vector_store.search, query, k)
return [
f"/static/icons/bold/{result[0].split('||')[0]}.png" for result in result
]

View file

@ -20,7 +20,10 @@ class ImageGenerationService:
self.output_directory = output_directory
os.makedirs(output_directory, exist_ok=True)
self.use_pexels = get_pexels_api_key_env() is not None
self.use_pexels = False
if get_pexels_api_key_env():
self.use_pexels = True
self.image_gen_func = self.get_image_gen_func()
def get_image_gen_func(self):
@ -50,7 +53,7 @@ class ImageGenerationService:
print(f"Error generating image: {e}")
return "/static/images/placeholder.jpg"
async def generate_image_openai(prompt: str, output_directory: str) -> str:
async def generate_image_openai(self, prompt: str, output_directory: str) -> str:
client = get_llm_client()
result = await client.images.generate(
model="dall-e-3",
@ -62,7 +65,7 @@ class ImageGenerationService:
image_url = result.data[0].url
return await download_file(image_url, output_directory)
async def generate_image_google(prompt: str, output_directory: str) -> str:
async def generate_image_google(self, prompt: str, output_directory: str) -> str:
client = genai.Client()
response = await asyncio.to_thread(
client.models.generate_content,
@ -81,11 +84,11 @@ class ImageGenerationService:
return image_path
async def get_image_from_pexels(prompt: str, output_directory: str) -> str:
async def get_image_from_pexels(self, prompt: str, output_directory: str) -> str:
async with aiohttp.ClientSession() as session:
response = await session.get(
f"https://api.pexels.com/v1/search?query={prompt}&per_page=1",
headers={"Authorization": f'{os.getenv("PEXELS_API_KEY")}'},
headers={"Authorization": f"{get_pexels_api_key_env()}"},
)
data = await response.json()
image_url = data["photos"][0]["src"]["large"]

View file

@ -6,6 +6,11 @@ export const layoutId = 'bullet-point-slide'
export const layoutName = 'Bullet Point Slide'
export const layoutDescription = 'A slide with a title, subtitle, and a list of bullet points.'
const imageSchema = z.object({
url: z.string().url().describe('URL to image'),
prompt: z.string().describe('Prompt used to generate the image'),
})
const bulletPointSlideSchema = z.object({
title: z.string().min(3).max(100).default('Key Points').describe('Title of the slide'),
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),
@ -16,7 +21,7 @@ const bulletPointSlideSchema = z.object({
'Third point demonstrating clear benefits',
'Fourth item showcasing key features'
]).describe('List of bullet points (2-8 items)'),
backgroundImage: z.string().optional().describe('URL to background image for the slide')
backgroundImage: imageSchema.optional().describe('Background image for the slide'),
})