File size: 9,830 Bytes
9328e91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
"""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)