tester343 commited on
Commit
f1eaa74
·
verified ·
1 Parent(s): 3df3e35

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +201 -751
app_enhanced.py CHANGED
@@ -1,34 +1,28 @@
1
  import os
2
  import time
3
- import threading
4
- import uuid
5
  import shutil
6
  import json
7
- import traceback
8
  import logging
9
  import string
10
  import random
11
- from concurrent.futures import ThreadPoolExecutor
12
 
13
- import gradio as gr
14
 
15
  # ============================================================================
16
- # 0. LOGGING & CONFIG
17
  # ============================================================================
18
 
19
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
20
  logger = logging.getLogger(__name__)
21
 
22
- print("===== Application Startup at", time.strftime("%Y-%m-%d %H:%M:%S"), "=====")
23
-
24
- # Use /tmp for HF Spaces
25
  BASE_USER_DIR = "/tmp/userdata"
26
  SAVED_COMICS_DIR = "/tmp/saved_comics"
27
  os.makedirs(BASE_USER_DIR, exist_ok=True)
28
  os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
29
 
30
  # ============================================================================
31
- # 1. ZEROGPU SETUP
32
  # ============================================================================
33
 
34
  ZERO_GPU = False
@@ -37,879 +31,335 @@ try:
37
  ZERO_GPU = True
38
  logger.info("✅ ZeroGPU detected")
39
  except ImportError:
40
- logger.info("⚠️ ZeroGPU not available, using CPU mode")
41
  class spaces:
42
  @staticmethod
43
  def GPU(duration=60):
44
- def decorator(func):
45
- return func
46
- return decorator
47
 
48
  # ============================================================================
49
- # 2. CORE DEPENDENCIES
50
  # ============================================================================
51
 
52
  try:
53
  import cv2
54
  import numpy as np
55
  from PIL import Image, ImageEnhance
56
- logger.info("✅ OpenCV and PIL loaded")
57
  except ImportError as e:
58
- logger.error(f"Missing dependency: {e}")
59
  cv2 = None
60
- np = None
61
  Image = None
62
 
63
  try:
64
  import srt
65
- logger.info("✅ SRT parser loaded")
66
  except ImportError:
67
  srt = None
68
- logger.warning("⚠️ SRT not available")
 
 
69
 
70
  # ============================================================================
71
- # 3. WHISPER TRANSCRIPTION (GPU ACCELERATED)
72
  # ============================================================================
73
 
74
  if ZERO_GPU:
75
  @spaces.GPU(duration=120)
76
- def transcribe_audio_gpu(video_path):
77
- """GPU-accelerated transcription"""
78
  try:
79
  import whisper
80
- logger.info("🎤 Loading Whisper model (GPU)...")
81
  model = whisper.load_model("tiny")
82
  result = model.transcribe(video_path)
83
- logger.info("✅ Transcription complete")
84
  return result.get('segments', [])
85
  except Exception as e:
86
- logger.error(f"GPU transcription failed: {e}")
87
  return None
88
  else:
89
- def transcribe_audio_gpu(video_path):
90
- return transcribe_audio_cpu(video_path)
91
 
92
-
93
- def transcribe_audio_cpu(video_path):
94
- """CPU fallback transcription"""
95
  try:
96
  import whisper
97
- logger.info("🎤 Loading Whisper model (CPU)...")
98
  model = whisper.load_model("tiny", device="cpu")
99
  result = model.transcribe(video_path, fp16=False)
100
- logger.info("✅ Transcription complete (CPU)")
101
  return result.get('segments', [])
102
  except Exception as e:
103
- logger.error(f"CPU transcription failed: {e}")
104
  return None
105
 
106
-
107
- def transcribe_with_fallback(video_path):
108
- """Try GPU first, fallback to CPU"""
109
  try:
110
- logger.info("Attempting GPU transcription...")
111
- segments = transcribe_audio_gpu(video_path)
112
- if segments is not None:
113
- return segments
114
- except Exception as e:
115
- error_msg = str(e).lower()
116
- if "quota" in error_msg or "429" in error_msg:
117
- logger.warning("⚠️ GPU quota exceeded, using CPU...")
118
- else:
119
- logger.warning(f"⚠️ GPU failed: {e}, using CPU...")
120
-
121
- return transcribe_audio_cpu(video_path)
122
-
123
 
124
  # ============================================================================
125
- # 4. HELPER CLASSES
126
  # ============================================================================
127
 
128
  class FaceDetector:
129
- """Face detection using Haar Cascades"""
130
  def __init__(self):
131
  self.cascade = None
132
- if cv2 is not None:
133
  try:
134
- cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
135
- if os.path.exists(cascade_path):
136
- self.cascade = cv2.CascadeClassifier(cascade_path)
137
- logger.info("✅ Face detector loaded")
138
- except Exception as e:
139
- logger.warning(f"Face detector failed: {e}")
140
-
141
- def detect_faces(self, image_path):
142
- if not self.cascade or cv2 is None:
143
- return []
144
- try:
145
- img = cv2.imread(image_path)
146
- if img is None:
147
- return []
148
- gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
149
- faces = self.cascade.detectMultiScale(gray, 1.1, 5, minSize=(30, 30))
150
- return [list(f) for f in faces]
151
- except:
152
- return []
153
-
154
- def get_lip_position(self, image_path, face_rect):
155
- try:
156
- x, y, w, h = face_rect
157
- return x + w // 2, y + int(h * 0.85)
158
- except:
159
- return -1, -1
160
-
161
-
162
- class ColorEnhancer:
163
- """Comic-style color enhancement"""
164
- def enhance(self, image_path):
165
- if Image is None:
166
- return False
167
- try:
168
- img = Image.open(image_path)
169
- img = ImageEnhance.Color(img).enhance(1.4)
170
- img = ImageEnhance.Contrast(img).enhance(1.25)
171
- img = ImageEnhance.Sharpness(img).enhance(1.2)
172
- img.save(image_path)
173
- return True
174
- except:
175
- return False
176
-
177
 
178
- class BubblePlacer:
179
- """Smart speech bubble placement"""
180
- def place(self, image_path, lip_pos=None):
181
  try:
182
- if Image is None:
183
- return 50, 20
184
- img = Image.open(image_path)
185
- w, h = img.size
186
-
187
- if lip_pos and lip_pos[0] > 0:
188
- lip_x, lip_y = lip_pos
189
- if lip_x < w / 2:
190
- return w - 140, max(10, lip_y - 60)
191
- return 20, max(10, lip_y - 60)
192
- return 50, 20
193
- except:
194
- return 50, 20
195
-
196
 
197
- # Initialize helpers
198
  face_detector = FaceDetector()
199
- color_enhancer = ColorEnhancer()
200
- bubble_placer = BubblePlacer()
201
 
 
 
 
 
 
 
 
 
202
 
203
- def generate_save_code(length=8):
204
- """Generate unique save code"""
205
  chars = string.ascii_uppercase + string.digits
206
  while True:
207
- code = ''.join(random.choices(chars, k=length))
208
  if not os.path.exists(os.path.join(SAVED_COMICS_DIR, code)):
209
  return code
210
 
211
-
212
  # ============================================================================
213
- # 5. COMIC GENERATOR CLASS
214
  # ============================================================================
215
 
216
- class ComicGenerator:
217
  def __init__(self, sid):
218
  self.sid = sid
219
- self.user_dir = os.path.join(BASE_USER_DIR, sid)
220
- self.video_path = os.path.join(self.user_dir, 'uploaded.mp4')
221
- self.frames_dir = os.path.join(self.user_dir, 'frames')
222
- self.output_dir = os.path.join(self.user_dir, 'output')
223
- self.status_file = os.path.join(self.output_dir, 'status.json')
224
- self.metadata_path = os.path.join(self.frames_dir, 'frame_metadata.json')
225
- self.video_fps = 25
226
-
227
- os.makedirs(self.frames_dir, exist_ok=True)
228
- os.makedirs(self.output_dir, exist_ok=True)
229
-
230
- def update_status(self, message, progress):
 
 
 
 
 
231
  try:
232
- with open(self.status_file, 'w') as f:
233
- json.dump({'message': message, 'progress': progress}, f)
234
- except:
235
- pass
236
-
237
- def get_status(self):
238
- try:
239
- if os.path.exists(self.status_file):
240
- with open(self.status_file, 'r') as f:
241
- return json.load(f)
242
- except:
243
- pass
244
- return {'message': 'Waiting...', 'progress': 0}
245
-
246
- def cleanup(self):
247
- """Clean previous run"""
248
- logger.info(f"🧹 Cleaning session {self.sid}")
249
- for directory in [self.frames_dir, self.output_dir]:
250
- if os.path.exists(directory):
251
- for f in os.listdir(directory):
252
- if f != 'status.json':
253
- try:
254
- os.remove(os.path.join(directory, f))
255
- except:
256
- pass
257
-
258
- def extract_keyframes(self, key_moments, max_frames=48):
259
- """Extract frames at key moments"""
260
- if cv2 is None:
261
- return False
262
-
263
- try:
264
- cap = cv2.VideoCapture(self.video_path)
265
- if not cap.isOpened():
266
- raise Exception("Cannot open video")
267
-
268
- fps = cap.get(cv2.CAP_PROP_FPS) or 25
269
- self.video_fps = fps
270
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
271
- duration = total_frames / fps
272
-
273
- key_moments.sort(key=lambda x: x['start'])
274
- frame_metadata = {}
275
- frame_count = 0
276
-
277
- for i, moment in enumerate(key_moments[:max_frames]):
278
- self.update_status(f"Extracting frame {i+1}/{min(len(key_moments), max_frames)}...",
279
- 25 + int(25 * i / min(len(key_moments), max_frames)))
280
-
281
- frame_time = (moment['start'] + moment['end']) / 2
282
- if frame_time > duration:
283
- continue
284
-
285
- frame_number = int(frame_time * fps)
286
- cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
287
  ret, frame = cap.read()
288
-
289
  if ret:
290
- fname = f"frame_{frame_count:04d}.png"
291
- fpath = os.path.join(self.frames_dir, fname)
292
- cv2.imwrite(fpath, frame)
293
-
294
- frame_metadata[fname] = {
295
- 'time': frame_time,
296
- 'dialogue': moment.get('text', ''),
297
- 'start': moment['start'],
298
- 'end': moment['end']
299
- }
300
- frame_count += 1
301
-
302
  cap.release()
303
 
304
- with open(self.metadata_path, 'w') as f:
305
- json.dump(frame_metadata, f, indent=2)
306
-
307
- logger.info(f"✅ Extracted {frame_count} keyframes")
308
  return True
309
-
310
  except Exception as e:
311
- logger.error(f"Keyframe extraction failed: {e}")
312
  return False
313
 
314
- def enhance_frames(self):
315
- """Enhance all frames"""
316
- frame_files = [f for f in os.listdir(self.frames_dir) if f.endswith('.png')]
317
- for fname in frame_files:
318
- color_enhancer.enhance(os.path.join(self.frames_dir, fname))
319
- logger.info("✅ Frame enhancement complete")
320
-
321
- def create_bubbles(self):
322
- """Create speech bubbles for frames"""
323
- frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
324
-
325
- metadata = {}
326
- if os.path.exists(self.metadata_path):
327
- with open(self.metadata_path, 'r') as f:
328
- metadata = json.load(f)
329
-
330
- bubbles_list = []
331
- for fname in frame_files:
332
- fpath = os.path.join(self.frames_dir, fname)
333
- meta = metadata.get(fname, {})
334
- dialogue = meta.get('dialogue', '') if isinstance(meta, dict) else ''
335
-
336
- # Detect faces and place bubbles
337
- faces = face_detector.detect_faces(fpath)
338
- lip_pos = face_detector.get_lip_position(fpath, faces[0]) if faces else (-1, -1)
339
- bx, by = bubble_placer.place(fpath, lip_pos)
340
-
341
- bubbles_list.append({
342
- 'dialog': dialogue,
343
- 'bubble_offset_x': bx,
344
- 'bubble_offset_y': by,
345
- 'lip_x': lip_pos[0],
346
- 'lip_y': lip_pos[1],
347
- 'emotion': 'normal'
348
- })
349
-
350
- return bubbles_list
351
-
352
- def generate_pages(self, bubbles_list):
353
- """Generate comic pages"""
354
- frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
355
 
356
- pages = []
357
- panels_per_page = 4
 
 
 
 
358
 
359
- for i in range(0, len(frame_files), panels_per_page):
360
- page_frames = frame_files[i:i + panels_per_page]
361
- page_bubbles = bubbles_list[i:i + panels_per_page]
362
-
363
- pages.append({
364
- 'panels': [{'image': f} for f in page_frames],
365
- 'bubbles': page_bubbles
366
- })
367
-
368
- return pages
369
 
370
- def save_results(self, pages):
371
- """Save results to JSON"""
372
  try:
373
- output_path = os.path.join(self.output_dir, 'pages.json')
374
- with open(output_path, 'w') as f:
375
- json.dump(pages, f, indent=2)
376
- logger.info("✅ Results saved")
377
- return True
378
- except Exception as e:
379
- logger.error(f"Save failed: {e}")
380
- return False
381
-
382
- def generate(self):
383
- """Main generation pipeline"""
384
- start_time = time.time()
385
-
386
- try:
387
- if cv2 is None:
388
- raise Exception("OpenCV not available")
389
-
390
- self.update_status("Initializing...", 5)
391
- self.cleanup()
392
-
393
- # Analyze video
394
- self.update_status("Analyzing video...", 10)
395
- cap = cv2.VideoCapture(self.video_path)
396
- if not cap.isOpened():
397
- raise Exception("Cannot open video")
398
 
399
- self.video_fps = cap.get(cv2.CAP_PROP_FPS) or 25
400
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
401
- duration = total_frames / self.video_fps
 
402
  cap.release()
403
- logger.info(f"📹 Video: {duration:.1f}s @ {self.video_fps:.1f}fps")
404
 
405
- # Transcribe
406
- self.update_status("Transcribing audio...", 15)
407
- segments = transcribe_with_fallback(self.video_path)
408
 
409
- # Create key moments
410
- if segments:
411
- key_moments = [{'text': s.get('text', ''), 'start': s.get('start', 0), 'end': s.get('end', 0)}
412
- for s in segments]
413
- logger.info(f"📝 Found {len(key_moments)} dialogue segments")
414
  else:
415
- # Fallback: regular intervals
416
- key_moments = [{'text': '', 'start': i * 3, 'end': i * 3 + 3}
417
- for i in range(min(16, int(duration / 3) + 1))]
418
- logger.warning("⚠️ Using fallback keyframes")
419
-
420
- # Extract frames
421
- self.update_status("Extracting keyframes...", 25)
422
- if not self.extract_keyframes(key_moments):
423
- raise Exception("Keyframe extraction failed")
424
-
425
- # Enhance
426
- self.update_status("Enhancing images...", 55)
427
- self.enhance_frames()
428
 
429
- # Create bubbles
430
- self.update_status("Placing speech bubbles...", 70)
431
- bubbles = self.create_bubbles()
432
 
433
- # Generate pages
434
- self.update_status("Generating pages...", 85)
435
- pages = self.generate_pages(bubbles)
436
-
437
- # Save
438
- self.update_status("Saving...", 95)
439
- self.save_results(pages)
440
-
441
- elapsed = time.time() - start_time
442
- logger.info(f"✅ Done in {elapsed:.1f}s")
443
- self.update_status("Complete!", 100)
444
- return True
445
 
 
 
446
  except Exception as e:
447
- logger.error(f"Generation failed: {e}")
448
- traceback.print_exc()
449
- self.update_status(f"Error: {str(e)}", -1)
450
- return False
451
-
452
- def regenerate_frame(self, filename, direction):
453
- """Regenerate frame forward/backward"""
454
- if cv2 is None:
455
- return {"success": False, "message": "OpenCV not available"}
456
-
457
- try:
458
- if not os.path.exists(self.metadata_path):
459
- return {"success": False, "message": "No metadata"}
460
-
461
- with open(self.metadata_path, 'r') as f:
462
- meta = json.load(f)
463
-
464
- if filename not in meta:
465
- return {"success": False, "message": "Frame not found"}
466
-
467
- data = meta[filename]
468
- current_time = data['time'] if isinstance(data, dict) else data
469
-
470
- offset = (1.0 / self.video_fps) * (1 if direction == 'forward' else -1)
471
- new_time = max(0, current_time + offset)
472
-
473
- cap = cv2.VideoCapture(self.video_path)
474
- cap.set(cv2.CAP_PROP_POS_MSEC, new_time * 1000)
475
- ret, frame = cap.read()
476
- cap.release()
477
-
478
- if ret:
479
- fpath = os.path.join(self.frames_dir, filename)
480
- cv2.imwrite(fpath, frame)
481
- color_enhancer.enhance(fpath)
482
-
483
- if isinstance(meta[filename], dict):
484
- meta[filename]['time'] = new_time
485
- else:
486
- meta[filename] = new_time
487
-
488
- with open(self.metadata_path, 'w') as f:
489
- json.dump(meta, f, indent=2)
490
-
491
- return {"success": True, "message": f"Moved to {new_time:.3f}s", "new_time": new_time}
492
-
493
- return {"success": False, "message": "End of video"}
494
-
495
- except Exception as e:
496
- return {"success": False, "message": str(e)}
497
-
498
- def goto_timestamp(self, filename, timestamp):
499
- """Jump to specific timestamp"""
500
- if cv2 is None:
501
- return {"success": False, "message": "OpenCV not available"}
502
-
503
- try:
504
- cap = cv2.VideoCapture(self.video_path)
505
- fps = cap.get(cv2.CAP_PROP_FPS) or 25
506
- total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
507
- duration = total / fps
508
-
509
- if timestamp < 0 or timestamp > duration:
510
- cap.release()
511
- return {"success": False, "message": f"Time must be 0-{duration:.1f}s"}
512
-
513
- cap.set(cv2.CAP_PROP_POS_MSEC, timestamp * 1000)
514
- ret, frame = cap.read()
515
- cap.release()
516
-
517
- if ret:
518
- fpath = os.path.join(self.frames_dir, filename)
519
- cv2.imwrite(fpath, frame)
520
- color_enhancer.enhance(fpath)
521
-
522
- if os.path.exists(self.metadata_path):
523
- with open(self.metadata_path, 'r') as f:
524
- meta = json.load(f)
525
- if filename in meta:
526
- if isinstance(meta[filename], dict):
527
- meta[filename]['time'] = timestamp
528
- else:
529
- meta[filename] = timestamp
530
- with open(self.metadata_path, 'w') as f:
531
- json.dump(meta, f, indent=2)
532
-
533
- return {"success": True, "message": f"Jumped to {timestamp:.3f}s"}
534
-
535
- return {"success": False, "message": "Invalid time"}
536
- except Exception as e:
537
- return {"success": False, "message": str(e)}
538
-
539
 
540
  # ============================================================================
541
- # 6. GRADIO HANDLERS
542
  # ============================================================================
543
 
544
- def new_session():
545
- return str(uuid.uuid4())[:8]
546
-
547
-
548
- def handle_upload(video_file, session_id, progress=gr.Progress()):
549
- """Handle video upload"""
550
- if video_file is None:
551
- return "❌ Please upload a video", session_id, "", gr.update(interactive=True)
552
 
553
- if not session_id:
554
- session_id = new_session()
555
 
556
- try:
557
- gen = ComicGenerator(session_id)
558
-
559
- # Copy video
560
- if hasattr(video_file, 'name'):
561
- shutil.copy(video_file.name, gen.video_path)
562
- else:
563
- shutil.copy(video_file, gen.video_path)
564
-
565
- progress(0.05, desc="Starting generation...")
566
-
567
- # Run generation (synchronous for progress tracking)
568
- gen.update_status("Starting...", 5)
569
-
570
- if cv2 is None:
571
- return "❌ OpenCV not available", session_id, "", gr.update(interactive=True)
572
-
573
- gen.cleanup()
574
-
575
- # Analyze video
576
- progress(0.1, desc="Analyzing video...")
577
- cap = cv2.VideoCapture(gen.video_path)
578
- if not cap.isOpened():
579
- return "❌ Cannot open video", session_id, "", gr.update(interactive=True)
580
-
581
- gen.video_fps = cap.get(cv2.CAP_PROP_FPS) or 25
582
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
583
- duration = total_frames / gen.video_fps
584
- cap.release()
585
-
586
- # Transcribe
587
- progress(0.15, desc="Transcribing audio (may take a while)...")
588
- segments = transcribe_with_fallback(gen.video_path)
589
-
590
- if segments:
591
- key_moments = [{'text': s.get('text', ''), 'start': s.get('start', 0), 'end': s.get('end', 0)}
592
- for s in segments]
593
- else:
594
- key_moments = [{'text': '', 'start': i * 3, 'end': i * 3 + 3}
595
- for i in range(min(16, int(duration / 3) + 1))]
596
-
597
- # Extract frames
598
- progress(0.3, desc="Extracting keyframes...")
599
- if not gen.extract_keyframes(key_moments):
600
- return "❌ Frame extraction failed", session_id, "", gr.update(interactive=True)
601
-
602
- # Enhance
603
- progress(0.6, desc="Enhancing images...")
604
- gen.enhance_frames()
605
-
606
- # Bubbles
607
- progress(0.75, desc="Placing speech bubbles...")
608
- bubbles = gen.create_bubbles()
609
-
610
- # Pages
611
- progress(0.9, desc="Generating pages...")
612
- pages = gen.generate_pages(bubbles)
613
- gen.save_results(pages)
614
-
615
- progress(1.0, desc="Complete!")
616
-
617
- # Generate HTML
618
- html = generate_comic_html(pages, session_id)
619
-
620
- return f"✅ Comic generated! ({len(pages)} pages)", session_id, html, gr.update(interactive=True)
621
-
622
- except Exception as e:
623
- logger.error(f"Error: {e}")
624
- traceback.print_exc()
625
- return f"❌ Error: {str(e)}", session_id, "", gr.update(interactive=True)
626
-
627
-
628
- def generate_comic_html(pages, session_id):
629
- """Generate HTML preview"""
630
- html = '''
631
- <style>
632
- .comic-wrapper { max-width: 900px; margin: 0 auto; font-family: 'Comic Sans MS', cursive; }
633
- .comic-page {
634
- display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
635
- background: #fff; padding: 12px; margin: 15px 0;
636
- border: 4px solid #222; border-radius: 8px;
637
- box-shadow: 6px 6px 0 #333;
638
- }
639
- .panel {
640
- position: relative; border: 3px solid #333;
641
- background: #f0f0f0; min-height: 200px; overflow: hidden;
642
- }
643
- .panel img { width: 100%; height: 200px; object-fit: cover; }
644
- .bubble {
645
- position: absolute; top: 8px; left: 8px; right: 8px;
646
- background: #fff; border: 2px solid #000; border-radius: 18px;
647
- padding: 8px 12px; font-size: 13px; font-weight: bold;
648
- box-shadow: 2px 2px 0 #333; max-width: 85%;
649
- }
650
- .bubble::after {
651
- content: ''; position: absolute; bottom: -15px; left: 25px;
652
- border: 10px solid transparent; border-top-color: #000;
653
- }
654
- .page-label { text-align: center; font-size: 20px; font-weight: bold; margin: 10px 0; }
655
- </style>
656
- <div class="comic-wrapper">
657
- '''
658
 
659
- for idx, page in enumerate(pages):
660
- html += f'<div class="page-label">📖 Page {idx + 1}</div>'
661
- html += '<div class="comic-page">'
662
-
663
- panels = page.get('panels', [])
664
- bubbles = page.get('bubbles', [])
665
-
666
- for i, panel in enumerate(panels):
667
- img_name = panel.get('image', '')
668
- bubble = bubbles[i] if i < len(bubbles) else {}
669
- dialog = str(bubble.get('dialog', ''))[:100]
670
-
671
- img_url = f"/file=/tmp/userdata/{session_id}/frames/{img_name}"
672
-
 
 
 
 
673
  html += f'''
674
- <div class="panel" data-filename="{img_name}">
675
- <img src="{img_url}" alt="Panel {i+1}" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22300%22 height=%22200%22><rect fill=%22%23ddd%22 width=%22300%22 height=%22200%22/><text x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22 fill=%22%23999%22>Panel {i+1}</text></svg>'">
676
- {f'<div class="bubble">{dialog}</div>' if dialog else ''}
677
- </div>
678
- '''
679
-
680
  html += '</div>'
681
-
682
  html += '</div>'
683
  return html
684
 
685
-
686
- def save_comic_handler(session_id):
687
- """Save comic and return code"""
688
- if not session_id:
689
- return "❌ No session", ""
690
-
691
  try:
692
- gen = ComicGenerator(session_id)
693
- pages_path = os.path.join(gen.output_dir, 'pages.json')
694
-
695
- if not os.path.exists(pages_path):
696
- return "❌ No comic to save", ""
697
 
698
- with open(pages_path, 'r') as f:
699
  pages = json.load(f)
700
 
701
- save_code = generate_save_code()
702
- save_dir = os.path.join(SAVED_COMICS_DIR, save_code)
703
- os.makedirs(save_dir, exist_ok=True)
704
 
705
- # Copy frames
706
- if os.path.exists(gen.frames_dir):
707
- saved_frames = os.path.join(save_dir, 'frames')
708
- if os.path.exists(saved_frames):
709
- shutil.rmtree(saved_frames)
710
- shutil.copytree(gen.frames_dir, saved_frames)
711
 
712
- # Save state
713
- save_data = {
714
- 'code': save_code,
715
- 'sid': session_id,
716
- 'pages': pages,
717
- 'savedAt': time.strftime('%Y-%m-%d %H:%M:%S')
718
- }
719
-
720
- with open(os.path.join(save_dir, 'state.json'), 'w') as f:
721
- json.dump(save_data, f, indent=2)
722
-
723
- logger.info(f"💾 Saved: {save_code}")
724
- return f"✅ Saved! Your code:", save_code
725
 
 
726
  except Exception as e:
727
- logger.error(f"Save error: {e}")
728
- return f"❌ Error: {e}", ""
729
-
730
 
731
- def load_comic_handler(code):
732
- """Load comic from code"""
733
- if not code:
734
- return "❌ Enter a code", "", ""
735
-
736
  code = code.strip().upper()
737
- save_dir = os.path.join(SAVED_COMICS_DIR, code)
738
- state_file = os.path.join(save_dir, 'state.json')
739
-
740
- if not os.path.exists(state_file):
741
- return "❌ Code not found", "", ""
742
 
743
  try:
744
- with open(state_file, 'r') as f:
745
  data = json.load(f)
746
 
747
- sid = data.get('sid', '')
748
- pages = data.get('pages', [])
 
 
749
 
750
- # Restore frames
751
- saved_frames = os.path.join(save_dir, 'frames')
752
- if sid and os.path.exists(saved_frames):
753
- user_frames = os.path.join(BASE_USER_DIR, sid, 'frames')
754
- os.makedirs(user_frames, exist_ok=True)
755
- for f in os.listdir(saved_frames):
756
- src = os.path.join(saved_frames, f)
757
- dst = os.path.join(user_frames, f)
758
- if not os.path.exists(dst):
759
- shutil.copy2(src, dst)
760
-
761
- html = generate_comic_html(pages, sid)
762
- return f"✅ Loaded comic (saved {data.get('savedAt', 'unknown')})", sid, html
763
 
 
764
  except Exception as e:
765
- return f"Error: {e}", "", ""
766
-
767
-
768
- def adjust_frame(session_id, filename, direction):
769
- """Adjust frame timing"""
770
- if not session_id or not filename:
771
- return "Select a panel first"
772
-
773
- gen = ComicGenerator(session_id)
774
- result = gen.regenerate_frame(filename, direction)
775
- return result.get('message', 'Error')
776
-
777
-
778
- def jump_to_time(session_id, filename, time_str):
779
- """Jump to timestamp"""
780
- if not session_id or not filename:
781
- return "Select a panel first"
782
-
783
- try:
784
- if ':' in time_str:
785
- parts = time_str.split(':')
786
- ts = int(parts[0]) * 60 + float(parts[1])
787
- else:
788
- ts = float(time_str)
789
- except:
790
- return "Invalid time format"
791
-
792
- gen = ComicGenerator(session_id)
793
- result = gen.goto_timestamp(filename, ts)
794
- return result.get('message', 'Error')
795
-
796
 
797
  # ============================================================================
798
- # 7. GRADIO UI
799
  # ============================================================================
800
 
801
- CSS = """
802
- .main-title { text-align: center; margin-bottom: 20px; }
803
- .upload-box { border: 2px dashed #ccc; padding: 20px; border-radius: 10px; }
804
- .comic-output { min-height: 400px; background: #fafafa; border-radius: 8px; padding: 10px; }
805
- """
806
-
807
- with gr.Blocks(title="🎬 Comic Generator", theme=gr.themes.Soft(), css=CSS) as demo:
808
-
809
- # State
810
- session_state = gr.State("")
811
- selected_panel = gr.State("")
812
-
813
- gr.Markdown("""
814
- # 🎬 Video to Comic Generator
815
- Transform any video into a comic book with AI-powered transcription!
816
- """, elem_classes="main-title")
817
 
818
- with gr.Tabs():
819
- # TAB 1: Generate
820
- with gr.TabItem("🎥 Generate Comic"):
821
- with gr.Row():
822
- with gr.Column(scale=1):
823
- video_input = gr.Video(label="📤 Upload Video")
824
- generate_btn = gr.Button("🚀 Generate Comic", variant="primary", size="lg")
825
- status_text = gr.Textbox(label="Status", interactive=False)
826
-
827
- with gr.Accordion("💾 Save & Load", open=False):
828
- save_btn = gr.Button("💾 Save Comic", variant="secondary")
829
- save_status = gr.Textbox(label="Save Status", interactive=False)
830
- save_code_out = gr.Textbox(label="Your Save Code", interactive=False)
831
-
832
- gr.Markdown("---")
833
- load_code_in = gr.Textbox(label="Enter Code to Load", placeholder="XXXXXXXX")
834
- load_btn = gr.Button("📂 Load Saved Comic")
835
-
836
- with gr.Column(scale=2):
837
- comic_html = gr.HTML(label="Comic Preview", elem_classes="comic-output")
838
-
839
- # TAB 2: Edit
840
- with gr.TabItem("✏️ Edit Panels"):
841
- gr.Markdown("### Panel Adjustment")
842
- with gr.Row():
843
- panel_input = gr.Textbox(label="Panel Filename", placeholder="frame_0001.png")
844
- with gr.Column():
845
- prev_btn = gr.Button("⬅️ Previous Frame")
846
- next_btn = gr.Button("➡️ Next Frame")
847
-
848
- with gr.Row():
849
- time_input = gr.Textbox(label="Jump to Time", placeholder="mm:ss or seconds")
850
- goto_btn = gr.Button("⏱️ Go")
851
-
852
- edit_status = gr.Textbox(label="Result", interactive=False)
853
-
854
- gr.Markdown("""
855
- ---
856
- ### 💡 Tips
857
- - Keep videos **under 3 minutes** for best results
858
- - Clear audio = better transcription
859
- - **Login to HuggingFace** for more GPU quota
860
- - Save your comic with a **unique code** to continue later!
861
- """)
862
-
863
- # Events
864
- generate_btn.click(
865
- fn=handle_upload,
866
- inputs=[video_input, session_state],
867
- outputs=[status_text, session_state, comic_html, generate_btn]
868
- )
869
 
870
- save_btn.click(
871
- fn=save_comic_handler,
872
- inputs=[session_state],
873
- outputs=[save_status, save_code_out]
874
- )
 
 
 
 
 
 
 
 
 
875
 
876
- load_btn.click(
877
- fn=load_comic_handler,
878
- inputs=[load_code_in],
879
- outputs=[status_text, session_state, comic_html]
880
- )
881
 
882
- prev_btn.click(
883
- fn=lambda sid, fname: adjust_frame(sid, fname, 'backward'),
884
- inputs=[session_state, panel_input],
885
- outputs=[edit_status]
886
- )
887
-
888
- next_btn.click(
889
- fn=lambda sid, fname: adjust_frame(sid, fname, 'forward'),
890
- inputs=[session_state, panel_input],
891
- outputs=[edit_status]
892
- )
893
-
894
- goto_btn.click(
895
- fn=jump_to_time,
896
- inputs=[session_state, panel_input, time_input],
897
- outputs=[edit_status]
898
- )
899
-
900
 
901
  # ============================================================================
902
- # 8. LAUNCH
903
  # ============================================================================
904
 
905
  if __name__ == "__main__":
906
- logger.info("🚀 Starting Comic Generator")
907
- logger.info(f"📁 Data: {BASE_USER_DIR}")
908
- logger.info(f"💾 Saves: {SAVED_COMICS_DIR}")
909
-
910
  demo.queue(max_size=5).launch(
911
  server_name="0.0.0.0",
912
  server_port=7860,
913
- show_error=True,
914
  allowed_paths=["/tmp"]
915
  )
 
1
  import os
2
  import time
 
 
3
  import shutil
4
  import json
 
5
  import logging
6
  import string
7
  import random
8
+ import uuid
9
 
10
+ print("===== Application Startup at", time.strftime("%Y-%m-%d %H:%M:%S"), "=====")
11
 
12
  # ============================================================================
13
+ # CONFIG
14
  # ============================================================================
15
 
16
+ logging.basicConfig(level=logging.INFO)
17
  logger = logging.getLogger(__name__)
18
 
 
 
 
19
  BASE_USER_DIR = "/tmp/userdata"
20
  SAVED_COMICS_DIR = "/tmp/saved_comics"
21
  os.makedirs(BASE_USER_DIR, exist_ok=True)
22
  os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
23
 
24
  # ============================================================================
25
+ # ZEROGPU
26
  # ============================================================================
27
 
28
  ZERO_GPU = False
 
31
  ZERO_GPU = True
32
  logger.info("✅ ZeroGPU detected")
33
  except ImportError:
34
+ logger.info("⚠️ No ZeroGPU")
35
  class spaces:
36
  @staticmethod
37
  def GPU(duration=60):
38
+ def dec(fn): return fn
39
+ return dec
 
40
 
41
  # ============================================================================
42
+ # DEPENDENCIES
43
  # ============================================================================
44
 
45
  try:
46
  import cv2
47
  import numpy as np
48
  from PIL import Image, ImageEnhance
49
+ logger.info("✅ CV2, PIL loaded")
50
  except ImportError as e:
51
+ logger.error(f"Missing: {e}")
52
  cv2 = None
 
53
  Image = None
54
 
55
  try:
56
  import srt
 
57
  except ImportError:
58
  srt = None
59
+
60
+ # Import Gradio AFTER other imports
61
+ import gradio as gr
62
 
63
  # ============================================================================
64
+ # WHISPER
65
  # ============================================================================
66
 
67
  if ZERO_GPU:
68
  @spaces.GPU(duration=120)
69
+ def transcribe_gpu(video_path):
 
70
  try:
71
  import whisper
 
72
  model = whisper.load_model("tiny")
73
  result = model.transcribe(video_path)
 
74
  return result.get('segments', [])
75
  except Exception as e:
76
+ logger.error(f"GPU transcribe error: {e}")
77
  return None
78
  else:
79
+ def transcribe_gpu(video_path):
80
+ return transcribe_cpu(video_path)
81
 
82
+ def transcribe_cpu(video_path):
 
 
83
  try:
84
  import whisper
 
85
  model = whisper.load_model("tiny", device="cpu")
86
  result = model.transcribe(video_path, fp16=False)
 
87
  return result.get('segments', [])
88
  except Exception as e:
89
+ logger.error(f"CPU transcribe error: {e}")
90
  return None
91
 
92
+ def transcribe(path):
 
 
93
  try:
94
+ result = transcribe_gpu(path)
95
+ if result: return result
96
+ except: pass
97
+ return transcribe_cpu(path)
 
 
 
 
 
 
 
 
 
98
 
99
  # ============================================================================
100
+ # HELPERS
101
  # ============================================================================
102
 
103
  class FaceDetector:
 
104
  def __init__(self):
105
  self.cascade = None
106
+ if cv2:
107
  try:
108
+ self.cascade = cv2.CascadeClassifier(
109
+ cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
110
+ )
111
+ except: pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ def detect(self, path):
114
+ if not self.cascade: return []
 
115
  try:
116
+ img = cv2.imread(path)
117
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
118
+ return list(self.cascade.detectMultiScale(gray, 1.1, 5))
119
+ except: return []
 
 
 
 
 
 
 
 
 
 
120
 
 
121
  face_detector = FaceDetector()
 
 
122
 
123
+ def enhance(path):
124
+ if not Image: return
125
+ try:
126
+ img = Image.open(path)
127
+ img = ImageEnhance.Color(img).enhance(1.4)
128
+ img = ImageEnhance.Contrast(img).enhance(1.2)
129
+ img.save(path)
130
+ except: pass
131
 
132
+ def gen_code():
 
133
  chars = string.ascii_uppercase + string.digits
134
  while True:
135
+ code = ''.join(random.choices(chars, k=8))
136
  if not os.path.exists(os.path.join(SAVED_COMICS_DIR, code)):
137
  return code
138
 
 
139
  # ============================================================================
140
+ # GENERATOR
141
  # ============================================================================
142
 
143
+ class Generator:
144
  def __init__(self, sid):
145
  self.sid = sid
146
+ self.base = os.path.join(BASE_USER_DIR, sid)
147
+ self.video = os.path.join(self.base, 'video.mp4')
148
+ self.frames = os.path.join(self.base, 'frames')
149
+ self.output = os.path.join(self.base, 'output')
150
+ self.meta = os.path.join(self.frames, 'meta.json')
151
+ self.fps = 25
152
+ os.makedirs(self.frames, exist_ok=True)
153
+ os.makedirs(self.output, exist_ok=True)
154
+
155
+ def clean(self):
156
+ for d in [self.frames, self.output]:
157
+ for f in os.listdir(d) if os.path.exists(d) else []:
158
+ try: os.remove(os.path.join(d, f))
159
+ except: pass
160
+
161
+ def extract(self, moments, max_n=48):
162
+ if not cv2: return False
163
  try:
164
+ cap = cv2.VideoCapture(self.video)
165
+ self.fps = cap.get(cv2.CAP_PROP_FPS) or 25
166
+ dur = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) / self.fps
167
+
168
+ meta, cnt = {}, 0
169
+ for m in moments[:max_n]:
170
+ t = (m['start'] + m['end']) / 2
171
+ if t > dur: continue
172
+ cap.set(cv2.CAP_PROP_POS_FRAMES, int(t * self.fps))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  ret, frame = cap.read()
 
174
  if ret:
175
+ name = f"frame_{cnt:04d}.png"
176
+ path = os.path.join(self.frames, name)
177
+ cv2.imwrite(path, frame)
178
+ enhance(path)
179
+ meta[name] = {'time': t, 'text': m.get('text', '')}
180
+ cnt += 1
 
 
 
 
 
 
181
  cap.release()
182
 
183
+ with open(self.meta, 'w') as f:
184
+ json.dump(meta, f)
 
 
185
  return True
 
186
  except Exception as e:
187
+ logger.error(f"Extract error: {e}")
188
  return False
189
 
190
+ def pages(self):
191
+ files = sorted([f for f in os.listdir(self.frames) if f.endswith('.png')])
192
+ meta = {}
193
+ if os.path.exists(self.meta):
194
+ with open(self.meta) as f:
195
+ meta = json.load(f)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
+ result = []
198
+ for i in range(0, len(files), 4):
199
+ chunk = files[i:i+4]
200
+ panels = [{'image': f} for f in chunk]
201
+ bubbles = [{'dialog': meta.get(f, {}).get('text', ''), 'x': 20, 'y': 15} for f in chunk]
202
+ result.append({'panels': panels, 'bubbles': bubbles})
203
 
204
+ with open(os.path.join(self.output, 'pages.json'), 'w') as f:
205
+ json.dump(result, f)
206
+ return result
 
 
 
 
 
 
 
207
 
208
+ def run(self, progress=None):
 
209
  try:
210
+ if not cv2: raise Exception("No OpenCV")
211
+ self.clean()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
+ if progress: progress(0.1, desc="Analyzing...")
214
+ cap = cv2.VideoCapture(self.video)
215
+ self.fps = cap.get(cv2.CAP_PROP_FPS) or 25
216
+ dur = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) / self.fps
217
  cap.release()
 
218
 
219
+ if progress: progress(0.2, desc="Transcribing...")
220
+ segs = transcribe(self.video)
 
221
 
222
+ if segs:
223
+ moments = [{'text': s.get('text', ''), 'start': s.get('start', 0), 'end': s.get('end', 0)} for s in segs]
 
 
 
224
  else:
225
+ moments = [{'text': '', 'start': i*3, 'end': i*3+3} for i in range(min(16, int(dur/3)+1))]
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
+ if progress: progress(0.5, desc="Extracting frames...")
228
+ if not self.extract(moments):
229
+ raise Exception("Extraction failed")
230
 
231
+ if progress: progress(0.9, desc="Creating pages...")
232
+ pages = self.pages()
 
 
 
 
 
 
 
 
 
 
233
 
234
+ if progress: progress(1.0, desc="Done!")
235
+ return True, pages
236
  except Exception as e:
237
+ logger.error(f"Error: {e}")
238
+ return False, str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  # ============================================================================
241
+ # HANDLERS
242
  # ============================================================================
243
 
244
+ def upload(video, sid, progress=gr.Progress()):
245
+ if not video:
246
+ return "❌ Upload a video", "", "", sid
 
 
 
 
 
247
 
248
+ if not sid:
249
+ sid = str(uuid.uuid4())[:8]
250
 
251
+ gen = Generator(sid)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
+ src = video.name if hasattr(video, 'name') else video
254
+ shutil.copy(src, gen.video)
255
+
256
+ ok, result = gen.run(progress)
257
+
258
+ if ok:
259
+ html = make_html(result, sid)
260
+ return f"✅ {len(result)} pages created", html, sid, sid
261
+ return f"❌ {result}", "", "", sid
262
+
263
+ def make_html(pages, sid):
264
+ html = '<div style="max-width:800px;margin:auto;font-family:Comic Sans MS,cursive">'
265
+ for i, p in enumerate(pages):
266
+ html += f'<h3 style="text-align:center">Page {i+1}</h3>'
267
+ html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;background:#fff;padding:10px;border:3px solid #333;margin-bottom:20px">'
268
+ for j, panel in enumerate(p['panels']):
269
+ img = panel['image']
270
+ txt = p['bubbles'][j]['dialog'][:80] if j < len(p['bubbles']) else ''
271
  html += f'''
272
+ <div style="position:relative;border:2px solid #333;min-height:180px;background:#eee">
273
+ <img src="/file=/tmp/userdata/{sid}/frames/{img}" style="width:100%;height:180px;object-fit:cover" onerror="this.style.display='none'">
274
+ {f'<div style="position:absolute;top:5px;left:5px;right:5px;background:#fff;border:2px solid #000;border-radius:12px;padding:6px;font-size:11px">{txt}</div>' if txt else ''}
275
+ </div>'''
 
 
276
  html += '</div>'
 
277
  html += '</div>'
278
  return html
279
 
280
+ def save(sid):
281
+ if not sid: return "No comic", ""
 
 
 
 
282
  try:
283
+ gen = Generator(sid)
284
+ pf = os.path.join(gen.output, 'pages.json')
285
+ if not os.path.exists(pf): return "Nothing to save", ""
 
 
286
 
287
+ with open(pf) as f:
288
  pages = json.load(f)
289
 
290
+ code = gen_code()
291
+ sdir = os.path.join(SAVED_COMICS_DIR, code)
292
+ os.makedirs(sdir, exist_ok=True)
293
 
294
+ if os.path.exists(gen.frames):
295
+ shutil.copytree(gen.frames, os.path.join(sdir, 'frames'), dirs_exist_ok=True)
 
 
 
 
296
 
297
+ with open(os.path.join(sdir, 'state.json'), 'w') as f:
298
+ json.dump({'sid': sid, 'pages': pages}, f)
 
 
 
 
 
 
 
 
 
 
 
299
 
300
+ return "✅ Saved!", code
301
  except Exception as e:
302
+ return f"Error: {e}", ""
 
 
303
 
304
+ def load(code):
 
 
 
 
305
  code = code.strip().upper()
306
+ sf = os.path.join(SAVED_COMICS_DIR, code, 'state.json')
307
+ if not os.path.exists(sf): return "Not found", "", ""
 
 
 
308
 
309
  try:
310
+ with open(sf) as f:
311
  data = json.load(f)
312
 
313
+ sid = data['sid']
314
+ frames_src = os.path.join(SAVED_COMICS_DIR, code, 'frames')
315
+ frames_dst = os.path.join(BASE_USER_DIR, sid, 'frames')
316
+ os.makedirs(frames_dst, exist_ok=True)
317
 
318
+ if os.path.exists(frames_src):
319
+ for fn in os.listdir(frames_src):
320
+ shutil.copy2(os.path.join(frames_src, fn), os.path.join(frames_dst, fn))
 
 
 
 
 
 
 
 
 
 
321
 
322
+ return "✅ Loaded!", make_html(data['pages'], sid), sid
323
  except Exception as e:
324
+ return f"Error: {e}", "", ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
  # ============================================================================
327
+ # UI
328
  # ============================================================================
329
 
330
+ with gr.Blocks(title="🎬 Comic Generator", theme=gr.themes.Soft()) as demo:
331
+ state = gr.State("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
+ gr.Markdown("# 🎬 Video to Comic Generator\nUpload a video to create a comic!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
+ with gr.Row():
336
+ with gr.Column(scale=1):
337
+ vid = gr.Video(label="📤 Upload Video")
338
+ btn = gr.Button("🚀 Generate", variant="primary", size="lg")
339
+ status = gr.Textbox(label="Status", interactive=False)
340
+
341
+ gr.Markdown("---")
342
+ save_btn = gr.Button("💾 Save")
343
+ code_out = gr.Textbox(label="Save Code", interactive=False)
344
+ code_in = gr.Textbox(label="Load Code", placeholder="XXXXXXXX")
345
+ load_btn = gr.Button("📂 Load")
346
+
347
+ with gr.Column(scale=2):
348
+ html = gr.HTML()
349
 
350
+ gr.Markdown("💡 Short videos work best. Login to HF for more GPU time.")
 
 
 
 
351
 
352
+ btn.click(upload, [vid, state], [status, html, code_out, state])
353
+ save_btn.click(save, [state], [status, code_out])
354
+ load_btn.click(load, [code_in], [status, html, state])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
 
356
  # ============================================================================
357
+ # LAUNCH
358
  # ============================================================================
359
 
360
  if __name__ == "__main__":
 
 
 
 
361
  demo.queue(max_size=5).launch(
362
  server_name="0.0.0.0",
363
  server_port=7860,
 
364
  allowed_paths=["/tmp"]
365
  )