Manish Gupta commited on
Commit
e363158
·
1 Parent(s): 3ada460

First commit

Browse files
Files changed (9) hide show
  1. .gitignore +3 -0
  2. app.py +308 -0
  3. pipeline.py +404 -0
  4. src/aws_utils.py +136 -0
  5. src/custom_log.py +52 -0
  6. src/datastructures.py +55 -0
  7. src/grok_wrapper.py +81 -0
  8. src/parameters.py +24 -0
  9. src/script_gen.py +204 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ *.env
2
+ *.ipynb
3
+ __pycache__/
app.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import textwrap
2
+ import jinja2
3
+ import gradio as gr
4
+
5
+ from src import script_gen
6
+ import pipeline
7
+
8
+ GENRE_CHOICES = ["ROMANTIC", "HORROR", "FANTACY", "THRILLER", "ACTION", "MYTHOLOGY", "ADVENTURE"]
9
+
10
+
11
+ def load_metadata(id, name, location, genre):
12
+ states = (
13
+ gr.Column(visible=False),
14
+ gr.Column(visible=True),
15
+ )
16
+
17
+ comic = pipeline.Comic(
18
+ id=id,
19
+ name=name,
20
+ location=location,
21
+ genre=genre,
22
+ )
23
+ return comic, "Generate Comic Synopsis", *states, "synopsis"
24
+
25
+
26
+
27
+
28
+ character_profiles_format = """\
29
+ ### {{name}}<br>
30
+ Age: {{age}}&nbsp;&nbsp;Gender: {{gender}}<br>
31
+ Bio:<br>
32
+ {{description}}<br>
33
+ ======================================<br><br>"""
34
+
35
+ common_format = """\
36
+ ### {{text1}}: {{text2}}<br>
37
+ =======================================<br>
38
+ {{synopsis}}<br>
39
+
40
+ {% if isFrame -%}
41
+ Narration: {{narration}}
42
+ Location: {{location}}
43
+ Frame Setting: {{setting}}
44
+ Dilouge:: {{char}}: '{{dilouge}}'
45
+ {% endif %}<br><br>
46
+ <br>"""
47
+
48
+ def format_output(output_type: str, output: ..., episode_idx: int = -1, scene_idx: int = -1):
49
+ if output_type == "synopsis":
50
+ return textwrap.dedent(f"""\
51
+ ## Synopsis:
52
+ {output}""")
53
+ elif output_type == "episode_breakdown":
54
+ output_string = ""
55
+ idxs = sorted([idx for idx, _ in output.items()])
56
+ for idx in idxs:
57
+ episode = output[idx]
58
+ text = jinja2.Template(source=common_format).render({
59
+ "text1": idx,
60
+ "text2": episode.title,
61
+ "synopsis": episode.abstract,
62
+ }
63
+ )
64
+ output_string += text
65
+ return output_string
66
+ elif output_type == "character_generation":
67
+ output_string = ""
68
+ for name, profile in output.items():
69
+ text = jinja2.Template(source=character_profiles_format).render({
70
+ "name": name,
71
+ "age": profile.age,
72
+ "gender": profile.gender,
73
+ "description": profile.description,
74
+ })
75
+ output_string += text
76
+ return output_string
77
+ elif output_type == "scenes":
78
+ output_string = ""
79
+ for episode_num, episode in output.items():
80
+ if episode_num != episode_idx:
81
+ continue
82
+ for scene_num, scene in enumerate(episode.scenes):
83
+ text = jinja2.Template(source=common_format).render({
84
+ "text1": "Scene",
85
+ "text2": scene_num,
86
+ "synopsis": scene.description,
87
+ })
88
+ output_string += text
89
+ return output_string
90
+ elif output_type == "frames":
91
+ output_string = ""
92
+ for episode_num, episode in output.items():
93
+ if episode_num != episode_idx:
94
+ continue
95
+ for scene_num, scene in enumerate(episode.scenes):
96
+ if scene_num != scene_idx:
97
+ continue
98
+ for frame_num, frame in enumerate(scene.frames):
99
+ text = jinja2.Template(source=common_format).render(
100
+ text1="Frame",
101
+ text2=frame_num,
102
+ synopsis=frame.description,
103
+ isFrame=True,
104
+ narration=frame.narration,
105
+ location=frame.location,
106
+ setting=frame.frame_setting,
107
+ char=frame.audio_cue_character,
108
+ dilouge=frame.audio_cue_text,
109
+ )
110
+ output_string += text
111
+ return output_string
112
+
113
+
114
+ def generate_llm_output(
115
+ user_input: str,
116
+ system_instruction: str,
117
+ current_status: str,
118
+ currComic: pipeline.Comic,
119
+ episode_idx: int,
120
+ scene_idx: int,
121
+ regenerate: bool = False,
122
+ ):
123
+ model_output = ""
124
+ is_scrollable = False
125
+ print(f"Pipeline is at: {current_status}")
126
+ if current_status == "synopsis":
127
+ currComic.generate_comic_synopsis(user_input, system_instruction, regenerate)
128
+ model_output = format_output(current_status, currComic.abstract)
129
+ elif current_status == "episode_breakdown":
130
+ currComic.generate_episode_breakdowns(system_instruction, regenerate)
131
+ model_output = format_output(current_status, currComic.episodes)
132
+ elif current_status == "character_generation":
133
+ currComic.generate_character_profiles(system_instruction, regenerate)
134
+ model_output = format_output(current_status, currComic.characters)
135
+ elif current_status == "scenes":
136
+ currComic.generate_scenes(
137
+ system_instruction, episode_idx, regenerate
138
+ )
139
+ model_output = format_output(
140
+ current_status, currComic.episodes, episode_idx
141
+ )
142
+ is_scrollable = True
143
+ elif current_status == "frames":
144
+ currComic.generate_frames(
145
+ system_instruction, episode_idx, scene_idx, regenerate
146
+ )
147
+ model_output = format_output(
148
+ current_status, currComic.episodes, episode_idx, scene_idx
149
+ )
150
+ is_scrollable = True
151
+
152
+ return (
153
+ gr.Button(visible=is_scrollable), # Prev Button
154
+ gr.Button(visible=is_scrollable), # Next button
155
+ gr.Button(visible=False), # Generate Button
156
+ gr.Button(visible=True), # Regenerate Button
157
+ gr.Markdown(value=model_output, visible=True), # Model output
158
+ gr.Button(visible=True), # Submit Button
159
+ )
160
+
161
+
162
+ def submit_data(
163
+ current_status: str, currComic: pipeline.Comic, episode_idx: int
164
+ ):
165
+ currComic.save_comic_to_s3()
166
+ preamble = ""
167
+
168
+ if current_status == "synopsis":
169
+ current_status = "episode_breakdown"
170
+ preamble = script_gen.generate_episode_breakdown_instruction
171
+ elif current_status == "episode_breakdown":
172
+ current_status = "character_generation"
173
+ preamble = script_gen.generate_character_profile_instruction
174
+ elif current_status == "character_generation":
175
+ current_status = "scenes"
176
+ preamble = script_gen.generate_scene_instruction
177
+ episode_idx = min(list(currComic.episodes.keys()))
178
+ elif current_status == "scenes":
179
+ current_status = "frames"
180
+ preamble = script_gen.generate_frames_instruction
181
+ episode_idx = min(list(currComic.episodes.keys()))
182
+ elif current_status == "frames":
183
+ current_status = "finished"
184
+ print(f"Next Pipeline stage is: {current_status}")
185
+ return (
186
+ episode_idx,
187
+ current_status,
188
+ gr.Textbox(visible=False), # user_input
189
+ gr.Textbox(value=preamble, visible=True), # preamble
190
+ gr.Button(visible=False), # Prev Button
191
+ gr.Button(visible=False), # Next button
192
+ gr.Button(visible=True), # Generate Button
193
+ gr.Button(visible=False), # Regenerate Button
194
+ gr.Markdown(value="", visible=False), # Model output
195
+ )
196
+
197
+
198
+ with gr.Blocks() as demo:
199
+ currComic = gr.State(None)
200
+ current_status = gr.State("")
201
+ episode_idx = gr.State(-1)
202
+ scene_idx = gr.State(0)
203
+ regenerate = gr.State(True)
204
+
205
+ instruction = gr.Markdown(value="Enter the comic meta data.")
206
+ with gr.Column(visible=True) as metadata:
207
+ with gr.Row():
208
+ comic_id = gr.Textbox(label="Enter Comic ID:", placeholder="Enter Comic ID")
209
+ comic_name = gr.Textbox(
210
+ label="Enter Comic Title:", placeholder="Enter Comic Title"
211
+ )
212
+ comic_location = gr.Textbox(
213
+ label="Enter Comic Location:", placeholder="Enter Comic Location"
214
+ )
215
+ comic_genre = gr.Dropdown(
216
+ choices=GENRE_CHOICES, label="Current Genre", interactive=True
217
+ )
218
+ metadata_btn = gr.Button("Submit Metadata")
219
+
220
+ with gr.Column(visible=False) as synopsis:
221
+ with gr.Row():
222
+ user_input = gr.Textbox(label="Enter Your Idea for the comic:")
223
+ preamble = gr.Textbox(
224
+ label="Model Instruction:",
225
+ value=script_gen.generate_synopsis_instruction,
226
+ lines=10,
227
+ )
228
+
229
+ with gr.Row() as navigation:
230
+ previous_btn = gr.Button("Previous", visible=False)
231
+ generate_btn = gr.Button("Generate")
232
+ regenerate_btn = gr.Button("Re-Generate", visible=False)
233
+ next_btn = gr.Button("Next", visible=False)
234
+
235
+ model_response = gr.Markdown(label="Model Output")
236
+ submit_btn = gr.Button("Submit", visible=False)
237
+
238
+ epi = gr.Textbox(label="Current Episode Synopsis", visible=False)
239
+ scene = gr.Textbox(label="Current Scene Synopsis", visible=False)
240
+
241
+ metadata_btn.click(
242
+ load_metadata,
243
+ inputs=[comic_id, comic_name, comic_location, comic_genre],
244
+ outputs=[currComic, instruction, metadata, synopsis, current_status],
245
+ )
246
+
247
+ generate_btn.click(
248
+ generate_llm_output,
249
+ inputs=[
250
+ user_input,
251
+ preamble,
252
+ current_status,
253
+ currComic,
254
+ episode_idx,
255
+ scene_idx
256
+ ],
257
+ outputs=[
258
+ previous_btn,
259
+ next_btn,
260
+ generate_btn,
261
+ regenerate_btn,
262
+ model_response,
263
+ submit_btn
264
+ ],
265
+ )
266
+
267
+ regenerate_btn.click(
268
+ generate_llm_output,
269
+ inputs=[
270
+ user_input,
271
+ preamble,
272
+ current_status,
273
+ currComic,
274
+ episode_idx,
275
+ scene_idx,
276
+ regenerate, # regenrate option
277
+ ],
278
+ outputs=[
279
+ previous_btn,
280
+ next_btn,
281
+ generate_btn,
282
+ regenerate_btn,
283
+ model_response,
284
+ submit_btn
285
+ ],
286
+ )
287
+
288
+ submit_btn.click(
289
+ submit_data,
290
+ inputs=[
291
+ current_status,
292
+ currComic,
293
+ episode_idx,
294
+ ],
295
+ outputs=[
296
+ episode_idx,
297
+ current_status,
298
+ user_input,
299
+ preamble,
300
+ previous_btn,
301
+ next_btn,
302
+ generate_btn,
303
+ regenerate_btn,
304
+ model_response
305
+ ]
306
+ )
307
+
308
+ demo.launch(auth=("admin", "Qrt@12*34#immersfy"), share=True, ssr_mode=False)
pipeline.py ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import abc
2
+ import jinja2
3
+ import base64
4
+ import io
5
+ import random
6
+ import dataclasses
7
+ import traceback as tc
8
+ from dacite import from_dict
9
+ from src import datastructures
10
+ from src import aws_utils
11
+ from src import parameters
12
+ from src import script_gen
13
+ from src import grok_wrapper
14
+
15
+ logger = parameters.LOGGER
16
+ Episode = datastructures.Episode
17
+ Scene = datastructures.Scene
18
+ Frame = datastructures.Frame
19
+ Composition = datastructures.Composition
20
+ Character = datastructures.Character
21
+ Location = datastructures.Location
22
+ llm = grok_wrapper.GROK_2
23
+
24
+
25
+ class Comic(abc.ABC):
26
+
27
+ def __init__(
28
+ self,
29
+ id: str,
30
+ name: str,
31
+ location: str,
32
+ genre: str,
33
+ ):
34
+ self.id = id
35
+ self.name = name
36
+ self.abstract = ""
37
+ # self.abstract = abstract
38
+ self.genre = genre
39
+ self.location = location
40
+ self._post_init()
41
+
42
+ def _post_init(self):
43
+ self.episodes = self._get_current_episodes()
44
+ self.characters = self._get_current_characters()
45
+
46
+ def generate_comic_synopsis(
47
+ self,
48
+ user_input: str,
49
+ system_instruction: str,
50
+ regenerate: bool = False,
51
+ ):
52
+ """Class method to generate synopsis for the comic."""
53
+ if self.abstract and not regenerate:
54
+ logger.info(
55
+ f"Synopsis is aleady generated for the comic: {self.id}. Skipping generation!"
56
+ )
57
+ return ""
58
+
59
+ try:
60
+ logger.info("Started generating comic Synopsis!")
61
+ prompt_dict = {
62
+ "system": system_instruction,
63
+ "user": user_input,
64
+ }
65
+ synopsis = llm.generate_response(prompt_dict)
66
+
67
+ # Save the synopsis to s3 bucket at the path formed by:
68
+ # f"s3://{parameters.AWS_BUCKET}/{self.id}/synopsis.json"
69
+ aws_utils.save_to_s3(
70
+ parameters.AWS_BUCKET,
71
+ self.id,
72
+ {"synopsis": synopsis},
73
+ "synopsis.json",
74
+ )
75
+ self.abstract = synopsis
76
+ logger.info("Completed generating comic synopsis!")
77
+ except Exception:
78
+ logger.error(
79
+ f"Faced an exception while generating comic synopsis! See full traceback below"
80
+ )
81
+ logger.error(tc.format_exc())
82
+ return tc.format_exc()
83
+
84
+ def _get_current_episodes(self) -> dict:
85
+ """Fetches the epiodes stored on S3 if present."""
86
+ try:
87
+ response = aws_utils.S3_CLIENT.list_objects_v2(
88
+ Bucket=parameters.AWS_BUCKET,
89
+ Prefix=f"{self.id}/episodes/",
90
+ Delimiter="/",
91
+ ).get("CommonPrefixes", None)
92
+
93
+ # In case the URL is incorrect or ill-formatted.
94
+ if response is None:
95
+ logger.warning(f"No episodes found for the comic id: {self.id}")
96
+ return {}
97
+
98
+ episodes = {}
99
+ for prefix in response:
100
+ folder = prefix["Prefix"]
101
+ if "episode" in folder:
102
+ json_path = f"s3://{parameters.AWS_BUCKET}/{folder}episode.json"
103
+ # The sample value of folder: 301/episodes/episode-10
104
+ # Thus, split by / and 2nd index gives episode-10
105
+ # then, split by - and last index gives the number 10
106
+ idx = int(folder.split("/")[2].split("-")[-1])
107
+ episodes[idx] = from_dict(
108
+ data_class=Episode,
109
+ data=eval(
110
+ aws_utils.fetch_from_s3(source=json_path).decode("utf-8")
111
+ ),
112
+ )
113
+ logger.info(f"Fetched {len(episodes)} episodes for comic id: {self.id}")
114
+ return episodes
115
+ except Exception:
116
+ logger.error(
117
+ f"Faced an exception while fetching episode breakdowns! See full traceback below:"
118
+ )
119
+ logger.error(tc.format_exc())
120
+ return {}
121
+
122
+ def _get_current_characters(self) -> dict:
123
+ """Fetches the character profiles stored on S3 if present."""
124
+ try:
125
+ character_json = (
126
+ f"s3://{parameters.AWS_BUCKET}/{self.id}/characters/characters.json"
127
+ )
128
+ data = eval(aws_utils.fetch_from_s3(source=character_json).decode("utf-8"))
129
+ chars = {}
130
+ for name, profile in data.items():
131
+ # profile["expressions"] = profile.get("expressions", [])
132
+ chars[name] = Character(**profile)
133
+ logger.info(
134
+ f"Fetched {len(chars)} character profiles for comic id: {self.id}"
135
+ )
136
+ return chars
137
+ except Exception:
138
+ logger.error(
139
+ f"Faced an exception while fetching character profiles! See full traceback below:"
140
+ )
141
+ logger.error(tc.format_exc())
142
+ return {}
143
+
144
+ def generate_episode_breakdowns(self, system_instruction: str, regenerate: bool = False) -> str:
145
+ """Class method to generate episode breakdowns for the comic."""
146
+ if len(self.episodes) > 0 and not regenerate:
147
+ logger.info(
148
+ f"Episodes are aleady generated for the comic: {self.id}. Skipping generation!"
149
+ )
150
+ return ""
151
+
152
+ try:
153
+ logger.info("Started generating episode breakdowns!")
154
+ # We employ a 2-step process to generate the abstracts for each episodes.
155
+ # 1. Initially we take the comic abstract and expand it to include some more creative
156
+ # twists and turns for the story.
157
+ # 2. Next, we use that expanded synopsis to generate per episode abstract
158
+ # ensuring the flow of the story doesn't break and each episode is interesting enough
159
+ # to read.
160
+ prompt_dict = {
161
+ "system": script_gen.generate_detailed_synopsis_instruction,
162
+ "user": jinja2.Template(
163
+ source=script_gen.generate_detailed_synopsis_user_prompt
164
+ ).render(
165
+ {
166
+ "TITLE": self.name,
167
+ "GENRE": self.genre,
168
+ "SYNOPSIS": self.abstract,
169
+ "LOCATION": self.location,
170
+ }
171
+ ),
172
+ }
173
+ extended_synopsis = llm.generate_response(prompt_dict)
174
+
175
+ prompt_dict = {
176
+ "system": system_instruction,
177
+ "user": jinja2.Template(
178
+ source=script_gen.generate_episode_breakdown_user_prompt
179
+ ).render({"SYNOPSIS": extended_synopsis}),
180
+ }
181
+ episode_breakdown = llm.generate_valid_json_response(prompt_dict)
182
+ logger.info(
183
+ f"Generated a total of {len(episode_breakdown)} episodes for comic: {self.id}"
184
+ )
185
+
186
+ # Save the breakdown to s3 bucket at the path formed by:
187
+ # f"s3://{parameters.AWS_BUCKET}/{self.id}/episode_breakdown.json"
188
+ episode_breakdown_path = aws_utils.save_to_s3(
189
+ parameters.AWS_BUCKET,
190
+ self.id,
191
+ episode_breakdown,
192
+ "episode_breakdown.json",
193
+ )
194
+
195
+ for idx, data in episode_breakdown.items():
196
+ self.episodes[idx] = Episode(
197
+ title=data["title"],
198
+ abstract=data["synopsis"],
199
+ )
200
+ self.save_comic_to_s3()
201
+
202
+ logger.info("Completed generating episode breakdowns!")
203
+ return episode_breakdown_path
204
+ except Exception:
205
+ logger.error(
206
+ f"Faced an exception while generating episode breakdowns! See full traceback below"
207
+ )
208
+ logger.error(tc.format_exc())
209
+ return tc.format_exc()
210
+
211
+ def generate_character_profiles(self, system_instruction: str, regenerate: bool = False) -> str:
212
+ """Class method to generate character profiles for the comic."""
213
+ if len(self.characters) > 0 and not regenerate:
214
+ logger.info(
215
+ f"Characters are aleady generated for the comic: {self.id}. Skipping generation!"
216
+ )
217
+ return ""
218
+
219
+ try:
220
+ logger.info("Started generating character profiles!")
221
+ prompt_dict = {
222
+ "system": system_instruction,
223
+ "user": jinja2.Template(
224
+ source=script_gen.generate_character_profile_user_prompt
225
+ ).render(
226
+ {
227
+ "COMIC_NAME": self.name,
228
+ "ABSTRACT": self.abstract,
229
+ "episode_breakdown": [
230
+ dataclasses.asdict(episode)
231
+ for _, episode in self.episodes.items()
232
+ ],
233
+ }
234
+ ),
235
+ }
236
+ characters = llm.generate_valid_json_response(prompt_dict)
237
+
238
+ for char in characters:
239
+ char["profile_image"] = (
240
+ f"s3://blix-demo-v0/{self.id}/characters/images/{char['name']}.jpg"
241
+ )
242
+ self.characters[char["name"]] = Character(**char)
243
+
244
+ character_profile_path = aws_utils.save_to_s3(
245
+ parameters.AWS_BUCKET,
246
+ f"{self.id}/characters",
247
+ {
248
+ name: dataclasses.asdict(char)
249
+ for name, char in self.characters.items()
250
+ },
251
+ "characters.json",
252
+ )
253
+ logger.info(
254
+ f"Model extracted a total of {len(self.characters)} characters."
255
+ )
256
+ logger.info("Completed generating character profiles!")
257
+ return character_profile_path
258
+ except Exception:
259
+ logger.error(
260
+ f"Faced an exception while generating character profiles! See full traceback below"
261
+ )
262
+ logger.error(tc.format_exc())
263
+ return tc.format_exc()
264
+
265
+ def generate_scenes(self, system_instruction: str, episode_idx: int, regenerate: bool = False) -> str:
266
+ """Class method to generate scenes for the comic."""
267
+ try:
268
+ logger.info("Started generating Scenes!")
269
+ # The idea is to take each of the episodes and break them down into different
270
+ # scenes while maintaining the overall story flow and character details.
271
+ for episode_num, episode in self.episodes.items():
272
+ if len(episode.scenes) > 0 and not regenerate:
273
+ logger.info(
274
+ f"Scenes are aleady generated for the episode: {episode_num}. Skipping generation!"
275
+ )
276
+ continue
277
+
278
+ # In case regeneration is enabled, we check if we're at the correct episode index.
279
+ if regenerate and episode_num != episode_idx:
280
+ continue
281
+ logger.info(f"Generating Scenes for episode: {episode_num}")
282
+ prompt_dict = {
283
+ "system": system_instruction,
284
+ "user": jinja2.Template(
285
+ source=script_gen.generate_scene_user_prompt
286
+ ).render(
287
+ {
288
+ "SYNOPSIS": episode.abstract,
289
+ "CHARS": [
290
+ dataclasses.asdict(char)
291
+ for _, char in self.characters.items()
292
+ ],
293
+ }
294
+ ),
295
+ }
296
+ episode_scenes = llm.generate_valid_json_response(prompt_dict)
297
+
298
+ episode.scenes = [
299
+ Scene(description=description)
300
+ for _, description in episode_scenes["scenes"].items()
301
+ ]
302
+ _ = aws_utils.save_to_s3(
303
+ parameters.AWS_BUCKET,
304
+ f"{self.id}/episodes/episode-{episode_num}",
305
+ episode_scenes,
306
+ "scenes.json",
307
+ )
308
+ self.save_comic_to_s3()
309
+ scenes_path = (
310
+ f"s3://{parameters.AWS_BUCKET}/{self.id}/episodes/episode-*/scenes.json"
311
+ )
312
+ logger.info("Completed generating Scenes!")
313
+ return scenes_path
314
+ except Exception:
315
+ logger.error(
316
+ f"Faced an exception while generating scenes! See full traceback below"
317
+ )
318
+ logger.error(tc.format_exc())
319
+ return tc.format_exc()
320
+
321
+ def generate_frames(self, system_instruction: str, episode_idx: int, scene_idx: int, regenerate: bool = False) -> str:
322
+ """Class method to generate frames for the comic."""
323
+ try:
324
+ location_list = []
325
+ logger.info("Started generating frames!")
326
+ # Next step is to take each of the generated scenes and break them down
327
+ # further into various frames. We target to achieve a specific still moment
328
+ # in the comic book story that resembles a frame and then try to visualize it.
329
+ for episode_num, episode in self.episodes.items():
330
+ for scene_num, scene in enumerate(episode.scenes):
331
+ if len(scene.frames) > 0 and not regenerate:
332
+ logger.info(
333
+ f"Frames are aleady generated for the episode: {episode_num} and scene: {scene_num}. Skipping generation!"
334
+ )
335
+ continue
336
+
337
+ # In case regeneration is enabled, we check if we're at the correct (episode, scene) index.
338
+ if regenerate and (episode_num != episode_idx or scene_num != scene_idx):
339
+ continue
340
+ logger.info(
341
+ f"Generating frames for episode: {episode_num} and scene: {scene_num}"
342
+ )
343
+ prompt_dict = {
344
+ "system": system_instruction,
345
+ "user": jinja2.Template(
346
+ source=script_gen.generate_frames_user_prompt
347
+ ).render(
348
+ {
349
+ "SYNOPSIS": scene.description,
350
+ "CHARACTERS": [
351
+ dataclasses.asdict(char)
352
+ for _, char in self.characters.items()
353
+ ],
354
+ }
355
+ ),
356
+ }
357
+ frames = llm.generate_valid_json_response(prompt_dict)
358
+ for frame in frames:
359
+ chars = []
360
+ for char in frame["characters"]:
361
+ if self.characters.get(char, None) is not None:
362
+ chars.append(self.characters[char])
363
+ frame["characters"] = chars
364
+
365
+ scene.frames = [Frame(**frame) for frame in frames["images"]]
366
+ aws_utils.save_to_s3(
367
+ parameters.AWS_BUCKET,
368
+ f"{self.id}/episodes/episode-{episode_num}",
369
+ {
370
+ "frames": [
371
+ dataclasses.asdict(frame) for frame in scene.frames
372
+ ]
373
+ },
374
+ f"scene-{scene_num}-frames.json",
375
+ )
376
+ self.save_comic_to_s3()
377
+ frames_path = f"s3://{parameters.AWS_BUCKET}/{self.id}/episodes/episode-*/scene-*-frames.json"
378
+ logger.info("Completed generating frames!")
379
+ return frames_path
380
+ except Exception:
381
+ logger.error(
382
+ f"Faced an exception while generating frames! See full traceback below"
383
+ )
384
+ logger.error(tc.format_exc())
385
+ return tc.format_exc()
386
+
387
+ def save_comic_to_s3(self) -> bool:
388
+ """Class method to save data to s3 bucket."""
389
+ try:
390
+ for episode_num, episode in self.episodes.items():
391
+ logger.info(f"Saving episode: {episode_num}")
392
+ path = aws_utils.save_to_s3(
393
+ parameters.AWS_BUCKET,
394
+ f"{self.id}/episodes/episode-{episode_num}",
395
+ dataclasses.asdict(episode),
396
+ "episode.json",
397
+ )
398
+ return True
399
+ except Exception:
400
+ logger.error(
401
+ f"Faced an exception while saving comic to S3! See full traceback below"
402
+ )
403
+ logger.error(tc.format_exc())
404
+ return False
src/aws_utils.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from io import BytesIO
4
+ from typing import Union
5
+ from urllib.parse import urlparse
6
+
7
+ import boto3
8
+ from botocore.client import Config
9
+ from botocore.exceptions import NoCredentialsError
10
+
11
+ AWS_REGION = os.getenv("AWS_REGION")
12
+
13
+ # Initialize the S3 client
14
+ S3_CLIENT = boto3.client(
15
+ "s3", region_name=AWS_REGION, config=Config(signature_version="s3v4")
16
+ )
17
+
18
+
19
+ def save_to_s3(
20
+ bucket_name: str,
21
+ folder_name: str,
22
+ content: Union[str, dict, BytesIO],
23
+ file_name: str,
24
+ ) -> str:
25
+ """
26
+ Save a file to an S3 bucket, determining the content type based on the input type.
27
+
28
+ Args:
29
+ bucket_name (str): The name of the S3 bucket.
30
+ folder_name (str): The folder path in the S3 bucket.
31
+ content (Union[str, dict, BytesIO]): The content to save, can be a string, dictionary, or BytesIO.
32
+ file_name (str): The file name under which the content should be saved.
33
+
34
+ Returns:
35
+ str: The S3 URL of the uploaded file, or an error message if credentials are not available.
36
+ """
37
+ # Ensure the folder name ends with a '/'
38
+ # if not folder_name.endswith('/'):
39
+ # folder_name += '/'
40
+ # Determine file name and content type based on the input
41
+ if isinstance(content, str):
42
+ file_content = content
43
+ content_type = "text/plain"
44
+ elif isinstance(content, dict):
45
+ file_content = json.dumps(content)
46
+ content_type = "application/json"
47
+ elif isinstance(content, BytesIO):
48
+ file_content = content
49
+ content_type = "image/jpeg"
50
+ else:
51
+ print(
52
+ "Invalid content type. Content must be a string, dictionary, or BytesIO."
53
+ )
54
+ raise ValueError("Content must be either a string, dictionary, or BytesIO.")
55
+
56
+ # Ensure the folder name ends with a '/'
57
+ s3_file_path = f"{folder_name.rstrip('/')}/{file_name}"
58
+
59
+ try:
60
+ # Upload the file to S3
61
+ S3_CLIENT.put_object(
62
+ Bucket=bucket_name,
63
+ Key=s3_file_path,
64
+ Body=file_content,
65
+ ContentType=content_type,
66
+ )
67
+ s3_url = f"s3://{bucket_name}/{s3_file_path}"
68
+ print(f"File successfully uploaded to {s3_url}")
69
+ return s3_url
70
+
71
+ except NoCredentialsError:
72
+ print("AWS credentials not available.")
73
+ return "Error: AWS credentials not available."
74
+
75
+
76
+ def fetch_from_s3(source: Union[str, dict], region_name: str = "ap-south-1") -> bytes:
77
+ """
78
+ Fetch a file's content from S3 given a source URL or dictionary with bucket and key.
79
+
80
+ Args:
81
+ source (Union[str, dict]): The source S3 URL or a dictionary with 'bucket_name' and 'file_key'.
82
+ region_name (str): The AWS region name for the S3 client (default is 'ap-south-1').
83
+
84
+ Returns:
85
+ bytes: The content of the file fetched from S3.
86
+ """
87
+ print(f"Fetching file from S3. Source: {source}")
88
+ s3_client = boto3.client("s3", region_name=region_name)
89
+
90
+ # Parse the source depending on its type
91
+ if isinstance(source, str):
92
+ parsed_url = urlparse(source)
93
+ bucket_name = parsed_url.netloc.split(".")[0]
94
+ file_path = parsed_url.path.lstrip("/")
95
+ elif isinstance(source, dict):
96
+ bucket_name = source.get("bucket_name")
97
+ file_path = source.get("file_key")
98
+ if not bucket_name or not file_path:
99
+ print("Dictionary input must contain 'bucket_name' and 'file_key'.")
100
+ raise ValueError(
101
+ "Dictionary input must contain 'bucket_name' and 'file_key'."
102
+ )
103
+ else:
104
+ print("Source must be a string URL or a dictionary.")
105
+ raise ValueError("Source must be a string URL or a dictionary.")
106
+
107
+ print(f"Attempting to download from bucket: {bucket_name}, path: {file_path}")
108
+ try:
109
+ response = s3_client.get_object(Bucket=bucket_name, Key=file_path)
110
+ file_content = response["Body"].read()
111
+ print(f"File fetched successfully from {bucket_name}/{file_path}")
112
+ return file_content
113
+ except Exception as e:
114
+ print(f"Failed to fetch file from S3: {e}")
115
+ raise
116
+
117
+
118
+ def list_s3_objects(bucket_name: str, folder_path: str = "") -> list:
119
+ """
120
+ Lists a content of the given a directory URL.
121
+
122
+ Args:
123
+ bucket_name (str): The name of the S3 bucket.
124
+ folder_name (str): The folder path in the S3 bucket.
125
+
126
+ Returns:
127
+ list: The list of files found inside the given directory URL.
128
+ """
129
+ response = S3_CLIENT.list_objects_v2(Bucket=bucket_name, Prefix=folder_path)
130
+ # Check if the bucket contains objects
131
+ objects = []
132
+ if "Contents" in response:
133
+ for obj in response["Contents"]:
134
+ objects.append(obj["Key"])
135
+
136
+ return objects
src/custom_log.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+
4
+ import colorlog
5
+
6
+
7
+ def setup_logger(name: str) -> logging.Logger:
8
+ """
9
+ Set up a logger with colored output for console if running in a terminal.
10
+
11
+ Args:
12
+ name (str): Name of the logger.
13
+
14
+ Returns:
15
+ logging.Logger: Configured logger instance.
16
+ """
17
+ # Check if the output is a terminal to enable colored output
18
+ use_colors = sys.stdout.isatty()
19
+
20
+ # Create a stream handler
21
+ handler = colorlog.StreamHandler()
22
+
23
+ if use_colors:
24
+ # Use colored formatter for terminal output
25
+ handler.setFormatter(
26
+ colorlog.ColoredFormatter(
27
+ "%(asctime)s - %(log_color)s%(levelname)s%(reset)s - %(message)s",
28
+ datefmt="%Y-%m-%d %H:%M:%S",
29
+ log_colors={
30
+ "DEBUG": "cyan",
31
+ "INFO": "green",
32
+ "WARNING": "yellow",
33
+ "ERROR": "red",
34
+ "CRITICAL": "bold_red",
35
+ },
36
+ )
37
+ )
38
+ else:
39
+ # Use standard formatter when not in a terminal
40
+ handler.setFormatter(
41
+ logging.Formatter(
42
+ "%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
43
+ )
44
+ )
45
+
46
+ # Configure the logger
47
+ logger = logging.getLogger(name)
48
+ logger.addHandler(handler)
49
+ logger.setLevel(logging.INFO)
50
+ logger.propagate = False
51
+
52
+ return logger
src/datastructures.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict
2
+ import dataclasses
3
+
4
+
5
+ @dataclasses.dataclass
6
+ class Composition:
7
+ prompt: str
8
+ shot_type: str
9
+ seed: int = None
10
+ image: str = ""
11
+
12
+
13
+ @dataclasses.dataclass
14
+ class Character:
15
+ name: str
16
+ age: str
17
+ gender: str
18
+ description: str
19
+ profile_image: str
20
+ expressions : List[str] = dataclasses.field(default_factory=list)
21
+ compositions: List[Composition] = dataclasses.field(default_factory=list)
22
+
23
+
24
+ @dataclasses.dataclass(frozen=True)
25
+ class Location:
26
+ name: str
27
+ description: str
28
+
29
+ @dataclasses.dataclass
30
+ class Frame:
31
+ description: str
32
+ narration: str
33
+ audio_cue_text: str
34
+ audio_cue_character: str
35
+ location: str
36
+ frame_setting: str
37
+ characters: List[Character] = dataclasses.field(default_factory=list)
38
+ compositions: List[Composition] = dataclasses.field(default_factory=list)
39
+ character_expression: Dict[str, str] = dataclasses.field(default_factory=dict)
40
+ audio_path: str = ""
41
+ stitch_image: str = ""
42
+
43
+ @dataclasses.dataclass
44
+ class Scene:
45
+ description: str
46
+ frames: List[Frame] = dataclasses.field(default_factory=list)
47
+
48
+
49
+ @dataclasses.dataclass
50
+ class Episode:
51
+ title: str
52
+ abstract: str
53
+ scenes: List[Scene] = dataclasses.field(default_factory=list)
54
+ thumbnail_image: str = ""
55
+ thumbnail_image_prompt: str = ""
src/grok_wrapper.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model wrapper to interact with OpenAI models."""
2
+ import abc
3
+ import ast
4
+ from typing import Mapping
5
+
6
+ import openai
7
+
8
+ from src import parameters
9
+
10
+ logger = parameters.LOGGER
11
+
12
+
13
+ class xAIModel(abc.ABC):
14
+ API_KEY = ""
15
+
16
+ def __init__(self, model_name: str, API_KEY: str):
17
+ try:
18
+ self.client = openai.OpenAI(
19
+ api_key=API_KEY,
20
+ base_url="https://api.x.ai/v1",
21
+ )
22
+ self.model_name = model_name
23
+ except Exception as exc:
24
+ raise Exception(
25
+ "Failed to initialize Grok xAI model client. See traceback for more details.",
26
+ ) from exc
27
+
28
+ def prepare_input(self, prompt_dict: Mapping[str, str]) -> str:
29
+ conversation = []
30
+ try:
31
+ for role, content in prompt_dict.items():
32
+ conversation.append({"role": role, "content": content})
33
+ return conversation
34
+ except Exception as exc:
35
+ raise Exception(
36
+ f"Incomplete Prompt Dictionary Passed. Expected to have atleast a role and it's content.\nPassed dict: {prompt_dict}",
37
+ ) from exc
38
+
39
+ def generate_response(
40
+ self,
41
+ prompt_dict: Mapping[str, str],
42
+ max_output_tokens: int = None,
43
+ temperature: int = 0.6,
44
+ response_format: dict = None,
45
+ ) -> str:
46
+ conversation = self.prepare_input(prompt_dict)
47
+ try:
48
+ response = self.client.chat.completions.create(
49
+ model=self.model_name,
50
+ messages=conversation,
51
+ max_tokens=max_output_tokens if max_output_tokens else None,
52
+ temperature=temperature,
53
+ response_format=response_format,
54
+ )
55
+ return response.choices[0].message.content
56
+ except Exception as exc:
57
+ raise Exception(
58
+ f"Exception in generating model response.\nModel name: {self.model_name}\nInput prompt: {str(conversation)}",
59
+ ) from exc
60
+
61
+ def generate_valid_json_response(
62
+ self,
63
+ prompt_dict: Mapping[str, str],
64
+ max_output_tokens: int = None,
65
+ temperature: int = 0.6,
66
+ ) -> str:
67
+ """Generate a response with retries, returning a valid JSON."""
68
+ for _ in range(parameters.MAX_TRIES):
69
+ try:
70
+ model_response = self.generate_response(
71
+ prompt_dict, max_output_tokens, temperature, {"type": "json_object"}
72
+ )
73
+ return ast.literal_eval(model_response)
74
+ except Exception as e:
75
+ continue
76
+ raise Exception(
77
+ f"Maximum retries met before valid JSON structure was found.\nModel name: {self.model_name}\nInput prompt: {str(prompt_dict)}"
78
+ )
79
+
80
+
81
+ GROK_2 = xAIModel("grok-2-1212", parameters.XAI_API_KEY)
src/parameters.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from dotenv import load_dotenv
4
+
5
+ from src import custom_log
6
+
7
+ load_dotenv()
8
+
9
+ # AWS Resouce variables
10
+ os.environ["AWS_ACCESS_KEY_ID"] = os.environ.get("AWS_ACCESS_KEY_ID")
11
+ os.environ["AWS_SECRET_ACCESS_KEY"] = os.environ.get("AWS_SECRET_ACCESS_KEY")
12
+ os.environ["S3_BUCKET_NAME"] = os.environ.get("AWS_BUCKET")
13
+ AWS_BUCKET = os.environ.get("AWS_BUCKET")
14
+ AWS_REGION = os.environ.get("AWS_REGION")
15
+
16
+ # Local variables
17
+ MAX_WORKERS = int(os.environ.get("MAX_WORKERS"))
18
+ MAX_TRIES = int(os.environ.get("MAX_TRIES"))
19
+
20
+ # xAI AI API Key
21
+ XAI_API_KEY = os.environ.get("XAI_API_KEY")
22
+
23
+ # Custom logger
24
+ LOGGER = custom_log.setup_logger(__name__)
src/script_gen.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Collection of all system prompts for `/generate_episode_breakdown` and `/generate_episode_content` API."""
2
+
3
+ ###################################### STEP 0: GENERATE COMIC SYNOPSIS ######################################
4
+
5
+ generate_synopsis_instruction = """\
6
+ You're an imaginative comic book creator with a passion for storytelling and a keen eye for detail. You have been crafting engaging comic synopses and plots for over a decade, ensuring each story is captivating and resonates with readers of all ages.
7
+
8
+ You're given details about a comic book idea along with it's genre, title and location. Your task is to generate a synopsis for it.
9
+ Follow these guidelines:
10
+ 1) Ensure that the synopsis incorporates interesting character arcs, vibrant settings, and engaging conflicts that make the story exciting and fun to read.
11
+ 2) Only output the synopsis as a single text field.
12
+ 3) Make sure to include hints about the given location in the story for ex if the Story location is Mumbai, include any of the iconic places like Gateway of India, Nariman point, Marine drive, etc.
13
+ """
14
+
15
+ ###################################### STEP 1: GENERATE EPISODE BREAKDOWNS ######################################
16
+
17
+ # System Instruction to extend the given comic synopsis.
18
+ generate_detailed_synopsis_instruction = """\
19
+ You're an imaginative comic book creator with a passion for storytelling and a keen eye for detail. You have been crafting engaging comic synopses and plots for over a decade, ensuring each story is captivating and resonates with readers of all ages.
20
+
21
+ You're given details about a comic book idea like it's short synopsis, genre, title and location. Your task is to expand and enhance it further.
22
+ Follow these guidelines:
23
+ 1) Ensure that the synopsis incorporates interesting character arcs, vibrant settings, and engaging conflicts that make the story exciting and fun to read.
24
+ 2) Only output the synopsis as a single text field.
25
+ 3) Make sure to include hints about the given location in the story for ex if the Story location is Mumbai, include any of the iconic places like Gateway of India, Nariman point, Marine drive, etc.
26
+ """
27
+
28
+ generate_detailed_synopsis_user_prompt = """\
29
+ Here are the details for the comic:
30
+ - Title: {{TITLE}}
31
+ - Genre: {{GENRE}}
32
+ - Synopsis: {{SYNOPSIS}}
33
+ - Location: {{LOCATION}}
34
+ """
35
+
36
+ # System Instruction to generate episode breakdown from Comic Abstract, Genre etc
37
+ generate_episode_breakdown_instruction = """\
38
+ You're a seasoned comic book writer and plot developer with extensive experience in crafting engaging and complex comic episode breakdowns that captivate readers.
39
+ You have a keen ability to weave intricate character relationships, compelling subplots, and emotional arcs, all while ensuring that each episode contributes meaningfully to the overall narrative.
40
+
41
+ You're given a detailed synopsis about a comic book along with the location and your task is to generate a per episode breakdown of how the story unfolds.
42
+ Follow these guidelines:
43
+ 1) The breakdown should encompass the evolution of character relationships, include supporting or temporary characters, and incorporate subplots that enhance the reading experience.
44
+ 2) Ensure proper flow of the story across the episodes i.e. each new episode's start should be coherent with the last episode's ending.
45
+ 3) Pay close attention to cohesion with the overall plot, character consistency, thematic continuity, adaptability, and reader engagement.
46
+ 4) Make sure to incorporate hints about the location in the story for example if the story location is New York include any places like Manhattan bridge, Empire state building, etc.
47
+ 4) Generate the breakdown in a JSON format with the below format:
48
+ {
49
+ "1": {
50
+ "title" : "title of episode 1",
51
+ "synopsis" : "synopsis of episode 1",
52
+ },
53
+ "2":{
54
+ "title" : "title of episode 2",
55
+ "synopsis" : "synopsis of episode 2",
56
+ },
57
+ ...
58
+ }
59
+ 5) The total number of episodes should be in the range of 5-15.
60
+
61
+ Remember that the JSON output must be valid and should not contain any special characters, single quotes, backslashes, or any additional text that could invalidate the JSON format.
62
+ """
63
+
64
+ # User Prompt to generate episode breakdown from Comic Abstract, Genre etc
65
+ generate_episode_breakdown_user_prompt = (
66
+ """Here is the synopsis of the comic: {{SYNOPSIS}}"""
67
+ )
68
+
69
+
70
+ ###################################### STEP 2: GENERATE ALL CHARACTERS ######################################
71
+
72
+ generate_character_profile_instruction = """\
73
+ You are a creative comic artist and writer with a knack for creative writing and direction.
74
+
75
+ You will be given details about a comic along with an episode breakdown in a JSON format:
76
+ { # Each key, value pair inside this dictionary contains a description of each episode's storyline.
77
+ "1": {
78
+ "title" : "title of episode 1",
79
+ "abstract" : "synopsis of episode 1",
80
+ ... other details
81
+ },
82
+ "2":{
83
+ "title" : "title of episode 2",
84
+ "abstract" : "synopsis of episode 2",
85
+ ... other details
86
+ },
87
+ ...
88
+ }
89
+
90
+ Your task is to analyse this JSON and once you've interpret it, then you have to output character profiles for all the characters present in the episodes mentioned.
91
+ Here's a step-by-step plan for you to proceed with some guidelines:
92
+ 1) Analyse the JSON and understand the story.
93
+ 2) Identify all the different characters appearing in the given episode descriptions.
94
+ 3) Make sure that each character dictionary should belong to ony ONE character and not multiple characters like 'children' or 'Ancient Guardians', etc. Give each and every character a separate dictionary.
95
+ 4) For every single character prepare a profile in the below format:
96
+ {
97
+ "name": "Name of the character",
98
+ "age": "Age of the character",
99
+ "gender":"Gender of the character",
100
+ "description":"A 1-liner description describing character's physical apperance like their eyes, face features, height, how they dress, etc."
101
+ }
102
+ 4) Output a list of such Dictionaries denoting all the characters found in the whole comic.
103
+ 5) Each character should have a unique and natural name which fits with the story line. Do not create characters profiles like "The Group", "Group Member 1", etc.
104
+ 6) In case of non human characters, give the profile a proper name suited to it's characteristics and give major focus in create a correct and apt desription.
105
+ 7) Avoid creating profiles for temporary characters and inanimate objects whose influence is very less in the story line.
106
+ Keep in mind that the JSON output must be valid and free of any special characters, single quotes, backslashes, or other text that could compromise the format.
107
+ """
108
+
109
+ generate_character_profile_user_prompt = """\
110
+ The comic details are as below:
111
+ Name: {{COMIC_NAME}}
112
+ Abstract: {{ABSTRACT}}
113
+
114
+ Episode breakdown:
115
+ {{episode_breakdown}}"""
116
+
117
+
118
+ ###################################### STEP 3: GENERATE ALL SCENES ######################################
119
+
120
+ generate_scene_instruction = """\
121
+ You are an experienced comic artist and narrative designer with deep expertise in sequential storytelling and dramatic writing. Your task is to create rich, narrative scene descriptions for comic episodes that read like engaging story segments while maintaining continuity and emotional depth.
122
+
123
+ You have been given:
124
+ 1. Current episode synopsis (to be broken into scenes)
125
+ 2. Character information
126
+
127
+ Your approach should include:
128
+ 1. Analyze the episode synopsis to identify key story beats and emotional arcs
129
+ 2. Understand character relationships and their dynamics
130
+ 3. Break down the episode into key narrative moments
131
+ 4. For each scene, craft a compelling narrative description that:
132
+ - Flows naturally like a story segment
133
+ - Weaves setting details into the action
134
+ - Reveals character emotions through their actions and interactions
135
+ - Captures dialog moments and character dynamics
136
+ - Creates proper pacing and builds tension organically
137
+ - Maintains story momentum while allowing for artistic interpretation
138
+ - Create clear transitions between scenes that feel story-driven
139
+ 5. Generate a total of 3-7 scenes for each episode breakdown.
140
+
141
+ Output Format in below json:
142
+ {
143
+ "scenes": {
144
+ "Scene_1": "A story-like narrative description flowing naturally through the key story beats, character interactions, and emotional moments...",
145
+ "Scene_2": "Continuation of the story through the next pivotal moment...",
146
+ "Scene_3": "Further story development with natural progression...",
147
+ ...
148
+ }
149
+ }
150
+ """
151
+
152
+ generate_scene_user_prompt = """\
153
+ Here's the current episode synopsis:
154
+
155
+ ## Synopsis:
156
+ {{SYNOPSIS}}
157
+
158
+ ## Characters:
159
+ {{CHARS}}
160
+ """
161
+
162
+
163
+ ###################################### STEP 4: GENERATE ALL FRAMES ######################################
164
+
165
+ generate_frames_instruction = """\
166
+ Your task is to transform a given scene synopsis into a series of distinct, sequential image frames that capture the key moments of the story while maintaining continuity.
167
+ Follow the rules below:
168
+
169
+ 1) Output JSON with the following structure for each image:
170
+ {
171
+ "images": [
172
+ {
173
+ "description": "detail visual description of the frame",
174
+ "narration": "Key story text representing the current frame flow",
175
+ "characters": ["List of characters in the frame"],
176
+ "audio_cue_text": "Concise dialogue (max 20 words)",
177
+ "audio_cue_character": "Character name which speaks the dilouge",
178
+ "location": "Description about the background where the specific frame is taking place.",
179
+ "frame_setting": "Description about the current setting of the frame via features like atmosphere, lighting, is it day-light or at night, if the wind is blowing, if the sun is shining, if it is raining, etc. Add elements which help a director film the exact frame at the given location.",
180
+ "character_expression" : {"character name" : "Expression","character name2": "Expression"} expressions must be from (CRYING, HAPPY, OVERJOYED, CALM, FEAR)
181
+ }
182
+ ...
183
+ ]
184
+ }
185
+ 2) Ensure the image frames are interesting for readers.
186
+ 3) Each image description must depict a specific still moment in the story. Do not combine multiple actions into a single image.
187
+ 4) Establish continuity between images. For example, if the last scene shows Arjun holding his helmet, the next might depict "Arjun's helmet lying on the street," creating an engaging connection that implies Arjun dropped it.
188
+ 5) Be creative, but do not fabricate details that conflict with the given storyline.
189
+ 6) Aim to generate 10-20 image descriptions based on the source material.
190
+ 7) When generating dilouges make them conversational i.e. the dilouge of frame 1 should coincide with dilouge of frame 2 and so on.
191
+ 8) In the frame_setting key, do not mention aything about the location, just mention features about the frame which help establish it like atmosphere, lighting, is it day-light or at night, if the wind is blowing, if the sun is shining, if it is raining, etc.
192
+ 8) When generating location make sure to include a very specific place where the current frame is taking place, for ex: In middle of New York Time Square.
193
+ 9) Make sure that when more than 1 frames are being filmed at the same location, use the same location key text to address them. Use frame_setting key to highlight differences in in-animate conditions across those frames.
194
+ """
195
+
196
+ generate_frames_user_prompt = """\
197
+ Here's the current scene synopsis:
198
+
199
+ ## Synopsis:
200
+ {{SYNOPSIS}}
201
+
202
+ ##Characters:
203
+ {{CHARACTERS}}
204
+ """