Release Candidate: Production Release with Multi-Tenant & Onboarding Enhancements
This commit is contained in:
@@ -2,6 +2,8 @@ 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 (
|
||||
@@ -13,8 +15,14 @@ from models.story_models import (
|
||||
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
|
||||
|
||||
@@ -39,6 +47,9 @@ async def generate_story_setup(
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -52,6 +63,152 @@ async def generate_story_setup(
|
||||
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,
|
||||
@@ -108,6 +265,9 @@ async def generate_outline(
|
||||
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,
|
||||
@@ -122,15 +282,37 @@ async def generate_outline(
|
||||
ending_preference=request.ending_preference,
|
||||
user_id=user_id,
|
||||
use_structured_output=use_structured,
|
||||
include_anime_bible=True,
|
||||
)
|
||||
|
||||
if isinstance(outline, list):
|
||||
scenes: List[StoryScene] = [
|
||||
StoryScene(**scene) if isinstance(scene, dict) else scene for scene in outline
|
||||
]
|
||||
return StoryOutlineResponse(outline=scenes, success=True, is_structured=True)
|
||||
anime_bible: Dict[str, Any] | None = None
|
||||
outline_payload: Any = outline
|
||||
|
||||
return StoryOutlineResponse(outline=str(outline), success=True, is_structured=False)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user