324 lines
12 KiB
Python
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))
|
|
|
|
|