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