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