From ffa22b166324ce06557b72e4172b4f935be1590f Mon Sep 17 00:00:00 2001 From: sauravniraula Date: Wed, 16 Jul 2025 04:36:19 +0545 Subject: [PATCH] feat(fastapi): adds image gen and icon search endpoints, chore(fastapi): seperates middlewares and lifespan from main.py --- servers/fastapi/api/lifespan.py | 16 + servers/fastapi/api/main.py | 43 +- servers/fastapi/api/middlewares.py | 13 + servers/fastapi/api/v1/ppt/endpoints/icons.py | 11 + .../fastapi/api/v1/ppt/endpoints/images.py | 18 +- servers/fastapi/api/v1/ppt/router.py | 2 + servers/fastapi/get_test_schema.py | 559 +++++++++++++++++- servers/fastapi/server.py | 2 +- .../fastapi/services/icon_finder_service.py | 9 +- .../services/image_generation_service.py | 13 +- ..._availability.py => model_availability.py} | 0 .../layouts/BulletPointSlideLayout.tsx | 7 +- 12 files changed, 631 insertions(+), 62 deletions(-) create mode 100644 servers/fastapi/api/lifespan.py create mode 100644 servers/fastapi/api/middlewares.py create mode 100644 servers/fastapi/api/v1/ppt/endpoints/icons.py rename servers/fastapi/utils/{check_llm_model_availability.py => model_availability.py} (100%) diff --git a/servers/fastapi/api/lifespan.py b/servers/fastapi/api/lifespan.py new file mode 100644 index 00000000..0d92a2d0 --- /dev/null +++ b/servers/fastapi/api/lifespan.py @@ -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 diff --git a/servers/fastapi/api/main.py b/servers/fastapi/api/main.py index 40589d17..b56a50b3 100644 --- a/servers/fastapi/api/main.py +++ b/servers/fastapi/api/main.py @@ -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) diff --git a/servers/fastapi/api/middlewares.py b/servers/fastapi/api/middlewares.py new file mode 100644 index 00000000..6c4d83d9 --- /dev/null +++ b/servers/fastapi/api/middlewares.py @@ -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) diff --git a/servers/fastapi/api/v1/ppt/endpoints/icons.py b/servers/fastapi/api/v1/ppt/endpoints/icons.py new file mode 100644 index 00000000..36116f53 --- /dev/null +++ b/servers/fastapi/api/v1/ppt/endpoints/icons.py @@ -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) diff --git a/servers/fastapi/api/v1/ppt/endpoints/images.py b/servers/fastapi/api/v1/ppt/endpoints/images.py index bb8dce84..7e8997ec 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/images.py +++ b/servers/fastapi/api/v1/ppt/endpoints/images.py @@ -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) diff --git a/servers/fastapi/api/v1/ppt/router.py b/servers/fastapi/api/v1/ppt/router.py index 707fb3b1..ddff5676 100644 --- a/servers/fastapi/api/v1/ppt/router.py +++ b/servers/fastapi/api/v1/ppt/router.py @@ -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) diff --git a/servers/fastapi/get_test_schema.py b/servers/fastapi/get_test_schema.py index 9d5e642a..82c825ca 100644 --- a/servers/fastapi/get_test_schema.py +++ b/servers/fastapi/get_test_schema.py @@ -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(), ), ], ) diff --git a/servers/fastapi/server.py b/servers/fastapi/server.py index 70c8078c..ac2bfaa8 100644 --- a/servers/fastapi/server.py +++ b/servers/fastapi/server.py @@ -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", diff --git a/servers/fastapi/services/icon_finder_service.py b/servers/fastapi/services/icon_finder_service.py index 875fb166..f90dde7f 100644 --- a/servers/fastapi/services/icon_finder_service.py +++ b/servers/fastapi/services/icon_finder_service.py @@ -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 + ] diff --git a/servers/fastapi/services/image_generation_service.py b/servers/fastapi/services/image_generation_service.py index f45ad241..94f06deb 100644 --- a/servers/fastapi/services/image_generation_service.py +++ b/servers/fastapi/services/image_generation_service.py @@ -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"] diff --git a/servers/fastapi/utils/check_llm_model_availability.py b/servers/fastapi/utils/model_availability.py similarity index 100% rename from servers/fastapi/utils/check_llm_model_availability.py rename to servers/fastapi/utils/model_availability.py diff --git a/servers/nextjs/components/layouts/BulletPointSlideLayout.tsx b/servers/nextjs/components/layouts/BulletPointSlideLayout.tsx index 5aac05e6..584ec1dd 100644 --- a/servers/nextjs/components/layouts/BulletPointSlideLayout.tsx +++ b/servers/nextjs/components/layouts/BulletPointSlideLayout.tsx @@ -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'), })