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:
ajaysi
2025-11-13 16:14:26 +05:30
parent 7191c7e7f0
commit 3b9356e2c8
124 changed files with 20055 additions and 1208 deletions

View 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",
]

View 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)}")

View 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

View 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

View File

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