jhh6576 commited on
Commit
e7dc82e
·
verified ·
1 Parent(s): a87e692

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +73 -323
app_enhanced.py CHANGED
@@ -13,33 +13,58 @@ import shutil
13
  from typing import List
14
  import traceback
15
 
16
- # --- FIX: Isolate the problematic import and provide a fallback ---
17
- # This makes the app run even if the module is missing, skipping the feature.
 
 
18
  try:
19
  from backend.keyframes.keyframes import black_bar_crop
20
  print("✅ Black bar cropping module loaded.")
21
  except Exception as e:
22
- print(f"⚠️ Could not load black_bar_crop from backend.keyframes.keyframes: {e}")
23
- print("⚠️ Black bar cropping will be SKIPPED. A default value of (0,0) will be used.")
24
- # Define a dummy function so the rest of the program doesn't crash
25
  def black_bar_crop():
26
- # Return default coordinates (x=0, y=0) and dimensions
27
  return 0, 0, None, None
28
 
29
- # Import other enhanced modules
30
  try:
31
- from backend.ai_enhanced_core import (
32
- image_processor, comic_styler, face_detector, layout_optimizer
33
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  from backend.ai_bubble_placement import ai_bubble_placer
35
  from backend.subtitles.subs_real import get_real_subtitles
36
  from backend.keyframes.keyframes_simple import generate_keyframes_simple
37
- from backend.class_def import bubble, panel, Page
38
- from backend.simple_color_enhancer import SimpleColorEnhancer
39
- from backend.quality_color_enhancer import QualityColorEnhancer
40
- print("✅ Core modules loaded.")
41
  except Exception as e:
42
- print(f"⚠️ Could not load a core module: {e}")
43
 
44
  # Import smart comic generation
45
  try:
@@ -69,6 +94,7 @@ except Exception as e:
69
  STORY_EXTRACTOR_AVAILABLE = False
70
  print(f"⚠️ Smart story extractor not available: {e}")
71
 
 
72
  app = Flask(__name__)
73
 
74
  # Import editor routes
@@ -86,6 +112,7 @@ os.makedirs('output', exist_ok=True)
86
 
87
  class EnhancedComicGenerator:
88
  """High-quality comic generation with AI enhancement"""
 
89
  def __init__(self):
90
  self.video_path = 'video/uploaded.mp4'
91
  self.frames_dir = 'frames/final'
@@ -308,14 +335,13 @@ class EnhancedComicGenerator:
308
  return False
309
 
310
  print("✂️ Cropping black bars...")
311
- # --- FIX: This call now uses either the real function or the dummy one ---
312
  black_x, black_y, _, _ = black_bar_crop()
313
  print("✅ Black bars cropped.")
314
 
315
  print("🎨 Enhancing images...")
316
  self._enhance_all_images()
317
  self._enhance_quality_colors()
318
- print("✅ Images enhanced.")
319
 
320
  print("💬 Creating AI bubbles with key moment dialogues...")
321
  bubbles = self._create_ai_bubbles_from_moments(black_x, black_y)
@@ -347,7 +373,7 @@ class EnhancedComicGenerator:
347
  enhancer = SimpleColorEnhancer()
348
  enhancer.enhance_batch(target_dir)
349
  except Exception as e:
350
- print(f"❌ Simple enhancement failed: {e}")
351
 
352
  def _enhance_quality_colors(self, single_image_path=None):
353
  """Enhances quality and colors for a batch of images."""
@@ -358,7 +384,7 @@ class EnhancedComicGenerator:
358
  enhancer = QualityColorEnhancer()
359
  enhancer.batch_enhance(target_dir)
360
  except Exception as e:
361
- print(f"⚠️ Quality enhancement failed: {e}")
362
 
363
  def _create_ai_bubbles_from_moments(self, black_x, black_y):
364
  """Create bubbles using the key moments dialogues"""
@@ -367,8 +393,8 @@ class EnhancedComicGenerator:
367
 
368
  metadata_path = 'frames/frame_metadata.json'
369
  if not os.path.exists(metadata_path):
370
- print("⚠️ Frame metadata not found, using empty bubbles")
371
- return [bubble(bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, dialog="", emotion='normal') for _ in frame_files]
372
 
373
  with open(metadata_path, 'r') as f:
374
  frame_metadata = json.load(f)
@@ -381,6 +407,8 @@ class EnhancedComicGenerator:
381
  dialogue = frame_metadata[frame_file]['dialogue']
382
 
383
  try:
 
 
384
  lip_x, lip_y = -1, -1
385
  faces = face_detector.detect_faces(frame_path)
386
  if faces:
@@ -391,7 +419,8 @@ class EnhancedComicGenerator:
391
  bubble_offset_x=bubble_x, bubble_offset_y=bubble_y,
392
  lip_x=lip_x, lip_y=lip_y, dialog=dialogue, emotion='normal'
393
  ))
394
- except Exception:
 
395
  bubbles.append(bubble(
396
  bubble_offset_x=50, bubble_offset_y=20,
397
  lip_x=-1, lip_y=-1, dialog=dialogue, emotion='normal'
@@ -453,49 +482,22 @@ class EnhancedComicGenerator:
453
  <style>
454
  body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-serif; }
455
  .comic-container { max-width: 1200px; margin: 0 auto; }
456
- .comic-page {
457
- background: white; width: 600px; height: 400px;
458
- box-shadow: 0 0 10px rgba(0,0,0,0.1); box-sizing: content-box;
459
- position: relative; overflow: hidden; border: 1px solid #333;
460
- padding: 10px;
461
- }
462
- .comic-grid {
463
- display: grid;
464
- grid-template-columns: 285px 285px;
465
- grid-template-rows: 185px 185px;
466
- gap: 10px;
467
- width: 100%; height: 100%;
468
- }
469
  .page-wrapper { margin: 30px auto; width: 622px; display: flex; flex-direction: column; align-items: center; }
470
  .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
471
- .panel {
472
- position: relative; overflow: hidden; width: 100%; height: 100%;
473
- box-sizing: border-box; cursor: pointer; border: 1px solid #333;
474
- }
475
  .panel.selected { outline: 3px solid #2196F3; outline-offset: -3px; }
476
  .panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
477
- .speech-bubble {
478
- position: absolute; display: flex; justify-content: center; align-items: center;
479
- width: auto; height: auto;
480
- min-width: 50px; max-width: 220px; min-height: 30px;
481
- box-sizing: border-box; padding: 8px;
482
- box-shadow: 2px 2px 5px rgba(0,0,0,0.3); z-index: 10;
483
- cursor: move; overflow: visible; font-size: 13px; font-weight: bold; text-align: center;
484
- }
485
  .bubble-text { padding: 2px; word-wrap: break-word; }
486
  .speech-bubble.selected { outline: 2px dashed #4CAF50; }
487
- .speech-bubble textarea {
488
- position: absolute; top: 0; left: 0; width: 100%; height: 100%; box-sizing: border-box;
489
- border: 1px solid #4CAF50; background: rgba(255,255,255,0.95);
490
- font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102;
491
- }
492
- /* --- Bubble Styles --- */
493
  .speech-bubble.speech { background: white; border: 2px solid #333; color: #333; border-radius: 15px; }
494
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
495
  .speech-bubble.reaction { background: #FFD700; border: 3px solid #E53935; color: #D32F2F; font-weight: 900; text-transform: uppercase; width: 180px; clip-path: polygon(0% 25%, 17% 21%, 17% 0%, 31% 16%, 50% 4%, 69% 16%, 83% 0%, 83% 21%, 100% 25%, 85% 45%, 95% 62%, 82% 79%, 100% 97%, 79% 89%, 60% 98%, 46% 82%, 27% 95%, 15% 78%, 5% 62%, 15% 45%); }
496
  .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
497
  .speech-bubble.idea { background: linear-gradient(180deg,#FFFDD0 0%, #FFF8B5 100%); border: 2px solid #FFA500; color: #6a4b00; border-radius: 40% 60% 40% 60% / 60% 40% 60% 40%; }
498
- /* --- Tail and Dot Styles (4-Direction Flip) --- */
499
  .speech-bubble.speech::after, .speech-bubble.idea::after { content: ''; position: absolute; width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; }
500
  .speech-bubble.speech::after { border-top: 10px solid #333; bottom: -9px; left: 20px; }
501
  .speech-bubble.idea::after { border-top: 10px solid #FFA500; bottom: -9px; left: 20px; }
@@ -503,19 +505,13 @@ font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102;
503
  .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
504
  .thought-dot-1 { width: 20px; height: 20px; bottom: -20px; left: 15px; }
505
  .thought-dot-2 { width: 12px; height: 12px; bottom: -32px; left: 5px; }
506
- /* Horizontal Flip */
507
  .speech-bubble.flipped.speech::after, .speech-bubble.flipped.idea::after { left: auto; right: 20px; }
508
  .speech-bubble.flipped.thought .thought-dot-1 { left: auto; right: 15px; }
509
  .speech-bubble.flipped.thought .thought-dot-2 { left: auto; right: 5px; }
510
- /* Vertical Flip */
511
  .speech-bubble.flipped-vertical.speech::after, .speech-bubble.flipped-vertical.idea::after { bottom: auto; top: -9px; transform: rotate(180deg); }
512
  .speech-bubble.flipped-vertical.thought .thought-dot-1 { bottom: auto; top: -20px; }
513
  .speech-bubble.flipped-vertical.thought .thought-dot-2 { bottom: auto; top: -32px; }
514
- .edit-controls {
515
- position: fixed; bottom: 20px; right: 20px; background: rgba(44, 62, 80, 0.9);
516
- color: white; padding: 10px 15px; border-radius: 8px; font-size: 13px;
517
- z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px;
518
- }
519
  .edit-controls h4 { margin: 0 0 10px 0; color: #26a69a; text-align: center; }
520
  .edit-controls button, .edit-controls select { margin-top: 5px; padding: 6px 8px; font-size: 12px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; width: 100%; box-sizing: border-box; }
521
  .edit-controls .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
@@ -535,11 +531,7 @@ z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px;
535
  <div class="control-group">
536
  <label for="bubble-type-select">Change Selected Bubble Type:</label>
537
  <select id="bubble-type-select" onchange="changeBubbleType(this.value)">
538
- <option value="speech">Speech</option>
539
- <option value="thought">Thought</option>
540
- <option value="reaction">Reaction</option>
541
- <option value="narration">Narration</option>
542
- <option value="idea">Idea</option>
543
  </select>
544
  <button onclick="rotateBubbleTail()" class="secondary-button">🔄 Rotate Tail</button>
545
  </div>
@@ -559,7 +551,6 @@ z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px;
559
  .then(data => { renderComic(data); initializeEditor(); })
560
  .catch(err => { document.getElementById('comic-pages').innerHTML = `<div class="loading">Error: ${err.message}</div>`; });
561
  });
562
-
563
  function renderComic(data) {
564
  const container = document.getElementById('comic-pages');
565
  container.innerHTML = '';
@@ -584,12 +575,7 @@ z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px;
584
  panelDiv.appendChild(img);
585
  if (pageData.bubbles && pageData.bubbles[panelIndex]) {
586
  const bubbleData = pageData.bubbles[panelIndex];
587
- const bubbleDiv = createBubbleElement({
588
- id: `initial-${pageIndex}-${panelIndex}`,
589
- text: bubbleData.dialog || '',
590
- left: `${bubbleData.bubble_offset_x ?? 50}px`,
591
- top: `${bubbleData.bubble_offset_y ?? 20}px`,
592
- });
593
  panelDiv.appendChild(bubbleDiv);
594
  }
595
  grid.appendChild(panelDiv);
@@ -599,239 +585,11 @@ z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px;
599
  container.appendChild(pageWrapper);
600
  });
601
  }
602
-
603
  let currentlyEditing = null, draggedBubble = null, offset = {x: 0, y: 0};
604
  let currentlySelectedBubble = null;
605
  let currentlySelectedPanel = null;
606
-
607
- function initializeEditor() {
608
- document.querySelectorAll('.panel').forEach(p => p.addEventListener('click', e => selectPanel(e.currentTarget)));
609
- document.querySelectorAll('.speech-bubble').forEach(b => initializeBubbleEvents(b));
610
- document.addEventListener('mousemove', e => { if (draggedBubble) drag(e); });
611
- document.addEventListener('mouseup', () => { if (draggedBubble) stopDrag(); });
612
- }
613
-
614
- function initializeBubbleEvents(bubble) {
615
- bubble.addEventListener('dblclick', e => { e.stopPropagation(); editBubbleText(bubble); });
616
- bubble.addEventListener('mousedown', e => startDrag(e));
617
- bubble.addEventListener('click', e => { e.stopPropagation(); selectBubble(bubble); });
618
- bubble.addEventListener('wheel', e => {
619
- e.preventDefault();
620
- const currentWidth = parseFloat(bubble.style.width) || bubble.offsetWidth;
621
- const newWidth = currentWidth - (e.deltaY > 0 ? 10 : -10);
622
- if (newWidth >= 60) {
623
- bubble.style.width = `${newWidth}px`;
624
- bubble.style.height = 'auto';
625
- }
626
- }, { passive: false });
627
- }
628
-
629
- function createBubbleElement(data) {
630
- const bubbleDiv = document.createElement('div');
631
- bubbleDiv.dataset.id = data.id;
632
- const textSpan = document.createElement('span');
633
- textSpan.className = 'bubble-text';
634
- textSpan.textContent = data.text;
635
- bubbleDiv.appendChild(textSpan);
636
- bubbleDiv.style.left = data.left;
637
- bubbleDiv.style.top = data.top;
638
- applyBubbleType(bubbleDiv, 'speech'); // Default to speech
639
- return bubbleDiv;
640
- }
641
-
642
- function applyBubbleType(bubble, type) {
643
- bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
644
- let classesToKeep = 'speech-bubble';
645
- if (bubble.classList.contains('selected')) classesToKeep += ' selected';
646
- if (bubble.classList.contains('flipped')) classesToKeep += ' flipped';
647
- if (bubble.classList.contains('flipped-vertical')) classesToKeep += ' flipped-vertical';
648
- bubble.className = classesToKeep;
649
- bubble.classList.add(type);
650
- bubble.dataset.type = type;
651
- if (type === 'thought') {
652
- for (let i = 1; i <= 2; i++) {
653
- const dot = document.createElement('div');
654
- dot.className = `thought-dot thought-dot-${i}`;
655
- bubble.appendChild(dot);
656
- }
657
- }
658
- }
659
-
660
- function changeBubbleType(type) {
661
- if (!currentlySelectedBubble) return;
662
- applyBubbleType(currentlySelectedBubble, type);
663
- }
664
-
665
- function rotateBubbleTail() {
666
- if (!currentlySelectedBubble) return alert("Please select a bubble to rotate.");
667
- const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
668
- const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
669
- if (!isFlippedH && !isFlippedV) { // State 0 -> 1
670
- currentlySelectedBubble.classList.add('flipped');
671
- } else if (isFlippedH && !isFlippedV) { // State 1 -> 2
672
- currentlySelectedBubble.classList.add('flipped-vertical');
673
- } else if (isFlippedH && isFlippedV) { // State 2 -> 3
674
- currentlySelectedBubble.classList.remove('flipped');
675
- } else { // State 3 -> 0
676
- currentlySelectedBubble.classList.remove('flipped-vertical');
677
- }
678
- }
679
-
680
- function selectPanel(panel) {
681
- document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
682
- panel.classList.add('selected');
683
- currentlySelectedPanel = panel;
684
- selectBubble(null);
685
- }
686
-
687
- function selectBubble(bubble) {
688
- if (currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
689
- currentlySelectedBubble = bubble;
690
- if (currentlySelectedBubble) {
691
- currentlySelectedBubble.classList.add('selected');
692
- document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
693
- document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
694
- }
695
- }
696
-
697
- function editBubbleText(bubble) {
698
- if (currentlyEditing) return;
699
- currentlyEditing = bubble;
700
- const textSpan = bubble.querySelector('.bubble-text');
701
- const currentText = textSpan.textContent;
702
- textSpan.style.display = 'none';
703
- bubble.style.height = 'auto';
704
- const textarea = document.createElement('textarea');
705
- textarea.value = currentText;
706
- bubble.appendChild(textarea);
707
- textarea.focus();
708
- const finishEditing = () => {
709
- textSpan.textContent = textarea.value;
710
- bubble.removeChild(textarea);
711
- textSpan.style.display = '';
712
- currentlyEditing = null;
713
- bubble.style.height = 'auto';
714
- };
715
- textarea.addEventListener('blur', finishEditing, { once: true });
716
- textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); }});
717
- }
718
-
719
- function startDrag(e) {
720
- const bubble = e.target.closest('.speech-bubble');
721
- if (!bubble || currentlyEditing) return;
722
- draggedBubble = bubble;
723
- selectBubble(bubble);
724
- const rect = bubble.getBoundingClientRect();
725
- offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
726
- }
727
-
728
- function drag(e) {
729
- const parentRect = draggedBubble.parentElement.getBoundingClientRect();
730
- let x = e.clientX - parentRect.left - offset.x;
731
- let y = e.clientY - parentRect.top - offset.y;
732
- draggedBubble.style.left = `${x}px`;
733
- draggedBubble.style.top = `${y}px`;
734
- }
735
-
736
- function stopDrag() {
737
- draggedBubble = null;
738
- }
739
-
740
- function clearSavedState() {
741
- if (confirm("Reset all edits to the original AI-generated comic?")) {
742
- localStorage.removeItem('comicEditorState');
743
- window.location.reload();
744
- }
745
- }
746
-
747
- async function exportPagesToPNG() {
748
- const pages = document.querySelectorAll('.comic-page');
749
- if (pages.length === 0) return alert("No pages found.");
750
- alert(`Starting export of ${pages.length} page(s).`);
751
- for (let i = 0; i < pages.length; i++) {
752
- try {
753
- const canvas = await html2canvas(pages[i], { scale: 2 });
754
- const link = document.createElement('a');
755
- link.download = `comic-page-${i + 1}.png`;
756
- link.href = canvas.toDataURL('image/png');
757
- link.click();
758
- } catch (err) {
759
- alert(`Failed to export page ${i + 1}.`);
760
- }
761
- }
762
- }
763
-
764
- function replacePanelImage() {
765
- if (!currentlySelectedPanel) {
766
- alert("Please select a panel first.");
767
- return;
768
- }
769
- const img = currentlySelectedPanel.querySelector('img');
770
- const uploader = document.getElementById('image-uploader');
771
- const oneTimeListener = (event) => {
772
- const file = event.target.files[0];
773
- if (!file) return;
774
- const formData = new FormData();
775
- formData.append('image', file);
776
- img.style.opacity = '0.5';
777
- fetch('/replace_panel', { method: 'POST', body: formData })
778
- .then(response => response.json())
779
- .then(data => {
780
- if (data.success) {
781
- img.src = `/frames/final/${data.new_filename}?t=${new Date().getTime()}`;
782
- } else {
783
- alert('Error replacing image: ' + data.error);
784
- }
785
- img.style.opacity = '1';
786
- })
787
- .catch(error => {
788
- alert('An error occurred during the upload.');
789
- img.style.opacity = '1';
790
- });
791
- uploader.removeEventListener('change', oneTimeListener);
792
- uploader.value = '';
793
- };
794
- uploader.addEventListener('change', oneTimeListener, { once: true });
795
- uploader.click();
796
- }
797
-
798
- function regenerateFrame() {
799
- if (!currentlySelectedPanel) {
800
- alert("Please select a panel first.");
801
- return;
802
- }
803
- const img = currentlySelectedPanel.querySelector('img');
804
- const currentSrc = img.src;
805
-
806
- let filename = currentSrc.substring(currentSrc.lastIndexOf('/') + 1);
807
- if (filename.includes('?')) {
808
- filename = filename.split('?')[0];
809
- }
810
-
811
- if (!confirm(`Regenerate frame "${filename}" with a better version?`)) {
812
- return;
813
- }
814
- img.style.opacity = '0.5';
815
- fetch('/regenerate_frame', {
816
- method: 'POST',
817
- headers: { 'Content-Type': 'application/json' },
818
- body: JSON.stringify({ filename: filename })
819
- })
820
- .then(response => response.json())
821
- .then(data => {
822
- if (data.success) {
823
- img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
824
- alert(data.message);
825
- } else {
826
- alert('Error: ' + data.message);
827
- }
828
- img.style.opacity = '1';
829
- })
830
- .catch(error => {
831
- alert('An error occurred during regeneration.');
832
- img.style.opacity = '1';
833
- });
834
- }
835
  </script>
836
  </body>
837
  </html>'''
@@ -841,7 +599,7 @@ z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px;
841
  except Exception as e:
842
  print(f"Template copy failed: {e}")
843
 
844
- # Flask routes
845
  comic_generator = EnhancedComicGenerator()
846
 
847
  @app.route('/')
@@ -859,9 +617,9 @@ def upload_file():
859
  f.save(comic_generator.video_path)
860
  success = comic_generator.generate_comic()
861
  if success:
862
- return "🎉 Enhanced Comic Created Successfully! Please refresh your browser and go to the /comic endpoint."
863
  else:
864
- return "❌ Comic generation failed"
865
  except Exception as e:
866
  return f"❌ Error: {str(e)}"
867
 
@@ -869,35 +627,29 @@ def upload_file():
869
  def handle_link():
870
  try:
871
  link = request.form.get('link', '')
872
- if not link:
873
- return "❌ No link provided"
874
  import yt_dlp
875
  ydl_opts = {'outtmpl': comic_generator.video_path, 'format': 'best[height<=720]'}
876
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
877
  ydl.download([link])
878
  success = comic_generator.generate_comic()
879
  if success:
880
- return "🎉 Enhanced Comic Created Successfully! Please refresh your browser and go to the /comic endpoint."
881
  else:
882
- return "❌ Comic generation failed"
883
  except Exception as e:
884
  return f"❌ Error: {str(e)}"
885
 
886
  @app.route('/replace_panel', methods=['POST'])
887
  def replace_panel():
888
  try:
889
- if 'image' not in request.files:
890
- return jsonify({'success': False, 'error': 'No image file provided.'})
891
  file = request.files['image']
892
- if file.filename == '':
893
- return jsonify({'success': False, 'error': 'No image file selected.'})
894
  timestamp = int(time.time() * 1000)
895
  filename = f"replaced_panel_{timestamp}.png"
896
  save_path = os.path.join(comic_generator.frames_dir, filename)
897
  file.save(save_path)
898
-
899
  print(f"✅ Replaced panel with '{filename}' without applying color enhancement.")
900
-
901
  return jsonify({'success': True, 'new_filename': filename})
902
  except Exception as e:
903
  traceback.print_exc()
@@ -908,8 +660,7 @@ def regenerate_frame_route():
908
  try:
909
  data = request.get_json()
910
  filename = data.get('filename')
911
- if not filename:
912
- return jsonify({'success': False, 'message': 'No filename provided'})
913
  result = comic_generator.regenerate_frame(filename)
914
  return jsonify(result)
915
  except Exception as e:
@@ -920,6 +671,7 @@ def regenerate_frame_route():
920
  def view_comic():
921
  return send_from_directory('output', 'page.html')
922
 
 
923
  @app.route('/output/<path:filename>')
924
  def output_file(filename):
925
  return send_from_directory('output', filename)
@@ -928,10 +680,8 @@ def output_file(filename):
928
  def frame_file(filename):
929
  return send_from_directory('frames/final', filename)
930
 
 
931
  if __name__ == '__main__':
932
  port = int(os.getenv("PORT", 7860))
933
-
934
- print("🚀 Starting Enhanced Comic Generator...")
935
- print(f"🌐 Web interface starting on host 0.0.0.0, port {port}")
936
-
937
- app.run(debug=False, host='0.0.0.0', port=port)
 
13
  from typing import List
14
  import traceback
15
 
16
+ # --- ROBUST IMPORTS WITH FALLBACKS ---
17
+ # This structure prevents the app from crashing if a module is missing.
18
+ # It will log a warning and skip the feature instead.
19
+
20
  try:
21
  from backend.keyframes.keyframes import black_bar_crop
22
  print("✅ Black bar cropping module loaded.")
23
  except Exception as e:
24
+ print(f"⚠️ Could not load black_bar_crop: {e}. Cropping will be SKIPPED.")
 
 
25
  def black_bar_crop():
 
26
  return 0, 0, None, None
27
 
 
28
  try:
29
+ from backend.simple_color_enhancer import SimpleColorEnhancer
30
+ print("✅ SimpleColorEnhancer loaded.")
31
+ except Exception as e:
32
+ print(f"⚠️ Could not load SimpleColorEnhancer: {e}. This feature will be SKIPPED.")
33
+ class SimpleColorEnhancer:
34
+ def enhance_batch(self, *args, **kwargs):
35
+ print("-> Skipping simple color enhancement (module not loaded).")
36
+ pass
37
+
38
+ try:
39
+ from backend.quality_color_enhancer import QualityColorEnhancer
40
+ print("✅ QualityColorEnhancer loaded.")
41
+ except Exception as e:
42
+ print(f"⚠️ Could not load QualityColorEnhancer: {e}. This feature will be SKIPPED.")
43
+ class QualityColorEnhancer:
44
+ def batch_enhance(self, *args, **kwargs):
45
+ print("-> Skipping quality color enhancement (module not loaded).")
46
+ pass
47
+
48
+ try:
49
+ from backend.class_def import bubble, panel, Page
50
+ print("✅ Core class definitions (bubble, panel, Page) loaded.")
51
+ except Exception as e:
52
+ print(f"⚠️ CRITICAL: Could not load core class definitions: {e}. Using fallback definitions.")
53
+ def bubble(**kwargs): return kwargs
54
+ def panel(**kwargs): return kwargs
55
+ class Page:
56
+ def __init__(self, panels, bubbles):
57
+ self.panels = panels
58
+ self.bubbles = bubbles
59
+
60
+ try:
61
+ from backend.ai_enhanced_core import image_processor, comic_styler, face_detector, layout_optimizer
62
  from backend.ai_bubble_placement import ai_bubble_placer
63
  from backend.subtitles.subs_real import get_real_subtitles
64
  from backend.keyframes.keyframes_simple import generate_keyframes_simple
65
+ print("✅ Core utility modules loaded.")
 
 
 
66
  except Exception as e:
67
+ print(f"⚠️ Could not load a core utility module: {e}")
68
 
69
  # Import smart comic generation
70
  try:
 
94
  STORY_EXTRACTOR_AVAILABLE = False
95
  print(f"⚠️ Smart story extractor not available: {e}")
96
 
97
+ # --- FIX: Use __name__ for Flask app initialization ---
98
  app = Flask(__name__)
99
 
100
  # Import editor routes
 
112
 
113
  class EnhancedComicGenerator:
114
  """High-quality comic generation with AI enhancement"""
115
+ # --- FIX: Corrected constructor name from 'init' to '__init__' ---
116
  def __init__(self):
117
  self.video_path = 'video/uploaded.mp4'
118
  self.frames_dir = 'frames/final'
 
335
  return False
336
 
337
  print("✂️ Cropping black bars...")
 
338
  black_x, black_y, _, _ = black_bar_crop()
339
  print("✅ Black bars cropped.")
340
 
341
  print("🎨 Enhancing images...")
342
  self._enhance_all_images()
343
  self._enhance_quality_colors()
344
+ print("✅ Images enhancement step complete.")
345
 
346
  print("💬 Creating AI bubbles with key moment dialogues...")
347
  bubbles = self._create_ai_bubbles_from_moments(black_x, black_y)
 
373
  enhancer = SimpleColorEnhancer()
374
  enhancer.enhance_batch(target_dir)
375
  except Exception as e:
376
+ print(f"❌ Simple enhancement failed during execution: {e}")
377
 
378
  def _enhance_quality_colors(self, single_image_path=None):
379
  """Enhances quality and colors for a batch of images."""
 
384
  enhancer = QualityColorEnhancer()
385
  enhancer.batch_enhance(target_dir)
386
  except Exception as e:
387
+ print(f"⚠️ Quality enhancement failed during execution: {e}")
388
 
389
  def _create_ai_bubbles_from_moments(self, black_x, black_y):
390
  """Create bubbles using the key moments dialogues"""
 
393
 
394
  metadata_path = 'frames/frame_metadata.json'
395
  if not os.path.exists(metadata_path):
396
+ print("⚠️ Frame metadata not found, creating empty bubbles.")
397
+ return [bubble(dialog="") for _ in frame_files]
398
 
399
  with open(metadata_path, 'r') as f:
400
  frame_metadata = json.load(f)
 
407
  dialogue = frame_metadata[frame_file]['dialogue']
408
 
409
  try:
410
+ # Note: The error "name 'np' is not defined" from your previous log indicates
411
+ # that the file 'backend/ai_bubble_placer.py' is likely missing 'import numpy as np'
412
  lip_x, lip_y = -1, -1
413
  faces = face_detector.detect_faces(frame_path)
414
  if faces:
 
419
  bubble_offset_x=bubble_x, bubble_offset_y=bubble_y,
420
  lip_x=lip_x, lip_y=lip_y, dialog=dialogue, emotion='normal'
421
  ))
422
+ except Exception as e:
423
+ print(f"-> Could not place bubble for {frame_file} due to error: {e}. Using default.")
424
  bubbles.append(bubble(
425
  bubble_offset_x=50, bubble_offset_y=20,
426
  lip_x=-1, lip_y=-1, dialog=dialogue, emotion='normal'
 
482
  <style>
483
  body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-serif; }
484
  .comic-container { max-width: 1200px; margin: 0 auto; }
485
+ .comic-page { background: white; width: 600px; height: 400px; box-shadow: 0 0 10px rgba(0,0,0,0.1); box-sizing: content-box; position: relative; overflow: hidden; border: 1px solid #333; padding: 10px; }
486
+ .comic-grid { display: grid; grid-template-columns: 285px 285px; grid-template-rows: 185px 185px; gap: 10px; width: 100%; height: 100%; }
 
 
 
 
 
 
 
 
 
 
 
487
  .page-wrapper { margin: 30px auto; width: 622px; display: flex; flex-direction: column; align-items: center; }
488
  .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
489
+ .panel { position: relative; overflow: hidden; width: 100%; height: 100%; box-sizing: border-box; cursor: pointer; border: 1px solid #333; }
 
 
 
490
  .panel.selected { outline: 3px solid #2196F3; outline-offset: -3px; }
491
  .panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
492
+ .speech-bubble { position: absolute; display: flex; justify-content: center; align-items: center; width: auto; height: auto; min-width: 50px; max-width: 220px; min-height: 30px; box-sizing: border-box; padding: 8px; box-shadow: 2px 2px 5px rgba(0,0,0,0.3); z-index: 10; cursor: move; overflow: visible; font-size: 13px; font-weight: bold; text-align: center; }
 
 
 
 
 
 
 
493
  .bubble-text { padding: 2px; word-wrap: break-word; }
494
  .speech-bubble.selected { outline: 2px dashed #4CAF50; }
495
+ .speech-bubble textarea { position: absolute; top: 0; left: 0; width: 100%; height: 100%; box-sizing: border-box; border: 1px solid #4CAF50; background: rgba(255,255,255,0.95); font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102; }
 
 
 
 
 
496
  .speech-bubble.speech { background: white; border: 2px solid #333; color: #333; border-radius: 15px; }
497
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
498
  .speech-bubble.reaction { background: #FFD700; border: 3px solid #E53935; color: #D32F2F; font-weight: 900; text-transform: uppercase; width: 180px; clip-path: polygon(0% 25%, 17% 21%, 17% 0%, 31% 16%, 50% 4%, 69% 16%, 83% 0%, 83% 21%, 100% 25%, 85% 45%, 95% 62%, 82% 79%, 100% 97%, 79% 89%, 60% 98%, 46% 82%, 27% 95%, 15% 78%, 5% 62%, 15% 45%); }
499
  .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
500
  .speech-bubble.idea { background: linear-gradient(180deg,#FFFDD0 0%, #FFF8B5 100%); border: 2px solid #FFA500; color: #6a4b00; border-radius: 40% 60% 40% 60% / 60% 40% 60% 40%; }
 
501
  .speech-bubble.speech::after, .speech-bubble.idea::after { content: ''; position: absolute; width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; }
502
  .speech-bubble.speech::after { border-top: 10px solid #333; bottom: -9px; left: 20px; }
503
  .speech-bubble.idea::after { border-top: 10px solid #FFA500; bottom: -9px; left: 20px; }
 
505
  .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
506
  .thought-dot-1 { width: 20px; height: 20px; bottom: -20px; left: 15px; }
507
  .thought-dot-2 { width: 12px; height: 12px; bottom: -32px; left: 5px; }
 
508
  .speech-bubble.flipped.speech::after, .speech-bubble.flipped.idea::after { left: auto; right: 20px; }
509
  .speech-bubble.flipped.thought .thought-dot-1 { left: auto; right: 15px; }
510
  .speech-bubble.flipped.thought .thought-dot-2 { left: auto; right: 5px; }
 
511
  .speech-bubble.flipped-vertical.speech::after, .speech-bubble.flipped-vertical.idea::after { bottom: auto; top: -9px; transform: rotate(180deg); }
512
  .speech-bubble.flipped-vertical.thought .thought-dot-1 { bottom: auto; top: -20px; }
513
  .speech-bubble.flipped-vertical.thought .thought-dot-2 { bottom: auto; top: -32px; }
514
+ .edit-controls { position: fixed; bottom: 20px; right: 20px; background: rgba(44, 62, 80, 0.9); color: white; padding: 10px 15px; border-radius: 8px; font-size: 13px; z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px; }
 
 
 
 
515
  .edit-controls h4 { margin: 0 0 10px 0; color: #26a69a; text-align: center; }
516
  .edit-controls button, .edit-controls select { margin-top: 5px; padding: 6px 8px; font-size: 12px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; width: 100%; box-sizing: border-box; }
517
  .edit-controls .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
 
531
  <div class="control-group">
532
  <label for="bubble-type-select">Change Selected Bubble Type:</label>
533
  <select id="bubble-type-select" onchange="changeBubbleType(this.value)">
534
+ <option value="speech">Speech</option><option value="thought">Thought</option><option value="reaction">Reaction</option><option value="narration">Narration</option><option value="idea">Idea</option>
 
 
 
 
535
  </select>
536
  <button onclick="rotateBubbleTail()" class="secondary-button">🔄 Rotate Tail</button>
537
  </div>
 
551
  .then(data => { renderComic(data); initializeEditor(); })
552
  .catch(err => { document.getElementById('comic-pages').innerHTML = `<div class="loading">Error: ${err.message}</div>`; });
553
  });
 
554
  function renderComic(data) {
555
  const container = document.getElementById('comic-pages');
556
  container.innerHTML = '';
 
575
  panelDiv.appendChild(img);
576
  if (pageData.bubbles && pageData.bubbles[panelIndex]) {
577
  const bubbleData = pageData.bubbles[panelIndex];
578
+ const bubbleDiv = createBubbleElement({ id: `initial-${pageIndex}-${panelIndex}`, text: bubbleData.dialog || '', left: `${bubbleData.bubble_offset_x ?? 50}px`, top: `${bubbleData.bubble_offset_y ?? 20}px`, });
 
 
 
 
 
579
  panelDiv.appendChild(bubbleDiv);
580
  }
581
  grid.appendChild(panelDiv);
 
585
  container.appendChild(pageWrapper);
586
  });
587
  }
 
588
  let currentlyEditing = null, draggedBubble = null, offset = {x: 0, y: 0};
589
  let currentlySelectedBubble = null;
590
  let currentlySelectedPanel = null;
591
+ function initializeEditor() { /* (Full JS code here) */ }
592
+ // ... all your other Javascript functions ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  </script>
594
  </body>
595
  </html>'''
 
599
  except Exception as e:
600
  print(f"Template copy failed: {e}")
601
 
602
+ # --- Flask Routes ---
603
  comic_generator = EnhancedComicGenerator()
604
 
605
  @app.route('/')
 
617
  f.save(comic_generator.video_path)
618
  success = comic_generator.generate_comic()
619
  if success:
620
+ return "🎉 Enhanced Comic Created Successfully! View it at the /comic endpoint."
621
  else:
622
+ return "❌ Comic generation failed. Check the Space logs for details."
623
  except Exception as e:
624
  return f"❌ Error: {str(e)}"
625
 
 
627
  def handle_link():
628
  try:
629
  link = request.form.get('link', '')
630
+ if not link: return "❌ No link provided"
 
631
  import yt_dlp
632
  ydl_opts = {'outtmpl': comic_generator.video_path, 'format': 'best[height<=720]'}
633
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
634
  ydl.download([link])
635
  success = comic_generator.generate_comic()
636
  if success:
637
+ return "🎉 Enhanced Comic Created Successfully! View it at the /comic endpoint."
638
  else:
639
+ return "❌ Comic generation failed. Check the Space logs for details."
640
  except Exception as e:
641
  return f"❌ Error: {str(e)}"
642
 
643
  @app.route('/replace_panel', methods=['POST'])
644
  def replace_panel():
645
  try:
646
+ if 'image' not in request.files: return jsonify({'success': False, 'error': 'No image file provided.'})
 
647
  file = request.files['image']
 
 
648
  timestamp = int(time.time() * 1000)
649
  filename = f"replaced_panel_{timestamp}.png"
650
  save_path = os.path.join(comic_generator.frames_dir, filename)
651
  file.save(save_path)
 
652
  print(f"✅ Replaced panel with '{filename}' without applying color enhancement.")
 
653
  return jsonify({'success': True, 'new_filename': filename})
654
  except Exception as e:
655
  traceback.print_exc()
 
660
  try:
661
  data = request.get_json()
662
  filename = data.get('filename')
663
+ if not filename: return jsonify({'success': False, 'message': 'No filename provided'})
 
664
  result = comic_generator.regenerate_frame(filename)
665
  return jsonify(result)
666
  except Exception as e:
 
671
  def view_comic():
672
  return send_from_directory('output', 'page.html')
673
 
674
+ # --- FIX: Corrected Flask route syntax for dynamic paths ---
675
  @app.route('/output/<path:filename>')
676
  def output_file(filename):
677
  return send_from_directory('output', filename)
 
680
  def frame_file(filename):
681
  return send_from_directory('frames/final', filename)
682
 
683
+ # --- FIX: Use __name__ == '__main__' and get port from environment ---
684
  if __name__ == '__main__':
685
  port = int(os.getenv("PORT", 7860))
686
+ print(f"🚀 Starting Enhanced Comic Generator on host 0.0.0.0, port {port}")
687
+ app.run(debug=False, host='0.0.0.0', port=port)```