| | """ |
| | Request/response schemas for PsyAdGenesis API. |
| | Keeps all Pydantic models in one place for consistency and reuse. |
| | """ |
| |
|
| | from pydantic import BaseModel, Field |
| | from typing import Optional, List, Literal, Any, Dict |
| |
|
| |
|
| | |
| | class GenerateRequest(BaseModel): |
| | """Request schema for ad generation.""" |
| | niche: Literal["home_insurance", "glp1", "auto_insurance"] = Field( |
| | description="Target niche: home_insurance, glp1, or auto_insurance" |
| | ) |
| | num_images: int = Field(default=1, ge=1, le=10, description="Number of images to generate (1-10)") |
| | image_model: Optional[str] = Field(default=None, description="Image generation model to use") |
| | target_audience: Optional[str] = Field(default=None, description="Optional target audience description") |
| | offer: Optional[str] = Field(default=None, description="Optional offer to run") |
| | use_trending: bool = Field(default=False, description="Whether to incorporate current trending topics") |
| | trending_context: Optional[str] = Field(default=None, description="Specific trending context when use_trending=True") |
| |
|
| |
|
| | class GenerateBatchRequest(BaseModel): |
| | """Request schema for batch ad generation.""" |
| | niche: Literal["home_insurance", "glp1", "auto_insurance"] = Field(description="Target niche") |
| | count: int = Field(default=5, ge=1, le=100, description="Number of ads to generate (1-100)") |
| | images_per_ad: int = Field(default=1, ge=1, le=3, description="Images per ad (1-3)") |
| | image_model: Optional[str] = Field(default=None, description="Image generation model to use") |
| | method: Optional[Literal["standard", "matrix"]] = Field(default=None, description="Generation method or None for mixed") |
| | target_audience: Optional[str] = Field(default=None, description="Optional target audience") |
| | offer: Optional[str] = Field(default=None, description="Optional offer to run") |
| |
|
| |
|
| | class ImageResult(BaseModel): |
| | """Image result schema.""" |
| | filename: Optional[str] = None |
| | filepath: Optional[str] = None |
| | image_url: Optional[str] = None |
| | r2_url: Optional[str] = None |
| | model_used: Optional[str] = None |
| | seed: Optional[int] = None |
| | error: Optional[str] = None |
| |
|
| |
|
| | class AdMetadata(BaseModel): |
| | """Metadata about the generation.""" |
| | strategies_used: List[str] |
| | creative_direction: str |
| | visual_mood: str |
| | framework: Optional[str] = None |
| | camera_angle: Optional[str] = None |
| | lighting: Optional[str] = None |
| | composition: Optional[str] = None |
| | hooks_inspiration: List[str] |
| | visual_styles: List[str] |
| |
|
| |
|
| | class GenerateResponse(BaseModel): |
| | """Response schema for ad generation.""" |
| | id: str |
| | niche: str |
| | created_at: str |
| | title: Optional[str] = Field(default=None, description="Short punchy ad title (3-5 words)") |
| | headline: str |
| | primary_text: str |
| | description: str |
| | body_story: str = Field(description="Compelling 8-12 sentence story that hooks emotionally") |
| | cta: str |
| | psychological_angle: str |
| | why_it_works: Optional[str] = None |
| | images: List[ImageResult] |
| | metadata: AdMetadata |
| |
|
| |
|
| | class BatchResponse(BaseModel): |
| | """Response schema for batch generation.""" |
| | count: int |
| | ads: List[GenerateResponse] |
| |
|
| |
|
| | |
| | class MatrixGenerateRequest(BaseModel): |
| | """Request for angle × concept matrix generation.""" |
| | niche: Literal["home_insurance", "glp1", "auto_insurance"] = Field(description="Target niche") |
| | angle_key: Optional[str] = Field(default=None, description="Specific angle key (random if not provided)") |
| | concept_key: Optional[str] = Field(default=None, description="Specific concept key (random if not provided)") |
| | custom_angle: Optional[str] = Field(default=None, description="Custom angle text when angle_key is 'custom'") |
| | custom_concept: Optional[str] = Field(default=None, description="Custom concept text when concept_key is 'custom'") |
| | num_images: int = Field(default=1, ge=1, le=5, description="Number of images to generate") |
| | image_model: Optional[str] = Field(default=None, description="Image generation model to use") |
| | target_audience: Optional[str] = Field(default=None, description="Optional target audience") |
| | offer: Optional[str] = Field(default=None, description="Optional offer to run") |
| | core_motivator: Optional[str] = Field(default=None, description="Optional motivator to guide generation") |
| |
|
| |
|
| | class RefineCustomRequest(BaseModel): |
| | """Request to refine custom angle or concept text using AI.""" |
| | text: str = Field(description="The raw custom text from user") |
| | type: Literal["angle", "concept"] = Field(description="Whether this is an angle or concept") |
| | niche: Literal["home_insurance", "glp1", "auto_insurance"] = Field(description="Target niche for context") |
| | goal: Optional[str] = Field(default=None, description="Optional user goal or context") |
| |
|
| |
|
| | class RefinedAngleResponse(BaseModel): |
| | """Response for refined angle.""" |
| | key: str = Field(default="custom") |
| | name: str |
| | trigger: str |
| | example: str |
| | category: str = Field(default="Custom") |
| | original_text: str |
| |
|
| |
|
| | class RefinedConceptResponse(BaseModel): |
| | """Response for refined concept.""" |
| | key: str = Field(default="custom") |
| | name: str |
| | structure: str |
| | visual: str |
| | category: str = Field(default="Custom") |
| | original_text: str |
| |
|
| |
|
| | class RefineCustomResponse(BaseModel): |
| | """Response for refined custom angle or concept.""" |
| | status: str |
| | type: Literal["angle", "concept"] |
| | refined: Optional[dict] = None |
| | error: Optional[str] = None |
| |
|
| |
|
| | class MotivatorGenerateRequest(BaseModel): |
| | """Request to generate motivators from niche + angle + concept.""" |
| | niche: Literal["home_insurance", "glp1", "auto_insurance"] = Field(description="Target niche") |
| | angle: Dict[str, Any] = Field(description="Angle context: name, trigger, example") |
| | concept: Dict[str, Any] = Field(description="Concept context: name, structure, visual") |
| | target_audience: Optional[str] = Field(default=None, description="Optional target audience") |
| | offer: Optional[str] = Field(default=None, description="Optional offer") |
| | count: int = Field(default=6, ge=3, le=10, description="Number of motivators to generate") |
| |
|
| |
|
| | class MotivatorGenerateResponse(BaseModel): |
| | """Response with generated motivators.""" |
| | motivators: List[str] |
| |
|
| |
|
| | class MatrixBatchRequest(BaseModel): |
| | """Request for batch matrix generation.""" |
| | niche: Literal["home_insurance", "glp1"] = Field(description="Target niche") |
| | angle_count: int = Field(default=6, ge=1, le=10, description="Number of angles to test") |
| | concept_count: int = Field(default=5, ge=1, le=10, description="Number of concepts per angle") |
| | strategy: Literal["balanced", "top_performers", "diverse"] = Field(default="balanced", description="Selection strategy") |
| |
|
| |
|
| | class AngleInfo(BaseModel): |
| | """Angle information.""" |
| | key: str |
| | name: str |
| | trigger: str |
| | category: str |
| |
|
| |
|
| | class ConceptInfo(BaseModel): |
| | """Concept information.""" |
| | key: str |
| | name: str |
| | structure: str |
| | visual: str |
| | category: str |
| |
|
| |
|
| | class MatrixMetadata(BaseModel): |
| | """Matrix generation metadata.""" |
| | generation_method: str = "angle_concept_matrix" |
| |
|
| |
|
| | class MatrixResult(BaseModel): |
| | """Result from matrix-based generation.""" |
| | angle: AngleInfo |
| | concept: ConceptInfo |
| |
|
| |
|
| | class MatrixGenerateResponse(BaseModel): |
| | """Response for matrix-based ad generation.""" |
| | id: str |
| | niche: str |
| | created_at: str |
| | title: Optional[str] = Field(default=None, description="Short punchy ad title") |
| | headline: str |
| | primary_text: str |
| | description: str |
| | body_story: str = Field(description="Compelling 8-12 sentence story that hooks emotionally") |
| | cta: str |
| | psychological_angle: str |
| | why_it_works: Optional[str] = None |
| | images: List[ImageResult] |
| | matrix: MatrixResult |
| | metadata: MatrixMetadata |
| |
|
| |
|
| | class CombinationInfo(BaseModel): |
| | """Info about a single angle × concept combination.""" |
| | combination_id: str |
| | angle: AngleInfo |
| | concept: ConceptInfo |
| | compatibility_score: float |
| | prompt_guidance: str |
| |
|
| |
|
| | class MatrixSummary(BaseModel): |
| | """Summary of a testing matrix.""" |
| | total_combinations: int |
| | unique_angles: int |
| | unique_concepts: int |
| | average_compatibility: float |
| | angles_used: List[str] |
| | concepts_used: List[str] |
| |
|
| |
|
| | class TestingMatrixResponse(BaseModel): |
| | """Response for testing matrix generation.""" |
| | niche: str |
| | strategy: str |
| | summary: MatrixSummary |
| | combinations: List[CombinationInfo] |
| |
|
| |
|
| | |
| | class LoginRequest(BaseModel): |
| | """Login request.""" |
| | username: str = Field(description="Username") |
| | password: str = Field(description="Password") |
| |
|
| |
|
| | class LoginResponse(BaseModel): |
| | """Login response.""" |
| | token: str |
| | username: str |
| | message: str = "Login successful" |
| |
|
| |
|
| | |
| | class ImageCorrectRequest(BaseModel): |
| | """Request schema for image correction.""" |
| | image_id: str = Field(description="ID of existing ad creative or 'temp-id' for images not in DB") |
| | image_url: Optional[str] = Field(default=None, description="Optional image URL when image_id='temp-id'") |
| | user_instructions: Optional[str] = Field(default=None, description="User instructions for correction") |
| | auto_analyze: bool = Field(default=False, description="Auto-analyze image for issues if no instructions") |
| |
|
| |
|
| | class SpellingCorrection(BaseModel): |
| | """Spelling correction entry.""" |
| | detected: str |
| | corrected: str |
| | context: Optional[str] = None |
| |
|
| |
|
| | class VisualCorrection(BaseModel): |
| | """Visual correction entry.""" |
| | issue: str |
| | suggestion: str |
| | priority: Optional[str] = None |
| |
|
| |
|
| | class CorrectionData(BaseModel): |
| | """Correction data structure.""" |
| | spelling_corrections: List[SpellingCorrection] |
| | visual_corrections: List[VisualCorrection] |
| | corrected_prompt: str |
| |
|
| |
|
| | class CorrectedImageResult(BaseModel): |
| | """Corrected image result.""" |
| | filename: Optional[str] = None |
| | filepath: Optional[str] = None |
| | image_url: Optional[str] = None |
| | r2_url: Optional[str] = None |
| | model_used: Optional[str] = None |
| | corrected_prompt: Optional[str] = None |
| |
|
| |
|
| | class ImageCorrectResponse(BaseModel): |
| | """Response schema for image correction.""" |
| | status: str |
| | analysis: Optional[str] = None |
| | corrections: Optional[CorrectionData] = None |
| | corrected_image: Optional[CorrectedImageResult] = None |
| | error: Optional[str] = None |
| |
|
| |
|
| | class ImageRegenerateRequest(BaseModel): |
| | """Request schema for image regeneration.""" |
| | image_id: str = Field(description="ID of existing ad creative in database") |
| | image_model: Optional[str] = Field(default=None, description="Image model to use (or original if not provided)") |
| | preview_only: bool = Field(default=True, description="If True, preview only; user confirms selection later") |
| |
|
| |
|
| | class RegeneratedImageResult(BaseModel): |
| | """Regenerated image result.""" |
| | filename: Optional[str] = None |
| | filepath: Optional[str] = None |
| | image_url: Optional[str] = None |
| | r2_url: Optional[str] = None |
| | model_used: Optional[str] = None |
| | prompt_used: Optional[str] = None |
| | seed_used: Optional[int] = None |
| |
|
| |
|
| | class ImageRegenerateResponse(BaseModel): |
| | """Response schema for image regeneration.""" |
| | status: str |
| | regenerated_image: Optional[RegeneratedImageResult] = None |
| | original_image_url: Optional[str] = None |
| | original_preserved: bool = Field(default=True, description="Whether original image info was preserved") |
| | is_preview: bool = Field(default=False, description="Whether this is a preview (not yet saved)") |
| | error: Optional[str] = None |
| |
|
| |
|
| | class ImageSelectionRequest(BaseModel): |
| | """Request schema for confirming image selection after regeneration.""" |
| | image_id: str = Field(description="ID of existing ad creative in database") |
| | selection: str = Field(description="Which image to keep: 'new' or 'original'") |
| | new_image_url: Optional[str] = Field(default=None, description="URL of new image (required if selection='new')") |
| | new_r2_url: Optional[str] = Field(default=None, description="R2 URL of the new image") |
| | new_filename: Optional[str] = Field(default=None, description="Filename of the new image") |
| | new_model: Optional[str] = Field(default=None, description="Model used for the new image") |
| | new_seed: Optional[int] = Field(default=None, description="Seed used for the new image") |
| |
|
| |
|
| | |
| | class ExtensiveGenerateRequest(BaseModel): |
| | """Request for extensive generation.""" |
| | niche: str = Field(description="Target niche or 'others' with custom_niche") |
| | custom_niche: Optional[str] = Field(default=None, description="Custom niche when 'others' is selected") |
| | target_audience: Optional[str] = Field(default=None, description="Optional target audience") |
| | offer: Optional[str] = Field(default=None, description="Optional offer to run") |
| | num_images: int = Field(default=1, ge=1, le=3, description="Number of images per strategy (1-3)") |
| | image_model: Optional[str] = Field(default=None, description="Image generation model to use") |
| | num_strategies: int = Field(default=5, ge=1, le=10, description="Number of creative strategies (1-10)") |
| | use_creative_inventor: bool = Field( |
| | default=True, |
| | description="If True, invent new angles/concepts/visuals/triggers; if False, use researcher", |
| | ) |
| | trend_context: Optional[str] = Field(default=None, description="Optional trend or occasion (e.g. New Year, Valentine's)") |
| |
|
| |
|
| | class ExtensiveJobResponse(BaseModel): |
| | """Response when extensive generation is started (202 Accepted).""" |
| | job_id: str |
| | message: str = "Extensive generation started. Poll /extensive/status/{job_id} for progress." |
| |
|
| |
|
| | class InventOnlyRequest(BaseModel): |
| | """Request for invent-only (no ad generation).""" |
| | niche: str = Field(description="Niche (e.g. GLP-1, Home Insurance)") |
| | target_audience: Optional[str] = Field(default=None, description="Target audience") |
| | offer: Optional[str] = Field(default=None, description="Offer to run") |
| | n: int = Field(default=5, ge=1, le=15, description="Number of invented essentials") |
| | trend_context: Optional[str] = Field(default=None, description="Optional trend or occasion") |
| | export_as_text: bool = Field(default=False, description="If True, return human-readable text; else JSON") |
| |
|
| |
|
| | class InventedEssentialSchema(BaseModel): |
| | """One invented creative essential (for API response).""" |
| | psychology_trigger: str |
| | angles: List[str] |
| | concepts: List[str] |
| | visual_directions: List[str] |
| | hooks: List[str] = [] |
| | visual_styles: List[str] = [] |
| | target_audience: str = "" |
| |
|
| |
|
| | class InventOnlyResponse(BaseModel): |
| | """Response from invent-only endpoint.""" |
| | essentials: List[InventedEssentialSchema] |
| | export_text: Optional[str] = None |
| |
|
| |
|
| | |
| | class CreativeAnalysisData(BaseModel): |
| | """Structured analysis of a creative.""" |
| | visual_style: str |
| | color_palette: List[str] |
| | mood: str |
| | composition: str |
| | subject_matter: str |
| | text_content: Optional[str] = None |
| | current_angle: Optional[str] = None |
| | current_concept: Optional[str] = None |
| | target_audience: Optional[str] = None |
| | strengths: List[str] |
| | areas_for_improvement: List[str] |
| |
|
| |
|
| | class CreativeAnalyzeRequest(BaseModel): |
| | """Request for creative analysis.""" |
| | image_url: Optional[str] = Field(default=None, description="URL of the image to analyze (alternative to file upload)") |
| |
|
| |
|
| | class CreativeAnalysisResponse(BaseModel): |
| | """Response for creative analysis.""" |
| | status: str |
| | analysis: Optional[CreativeAnalysisData] = None |
| | suggested_angles: Optional[List[str]] = None |
| | suggested_concepts: Optional[List[str]] = None |
| | error: Optional[str] = None |
| |
|
| |
|
| | class CreativeModifyRequest(BaseModel): |
| | """Request for creative modification.""" |
| | image_url: str = Field(description="URL of the original image") |
| | analysis: Optional[Dict[str, Any]] = Field(default=None, description="Previous analysis data (optional)") |
| | angle: Optional[str] = Field(default=None, description="Angle to apply to the creative") |
| | concept: Optional[str] = Field(default=None, description="Concept to apply to the creative") |
| | mode: Literal["modify", "inspired"] = Field(default="modify", description="modify = image-to-image, inspired = new generation") |
| | image_model: Optional[str] = Field(default=None, description="Image generation model to use") |
| | user_prompt: Optional[str] = Field(default=None, description="Optional custom user prompt for modification") |
| |
|
| |
|
| | class ModifiedImageResult(BaseModel): |
| | """Result of creative modification.""" |
| | filename: Optional[str] = None |
| | filepath: Optional[str] = None |
| | image_url: Optional[str] = None |
| | r2_url: Optional[str] = None |
| | model_used: Optional[str] = None |
| | mode: Optional[str] = None |
| | applied_angle: Optional[str] = None |
| | applied_concept: Optional[str] = None |
| |
|
| |
|
| | class CreativeModifyResponse(BaseModel): |
| | """Response for creative modification.""" |
| | status: str |
| | prompt: Optional[str] = None |
| | image: Optional[ModifiedImageResult] = None |
| | error: Optional[str] = None |
| |
|
| |
|
| | class FileUploadResponse(BaseModel): |
| | """Response for file upload.""" |
| | status: str |
| | image_url: Optional[str] = None |
| | filename: Optional[str] = None |
| | error: Optional[str] = None |
| |
|
| |
|
| | |
| | class AdCreativeDB(BaseModel): |
| | """Ad creative from database.""" |
| | id: str |
| | niche: str |
| | title: Optional[str] = None |
| | headline: str |
| | primary_text: Optional[str] = None |
| | description: Optional[str] = None |
| | body_story: Optional[str] = None |
| | cta: Optional[str] = None |
| | psychological_angle: Optional[str] = None |
| | why_it_works: Optional[str] = None |
| | image_url: Optional[str] = None |
| | image_filename: Optional[str] = None |
| | image_model: Optional[str] = None |
| | image_seed: Optional[int] = None |
| | angle_key: Optional[str] = None |
| | angle_name: Optional[str] = None |
| | concept_key: Optional[str] = None |
| | concept_name: Optional[str] = None |
| | generation_method: Optional[str] = None |
| | created_at: Optional[str] = None |
| |
|
| |
|
| | class DbStatsResponse(BaseModel): |
| | """Database statistics response.""" |
| | connected: bool |
| | total_ads: Optional[int] = None |
| | by_niche: Optional[Dict[str, int]] = None |
| | by_method: Optional[Dict[str, int]] = None |
| | error: Optional[str] = None |
| |
|
| |
|
| | class EditAdCopyRequest(BaseModel): |
| | """Request for editing ad copy.""" |
| | ad_id: str = Field(description="ID of the ad to edit") |
| | field: Literal["title", "headline", "primary_text", "description", "body_story", "cta"] = Field(description="Field to edit") |
| | value: str = Field(description="New value (manual) or current value (AI edit)") |
| | mode: Literal["manual", "ai"] = Field(description="Edit mode: manual or ai") |
| | user_suggestion: Optional[str] = Field(default=None, description="User suggestion for AI editing (optional)") |
| |
|
| |
|
| | |
| | class BulkExportRequest(BaseModel): |
| | """Request schema for bulk export.""" |
| | ad_ids: List[str] = Field(description="List of ad IDs to export", min_length=1, max_length=50) |
| |
|
| |
|
| | class BulkExportResponse(BaseModel): |
| | """Response schema for bulk export (actual response is FileResponse with ZIP).""" |
| | status: str |
| | message: str |
| | filename: str |
| |
|