Files
ALwrity/backend/api/story_writer/routes/story_setup.py

324 lines
12 KiB
Python

from typing import Any, Dict, List
from fastapi import APIRouter, Depends, HTTPException
from loguru import logger
from sqlalchemy.orm import Session
from sqlalchemy import desc
from middleware.auth_middleware import get_current_user
from models.story_models import (
StorySetupGenerationRequest,
StorySetupGenerationResponse,
StorySetupOption,
StoryGenerationRequest,
StoryOutlineResponse,
StoryScene,
StoryStartRequest,
StoryPremiseResponse,
StoryIdeaEnhanceRequest,
StoryIdeaEnhanceResponse,
StoryIdeaEnhanceSuggestion,
)
from services.story_writer.story_service import StoryWriterService
from api.onboarding_utils.onboarding_summary_service import OnboardingSummaryService
from services.database import get_session_for_user
from models.content_asset_models import ContentAsset, AssetType, AssetSource
from ..utils.auth import require_authenticated_user
router = APIRouter()
story_service = StoryWriterService()
@router.post("/generate-setup", response_model=StorySetupGenerationResponse)
async def generate_story_setup(
request: StorySetupGenerationRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
) -> StorySetupGenerationResponse:
"""Generate 3 story setup options from a user's story idea."""
try:
user_id = require_authenticated_user(current_user)
if not request.story_idea or not request.story_idea.strip():
raise HTTPException(status_code=400, detail="Story idea is required")
logger.info(f"[StoryWriter] Generating story setup options for user {user_id}")
options = story_service.generate_story_setup_options(
story_idea=request.story_idea,
story_mode=request.story_mode,
story_template=request.story_template,
brand_context=request.brand_context,
user_id=user_id,
)
setup_options = [StorySetupOption(**option) for option in options]
return StorySetupGenerationResponse(options=setup_options, success=True)
except HTTPException:
raise
except Exception as exc:
logger.error(f"[StoryWriter] Failed to generate story setup options: {exc}")
raise HTTPException(status_code=500, detail=str(exc))
@router.post("/enhance-idea", response_model=StoryIdeaEnhanceResponse)
async def enhance_story_idea(
request: StoryIdeaEnhanceRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
) -> StoryIdeaEnhanceResponse:
try:
user_id = require_authenticated_user(current_user)
if not request.story_idea or not request.story_idea.strip():
raise HTTPException(status_code=400, detail="Story idea is required")
logger.info(f"[StoryWriter] Enhancing story idea for user {user_id}")
suggestions = story_service.enhance_story_idea(
story_idea=request.story_idea,
story_mode=request.story_mode,
story_template=request.story_template,
brand_context=request.brand_context,
user_id=user_id,
fiction_variant=request.fiction_variant,
narrative_energy=request.narrative_energy,
)
return StoryIdeaEnhanceResponse(
suggestions=[StoryIdeaEnhanceSuggestion(**s) for s in suggestions],
success=True,
)
except HTTPException:
raise
except Exception as exc:
logger.error(f"[StoryWriter] Failed to enhance story idea: {exc}")
raise HTTPException(status_code=500, detail=str(exc))
@router.get("/context")
async def get_story_context(
current_user: Dict[str, Any] = Depends(get_current_user),
) -> Dict[str, Any]:
"""Return onboarding-based story context for the current user."""
try:
user_id = require_authenticated_user(current_user)
summary_service = OnboardingSummaryService(user_id)
summary = await summary_service.get_onboarding_summary()
canonical_profile = summary.get("canonical_profile") or {}
persona_readiness = summary.get("persona_readiness") or {}
capabilities = summary.get("capabilities") or {}
website_url = summary.get("website_url")
style_analysis = summary.get("style_analysis") or {}
research_preferences = summary.get("research_preferences") or {}
brand_name = None
if isinstance(style_analysis, dict):
brand_name = style_analysis.get("brand_name") or style_analysis.get("site_title")
writing_tone = canonical_profile.get("writing_tone")
target_audience = canonical_profile.get("target_audience")
brand_context = {
"brand_name": brand_name,
"writing_tone": writing_tone,
"target_audience": target_audience,
}
avatar_url = None
voice_preview_url = None
custom_voice_id = None
db: Session | None = get_session_for_user(user_id)
if db:
try:
avatar_asset = (
db.query(ContentAsset)
.filter(
ContentAsset.user_id == user_id,
ContentAsset.asset_type == AssetType.IMAGE,
ContentAsset.source_module.in_(
[AssetSource.BRAND_AVATAR_GENERATOR, AssetSource.STORY_WRITER]
),
)
.order_by(desc(ContentAsset.created_at))
.limit(50)
.all()
)
selected_avatar = None
for candidate in avatar_asset:
if candidate.source_module == AssetSource.BRAND_AVATAR_GENERATOR:
selected_avatar = candidate
break
meta = candidate.asset_metadata or {}
if meta.get("category") == "brand_avatar":
selected_avatar = candidate
break
if selected_avatar:
avatar_url = selected_avatar.file_url
voice_asset = (
db.query(ContentAsset)
.filter(
ContentAsset.user_id == user_id,
ContentAsset.asset_type == AssetType.AUDIO,
ContentAsset.source_module == AssetSource.VOICE_CLONER,
)
.order_by(desc(ContentAsset.created_at))
.first()
)
if voice_asset:
meta = voice_asset.asset_metadata or {}
voice_preview_url = meta.get("preview_url") or voice_asset.file_url
custom_voice_id = meta.get("custom_voice_id")
finally:
db.close()
persona_enabled = bool(persona_readiness.get("ready")) and bool(
capabilities.get("persona_generation")
)
has_persona_context = persona_enabled and bool(
brand_name or writing_tone or target_audience or avatar_url or voice_preview_url
)
return {
"canonical_profile": canonical_profile,
"website_url": website_url,
"research_preferences": research_preferences,
"brand_context": brand_context,
"brand_assets": {
"avatar_url": avatar_url,
"voice_preview_url": voice_preview_url,
"custom_voice_id": custom_voice_id,
},
"persona_enabled": persona_enabled,
"has_persona_context": has_persona_context,
}
except HTTPException:
raise
except Exception as exc:
logger.error(f"[StoryWriter] Failed to get story context: {exc}")
raise HTTPException(status_code=500, detail="Failed to load story context")
@router.post("/generate-premise", response_model=StoryPremiseResponse)
async def generate_premise(
request: StoryGenerationRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
) -> StoryPremiseResponse:
"""Generate a story premise."""
try:
user_id = require_authenticated_user(current_user)
logger.info(f"[StoryWriter] Generating premise for user {user_id}")
premise = story_service.generate_premise(
persona=request.persona,
story_setting=request.story_setting,
character_input=request.character_input,
plot_elements=request.plot_elements,
writing_style=request.writing_style,
story_tone=request.story_tone,
narrative_pov=request.narrative_pov,
audience_age_group=request.audience_age_group,
content_rating=request.content_rating,
ending_preference=request.ending_preference,
user_id=user_id,
)
return StoryPremiseResponse(premise=premise, success=True)
except HTTPException:
raise
except Exception as exc:
logger.error(f"[StoryWriter] Failed to generate premise: {exc}")
raise HTTPException(status_code=500, detail=str(exc))
@router.post("/generate-outline", response_model=StoryOutlineResponse)
async def generate_outline(
request: StoryStartRequest,
current_user: Dict[str, Any] = Depends(get_current_user),
use_structured: bool = True,
) -> StoryOutlineResponse:
"""Generate a story outline from a premise."""
try:
user_id = require_authenticated_user(current_user)
if not request.premise or not request.premise.strip():
raise HTTPException(status_code=400, detail="Premise is required")
logger.info(
f"[StoryWriter] Generating outline for user {user_id} (structured={use_structured})"
)
logger.info(
"[StoryWriter] Outline params: audience_age_group=%s, writing_style=%s, story_tone=%s",
request.audience_age_group,
request.writing_style,
request.story_tone,
)
# For now, treat all outlines as potentially anime-aware. The downstream
# generation logic will decide whether to actually create a bible based
# on how the prompt is interpreted (e.g., anime templates in persona).
outline = story_service.generate_outline(
premise=request.premise,
persona=request.persona,
story_setting=request.story_setting,
character_input=request.character_input,
plot_elements=request.plot_elements,
writing_style=request.writing_style,
story_tone=request.story_tone,
narrative_pov=request.narrative_pov,
audience_age_group=request.audience_age_group,
content_rating=request.content_rating,
ending_preference=request.ending_preference,
user_id=user_id,
use_structured_output=use_structured,
include_anime_bible=True,
)
anime_bible: Dict[str, Any] | None = None
outline_payload: Any = outline
if isinstance(outline, dict):
if "anime_bible" in outline:
anime_bible = outline.get("anime_bible")
if "scenes" in outline:
outline_payload = outline.get("scenes")
elif "outline" in outline:
outline_payload = outline.get("outline")
if isinstance(outline_payload, list):
scenes: List[StoryScene] = [
StoryScene(**scene) if isinstance(scene, dict) else scene for scene in outline_payload
]
return StoryOutlineResponse(
outline=scenes,
success=True,
is_structured=True,
anime_bible=anime_bible,
)
return StoryOutlineResponse(
outline=str(outline_payload),
success=True,
is_structured=False,
anime_bible=anime_bible,
)
except HTTPException:
raise
except Exception as exc:
logger.error(f"[StoryWriter] Failed to generate outline: {exc}")
raise HTTPException(status_code=500, detail=str(exc))