story writer backend migration complete, Blog writer SEO and story writer backend migration complete, Blog writer SEO and story writer frontend migration complete
This commit is contained in:
14
backend/services/story_writer/service_components/__init__.py
Normal file
14
backend/services/story_writer/service_components/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Story Writer service component helpers."""
|
||||
|
||||
from .base import StoryServiceBase
|
||||
from .setup import StorySetupMixin
|
||||
from .outline import StoryOutlineMixin
|
||||
from .story_content import StoryContentMixin
|
||||
|
||||
__all__ = [
|
||||
"StoryServiceBase",
|
||||
"StorySetupMixin",
|
||||
"StoryOutlineMixin",
|
||||
"StoryContentMixin",
|
||||
]
|
||||
|
||||
332
backend/services/story_writer/service_components/base.py
Normal file
332
backend/services/story_writer/service_components/base.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""Core shared functionality for Story Writer service components."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
class StoryServiceBase:
|
||||
"""Base class providing shared helpers for story writer operations."""
|
||||
|
||||
guidelines: str = """\
|
||||
Writing Guidelines:
|
||||
|
||||
Delve deeper. Lose yourself in the world you're building. Unleash vivid
|
||||
descriptions to paint the scenes in your reader's mind.
|
||||
Develop your characters — let their motivations, fears, and complexities unfold naturally.
|
||||
Weave in the threads of your outline, but don't feel constrained by it.
|
||||
Allow your story to surprise you as you write. Use rich imagery, sensory details, and
|
||||
evocative language to bring the setting, characters, and events to life.
|
||||
Introduce elements subtly that can blossom into complex subplots, relationships,
|
||||
or worldbuilding details later in the story.
|
||||
Keep things intriguing but not fully resolved.
|
||||
Avoid boxing the story into a corner too early.
|
||||
Plant the seeds of subplots or potential character arc shifts that can be expanded later.
|
||||
|
||||
IMPORTANT: Respect the story length target. Write with appropriate detail and pacing
|
||||
to reach the target word count, but do NOT exceed it. Once you've reached the target
|
||||
length and provided satisfying closure, conclude the story by writing IAMDONE.
|
||||
"""
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# LLM Utilities
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def generate_with_retry(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
system_prompt: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Generate content using llm_text_gen with retry handling and subscription support."""
|
||||
if not user_id:
|
||||
raise RuntimeError("user_id is required for subscription checking")
|
||||
|
||||
try:
|
||||
return llm_text_gen(prompt=prompt, system_prompt=system_prompt, user_id=user_id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.error(f"Error generating content: {exc}")
|
||||
raise RuntimeError(f"Failed to generate content: {exc}") from exc
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Prompt helpers
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def build_persona_prompt(
|
||||
self,
|
||||
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,
|
||||
ending_preference: str,
|
||||
) -> str:
|
||||
"""Build the persona prompt with all story parameters."""
|
||||
return f"""{persona}
|
||||
|
||||
**STORY SETUP CONTEXT:**
|
||||
|
||||
**Setting:**
|
||||
{story_setting}
|
||||
- Use this specific setting throughout the story
|
||||
- Incorporate setting details naturally into scenes and descriptions
|
||||
- Ensure the setting is clearly established and consistent
|
||||
|
||||
**Characters:**
|
||||
{character_input}
|
||||
- Use these specific characters in the story
|
||||
- Develop these characters according to their descriptions
|
||||
- Maintain character consistency across all scenes
|
||||
- Create character arcs that align with the plot elements
|
||||
|
||||
**Plot Elements:**
|
||||
{plot_elements}
|
||||
- Incorporate these plot elements into the story structure
|
||||
- Address each plot element in relevant scenes
|
||||
- Build connections between plot elements logically
|
||||
- Ensure the ending addresses the main plot elements
|
||||
|
||||
**Writing Style:**
|
||||
{writing_style}
|
||||
- This writing style should be reflected in EVERY aspect of the story
|
||||
- The language, sentence structure, and narrative approach must match this style exactly
|
||||
- If this is a custom or combined style, interpret it in the context of the audience age group
|
||||
- Adapt the style's complexity to match {audience_age_group}
|
||||
|
||||
**Story Tone:**
|
||||
{story_tone}
|
||||
- This tone must be maintained consistently throughout the entire story
|
||||
- The emotional atmosphere, mood, and overall feeling must match this tone
|
||||
- If this is a custom or combined tone, interpret it age-appropriately for {audience_age_group}
|
||||
- Ensure the tone is suitable for {content_rating} content rating
|
||||
|
||||
**Narrative Point of View:**
|
||||
{narrative_pov}
|
||||
- Use this perspective consistently throughout the story
|
||||
- Maintain the chosen perspective in all narration
|
||||
- Apply the perspective appropriately for {audience_age_group}
|
||||
|
||||
**Target Audience:**
|
||||
{audience_age_group}
|
||||
- ALL content must be age-appropriate for this audience
|
||||
- Language complexity, vocabulary, sentence length, and themes must match this age group
|
||||
- Concepts must be understandable and relatable to this audience
|
||||
- Adjust all story elements (style, tone, plot) to be appropriate for this age group
|
||||
|
||||
**Content Rating:**
|
||||
{content_rating}
|
||||
- All content must stay within these content boundaries
|
||||
- Themes, language, and subject matter must respect this rating
|
||||
- Ensure the writing style and tone are compatible with this rating
|
||||
|
||||
**Ending Preference:**
|
||||
{ending_preference}
|
||||
- The story should build toward this type of ending
|
||||
- All plot development should lead naturally to this ending style
|
||||
- Create expectations that align with this ending preference
|
||||
- Ensure the ending is appropriate for {audience_age_group} and {content_rating}
|
||||
|
||||
**CRITICAL INSTRUCTIONS:**
|
||||
- Use ALL of the above story setup parameters to guide your writing
|
||||
- The writing style, tone, narrative POV, audience age group, and content rating are NOT optional - they are REQUIRED constraints
|
||||
- Every word, sentence, and description must align with these parameters
|
||||
- When parameters interact (e.g., style + age group, tone + content rating), ensure they work together harmoniously
|
||||
- Tailor the language complexity, vocabulary, and concepts to the specified audience age group
|
||||
- Maintain consistency with the specified writing style and tone throughout
|
||||
- Ensure all content is appropriate for the specified content rating
|
||||
- Build the narrative toward the specified ending preference
|
||||
- Use the setting, characters, and plot elements provided to create a coherent, engaging story
|
||||
|
||||
Make sure the story is engaging, well-crafted, and perfectly tailored to ALL of the specified parameters above.
|
||||
"""
|
||||
|
||||
def _get_parameter_interaction_guidance(
|
||||
self,
|
||||
writing_style: str,
|
||||
story_tone: str,
|
||||
audience_age_group: str,
|
||||
content_rating: str,
|
||||
) -> str:
|
||||
"""Generate guidance for interpreting custom/combined parameter values and their interactions."""
|
||||
guidance = "**PARAMETER INTERACTION GUIDANCE:**\n\n"
|
||||
|
||||
style_words = writing_style.lower().split()
|
||||
if len(style_words) > 1:
|
||||
guidance += f"**Writing Style Analysis:** The style '{writing_style}' appears to combine multiple approaches:\n"
|
||||
for word in style_words:
|
||||
guidance += f"- '{word.title()}': Interpret this aspect in the context of {audience_age_group}\n"
|
||||
guidance += (
|
||||
"Combine all aspects naturally. For example, if 'Educational Playful':\n"
|
||||
f" → Use playful, engaging language to teach concepts naturally\n"
|
||||
f" → Make learning fun and interactive for {audience_age_group}\n"
|
||||
" → Combine educational content with fun, magical elements\n\n"
|
||||
)
|
||||
else:
|
||||
guidance += f"**Writing Style:** '{writing_style}'\n"
|
||||
guidance += f"- Interpret this style appropriately for {audience_age_group}\n"
|
||||
guidance += "- Adapt the style's complexity to match the audience's reading level\n\n"
|
||||
|
||||
tone_words = story_tone.lower().split()
|
||||
if len(tone_words) > 1:
|
||||
guidance += f"**Story Tone Analysis:** The tone '{story_tone}' combines multiple emotional qualities:\n"
|
||||
for word in tone_words:
|
||||
guidance += f"- '{word.title()}': Express this emotion in an age-appropriate way for {audience_age_group}\n"
|
||||
guidance += (
|
||||
"Blend these emotions throughout the story. For example, if 'Educational Whimsical':\n"
|
||||
" → Use whimsical, playful language to convey educational concepts\n"
|
||||
" → Make the tone both informative and magical\n"
|
||||
f" → Combine wonder and learning in an age-appropriate way for {audience_age_group}\n\n"
|
||||
)
|
||||
else:
|
||||
guidance += f"**Story Tone:** '{story_tone}'\n"
|
||||
guidance += f"- Interpret this tone age-appropriately for {audience_age_group}\n"
|
||||
guidance += f"- Ensure the tone is suitable for {content_rating} content rating\n\n"
|
||||
|
||||
guidance += "**PARAMETER INTERACTION EXAMPLES:**\n\n"
|
||||
|
||||
if "Children (5-12)" in audience_age_group:
|
||||
guidance += f"- When writing_style is '{writing_style}' AND audience_age_group is 'Children (5-12)':\n"
|
||||
guidance += " → Simplify the style's complexity while maintaining its essence\n"
|
||||
guidance += " → Use age-appropriate vocabulary and sentence structure\n"
|
||||
guidance += " → Make the style engaging and accessible for children\n\n"
|
||||
|
||||
if "Children (5-12)" in audience_age_group and "dark" in story_tone.lower():
|
||||
guidance += f"- When story_tone is '{story_tone}' AND audience_age_group is 'Children (5-12)':\n"
|
||||
guidance += " → Interpret 'dark' as mysterious and adventurous, not scary or frightening\n"
|
||||
guidance += " → Use shadows, secrets, and puzzles rather than fear or horror\n"
|
||||
guidance += " → Maintain a sense of wonder and excitement\n"
|
||||
guidance += " → Keep it thrilling but age-appropriate\n\n"
|
||||
|
||||
guidance += f"- When writing_style is '{writing_style}' AND story_tone is '{story_tone}':\n"
|
||||
guidance += " → Combine the style and tone naturally\n"
|
||||
guidance += " → Use the style to express the tone effectively\n"
|
||||
guidance += f" → Ensure both work together harmoniously for {audience_age_group}\n\n"
|
||||
|
||||
guidance += f"- When content_rating is '{content_rating}':\n"
|
||||
guidance += " → Ensure the writing style and tone respect these content boundaries\n"
|
||||
guidance += " → Adjust language, themes, and subject matter to fit the rating\n"
|
||||
guidance += f" → Maintain age-appropriateness for {audience_age_group}\n\n"
|
||||
|
||||
guidance += "**PARAMETER CONFLICT RESOLUTION:**\n"
|
||||
guidance += "If parameters seem to conflict, prioritize in this order:\n"
|
||||
guidance += "1. Audience age group appropriateness (safety and comprehension) - HIGHEST PRIORITY\n"
|
||||
guidance += "2. Content rating compliance (content boundaries)\n"
|
||||
guidance += "3. Writing style and tone (creative expression)\n"
|
||||
guidance += "4. Other parameters (narrative POV, ending preference)\n\n"
|
||||
guidance += "Always ensure that ALL parameters work together to create appropriate, engaging content.\n"
|
||||
|
||||
return guidance
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Outline helpers shared across modules
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _format_outline_for_prompt(self, outline: Any) -> str:
|
||||
"""Format outline (structured or text) for use in prompts."""
|
||||
if isinstance(outline, list):
|
||||
outline_text = "\n".join(
|
||||
[
|
||||
f"Scene {scene.get('scene_number', idx + 1)}: {scene.get('title', 'Untitled')}\n"
|
||||
f" Description: {scene.get('description', '')}\n"
|
||||
f" Key Events: {', '.join(scene.get('key_events', []))}"
|
||||
for idx, scene in enumerate(outline)
|
||||
]
|
||||
)
|
||||
return outline_text
|
||||
return str(outline)
|
||||
|
||||
def _parse_text_outline(self, outline_prompt: str, user_id: str) -> List[Dict[str, Any]]:
|
||||
"""Fallback method to parse text outline if JSON parsing fails."""
|
||||
outline_text = self.generate_with_retry(outline_prompt, user_id=user_id)
|
||||
|
||||
lines = outline_text.strip().split("\n")
|
||||
scenes: List[Dict[str, Any]] = []
|
||||
current_scene: Optional[Dict[str, Any]] = None
|
||||
|
||||
for line in lines:
|
||||
cleaned = line.strip()
|
||||
if not cleaned:
|
||||
continue
|
||||
|
||||
if cleaned[0].isdigit() or cleaned.startswith("Scene") or cleaned.startswith("Chapter"):
|
||||
if current_scene:
|
||||
scenes.append(current_scene)
|
||||
|
||||
scene_number = len(scenes) + 1
|
||||
title = cleaned.replace(f"{scene_number}.", "").replace("Scene", "").replace("Chapter", "").strip()
|
||||
current_scene = {
|
||||
"scene_number": scene_number,
|
||||
"title": title or f"Scene {scene_number}",
|
||||
"description": "",
|
||||
"image_prompt": f"A scene from the story: {title}",
|
||||
"audio_narration": "",
|
||||
"character_descriptions": [],
|
||||
"key_events": [],
|
||||
}
|
||||
continue
|
||||
|
||||
if current_scene:
|
||||
if current_scene["description"]:
|
||||
current_scene["description"] += " " + cleaned
|
||||
else:
|
||||
current_scene["description"] = cleaned
|
||||
|
||||
if current_scene["image_prompt"].startswith("A scene from the story"):
|
||||
current_scene["image_prompt"] = f"A detailed visual representation of: {current_scene['description'][:200]}"
|
||||
if not current_scene["audio_narration"]:
|
||||
current_scene["audio_narration"] = (
|
||||
current_scene["description"][:150] + "..."
|
||||
if len(current_scene["description"]) > 150
|
||||
else current_scene["description"]
|
||||
)
|
||||
|
||||
if current_scene:
|
||||
scenes.append(current_scene)
|
||||
|
||||
if not scenes:
|
||||
scenes.append(
|
||||
{
|
||||
"scene_number": 1,
|
||||
"title": "Story Outline",
|
||||
"description": outline_text.strip(),
|
||||
"image_prompt": f"A scene from the story: {outline_text[:200]}",
|
||||
"audio_narration": outline_text[:150] + "..." if len(outline_text) > 150 else outline_text,
|
||||
"character_descriptions": [],
|
||||
"key_events": [],
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"[StoryWriter] Parsed {len(scenes)} scenes from text outline")
|
||||
return scenes
|
||||
|
||||
def _get_story_length_guidance(self, story_length: str) -> tuple[int, int]:
|
||||
"""Return word count guidance based on story length."""
|
||||
story_length_lower = story_length.lower()
|
||||
if "short" in story_length_lower or "1000" in story_length_lower:
|
||||
return (1000, 0)
|
||||
if "long" in story_length_lower or "10000" in story_length_lower:
|
||||
return (3000, 2500)
|
||||
return (2000, 1500)
|
||||
|
||||
@staticmethod
|
||||
def load_json_response(response_text: Any) -> Dict[str, Any]:
|
||||
"""Normalize responses from llm_text_gen (dict or json string)."""
|
||||
if isinstance(response_text, dict):
|
||||
return response_text
|
||||
if isinstance(response_text, str):
|
||||
return json.loads(response_text)
|
||||
raise ValueError(f"Unexpected response type: {type(response_text)}")
|
||||
|
||||
171
backend/services/story_writer/service_components/outline.py
Normal file
171
backend/services/story_writer/service_components/outline.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""Story outline generation helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from services.llm_providers.main_text_generation import llm_text_gen
|
||||
|
||||
from .base import StoryServiceBase
|
||||
|
||||
|
||||
class StoryOutlineMixin(StoryServiceBase):
|
||||
"""Provides outline generation behaviour."""
|
||||
|
||||
def _get_outline_schema(self) -> Dict[str, Any]:
|
||||
"""Return JSON schema for structured story outlines."""
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scenes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scene_number": {"type": "integer"},
|
||||
"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": ["scene_number", "title", "description", "image_prompt", "audio_narration"],
|
||||
},
|
||||
}
|
||||
},
|
||||
"required": ["scenes"],
|
||||
}
|
||||
|
||||
def generate_outline(
|
||||
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,
|
||||
ending_preference: str,
|
||||
user_id: str,
|
||||
use_structured_output: bool = True,
|
||||
) -> Any:
|
||||
"""Generate a story outline with optional structured JSON output."""
|
||||
persona_prompt = self.build_persona_prompt(
|
||||
persona,
|
||||
story_setting,
|
||||
character_input,
|
||||
plot_elements,
|
||||
writing_style,
|
||||
story_tone,
|
||||
narrative_pov,
|
||||
audience_age_group,
|
||||
content_rating,
|
||||
ending_preference,
|
||||
)
|
||||
|
||||
parameter_guidance = self._get_parameter_interaction_guidance(
|
||||
writing_style, story_tone, audience_age_group, content_rating
|
||||
)
|
||||
|
||||
outline_prompt = f"""\
|
||||
{persona_prompt}
|
||||
|
||||
**PREMISE:**
|
||||
{premise}
|
||||
|
||||
{parameter_guidance}
|
||||
|
||||
**YOUR TASK:**
|
||||
Create a detailed story outline with multiple scenes that brings this premise to life. The outline must perfectly align with ALL of the story setup parameters provided above.
|
||||
|
||||
**SCENE PROGRESSION STRUCTURE:**
|
||||
|
||||
**Scene 1-2 (Opening):**
|
||||
- Introduce the setting ({story_setting}) and main characters ({character_input})
|
||||
- Establish the {story_tone} tone from the beginning
|
||||
- Set up the main conflict or adventure based on the plot elements ({plot_elements})
|
||||
- Hook the audience with an engaging opening that matches {writing_style} style
|
||||
- Use the {narrative_pov} perspective to establish the story world
|
||||
- Create intrigue and interest appropriate for {audience_age_group}
|
||||
- Respect the {content_rating} content rating from the start
|
||||
|
||||
**Scene 3-7 (Development):**
|
||||
- Develop the plot elements ({plot_elements}) in detail
|
||||
- Build character relationships and growth using the specified characters ({character_input})
|
||||
- Create tension, obstacles, or challenges that advance the story
|
||||
- Maintain the {writing_style} style consistently throughout
|
||||
- Progress toward the {ending_preference} ending
|
||||
- Explore the setting ({story_setting}) more deeply
|
||||
- Ensure all content is age-appropriate for {audience_age_group}
|
||||
- Maintain the {story_tone} tone while developing the plot
|
||||
- Respect the {content_rating} content rating in all scenes
|
||||
- Use the {narrative_pov} perspective consistently
|
||||
|
||||
**Final Scenes (Resolution):**
|
||||
- Resolve the main conflict established in the plot elements ({plot_elements})
|
||||
- Deliver the {ending_preference} ending
|
||||
- Tie together all plot elements and character arcs
|
||||
- Provide satisfying closure appropriate for {audience_age_group}
|
||||
- Maintain the {writing_style} style and {story_tone} tone until the end
|
||||
- Ensure the ending respects the {content_rating} content rating
|
||||
- Use the {narrative_pov} perspective to conclude the story
|
||||
|
||||
**OUTLINE STRUCTURE:**
|
||||
For each scene, provide:
|
||||
1. **Scene Number and Title**
|
||||
2. **Description** (written in {writing_style}, maintaining {story_tone}, and age-appropriate for {audience_age_group})
|
||||
3. **Image Prompt** (vivid, visually descriptive, includes setting/characters, age-appropriate)
|
||||
4. **Audio Narration** (2-3 sentences, engaging, maintains style/tone, suitable for narration)
|
||||
5. **Character Descriptions** (for characters appearing in the scene)
|
||||
6. **Key Events** (bullet list of important happenings)
|
||||
|
||||
**CONTEXT INTEGRATION REQUIREMENTS:**
|
||||
- Ensure every scene reflects the setting ({story_setting})
|
||||
- Keep characters consistent with ({character_input})
|
||||
- Integrate plot elements ({plot_elements}) logically
|
||||
- Maintain persona voice ({persona})
|
||||
- Respect audience age group ({audience_age_group}) and content rating ({content_rating})
|
||||
|
||||
Before finalizing, verify that every scene adheres to the writing style, tone, age appropriateness, content rating, and narrative POV. Create 5-10 scenes that tell a complete, engaging story with clear progression and satisfying resolution.
|
||||
"""
|
||||
|
||||
try:
|
||||
if use_structured_output:
|
||||
outline_schema = self._get_outline_schema()
|
||||
try:
|
||||
response = self.load_json_response(
|
||||
llm_text_gen(prompt=outline_prompt, json_struct=outline_schema, user_id=user_id)
|
||||
)
|
||||
scenes = response.get("scenes", [])
|
||||
if scenes:
|
||||
logger.info(f"[StoryWriter] Generated {len(scenes)} structured scenes for user {user_id}")
|
||||
logger.info(
|
||||
"[StoryWriter] Outline generated with parameters: "
|
||||
f"audience={audience_age_group}, style={writing_style}, tone={story_tone}"
|
||||
)
|
||||
return scenes
|
||||
logger.warning("[StoryWriter] No scenes found in structured output, falling back to text parsing")
|
||||
raise ValueError("No scenes found in structured output")
|
||||
except (json.JSONDecodeError, ValueError, KeyError) as exc:
|
||||
logger.warning(
|
||||
f"[StoryWriter] Failed to parse structured JSON outline ({exc}), falling back to text parsing"
|
||||
)
|
||||
return self._parse_text_outline(outline_prompt, user_id)
|
||||
|
||||
outline = self.generate_with_retry(outline_prompt, user_id=user_id)
|
||||
return outline.strip()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.error(f"Outline Generation Error: {exc}")
|
||||
raise RuntimeError(f"Failed to generate outline: {exc}") from exc
|
||||
|
||||
273
backend/services/story_writer/service_components/setup.py
Normal file
273
backend/services/story_writer/service_components/setup.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""Story setup generation helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from .base import StoryServiceBase
|
||||
|
||||
|
||||
class StorySetupMixin(StoryServiceBase):
|
||||
"""Provides story setup generation behaviour."""
|
||||
|
||||
def generate_premise(
|
||||
self,
|
||||
*,
|
||||
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,
|
||||
ending_preference: str,
|
||||
user_id: str,
|
||||
) -> str:
|
||||
"""Generate a story premise."""
|
||||
persona_prompt = self.build_persona_prompt(
|
||||
persona,
|
||||
story_setting,
|
||||
character_input,
|
||||
plot_elements,
|
||||
writing_style,
|
||||
story_tone,
|
||||
narrative_pov,
|
||||
audience_age_group,
|
||||
content_rating,
|
||||
ending_preference,
|
||||
)
|
||||
|
||||
parameter_guidance = self._get_parameter_interaction_guidance(
|
||||
writing_style, story_tone, audience_age_group, content_rating
|
||||
)
|
||||
|
||||
premise_prompt = f"""\
|
||||
{persona_prompt}
|
||||
|
||||
{parameter_guidance}
|
||||
|
||||
**TASK: Write a SINGLE, BRIEF premise sentence (1-2 sentences maximum, approximately 20-40 words) for this story.**
|
||||
|
||||
The premise MUST:
|
||||
1. Be written in the specified {writing_style} writing style
|
||||
- Interpret and apply this style appropriately for {audience_age_group}
|
||||
- Match the language complexity, sentence structure, and narrative approach of this style
|
||||
2. Match the {story_tone} story tone exactly
|
||||
- Express the emotional atmosphere and mood indicated by this tone
|
||||
- Ensure the tone is age-appropriate for {audience_age_group}
|
||||
3. Be appropriate for {audience_age_group} with {content_rating} content rating
|
||||
- Use language complexity that matches this audience's reading level
|
||||
- Use vocabulary that is understandable to this age group
|
||||
- Present concepts that are relatable and explainable to this audience
|
||||
- Respect the {content_rating} content rating boundaries
|
||||
4. Briefly describe the story elements:
|
||||
- Setting: {story_setting}
|
||||
- Characters: {character_input}
|
||||
- Main plot: {plot_elements}
|
||||
5. Be clear, engaging, and set up the story without telling the whole story
|
||||
6. Be written from the {narrative_pov} point of view
|
||||
7. Set up for a {ending_preference} ending
|
||||
|
||||
**CRITICAL: This is a PREMISE, not the full story.**
|
||||
- Keep it to 1-2 sentences maximum (approximately 20-40 words)
|
||||
- Do NOT write the full story or multiple paragraphs
|
||||
- Do NOT reveal the resolution or ending
|
||||
- Focus on the setup: who, where, and what the main challenge/adventure is
|
||||
- Use ALL story setup parameters to guide your language and content choices
|
||||
- Tailor every word to the target audience ({audience_age_group}) and writing style ({writing_style})
|
||||
|
||||
Write ONLY the premise sentence(s). Do not write anything else.
|
||||
"""
|
||||
|
||||
try:
|
||||
premise = self.generate_with_retry(premise_prompt, user_id=user_id).strip()
|
||||
sentences = premise.split(". ")
|
||||
if len(sentences) > 2:
|
||||
premise = ". ".join(sentences[:2])
|
||||
if not premise.endswith("."):
|
||||
premise += "."
|
||||
return premise
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.error(f"Premise Generation Error: {exc}")
|
||||
raise RuntimeError(f"Failed to generate premise: {exc}") from exc
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Setup options
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _build_setup_schema(self) -> Dict[str, Any]:
|
||||
"""Return JSON schema for structured setup options."""
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"persona": {"type": "string"},
|
||||
"story_setting": {"type": "string"},
|
||||
"character_input": {"type": "string"},
|
||||
"plot_elements": {"type": "string"},
|
||||
"writing_style": {"type": "string"},
|
||||
"story_tone": {"type": "string"},
|
||||
"narrative_pov": {"type": "string"},
|
||||
"audience_age_group": {"type": "string"},
|
||||
"content_rating": {"type": "string"},
|
||||
"ending_preference": {"type": "string"},
|
||||
"story_length": {"type": "string"},
|
||||
"premise": {"type": "string"},
|
||||
"reasoning": {"type": "string"},
|
||||
},
|
||||
"required": [
|
||||
"persona",
|
||||
"story_setting",
|
||||
"character_input",
|
||||
"plot_elements",
|
||||
"writing_style",
|
||||
"story_tone",
|
||||
"narrative_pov",
|
||||
"audience_age_group",
|
||||
"content_rating",
|
||||
"ending_preference",
|
||||
"story_length",
|
||||
"premise",
|
||||
"reasoning",
|
||||
],
|
||||
},
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
}
|
||||
},
|
||||
"required": ["options"],
|
||||
}
|
||||
|
||||
def generate_story_setup_options(
|
||||
self,
|
||||
*,
|
||||
story_idea: str,
|
||||
user_id: str,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Generate 3 story setup options 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']
|
||||
suggested_narrative_povs = ['First Person', 'Third Person Limited', 'Third Person Omniscient']
|
||||
suggested_audience_age_groups = ['Children (5-12)', 'Young Adults (13-17)', 'Adults (18+)', 'All Ages']
|
||||
suggested_content_ratings = ['G', 'PG', 'PG-13', 'R']
|
||||
suggested_ending_preferences = ['Happy', 'Tragic', 'Cliffhanger', 'Twist', 'Open-ended', 'Bittersweet']
|
||||
|
||||
setup_prompt = f"""\
|
||||
You are an expert story writer and creative writing assistant. A 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.
|
||||
|
||||
**CRITICAL - Creative Freedom:**
|
||||
- You have COMPLETE FREEDOM to craft personalized values that best fit the user's story idea
|
||||
- Do NOT limit yourself to predefined options - create custom, creative values that perfectly match the story concept
|
||||
- For example, if the user wants "a story about how stars are made for a 5-year-old", you might create:
|
||||
- Writing Style: "Educational Playful" or "Simple Scientific" (not just "Casual" or "Poetic")
|
||||
- Story Tone: "Wonder-filled" or "Curious Discovery" (not just "Whimsical" or "Uplifting")
|
||||
- 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:
|
||||
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
|
||||
4. Include key plot elements that drive the narrative
|
||||
5. Create CUSTOM, PERSONALIZED values for writing style, story tone, narrative POV, audience age group, content rating, and ending preference that best serve the story idea
|
||||
6. Select an appropriate story length: "Short (>1000 words)" for brief stories, "Medium (>5000 words)" for standard-length stories, or "Long (>10000 words)" for extended, detailed stories
|
||||
7. Generate a brief story premise (1-2 sentences, approximately 20-40 words) that summarizes the story concept
|
||||
8. Provide a brief reasoning (2-3 sentences) explaining why this setup works well for the story idea
|
||||
|
||||
**IMPORTANT - Premise Requirements:**
|
||||
- The premise MUST be age-appropriate for the selected audience_age_group
|
||||
- For Children (5-12): Use simple, everyday words. Avoid complex vocabulary like "nebular", "ionized", "cosmic", "stellar", "melancholic", "bittersweet"
|
||||
- The premise MUST match the selected writing_style (e.g., if custom style is "Educational Playful", use playful educational language)
|
||||
- The premise MUST match the selected story_tone (e.g., if custom tone is "Wonder-filled", create a sense of wonder)
|
||||
- Keep the premise to 1-2 sentences maximum
|
||||
- Focus on who, where, and what the main challenge/adventure is
|
||||
|
||||
**Suggested Options (for reference only - feel free to create better custom values):**
|
||||
- Writing Styles (suggestions): {', '.join(suggested_writing_styles)}
|
||||
- Story Tones (suggestions): {', '.join(suggested_story_tones)}
|
||||
- Narrative POVs (suggestions): {', '.join(suggested_narrative_povs)}
|
||||
- Audience Age Groups (suggestions): {', '.join(suggested_audience_age_groups)}
|
||||
- Content Ratings (suggestions): {', '.join(suggested_content_ratings)}
|
||||
- Ending Preferences (suggestions): {', '.join(suggested_ending_preferences)}
|
||||
- Story Lengths: "Short (>1000 words)", "Medium (>5000 words)", "Long (>10000 words)"
|
||||
|
||||
**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.
|
||||
"""
|
||||
|
||||
setup_schema = self._build_setup_schema()
|
||||
|
||||
try:
|
||||
logger.info(f"[StoryWriter] Generating story setup options 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]
|
||||
|
||||
for idx, option in enumerate(options):
|
||||
if not option.get("premise") or not option.get("premise", "").strip():
|
||||
logger.info(f"[StoryWriter] Generating premise for option {idx + 1}")
|
||||
try:
|
||||
option["premise"] = self.generate_premise(
|
||||
persona=option.get("persona", ""),
|
||||
story_setting=option.get("story_setting", ""),
|
||||
character_input=option.get("character_input", ""),
|
||||
plot_elements=option.get("plot_elements", ""),
|
||||
writing_style=option.get("writing_style", "Narrative"),
|
||||
story_tone=option.get("story_tone", "Adventurous"),
|
||||
narrative_pov=option.get("narrative_pov", "Third Person Limited"),
|
||||
audience_age_group=option.get("audience_age_group", "All Ages"),
|
||||
content_rating=option.get("content_rating", "G"),
|
||||
ending_preference=option.get("ending_preference", "Happy"),
|
||||
user_id=user_id,
|
||||
)
|
||||
except Exception as exc: # pragma: no cover - fallback clause
|
||||
logger.warning(f"[StoryWriter] Failed to generate premise for option {idx + 1}: {exc}")
|
||||
option["premise"] = (
|
||||
f"A {option.get('story_setting', 'story')} story featuring "
|
||||
f"{option.get('character_input', 'characters')}."
|
||||
)
|
||||
else:
|
||||
premise = option["premise"].strip()
|
||||
sentences = premise.split(". ")
|
||||
if len(sentences) > 2:
|
||||
premise = ". ".join(sentences[:2])
|
||||
if not premise.endswith("."):
|
||||
premise += "."
|
||||
option["premise"] = premise
|
||||
|
||||
logger.info(f"[StoryWriter] Generated {len(options)} story setup options with premises for user {user_id}")
|
||||
return options
|
||||
except HTTPException:
|
||||
raise
|
||||
except json.JSONDecodeError as exc:
|
||||
logger.error(f"[StoryWriter] Failed to parse JSON response for story setup: {exc}")
|
||||
raise RuntimeError(f"Failed to parse story setup options: {exc}") from exc
|
||||
except Exception as exc:
|
||||
logger.error(f"[StoryWriter] Error generating story setup options: {exc}")
|
||||
raise RuntimeError(f"Failed to generate story setup options: {exc}") from exc
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
"""Story content generation helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from services.story_writer.image_generation_service import StoryImageGenerationService
|
||||
|
||||
from .base import StoryServiceBase
|
||||
from .outline import StoryOutlineMixin
|
||||
|
||||
|
||||
class StoryContentMixin(StoryOutlineMixin):
|
||||
"""Provides story drafting and continuation behaviour."""
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Story start
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def generate_story_start(
|
||||
self,
|
||||
*,
|
||||
premise: str,
|
||||
outline: 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,
|
||||
ending_preference: str,
|
||||
story_length: str = "Medium",
|
||||
user_id: str,
|
||||
) -> str:
|
||||
"""Generate the starting section (or full short story)."""
|
||||
persona_prompt = self.build_persona_prompt(
|
||||
persona,
|
||||
story_setting,
|
||||
character_input,
|
||||
plot_elements,
|
||||
writing_style,
|
||||
story_tone,
|
||||
narrative_pov,
|
||||
audience_age_group,
|
||||
content_rating,
|
||||
ending_preference,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
if is_short_story:
|
||||
logger.info(f"[StoryWriter] Generating complete short story (~1000 words) in single call for user {user_id}")
|
||||
short_story_prompt = f"""\
|
||||
{persona_prompt}
|
||||
|
||||
You have a gripping premise in mind:
|
||||
|
||||
{premise}
|
||||
|
||||
Your imagination has crafted a rich narrative outline:
|
||||
|
||||
{outline_text}
|
||||
|
||||
**YOUR TASK:**
|
||||
Write the COMPLETE story from beginning to end. This is a SHORT story, so you need to write the entire narrative in a single response.
|
||||
|
||||
**STORY LENGTH TARGET:**
|
||||
- Target: Approximately 1000 words (900-1100 words acceptable)
|
||||
- This is a SHORT story, so be concise but complete
|
||||
- Cover all key scenes from your outline
|
||||
- Provide a satisfying conclusion that addresses all plot elements
|
||||
- Ensure the story makes sense as a complete narrative
|
||||
|
||||
**STORY STRUCTURE:**
|
||||
1. **Opening**: Establish setting, characters, and initial situation
|
||||
2. **Development**: Develop the plot, introduce conflicts, build tension
|
||||
3. **Climax**: Reach the story's peak moment
|
||||
4. **Resolution**: Resolve conflicts and provide closure
|
||||
|
||||
**IMPORTANT INSTRUCTIONS:**
|
||||
- Write the COMPLETE story in this single response
|
||||
- Aim for approximately 1000 words (900-1100 words)
|
||||
- Ensure the story is complete and makes sense as a standalone narrative
|
||||
- Include all essential elements from your outline
|
||||
- Provide a satisfying ending that matches the ending preference: {ending_preference}
|
||||
- Do NOT leave the story incomplete - this is the only generation call for short stories
|
||||
- Once you've finished the complete story, conclude naturally - do NOT write IAMDONE
|
||||
|
||||
**WRITING STYLE:**
|
||||
{self.guidelines}
|
||||
|
||||
**REMEMBER:**
|
||||
- This is a SHORT story - be concise but complete
|
||||
- Write the ENTIRE story in this response
|
||||
- Aim for ~1000 words
|
||||
- Ensure the story is complete and satisfying
|
||||
- Cover all key elements from your outline
|
||||
"""
|
||||
try:
|
||||
complete_story = self.generate_with_retry(short_story_prompt, user_id=user_id)
|
||||
complete_story = complete_story.replace("IAMDONE", "").strip()
|
||||
logger.info(
|
||||
f"[StoryWriter] Generated complete short story ({len(complete_story.split())} words) for user {user_id}"
|
||||
)
|
||||
return complete_story
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.error(f"Short Story Generation Error: {exc}")
|
||||
raise RuntimeError(f"Failed to generate short story: {exc}") from exc
|
||||
|
||||
initial_word_count, _ = self._get_story_length_guidance(story_length)
|
||||
|
||||
starting_prompt = f"""\
|
||||
{persona_prompt}
|
||||
|
||||
You have a gripping premise in mind:
|
||||
|
||||
{premise}
|
||||
|
||||
Your imagination has crafted a rich narrative outline:
|
||||
|
||||
{outline_text}
|
||||
|
||||
First, silently review the outline and the premise. Consider how to start the story.
|
||||
|
||||
Start to write the very beginning of the story. You are not expected to finish
|
||||
the whole story now. Your writing should be detailed enough that you are only
|
||||
scratching the surface of the first bullet of your outline. Try to write AT
|
||||
MINIMUM {initial_word_count} WORDS.
|
||||
|
||||
**STORY LENGTH TARGET:**
|
||||
This story is targeted to be {story_length}. Write with appropriate detail and pacing
|
||||
to reach this target length across the entire story. For this initial section, focus
|
||||
on establishing the setting, characters, and beginning of the plot in {initial_word_count} words.
|
||||
|
||||
{self.guidelines}
|
||||
"""
|
||||
|
||||
try:
|
||||
starting_draft = self.generate_with_retry(starting_prompt, user_id=user_id)
|
||||
return starting_draft.strip()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.error(f"Story Start Generation Error: {exc}")
|
||||
raise RuntimeError(f"Failed to generate story start: {exc}") from exc
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Continuation
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def continue_story(
|
||||
self,
|
||||
*,
|
||||
premise: str,
|
||||
outline: Any,
|
||||
story_text: 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,
|
||||
ending_preference: str,
|
||||
story_length: str = "Medium",
|
||||
user_id: str,
|
||||
) -> str:
|
||||
"""Continue writing the story."""
|
||||
persona_prompt = self.build_persona_prompt(
|
||||
persona,
|
||||
story_setting,
|
||||
character_input,
|
||||
plot_elements,
|
||||
writing_style,
|
||||
story_tone,
|
||||
narrative_pov,
|
||||
audience_age_group,
|
||||
content_rating,
|
||||
ending_preference,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
story_length_lower = story_length.lower()
|
||||
if "short" in story_length_lower or "1000" in story_length_lower:
|
||||
# Safety check: short stories shouldn't reach here
|
||||
return "IAMDONE"
|
||||
|
||||
if "long" in story_length_lower or "10000" in story_length_lower:
|
||||
target_total_words = 10000
|
||||
else:
|
||||
target_total_words = 4500
|
||||
|
||||
buffer_target = int(target_total_words * 1.05)
|
||||
|
||||
if current_word_count >= buffer_target:
|
||||
logger.info(
|
||||
f"[StoryWriter] Word count ({current_word_count}) at or past buffer target ({buffer_target}). Story is complete."
|
||||
)
|
||||
return "IAMDONE"
|
||||
|
||||
if current_word_count >= target_total_words and (current_word_count - target_total_words) < 50:
|
||||
logger.info(
|
||||
f"[StoryWriter] Word count ({current_word_count}) is very close to target ({target_total_words}). Story is complete."
|
||||
)
|
||||
return "IAMDONE"
|
||||
|
||||
remaining_words = max(0, buffer_target - current_word_count)
|
||||
if remaining_words < 50:
|
||||
logger.info(f"[StoryWriter] Remaining words ({remaining_words}) are minimal. Story is complete.")
|
||||
return "IAMDONE"
|
||||
|
||||
continuation_prompt = f"""\
|
||||
{persona_prompt}
|
||||
|
||||
You have a gripping premise in mind:
|
||||
|
||||
{premise}
|
||||
|
||||
Your imagination has crafted a rich narrative outline:
|
||||
|
||||
{outline_text}
|
||||
|
||||
You've begun to immerse yourself in this world, and the words are flowing.
|
||||
Here's what you've written so far:
|
||||
|
||||
{story_text}
|
||||
|
||||
=====
|
||||
|
||||
First, silently review the outline and story so far. Identify what the single
|
||||
next part of your outline you should write.
|
||||
|
||||
Your task is to continue where you left off and write the next part of the story.
|
||||
You are not expected to finish the whole story now. Your writing should be
|
||||
detailed enough that you are only scratching the surface of the next part of
|
||||
your outline. Try to write AT MINIMUM {continuation_word_count} WORDS.
|
||||
|
||||
**STORY LENGTH TARGET:**
|
||||
This story is targeted to be {story_length} (target: {target_total_words} words total, with 5% buffer allowed).
|
||||
You have written approximately {current_word_count} words so far, leaving approximately
|
||||
{remaining_words} words remaining.
|
||||
|
||||
**CRITICAL INSTRUCTIONS - READ CAREFULLY:**
|
||||
1. Write the next section with appropriate detail, aiming for approximately {min(continuation_word_count, remaining_words)} words.
|
||||
2. **STOP CONDITION:** If after writing this continuation, the total word count will reach or exceed {target_total_words} words, you MUST conclude the story immediately by writing IAMDONE.
|
||||
3. The story should reach a natural conclusion that addresses all plot elements and provides satisfying closure.
|
||||
4. Once you've written IAMDONE, do NOT write any more content - stop immediately.
|
||||
|
||||
**WORD COUNT LIMIT:**
|
||||
- Target: {target_total_words} words total (with 5% buffer: {int(target_total_words * 1.05)} words maximum)
|
||||
- Current word count: {current_word_count} words
|
||||
- Remaining words: {remaining_words} words
|
||||
- **CRITICAL: If your continuation would bring the total to {target_total_words} words or more, conclude the story NOW and write IAMDONE.**
|
||||
- **Do NOT exceed {int(target_total_words * 1.05)} words. This is a hard limit.**
|
||||
- **Ensure the story is complete and makes sense when you write IAMDONE.**
|
||||
|
||||
{self.guidelines}
|
||||
"""
|
||||
|
||||
try:
|
||||
continuation = self.generate_with_retry(continuation_prompt, user_id=user_id)
|
||||
return continuation.strip()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.error(f"Story Continuation Error: {exc}")
|
||||
raise RuntimeError(f"Failed to continue story: {exc}") from exc
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Full generation orchestration
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def generate_full_story(
|
||||
self,
|
||||
*,
|
||||
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,
|
||||
ending_preference: str,
|
||||
user_id: str,
|
||||
max_iterations: int = 10,
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate a complete story using prompt chaining."""
|
||||
try:
|
||||
logger.info(f"[StoryWriter] Generating premise for user {user_id}")
|
||||
premise = self.generate_premise(
|
||||
persona=persona,
|
||||
story_setting=story_setting,
|
||||
character_input=character_input,
|
||||
plot_elements=plot_elements,
|
||||
writing_style=writing_style,
|
||||
story_tone=story_tone,
|
||||
narrative_pov=narrative_pov,
|
||||
audience_age_group=audience_age_group,
|
||||
content_rating=content_rating,
|
||||
ending_preference=ending_preference,
|
||||
user_id=user_id,
|
||||
)
|
||||
if not premise:
|
||||
raise RuntimeError("Failed to generate premise")
|
||||
|
||||
logger.info(f"[StoryWriter] Generating outline for user {user_id}")
|
||||
outline = self.generate_outline(
|
||||
premise=premise,
|
||||
persona=persona,
|
||||
story_setting=story_setting,
|
||||
character_input=character_input,
|
||||
plot_elements=plot_elements,
|
||||
writing_style=writing_style,
|
||||
story_tone=story_tone,
|
||||
narrative_pov=narrative_pov,
|
||||
audience_age_group=audience_age_group,
|
||||
content_rating=content_rating,
|
||||
ending_preference=ending_preference,
|
||||
user_id=user_id,
|
||||
)
|
||||
if not outline:
|
||||
raise RuntimeError("Failed to generate outline")
|
||||
|
||||
logger.info(f"[StoryWriter] Generating story start for user {user_id}")
|
||||
draft = self.generate_story_start(
|
||||
premise=premise,
|
||||
outline=outline,
|
||||
persona=persona,
|
||||
story_setting=story_setting,
|
||||
character_input=character_input,
|
||||
plot_elements=plot_elements,
|
||||
writing_style=writing_style,
|
||||
story_tone=story_tone,
|
||||
narrative_pov=narrative_pov,
|
||||
audience_age_group=audience_age_group,
|
||||
content_rating=content_rating,
|
||||
ending_preference=ending_preference,
|
||||
user_id=user_id,
|
||||
)
|
||||
if not draft:
|
||||
raise RuntimeError("Failed to generate story start")
|
||||
|
||||
iteration = 0
|
||||
while "IAMDONE" not in draft and iteration < max_iterations:
|
||||
iteration += 1
|
||||
logger.info(f"[StoryWriter] Continuation iteration {iteration}/{max_iterations}")
|
||||
continuation = self.continue_story(
|
||||
premise=premise,
|
||||
outline=outline,
|
||||
story_text=draft,
|
||||
persona=persona,
|
||||
story_setting=story_setting,
|
||||
character_input=character_input,
|
||||
plot_elements=plot_elements,
|
||||
writing_style=writing_style,
|
||||
story_tone=story_tone,
|
||||
narrative_pov=narrative_pov,
|
||||
audience_age_group=audience_age_group,
|
||||
content_rating=content_rating,
|
||||
ending_preference=ending_preference,
|
||||
user_id=user_id,
|
||||
)
|
||||
if continuation:
|
||||
draft += "\n\n" + continuation
|
||||
else:
|
||||
logger.warning(f"[StoryWriter] Empty continuation at iteration {iteration}")
|
||||
break
|
||||
|
||||
final_story = draft.replace("IAMDONE", "").strip()
|
||||
|
||||
outline_response = outline
|
||||
if isinstance(outline, list):
|
||||
outline_response = "\n".join(
|
||||
[
|
||||
f"Scene {scene.get('scene_number', idx + 1)}: {scene.get('title', 'Untitled')}\n"
|
||||
f" {scene.get('description', '')}"
|
||||
for idx, scene in enumerate(outline)
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
"premise": premise,
|
||||
"outline": str(outline_response),
|
||||
"story": final_story,
|
||||
"iterations": iteration,
|
||||
"is_complete": "IAMDONE" in draft or iteration >= max_iterations,
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.error(f"[StoryWriter] Error generating full story: {exc}")
|
||||
raise RuntimeError(f"Failed to generate full story: {exc}") from exc
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Multimedia helpers
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def generate_scene_images(
|
||||
self,
|
||||
*,
|
||||
scenes: List[Dict[str, Any]],
|
||||
user_id: str,
|
||||
provider: Optional[str] = None,
|
||||
width: int = 1024,
|
||||
height: int = 1024,
|
||||
model: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Generate images for story scenes."""
|
||||
image_service = StoryImageGenerationService()
|
||||
return image_service.generate_scene_images(
|
||||
scenes=scenes, user_id=user_id, provider=provider, width=width, height=height, model=model
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user