Spaces:
Build error
Build error
| """Story beat data and sequence generation. | |
| This module provides: | |
| - STORY_BEATS: Dictionary of story beats organized by format and genre | |
| - generate_beat_sequence: Function to generate story sequences based on format/genre | |
| """ | |
| import json | |
| import random | |
| # Story beats organized by narrative function | |
| STORY_BEATS = { | |
| "setup": [ | |
| ("Ordinary World", "Establish the protagonist's normal life before the adventure"), | |
| ("Meet the Hero", "Introduction to protagonist with defining character moment"), | |
| ("Establish Stakes", "Show what the protagonist stands to lose"), | |
| ("The Want", "Protagonist expresses a desire or goal"), | |
| ("The Flaw", "Hint at protagonist's weakness that must be overcome"), | |
| ("Supporting Cast", "Introduce key allies and their relationships"), | |
| ], | |
| "catalyst": [ | |
| ("Call to Adventure", "External event disrupts the ordinary world"), | |
| ("Inciting Incident", "Something happens that demands a response"), | |
| ("The Invitation", "Opportunity presents itself to the protagonist"), | |
| ("The Challenge", "Direct challenge forces protagonist to act"), | |
| ("Bad News", "Devastating information changes everything"), | |
| ("The Stranger", "New character brings change or information"), | |
| ], | |
| "debate": [ | |
| ("Refusal of Call", "Protagonist hesitates or refuses the challenge"), | |
| ("The Doubt", "Protagonist questions their ability"), | |
| ("Seeking Advice", "Protagonist consults mentor or wise figure"), | |
| ("Weighing Options", "Protagonist considers paths forward"), | |
| ("The Warning", "Someone cautions against the journey"), | |
| ("Internal Conflict", "Protagonist struggles with competing desires"), | |
| ], | |
| "rising_action": [ | |
| ("Crossing Threshold", "Protagonist commits to the journey"), | |
| ("New World Rules", "Protagonist learns how this new world works"), | |
| ("Tests and Allies", "Series of challenges, gaining companions"), | |
| ("Fun and Games", "Promise of the premise fulfilled"), | |
| ("Skill Building", "Protagonist develops abilities needed for climax"), | |
| ("B-Story Develops", "Secondary plot line advances"), | |
| ("Approaching Cave", "Preparing for the major challenge"), | |
| ], | |
| "midpoint": [ | |
| ("False Victory", "Apparent success that will prove hollow"), | |
| ("False Defeat", "Apparent failure that motivates comeback"), | |
| ("Major Revelation", "Game-changing information revealed"), | |
| ("Point of No Return", "Commitment that cannot be undone"), | |
| ("Raised Stakes", "The cost of failure increases dramatically"), | |
| ("New Goal", "Original objective shifts to something bigger"), | |
| ], | |
| "complications": [ | |
| ("Bad Guys Close In", "Opposition intensifies, allies scatter"), | |
| ("Betrayal", "Trusted ally reveals true colors"), | |
| ("All Is Lost", "Lowest point - everything seems hopeless"), | |
| ("Death Moment", "Literal or symbolic death experience"), | |
| ("Dark Night", "Protagonist faces their deepest fears"), | |
| ("Whiff of Death", "Mortality or failure becomes very real"), | |
| ], | |
| "climax": [ | |
| ("Gathering the Team", "Final assembly before the battle"), | |
| ("The Plan", "Strategy for the final confrontation revealed"), | |
| ("Storming the Castle", "Direct assault on the antagonist"), | |
| ("Final Battle", "Climactic confrontation begins"), | |
| ("High Tower Surprise", "Unexpected complication in the climax"), | |
| ("The Dig Deep", "Protagonist must use everything they've learned"), | |
| ("Victory/Defeat", "The outcome of the main conflict"), | |
| ], | |
| "resolution": [ | |
| ("New Equilibrium", "World rebalanced after the adventure"), | |
| ("Character Changed", "Demonstrate protagonist's transformation"), | |
| ("Reward", "Protagonist receives what they've earned"), | |
| ("Return Home", "Protagonist returns to ordinary world, changed"), | |
| ("Open Loop", "Hint at future adventures"), | |
| ("Final Image", "Mirror of opening that shows change"), | |
| ], | |
| } | |
| # Genre-specific flavor modifiers | |
| GENRE_FLAVORS = { | |
| "action": { | |
| "prefix": "PULSE-POUNDING: ", | |
| "descriptors": ["explosive", "high-octane", "adrenaline-fueled", "relentless"], | |
| }, | |
| "drama": { | |
| "prefix": "EMOTIONALLY CHARGED: ", | |
| "descriptors": ["poignant", "raw", "intimate", "devastating"], | |
| }, | |
| "comedy": { | |
| "prefix": "HILARIOUS: ", | |
| "descriptors": ["absurd", "witty", "chaotic", "perfectly timed"], | |
| }, | |
| "thriller": { | |
| "prefix": "HEART-STOPPING: ", | |
| "descriptors": ["tense", "paranoid", "claustrophobic", "nerve-wracking"], | |
| }, | |
| "romance": { | |
| "prefix": "SWOON-WORTHY: ", | |
| "descriptors": ["tender", "passionate", "yearning", "chemistry-filled"], | |
| }, | |
| "scifi": { | |
| "prefix": "MIND-BENDING: ", | |
| "descriptors": ["futuristic", "technological", "alien", "conceptual"], | |
| }, | |
| "fantasy": { | |
| "prefix": "EPIC: ", | |
| "descriptors": ["mystical", "legendary", "enchanted", "otherworldly"], | |
| }, | |
| "horror": { | |
| "prefix": "TERRIFYING: ", | |
| "descriptors": ["dread-filled", "nightmarish", "unsettling", "visceral"], | |
| }, | |
| } | |
| # Format-specific beat counts (simplified for game flow) | |
| FORMAT_BEATS = { | |
| "film_90min": { | |
| "name": "90-Minute Feature Film", | |
| "structure": ["setup", "catalyst", "debate", "rising_action", "midpoint", | |
| "complications", "climax", "resolution"], | |
| "typical_count": 8, | |
| }, | |
| "tv_30min": { | |
| "name": "30-Minute TV Episode", | |
| "structure": ["setup", "catalyst", "rising_action", "midpoint", | |
| "complications", "climax", "resolution"], | |
| "typical_count": 6, | |
| }, | |
| "youtube_9min": { | |
| "name": "9-Minute YouTube Video", | |
| "structure": ["setup", "catalyst", "rising_action", "climax", "resolution"], | |
| "typical_count": 5, | |
| }, | |
| "short_3min": { | |
| "name": "3-Minute Short", | |
| "structure": ["setup", "catalyst", "climax", "resolution"], | |
| "typical_count": 4, | |
| }, | |
| } | |
| def generate_beat_sequence(format_type, genre, beat_count): | |
| """Generate a story beat sequence based on format and genre. | |
| Args: | |
| format_type: One of 'film_90min', 'tv_30min', 'youtube_9min', 'short_3min' | |
| genre: One of 'action', 'drama', 'comedy', 'thriller', 'romance', 'scifi', 'fantasy', 'horror' | |
| beat_count: Number of beats to generate (3-15) | |
| Returns: | |
| Tuple of (list_output, json_output, prompts_output) | |
| """ | |
| format_info = FORMAT_BEATS.get(format_type, FORMAT_BEATS["film_90min"]) | |
| genre_info = GENRE_FLAVORS.get(genre, GENRE_FLAVORS["drama"]) | |
| structure = format_info["structure"] | |
| # Build sequence following structure | |
| sequence = [] | |
| beats_per_section = max(1, beat_count // len(structure)) | |
| remaining = beat_count - (beats_per_section * len(structure)) | |
| for section in structure: | |
| section_beats = STORY_BEATS.get(section, STORY_BEATS["setup"]) | |
| # Pick random beats from this section | |
| count_for_section = beats_per_section + (1 if remaining > 0 else 0) | |
| if remaining > 0: | |
| remaining -= 1 | |
| available = list(section_beats) | |
| random.shuffle(available) | |
| for beat in available[:count_for_section]: | |
| descriptor = random.choice(genre_info["descriptors"]) | |
| sequence.append((beat[0], beat[1], section, descriptor)) | |
| # Trim or pad to exact count | |
| if len(sequence) > beat_count: | |
| sequence = sequence[:beat_count] | |
| if not sequence: | |
| return "Select a valid format and genre!", "{}", "" | |
| # Format as list | |
| list_output = f"## {format_info['name']} - {genre.title()} Genre\n\n" | |
| for i, (name, desc, section, descriptor) in enumerate(sequence, 1): | |
| list_output += f"**{i}. {name}** [{section}]\n{descriptor.title()}: {desc}\n\n" | |
| # Format as config JSON | |
| config = {"story_location": {}} | |
| for i, (name, desc, section, descriptor) in enumerate(sequence, 1): | |
| state_name = name.lower().replace(" ", "_").replace("/", "_").replace("-", "_") | |
| current_state_id = f"beat_{i}_{state_name}" | |
| # Determine next state | |
| if i < len(sequence): | |
| next_name = sequence[i][0] | |
| next_state_name = next_name.lower().replace(" ", "_").replace("/", "_").replace("-", "_") | |
| next_state_id = f"beat_{i+1}_{next_state_name}" | |
| else: | |
| next_state_id = "story_end" | |
| config["story_location"][current_state_id] = { | |
| "description": f"[{section.upper()}] {genre_info['prefix']}{desc}", | |
| "media_prompt": f"{genre.title()} {format_info['name']} scene: {name} - {descriptor} {desc}", | |
| "choices": ["Continue"], | |
| "transitions": { | |
| "Continue": next_state_id | |
| } | |
| } | |
| # Add ending state | |
| first_beat = sequence[0][0].lower().replace(" ", "_").replace("/", "_").replace("-", "_") | |
| config["story_location"]["story_end"] = { | |
| "description": f"The {genre} story concludes. The journey has changed everything.", | |
| "choices": ["Experience Again"], | |
| "transitions": { | |
| "Experience Again": f"beat_1_{first_beat}" | |
| } | |
| } | |
| json_output = json.dumps(config, indent=2) | |
| # Format prompts | |
| prompts_output = f"## {genre.title()} Story Prompts\n\n" | |
| for i, (name, desc, section, descriptor) in enumerate(sequence, 1): | |
| prompts_output += f"{genre.title()} {format_info['name']} scene: {name} - {descriptor} {desc}\n" | |
| return list_output, json_output, prompts_output | |
| # Quick test | |
| if __name__ == "__main__": | |
| list_out, json_out, prompts_out = generate_beat_sequence("film_90min", "thriller", 5) | |
| print(list_out) | |
| print(json_out) | |