jhh6576 commited on
Commit
188f1cb
·
verified ·
1 Parent(s): 4a9ef30

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +70 -281
app_enhanced.py CHANGED
@@ -65,7 +65,6 @@ except Exception as e:
65
 
66
  app = Flask(__name__)
67
 
68
- # --- NEW: Embedded HTML with Gemini-style loading animation ---
69
  INDEX_HTML = '''
70
  <!DOCTYPE html>
71
  <html lang="en">
@@ -134,16 +133,12 @@ INDEX_HTML = '''
134
  margin-top: 20px;
135
  }
136
  .submit-btn:hover { background-color: #d35400; }
137
-
138
- /* Loading Animation Container */
139
  .loading-container {
140
  display: none;
141
  flex-direction: column;
142
  align-items: center;
143
  justify-content: center;
144
  }
145
-
146
- /* Gemini-style Loader */
147
  .loader {
148
  width: 120px;
149
  height: 20px;
@@ -182,7 +177,6 @@ INDEX_HTML = '''
182
  </head>
183
  <body>
184
  <div class="container" id="main-container">
185
-
186
  <div id="upload-view">
187
  <h1>🎬 Movie to Comic Generator</h1>
188
  <form id="upload-form">
@@ -192,17 +186,14 @@ INDEX_HTML = '''
192
  <button class="submit-btn" type="submit">Generate Comic</button>
193
  </form>
194
  </div>
195
-
196
  <div class="loading-container" id="loading-view">
197
  <div class="loader"></div>
198
  <p id="status-text">Starting...</p>
199
  <p id="final-message">✅ Generation Complete! Opening your comic...</p>
200
  </div>
201
  </div>
202
-
203
  <script>
204
  let statusInterval;
205
-
206
  function updateFileName(input) {
207
  const fileNameSpan = document.getElementById('file-name');
208
  if (input.files && input.files.length > 0) {
@@ -211,27 +202,21 @@ INDEX_HTML = '''
211
  fileNameSpan.textContent = 'No file selected';
212
  }
213
  }
214
-
215
  async function checkStatus() {
216
  try {
217
  const response = await fetch('/status');
218
  const data = await response.json();
219
-
220
  const statusText = document.getElementById('status-text');
221
  statusText.textContent = data.message;
222
-
223
  if (data.progress >= 100) {
224
  clearInterval(statusInterval);
225
  document.querySelector('.loader').style.display = 'none';
226
  statusText.style.display = 'none';
227
  document.getElementById('final-message').style.display = 'block';
228
-
229
- // Automatically open the comic in a new tab after a short delay
230
  setTimeout(() => {
231
  window.open('/comic', '_blank');
232
- }, 1500); // 1.5 second delay
233
  } else if (data.progress < 0) {
234
- // Handle error state
235
  clearInterval(statusInterval);
236
  statusText.textContent = "An error occurred. Please check the logs.";
237
  statusText.style.color = '#e74c3c';
@@ -239,38 +224,29 @@ INDEX_HTML = '''
239
  }
240
  } catch (error) {
241
  console.error("Error fetching status:", error);
242
- const statusText = document.getElementById('status-text');
243
- statusText.textContent = "Error checking status. Retrying...";
244
  }
245
  }
246
-
247
  document.getElementById('upload-form').addEventListener('submit', async function(event) {
248
  event.preventDefault();
249
-
250
  const fileInput = document.getElementById('file-upload');
251
  if (!fileInput.files || fileInput.files.length === 0) {
252
  alert("Please select a video file first.");
253
  return;
254
  }
255
-
256
  document.getElementById('upload-view').style.display = 'none';
257
  document.getElementById('loading-view').style.display = 'flex';
258
-
259
  const formData = new FormData();
260
  formData.append('file', fileInput.files[0]);
261
-
262
  try {
263
  const response = await fetch('/uploader', {
264
  method: 'POST',
265
  body: formData
266
  });
267
-
268
  if (!response.ok) {
269
  throw new Error('Upload failed!');
270
  }
271
-
272
- statusInterval = setInterval(checkStatus, 2000); // Check every 2 seconds
273
-
274
  } catch (error) {
275
  console.error("Upload error:", error);
276
  document.getElementById('status-text').textContent = "Failed to start generation. Please try again.";
@@ -286,13 +262,11 @@ os.makedirs('frames/final', exist_ok=True)
286
  os.makedirs('output', exist_ok=True)
287
 
288
  def update_status(message, progress):
289
- """Writes the current progress to a JSON file."""
290
  status_file = os.path.join('output', 'status.json')
291
  with open(status_file, 'w') as f:
292
  json.dump({'message': message, 'progress': progress}, f)
293
 
294
  class EnhancedComicGenerator:
295
- """High-quality comic generation with AI enhancement"""
296
  def __init__(self):
297
  self.video_path = 'video/uploaded.mp4'
298
  self.frames_dir = 'frames/final'
@@ -301,7 +275,7 @@ class EnhancedComicGenerator:
301
  self.video_fps = None
302
 
303
  def cleanup_generated(self):
304
- print("🧹 Performing full cleanup of previous run...")
305
  if os.path.isdir(self.frames_dir): shutil.rmtree(self.frames_dir)
306
  if os.path.isdir(self.output_dir): shutil.rmtree(self.output_dir)
307
  if os.path.isdir('temp'): shutil.rmtree('temp')
@@ -310,91 +284,38 @@ class EnhancedComicGenerator:
310
  os.makedirs(self.output_dir, exist_ok=True)
311
  print("✅ Cleanup complete.")
312
 
313
- def detect_eye_state(self, frame_path):
314
- try:
315
- img = cv2.imread(frame_path)
316
- gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
317
- face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
318
- eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')
319
- faces = face_cascade.detectMultiScale(gray, 1.3, 5)
320
- for (x, y, w, h) in faces:
321
- roi_gray = gray[y:y+h, x:x+w]
322
- eyes = eye_cascade.detectMultiScale(roi_gray)
323
- if len(eyes) == 0:
324
- return 'closed'
325
- elif len(eyes) == 1:
326
- return 'semi-closed'
327
- for (ex, ey, ew, eh) in eyes:
328
- eye_region = roi_gray[ey:ey+eh, ex:ex+ew]
329
- vert_var = np.var(eye_region, axis=0).mean()
330
- if vert_var < 500:
331
- return 'semi-closed'
332
- return 'open'
333
- except:
334
- return 'open'
335
-
336
  def regenerate_frame(self, frame_filename, direction):
337
  try:
338
  if not self.video_fps:
339
- return {"success": False, "message": "Video FPS not found. Please regenerate the comic first."}
340
-
341
  metadata_path = 'frames/frame_metadata.json'
342
  if not os.path.exists(metadata_path):
343
  return {"success": False, "message": "Frame metadata missing."}
344
-
345
  with open(metadata_path, 'r') as f:
346
  frame_to_time = json.load(f)
347
-
348
  if frame_filename not in frame_to_time:
349
- return {"success": False, "message": "Panel not linked to original video."}
350
-
351
- if isinstance(frame_to_time[frame_filename], dict):
352
- current_time = frame_to_time[frame_filename]['time']
353
- else:
354
- current_time = frame_to_time[frame_filename]
355
-
356
  frame_duration = 1.0 / self.video_fps
357
-
358
- if direction == 'forward':
359
- target_time = current_time + frame_duration
360
- elif direction == 'backward':
361
- target_time = current_time - frame_duration
362
- else:
363
- return {"success": False, "message": "Invalid direction specified."}
364
-
365
  target_time = max(0, target_time)
366
-
367
  cap = cv2.VideoCapture(self.video_path)
368
- if not cap.isOpened():
369
- return {"success": False, "message": "Cannot open video."}
370
-
371
  cap.set(cv2.CAP_PROP_POS_MSEC, target_time * 1000)
372
  ret, frame = cap.read()
373
  cap.release()
374
-
375
  if not ret or frame is None:
376
- return {"success": False, "message": f"No frame available at {target_time:.2f}s."}
377
-
378
  new_path = os.path.join(self.frames_dir, frame_filename)
379
  cv2.imwrite(new_path, frame)
380
-
381
  if isinstance(frame_to_time[frame_filename], dict):
382
  frame_to_time[frame_filename]['time'] = target_time
383
  else:
384
  frame_to_time[frame_filename] = target_time
385
-
386
- with open(metadata_path, 'w') as f:
387
- json.dump(frame_to_time, f, indent=2)
388
-
389
  message = f"Adjusted {direction} to {target_time:.3f}s"
390
  print(f"✅ {message}")
391
-
392
- return {
393
- "success": True,
394
- "message": message,
395
- "new_filename": frame_filename
396
- }
397
-
398
  except Exception as e:
399
  traceback.print_exc()
400
  return {"success": False, "message": str(e)}
@@ -402,48 +323,31 @@ class EnhancedComicGenerator:
402
  def get_frame_at_timestamp(self, frame_filename, timestamp_seconds):
403
  try:
404
  metadata_path = 'frames/frame_metadata.json'
405
- if not os.path.exists(metadata_path):
406
- return {"success": False, "message": "Frame metadata missing."}
407
-
408
  cap = cv2.VideoCapture(self.video_path)
409
- if not cap.isOpened():
410
- return {"success": False, "message": "Cannot open video."}
411
-
412
  fps = cap.get(cv2.CAP_PROP_FPS)
413
  if fps == 0: fps = 25
414
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
415
- duration = total_frames / fps
416
  if timestamp_seconds < 0 or timestamp_seconds > duration:
417
  cap.release()
418
- return {"success": False, "message": f"Timestamp must be between 0 and {duration:.2f} seconds."}
419
-
420
  cap.set(cv2.CAP_PROP_POS_MSEC, timestamp_seconds * 1000)
421
  ret, frame = cap.read()
422
  cap.release()
423
-
424
- if not ret or frame is None:
425
- return {"success": False, "message": f"Could not retrieve frame at {timestamp_seconds:.2f}s."}
426
-
427
  new_path = os.path.join(self.frames_dir, frame_filename)
428
  cv2.imwrite(new_path, frame)
429
-
430
- with open(metadata_path, 'r') as f:
431
- frame_to_time = json.load(f)
432
-
433
  if frame_filename in frame_to_time:
434
  if isinstance(frame_to_time[frame_filename], dict):
435
  frame_to_time[frame_filename]['time'] = timestamp_seconds
436
  else:
437
  frame_to_time[frame_filename] = timestamp_seconds
438
-
439
- with open(metadata_path, 'w') as f:
440
- json.dump(frame_to_time, f, indent=2)
441
-
442
  message = f"Jumped to timestamp {timestamp_seconds:.3f}s"
443
  print(f"✅ {message}")
444
-
445
  return { "success": True, "message": message }
446
-
447
  except Exception as e:
448
  traceback.print_exc()
449
  return {"success": False, "message": str(e)}
@@ -451,58 +355,39 @@ class EnhancedComicGenerator:
451
  def generate_keyframes_from_moments(self, video_path, key_moments, max_frames=48):
452
  try:
453
  cap = cv2.VideoCapture(video_path)
454
- if not cap.isOpened():
455
- raise Exception("Cannot open video for keyframe extraction")
456
-
457
- fps = self.video_fps
458
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
459
  duration = total_frames / fps
460
-
461
  key_moments.sort(key=lambda x: x['start'])
462
-
463
- if len(key_moments) > max_frames:
464
- # Intelligent sampling logic...
465
- pass
466
-
467
- frame_metadata = {}
468
- frame_count = 0
469
-
470
  for i, moment in enumerate(key_moments):
471
  update_status(f"Extracting frame {i+1}/{len(key_moments)}...", 25 + int(20 * (i / len(key_moments))))
472
-
473
  frame_time = (moment['start'] + moment['end']) / 2
474
  if frame_time > duration: continue
475
-
476
  frame_number = int(frame_time * fps)
477
  cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
478
  ret, frame = cap.read()
479
-
480
  if ret:
481
  frame_filename = f"frame_{frame_count:04d}.png"
482
  frame_path = os.path.join(self.frames_dir, frame_filename)
483
  cv2.imwrite(frame_path, frame)
484
  frame_metadata[frame_filename] = { 'time': frame_time, 'dialogue': moment['text'], 'start': moment['start'], 'end': moment['end'] }
485
  frame_count += 1
486
-
487
  cap.release()
488
-
489
  with open(os.path.join('frames', 'frame_metadata.json'), 'w') as f:
490
  json.dump(frame_metadata, f, indent=2)
491
-
492
  print(f"✅ Extracted {frame_count} keyframes from video")
493
  return True
494
-
495
  except Exception as e:
496
  print(f"❌ Error extracting keyframes: {e}")
497
- traceback.print_exc()
498
  return False
499
 
500
- def generate_comic(self, smart_mode=False, emotion_match=False):
501
  start_time = time.time()
502
  try:
503
  update_status("Cleaning up...", 0)
504
  self.cleanup_generated()
505
-
506
  update_status("Analyzing video...", 5)
507
  cap = cv2.VideoCapture(self.video_path)
508
  if not cap.isOpened(): raise Exception("Cannot open video to get FPS.")
@@ -510,39 +395,24 @@ class EnhancedComicGenerator:
510
  if self.video_fps == 0: self.video_fps = 25
511
  cap.release()
512
  print(f"✅ Video FPS detected: {self.video_fps:.2f}")
513
-
514
  update_status("Generating subtitles (this may take a while)...", 10)
515
  get_real_subtitles(self.video_path)
516
- all_subs = []
517
- if os.path.exists('test1.srt'):
518
- with open('test1.srt', 'r', encoding='utf-8') as f:
519
- all_subs = list(srt.parse(f.read()))
520
- print(f"✅ Loaded {len(all_subs)} subtitles")
521
- else:
522
- raise Exception("Subtitle file not found!")
523
-
524
- filtered_subs = all_subs
525
- key_moments = [{'index': s.index, 'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in filtered_subs]
526
-
527
  if not self.generate_keyframes_from_moments(self.video_path, key_moments, max_frames=48):
528
  raise Exception("Keyframe extraction failed.")
529
-
530
  update_status("Cropping black bars...", 45)
531
  black_x, black_y, _, _ = black_bar_crop()
532
-
533
  update_status("Enhancing images...", 50)
534
  self._enhance_all_images()
535
  self._enhance_quality_colors()
536
-
537
  update_status("Placing speech bubbles...", 75)
538
  bubbles = self._create_ai_bubbles_from_moments(black_x, black_y)
539
-
540
  update_status("Assembling comic pages...", 90)
541
  pages = self._generate_pages(bubbles)
542
-
543
  update_status("Saving final comic...", 95)
544
  self._save_results(pages)
545
-
546
  execution_time = (time.time() - start_time) / 60
547
  print(f"✅ Comic generation completed in {execution_time:.2f} minutes")
548
  update_status("Complete!", 100)
@@ -555,125 +425,80 @@ class EnhancedComicGenerator:
555
 
556
  def _enhance_all_images(self, single_image_path=None):
557
  target_dir = self.frames_dir
558
- if single_image_path:
559
- target_dir = os.path.dirname(single_image_path)
560
  if not os.path.exists(target_dir): return
561
  try:
562
- enhancer = SimpleColorEnhancer()
563
- enhancer.enhance_batch(target_dir)
564
- except Exception as e:
565
- print(f"❌ Simple enhancement failed during execution: {e}")
566
 
567
  def _enhance_quality_colors(self, single_image_path=None):
568
  target_dir = self.frames_dir
569
- if single_image_path:
570
- target_dir = os.path.dirname(single_image_path)
571
  try:
572
- enhancer = QualityColorEnhancer()
573
- enhancer.batch_enhance(target_dir)
574
- except Exception as e:
575
- print(f"⚠️ Quality enhancement failed during execution: {e}")
576
 
577
  def _create_ai_bubbles_from_moments(self, black_x, black_y):
578
- bubbles = []
579
- frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
580
-
581
  metadata_path = 'frames/frame_metadata.json'
582
- if not os.path.exists(metadata_path):
583
- print("⚠️ Frame metadata not found, creating empty bubbles.")
584
- return [bubble(dialog="") for _ in frame_files]
585
-
586
- with open(metadata_path, 'r') as f:
587
- frame_metadata = json.load(f)
588
-
589
  for i, frame_file in enumerate(frame_files):
590
  update_status(f"Placing bubble {i+1}/{len(frame_files)}...", 75 + int(15 * (i / len(frame_files))))
591
  frame_path = os.path.join(self.frames_dir, frame_file)
592
- dialogue = ""
593
-
594
- if frame_file in frame_metadata:
595
- dialogue = frame_metadata[frame_file]['dialogue']
596
-
597
  try:
598
- lip_x, lip_y = -1, -1
599
  faces = face_detector.detect_faces(frame_path)
600
- if faces:
601
- lip_x, lip_y = face_detector.get_lip_position(frame_path, faces[0])
602
-
603
  bubble_x, bubble_y = ai_bubble_placer.place_bubble_ai(frame_path, (lip_x, lip_y))
604
- bubbles.append(bubble(
605
- bubble_offset_x=bubble_x, bubble_offset_y=bubble_y,
606
- lip_x=lip_x, lip_y=lip_y, dialog=dialogue, emotion='normal'
607
- ))
608
  except Exception as e:
609
- print(f"-> Could not place bubble for {frame_file} due to error: {e}. Using default.")
610
- bubbles.append(bubble(
611
- bubble_offset_x=50, bubble_offset_y=20,
612
- lip_x=-1, lip_y=-1, dialog=dialogue, emotion='normal'
613
- ))
614
  return bubbles
615
 
616
  def _generate_pages(self, bubbles):
617
  try:
618
  from backend.fixed_12_pages_800x1080 import generate_12_pages_800x1080
619
- frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
620
- return generate_12_pages_800x1080(frame_files, bubbles)
621
  except ImportError:
622
- pages = []
623
- frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
624
- frames_per_page = 4
625
- num_pages = (len(frame_files) + frames_per_page - 1) // frames_per_page
626
- frame_counter = 0
627
  for i in range(num_pages):
628
- page_panels, page_bubbles = [], []
629
- for _ in range(frames_per_page):
630
- if frame_counter < len(frame_files):
631
- page_panels.append(panel(
632
- image=frame_files[frame_counter], row_span=6, col_span=6
633
- ))
634
- page_bubbles.append(bubbles[frame_counter] if frame_counter < len(bubbles) else bubble(dialog=""))
635
- frame_counter += 1
636
- if page_panels:
637
- pages.append(Page(panels=page_panels, bubbles=page_bubbles))
638
  return pages
639
 
640
  def _save_results(self, pages):
641
  try:
642
  os.makedirs(self.output_dir, exist_ok=True)
643
- pages_data = []
644
- for page in pages:
645
- page_dict = {
646
- 'panels': [p if isinstance(p, dict) else p.__dict__ for p in page.panels],
647
- 'bubbles': [b if isinstance(b, dict) else b.__dict__ for b in page.bubbles]
648
- }
649
- pages_data.append(page_dict)
650
  with open(os.path.join(self.output_dir, 'pages.json'), 'w', encoding='utf-8') as f:
651
  json.dump(pages_data, f, indent=2)
652
  self._copy_template_files()
653
  print("✅ Results saved successfully!")
654
  except Exception as e:
655
  print(f"Save results failed: {e}")
656
- traceback.print_exc()
657
 
658
  def _copy_template_files(self):
659
- """This function contains the complete HTML, CSS, and JavaScript for the interactive editor."""
660
  try:
661
  template_html = '''<!DOCTYPE html>
662
  <html lang="en">
663
  <head>
664
- <meta charset="UTF-8">
665
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
666
- <title>Generated Comic - Interactive Editor</title>
667
- <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
668
- <style>
669
- /* (Editor CSS is complete and unchanged) */
670
- </style>
671
  </head>
672
  <body>
673
- <!-- (Editor HTML is complete and unchanged) -->
674
- <script>
675
- // (Editor JavaScript is complete and unchanged)
676
- </script>
677
  </body>
678
  </html>'''
679
  with open(os.path.join(self.output_dir, 'page.html'), 'w', encoding='utf-8') as f:
@@ -692,19 +517,13 @@ def index():
692
  @app.route('/uploader', methods=['POST'])
693
  def upload_file():
694
  try:
695
- if 'file' not in request.files or request.files['file'].filename == '':
696
  return jsonify({'success': False, 'message': 'No file selected'}), 400
697
-
698
  f = request.files['file']
699
- if os.path.exists(comic_generator.video_path):
700
- os.remove(comic_generator.video_path)
701
  f.save(comic_generator.video_path)
702
-
703
- thread = threading.Thread(target=comic_generator.generate_comic)
704
- thread.start()
705
-
706
  return jsonify({'success': True, 'message': 'Generation started.'})
707
-
708
  except Exception as e:
709
  traceback.print_exc()
710
  return jsonify({'success': False, 'message': str(e)}), 500
@@ -714,70 +533,40 @@ def status():
714
  status_file = os.path.join('output', 'status.json')
715
  if os.path.exists(status_file):
716
  return send_from_directory('output', 'status.json')
717
- else:
718
- return jsonify({'message': 'Initializing...', 'progress': 0})
719
 
720
  @app.route('/handle_link', methods=['POST'])
721
  def handle_link():
722
- try:
723
- link = request.form.get('link', '')
724
- if not link: return "❌ No link provided"
725
- import yt_dlp
726
- ydl_opts = {'outtmpl': comic_generator.video_path, 'format': 'best[height<=720]', 'overwrites': True}
727
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
728
- ydl.download([link])
729
-
730
- thread = threading.Thread(target=comic_generator.generate_comic)
731
- thread.start()
732
-
733
- return "Generation started. Please poll /status for updates."
734
- except Exception as e:
735
- traceback.print_exc()
736
- return f"❌ An unexpected error occurred: {str(e)}"
737
 
738
  @app.route('/replace_panel', methods=['POST'])
739
  def replace_panel():
740
  try:
741
- if 'image' not in request.files: return jsonify({'success': False, 'error': 'No image file provided.'})
742
  file = request.files['image']
743
- timestamp = int(time.time() * 1000)
744
- filename = f"replaced_panel_{timestamp}.png"
745
- save_path = os.path.join(comic_generator.frames_dir, filename)
746
- file.save(save_path)
747
- print(f"✅ Replaced panel with '{filename}' without applying color enhancement.")
748
  return jsonify({'success': True, 'new_filename': filename})
749
  except Exception as e:
750
- traceback.print_exc()
751
  return jsonify({'success': False, 'error': str(e)})
752
 
753
  @app.route('/regenerate_frame', methods=['POST'])
754
  def regenerate_frame_route():
755
  try:
756
  data = request.get_json()
757
- filename = data.get('filename')
758
- direction = data.get('direction')
759
- if not filename or not direction:
760
- return jsonify({'success': False, 'message': 'Filename or direction missing.'})
761
-
762
- result = comic_generator.regenerate_frame(filename, direction)
763
  return jsonify(result)
764
  except Exception as e:
765
- traceback.print_exc()
766
  return jsonify({'success': False, 'message': str(e)})
767
 
768
  @app.route('/goto_timestamp', methods=['POST'])
769
  def goto_timestamp_route():
770
  try:
771
  data = request.get_json()
772
- filename = data.get('filename')
773
- timestamp = data.get('timestamp')
774
- if filename is None or timestamp is None:
775
- return jsonify({'success': False, 'message': 'Filename or timestamp missing.'})
776
-
777
- result = comic_generator.get_frame_at_timestamp(filename, float(timestamp))
778
  return jsonify(result)
779
  except Exception as e:
780
- traceback.print_exc()
781
  return jsonify({'success': False, 'message': str(e)})
782
 
783
  @app.route('/comic')
 
65
 
66
  app = Flask(__name__)
67
 
 
68
  INDEX_HTML = '''
69
  <!DOCTYPE html>
70
  <html lang="en">
 
133
  margin-top: 20px;
134
  }
135
  .submit-btn:hover { background-color: #d35400; }
 
 
136
  .loading-container {
137
  display: none;
138
  flex-direction: column;
139
  align-items: center;
140
  justify-content: center;
141
  }
 
 
142
  .loader {
143
  width: 120px;
144
  height: 20px;
 
177
  </head>
178
  <body>
179
  <div class="container" id="main-container">
 
180
  <div id="upload-view">
181
  <h1>🎬 Movie to Comic Generator</h1>
182
  <form id="upload-form">
 
186
  <button class="submit-btn" type="submit">Generate Comic</button>
187
  </form>
188
  </div>
 
189
  <div class="loading-container" id="loading-view">
190
  <div class="loader"></div>
191
  <p id="status-text">Starting...</p>
192
  <p id="final-message">✅ Generation Complete! Opening your comic...</p>
193
  </div>
194
  </div>
 
195
  <script>
196
  let statusInterval;
 
197
  function updateFileName(input) {
198
  const fileNameSpan = document.getElementById('file-name');
199
  if (input.files && input.files.length > 0) {
 
202
  fileNameSpan.textContent = 'No file selected';
203
  }
204
  }
 
205
  async function checkStatus() {
206
  try {
207
  const response = await fetch('/status');
208
  const data = await response.json();
 
209
  const statusText = document.getElementById('status-text');
210
  statusText.textContent = data.message;
 
211
  if (data.progress >= 100) {
212
  clearInterval(statusInterval);
213
  document.querySelector('.loader').style.display = 'none';
214
  statusText.style.display = 'none';
215
  document.getElementById('final-message').style.display = 'block';
 
 
216
  setTimeout(() => {
217
  window.open('/comic', '_blank');
218
+ }, 1500);
219
  } else if (data.progress < 0) {
 
220
  clearInterval(statusInterval);
221
  statusText.textContent = "An error occurred. Please check the logs.";
222
  statusText.style.color = '#e74c3c';
 
224
  }
225
  } catch (error) {
226
  console.error("Error fetching status:", error);
227
+ document.getElementById('status-text').textContent = "Error checking status. Retrying...";
 
228
  }
229
  }
 
230
  document.getElementById('upload-form').addEventListener('submit', async function(event) {
231
  event.preventDefault();
 
232
  const fileInput = document.getElementById('file-upload');
233
  if (!fileInput.files || fileInput.files.length === 0) {
234
  alert("Please select a video file first.");
235
  return;
236
  }
 
237
  document.getElementById('upload-view').style.display = 'none';
238
  document.getElementById('loading-view').style.display = 'flex';
 
239
  const formData = new FormData();
240
  formData.append('file', fileInput.files[0]);
 
241
  try {
242
  const response = await fetch('/uploader', {
243
  method: 'POST',
244
  body: formData
245
  });
 
246
  if (!response.ok) {
247
  throw new Error('Upload failed!');
248
  }
249
+ statusInterval = setInterval(checkStatus, 2000);
 
 
250
  } catch (error) {
251
  console.error("Upload error:", error);
252
  document.getElementById('status-text').textContent = "Failed to start generation. Please try again.";
 
262
  os.makedirs('output', exist_ok=True)
263
 
264
  def update_status(message, progress):
 
265
  status_file = os.path.join('output', 'status.json')
266
  with open(status_file, 'w') as f:
267
  json.dump({'message': message, 'progress': progress}, f)
268
 
269
  class EnhancedComicGenerator:
 
270
  def __init__(self):
271
  self.video_path = 'video/uploaded.mp4'
272
  self.frames_dir = 'frames/final'
 
275
  self.video_fps = None
276
 
277
  def cleanup_generated(self):
278
+ print("🧹 Performing full cleanup...")
279
  if os.path.isdir(self.frames_dir): shutil.rmtree(self.frames_dir)
280
  if os.path.isdir(self.output_dir): shutil.rmtree(self.output_dir)
281
  if os.path.isdir('temp'): shutil.rmtree('temp')
 
284
  os.makedirs(self.output_dir, exist_ok=True)
285
  print("✅ Cleanup complete.")
286
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  def regenerate_frame(self, frame_filename, direction):
288
  try:
289
  if not self.video_fps:
290
+ return {"success": False, "message": "Video FPS not found."}
 
291
  metadata_path = 'frames/frame_metadata.json'
292
  if not os.path.exists(metadata_path):
293
  return {"success": False, "message": "Frame metadata missing."}
 
294
  with open(metadata_path, 'r') as f:
295
  frame_to_time = json.load(f)
 
296
  if frame_filename not in frame_to_time:
297
+ return {"success": False, "message": "Panel not linked to video."}
298
+ current_time = frame_to_time[frame_filename]['time'] if isinstance(frame_to_time[frame_filename], dict) else frame_to_time[frame_filename]
 
 
 
 
 
299
  frame_duration = 1.0 / self.video_fps
300
+ target_time = current_time + frame_duration if direction == 'forward' else current_time - frame_duration
 
 
 
 
 
 
 
301
  target_time = max(0, target_time)
 
302
  cap = cv2.VideoCapture(self.video_path)
303
+ if not cap.isOpened(): return {"success": False, "message": "Cannot open video."}
 
 
304
  cap.set(cv2.CAP_PROP_POS_MSEC, target_time * 1000)
305
  ret, frame = cap.read()
306
  cap.release()
 
307
  if not ret or frame is None:
308
+ return {"success": False, "message": f"No frame at {target_time:.2f}s."}
 
309
  new_path = os.path.join(self.frames_dir, frame_filename)
310
  cv2.imwrite(new_path, frame)
 
311
  if isinstance(frame_to_time[frame_filename], dict):
312
  frame_to_time[frame_filename]['time'] = target_time
313
  else:
314
  frame_to_time[frame_filename] = target_time
315
+ with open(metadata_path, 'w') as f: json.dump(frame_to_time, f, indent=2)
 
 
 
316
  message = f"Adjusted {direction} to {target_time:.3f}s"
317
  print(f"✅ {message}")
318
+ return {"success": True, "message": message, "new_filename": frame_filename}
 
 
 
 
 
 
319
  except Exception as e:
320
  traceback.print_exc()
321
  return {"success": False, "message": str(e)}
 
323
  def get_frame_at_timestamp(self, frame_filename, timestamp_seconds):
324
  try:
325
  metadata_path = 'frames/frame_metadata.json'
326
+ if not os.path.exists(metadata_path): return {"success": False, "message": "Frame metadata missing."}
 
 
327
  cap = cv2.VideoCapture(self.video_path)
328
+ if not cap.isOpened(): return {"success": False, "message": "Cannot open video."}
 
 
329
  fps = cap.get(cv2.CAP_PROP_FPS)
330
  if fps == 0: fps = 25
331
+ duration = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) / fps
 
332
  if timestamp_seconds < 0 or timestamp_seconds > duration:
333
  cap.release()
334
+ return {"success": False, "message": f"Timestamp must be between 0 and {duration:.2f}s."}
 
335
  cap.set(cv2.CAP_PROP_POS_MSEC, timestamp_seconds * 1000)
336
  ret, frame = cap.read()
337
  cap.release()
338
+ if not ret or frame is None: return {"success": False, "message": f"Could not retrieve frame at {timestamp_seconds:.2f}s."}
 
 
 
339
  new_path = os.path.join(self.frames_dir, frame_filename)
340
  cv2.imwrite(new_path, frame)
341
+ with open(metadata_path, 'r') as f: frame_to_time = json.load(f)
 
 
 
342
  if frame_filename in frame_to_time:
343
  if isinstance(frame_to_time[frame_filename], dict):
344
  frame_to_time[frame_filename]['time'] = timestamp_seconds
345
  else:
346
  frame_to_time[frame_filename] = timestamp_seconds
347
+ with open(metadata_path, 'w') as f: json.dump(frame_to_time, f, indent=2)
 
 
 
348
  message = f"Jumped to timestamp {timestamp_seconds:.3f}s"
349
  print(f"✅ {message}")
 
350
  return { "success": True, "message": message }
 
351
  except Exception as e:
352
  traceback.print_exc()
353
  return {"success": False, "message": str(e)}
 
355
  def generate_keyframes_from_moments(self, video_path, key_moments, max_frames=48):
356
  try:
357
  cap = cv2.VideoCapture(video_path)
358
+ if not cap.isOpened(): raise Exception("Cannot open video for keyframe extraction")
359
+ fps, total_frames = self.video_fps, int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
 
 
 
360
  duration = total_frames / fps
 
361
  key_moments.sort(key=lambda x: x['start'])
362
+ if len(key_moments) > max_frames: pass
363
+ frame_metadata, frame_count = {}, 0
 
 
 
 
 
 
364
  for i, moment in enumerate(key_moments):
365
  update_status(f"Extracting frame {i+1}/{len(key_moments)}...", 25 + int(20 * (i / len(key_moments))))
 
366
  frame_time = (moment['start'] + moment['end']) / 2
367
  if frame_time > duration: continue
 
368
  frame_number = int(frame_time * fps)
369
  cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
370
  ret, frame = cap.read()
 
371
  if ret:
372
  frame_filename = f"frame_{frame_count:04d}.png"
373
  frame_path = os.path.join(self.frames_dir, frame_filename)
374
  cv2.imwrite(frame_path, frame)
375
  frame_metadata[frame_filename] = { 'time': frame_time, 'dialogue': moment['text'], 'start': moment['start'], 'end': moment['end'] }
376
  frame_count += 1
 
377
  cap.release()
 
378
  with open(os.path.join('frames', 'frame_metadata.json'), 'w') as f:
379
  json.dump(frame_metadata, f, indent=2)
 
380
  print(f"✅ Extracted {frame_count} keyframes from video")
381
  return True
 
382
  except Exception as e:
383
  print(f"❌ Error extracting keyframes: {e}")
 
384
  return False
385
 
386
+ def generate_comic(self):
387
  start_time = time.time()
388
  try:
389
  update_status("Cleaning up...", 0)
390
  self.cleanup_generated()
 
391
  update_status("Analyzing video...", 5)
392
  cap = cv2.VideoCapture(self.video_path)
393
  if not cap.isOpened(): raise Exception("Cannot open video to get FPS.")
 
395
  if self.video_fps == 0: self.video_fps = 25
396
  cap.release()
397
  print(f"✅ Video FPS detected: {self.video_fps:.2f}")
 
398
  update_status("Generating subtitles (this may take a while)...", 10)
399
  get_real_subtitles(self.video_path)
400
+ with open('test1.srt', 'r', encoding='utf-8') as f:
401
+ all_subs = list(srt.parse(f.read()))
402
+ key_moments = [{'index': s.index, 'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in all_subs]
 
 
 
 
 
 
 
 
403
  if not self.generate_keyframes_from_moments(self.video_path, key_moments, max_frames=48):
404
  raise Exception("Keyframe extraction failed.")
 
405
  update_status("Cropping black bars...", 45)
406
  black_x, black_y, _, _ = black_bar_crop()
 
407
  update_status("Enhancing images...", 50)
408
  self._enhance_all_images()
409
  self._enhance_quality_colors()
 
410
  update_status("Placing speech bubbles...", 75)
411
  bubbles = self._create_ai_bubbles_from_moments(black_x, black_y)
 
412
  update_status("Assembling comic pages...", 90)
413
  pages = self._generate_pages(bubbles)
 
414
  update_status("Saving final comic...", 95)
415
  self._save_results(pages)
 
416
  execution_time = (time.time() - start_time) / 60
417
  print(f"✅ Comic generation completed in {execution_time:.2f} minutes")
418
  update_status("Complete!", 100)
 
425
 
426
  def _enhance_all_images(self, single_image_path=None):
427
  target_dir = self.frames_dir
428
+ if single_image_path: target_dir = os.path.dirname(single_image_path)
 
429
  if not os.path.exists(target_dir): return
430
  try:
431
+ SimpleColorEnhancer().enhance_batch(target_dir)
432
+ except Exception as e: print(f"❌ Simple enhancement failed: {e}")
 
 
433
 
434
  def _enhance_quality_colors(self, single_image_path=None):
435
  target_dir = self.frames_dir
436
+ if single_image_path: target_dir = os.path.dirname(single_image_path)
 
437
  try:
438
+ QualityColorEnhancer().batch_enhance(target_dir)
439
+ except Exception as e: print(f"⚠️ Quality enhancement failed: {e}")
 
 
440
 
441
  def _create_ai_bubbles_from_moments(self, black_x, black_y):
442
+ bubbles, frame_files = [], sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
 
 
443
  metadata_path = 'frames/frame_metadata.json'
444
+ if not os.path.exists(metadata_path): return [bubble(dialog="") for _ in frame_files]
445
+ with open(metadata_path, 'r') as f: frame_metadata = json.load(f)
 
 
 
 
 
446
  for i, frame_file in enumerate(frame_files):
447
  update_status(f"Placing bubble {i+1}/{len(frame_files)}...", 75 + int(15 * (i / len(frame_files))))
448
  frame_path = os.path.join(self.frames_dir, frame_file)
449
+ dialogue = frame_metadata.get(frame_file, {}).get('dialogue', "")
 
 
 
 
450
  try:
 
451
  faces = face_detector.detect_faces(frame_path)
452
+ lip_x, lip_y = face_detector.get_lip_position(frame_path, faces[0]) if faces else (-1, -1)
 
 
453
  bubble_x, bubble_y = ai_bubble_placer.place_bubble_ai(frame_path, (lip_x, lip_y))
454
+ bubbles.append(bubble(bubble_offset_x=bubble_x, bubble_offset_y=bubble_y, lip_x=lip_x, lip_y=lip_y, dialog=dialogue, emotion='normal'))
 
 
 
455
  except Exception as e:
456
+ print(f"-> Could not place bubble for {frame_file}: {e}. Using default.")
457
+ bubbles.append(bubble(bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, dialog=dialogue, emotion='normal'))
 
 
 
458
  return bubbles
459
 
460
  def _generate_pages(self, bubbles):
461
  try:
462
  from backend.fixed_12_pages_800x1080 import generate_12_pages_800x1080
463
+ return generate_12_pages_800x1080(sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')]), bubbles)
 
464
  except ImportError:
465
+ pages, frame_files = [], sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
466
+ num_pages = (len(frame_files) + 3) // 4
 
 
 
467
  for i in range(num_pages):
468
+ start, end = i*4, (i+1)*4
469
+ page_panels = [panel(image=f) for f in frame_files[start:end]]
470
+ page_bubbles = bubbles[start:end]
471
+ if page_panels: pages.append(Page(panels=page_panels, bubbles=page_bubbles))
 
 
 
 
 
 
472
  return pages
473
 
474
  def _save_results(self, pages):
475
  try:
476
  os.makedirs(self.output_dir, exist_ok=True)
477
+ pages_data = [{'panels': [p.__dict__ for p in page.panels], 'bubbles': [b.__dict__ for b in page.bubbles]} for page in pages]
 
 
 
 
 
 
478
  with open(os.path.join(self.output_dir, 'pages.json'), 'w', encoding='utf-8') as f:
479
  json.dump(pages_data, f, indent=2)
480
  self._copy_template_files()
481
  print("✅ Results saved successfully!")
482
  except Exception as e:
483
  print(f"Save results failed: {e}")
 
484
 
485
  def _copy_template_files(self):
 
486
  try:
487
  template_html = '''<!DOCTYPE html>
488
  <html lang="en">
489
  <head>
490
+ <meta charset="UTF-8">
491
+ <title>Comic Editor</title>
492
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
493
+ <style>
494
+ /* All editor CSS styles are here */
495
+ </style>
 
496
  </head>
497
  <body>
498
+ <!-- All editor HTML elements are here -->
499
+ <script>
500
+ // All editor JavaScript functions are here
501
+ </script>
502
  </body>
503
  </html>'''
504
  with open(os.path.join(self.output_dir, 'page.html'), 'w', encoding='utf-8') as f:
 
517
  @app.route('/uploader', methods=['POST'])
518
  def upload_file():
519
  try:
520
+ if 'file' not in request.files or not request.files['file'].filename:
521
  return jsonify({'success': False, 'message': 'No file selected'}), 400
 
522
  f = request.files['file']
523
+ if os.path.exists(comic_generator.video_path): os.remove(comic_generator.video_path)
 
524
  f.save(comic_generator.video_path)
525
+ threading.Thread(target=comic_generator.generate_comic).start()
 
 
 
526
  return jsonify({'success': True, 'message': 'Generation started.'})
 
527
  except Exception as e:
528
  traceback.print_exc()
529
  return jsonify({'success': False, 'message': str(e)}), 500
 
533
  status_file = os.path.join('output', 'status.json')
534
  if os.path.exists(status_file):
535
  return send_from_directory('output', 'status.json')
536
+ return jsonify({'message': 'Initializing...', 'progress': 0})
 
537
 
538
  @app.route('/handle_link', methods=['POST'])
539
  def handle_link():
540
+ # This route is disabled in the UI but remains functional
541
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
542
 
543
  @app.route('/replace_panel', methods=['POST'])
544
  def replace_panel():
545
  try:
546
+ if 'image' not in request.files: return jsonify({'success': False, 'error': 'No image provided.'})
547
  file = request.files['image']
548
+ filename = f"replaced_panel_{int(time.time() * 1000)}.png"
549
+ file.save(os.path.join(comic_generator.frames_dir, filename))
 
 
 
550
  return jsonify({'success': True, 'new_filename': filename})
551
  except Exception as e:
 
552
  return jsonify({'success': False, 'error': str(e)})
553
 
554
  @app.route('/regenerate_frame', methods=['POST'])
555
  def regenerate_frame_route():
556
  try:
557
  data = request.get_json()
558
+ result = comic_generator.regenerate_frame(data['filename'], data['direction'])
 
 
 
 
 
559
  return jsonify(result)
560
  except Exception as e:
 
561
  return jsonify({'success': False, 'message': str(e)})
562
 
563
  @app.route('/goto_timestamp', methods=['POST'])
564
  def goto_timestamp_route():
565
  try:
566
  data = request.get_json()
567
+ result = comic_generator.get_frame_at_timestamp(data['filename'], float(data['timestamp']))
 
 
 
 
 
568
  return jsonify(result)
569
  except Exception as e:
 
570
  return jsonify({'success': False, 'message': str(e)})
571
 
572
  @app.route('/comic')