Release Candidate: Production Release with Multi-Tenant & Onboarding Enhancements
This commit is contained in:
@@ -57,6 +57,7 @@ class StoryOutlineMixin(StoryServiceBase):
|
||||
ending_preference: str,
|
||||
user_id: str,
|
||||
use_structured_output: bool = True,
|
||||
include_anime_bible: bool = False,
|
||||
) -> Any:
|
||||
"""Generate a story outline with optional structured JSON output."""
|
||||
persona_prompt = self.build_persona_prompt(
|
||||
|
||||
@@ -145,20 +145,45 @@ Write ONLY the premise sentence(s). Do not write anything else.
|
||||
"reasoning",
|
||||
],
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
}
|
||||
},
|
||||
"required": ["options"],
|
||||
}
|
||||
|
||||
def _build_idea_enhance_schema(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"suggestions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"idea": {"type": "string"},
|
||||
"whats_missing": {"type": "string"},
|
||||
"why_choose": {"type": "string"},
|
||||
},
|
||||
"required": ["idea", "whats_missing", "why_choose"],
|
||||
},
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
}
|
||||
},
|
||||
"required": ["options"],
|
||||
"required": ["suggestions"],
|
||||
}
|
||||
|
||||
def generate_story_setup_options(
|
||||
self,
|
||||
*,
|
||||
story_idea: str,
|
||||
story_mode: str | None,
|
||||
story_template: str | None,
|
||||
brand_context: Dict[str, Any] | None,
|
||||
user_id: str,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Generate 3 story setup options from a user's story idea."""
|
||||
"""Generate a single story setup option from a user's story idea."""
|
||||
|
||||
suggested_writing_styles = ['Formal', 'Casual', 'Poetic', 'Humorous', 'Academic', 'Journalistic', 'Narrative']
|
||||
suggested_story_tones = ['Dark', 'Uplifting', 'Suspenseful', 'Whimsical', 'Melancholic', 'Mysterious', 'Romantic', 'Adventurous']
|
||||
@@ -167,12 +192,59 @@ Write ONLY the premise sentence(s). Do not write anything else.
|
||||
suggested_content_ratings = ['G', 'PG', 'PG-13', 'R']
|
||||
suggested_ending_preferences = ['Happy', 'Tragic', 'Cliffhanger', 'Twist', 'Open-ended', 'Bittersweet']
|
||||
|
||||
mode_label = None
|
||||
if story_mode == "marketing":
|
||||
mode_label = "Non-fiction marketing story (brand or product campaign)"
|
||||
elif story_mode == "pure":
|
||||
mode_label = "Fiction story"
|
||||
|
||||
template_label = None
|
||||
if story_template == "product_story":
|
||||
template_label = "Product Story"
|
||||
elif story_template == "brand_manifesto":
|
||||
template_label = "Brand Manifesto"
|
||||
elif story_template == "founder_story":
|
||||
template_label = "Founder Story"
|
||||
elif story_template == "customer_story":
|
||||
template_label = "Customer Story"
|
||||
elif story_template == "short_fiction":
|
||||
template_label = "Short Fiction"
|
||||
elif story_template == "long_fiction":
|
||||
template_label = "Long Fiction"
|
||||
elif story_template == "anime_fiction":
|
||||
template_label = "Anime Fiction"
|
||||
elif story_template == "experimental_fiction":
|
||||
template_label = "Experimental Fiction"
|
||||
|
||||
brand_name = None
|
||||
writing_tone = None
|
||||
audience_description = None
|
||||
if isinstance(brand_context, dict):
|
||||
brand_name = brand_context.get("brand_name")
|
||||
writing_tone = brand_context.get("writing_tone")
|
||||
target_audience = brand_context.get("target_audience")
|
||||
if isinstance(target_audience, dict):
|
||||
audience_description = target_audience.get("description") or target_audience.get("summary")
|
||||
elif isinstance(target_audience, str):
|
||||
audience_description = target_audience
|
||||
|
||||
setup_prompt = f"""\
|
||||
You are an expert story writer and creative writing assistant. A user has provided the following story idea or information:
|
||||
You are an expert story writer and creative writing assistant.
|
||||
|
||||
{"This is a " + mode_label + "." if mode_label else ""}
|
||||
{("The user selected the template: " + template_label + ".") if template_label else ""}
|
||||
|
||||
The story should stay consistent with the brand and audience context below when relevant:
|
||||
|
||||
- Brand name or site: {brand_name or "Not specified"}
|
||||
- Headline/overall writing tone: {writing_tone or "Not specified"}
|
||||
- Audience description: {audience_description or "Not specified"}
|
||||
|
||||
The user has provided the following story idea or information:
|
||||
|
||||
{story_idea}
|
||||
|
||||
Based on this story idea, generate exactly 3 different, well-thought-out story setup options. Each option should be CREATIVE, PERSONALIZED, and perfectly tailored to the user's specific story idea.
|
||||
Based on this story idea, generate exactly 1 well-thought-out story setup option. The setup should be CREATIVE, PERSONALIZED, and perfectly tailored to the user's specific story idea.
|
||||
|
||||
**CRITICAL - Creative Freedom:**
|
||||
- You have COMPLETE FREEDOM to craft personalized values that best fit the user's story idea
|
||||
@@ -183,7 +255,7 @@ Based on this story idea, generate exactly 3 different, well-thought-out story s
|
||||
- Narrative POV: "Second Person (You)" or "Omniscient Narrator as Guide" (not just standard options)
|
||||
- The goal is to create the PERFECT setup for THIS specific story, not to fit into generic categories
|
||||
|
||||
Each option should:
|
||||
The setup should:
|
||||
1. Have a unique and creative persona that fits the story idea perfectly
|
||||
2. Define a compelling story setting that brings the idea to life
|
||||
3. Describe interesting and engaging characters
|
||||
@@ -212,23 +284,23 @@ Each option should:
|
||||
|
||||
**Remember:** These are ONLY suggestions. If a custom value better serves the story idea, CREATE IT!
|
||||
|
||||
Return exactly 3 options as a JSON array. Each option must include a "premise" field with the story premise.
|
||||
Return exactly 1 option as a JSON array with a single object in "options". The object must include a "premise" field with the story premise.
|
||||
"""
|
||||
|
||||
setup_schema = self._build_setup_schema()
|
||||
|
||||
try:
|
||||
logger.info(f"[StoryWriter] Generating story setup options for user {user_id}")
|
||||
logger.info(f"[StoryWriter] Generating story setup option for user {user_id}")
|
||||
response = self.load_json_response(
|
||||
llm_text_gen(prompt=setup_prompt, json_struct=setup_schema, user_id=user_id)
|
||||
)
|
||||
|
||||
options = response.get("options", [])
|
||||
if len(options) != 3:
|
||||
logger.warning(f"[StoryWriter] Expected 3 options but got {len(options)}, correcting count")
|
||||
if len(options) < 3:
|
||||
raise ValueError(f"Expected 3 options but got {len(options)}")
|
||||
options = options[:3]
|
||||
if len(options) != 1:
|
||||
logger.warning(f"[StoryWriter] Expected 1 option but got {len(options)}, correcting count")
|
||||
if len(options) < 1:
|
||||
raise ValueError(f"Expected 1 option but got {len(options)}")
|
||||
options = options[:1]
|
||||
|
||||
for idx, option in enumerate(options):
|
||||
if not option.get("premise") or not option.get("premise", "").strip():
|
||||
@@ -262,7 +334,7 @@ Return exactly 3 options as a JSON array. Each option must include a "premise" f
|
||||
premise += "."
|
||||
option["premise"] = premise
|
||||
|
||||
logger.info(f"[StoryWriter] Generated {len(options)} story setup options with premises for user {user_id}")
|
||||
logger.info(f"[StoryWriter] Generated {len(options)} story setup option(s) with premise for user {user_id}")
|
||||
return options
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -273,3 +345,119 @@ Return exactly 3 options as a JSON array. Each option must include a "premise" f
|
||||
logger.error(f"[StoryWriter] Error generating story setup options: {exc}")
|
||||
raise RuntimeError(f"Failed to generate story setup options: {exc}") from exc
|
||||
|
||||
def enhance_story_idea(
|
||||
self,
|
||||
*,
|
||||
story_idea: str,
|
||||
story_mode: str | None,
|
||||
story_template: str | None,
|
||||
brand_context: Dict[str, Any] | None,
|
||||
user_id: str,
|
||||
fiction_variant: str | None = None,
|
||||
narrative_energy: str | None = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
mode_label = None
|
||||
if story_mode == "marketing":
|
||||
mode_label = "Non-fiction marketing story (brand or product campaign)"
|
||||
elif story_mode == "pure":
|
||||
mode_label = "Fiction story"
|
||||
|
||||
template_label = None
|
||||
if story_template == "product_story":
|
||||
template_label = "Product Story"
|
||||
elif story_template == "brand_manifesto":
|
||||
template_label = "Brand Manifesto"
|
||||
elif story_template == "founder_story":
|
||||
template_label = "Founder Story"
|
||||
elif story_template == "customer_story":
|
||||
template_label = "Customer Story"
|
||||
elif story_template == "short_fiction":
|
||||
template_label = "Short Fiction"
|
||||
elif story_template == "long_fiction":
|
||||
template_label = "Long Fiction"
|
||||
elif story_template == "anime_fiction":
|
||||
template_label = "Anime Fiction"
|
||||
elif story_template == "experimental_fiction":
|
||||
template_label = "Experimental Fiction"
|
||||
|
||||
brand_name = None
|
||||
writing_tone = None
|
||||
audience_description = None
|
||||
if isinstance(brand_context, dict):
|
||||
brand_name = brand_context.get("brand_name")
|
||||
writing_tone = brand_context.get("writing_tone")
|
||||
target_audience = brand_context.get("target_audience")
|
||||
if isinstance(target_audience, dict):
|
||||
audience_description = target_audience.get("description") or target_audience.get("summary")
|
||||
elif isinstance(target_audience, str):
|
||||
audience_description = target_audience
|
||||
|
||||
fiction_focus_line = ""
|
||||
if fiction_variant:
|
||||
fiction_focus_line = f'Treat the story as "{fiction_variant}" and lean into that creative focus.'
|
||||
|
||||
energy_line = ""
|
||||
if narrative_energy:
|
||||
energy_line = f'Target narrative energy: {narrative_energy}.'
|
||||
|
||||
enhance_prompt = f"""You are a creative writing coach helping a user refine and expand a story idea.
|
||||
|
||||
{"This is a " + mode_label + "." if mode_label else ""}
|
||||
{("The user selected the template: " + template_label + ".") if template_label else ""}
|
||||
{fiction_focus_line}
|
||||
{energy_line}
|
||||
|
||||
When relevant, keep the idea aligned with this brand and audience context:
|
||||
- Brand name or site: {brand_name or "Not specified"}
|
||||
- Headline/overall writing tone: {writing_tone or "Not specified"}
|
||||
- Audience description: {audience_description or "Not specified"}
|
||||
|
||||
The user has written the following story idea or concept:
|
||||
|
||||
{story_idea}
|
||||
|
||||
Your task is to propose exactly 3 alternative enhanced story idea options.
|
||||
|
||||
Each option must:
|
||||
- Preserve the user's core premise and intent.
|
||||
- Make the premise clearer and more compelling.
|
||||
- Surface the central conflict or tension.
|
||||
- Clarify the main characters and their goals.
|
||||
- Strengthen the setting and stakes.
|
||||
- Stay at the "idea" level, not a full outline or beat-by-beat breakdown.
|
||||
|
||||
For each option, return three fields:
|
||||
- "idea": 2-4 sentences describing the improved story idea, suitable for a single textarea input.
|
||||
- "whats_missing": 2-4 sentences explaining what important details are missing or underspecified in the current brief. Focus on gaps such as: protagonist details, antagonist or opposing force, stakes, setting and time period, audience/age group, subgenre or type of fiction (for example, anime vs grounded sci-fi), language or tone preferences, and any format constraints.
|
||||
- "why_choose": 1-3 sentences explaining how this option interprets the original idea and why it might be a strong direction for the story.
|
||||
|
||||
Do not write a full story outline.
|
||||
Do not output numbered lists or markdown formatting.
|
||||
|
||||
Return a single JSON object with a "suggestions" array of 3 items, where each item has the keys "idea", "whats_missing", and "why_choose"."""
|
||||
|
||||
schema = self._build_idea_enhance_schema()
|
||||
|
||||
try:
|
||||
logger.info(f"[StoryWriter] Enhancing story idea with structured suggestions for user {user_id}")
|
||||
response = self.load_json_response(
|
||||
llm_text_gen(prompt=enhance_prompt, json_struct=schema, user_id=user_id)
|
||||
)
|
||||
suggestions = response.get("suggestions", [])
|
||||
if len(suggestions) != 3:
|
||||
logger.warning(
|
||||
f"[StoryWriter] Expected 3 idea suggestions but got {len(suggestions)}, correcting count"
|
||||
)
|
||||
if len(suggestions) < 3:
|
||||
raise ValueError(f"Expected 3 suggestions but got {len(suggestions)}")
|
||||
suggestions = suggestions[:3]
|
||||
return suggestions
|
||||
except HTTPException:
|
||||
raise
|
||||
except json.JSONDecodeError as exc:
|
||||
logger.error(f"[StoryWriter] Failed to parse JSON response for story idea enhancement: {exc}")
|
||||
raise RuntimeError(f"Failed to parse story idea enhancement suggestions: {exc}") from exc
|
||||
except Exception as exc:
|
||||
logger.error(f"[StoryWriter] Error enhancing story idea: {exc}")
|
||||
raise RuntimeError(f"Failed to enhance story idea: {exc}") from exc
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
import json
|
||||
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
from services.story_writer.image_generation_service import StoryImageGenerationService
|
||||
|
||||
from .base import StoryServiceBase
|
||||
@@ -36,6 +38,7 @@ class StoryContentMixin(StoryOutlineMixin):
|
||||
content_rating: str,
|
||||
ending_preference: str,
|
||||
story_length: str = "Medium",
|
||||
anime_bible: Optional[Dict[str, Any]] = None,
|
||||
user_id: str,
|
||||
) -> str:
|
||||
"""Generate the starting section (or full short story)."""
|
||||
@@ -52,6 +55,19 @@ class StoryContentMixin(StoryOutlineMixin):
|
||||
ending_preference,
|
||||
)
|
||||
|
||||
anime_bible_context = ""
|
||||
if anime_bible:
|
||||
try:
|
||||
serialized_bible = json.dumps(anime_bible, ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
serialized_bible = str(anime_bible)
|
||||
anime_bible_context = f"""
|
||||
|
||||
You also have a structured ANIME STORY BIBLE that defines the main cast, world rules, and visual style. Use it as a hard constraint for character consistency, worldbuilding, and visual storytelling:
|
||||
|
||||
{serialized_bible}
|
||||
"""
|
||||
|
||||
outline_text = self._format_outline_for_prompt(outline)
|
||||
story_length_lower = story_length.lower()
|
||||
is_short_story = "short" in story_length_lower or "1000" in story_length_lower
|
||||
@@ -61,6 +77,8 @@ class StoryContentMixin(StoryOutlineMixin):
|
||||
short_story_prompt = f"""\
|
||||
{persona_prompt}
|
||||
|
||||
{anime_bible_context}
|
||||
|
||||
You have a gripping premise in mind:
|
||||
|
||||
{premise}
|
||||
@@ -154,6 +172,285 @@ on establishing the setting, characters, and beginning of the plot in {initial_w
|
||||
logger.error(f"Story Start Generation Error: {exc}")
|
||||
raise RuntimeError(f"Failed to generate story start: {exc}") from exc
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Anime scene refinement
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def refine_anime_scene_text(
|
||||
self,
|
||||
*,
|
||||
scene: Dict[str, Any],
|
||||
persona: str,
|
||||
story_setting: str,
|
||||
character_input: str,
|
||||
plot_elements: str,
|
||||
writing_style: str,
|
||||
story_tone: str,
|
||||
narrative_pov: str,
|
||||
audience_age_group: str,
|
||||
content_rating: str,
|
||||
anime_bible: Optional[Dict[str, Any]],
|
||||
user_id: str,
|
||||
) -> Dict[str, Any]:
|
||||
persona_prompt = self.build_persona_prompt(
|
||||
persona,
|
||||
story_setting,
|
||||
character_input,
|
||||
plot_elements,
|
||||
writing_style,
|
||||
story_tone,
|
||||
narrative_pov,
|
||||
audience_age_group,
|
||||
content_rating,
|
||||
"Neutral",
|
||||
)
|
||||
|
||||
anime_bible_context = ""
|
||||
if anime_bible:
|
||||
try:
|
||||
serialized_bible = json.dumps(anime_bible, ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
serialized_bible = str(anime_bible)
|
||||
anime_bible_context = f"""
|
||||
|
||||
You also have a structured ANIME STORY BIBLE that defines the main cast, world rules, and visual style. Use it as a hard constraint for character consistency, worldbuilding, and visual storytelling:
|
||||
|
||||
{serialized_bible}
|
||||
"""
|
||||
|
||||
current_title = scene.get("title", "")
|
||||
current_description = scene.get("description", "")
|
||||
current_image_prompt = scene.get("image_prompt", "")
|
||||
current_audio_narration = scene.get("audio_narration", "")
|
||||
current_character_descriptions = scene.get("character_descriptions") or []
|
||||
current_key_events = scene.get("key_events") or []
|
||||
|
||||
scene_schema: Dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"image_prompt": {"type": "string"},
|
||||
"audio_narration": {"type": "string"},
|
||||
"character_descriptions": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"key_events": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"required": ["title", "description", "image_prompt", "audio_narration"],
|
||||
}
|
||||
|
||||
prompt = f"""
|
||||
{persona_prompt}
|
||||
|
||||
{anime_bible_context}
|
||||
|
||||
You are refining a single anime story scene so that it fully respects the anime story bible for characters, world rules, and visual style.
|
||||
|
||||
Current scene:
|
||||
- Title: {current_title}
|
||||
- Description: {current_description}
|
||||
- Image prompt: {current_image_prompt}
|
||||
- Audio narration: {current_audio_narration}
|
||||
- Character descriptions: {current_character_descriptions}
|
||||
- Key events: {current_key_events}
|
||||
|
||||
Refine the scene so that:
|
||||
- Title is concise and evocative
|
||||
- Description clearly describes what happens in the scene
|
||||
- Image prompt is vivid, visual, and aligned with the anime bible style and cast
|
||||
- Audio narration is natural, spoken-friendly text matching the scene
|
||||
- Character descriptions highlight key visual and personality traits relevant to this moment
|
||||
- Key events list the main beats of the scene
|
||||
|
||||
Respond with JSON matching this schema:
|
||||
{scene_schema}
|
||||
"""
|
||||
|
||||
try:
|
||||
raw = llm_text_gen(
|
||||
prompt=prompt.strip(),
|
||||
json_struct=scene_schema,
|
||||
user_id=user_id,
|
||||
)
|
||||
data = self.load_json_response(raw)
|
||||
except Exception as exc:
|
||||
logger.warning(f"[StoryWriter] Failed to refine anime scene text via LLM: {exc}")
|
||||
return {
|
||||
"scene_number": scene.get("scene_number"),
|
||||
"title": current_title,
|
||||
"description": current_description,
|
||||
"image_prompt": current_image_prompt,
|
||||
"audio_narration": current_audio_narration,
|
||||
"character_descriptions": current_character_descriptions,
|
||||
"key_events": current_key_events,
|
||||
}
|
||||
|
||||
refined = {
|
||||
"scene_number": scene.get("scene_number"),
|
||||
"title": data.get("title", current_title),
|
||||
"description": data.get("description", current_description),
|
||||
"image_prompt": data.get("image_prompt", current_image_prompt),
|
||||
"audio_narration": data.get("audio_narration", current_audio_narration),
|
||||
"character_descriptions": data.get(
|
||||
"character_descriptions", current_character_descriptions
|
||||
),
|
||||
"key_events": data.get("key_events", current_key_events),
|
||||
}
|
||||
return refined
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Anime scene generation from bible
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def generate_anime_scene_from_bible(
|
||||
self,
|
||||
*,
|
||||
premise: str,
|
||||
persona: str,
|
||||
story_setting: str,
|
||||
character_input: str,
|
||||
plot_elements: str,
|
||||
writing_style: str,
|
||||
story_tone: str,
|
||||
narrative_pov: str,
|
||||
audience_age_group: str,
|
||||
content_rating: str,
|
||||
anime_bible: Dict[str, Any],
|
||||
previous_scenes: Optional[List[Dict[str, Any]]],
|
||||
target_scene_number: Optional[int],
|
||||
user_id: str,
|
||||
) -> Dict[str, Any]:
|
||||
persona_prompt = self.build_persona_prompt(
|
||||
persona,
|
||||
story_setting,
|
||||
character_input,
|
||||
plot_elements,
|
||||
writing_style,
|
||||
story_tone,
|
||||
narrative_pov,
|
||||
audience_age_group,
|
||||
content_rating,
|
||||
"Neutral",
|
||||
)
|
||||
|
||||
try:
|
||||
serialized_bible = json.dumps(anime_bible, ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
serialized_bible = str(anime_bible)
|
||||
|
||||
anime_bible_context = f"""
|
||||
|
||||
You have a structured ANIME STORY BIBLE that defines the main cast, world rules, and visual style. You MUST treat it as a hard constraint for character consistency, worldbuilding, and visual storytelling:
|
||||
|
||||
{serialized_bible}
|
||||
"""
|
||||
|
||||
previous_summary_lines: List[str] = []
|
||||
if previous_scenes:
|
||||
for s in previous_scenes[:6]:
|
||||
num = s.get("scene_number")
|
||||
title = s.get("title") or ""
|
||||
desc = s.get("description") or ""
|
||||
summary = desc
|
||||
if len(summary) > 200:
|
||||
summary = summary[:197] + "..."
|
||||
previous_summary_lines.append(
|
||||
f"- Scene {num}: {title} — {summary}".strip()
|
||||
)
|
||||
|
||||
previous_block = ""
|
||||
if previous_summary_lines:
|
||||
previous_block = (
|
||||
"\nPrevious scenes so far (for continuity, do NOT contradict):\n"
|
||||
+ "\n".join(previous_summary_lines)
|
||||
)
|
||||
|
||||
scene_schema: Dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"image_prompt": {"type": "string"},
|
||||
"audio_narration": {"type": "string"},
|
||||
"character_descriptions": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"key_events": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"required": ["title", "description", "image_prompt", "audio_narration"],
|
||||
}
|
||||
|
||||
prompt = f"""
|
||||
{persona_prompt}
|
||||
|
||||
{anime_bible_context}
|
||||
|
||||
You are generating a brand new anime story scene that must fully respect the anime story bible for characters, world rules, and visual style.
|
||||
|
||||
Overall premise:
|
||||
{premise}
|
||||
{previous_block}
|
||||
|
||||
Your task:
|
||||
- Create the NEXT SCENE in this story.
|
||||
- It must be consistent with the anime bible (cast, world rules, visual style).
|
||||
- It must logically follow from any previous scenes given above.
|
||||
|
||||
Design the scene so that:
|
||||
- Title is concise and evocative.
|
||||
- Description clearly describes what happens in the scene.
|
||||
- Image prompt is vivid, visual, and aligned with the anime bible style and cast.
|
||||
- Audio narration is natural, spoken-friendly text matching the scene.
|
||||
- Character descriptions highlight key visual and personality traits relevant to this moment.
|
||||
- Key events list the main beats of the scene.
|
||||
|
||||
Respond with JSON matching this schema:
|
||||
{scene_schema}
|
||||
"""
|
||||
|
||||
try:
|
||||
raw = llm_text_gen(
|
||||
prompt=prompt.strip(),
|
||||
json_struct=scene_schema,
|
||||
user_id=user_id,
|
||||
)
|
||||
data = self.load_json_response(raw)
|
||||
except Exception as exc:
|
||||
logger.error(f"[StoryWriter] Failed to generate anime scene from bible: {exc}")
|
||||
raise RuntimeError(f"Failed to generate anime scene from bible: {exc}") from exc
|
||||
|
||||
next_scene_number = target_scene_number
|
||||
if next_scene_number is None:
|
||||
if previous_scenes and len(previous_scenes) > 0:
|
||||
last = previous_scenes[-1]
|
||||
try:
|
||||
last_num = int(last.get("scene_number") or 0)
|
||||
except Exception:
|
||||
last_num = len(previous_scenes)
|
||||
next_scene_number = last_num + 1
|
||||
else:
|
||||
next_scene_number = 1
|
||||
|
||||
result = {
|
||||
"scene_number": next_scene_number,
|
||||
"title": data.get("title", "").strip(),
|
||||
"description": data.get("description", "").strip(),
|
||||
"image_prompt": data.get("image_prompt", "").strip(),
|
||||
"audio_narration": data.get("audio_narration", "").strip(),
|
||||
"character_descriptions": data.get("character_descriptions") or [],
|
||||
"key_events": data.get("key_events") or [],
|
||||
}
|
||||
return result
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Continuation
|
||||
# ------------------------------------------------------------------ #
|
||||
@@ -174,6 +471,7 @@ on establishing the setting, characters, and beginning of the plot in {initial_w
|
||||
audience_age_group: str,
|
||||
content_rating: str,
|
||||
ending_preference: str,
|
||||
anime_bible: Optional[Dict[str, Any]] = None,
|
||||
story_length: str = "Medium",
|
||||
user_id: str,
|
||||
) -> str:
|
||||
@@ -191,6 +489,19 @@ on establishing the setting, characters, and beginning of the plot in {initial_w
|
||||
ending_preference,
|
||||
)
|
||||
|
||||
anime_bible_context = ""
|
||||
if anime_bible:
|
||||
try:
|
||||
serialized_bible = json.dumps(anime_bible, ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
serialized_bible = str(anime_bible)
|
||||
anime_bible_context = f"""
|
||||
|
||||
You also have a structured ANIME STORY BIBLE that defines the main cast, world rules, and visual style. Use it as a hard constraint for character consistency, worldbuilding, and visual storytelling:
|
||||
|
||||
{serialized_bible}
|
||||
"""
|
||||
|
||||
outline_text = self._format_outline_for_prompt(outline)
|
||||
_, continuation_word_count = self._get_story_length_guidance(story_length)
|
||||
current_word_count = len(story_text.split()) if story_text else 0
|
||||
@@ -227,6 +538,8 @@ on establishing the setting, characters, and beginning of the plot in {initial_w
|
||||
continuation_prompt = f"""\
|
||||
{persona_prompt}
|
||||
|
||||
{anime_bible_context}
|
||||
|
||||
You have a gripping premise in mind:
|
||||
|
||||
{premise}
|
||||
@@ -298,6 +611,7 @@ You have written approximately {current_word_count} words so far, leaving approx
|
||||
audience_age_group: str,
|
||||
content_rating: str,
|
||||
ending_preference: str,
|
||||
anime_bible: Optional[Dict[str, Any]] = None,
|
||||
user_id: str,
|
||||
max_iterations: int = 10,
|
||||
) -> Dict[str, Any]:
|
||||
@@ -352,6 +666,7 @@ You have written approximately {current_word_count} words so far, leaving approx
|
||||
audience_age_group=audience_age_group,
|
||||
content_rating=content_rating,
|
||||
ending_preference=ending_preference,
|
||||
anime_bible=anime_bible,
|
||||
user_id=user_id,
|
||||
)
|
||||
if not draft:
|
||||
@@ -375,6 +690,7 @@ You have written approximately {current_word_count} words so far, leaving approx
|
||||
audience_age_group=audience_age_group,
|
||||
content_rating=content_rating,
|
||||
ending_preference=ending_preference,
|
||||
anime_bible=anime_bible,
|
||||
user_id=user_id,
|
||||
)
|
||||
if continuation:
|
||||
@@ -420,6 +736,7 @@ You have written approximately {current_word_count} words so far, leaving approx
|
||||
height: int = 1024,
|
||||
model: Optional[str] = None,
|
||||
db: Optional[Session] = None,
|
||||
anime_bible: Optional[Dict[str, Any]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Generate images for story scenes."""
|
||||
image_service = StoryImageGenerationService()
|
||||
@@ -431,5 +748,6 @@ You have written approximately {current_word_count} words so far, leaving approx
|
||||
height=height,
|
||||
model=model,
|
||||
db=db,
|
||||
anime_bible=anime_bible,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user