knnn11 commited on
Commit
ed2ecef
·
verified ·
1 Parent(s): 642b5cb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +474 -60
app.py CHANGED
@@ -1,77 +1,491 @@
1
  import os
 
2
  import gradio as gr
 
 
 
3
  import requests
4
- from PIL import Image
5
  from io import BytesIO
6
- import logging
 
 
 
 
 
7
 
8
- # Set up logging
9
- logging.basicConfig(level=logging.INFO)
10
- logger = logging.getLogger(__name__)
11
 
12
- # Configuration - set these in your Hugging Face Space secrets
13
- RUNPOD_API_KEY = os.getenv("RUNPOD_API_KEY", "your_key_here")
14
- RUNPOD_ENDPOINT = os.getenv("RUNPOD_ENDPOINT", "your_endpoint_here")
15
-
16
- def debug_runpod(prompt):
17
- """Simplest possible RunPod test"""
18
- try:
19
- logger.info("Sending request to RunPod...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- payload = {
22
- "input": {
23
- "prompt": prompt,
24
- "width": 512,
25
- "height": 512,
26
- "num_inference_steps": 20,
27
- "return_base64": True
28
- }
29
- }
30
 
31
- headers = {
32
- "Authorization": f"Bearer {RUNPOD_API_KEY}",
33
- "Content-Type": "application/json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- logger.info(f"Payload: {payload}")
37
- response = requests.post(RUNPOD_ENDPOINT, json=payload, headers=headers, timeout=60)
38
- logger.info(f"Response status: {response.status_code}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- if response.status_code != 200:
41
- return None, f"API error: {response.text}"
42
-
43
- data = response.json()
44
- logger.info(f"Full response: {data}")
45
 
46
- if "output" in data and isinstance(data["output"], list):
47
- image_url = data["output"][0]
48
- img_data = requests.get(image_url).content
49
- return Image.open(BytesIO(img_data)), "Success!"
 
50
 
51
- elif "image" in data:
52
- img_data = base64.b64decode(data["image"].split(",")[1])
53
- return Image.open(BytesIO(img_data)), "Success!"
54
 
55
- return None, "No image data found in response"
 
56
 
57
- except Exception as e:
58
- logger.error(f"Error: {str(e)}")
59
- return None, f"Error: {str(e)}"
60
-
61
- # Basic Gradio interface
62
- with gr.Blocks() as app:
63
- gr.Markdown("## 🐞 RunPod Debugger")
64
-
65
- prompt = gr.Textbox(label="Test Prompt", value="A comic book superhero")
66
- generate = gr.Button("Test RunPod")
67
- output_image = gr.Image(label="Result")
68
- status = gr.Textbox(label="Debug Output")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
- generate.click(
71
- debug_runpod,
72
- inputs=[prompt],
73
- outputs=[output_image, status]
74
- )
75
 
76
- # Run with debug logging
77
- app.launch(debug=True, show_error=True)
 
 
 
1
  import os
2
+ import time
3
  import gradio as gr
4
+ import replicate
5
+ from dotenv import load_dotenv
6
+ from PIL import Image, ImageDraw, ImageFont, ImageOps
7
  import requests
 
8
  from io import BytesIO
9
+ import textwrap
10
+ import uuid
11
+ import json
12
+ from pathlib import Path
13
+ import numpy as np
14
+ import tempfile
15
 
16
+ # Load environment variables
17
+ load_dotenv()
 
18
 
19
+ # Configuration
20
+ class Config:
21
+ # Model configurations
22
+ SDXL_MODEL = "stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b"
23
+ VIDEO_MODEL = "stability-ai/stable-video-diffusion:3f0457e4619daac51203dedb472816fd4af51f3149fa7a9e0b5ffcf1b8172438"
24
+
25
+ # Path configurations
26
+ SAVE_FOLDER = "user_creations"
27
+ FONT_PATH = "assets/fonts/comic.ttf"
28
+ ASSET_PATH = "assets/"
29
+ BUBBLE_TEMPLATES = {
30
+ "Speech": "assets/bubbles/speech.png",
31
+ "Thought": "assets/bubbles/thought.png",
32
+ "Shout": "assets/bubbles/shout.png",
33
+ "Whisper": "assets/bubbles/whisper.png"
34
+ }
35
+
36
+ # Layout settings
37
+ LAYOUTS = {
38
+ "Single Panel": [{"x":0, "y":0, "w":1, "h":1}],
39
+ "Comic Strip": [{"x":i*0.33, "y":0, "w":0.33, "h":1} for i in range(3)],
40
+ "2x2 Grid": [
41
+ {"x":0, "y":0, "w":0.5, "h":0.5},
42
+ {"x":0.5, "y":0, "w":0.5, "h":0.5},
43
+ {"x":0, "y":0.5, "w":0.5, "h":0.5},
44
+ {"x":0.5, "y":0.5, "w":0.5, "h":0.5}
45
+ ]
46
+ }
47
+
48
+ @classmethod
49
+ def init(cls):
50
+ """Initialize required directories and files"""
51
+ Path(cls.SAVE_FOLDER).mkdir(exist_ok=True)
52
+ Path(cls.ASSET_PATH).mkdir(exist_ok=True)
53
 
54
+ # Download comic font if not exists
55
+ if not Path(cls.FONT_PATH).exists():
56
+ cls.download_comic_font()
 
 
 
 
 
 
57
 
58
+ # Create bubble templates if not exists
59
+ Path("assets/bubbles/").mkdir(exist_ok=True, parents=True)
60
+ cls.create_bubble_templates()
61
+
62
+ @classmethod
63
+ def download_comic_font(cls):
64
+ """Download comic font for speech bubbles"""
65
+ try:
66
+ url = "https://github.com/google/fonts/raw/main/ofl/comicneue/ComicNeue-Regular.ttf"
67
+ response = requests.get(url)
68
+ Path(cls.FONT_PATH).parent.mkdir(exist_ok=True)
69
+ with open(cls.FONT_PATH, 'wb') as f:
70
+ f.write(response.content)
71
+ except Exception as e:
72
+ print(f"Couldn't download comic font: {str(e)}")
73
+ cls.FONT_PATH = None
74
+
75
+ @classmethod
76
+ def create_bubble_templates(cls):
77
+ """Create default bubble templates if they don't exist"""
78
+ for bubble_type in cls.BUBBLE_TEMPLATES.values():
79
+ if not Path(bubble_type).exists():
80
+ img = Image.new('RGBA', (300, 150), (0, 0, 0, 0))
81
+ draw = ImageDraw.Draw(img)
82
+
83
+ if "speech" in bubble_type:
84
+ draw.rounded_rectangle([10, 10, 290, 140], radius=20,
85
+ fill=(255, 255, 255, 200), outline="black", width=3)
86
+ # Add tail
87
+ draw.polygon([(100, 140), (80, 160), (120, 160)],
88
+ fill=(255, 255, 255, 200), outline="black", width=3)
89
+
90
+ elif "thought" in bubble_type:
91
+ for i in range(3):
92
+ offset = i * 10
93
+ draw.ellipse([10-offset, 10-offset, 290+offset, 140+offset],
94
+ outline="black", width=2)
95
+
96
+ img.save(bubble_type)
97
+
98
+ Config.init()
99
+
100
+ class StoryEngine:
101
+ """Core engine for story generation and processing"""
102
+ DEFAULT_NEGATIVE = "blurry, deformed, bad anatomy, text errors, inconsistent character"
103
+
104
+ GENRE_PRESETS = {
105
+ "Pixar Style": {
106
+ "style": "3D animation, Pixar style, vibrant colors",
107
+ "negative": DEFAULT_NEGATIVE + ", realistic, photo"
108
+ },
109
+ "Anime": {
110
+ "style": "Anime style, vibrant colors, expressive characters",
111
+ "negative": DEFAULT_NEGATIVE + ", realistic, western, 3D"
112
+ },
113
+ "Comic Book": {
114
+ "style": "comic book style, bold outlines, halftone patterns",
115
+ "negative": DEFAULT_NEGATIVE + ", realistic, photograph"
116
+ },
117
+ "Storyboard": {
118
+ "style": "rough sketch, storyboard style, quick drawings",
119
+ "negative": DEFAULT_NEGATIVE + ", detailed, finished artwork"
120
  }
121
+ }
122
+
123
+ @staticmethod
124
+ def generate_images(prompt, num_outputs=1, style_preset=None):
125
+ """Generate consistent style images from prompt"""
126
+ try:
127
+ input_params = {
128
+ "prompt": f"{prompt}, {style_preset['style']}" if style_preset else prompt,
129
+ "negative_prompt": style_preset.get("negative", StoryEngine.DEFAULT_NEGATIVE),
130
+ "width": 1024,
131
+ "height": 1024,
132
+ "num_inference_steps": 30,
133
+ "num_outputs": num_outputs
134
+ }
135
+
136
+ output = replicate.run(Config.SDXL_MODEL, input=input_params)
137
+ return [url for url in output if url and isinstance(url, str) and url.startswith('http')]
138
+ except Exception as e:
139
+ print(f"Image generation failed: {str(e)}")
140
+ return []
141
+
142
+ @staticmethod
143
+ def generate_video(image_path, duration_sec=5):
144
+ """Generate video from image using Stable Video Diffusion"""
145
+ try:
146
+ with open(image_path, "rb") as f:
147
+ image_data = f.read()
148
+
149
+ output = replicate.run(
150
+ Config.VIDEO_MODEL,
151
+ input={
152
+ "input_image": image_data,
153
+ "motion_bucket_id": 80,
154
+ "fps": 8, # Lower FPS for smoother results
155
+ "duration": duration_sec
156
+ }
157
+ )
158
+ return output
159
+ except Exception as e:
160
+ print(f"Video generation failed: {str(e)}")
161
+ return None
162
+
163
+ class BubbleEditor:
164
+ """Handles customizable speech bubbles"""
165
+ @staticmethod
166
+ def add_custom_bubble(base_image, bubble_type, text, position, size):
167
+ """Add a customizable bubble to the image"""
168
+ try:
169
+ # Load bubble template
170
+ bubble_img = Image.open(Config.BUBBLE_TEMPLATES[bubble_type]).convert("RGBA")
171
+ bubble_img = bubble_img.resize(size)
172
+
173
+ # Create text layer
174
+ text_layer = Image.new('RGBA', size, (0, 0, 0, 0))
175
+ draw = ImageDraw.Draw(text_layer)
176
+
177
+ # Load font
178
+ if Config.FONT_PATH and os.path.exists(Config.FONT_PATH):
179
+ font = ImageFont.truetype(Config.FONT_PATH, int(size[1]/6))
180
+ else:
181
+ font = ImageFont.load_default()
182
+
183
+ # Calculate text position
184
+ lines = textwrap.wrap(text, width=20)
185
+ y_text = (size[1] - len(lines) * font.size) / 2
186
+
187
+ # Draw text
188
+ for line in lines:
189
+ line_width = font.getlength(line)
190
+ x_text = (size[0] - line_width) / 2
191
+ draw.text((x_text, y_text), line, fill="black", font=font)
192
+ y_text += font.size
193
+
194
+ # Combine bubble and text
195
+ bubble_with_text = Image.alpha_composite(bubble_img, text_layer)
196
+
197
+ # Paste onto base image
198
+ base_image.paste(bubble_with_text, position, bubble_with_text)
199
+ return base_image
200
+ except Exception as e:
201
+ print(f"Bubble editing error: {str(e)}")
202
+ return base_image
203
+
204
+ class VideoRenderer:
205
+ """Handles video and storyboard rendering"""
206
+ @staticmethod
207
+ def create_storyboard(images, script):
208
+ """Create professional storyboard with script"""
209
+ try:
210
+ storyboard = Image.new('RGB', (1024, 1024), (240, 240, 240))
211
+ draw = ImageDraw.Draw(storyboard)
212
+
213
+ # Load font
214
+ if Config.FONT_PATH and os.path.exists(Config.FONT_PATH):
215
+ title_font = ImageFont.truetype(Config.FONT_PATH, 40)
216
+ script_font = ImageFont.truetype(Config.FONT_PATH, 24)
217
+ else:
218
+ title_font = ImageFont.load_default(size=24)
219
+ script_font = ImageFont.load_default(size=16)
220
+
221
+ # Add title
222
+ draw.text((50, 30), "STORYBOARD", fill="black", font=title_font)
223
+
224
+ # Layout panels (2x2 grid)
225
+ for i, img_url in enumerate(images[:4]):
226
+ try:
227
+ response = requests.get(img_url)
228
+ img = Image.open(BytesIO(response.content))
229
+
230
+ x = (i % 2) * 512
231
+ y = 100 + (i // 2) * 412
232
+
233
+ panel = img.resize((492, 392))
234
+ storyboard.paste(panel, (x + 10, y))
235
+
236
+ # Add panel number
237
+ draw.rectangle([x + 10, y, x + 50, y + 40], fill="white")
238
+ draw.text((x + 20, y + 10), str(i+1), fill="black", font=title_font)
239
+ except Exception as e:
240
+ print(f"Couldn't process image {i}: {str(e)}")
241
+ continue
242
+
243
+ # Add script area
244
+ draw.rectangle([50, 824, 974, 994], fill="white", outline="black", width=2)
245
+
246
+ # Add script text
247
+ y_offset = 840
248
+ for line in textwrap.wrap(script, width=80)[:5]:
249
+ draw.text((60, y_offset), line, fill="black", font=script_font)
250
+ y_offset += 30
251
+
252
+ return storyboard
253
+ except Exception as e:
254
+ print(f"Storyboard creation failed: {str(e)}")
255
+ return None
256
+
257
+ def create_interface():
258
+ """Create the Gradio interface"""
259
+ with gr.Blocks(title="Comic Creator Pro", theme="soft") as app:
260
+ # Store for bubble positions
261
+ bubble_data = gr.State([])
262
 
263
+ # User Story Input
264
+ with gr.Row():
265
+ with gr.Column(scale=2):
266
+ script_input = gr.TextArea(
267
+ label="Your Script",
268
+ lines=8,
269
+ value="Detective (The clues point this way) examines the scene (thinks: Something's not right here)"
270
+ )
271
+
272
+ with gr.Row():
273
+ genre_select = gr.Dropdown(
274
+ list(StoryEngine.GENRE_PRESETS.keys()),
275
+ label="Style",
276
+ value="Comic Book"
277
+ )
278
+ layout_select = gr.Dropdown(
279
+ list(Config.LAYOUTS.keys()),
280
+ label="Layout",
281
+ value="Single Panel"
282
+ )
283
+
284
+ char_consistency = gr.Textbox(
285
+ label="Character Consistency",
286
+ value="hair_color:black, eye_color:green, outfit:detective"
287
+ )
288
+
289
+ negative_prompt = gr.Textbox(
290
+ label="Negative Prompt",
291
+ value=StoryEngine.DEFAULT_NEGATIVE
292
+ )
293
+
294
+ duration_slider = gr.Slider(
295
+ minimum=3,
296
+ maximum=15,
297
+ value=5,
298
+ step=1,
299
+ label="Video Duration (seconds)"
300
+ )
301
+
302
+ with gr.Column():
303
+ # Bubble customization tools
304
+ with gr.Accordion("Bubble Editor", open=False):
305
+ bubble_type = gr.Dropdown(
306
+ list(Config.BUBBLE_TEMPLATES.keys()),
307
+ label="Bubble Type",
308
+ value="Speech"
309
+ )
310
+ bubble_text = gr.Textbox(
311
+ label="Bubble Text",
312
+ value="Enter your text here"
313
+ )
314
+ bubble_size = gr.Slider(
315
+ minimum=50,
316
+ maximum=300,
317
+ value=150,
318
+ step=10,
319
+ label="Bubble Size"
320
+ )
321
+ add_bubble_btn = gr.Button("Add Bubble", variant="secondary")
322
+
323
+ preview_image = gr.Image(
324
+ label="Preview",
325
+ interactive=True,
326
+ tool="select"
327
+ )
328
 
329
+ # Generation Controls
330
+ with gr.Row():
331
+ generate_btn = gr.Button("Generate Panels", variant="primary")
332
+ generate_storyboard_btn = gr.Button("Create Storyboard")
333
+ generate_video_btn = gr.Button("Generate Video")
334
 
335
+ # Output Section
336
+ with gr.Tabs():
337
+ with gr.Tab("Comic Panels"):
338
+ panel_gallery = gr.Gallery(label="Generated Panels")
339
+ comic_output = gr.Image(label="Final Comic")
340
 
341
+ with gr.Tab("Storyboard"):
342
+ storyboard_output = gr.Image(label="Storyboard")
 
343
 
344
+ with gr.Tab("Video"):
345
+ video_output = gr.Video(label="Generated Video")
346
 
347
+ # Status
348
+ status = gr.Textbox(label="Status", visible=False)
349
+
350
+ # Generation Functions
351
+ def generate_panels(script, genre, consistency, negative):
352
+ """Generate comic panels from script"""
353
+ try:
354
+ full_prompt = f"{script}. {consistency}"
355
+ style_preset = StoryEngine.GENRE_PRESETS.get(genre)
356
+
357
+ # Update negative prompt
358
+ if style_preset:
359
+ style_preset["negative"] = negative
360
+
361
+ frames = StoryEngine.generate_images(
362
+ full_prompt,
363
+ num_outputs=4,
364
+ style_preset=style_preset
365
+ )
366
+
367
+ if not frames:
368
+ return [], None, "Failed to generate panels"
369
+
370
+ return frames, None, "Panels generated successfully"
371
+ except Exception as e:
372
+ print(f"Panel generation error: {str(e)}")
373
+ return [], None, f"Error: {str(e)}"
374
+
375
+ generate_btn.click(
376
+ generate_panels,
377
+ inputs=[script_input, genre_select, char_consistency, negative_prompt],
378
+ outputs=[panel_gallery, comic_output, status]
379
+ )
380
+
381
+ def create_storyboard(script, genre):
382
+ """Generate storyboard from script"""
383
+ try:
384
+ style_preset = StoryEngine.GENRE_PRESETS.get(genre)
385
+ frames = StoryEngine.generate_images(
386
+ f"Storyboard frame: {script}",
387
+ num_outputs=4,
388
+ style_preset=style_preset
389
+ )
390
+
391
+ if not frames:
392
+ return None, "Failed to generate storyboard frames"
393
+
394
+ storyboard = VideoRenderer.create_storyboard(frames, script)
395
+ if storyboard:
396
+ return storyboard, "Storyboard created successfully!"
397
+ return None, "Failed to create storyboard"
398
+ except Exception as e:
399
+ print(f"Storyboard generation error: {str(e)}")
400
+ return None, f"Error: {str(e)}"
401
+
402
+ generate_storyboard_btn.click(
403
+ create_storyboard,
404
+ inputs=[script_input, genre_select],
405
+ outputs=[storyboard_output, status]
406
+ )
407
+
408
+ def generate_video(images, duration):
409
+ """Generate video from selected image"""
410
+ try:
411
+ if not images:
412
+ return None, "No images to generate video"
413
+
414
+ # Use first image for video generation
415
+ img_url = images[0] if isinstance(images, list) else images
416
+ response = requests.get(img_url)
417
+
418
+ with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
419
+ tmp.write(response.content)
420
+ tmp_path = tmp.name
421
+
422
+ video_url = StoryEngine.generate_video(tmp_path, duration)
423
+ os.unlink(tmp_path)
424
+
425
+ if not video_url:
426
+ return None, "Failed to generate video"
427
+
428
+ return video_url, "Video generated successfully!"
429
+ except Exception as e:
430
+ print(f"Video generation error: {str(e)}")
431
+ return None, f"Error: {str(e)}"
432
+
433
+ generate_video_btn.click(
434
+ generate_video,
435
+ inputs=[panel_gallery, duration_slider],
436
+ outputs=[video_output, status]
437
+ )
438
+
439
+ # Bubble editing functions
440
+ def add_bubble(image, bubble_type, text, size, data):
441
+ """Add bubble to image"""
442
+ try:
443
+ if image is None:
444
+ return None, data, "No image to edit"
445
+
446
+ # Convert from Gradio format if needed
447
+ if isinstance(image, dict):
448
+ image = image["image"]
449
+ elif isinstance(image, str):
450
+ if image.startswith('http'):
451
+ response = requests.get(image)
452
+ image = Image.open(BytesIO(response.content))
453
+ else:
454
+ image = Image.open(image)
455
+
456
+ # Default position (center)
457
+ position = (
458
+ int(image.width/2 - size/2),
459
+ int(image.height/4)
460
+ )
461
+
462
+ # Add bubble
463
+ edited_image = BubbleEditor.add_custom_bubble(
464
+ image, bubble_type, text, position, (size, size//2)
465
+ )
466
+
467
+ # Update bubble data
468
+ new_data = data + [{
469
+ "type": bubble_type,
470
+ "text": text,
471
+ "position": position,
472
+ "size": (size, size//2)
473
+ }]
474
+
475
+ return edited_image, new_data, "Bubble added successfully"
476
+ except Exception as e:
477
+ print(f"Bubble addition error: {str(e)}")
478
+ return image, data, f"Error: {str(e)}"
479
+
480
+ add_bubble_btn.click(
481
+ add_bubble,
482
+ inputs=[preview_image, bubble_type, bubble_text, bubble_size, bubble_data],
483
+ outputs=[preview_image, bubble_data, status]
484
+ )
485
 
486
+ return app
 
 
 
 
487
 
488
+ if __name__ == "__main__":
489
+ # Create and launch the interface
490
+ app = create_interface()
491
+ app.launch(share=True, server_name="0.0.0.0", server_port=7860)