Release Candidate: Production Release with Multi-Tenant & Onboarding Enhancements
This commit is contained in:
65
backend/models/podcast_bible_models.py
Normal file
65
backend/models/podcast_bible_models.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Podcast Bible Models
|
||||
|
||||
Pydantic models for the structured Podcast Bible, used for hyper-personalization.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
class HostPersona(BaseModel):
|
||||
"""Details about the podcast host persona."""
|
||||
name: str = Field(..., description="Name of the podcast host")
|
||||
background: str = Field(..., description="Professional background and expertise")
|
||||
expertise_level: str = Field(..., description="Level of expertise (e.g., Expert, Practitioner, Enthusiast)")
|
||||
personality_traits: List[str] = Field(default_factory=list, description="Personality traits (e.g., Witty, Authoritative, Empathetic)")
|
||||
vocal_style: str = Field(..., description="Description of the vocal style and delivery")
|
||||
vocal_characteristics: List[str] = Field(default_factory=list, description="Specific vocal traits (e.g., Deep, Raspy, Energetic, Calm)")
|
||||
look: Optional[str] = Field(None, description="Visual description of the host (for avatar generation)")
|
||||
catchphrases: List[str] = Field(default_factory=list, description="Commonly used phrases or sign-offs")
|
||||
|
||||
class VisualStyle(BaseModel):
|
||||
"""Visual aesthetic for the podcast videos and avatars."""
|
||||
style_preset: str = Field(default="Professional Studio", description="Visual style (e.g., 3D Cartoon, Cinematic, Minimalist)")
|
||||
environment: str = Field(..., description="The studio or setting where the podcast takes place")
|
||||
lighting: str = Field(default="Soft Studio Lighting", description="Lighting mood and setup")
|
||||
color_palette: List[str] = Field(default_factory=list, description="Primary brand colors for the visual elements")
|
||||
camera_style: str = Field(default="Static Mid-shot", description="Preferred camera framing and movement")
|
||||
|
||||
class AudioEnvironment(BaseModel):
|
||||
"""The soundscape and audio characteristics of the podcast."""
|
||||
soundscape: str = Field(default="Quiet Studio", description="Acoustics and ambient noise level")
|
||||
music_mood: str = Field(default="Professional & Subtle", description="Genre and mood of background music")
|
||||
sfx_style: str = Field(default="Minimalist", description="Style of sound effects used (e.g., tech-inspired, natural)")
|
||||
|
||||
class ShowRules(BaseModel):
|
||||
"""Consistency rules for the podcast narrative and structure."""
|
||||
intro_format: str = Field(..., description="Standard way to start the episode")
|
||||
outro_format: str = Field(..., description="Standard way to end the episode")
|
||||
interaction_tone: str = Field(default="Conversational", description="Tone between hosts or with audience")
|
||||
constraints: List[str] = Field(default_factory=list, description="Specific things to always do or avoid")
|
||||
|
||||
class AudienceDNA(BaseModel):
|
||||
"""Details about the target audience."""
|
||||
expertise_level: str = Field(..., description="Target audience expertise level (Beginner, Intermediate, Expert)")
|
||||
interests: List[str] = Field(default_factory=list, description="Primary interests of the audience")
|
||||
pain_points: List[str] = Field(default_factory=list, description="Common challenges or problems the audience faces")
|
||||
demographics: Optional[str] = Field(None, description="General demographic information")
|
||||
|
||||
class BrandDNA(BaseModel):
|
||||
"""Details about the brand and industry context."""
|
||||
industry: str = Field(..., description="Primary industry or niche")
|
||||
tone: str = Field(..., description="Overall brand tone (e.g., Professional, Casual, Inspirational)")
|
||||
communication_style: str = Field(..., description="Preferred communication style (e.g., Socratic, Storytelling, Analytical)")
|
||||
key_messages: List[str] = Field(default_factory=list, description="Core messages the brand wants to convey")
|
||||
competitor_context: Optional[str] = Field(None, description="Context on how to differentiate from competitors")
|
||||
|
||||
class PodcastBible(BaseModel):
|
||||
"""The complete structured Podcast Bible SSOT."""
|
||||
project_id: Optional[str] = Field(default=None, description="Associated project ID")
|
||||
host: HostPersona = Field(..., description="Host persona details")
|
||||
audience: AudienceDNA = Field(..., description="Target audience details")
|
||||
brand: BrandDNA = Field(..., description="Brand and industry context")
|
||||
visual_style: VisualStyle = Field(..., description="Visual aesthetic and environment")
|
||||
audio_environment: AudioEnvironment = Field(..., description="Soundscape and music details")
|
||||
show_rules: ShowRules = Field(..., description="Consistency and structural rules")
|
||||
@@ -33,6 +33,7 @@ class PodcastProject(Base):
|
||||
|
||||
# Project state (stored as JSON)
|
||||
# This mirrors the PodcastProjectState interface from frontend
|
||||
bible = Column(JSON, nullable=True) # PodcastBible structured data
|
||||
analysis = Column(JSON, nullable=True) # PodcastAnalysis
|
||||
queries = Column(JSON, nullable=True) # List[Query]
|
||||
selected_queries = Column(JSON, nullable=True) # Array of query IDs
|
||||
@@ -56,6 +57,11 @@ class PodcastProject(Base):
|
||||
# Final combined video URL (persisted for reloads)
|
||||
final_video_url = Column(String(1000), nullable=True) # URL to final combined podcast video
|
||||
|
||||
# Avatar details
|
||||
avatar_url = Column(String(1000), nullable=True)
|
||||
avatar_prompt = Column(Text, nullable=True)
|
||||
avatar_persona_id = Column(String(255), nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False, index=True)
|
||||
|
||||
@@ -40,11 +40,53 @@ class StoryGenerationRequest(BaseModel):
|
||||
audio_lang: str = Field(default="en", description="Language code for TTS")
|
||||
audio_slow: bool = Field(default=False, description="Whether to speak slowly (gTTS only)")
|
||||
audio_rate: int = Field(default=150, description="Speech rate (pyttsx3 only)")
|
||||
anime_bible: Optional[Dict[str, Any]] = Field(
|
||||
default=None,
|
||||
description="Optional structured anime story bible for anime fiction templates",
|
||||
)
|
||||
|
||||
|
||||
class StorySetupGenerationRequest(BaseModel):
|
||||
"""Request model for AI story setup generation."""
|
||||
story_idea: str = Field(..., description="Basic story idea or information from the user")
|
||||
story_mode: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Story mode (marketing or pure) if provided by the UI",
|
||||
)
|
||||
story_template: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Optional story template identifier (e.g. product_story, brand_manifesto)",
|
||||
)
|
||||
brand_context: Optional[Dict[str, Any]] = Field(
|
||||
default=None,
|
||||
description="Optional high-signal brand context derived from onboarding",
|
||||
)
|
||||
|
||||
|
||||
class StoryIdeaEnhanceRequest(BaseModel):
|
||||
"""Request model for AI story idea enhancement."""
|
||||
story_idea: str = Field(..., description="Original story idea or concept text from the user")
|
||||
story_mode: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Story mode (marketing or pure) if provided by the UI",
|
||||
)
|
||||
story_template: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Optional story template identifier (e.g. product_story, brand_manifesto)",
|
||||
)
|
||||
brand_context: Optional[Dict[str, Any]] = Field(
|
||||
default=None,
|
||||
description="Optional high-signal brand context derived from onboarding",
|
||||
)
|
||||
|
||||
fiction_variant: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Optional fiction-specific focus label (e.g. high-concept twist, shonen action)",
|
||||
)
|
||||
narrative_energy: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Optional narrative energy or pacing hint (e.g. grounded, balanced, cinematic)",
|
||||
)
|
||||
|
||||
|
||||
class StorySetupOption(BaseModel):
|
||||
@@ -78,6 +120,43 @@ class StorySetupOption(BaseModel):
|
||||
audio_lang: str = Field(default="en", description="Language code for TTS")
|
||||
audio_slow: bool = Field(default=False, description="Whether to speak slowly (gTTS only)")
|
||||
audio_rate: int = Field(default=150, description="Speech rate (pyttsx3 only)")
|
||||
anime_bible: Optional["AnimeStoryBible"] = Field(
|
||||
default=None,
|
||||
description="Optional structured anime story bible for anime fiction templates",
|
||||
)
|
||||
|
||||
|
||||
class AnimeCharacter(BaseModel):
|
||||
id: str = Field(..., description="Stable identifier for the character (snake_case)")
|
||||
name: str = Field(..., description="Character name")
|
||||
age_range: str = Field(..., description="Approximate age range (e.g., 'late teens', '30s')")
|
||||
role: str = Field(..., description="Narrative role (protagonist, antagonist, mentor, etc.)")
|
||||
look: str = Field(..., description="Key visual details (hair, build, notable traits)")
|
||||
outfit_palette: str = Field(..., description="Main outfit colors and style")
|
||||
personality_tags: List[str] = Field(default_factory=list, description="Short tags describing personality")
|
||||
|
||||
|
||||
class AnimeWorld(BaseModel):
|
||||
setting: str = Field(..., description="World description and primary locations")
|
||||
era: str = Field(..., description="Time period (near-future, far future, alt 1990s, etc.)")
|
||||
tech_or_magic_level: str = Field(..., description="Technology or magic sophistication level")
|
||||
core_rules: List[str] = Field(default_factory=list, description="Key world rules and constraints")
|
||||
|
||||
|
||||
class AnimeVisualStyle(BaseModel):
|
||||
style_preset: str = Field(..., description="High level style preset (anime_manga, cinematic_anime, cozy_slice_of_life)")
|
||||
camera_style: str = Field(..., description="Typical camera behaviour and framing")
|
||||
color_mood: str = Field(..., description="Dominant color palette and contrast")
|
||||
lighting: str = Field(..., description="Lighting style")
|
||||
line_style: str = Field(..., description="Line art style (thick, thin, rough, etc.)")
|
||||
extra_tags: List[str] = Field(default_factory=list, description="Additional style tags")
|
||||
|
||||
|
||||
class AnimeStoryBible(BaseModel):
|
||||
story_id: Optional[str] = Field(default=None, description="Optional story identifier")
|
||||
main_cast: List[AnimeCharacter] = Field(default_factory=list, description="Main cast of characters")
|
||||
world: AnimeWorld = Field(..., description="World and rules description")
|
||||
visual_style: AnimeVisualStyle = Field(..., description="Visual style anchors for images and video")
|
||||
|
||||
|
||||
class StorySetupGenerationResponse(BaseModel):
|
||||
@@ -86,8 +165,28 @@ class StorySetupGenerationResponse(BaseModel):
|
||||
success: bool = Field(default=True, description="Whether the generation was successful")
|
||||
|
||||
|
||||
class StoryIdeaEnhanceSuggestion(BaseModel):
|
||||
"""A single enhanced story idea suggestion."""
|
||||
idea: str = Field(..., description="AI-enhanced story idea text")
|
||||
whats_missing: str = Field(
|
||||
...,
|
||||
description="Concise explanation of missing or underspecified plot/context elements",
|
||||
)
|
||||
why_choose: str = Field(
|
||||
...,
|
||||
description="Why this idea is a strong direction based on the original input",
|
||||
)
|
||||
|
||||
|
||||
class StoryIdeaEnhanceResponse(BaseModel):
|
||||
"""Response model for story idea enhancement."""
|
||||
suggestions: List[StoryIdeaEnhanceSuggestion] = Field(
|
||||
..., description="List of enhanced story idea suggestions"
|
||||
)
|
||||
success: bool = Field(default=True, description="Whether the enhancement was successful")
|
||||
|
||||
|
||||
class StoryScene(BaseModel):
|
||||
"""Model for a story scene."""
|
||||
scene_number: int = Field(..., description="Scene number")
|
||||
title: str = Field(..., description="Scene title")
|
||||
description: str = Field(..., description="Scene description")
|
||||
@@ -97,6 +196,58 @@ class StoryScene(BaseModel):
|
||||
key_events: List[str] = Field(default_factory=list, description="Key events in the scene")
|
||||
|
||||
|
||||
class AnimeSceneTextRequest(BaseModel):
|
||||
scene: StoryScene = Field(..., description="Scene to refine using the anime bible")
|
||||
persona: str = Field(..., description="Persona context for the scene")
|
||||
story_setting: str = Field(..., description="Story setting")
|
||||
character_input: str = Field(..., description="Characters description from story setup")
|
||||
plot_elements: str = Field(..., description="Plot elements from story setup")
|
||||
writing_style: str = Field(..., description="Writing style")
|
||||
story_tone: str = Field(..., description="Story tone")
|
||||
narrative_pov: str = Field(..., description="Narrative point of view")
|
||||
audience_age_group: str = Field(..., description="Audience age group")
|
||||
content_rating: str = Field(..., description="Content rating")
|
||||
anime_bible: Optional[Dict[str, Any]] = Field(
|
||||
default=None,
|
||||
description="Optional anime story bible used to refine the scene",
|
||||
)
|
||||
|
||||
|
||||
class AnimeSceneTextResponse(BaseModel):
|
||||
scene: StoryScene = Field(..., description="Refined scene with bible-aware text and prompts")
|
||||
success: bool = Field(default=True, description="Whether the refinement was successful")
|
||||
|
||||
|
||||
class AnimeSceneGenerateRequest(BaseModel):
|
||||
premise: str = Field(..., description="Overall story premise for context")
|
||||
persona: str = Field(..., description="Persona context for the scene")
|
||||
story_setting: str = Field(..., description="Story setting")
|
||||
character_input: str = Field(..., description="Characters description from story setup")
|
||||
plot_elements: str = Field(..., description="Plot elements from story setup")
|
||||
writing_style: str = Field(..., description="Writing style")
|
||||
story_tone: str = Field(..., description="Story tone")
|
||||
narrative_pov: str = Field(..., description="Narrative point of view")
|
||||
audience_age_group: str = Field(..., description="Audience age group")
|
||||
content_rating: str = Field(..., description="Content rating")
|
||||
anime_bible: Dict[str, Any] = Field(
|
||||
...,
|
||||
description="Anime story bible used as a hard constraint for generation",
|
||||
)
|
||||
previous_scenes: Optional[List[StoryScene]] = Field(
|
||||
default=None,
|
||||
description="Optional list of previous scenes for continuity context",
|
||||
)
|
||||
target_scene_number: Optional[int] = Field(
|
||||
default=None,
|
||||
description="Optional target scene number for the new scene",
|
||||
)
|
||||
|
||||
|
||||
class AnimeSceneGenerateResponse(BaseModel):
|
||||
scene: StoryScene = Field(..., description="Newly generated anime scene based on the bible")
|
||||
success: bool = Field(default=True, description="Whether the scene generation was successful")
|
||||
|
||||
|
||||
class StoryStartRequest(StoryGenerationRequest):
|
||||
"""Request model for story start generation."""
|
||||
premise: str = Field(..., description="The story premise")
|
||||
@@ -116,6 +267,10 @@ class StoryOutlineResponse(BaseModel):
|
||||
success: bool = Field(default=True, description="Whether the generation was successful")
|
||||
task_id: Optional[str] = Field(None, description="Task ID for async operations")
|
||||
is_structured: bool = Field(default=False, description="Whether the outline is structured (scenes) or plain text")
|
||||
anime_bible: Optional[AnimeStoryBible] = Field(
|
||||
default=None,
|
||||
description="Optional structured anime story bible generated from final story setup",
|
||||
)
|
||||
|
||||
|
||||
class StoryContentResponse(BaseModel):
|
||||
@@ -156,6 +311,10 @@ class StoryContinueRequest(BaseModel):
|
||||
content_rating: str = Field(..., description="The content rating")
|
||||
ending_preference: str = Field(..., description="The preferred ending")
|
||||
story_length: str = Field(default="Medium", description="Target story length (Short: >1000 words, Medium: >5000 words, Long: >10000 words)")
|
||||
anime_bible: Optional[Dict[str, Any]] = Field(
|
||||
default=None,
|
||||
description="Optional structured anime story bible for anime fiction templates",
|
||||
)
|
||||
|
||||
|
||||
class StoryContinueResponse(BaseModel):
|
||||
|
||||
55
backend/models/story_project_models.py
Normal file
55
backend/models/story_project_models.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Story Project Models
|
||||
|
||||
Database models for Story Studio project persistence and state management.
|
||||
Modeled after PodcastProject and ResearchProject for cross-device resume.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, JSON, Index
|
||||
|
||||
from models.subscription_models import Base
|
||||
|
||||
|
||||
class StoryProject(Base):
|
||||
"""
|
||||
Database model for Story Studio project state.
|
||||
Stores complete story project state to enable cross-device resume.
|
||||
"""
|
||||
|
||||
__tablename__ = "story_projects"
|
||||
|
||||
# Primary fields
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
project_id = Column(String(255), unique=True, nullable=False, index=True)
|
||||
user_id = Column(String(255), nullable=False, index=True)
|
||||
|
||||
# Project metadata
|
||||
title = Column(String(500), nullable=True)
|
||||
story_mode = Column(String(50), nullable=True)
|
||||
story_template = Column(String(100), nullable=True)
|
||||
|
||||
# Story state (stored as JSON)
|
||||
setup = Column(JSON, nullable=True)
|
||||
outline = Column(JSON, nullable=True)
|
||||
scenes = Column(JSON, nullable=True)
|
||||
story_content = Column(JSON, nullable=True)
|
||||
anime_bible = Column(JSON, nullable=True)
|
||||
media_state = Column(JSON, nullable=True)
|
||||
|
||||
# UI/progress state
|
||||
current_phase = Column(String(50), nullable=True)
|
||||
status = Column(String(50), default="draft", nullable=False, index=True)
|
||||
is_favorite = Column(Boolean, default=False, index=True)
|
||||
is_complete = Column(Boolean, default=False)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False, index=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_story_user_status_created", "user_id", "status", "created_at"),
|
||||
Index("idx_story_user_favorite_updated", "user_id", "is_favorite", "updated_at"),
|
||||
)
|
||||
|
||||
@@ -26,6 +26,8 @@ class UsageStatus(enum.Enum):
|
||||
WARNING = "warning" # 80% usage
|
||||
LIMIT_REACHED = "limit_reached" # 100% usage
|
||||
SUSPENDED = "suspended"
|
||||
CANCELLED = "cancelled"
|
||||
PAST_DUE = "past_due"
|
||||
|
||||
class APIProvider(enum.Enum):
|
||||
GEMINI = "gemini"
|
||||
@@ -389,4 +391,20 @@ class SubscriptionRenewalHistory(Base):
|
||||
# Indexes for performance
|
||||
__table_args__ = (
|
||||
{'mysql_engine': 'InnoDB'},
|
||||
)
|
||||
)
|
||||
|
||||
class FraudWarning(Base):
|
||||
__tablename__ = "fraud_warnings"
|
||||
|
||||
id = Column(String(100), primary_key=True)
|
||||
charge_id = Column(String(100), nullable=False)
|
||||
payment_intent_id = Column(String(100), nullable=True)
|
||||
user_id = Column(String(100), nullable=True)
|
||||
amount = Column(Integer, nullable=False, default=0)
|
||||
currency = Column(String(10), nullable=False, default="")
|
||||
status = Column(String(20), nullable=False, default="open")
|
||||
action = Column(String(20), nullable=False, default="none")
|
||||
action_at = Column(DateTime, nullable=True)
|
||||
reason_notes = Column(Text, nullable=True)
|
||||
meta_info = Column(JSON, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
Reference in New Issue
Block a user