From 265b8934a0deabd54ada5b318e03307d653a489c Mon Sep 17 00:00:00 2001 From: Mark Randall Havens Date: Thu, 12 Mar 2026 19:52:47 +0000 Subject: [PATCH] Add multiple story frameworks - Snowflake Method (already had) - Three-Act Structure - Save the Cat (Blake Snyder) - Hero's Journey (Joseph Campbell) - Story Circle (Dan Harmon) - The 7-Point Plot (The Pantone) - Fichtean Curve Pass framework='hero-journey' or 'save-the-cat' etc. to use. --- opus_orchestrator/frameworks.py | 278 ++++++++++++++++++++++++++++++ opus_orchestrator/orchestrator.py | 126 +++++++++----- 2 files changed, 359 insertions(+), 45 deletions(-) create mode 100644 opus_orchestrator/frameworks.py diff --git a/opus_orchestrator/frameworks.py b/opus_orchestrator/frameworks.py new file mode 100644 index 0000000..1a29021 --- /dev/null +++ b/opus_orchestrator/frameworks.py @@ -0,0 +1,278 @@ +"""Story frameworks for Opus Orchestrator. + +Implements multiple narrative structure frameworks: +- Snowflake Method +- Three-Act Structure +- Save the Cat (Blake Snyder) +- Hero's Journey (Joseph Campbell) +- Story Circle (Dan Harmon) +- The 7-Point Plot (The Pantone) +- Fichtean Curve +""" + +from enum import Enum +from typing import Any + + +class StoryFramework(str, Enum): + """Available story frameworks.""" + + SNOWFLAKE = "snowflake" + THREE_ACT = "three-act" + SAVE_THE_CAT = "save-the-cat" + HERO_JOURNEY = "hero-journey" + STORY_CIRCLE = "story-circle" + SEVEN_POINT = "seven-point" + FICHTEAN = "fichtean" + + +# Framework descriptions and prompts + +FRAMEWORKS = { + StoryFramework.SNOWFLAKE: { + "name": "Snowflake Method", + "description": "Fractal expansion from sentence to paragraph to full novel", + "stages": [ + "One sentence summary", + "One paragraph outline", + "Character sheets", + "Four-page outline", + "Detailed character charts", + "Scene list", + "Scene descriptions", + ], + }, + + StoryFramework.THREE_ACT: { + "name": "Three-Act Structure", + "description": "Classic setup, confrontation, resolution structure", + "stages": [ + "Act I: Setup - Introduce protagonist, status quo, inciting incident", + "Act I: Break into Two - Commit to journey", + "Act II, Part 1: Fun and Games - Promise of premise", + "Act II, Part 1: Midpoint - Stakes raise", + "Act II, Part 2: Bad Guys Close In - External pressure", + "Act II, Part 2: All Is Lost - Lowest point", + "Act III: Finale - Climax and resolution", + ], + }, + + StoryFramework.SAVE_THE_CAT: { + "name": "Save the Cat", + "description": "Blake Snyder's 15-beat screenwriting structure", + "beats": [ + ("Opening Image", "Visual hook that sets the tone"), + ("Theme Stated", "Someone states the theme"), + ("Setup", "Normal world, introduce key players"), + ("Catalyst", "Life-changing event"), + ("Debate", "Protagonist resists the call"), + ("Break Into Two", "Commit to the journey"), + ("B Story", "Subplot begins"), + ("Fun and Games", "Promise of the premise"), + ("Midpoint", "Stakes are raised"), + ("Bad Guys Close In", "External pressure increases"), + ("All Is Lost", "Darkest moment"), + ("Dark Night of the Soul", "Emotional nadir"), + ("Break Into Three", "Solution found"), + ("Finale", "Final confrontation"), + ("Final Image", "Changed world"), + ], + }, + + StoryFramework.HERO_JOURNEY: { + "name": "Hero's Journey", + "description": "Joseph Campbell's monomyth - 12 stages", + "beats": [ + ("Ordinary World", "Hero's normal life before the adventure"), + ("Call to Adventure", "Challenge or quest presented"), + ("Refusal of the Call", "Hero hesitates or declines"), + ("Meeting the Mentor", "Guide appears with wisdom/tools"), + ("Crossing the Threshold", "Hero commits, enters special world"), + ("Tests, Allies, Enemies", "Hero faces challenges, makes friends/foes"), + ("Approach to Inmost Cave", "Hero prepares for major challenge"), + ("Ordeal", "Hero faces greatest challenge, death/rebirth"), + ("Reward", "Hero gains prize/knowledge"), + ("The Road Back", "Hero begins return journey"), + ("Resurrection", "Final test, hero transformed"), + ("Return with Elixir", "Hero returns, transformed"), + ], + }, + + StoryFramework.STORY_CIRCLE: { + "name": "Story Circle", + "description": "Dan Harmon's 8-step circular structure", + "beats": [ + ("You", "Character in their comfort zone"), + ("Need", "Character feels something is missing"), + ("Go", "Character enters unfamiliar situation"), + ("Adapt", "Character adjusts to new world"), + ("Get", "Character gains what they sought"), + ("Return", "Character returns home"), + ("Change", "Character is transformed"), + ("Result", "Character's life is different"), + ], + }, + + StoryFramework.SEVEN_POINT: { + "name": "The 7-Point Plot", + "description": "The Pantone method - 7 key plot points", + "beats": [ + ("Hook", "Opening that grabs attention"), + ("Plot Turn 1", "First major change, inciting incident"), + ("Pinch Point 1", "Pressure from antagonist"), + ("Midpoint", "Character commits to action"), + ("Pinch Point 2", "Increased pressure, stakes raise"), + ("Plot Turn 2", "Major reversal, everything changes"), + ("Resolution", "Final confrontation, new equilibrium"), + ], + }, + + StoryFramework.FICHTEAN: { + "name": "Fichtean Curve", + "description": "Progressive rising action - series of crises", + "beats": [ + ("Inciting Incident", "Chain of events begins"), + ("Rising Action 1", "First crisis"), + ("Rising Action 2", "Second crisis"), + ("Rising Action 3", "Third crisis"), + ("Climax", "Maximum tension"), + ("Falling Action", "Aftermath"), + ("Resolution", "New status quo"), + ], + }, +} + + +def get_framework_prompt(framework: StoryFramework) -> str: + """Get the system prompt for a framework.""" + + if framework == StoryFramework.SAVE_THE_CAT: + return """You are an expert in Save the Cat story structure (Blake Snyder). + +The 15 beats of Save the Cat: +1. Opening Image - Visual hook, tone setter +2. Theme Stated - Someone states the theme +3. Setup - Normal world, introduce characters +4. Catalyst - Life-changing event +5. Debate - Protagonist resists the call +6. Break Into Two - Commit to journey +7. B Story - Subplot begins +8. Fun and Games - Promise of the premise +9. Midpoint - Stakes raise +10. Bad Guys Close In - External pressure +11. All Is Lost - Darkest moment +12. Dark Night of the Soul - Emotional nadirs +13. Break Into Three - Solution found +14. Finale - Final confrontation +15. Final Image - Changed world + +Use these beats to structure compelling narratives.""" + + elif framework == StoryFramework.HERO_JOURNEY: + return """You are an expert in the Hero's Journey (Joseph Campbell's monomyth). + +The 12 stages: +1. Ordinary World - Hero's normal life +2. Call to Adventure - Challenge presented +3. Refusal of the Call - Hero hesitates +4. Meeting the Mentor - Guide appears +5. Crossing the Threshold - Enter special world +6. Tests, Allies, Enemies - Face challenges +7. Approach to Inmost Cave - Prepare for major challenge +8. Ordeal - Greatest challenge, death/rebirth +9. Reward - Gain prize/knowledge +10. The Road Back - Begin return +11. Resurrection - Final test, transformation +12. Return with Elixir - Return transformed + +Use these stages to structure mythic narratives.""" + + elif framework == StoryFramework.STORY_CIRCLE: + return """You are an expert in Dan Harmon's Story Circle. + +The 8 steps: +1. You - Character in comfort zone +2. Need - Something missing +3. Go - Enter unfamiliar situation +4. Adapt - Adjust to new world +5. Get - Gain what sought +6. Return - Return home +7. Change - Transformed +8. Result - Life different + +Use this circular structure for balanced narratives.""" + + elif framework == StoryFramework.SEVEN_POINT: + return """You are an expert in The 7-Point Plot (The Pantone). + +The 7 plot points: +1. Hook - Opening grab +2. Plot Turn 1 - First major change +3. Pinch Point 1 - Antagonist pressure +4. Midpoint - Commit to action +5. Pinch Point 2 - Stakes raise +6. Plot Turn 2 - Major reversal +7. Resolution - New equilibrium + +Use this for tight, event-driven plots.""" + + elif framework == StoryFramework.FICHTEAN: + return """You are an expert in the Fichtean Curve. + +Progressive crisis structure: +1. Inciting Incident - Chain of events begins +2. Rising Action 1 - First crisis +3. Rising Action 2 - Second crisis (bigger) +4. Rising Action 3 - Third crisis (biggest) +5. Climax - Maximum tension +6. Falling Action - Aftermath +7. Resolution - New status quo + +Use this for action-driven, tension-building narratives.""" + + elif framework == StoryFramework.THREE_ACT: + return """You are an expert in Three-Act Structure. + +Classic narrative structure: +ACT I - SETUP +- Opening Image +- Theme Stated +- Setup (normal world) +- Catalyst (inciting incident) +- Debate (protagonist resists) +- Break Into Two (commit to journey) + +ACT II - CONFRONTATION +- B Story (subplot begins) +- Fun and Games (promise of premise) +- Midpoint (stakes raise) +- Bad Guys Close In (external pressure) +- All Is Lost (lowest point) +- Dark Night of the Soul (internal reckoning) +- Break Into Three (solution found) + +ACT III - RESOLUTION +- Finale (climax) +- Final Image (changed world) + +Use this traditional structure for any genre.""" + + else: + return "You are an expert story architect." + + +def get_framework_for_genre(genre: str) -> list[StoryFramework]: + """Suggest frameworks based on genre.""" + + suggestions = { + "fantasy": [StoryFramework.HERO_JOURNEY, StoryFramework.SNOWFLAKE], + "science-fiction": [StoryFramework.SNOWFLAKE, StoryFramework.SAVE_THE_CAT], + "thriller": [StoryFramework.SAVE_THE_CAT, StoryFramework.SEVEN_POINT, StoryFramework.FICHTEAN], + "horror": [StoryFramework.HERO_JOURNEY, StoryFramework.FICHTEAN], + "romance": [StoryFramework.STORY_CIRCLE, StoryFramework.SAVE_THE_CAT], + "mystery": [StoryFramework.SEVEN_POINT, StoryFramework.THREE_ACT], + "literary": [StoryFramework.SNOWFLAKE, StoryFramework.HERO_JOURNEY], + "adventure": [StoryFramework.HERO_JOURNEY, StoryFramework.FICHTEAN], + } + + return suggestions.get(genre.lower(), [StoryFramework.THREE_ACT, StoryFramework.SNOWFLAKE]) diff --git a/opus_orchestrator/orchestrator.py b/opus_orchestrator/orchestrator.py index 653f763..9e8c7fc 100644 --- a/opus_orchestrator/orchestrator.py +++ b/opus_orchestrator/orchestrator.py @@ -1,14 +1,13 @@ -"""Main Opus Orchestrator - Snowflake Method Implementation. +"""Main Opus Orchestrator - Snowflake Method Implementation with Multiple Frameworks. -Full pipeline following the Snowflake Method for book writing: -1. One sentence (concept) -2. One paragraph (outline) -3. Character sheets -4. Four-page outline -5. Detailed character charts -6. Scene list -7. Scene descriptions -8. First draft +Full pipeline supporting multiple story frameworks: +- Snowflake Method (fractal expansion) +- Three-Act Structure +- Save the Cat (Blake Snyder) +- Hero's Journey (Joseph Campbell) +- Story Circle (Dan Harmon) +- The 7-Point Plot (The Pantone) +- Fichtean Curve """ import asyncio @@ -35,6 +34,12 @@ from opus_orchestrator.agents.nonfiction import ( ResearcherAgent, ) from opus_orchestrator.config import OpusConfig, get_config +from opus_orchestrator.frameworks import ( + StoryFramework, + FRAMEWORKS, + get_framework_for_genre, + get_framework_prompt, +) from opus_orchestrator.schemas import ( BookBlueprint, BookIntent, @@ -50,7 +55,7 @@ from opus_orchestrator.state import OpusState class OpusOrchestrator: - """Main orchestrator implementing the Snowflake Method.""" + """Main orchestrator implementing multiple story frameworks.""" def __init__( self, @@ -61,9 +66,23 @@ class OpusOrchestrator: intended_outcome: str = "complete novel", tone: Optional[str] = None, target_word_count: int = 80000, + framework: str = "snowflake", config: Optional[OpusConfig] = None, ): - """Initialize the Opus Orchestrator with Snowflake Method.""" + """Initialize the Opus Orchestrator with selectable framework. + + Args: + repo_url: GitHub URL for content + book_type: "fiction" or "nonfiction" + genre: Genre (for framework suggestions) + target_audience: Who is this for + intended_outcome: What to produce + tone: Desired tone + target_word_count: Target length + framework: Story framework to use (snowflake, three-act, save-the-cat, + hero-journey, story-circle, seven-point, fichtean) + config: Optional config override + """ self.config = config or get_config() if not self.config.agent.api_key: @@ -71,6 +90,19 @@ class OpusOrchestrator: self.book_type = BookType(book_type.lower()) self.repo_url = repo_url + + # Handle framework + if isinstance(framework, str): + try: + self.framework = StoryFramework(framework.lower()) + except ValueError: + # Default to snowflake if invalid + self.framework = StoryFramework.SNOWFLAKE + else: + self.framework = framework + + # Get framework info + self.framework_info = FRAMEWORKS.get(self.framework, FRAMEWORKS[StoryFramework.SNOWFLAKE]) self.intent = BookIntent( book_type=self.book_type, @@ -171,23 +203,19 @@ Write ONE compelling sentence that captures the entire story. return self.one_sentence async def snowflake_stage_2(self) -> str: - """Stage 2: One paragraph summary. + """Stage 2: One paragraph outline (framework-dependent). Expand the one sentence to a paragraph with setup, 3 acts, and resolution. + Uses the selected story framework. """ - print("❄️ SNOWFLAKE STAGE 2: One paragraph outline...") + print(f"❄️ SNOWFLAKE STAGE 2: One paragraph outline ({self.framework_info['name']})...") + + # Get framework-specific prompt + framework_system_prompt = get_framework_prompt(self.framework) user_prompt = f"""Expand this one-sentence summary into a full one-paragraph story outline. -Include: -- Opening image (the "before" state) -- Setup (normal world, who the protagonist is) -- Catalyst (what changes everything) -- Rising action (attempts to solve the problem) -- Midpoint (major twist or revelation) -- Complications (things get worse) -- Crisis (lowest point) -- Resolution (how it ends) +Use the {self.framework_info['name']} framework: {self.framework_info.get('description', '')} ## One sentence: {self.one_sentence} @@ -196,7 +224,7 @@ Include: Write one detailed paragraph (4-8 sentences) that tells the complete story arc. """ response = await self.agents["architect"].call_llm( - system_prompt="You are an expert story architect. Create detailed, act-structured outlines.", + system_prompt=framework_system_prompt, user_prompt=user_prompt, ) @@ -316,27 +344,31 @@ Write comprehensive, detailed character charts. return self.character_charts async def snowflake_stage_6(self) -> str: - """Stage 6: Scene list. + """Stage 6: Scene list (framework-dependent). - Create a list of all scenes needed (like index cards). + Create a list of all scenes using the selected framework. """ - print("❄️ SNOWFLAKE STAGE 6: Scene list...") + print(f"❄️ SNOWFLAKE STAGE 6: Scene list ({self.framework_info['name']})...") + + # Get framework-specific prompt + framework_system_prompt = get_framework_prompt(self.framework) words_per_scene = 1500 # Average scene length num_scenes = max(10, self.intent.target_word_count // words_per_scene) - user_prompt = f"""Create a complete SCENE LIST for this story. - -For each scene, provide: -- Scene number -- POV character -- Setting/location -- What happens (one line) -- Purpose (advances plot? reveals character? builds world?) -- Chapter placement + # Get framework beats if available + framework_beats = "" + if "beats" in self.framework_info: + framework_beats = f"\n\n## Framework Beats:\n" + for beat_name, beat_desc in self.framework_info["beats"]: + framework_beats += f"- {beat_name}: {beat_desc}\n" + + user_prompt = f"""Create a complete SCENE LIST for this story using the {self.framework_info['name']}. Target: approximately {num_scenes} scenes for a {self.intent.target_word_count:,} word novel. +{framework_beats} + ## Four-page outline: {self.four_page_outline} @@ -347,7 +379,7 @@ Target: approximately {num_scenes} scenes for a {self.intent.target_word_count:, Create a comprehensive scene list with all scenes needed. """ response = await self.agents["architect"].call_llm( - system_prompt="You are an expert story architect. Create detailed scene lists.", + system_prompt=framework_system_prompt, user_prompt=user_prompt, ) @@ -545,7 +577,7 @@ Make it vivid, engaging, and true to the characters. themes=[], tone=self.intent.tone or "neutral", chapters=[ - BookBlueprint.model_construct( + ChapterBlueprint( chapter_number=i, title=f"Chapter {i}", summary=f"Chapter {i}", @@ -595,18 +627,21 @@ Make it vivid, engaging, and true to the characters. # ========================================================================= async def run(self) -> Manuscript: - """Run the full Snowflake Method pipeline.""" + """Run the full pipeline with selected framework.""" + framework_name = self.framework_info.get("name", "Unknown") + print(f"\n{'='*60}") - print("❄️ OPUS ORCHESTRATOR - SNOWFLAKE METHOD") - print(f"{'='*60}\n") + print(f"❄️ OPUS ORCHESTRATOR - {framework_name.upper()}") + print(f"{'='*60}") + print(f"Framework: {self.framework_info.get('description', '')}\n") await self.ingest() - # Pre-writing stages (Snowflake 1-7) + # Pre-writing stages await self.snowflake_stage_1() # One sentence - await self.snowflake_stage_2() # One paragraph + await self.snowflake_stage_2() # One paragraph/outline with framework await self.snowflake_stage_3() # Character sheets - await self.snowflake_stage_4() # Four-page outline + await self.snowflake_stage_4() # Expanded outline await self.snowflake_stage_5() # Detailed character charts await self.snowflake_stage_6() # Scene list await self.snowflake_stage_7() # Scene descriptions @@ -621,11 +656,12 @@ Make it vivid, engaging, and true to the characters. manuscript = await self.compile_manuscript() print(f"\n{'='*60}") - print("✅ SNOWFLAKE COMPLETE!") + print("✅ COMPLETE!") print(f"{'='*60}") print(f"📖 Title: {manuscript.title}") print(f"📄 Words: {manuscript.total_word_count:,}") print(f"📑 Chapters: {len(manuscript.chapters)}") + print(f"🎯 Framework: {framework_name}") return manuscript