Spaces:
Runtime error
Runtime error
Manish Gupta commited on
Commit ·
e363158
1
Parent(s): 3ada460
First commit
Browse files- .gitignore +3 -0
- app.py +308 -0
- pipeline.py +404 -0
- src/aws_utils.py +136 -0
- src/custom_log.py +52 -0
- src/datastructures.py +55 -0
- src/grok_wrapper.py +81 -0
- src/parameters.py +24 -0
- 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}} 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 |
+
"""
|