jhh6576 commited on
Commit
e22c38c
·
verified ·
1 Parent(s): 3fdbf42

update fix error

Browse files
Files changed (1) hide show
  1. app_enhanced.py +383 -411
app_enhanced.py CHANGED
@@ -21,7 +21,8 @@ try:
21
  from backend.ai_bubble_placement import ai_bubble_placer
22
  from backend.subtitles.subs_real import get_real_subtitles
23
  from backend.keyframes.keyframes_simple import generate_keyframes_simple
24
- from backend.keyframes.keyframes import black_bar_crop
 
25
  from backend.class_def import bubble, panel, Page
26
  from backend.simple_color_enhancer import SimpleColorEnhancer
27
  from backend.quality_color_enhancer import QualityColorEnhancer
@@ -57,6 +58,7 @@ except Exception as e:
57
  STORY_EXTRACTOR_AVAILABLE = False
58
  print(f"⚠️ Smart story extractor not available: {e}")
59
 
 
60
  app = Flask(__name__)
61
 
62
  # Import editor routes
@@ -74,6 +76,7 @@ os.makedirs('output', exist_ok=True)
74
 
75
  class EnhancedComicGenerator:
76
  """High-quality comic generation with AI enhancement"""
 
77
  def __init__(self):
78
  self.video_path = 'video/uploaded.mp4'
79
  self.frames_dir = 'frames/final'
@@ -133,8 +136,7 @@ class EnhancedComicGenerator:
133
 
134
  if frame_filename not in frame_to_time:
135
  return {"success": False, "message": "Panel not linked to original video."}
136
-
137
- # Fix: Handle the new metadata structure
138
  if isinstance(frame_to_time[frame_filename], dict):
139
  current_time = frame_to_time[frame_filename]['time']
140
  else:
@@ -156,7 +158,6 @@ class EnhancedComicGenerator:
156
  new_path = os.path.join(self.frames_dir, frame_filename)
157
  cv2.imwrite(new_path, frame)
158
 
159
- # Update metadata with new time
160
  if isinstance(frame_to_time[frame_filename], dict):
161
  frame_to_time[frame_filename]['time'] = target_time
162
  else:
@@ -187,18 +188,13 @@ class EnhancedComicGenerator:
187
  print("❌ Cannot open video for keyframe extraction")
188
  return False
189
 
190
- # Get video properties
191
  fps = cap.get(cv2.CAP_PROP_FPS)
192
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
193
  duration = total_frames / fps
194
 
195
- # Sort key moments by start time to maintain chronological order
196
  key_moments.sort(key=lambda x: x['start'])
197
 
198
- # Limit to max_frames while preserving story flow
199
  if len(key_moments) > max_frames:
200
- # Use a more intelligent sampling to preserve story flow
201
- # Take first few moments, then sample evenly, then last few moments
202
  first_count = min(5, max_frames // 4)
203
  last_count = min(5, max_frames // 4)
204
  middle_count = max_frames - first_count - last_count
@@ -208,7 +204,6 @@ class EnhancedComicGenerator:
208
  last_moments = key_moments[-last_count:]
209
  middle_moments = key_moments[first_count:-last_count]
210
 
211
- # Sample evenly from middle moments
212
  if len(middle_moments) > middle_count:
213
  step = len(middle_moments) / middle_count
214
  middle_sampled = [middle_moments[int(i * step)] for i in range(middle_count)]
@@ -217,7 +212,6 @@ class EnhancedComicGenerator:
217
 
218
  key_moments = first_moments + middle_sampled + last_moments
219
  else:
220
- # Just take evenly spaced moments
221
  step = len(key_moments) / max_frames
222
  key_moments = [key_moments[int(i * step)] for i in range(max_frames)]
223
 
@@ -225,17 +219,13 @@ class EnhancedComicGenerator:
225
  frame_count = 0
226
 
227
  for moment in key_moments:
228
- # Use the middle of the subtitle segment as the frame time
229
  frame_time = (moment['start'] + moment['end']) / 2
230
 
231
- # Skip if beyond video duration
232
  if frame_time > duration:
233
  continue
234
 
235
- # Calculate frame number
236
  frame_number = int(frame_time * fps)
237
 
238
- # Set position and extract frame
239
  cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
240
  ret, frame = cap.read()
241
 
@@ -244,7 +234,6 @@ class EnhancedComicGenerator:
244
  frame_path = os.path.join(self.frames_dir, frame_filename)
245
  cv2.imwrite(frame_path, frame)
246
 
247
- # Store metadata for this frame
248
  frame_metadata[frame_filename] = {
249
  'time': frame_time,
250
  'dialogue': moment['text'],
@@ -256,7 +245,6 @@ class EnhancedComicGenerator:
256
 
257
  cap.release()
258
 
259
- # Save frame metadata with dialogue
260
  with open(os.path.join('frames', 'frame_metadata.json'), 'w') as f:
261
  json.dump(frame_metadata, f, indent=2)
262
 
@@ -285,7 +273,6 @@ class EnhancedComicGenerator:
285
  print("❌ Subtitle file (test1.srt) not found!")
286
  return False
287
 
288
- # Extract story for key moments
289
  try:
290
  from backend.full_story_extractor import FullStoryExtractor
291
  extractor = FullStoryExtractor()
@@ -301,21 +288,19 @@ class EnhancedComicGenerator:
301
  print(f"⚠️ Full story extraction failed, using all subtitles: {e}")
302
  filtered_subs = all_subs
303
 
304
- # Convert to key moments format
305
  key_moments = [{'index': s.index, 'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in filtered_subs]
306
 
307
- # Save key moments for reference
308
  with open(os.path.join(self.output_dir, 'key_moments.json'), 'w', encoding='utf-8') as f:
309
  json.dump(key_moments, f, indent=2)
310
 
311
- # Generate frames at key moments
312
  print("🎬 Extracting frames at key moments...")
313
  if not self.generate_keyframes_from_moments(self.video_path, key_moments, max_frames=48):
314
  print("❌ Keyframe extraction failed.")
315
  return False
316
 
317
  print("✂️ Cropping black bars...")
318
- black_x, black_y, _, _ = black_bar_crop()
 
319
  print("✅ Black bars cropped.")
320
 
321
  print("🎨 Enhancing images...")
@@ -371,7 +356,6 @@ class EnhancedComicGenerator:
371
  bubbles = []
372
  frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
373
 
374
- # Load frame metadata with dialogues
375
  metadata_path = 'frames/frame_metadata.json'
376
  if not os.path.exists(metadata_path):
377
  print("⚠️ Frame metadata not found, using empty bubbles")
@@ -384,7 +368,6 @@ class EnhancedComicGenerator:
384
  frame_path = os.path.join(self.frames_dir, frame_file)
385
  dialogue = ""
386
 
387
- # Get dialogue from metadata
388
  if frame_file in frame_metadata:
389
  dialogue = frame_metadata[frame_file]['dialogue']
390
 
@@ -454,394 +437,393 @@ class EnhancedComicGenerator:
454
  template_html = '''<!DOCTYPE html>
455
  <html lang="en">
456
  <head>
457
- <meta charset="UTF-8">
458
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
459
- <title>Generated Comic - Interactive Editor</title>
460
- <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
461
- <style>
462
- body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-serif; }
463
- .comic-container { max-width: 1200px; margin: 0 auto; }
464
- .comic-page {
465
- background: white; width: 600px; height: 400px;
466
- box-shadow: 0 0 10px rgba(0,0,0,0.1); box-sizing: content-box;
467
- position: relative; overflow: hidden; border: 1px solid #333;
468
- padding: 10px;
469
- }
470
- .comic-grid {
471
- display: grid;
472
- grid-template-columns: 285px 285px;
473
- grid-template-rows: 185px 185px;
474
- gap: 10px;
475
- width: 100%; height: 100%;
476
- }
477
- .page-wrapper { margin: 30px auto; width: 622px; display: flex; flex-direction: column; align-items: center; }
478
- .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
479
- .panel {
480
- position: relative; overflow: hidden; width: 100%; height: 100%;
481
- box-sizing: border-box; cursor: pointer; border: 1px solid #333;
482
- }
483
- .panel.selected { outline: 3px solid #2196F3; outline-offset: -3px; }
484
- .panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
485
- .speech-bubble {
486
- position: absolute; display: flex; justify-content: center; align-items: center;
487
- width: auto; height: auto;
488
- min-width: 50px; max-width: 220px; min-height: 30px;
489
- box-sizing: border-box; padding: 8px;
490
- box-shadow: 2px 2px 5px rgba(0,0,0,0.3); z-index: 10;
491
- cursor: move; overflow: visible; font-size: 13px; font-weight: bold; text-align: center;
492
- }
493
- .bubble-text { padding: 2px; word-wrap: break-word; }
494
- .speech-bubble.selected { outline: 2px dashed #4CAF50; }
495
- .speech-bubble textarea {
496
- position: absolute; top: 0; left: 0; width: 100%; height: 100%; box-sizing: border-box;
497
- border: 1px solid #4CAF50; background: rgba(255,255,255,0.95);
498
- font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102;
499
- }
500
- /* --- Bubble Styles --- */
501
- .speech-bubble.speech { background: white; border: 2px solid #333; color: #333; border-radius: 15px; }
502
- .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
503
- .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%); }
504
- .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
505
- .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%; }
506
- /* --- Tail and Dot Styles (4-Direction Flip) --- */
507
- .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; }
508
- .speech-bubble.speech::after { border-top: 10px solid #333; bottom: -9px; left: 20px; }
509
- .speech-bubble.idea::after { border-top: 10px solid #FFA500; bottom: -9px; left: 20px; }
510
- .speech-bubble.thought::after { display: none; }
511
- .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
512
- .thought-dot-1 { width: 20px; height: 20px; bottom: -20px; left: 15px; }
513
- .thought-dot-2 { width: 12px; height: 12px; bottom: -32px; left: 5px; }
514
- /* Horizontal Flip */
515
- .speech-bubble.flipped.speech::after, .speech-bubble.flipped.idea::after { left: auto; right: 20px; }
516
- .speech-bubble.flipped.thought .thought-dot-1 { left: auto; right: 15px; }
517
- .speech-bubble.flipped.thought .thought-dot-2 { left: auto; right: 5px; }
518
- /* Vertical Flip */
519
- .speech-bubble.flipped-vertical.speech::after, .speech-bubble.flipped-vertical.idea::after { bottom: auto; top: -9px; transform: rotate(180deg); }
520
- .speech-bubble.flipped-vertical.thought .thought-dot-1 { bottom: auto; top: -20px; }
521
- .speech-bubble.flipped-vertical.thought .thought-dot-2 { bottom: auto; top: -32px; }
522
- .edit-controls {
523
- position: fixed; bottom: 20px; right: 20px; background: rgba(44, 62, 80, 0.9);
524
- color: white; padding: 10px 15px; border-radius: 8px; font-size: 13px;
525
- z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px;
526
- }
527
- .edit-controls h4 { margin: 0 0 10px 0; color: #26a69a; text-align: center; }
528
- .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; }
529
- .edit-controls .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
530
- .edit-controls .reset-button { background-color: #e74c3c; }
531
- .edit-controls .action-button { background-color: #4CAF50; }
532
- .edit-controls .secondary-button { background-color: #f39c12; }
533
- </style>
534
  </head>
535
  <body>
536
- <div class="comic-container">
537
- <h1 class="comic-title">🎬 Generated Comic</h1>
538
- <div id="comic-pages"><div class="loading">Loading comic...</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  </div>
540
- <input type="file" id="image-uploader" style="display: none;" accept="image/*">
541
-
542
- <div class="edit-controls">
543
- <h4>✏️ Interactive Editor</h4>
544
- <div class="control-group">
545
- <label for="bubble-type-select">Change Selected Bubble Type:</label>
546
- <select id="bubble-type-select" onchange="changeBubbleType(this.value)">
547
- <option value="speech">Speech</option>
548
- <option value="thought">Thought</option>
549
- <option value="reaction">Reaction</option>
550
- <option value="narration">Narration</option>
551
- <option value="idea">Idea</option>
552
- </select>
553
- <button onclick="rotateBubbleTail()" class="secondary-button">�� Rotate Tail</button>
554
- </div>
555
- <div class="control-group">
556
- <button onclick="replacePanelImage()" class="action-button">🖼️ Replace Panel Image</button>
557
- <button onclick="regenerateFrame()" class="action-button">🔄 Regenerate Frame</button>
558
- <button onclick="exportPagesToPNG()" class="action-button" style="background-color: #2196F3;">🖨️ Export Pages to PNG</button>
559
- </div>
560
- <div class="control-group">
561
- <button onclick="clearSavedState()" class="reset-button">🔄 Clear Edits & Reset</button>
562
- </div>
563
  </div>
564
- <script>
565
- document.addEventListener('DOMContentLoaded', () => {
566
- fetch('/output/pages.json')
567
- .then(res => res.ok ? res.json() : Promise.reject(new Error('Failed to load pages.json')))
568
- .then(data => { renderComic(data); initializeEditor(); })
569
- .catch(err => { document.getElementById('comic-pages').innerHTML = `<div class="loading">Error: ${err.message}</div>`; });
570
- });
571
-
572
- function renderComic(data) {
573
- const container = document.getElementById('comic-pages');
574
- container.innerHTML = '';
575
- if (!data || data.length === 0) return;
576
- data.forEach((pageData, pageIndex) => {
577
- if (!pageData.panels || pageData.panels.length === 0) return;
578
- const pageWrapper = document.createElement('div');
579
- pageWrapper.className = 'page-wrapper';
580
- const pageTitleEl = document.createElement('h2');
581
- pageTitleEl.className = 'page-title';
582
- pageTitleEl.textContent = `Page ${pageIndex + 1}`;
583
- pageWrapper.appendChild(pageTitleEl);
584
- const pageDiv = document.createElement('div');
585
- pageDiv.className = 'comic-page';
586
- const grid = document.createElement('div');
587
- grid.className = 'comic-grid';
588
- pageData.panels.forEach((panelData, panelIndex) => {
589
- const panelDiv = document.createElement('div');
590
- panelDiv.className = 'panel';
591
- const img = document.createElement('img');
592
- img.src = '/frames/final/' + panelData.image;
593
- panelDiv.appendChild(img);
594
- if (pageData.bubbles && pageData.bubbles[panelIndex]) {
595
- const bubbleData = pageData.bubbles[panelIndex];
596
- const bubbleDiv = createBubbleElement({
597
- id: `initial-${pageIndex}-${panelIndex}`,
598
- text: bubbleData.dialog || '',
599
- left: `${bubbleData.bubble_offset_x ?? 50}px`,
600
- top: `${bubbleData.bubble_offset_y ?? 20}px`,
601
- });
602
- panelDiv.appendChild(bubbleDiv);
603
- }
604
- grid.appendChild(panelDiv);
605
- });
606
- pageDiv.appendChild(grid);
607
- pageWrapper.appendChild(pageDiv);
608
- container.appendChild(pageWrapper);
609
- });
610
- }
611
-
612
- let currentlyEditing = null, draggedBubble = null, offset = {x: 0, y: 0};
613
- let currentlySelectedBubble = null;
614
- let currentlySelectedPanel = null;
615
-
616
- function initializeEditor() {
617
- document.querySelectorAll('.panel').forEach(p => p.addEventListener('click', e => selectPanel(e.currentTarget)));
618
- document.querySelectorAll('.speech-bubble').forEach(b => initializeBubbleEvents(b));
619
- document.addEventListener('mousemove', e => { if (draggedBubble) drag(e); });
620
- document.addEventListener('mouseup', () => { if (draggedBubble) stopDrag(); });
621
- }
622
-
623
- function initializeBubbleEvents(bubble) {
624
- bubble.addEventListener('dblclick', e => { e.stopPropagation(); editBubbleText(bubble); });
625
- bubble.addEventListener('mousedown', e => startDrag(e));
626
- bubble.addEventListener('click', e => { e.stopPropagation(); selectBubble(bubble); });
627
- bubble.addEventListener('wheel', e => {
628
- e.preventDefault();
629
- const currentWidth = parseFloat(bubble.style.width) || bubble.offsetWidth;
630
- const newWidth = currentWidth - (e.deltaY > 0 ? 10 : -10);
631
- if (newWidth >= 60) {
632
- bubble.style.width = `${newWidth}px`;
633
- bubble.style.height = 'auto';
634
- }
635
- }, { passive: false });
636
- }
637
-
638
- function createBubbleElement(data) {
639
- const bubbleDiv = document.createElement('div');
640
- bubbleDiv.dataset.id = data.id;
641
- const textSpan = document.createElement('span');
642
- textSpan.className = 'bubble-text';
643
- textSpan.textContent = data.text;
644
- bubbleDiv.appendChild(textSpan);
645
- bubbleDiv.style.left = data.left;
646
- bubbleDiv.style.top = data.top;
647
- applyBubbleType(bubbleDiv, 'speech'); // Default to speech
648
- return bubbleDiv;
649
- }
650
-
651
- function applyBubbleType(bubble, type) {
652
- bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
653
- let classesToKeep = 'speech-bubble';
654
- if (bubble.classList.contains('selected')) classesToKeep += ' selected';
655
- if (bubble.classList.contains('flipped')) classesToKeep += ' flipped';
656
- if (bubble.classList.contains('flipped-vertical')) classesToKeep += ' flipped-vertical';
657
- bubble.className = classesToKeep;
658
- bubble.classList.add(type);
659
- bubble.dataset.type = type;
660
- if (type === 'thought') {
661
- for (let i = 1; i <= 2; i++) {
662
- const dot = document.createElement('div');
663
- dot.className = `thought-dot thought-dot-${i}`;
664
- bubble.appendChild(dot);
665
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  }
667
- }
668
-
669
- function changeBubbleType(type) {
670
- if (!currentlySelectedBubble) return;
671
- applyBubbleType(currentlySelectedBubble, type);
672
- }
673
-
674
- function rotateBubbleTail() {
675
- if (!currentlySelectedBubble) return alert("Please select a bubble to rotate.");
676
- const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
677
- const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
678
- if (!isFlippedH && !isFlippedV) { // State 0 -> 1
679
- currentlySelectedBubble.classList.add('flipped');
680
- } else if (isFlippedH && !isFlippedV) { // State 1 -> 2
681
- currentlySelectedBubble.classList.add('flipped-vertical');
682
- } else if (isFlippedH && isFlippedV) { // State 2 -> 3
683
- currentlySelectedBubble.classList.remove('flipped');
684
- } else { // State 3 -> 0
685
- currentlySelectedBubble.classList.remove('flipped-vertical');
 
 
 
 
 
 
 
 
 
 
 
686
  }
687
  }
688
-
689
- function selectPanel(panel) {
690
- document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
691
- panel.classList.add('selected');
692
- currentlySelectedPanel = panel;
693
- selectBubble(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
694
  }
695
-
696
- function selectBubble(bubble) {
697
- if (currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
698
- currentlySelectedBubble = bubble;
699
- if (currentlySelectedBubble) {
700
- currentlySelectedBubble.classList.add('selected');
701
- document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
702
- document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
703
- }
 
 
 
 
 
 
 
704
  }
705
-
706
- function editBubbleText(bubble) {
707
- if (currentlyEditing) return;
708
- currentlyEditing = bubble;
709
- const textSpan = bubble.querySelector('.bubble-text');
710
- const currentText = textSpan.textContent;
711
- textSpan.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
712
  bubble.style.height = 'auto';
713
- const textarea = document.createElement('textarea');
714
- textarea.value = currentText;
715
- bubble.appendChild(textarea);
716
- textarea.focus();
717
- const finishEditing = () => {
718
- textSpan.textContent = textarea.value;
719
- bubble.removeChild(textarea);
720
- textSpan.style.display = '';
721
- currentlyEditing = null;
722
- bubble.style.height = 'auto';
723
- };
724
- textarea.addEventListener('blur', finishEditing, { once: true });
725
- textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); }});
726
- }
727
-
728
- function startDrag(e) {
729
- const bubble = e.target.closest('.speech-bubble');
730
- if (!bubble || currentlyEditing) return;
731
- draggedBubble = bubble;
732
- selectBubble(bubble);
733
- const rect = bubble.getBoundingClientRect();
734
- offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
735
- }
736
-
737
- function drag(e) {
738
- const parentRect = draggedBubble.parentElement.getBoundingClientRect();
739
- let x = e.clientX - parentRect.left - offset.x;
740
- let y = e.clientY - parentRect.top - offset.y;
741
- draggedBubble.style.left = `${x}px`;
742
- draggedBubble.style.top = `${y}px`;
743
- }
744
-
745
- function stopDrag() {
746
- draggedBubble = null;
747
  }
748
-
749
- function clearSavedState() {
750
- if (confirm("Reset all edits to the original AI-generated comic?")) {
751
- localStorage.removeItem('comicEditorState');
752
- window.location.reload();
 
 
 
 
 
 
 
 
 
 
753
  }
754
  }
755
-
756
- async function exportPagesToPNG() {
757
- const pages = document.querySelectorAll('.comic-page');
758
- if (pages.length === 0) return alert("No pages found.");
759
- alert(`Starting export of ${pages.length} page(s).`);
760
- for (let i = 0; i < pages.length; i++) {
761
- try {
762
- const canvas = await html2canvas(pages[i], { scale: 2 });
763
- const link = document.createElement('a');
764
- link.download = `comic-page-${i + 1}.png`;
765
- link.href = canvas.toDataURL('image/png');
766
- link.click();
767
- } catch (err) {
768
- alert(`Failed to export page ${i + 1}.`);
769
- }
770
- }
771
  }
772
-
773
- function replacePanelImage() {
774
- if (!currentlySelectedPanel) {
775
- alert("Please select a panel first.");
776
- return;
777
- }
778
- const img = currentlySelectedPanel.querySelector('img');
779
- const uploader = document.getElementById('image-uploader');
780
- const oneTimeListener = (event) => {
781
- const file = event.target.files[0];
782
- if (!file) return;
783
- const formData = new FormData();
784
- formData.append('image', file);
785
- img.style.opacity = '0.5';
786
- fetch('/replace_panel', { method: 'POST', body: formData })
787
- .then(response => response.json())
788
- .then(data => {
789
- if (data.success) {
790
- img.src = `/frames/final/${data.new_filename}?t=${new Date().getTime()}`;
791
- } else {
792
- alert('Error replacing image: ' + data.error);
793
- }
794
- img.style.opacity = '1';
795
- })
796
- .catch(error => {
797
- alert('An error occurred during the upload.');
798
- img.style.opacity = '1';
799
- });
800
- uploader.removeEventListener('change', oneTimeListener);
801
- uploader.value = '';
802
- };
803
- uploader.addEventListener('change', oneTimeListener, { once: true });
804
- uploader.click();
805
  }
 
 
806
 
807
- function regenerateFrame() {
808
- if (!currentlySelectedPanel) {
809
- alert("Please select a panel first.");
810
- return;
811
- }
812
- const img = currentlySelectedPanel.querySelector('img');
813
- const currentSrc = img.src;
814
-
815
- let filename = currentSrc.substring(currentSrc.lastIndexOf('/') + 1);
816
- if (filename.includes('?')) {
817
- filename = filename.split('?')[0];
818
- }
819
 
820
- if (!confirm(`Regenerate frame "${filename}" with a better version?`)) {
821
- return;
822
- }
823
- img.style.opacity = '0.5';
824
- fetch('/regenerate_frame', {
825
- method: 'POST',
826
- headers: { 'Content-Type': 'application/json' },
827
- body: JSON.stringify({ filename: filename })
828
- })
829
- .then(response => response.json())
830
- .then(data => {
831
- if (data.success) {
832
- img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
833
- alert(data.message);
834
- } else {
835
- alert('Error: ' + data.message);
836
- }
837
- img.style.opacity = '1';
838
- })
839
- .catch(error => {
840
- alert('An error occurred during regeneration.');
841
- img.style.opacity = '1';
842
- });
843
  }
844
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
845
  </body>
846
  </html>'''
847
  with open(os.path.join(self.output_dir, 'page.html'), 'w', encoding='utf-8') as f:
@@ -864,11 +846,11 @@ def upload_file():
864
  return "❌ No file selected"
865
  f = request.files['file']
866
  if os.path.exists(comic_generator.video_path):
867
- os.remove(comic_generator.video_path)
868
  f.save(comic_generator.video_path)
869
  success = comic_generator.generate_comic()
870
  if success:
871
- webbrowser.open("http://localhost:7860/comic")
872
  return "🎉 Enhanced Comic Created Successfully!"
873
  else:
874
  return "❌ Comic generation failed"
@@ -887,7 +869,7 @@ def handle_link():
887
  ydl.download([link])
888
  success = comic_generator.generate_comic()
889
  if success:
890
- webbrowser.open("http://localhost:7860/comic")
891
  return "🎉 Enhanced Comic Created Successfully!"
892
  else:
893
  return "❌ Comic generation failed"
@@ -907,11 +889,6 @@ def replace_panel():
907
  save_path = os.path.join(comic_generator.frames_dir, filename)
908
  file.save(save_path)
909
 
910
- # --- FIX: Color enhancement is now skipped for replaced images ---
911
- # print(f"🖼️ Enhancing replaced panel image: {filename}")
912
- # comic_generator._enhance_all_images(single_image_path=save_path)
913
- # comic_generator._enhance_quality_colors(single_image_path=save_path)
914
- # print(f"✅ Enhancement complete for {filename}")
915
  print(f"✅ Replaced panel with '{filename}' without applying color enhancement.")
916
 
917
  return jsonify({'success': True, 'new_filename': filename})
@@ -936,23 +913,18 @@ def regenerate_frame_route():
936
  def view_comic():
937
  return send_from_directory('output', 'page.html')
938
 
 
939
  @app.route('/output/<path:filename>')
940
  def output_file(filename):
941
  return send_from_directory('output', filename)
942
 
 
943
  @app.route('/frames/final/<path:filename>')
944
  def frame_file(filename):
945
  return send_from_directory('frames/final', filename)
946
 
 
947
  if __name__ == '__main__':
948
  print("🚀 Starting Enhanced Comic Generator...")
949
- print("🌐 Web interface available at: http://localhost:7860")
950
- app.run(debug=True, host='0.0.0.0', port=7860)
951
-
952
-
953
- if __name__ == "__main__":
954
- import os
955
- # Hugging Face Spaces provides the port in the PORT env var.
956
- port = int(os.environ.get("PORT", 7860))
957
- # listen on all interfaces so the container receives external traffic
958
- app.run(host="0.0.0.0", port=port)
 
21
  from backend.ai_bubble_placement import ai_bubble_placer
22
  from backend.subtitles.subs_real import get_real_subtitles
23
  from backend.keyframes.keyframes_simple import generate_keyframes_simple
24
+ # --- FIX: Import the module itself to prevent namespace issues ---
25
+ from backend.keyframes import keyframes
26
  from backend.class_def import bubble, panel, Page
27
  from backend.simple_color_enhancer import SimpleColorEnhancer
28
  from backend.quality_color_enhancer import QualityColorEnhancer
 
58
  STORY_EXTRACTOR_AVAILABLE = False
59
  print(f"⚠️ Smart story extractor not available: {e}")
60
 
61
+ # --- FIX: Use __name__ for Flask app initialization ---
62
  app = Flask(__name__)
63
 
64
  # Import editor routes
 
76
 
77
  class EnhancedComicGenerator:
78
  """High-quality comic generation with AI enhancement"""
79
+ # --- FIX: Corrected constructor name from 'init' to '__init__' ---
80
  def __init__(self):
81
  self.video_path = 'video/uploaded.mp4'
82
  self.frames_dir = 'frames/final'
 
136
 
137
  if frame_filename not in frame_to_time:
138
  return {"success": False, "message": "Panel not linked to original video."}
139
+
 
140
  if isinstance(frame_to_time[frame_filename], dict):
141
  current_time = frame_to_time[frame_filename]['time']
142
  else:
 
158
  new_path = os.path.join(self.frames_dir, frame_filename)
159
  cv2.imwrite(new_path, frame)
160
 
 
161
  if isinstance(frame_to_time[frame_filename], dict):
162
  frame_to_time[frame_filename]['time'] = target_time
163
  else:
 
188
  print("❌ Cannot open video for keyframe extraction")
189
  return False
190
 
 
191
  fps = cap.get(cv2.CAP_PROP_FPS)
192
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
193
  duration = total_frames / fps
194
 
 
195
  key_moments.sort(key=lambda x: x['start'])
196
 
 
197
  if len(key_moments) > max_frames:
 
 
198
  first_count = min(5, max_frames // 4)
199
  last_count = min(5, max_frames // 4)
200
  middle_count = max_frames - first_count - last_count
 
204
  last_moments = key_moments[-last_count:]
205
  middle_moments = key_moments[first_count:-last_count]
206
 
 
207
  if len(middle_moments) > middle_count:
208
  step = len(middle_moments) / middle_count
209
  middle_sampled = [middle_moments[int(i * step)] for i in range(middle_count)]
 
212
 
213
  key_moments = first_moments + middle_sampled + last_moments
214
  else:
 
215
  step = len(key_moments) / max_frames
216
  key_moments = [key_moments[int(i * step)] for i in range(max_frames)]
217
 
 
219
  frame_count = 0
220
 
221
  for moment in key_moments:
 
222
  frame_time = (moment['start'] + moment['end']) / 2
223
 
 
224
  if frame_time > duration:
225
  continue
226
 
 
227
  frame_number = int(frame_time * fps)
228
 
 
229
  cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
230
  ret, frame = cap.read()
231
 
 
234
  frame_path = os.path.join(self.frames_dir, frame_filename)
235
  cv2.imwrite(frame_path, frame)
236
 
 
237
  frame_metadata[frame_filename] = {
238
  'time': frame_time,
239
  'dialogue': moment['text'],
 
245
 
246
  cap.release()
247
 
 
248
  with open(os.path.join('frames', 'frame_metadata.json'), 'w') as f:
249
  json.dump(frame_metadata, f, indent=2)
250
 
 
273
  print("❌ Subtitle file (test1.srt) not found!")
274
  return False
275
 
 
276
  try:
277
  from backend.full_story_extractor import FullStoryExtractor
278
  extractor = FullStoryExtractor()
 
288
  print(f"⚠️ Full story extraction failed, using all subtitles: {e}")
289
  filtered_subs = all_subs
290
 
 
291
  key_moments = [{'index': s.index, 'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in filtered_subs]
292
 
 
293
  with open(os.path.join(self.output_dir, 'key_moments.json'), 'w', encoding='utf-8') as f:
294
  json.dump(key_moments, f, indent=2)
295
 
 
296
  print("🎬 Extracting frames at key moments...")
297
  if not self.generate_keyframes_from_moments(self.video_path, key_moments, max_frames=48):
298
  print("❌ Keyframe extraction failed.")
299
  return False
300
 
301
  print("✂️ Cropping black bars...")
302
+ # --- FIX: Call the function from its imported module 'keyframes' ---
303
+ black_x, black_y, _, _ = keyframes.black_bar_crop()
304
  print("✅ Black bars cropped.")
305
 
306
  print("🎨 Enhancing images...")
 
356
  bubbles = []
357
  frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
358
 
 
359
  metadata_path = 'frames/frame_metadata.json'
360
  if not os.path.exists(metadata_path):
361
  print("⚠️ Frame metadata not found, using empty bubbles")
 
368
  frame_path = os.path.join(self.frames_dir, frame_file)
369
  dialogue = ""
370
 
 
371
  if frame_file in frame_metadata:
372
  dialogue = frame_metadata[frame_file]['dialogue']
373
 
 
437
  template_html = '''<!DOCTYPE html>
438
  <html lang="en">
439
  <head>
440
+ <meta charset="UTF-8">
441
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
442
+ <title>Generated Comic - Interactive Editor</title>
443
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
444
+ <style>
445
+ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-serif; }
446
+ .comic-container { max-width: 1200px; margin: 0 auto; }
447
+ .comic-page {
448
+ background: white; width: 600px; height: 400px;
449
+ box-shadow: 0 0 10px rgba(0,0,0,0.1); box-sizing: content-box;
450
+ position: relative; overflow: hidden; border: 1px solid #333;
451
+ padding: 10px;
452
+ }
453
+ .comic-grid {
454
+ display: grid;
455
+ grid-template-columns: 285px 285px;
456
+ grid-template-rows: 185px 185px;
457
+ gap: 10px;
458
+ width: 100%; height: 100%;
459
+ }
460
+ .page-wrapper { margin: 30px auto; width: 622px; display: flex; flex-direction: column; align-items: center; }
461
+ .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
462
+ .panel {
463
+ position: relative; overflow: hidden; width: 100%; height: 100%;
464
+ box-sizing: border-box; cursor: pointer; border: 1px solid #333;
465
+ }
466
+ .panel.selected { outline: 3px solid #2196F3; outline-offset: -3px; }
467
+ .panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
468
+ .speech-bubble {
469
+ position: absolute; display: flex; justify-content: center; align-items: center;
470
+ width: auto; height: auto;
471
+ min-width: 50px; max-width: 220px; min-height: 30px;
472
+ box-sizing: border-box; padding: 8px;
473
+ box-shadow: 2px 2px 5px rgba(0,0,0,0.3); z-index: 10;
474
+ cursor: move; overflow: visible; font-size: 13px; font-weight: bold; text-align: center;
475
+ }
476
+ .bubble-text { padding: 2px; word-wrap: break-word; }
477
+ .speech-bubble.selected { outline: 2px dashed #4CAF50; }
478
+ .speech-bubble textarea {
479
+ position: absolute; top: 0; left: 0; width: 100%; height: 100%; box-sizing: border-box;
480
+ border: 1px solid #4CAF50; background: rgba(255,255,255,0.95);
481
+ font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102;
482
+ }
483
+ /* --- Bubble Styles --- */
484
+ .speech-bubble.speech { background: white; border: 2px solid #333; color: #333; border-radius: 15px; }
485
+ .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
486
+ .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%); }
487
+ .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
488
+ .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%; }
489
+ /* --- Tail and Dot Styles (4-Direction Flip) --- */
490
+ .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; }
491
+ .speech-bubble.speech::after { border-top: 10px solid #333; bottom: -9px; left: 20px; }
492
+ .speech-bubble.idea::after { border-top: 10px solid #FFA500; bottom: -9px; left: 20px; }
493
+ .speech-bubble.thought::after { display: none; }
494
+ .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
495
+ .thought-dot-1 { width: 20px; height: 20px; bottom: -20px; left: 15px; }
496
+ .thought-dot-2 { width: 12px; height: 12px; bottom: -32px; left: 5px; }
497
+ /* Horizontal Flip */
498
+ .speech-bubble.flipped.speech::after, .speech-bubble.flipped.idea::after { left: auto; right: 20px; }
499
+ .speech-bubble.flipped.thought .thought-dot-1 { left: auto; right: 15px; }
500
+ .speech-bubble.flipped.thought .thought-dot-2 { left: auto; right: 5px; }
501
+ /* Vertical Flip */
502
+ .speech-bubble.flipped-vertical.speech::after, .speech-bubble.flipped-vertical.idea::after { bottom: auto; top: -9px; transform: rotate(180deg); }
503
+ .speech-bubble.flipped-vertical.thought .thought-dot-1 { bottom: auto; top: -20px; }
504
+ .speech-bubble.flipped-vertical.thought .thought-dot-2 { bottom: auto; top: -32px; }
505
+ .edit-controls {
506
+ position: fixed; bottom: 20px; right: 20px; background: rgba(44, 62, 80, 0.9);
507
+ color: white; padding: 10px 15px; border-radius: 8px; font-size: 13px;
508
+ z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px;
509
+ }
510
+ .edit-controls h4 { margin: 0 0 10px 0; color: #26a69a; text-align: center; }
511
+ .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; }
512
+ .edit-controls .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
513
+ .edit-controls .reset-button { background-color: #e74c3c; }
514
+ .edit-controls .action-button { background-color: #4CAF50; }
515
+ .edit-controls .secondary-button { background-color: #f39c12; }
516
+ </style>
517
  </head>
518
  <body>
519
+ <div class="comic-container">
520
+ <h1 class="comic-title">🎬 Generated Comic</h1>
521
+ <div id="comic-pages"><div class="loading">Loading comic...</div></div>
522
+ </div>
523
+ <input type="file" id="image-uploader" style="display: none;" accept="image/*">
524
+ <div class="edit-controls">
525
+ <h4>✏️ Interactive Editor</h4>
526
+ <div class="control-group">
527
+ <label for="bubble-type-select">Change Selected Bubble Type:</label>
528
+ <select id="bubble-type-select" onchange="changeBubbleType(this.value)">
529
+ <option value="speech">Speech</option>
530
+ <option value="thought">Thought</option>
531
+ <option value="reaction">Reaction</option>
532
+ <option value="narration">Narration</option>
533
+ <option value="idea">Idea</option>
534
+ </select>
535
+ <button onclick="rotateBubbleTail()" class="secondary-button">🔄 Rotate Tail</button>
536
  </div>
537
+ <div class="control-group">
538
+ <button onclick="replacePanelImage()" class="action-button">🖼️ Replace Panel Image</button>
539
+ <button onclick="regenerateFrame()" class="action-button">🔄 Regenerate Frame</button>
540
+ <button onclick="exportPagesToPNG()" class="action-button" style="background-color: #2196F3;">🖨️ Export Pages to PNG</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  </div>
542
+ <div class="control-group">
543
+ <button onclick="clearSavedState()" class="reset-button">🔄 Clear Edits & Reset</button>
544
+ </div>
545
+ </div>
546
+ <script>
547
+ document.addEventListener('DOMContentLoaded', () => {
548
+ fetch('/output/pages.json')
549
+ .then(res => res.ok ? res.json() : Promise.reject(new Error('Failed to load pages.json')))
550
+ .then(data => { renderComic(data); initializeEditor(); })
551
+ .catch(err => { document.getElementById('comic-pages').innerHTML = `<div class="loading">Error: ${err.message}</div>`; });
552
+ });
553
+
554
+ function renderComic(data) {
555
+ const container = document.getElementById('comic-pages');
556
+ container.innerHTML = '';
557
+ if (!data || data.length === 0) return;
558
+ data.forEach((pageData, pageIndex) => {
559
+ if (!pageData.panels || pageData.panels.length === 0) return;
560
+ const pageWrapper = document.createElement('div');
561
+ pageWrapper.className = 'page-wrapper';
562
+ const pageTitleEl = document.createElement('h2');
563
+ pageTitleEl.className = 'page-title';
564
+ pageTitleEl.textContent = `Page ${pageIndex + 1}`;
565
+ pageWrapper.appendChild(pageTitleEl);
566
+ const pageDiv = document.createElement('div');
567
+ pageDiv.className = 'comic-page';
568
+ const grid = document.createElement('div');
569
+ grid.className = 'comic-grid';
570
+ pageData.panels.forEach((panelData, panelIndex) => {
571
+ const panelDiv = document.createElement('div');
572
+ panelDiv.className = 'panel';
573
+ const img = document.createElement('img');
574
+ img.src = '/frames/final/' + panelData.image;
575
+ panelDiv.appendChild(img);
576
+ if (pageData.bubbles && pageData.bubbles[panelIndex]) {
577
+ const bubbleData = pageData.bubbles[panelIndex];
578
+ const bubbleDiv = createBubbleElement({
579
+ id: `initial-${pageIndex}-${panelIndex}`,
580
+ text: bubbleData.dialog || '',
581
+ left: `${bubbleData.bubble_offset_x ?? 50}px`,
582
+ top: `${bubbleData.bubble_offset_y ?? 20}px`,
583
+ });
584
+ panelDiv.appendChild(bubbleDiv);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
  }
586
+ grid.appendChild(panelDiv);
587
+ });
588
+ pageDiv.appendChild(grid);
589
+ pageWrapper.appendChild(pageDiv);
590
+ container.appendChild(pageWrapper);
591
+ });
592
+ }
593
+
594
+ let currentlyEditing = null, draggedBubble = null, offset = {x: 0, y: 0};
595
+ let currentlySelectedBubble = null;
596
+ let currentlySelectedPanel = null;
597
+
598
+ function initializeEditor() {
599
+ document.querySelectorAll('.panel').forEach(p => p.addEventListener('click', e => selectPanel(e.currentTarget)));
600
+ document.querySelectorAll('.speech-bubble').forEach(b => initializeBubbleEvents(b));
601
+ document.addEventListener('mousemove', e => { if (draggedBubble) drag(e); });
602
+ document.addEventListener('mouseup', () => { if (draggedBubble) stopDrag(); });
603
+ }
604
+
605
+ function initializeBubbleEvents(bubble) {
606
+ bubble.addEventListener('dblclick', e => { e.stopPropagation(); editBubbleText(bubble); });
607
+ bubble.addEventListener('mousedown', e => startDrag(e));
608
+ bubble.addEventListener('click', e => { e.stopPropagation(); selectBubble(bubble); });
609
+ bubble.addEventListener('wheel', e => {
610
+ e.preventDefault();
611
+ const currentWidth = parseFloat(bubble.style.width) || bubble.offsetWidth;
612
+ const newWidth = currentWidth - (e.deltaY > 0 ? 10 : -10);
613
+ if (newWidth >= 60) {
614
+ bubble.style.width = `${newWidth}px`;
615
+ bubble.style.height = 'auto';
616
  }
617
+ }, { passive: false });
618
+ }
619
+
620
+ function createBubbleElement(data) {
621
+ const bubbleDiv = document.createElement('div');
622
+ bubbleDiv.dataset.id = data.id;
623
+ const textSpan = document.createElement('span');
624
+ textSpan.className = 'bubble-text';
625
+ textSpan.textContent = data.text;
626
+ bubbleDiv.appendChild(textSpan);
627
+ bubbleDiv.style.left = data.left;
628
+ bubbleDiv.style.top = data.top;
629
+ applyBubbleType(bubbleDiv, 'speech'); // Default to speech
630
+ return bubbleDiv;
631
+ }
632
+
633
+ function applyBubbleType(bubble, type) {
634
+ bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
635
+ let classesToKeep = 'speech-bubble';
636
+ if (bubble.classList.contains('selected')) classesToKeep += ' selected';
637
+ if (bubble.classList.contains('flipped')) classesToKeep += ' flipped';
638
+ if (bubble.classList.contains('flipped-vertical')) classesToKeep += ' flipped-vertical';
639
+ bubble.className = classesToKeep;
640
+ bubble.classList.add(type);
641
+ bubble.dataset.type = type;
642
+ if (type === 'thought') {
643
+ for (let i = 1; i <= 2; i++) {
644
+ const dot = document.createElement('div');
645
+ dot.className = `thought-dot thought-dot-${i}`;
646
+ bubble.appendChild(dot);
647
  }
648
  }
649
+ }
650
+
651
+ function changeBubbleType(type) {
652
+ if (!currentlySelectedBubble) return;
653
+ applyBubbleType(currentlySelectedBubble, type);
654
+ }
655
+
656
+ function rotateBubbleTail() {
657
+ if (!currentlySelectedBubble) return alert("Please select a bubble to rotate.");
658
+ const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
659
+ const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
660
+ if (!isFlippedH && !isFlippedV) { // State 0 -> 1
661
+ currentlySelectedBubble.classList.add('flipped');
662
+ } else if (isFlippedH && !isFlippedV) { // State 1 -> 2
663
+ currentlySelectedBubble.classList.add('flipped-vertical');
664
+ } else if (isFlippedH && isFlippedV) { // State 2 -> 3
665
+ currentlySelectedBubble.classList.remove('flipped');
666
+ } else { // State 3 -> 0
667
+ currentlySelectedBubble.classList.remove('flipped-vertical');
668
  }
669
+ }
670
+
671
+ function selectPanel(panel) {
672
+ document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
673
+ panel.classList.add('selected');
674
+ currentlySelectedPanel = panel;
675
+ selectBubble(null);
676
+ }
677
+
678
+ function selectBubble(bubble) {
679
+ if (currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
680
+ currentlySelectedBubble = bubble;
681
+ if (currentlySelectedBubble) {
682
+ currentlySelectedBubble.classList.add('selected');
683
+ document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
684
+ document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
685
  }
686
+ }
687
+
688
+ function editBubbleText(bubble) {
689
+ if (currentlyEditing) return;
690
+ currentlyEditing = bubble;
691
+ const textSpan = bubble.querySelector('.bubble-text');
692
+ const currentText = textSpan.textContent;
693
+ textSpan.style.display = 'none';
694
+ bubble.style.height = 'auto';
695
+ const textarea = document.createElement('textarea');
696
+ textarea.value = currentText;
697
+ bubble.appendChild(textarea);
698
+ textarea.focus();
699
+ const finishEditing = () => {
700
+ textSpan.textContent = textarea.value;
701
+ bubble.removeChild(textarea);
702
+ textSpan.style.display = '';
703
+ currentlyEditing = null;
704
  bubble.style.height = 'auto';
705
+ };
706
+ textarea.addEventListener('blur', finishEditing, { once: true });
707
+ textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); }});
708
+ }
709
+
710
+ function startDrag(e) {
711
+ const bubble = e.target.closest('.speech-bubble');
712
+ if (!bubble || currentlyEditing) return;
713
+ draggedBubble = bubble;
714
+ selectBubble(bubble);
715
+ const rect = bubble.getBoundingClientRect();
716
+ offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
717
+ }
718
+
719
+ function drag(e) {
720
+ const parentRect = draggedBubble.parentElement.getBoundingClientRect();
721
+ let x = e.clientX - parentRect.left - offset.x;
722
+ let y = e.clientY - parentRect.top - offset.y;
723
+ draggedBubble.style.left = `${x}px`;
724
+ draggedBubble.style.top = `${y}px`;
725
+ }
726
+
727
+ function stopDrag() {
728
+ draggedBubble = null;
729
+ }
730
+
731
+ function clearSavedState() {
732
+ if (confirm("Reset all edits to the original AI-generated comic?")) {
733
+ localStorage.removeItem('comicEditorState');
734
+ window.location.reload();
 
 
 
 
735
  }
736
+ }
737
+
738
+ async function exportPagesToPNG() {
739
+ const pages = document.querySelectorAll('.comic-page');
740
+ if (pages.length === 0) return alert("No pages found.");
741
+ alert(`Starting export of ${pages.length} page(s).`);
742
+ for (let i = 0; i < pages.length; i++) {
743
+ try {
744
+ const canvas = await html2canvas(pages[i], { scale: 2 });
745
+ const link = document.createElement('a');
746
+ link.download = `comic-page-${i + 1}.png`;
747
+ link.href = canvas.toDataURL('image/png');
748
+ link.click();
749
+ } catch (err) {
750
+ alert(`Failed to export page ${i + 1}.`);
751
  }
752
  }
753
+ }
754
+
755
+ function replacePanelImage() {
756
+ if (!currentlySelectedPanel) {
757
+ alert("Please select a panel first.");
758
+ return;
 
 
 
 
 
 
 
 
 
 
759
  }
760
+ const img = currentlySelectedPanel.querySelector('img');
761
+ const uploader = document.getElementById('image-uploader');
762
+ const oneTimeListener = (event) => {
763
+ const file = event.target.files[0];
764
+ if (!file) return;
765
+ const formData = new FormData();
766
+ formData.append('image', file);
767
+ img.style.opacity = '0.5';
768
+ fetch('/replace_panel', { method: 'POST', body: formData })
769
+ .then(response => response.json())
770
+ .then(data => {
771
+ if (data.success) {
772
+ img.src = `/frames/final/${data.new_filename}?t=${new Date().getTime()}`;
773
+ } else {
774
+ alert('Error replacing image: ' + data.error);
775
+ }
776
+ img.style.opacity = '1';
777
+ })
778
+ .catch(error => {
779
+ alert('An error occurred during the upload.');
780
+ img.style.opacity = '1';
781
+ });
782
+ uploader.removeEventListener('change', oneTimeListener);
783
+ uploader.value = '';
784
+ };
785
+ uploader.addEventListener('change', oneTimeListener, { once: true });
786
+ uploader.click();
787
+ }
788
+
789
+ function regenerateFrame() {
790
+ if (!currentlySelectedPanel) {
791
+ alert("Please select a panel first.");
792
+ return;
793
  }
794
+ const img = currentlySelectedPanel.querySelector('img');
795
+ const currentSrc = img.src;
796
 
797
+ let filename = currentSrc.substring(currentSrc.lastIndexOf('/') + 1);
798
+ if (filename.includes('?')) {
799
+ filename = filename.split('?')[0];
800
+ }
 
 
 
 
 
 
 
 
801
 
802
+ if (!confirm(`Regenerate frame "${filename}" with a better version?`)) {
803
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804
  }
805
+ img.style.opacity = '0.5';
806
+ fetch('/regenerate_frame', {
807
+ method: 'POST',
808
+ headers: { 'Content-Type': 'application/json' },
809
+ body: JSON.stringify({ filename: filename })
810
+ })
811
+ .then(response => response.json())
812
+ .then(data => {
813
+ if (data.success) {
814
+ img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
815
+ alert(data.message);
816
+ } else {
817
+ alert('Error: ' + data.message);
818
+ }
819
+ img.style.opacity = '1';
820
+ })
821
+ .catch(error => {
822
+ alert('An error occurred during regeneration.');
823
+ img.style.opacity = '1';
824
+ });
825
+ }
826
+ </script>
827
  </body>
828
  </html>'''
829
  with open(os.path.join(self.output_dir, 'page.html'), 'w', encoding='utf-8') as f:
 
846
  return "❌ No file selected"
847
  f = request.files['file']
848
  if os.path.exists(comic_generator.video_path):
849
+ os.remove(comic_generator.video_path)
850
  f.save(comic_generator.video_path)
851
  success = comic_generator.generate_comic()
852
  if success:
853
+ webbrowser.open("http://localhost:5000/comic")
854
  return "🎉 Enhanced Comic Created Successfully!"
855
  else:
856
  return "❌ Comic generation failed"
 
869
  ydl.download([link])
870
  success = comic_generator.generate_comic()
871
  if success:
872
+ webbrowser.open("http://localhost:5000/comic")
873
  return "🎉 Enhanced Comic Created Successfully!"
874
  else:
875
  return "❌ Comic generation failed"
 
889
  save_path = os.path.join(comic_generator.frames_dir, filename)
890
  file.save(save_path)
891
 
 
 
 
 
 
892
  print(f"✅ Replaced panel with '{filename}' without applying color enhancement.")
893
 
894
  return jsonify({'success': True, 'new_filename': filename})
 
913
  def view_comic():
914
  return send_from_directory('output', 'page.html')
915
 
916
+ # --- FIX: Corrected Flask route syntax for dynamic paths ---
917
  @app.route('/output/<path:filename>')
918
  def output_file(filename):
919
  return send_from_directory('output', filename)
920
 
921
+ # --- FIX: Corrected Flask route syntax for dynamic paths ---
922
  @app.route('/frames/final/<path:filename>')
923
  def frame_file(filename):
924
  return send_from_directory('frames/final', filename)
925
 
926
+ # --- FIX: Use __name__ == '__main__' for the execution block ---
927
  if __name__ == '__main__':
928
  print("🚀 Starting Enhanced Comic Generator...")
929
+ print("🌐 Web interface available at: http://localhost:5000")
930
+ app.run(debug=True, host='0.0.0.0', port=5000)