sushilideaclan01's picture
add new things in the extensive flow
026f283
"""
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
# ----- Generate -----
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
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]
# ----- Matrix -----
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]
# ----- Auth -----
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"
# ----- Correction -----
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")
# ----- Extensive -----
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 = "" # Hyper-specific audience for this essential (AI-decided)
class InventOnlyResponse(BaseModel):
"""Response from invent-only endpoint."""
essentials: List[InventedEssentialSchema]
export_text: Optional[str] = None
# ----- Creative (upload / analyze / modify) -----
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
# ----- Database -----
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)")
# ----- Export -----
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