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