Spaces:
tester343
/
Configuration error

jhh6576 commited on
Commit
a7ede2e
Β·
verified Β·
1 Parent(s): c2571ea

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +567 -46
app_enhanced.py CHANGED
@@ -1,4 +1,475 @@
1
- def _copy_template_files(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  """This function now includes the working 'Replace Image', 'Flip Bubble', and Panel Gaps features."""
3
  try:
4
  template_html = '''<!DOCTYPE html>
@@ -11,49 +482,22 @@ def _copy_template_files(self):
11
  <style>
12
  body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-serif; }
13
  .comic-container { max-width: 1200px; margin: 0 auto; }
14
- .comic-page {
15
- background: white; width: 600px; height: 400px;
16
- box-shadow: 0 0 10px rgba(0,0,0,0.1); box-sizing: content-box;
17
- position: relative; overflow: hidden; border: 1px solid #333;
18
- padding: 10px;
19
- }
20
- .comic-grid {
21
- display: grid;
22
- grid-template-columns: 285px 285px;
23
- grid-template-rows: 185px 185px;
24
- gap: 10px;
25
- width: 100%; height: 100%;
26
- }
27
  .page-wrapper { margin: 30px auto; width: 622px; display: flex; flex-direction: column; align-items: center; }
28
  .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
29
- .panel {
30
- position: relative; overflow: hidden; width: 100%; height: 100%;
31
- box-sizing: border-box; cursor: pointer; border: 1px solid #333;
32
- }
33
  .panel.selected { outline: 3px solid #2196F3; outline-offset: -3px; }
34
  .panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
35
- .speech-bubble {
36
- position: absolute; display: flex; justify-content: center; align-items: center;
37
- width: auto; height: auto;
38
- min-width: 50px; max-width: 220px; min-height: 30px;
39
- box-sizing: border-box; padding: 8px;
40
- box-shadow: 2px 2px 5px rgba(0,0,0,0.3); z-index: 10;
41
- cursor: move; overflow: visible; font-size: 13px; font-weight: bold; text-align: center;
42
- }
43
  .bubble-text { padding: 2px; word-wrap: break-word; }
44
  .speech-bubble.selected { outline: 2px dashed #4CAF50; }
45
- .speech-bubble textarea {
46
- position: absolute; top: 0; left: 0; width: 100%; height: 100%; box-sizing: border-box;
47
- border: 1px solid #4CAF50; background: rgba(255,255,255,0.95);
48
- font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102;
49
- }
50
- /* --- Bubble Styles --- */
51
  .speech-bubble.speech { background: white; border: 2px solid #333; color: #333; border-radius: 15px; }
52
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
53
  .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%); }
54
  .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
55
  .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%; }
56
- /* --- Tail and Dot Styles (4-Direction Flip) --- */
57
  .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; }
58
  .speech-bubble.speech::after { border-top: 10px solid #333; bottom: -9px; left: 20px; }
59
  .speech-bubble.idea::after { border-top: 10px solid #FFA500; bottom: -9px; left: 20px; }
@@ -61,19 +505,13 @@ font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102;
61
  .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
62
  .thought-dot-1 { width: 20px; height: 20px; bottom: -20px; left: 15px; }
63
  .thought-dot-2 { width: 12px; height: 12px; bottom: -32px; left: 5px; }
64
- /* Horizontal Flip */
65
  .speech-bubble.flipped.speech::after, .speech-bubble.flipped.idea::after { left: auto; right: 20px; }
66
  .speech-bubble.flipped.thought .thought-dot-1 { left: auto; right: 15px; }
67
  .speech-bubble.flipped.thought .thought-dot-2 { left: auto; right: 5px; }
68
- /* Vertical Flip */
69
  .speech-bubble.flipped-vertical.speech::after, .speech-bubble.flipped-vertical.idea::after { bottom: auto; top: -9px; transform: rotate(180deg); }
70
  .speech-bubble.flipped-vertical.thought .thought-dot-1 { bottom: auto; top: -20px; }
71
  .speech-bubble.flipped-vertical.thought .thought-dot-2 { bottom: auto; top: -32px; }
72
- .edit-controls {
73
- position: fixed; bottom: 20px; right: 20px; background: rgba(44, 62, 80, 0.9);
74
- color: white; padding: 10px 15px; border-radius: 8px; font-size: 13px;
75
- z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px;
76
- }
77
  .edit-controls h4 { margin: 0 0 10px 0; color: #26a69a; text-align: center; }
78
  .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; }
79
  .edit-controls .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
@@ -93,11 +531,7 @@ z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px;
93
  <div class="control-group">
94
  <label for="bubble-type-select">Change Selected Bubble Type:</label>
95
  <select id="bubble-type-select" onchange="changeBubbleType(this.value)">
96
- <option value="speech">Speech</option>
97
- <option value="thought">Thought</option>
98
- <option value="reaction">Reaction</option>
99
- <option value="narration">Narration</option>
100
- <option value="idea">Idea</option>
101
  </select>
102
  <button onclick="rotateBubbleTail()" class="secondary-button">πŸ”„ Rotate Tail</button>
103
  </div>
@@ -397,4 +831,91 @@ z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px;
397
  f.write(template_html)
398
  print("πŸ“„ Template files copied successfully!")
399
  except Exception as e:
400
- print(f"Template copy failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import webbrowser
3
+ import time
4
+ import threading
5
+ from flask import Flask, render_template, request, jsonify, send_from_directory, send_file
6
+ from pathlib import Path
7
+ import cv2
8
+ import numpy as np
9
+ from PIL import Image
10
+ import srt
11
+ import json
12
+ import shutil
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:
71
+ from backend.emotion_aware_comic import EmotionAwareComicGenerator
72
+ from backend.story_analyzer import SmartComicGenerator
73
+ SMART_COMIC_AVAILABLE = True
74
+ print("βœ… Smart comic generation available!")
75
+ except Exception as e:
76
+ SMART_COMIC_AVAILABLE = False
77
+ print(f"⚠️ Smart comic generation not available: {e}")
78
+
79
+ # Import panel extractor
80
+ try:
81
+ from backend.panel_extractor import PanelExtractor
82
+ PANEL_EXTRACTOR_AVAILABLE = True
83
+ print("βœ… Panel extractor available!")
84
+ except Exception as e:
85
+ PANEL_EXTRACTOR_AVAILABLE = False
86
+ print(f"⚠️ Panel extractor not available: {e}")
87
+
88
+ # Import smart story extractor
89
+ try:
90
+ from backend.smart_story_extractor import SmartStoryExtractor
91
+ STORY_EXTRACTOR_AVAILABLE = True
92
+ print("βœ… Smart story extractor available!")
93
+ except Exception as e:
94
+ STORY_EXTRACTOR_AVAILABLE = False
95
+ print(f"⚠️ Smart story extractor not available: {e}")
96
+
97
+ app = Flask(__name__)
98
+
99
+ # Import editor routes
100
+ try:
101
+ from comic_editor_server import add_editor_routes
102
+ add_editor_routes(app)
103
+ print("βœ… Comic editor integrated!")
104
+ except Exception as e:
105
+ print(f"⚠️ Could not load comic editor: {e}")
106
+
107
+ # Ensure directories exist
108
+ os.makedirs('video', exist_ok=True)
109
+ os.makedirs('frames/final', exist_ok=True)
110
+ os.makedirs('output', exist_ok=True)
111
+
112
+ class EnhancedComicGenerator:
113
+ """High-quality comic generation with AI enhancement"""
114
+ def __init__(self):
115
+ self.video_path = 'video/uploaded.mp4'
116
+ self.frames_dir = 'frames/final'
117
+ self.output_dir = 'output'
118
+ self.apply_comic_style = False
119
+
120
+ def cleanup_generated(self):
121
+ """Deletes all old files to ensure a fresh start."""
122
+ print("🧹 Performing full cleanup of previous run...")
123
+ if os.path.isdir(self.frames_dir): shutil.rmtree(self.frames_dir)
124
+ if os.path.isdir(self.output_dir): shutil.rmtree(self.output_dir)
125
+ if os.path.isdir('temp'): shutil.rmtree('temp')
126
+ if os.path.exists('test1.srt'): os.remove('test1.srt')
127
+ os.makedirs(self.frames_dir, exist_ok=True)
128
+ os.makedirs(self.output_dir, exist_ok=True)
129
+ print("βœ… Cleanup complete.")
130
+
131
+ def detect_eye_state(self, frame_path):
132
+ """
133
+ Detect if eyes are closed or semi-closed in a frame
134
+ Returns: 'open', 'semi-closed', or 'closed'
135
+ """
136
+ try:
137
+ img = cv2.imread(frame_path)
138
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
139
+ face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
140
+ eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')
141
+ faces = face_cascade.detectMultiScale(gray, 1.3, 5)
142
+ for (x, y, w, h) in faces:
143
+ roi_gray = gray[y:y+h, x:x+w]
144
+ eyes = eye_cascade.detectMultiScale(roi_gray)
145
+ if len(eyes) == 0:
146
+ return 'closed'
147
+ elif len(eyes) == 1:
148
+ return 'semi-closed'
149
+ for (ex, ey, ew, eh) in eyes:
150
+ eye_region = roi_gray[ey:ey+eh, ex:ex+ew]
151
+ vert_var = np.var(eye_region, axis=0).mean()
152
+ if vert_var < 500:
153
+ return 'semi-closed'
154
+ return 'open'
155
+ except:
156
+ return 'open'
157
+
158
+ def regenerate_frame(self, frame_filename):
159
+ """
160
+ Regenerate frame by moving +0.1s forward in the original video.
161
+ Updates metadata so repeated clicks keep advancing.
162
+ """
163
+ try:
164
+ metadata_path = 'frames/frame_metadata.json'
165
+ if not os.path.exists(metadata_path):
166
+ return {"success": False, "message": "Frame metadata missing."}
167
+
168
+ with open(metadata_path, 'r') as f:
169
+ frame_to_time = json.load(f)
170
+
171
+ if frame_filename not in frame_to_time:
172
+ return {"success": False, "message": "Panel not linked to original video."}
173
+
174
+ if isinstance(frame_to_time[frame_filename], dict):
175
+ current_time = frame_to_time[frame_filename]['time']
176
+ else:
177
+ current_time = frame_to_time[frame_filename]
178
+
179
+ target_time = current_time + 0.1
180
+
181
+ cap = cv2.VideoCapture(self.video_path)
182
+ if not cap.isOpened():
183
+ return {"success": False, "message": "Cannot open video."}
184
+
185
+ cap.set(cv2.CAP_PROP_POS_MSEC, target_time * 1000)
186
+ ret, frame = cap.read()
187
+ cap.release()
188
+
189
+ if not ret or frame is None:
190
+ return {"success": False, "message": "No next frame available at +0.1s."}
191
+
192
+ new_path = os.path.join(self.frames_dir, frame_filename)
193
+ cv2.imwrite(new_path, frame)
194
+
195
+ if isinstance(frame_to_time[frame_filename], dict):
196
+ frame_to_time[frame_filename]['time'] = target_time
197
+ else:
198
+ frame_to_time[frame_filename] = target_time
199
+
200
+ with open(metadata_path, 'w') as f:
201
+ json.dump(frame_to_time, f, indent=2)
202
+
203
+ print(f"βœ… Regenerated {frame_filename} to time {target_time:.2f}s without enhancement.")
204
+
205
+ return {
206
+ "success": True,
207
+ "message": f"Advanced to {target_time:.2f}s (+0.1s)",
208
+ "new_filename": frame_filename
209
+ }
210
+
211
+ except Exception as e:
212
+ traceback.print_exc()
213
+ return {"success": False, "message": str(e)}
214
+
215
+ def generate_keyframes_from_moments(self, video_path, key_moments, max_frames=48):
216
+ """
217
+ Generate frames specifically at the key moments timestamps
218
+ """
219
+ try:
220
+ cap = cv2.VideoCapture(video_path)
221
+ if not cap.isOpened():
222
+ print("❌ Cannot open video for keyframe extraction")
223
+ return False
224
+
225
+ fps = cap.get(cv2.CAP_PROP_FPS)
226
+ if fps == 0:
227
+ print("⚠️ Video FPS is 0, defaulting to 25. Keyframe extraction might be inaccurate.")
228
+ fps = 25
229
+
230
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
231
+ duration = total_frames / fps
232
+
233
+ key_moments.sort(key=lambda x: x['start'])
234
+
235
+ if len(key_moments) > max_frames:
236
+ first_count = min(5, max_frames // 4)
237
+ last_count = min(5, max_frames // 4)
238
+ middle_count = max_frames - first_count - last_count
239
+
240
+ if middle_count > 0 and len(key_moments) > (first_count + last_count):
241
+ first_moments = key_moments[:first_count]
242
+ last_moments = key_moments[-last_count:]
243
+ middle_moments = key_moments[first_count:-last_count]
244
+
245
+ if len(middle_moments) > middle_count:
246
+ step = len(middle_moments) / middle_count
247
+ middle_sampled = [middle_moments[int(i * step)] for i in range(middle_count)]
248
+ else:
249
+ middle_sampled = middle_moments
250
+
251
+ key_moments = first_moments + middle_sampled + last_moments
252
+ else:
253
+ step = len(key_moments) / max_frames
254
+ key_moments = [key_moments[int(i * step)] for i in range(max_frames)]
255
+
256
+ frame_metadata = {}
257
+ frame_count = 0
258
+
259
+ for moment in key_moments:
260
+ frame_time = (moment['start'] + moment['end']) / 2
261
+
262
+ if frame_time > duration:
263
+ continue
264
+
265
+ frame_number = int(frame_time * fps)
266
+
267
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
268
+ ret, frame = cap.read()
269
+
270
+ if ret:
271
+ frame_filename = f"frame_{frame_count:04d}.png"
272
+ frame_path = os.path.join(self.frames_dir, frame_filename)
273
+ cv2.imwrite(frame_path, frame)
274
+
275
+ frame_metadata[frame_filename] = {
276
+ 'time': frame_time,
277
+ 'dialogue': moment['text'],
278
+ 'start': moment['start'],
279
+ 'end': moment['end']
280
+ }
281
+ frame_count += 1
282
+ print(f"πŸ“Έ Extracted frame at {frame_time:.2f}s: {moment['text'][:30]}...")
283
+
284
+ cap.release()
285
+
286
+ with open(os.path.join('frames', 'frame_metadata.json'), 'w') as f:
287
+ json.dump(frame_metadata, f, indent=2)
288
+
289
+ print(f"βœ… Extracted {frame_count} keyframes from video")
290
+ return True
291
+
292
+ except Exception as e:
293
+ print(f"❌ Error extracting keyframes: {e}")
294
+ traceback.print_exc()
295
+ return False
296
+
297
+ def generate_comic(self, smart_mode=False, emotion_match=False):
298
+ """Main comic generation pipeline"""
299
+ start_time = time.time()
300
+ self.cleanup_generated()
301
+ print("🎬 Starting Enhanced Comic Generation...")
302
+ try:
303
+ print("πŸ“ Generating subtitles...")
304
+ get_real_subtitles(self.video_path)
305
+ all_subs = []
306
+ if os.path.exists('test1.srt'):
307
+ with open('test1.srt', 'r', encoding='utf-8') as f:
308
+ all_subs = list(srt.parse(f.read()))
309
+ print(f"βœ… Loaded {len(all_subs)} subtitles")
310
+ else:
311
+ print("❌ Subtitle file (test1.srt) not found!")
312
+ return False
313
+
314
+ try:
315
+ from backend.full_story_extractor import FullStoryExtractor
316
+ extractor = FullStoryExtractor()
317
+ sub_list = [{'index': s.index, 'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in all_subs]
318
+ os.makedirs('temp', exist_ok=True)
319
+ with open('temp/all_subs.json', 'w') as f: json.dump(sub_list, f)
320
+
321
+ story_subs = extractor.extract_full_story('temp/all_subs.json')
322
+ story_indices = {s.get('index') for s in story_subs}
323
+ filtered_subs = [sub for sub in all_subs if sub.index in story_indices]
324
+ print(f"πŸ“š Full story: {len(filtered_subs)} key moments from {len(all_subs)} total")
325
+ except Exception as e:
326
+ print(f"⚠️ Full story extraction failed, using all subtitles: {e}")
327
+ filtered_subs = all_subs
328
+
329
+ key_moments = [{'index': s.index, 'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in filtered_subs]
330
+
331
+ with open(os.path.join(self.output_dir, 'key_moments.json'), 'w', encoding='utf-8') as f:
332
+ json.dump(key_moments, f, indent=2)
333
+
334
+ print("🎬 Extracting frames at key moments...")
335
+ if not self.generate_keyframes_from_moments(self.video_path, key_moments, max_frames=48):
336
+ print("❌ Keyframe extraction failed.")
337
+ return False
338
+
339
+ print("βœ‚οΈ Cropping black bars...")
340
+ black_x, black_y, _, _ = black_bar_crop()
341
+ print("βœ… Black bars cropped.")
342
+
343
+ print("🎨 Enhancing images...")
344
+ self._enhance_all_images()
345
+ self._enhance_quality_colors()
346
+ print("βœ… Images enhancement step complete.")
347
+
348
+ print("πŸ’¬ Creating AI bubbles with key moment dialogues...")
349
+ bubbles = self._create_ai_bubbles_from_moments(black_x, black_y)
350
+ print(f"βœ… Created {len(bubbles)} bubbles.")
351
+
352
+ print("πŸ“‹ Generating pages...")
353
+ pages = self._generate_pages(bubbles)
354
+ print(f"βœ… Generated {len(pages)} pages.")
355
+
356
+ print("πŸ’Ύ Saving results...")
357
+ self._save_results(pages)
358
+ print("βœ… Results saved.")
359
+
360
+ execution_time = (time.time() - start_time) / 60
361
+ print(f"βœ… Comic generation completed in {execution_time:.2f} minutes")
362
+ return True
363
+ except Exception as e:
364
+ print(f"❌ Comic generation failed: {e}")
365
+ traceback.print_exc()
366
+ return False
367
+
368
+ def _enhance_all_images(self, single_image_path=None):
369
+ """Enhances colors for a batch of images."""
370
+ target_dir = self.frames_dir
371
+ if single_image_path:
372
+ target_dir = os.path.dirname(single_image_path)
373
+ if not os.path.exists(target_dir): return
374
+ try:
375
+ enhancer = SimpleColorEnhancer()
376
+ enhancer.enhance_batch(target_dir)
377
+ except Exception as e:
378
+ print(f"❌ Simple enhancement failed during execution: {e}")
379
+
380
+ def _enhance_quality_colors(self, single_image_path=None):
381
+ """Enhances quality and colors for a batch of images."""
382
+ target_dir = self.frames_dir
383
+ if single_image_path:
384
+ target_dir = os.path.dirname(single_image_path)
385
+ try:
386
+ enhancer = QualityColorEnhancer()
387
+ enhancer.batch_enhance(target_dir)
388
+ except Exception as e:
389
+ print(f"⚠️ Quality enhancement failed during execution: {e}")
390
+
391
+ def _create_ai_bubbles_from_moments(self, black_x, black_y):
392
+ """Create bubbles using the key moments dialogues"""
393
+ bubbles = []
394
+ frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
395
+
396
+ metadata_path = 'frames/frame_metadata.json'
397
+ if not os.path.exists(metadata_path):
398
+ print("⚠️ Frame metadata not found, creating empty bubbles.")
399
+ return [bubble(dialog="") for _ in frame_files]
400
+
401
+ with open(metadata_path, 'r') as f:
402
+ frame_metadata = json.load(f)
403
+
404
+ for frame_file in frame_files:
405
+ frame_path = os.path.join(self.frames_dir, frame_file)
406
+ dialogue = ""
407
+
408
+ if frame_file in frame_metadata:
409
+ dialogue = frame_metadata[frame_file]['dialogue']
410
+
411
+ try:
412
+ lip_x, lip_y = -1, -1
413
+ faces = face_detector.detect_faces(frame_path)
414
+ if faces:
415
+ lip_x, lip_y = face_detector.get_lip_position(frame_path, faces[0])
416
+
417
+ bubble_x, bubble_y = ai_bubble_placer.place_bubble_ai(frame_path, (lip_x, lip_y))
418
+ bubbles.append(bubble(
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'
427
+ ))
428
+ return bubbles
429
+
430
+ def _generate_pages(self, bubbles):
431
+ try:
432
+ from backend.fixed_12_pages_800x1080 import generate_12_pages_800x1080
433
+ frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
434
+ return generate_12_pages_800x1080(frame_files, bubbles)
435
+ except ImportError:
436
+ pages = []
437
+ frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
438
+ frames_per_page = 4
439
+ num_pages = (len(frame_files) + frames_per_page - 1) // frames_per_page
440
+ frame_counter = 0
441
+ for i in range(num_pages):
442
+ page_panels, page_bubbles = [], []
443
+ for _ in range(frames_per_page):
444
+ if frame_counter < len(frame_files):
445
+ page_panels.append(panel(
446
+ image=frame_files[frame_counter], row_span=6, col_span=6
447
+ ))
448
+ page_bubbles.append(bubbles[frame_counter] if frame_counter < len(bubbles) else bubble(dialog=""))
449
+ frame_counter += 1
450
+ if page_panels:
451
+ pages.append(Page(panels=page_panels, bubbles=page_bubbles))
452
+ return pages
453
+
454
+ def _save_results(self, pages):
455
+ try:
456
+ os.makedirs(self.output_dir, exist_ok=True)
457
+ pages_data = []
458
+ for page in pages:
459
+ page_dict = {
460
+ 'panels': [p if isinstance(p, dict) else p.__dict__ for p in page.panels],
461
+ 'bubbles': [b if isinstance(b, dict) else b.__dict__ for b in page.bubbles]
462
+ }
463
+ pages_data.append(page_dict)
464
+ with open(os.path.join(self.output_dir, 'pages.json'), 'w', encoding='utf-8') as f:
465
+ json.dump(pages_data, f, indent=2)
466
+ self._copy_template_files()
467
+ print("βœ… Results saved successfully!")
468
+ except Exception as e:
469
+ print(f"Save results failed: {e}")
470
+ traceback.print_exc()
471
+
472
+ def _copy_template_files(self):
473
  """This function now includes the working 'Replace Image', 'Flip Bubble', and Panel Gaps features."""
474
  try:
475
  template_html = '''<!DOCTYPE html>
 
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>
 
831
  f.write(template_html)
832
  print("πŸ“„ Template files copied successfully!")
833
  except Exception as e:
834
+ print(f"Template copy failed: {e}")
835
+
836
+ # --- Flask Routes ---
837
+ comic_generator = EnhancedComicGenerator()
838
+
839
+ @app.route('/')
840
+ def index():
841
+ return render_template('index.html')
842
+
843
+ @app.route('/uploader', methods=['POST'])
844
+ def upload_file():
845
+ try:
846
+ if 'file' not in request.files or request.files['file'].filename == '':
847
+ return "❌ No file selected"
848
+ f = request.files['file']
849
+ if os.path.exists(comic_generator.video_path):
850
+ os.remove(comic_generator.video_path)
851
+ f.save(comic_generator.video_path)
852
+ success = comic_generator.generate_comic()
853
+ if success:
854
+ return "πŸŽ‰ Enhanced Comic Created Successfully! View it at the /comic endpoint."
855
+ else:
856
+ return "❌ Comic generation failed. Check the Space logs for details."
857
+ except Exception as e:
858
+ traceback.print_exc()
859
+ return f"❌ An unexpected error occurred: {str(e)}"
860
+
861
+ @app.route('/handle_link', methods=['POST'])
862
+ def handle_link():
863
+ try:
864
+ link = request.form.get('link', '')
865
+ if not link: return "❌ No link provided"
866
+ import yt_dlp
867
+ ydl_opts = {'outtmpl': comic_generator.video_path, 'format': 'best[height<=720]'}
868
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
869
+ ydl.download([link])
870
+ success = comic_generator.generate_comic()
871
+ if success:
872
+ return "πŸŽ‰ Enhanced Comic Created Successfully! View it at the /comic endpoint."
873
+ else:
874
+ return "❌ Comic generation failed. Check the Space logs for details."
875
+ except Exception as e:
876
+ traceback.print_exc()
877
+ return f"❌ An unexpected error occurred: {str(e)}"
878
+
879
+ @app.route('/replace_panel', methods=['POST'])
880
+ def replace_panel():
881
+ try:
882
+ if 'image' not in request.files: return jsonify({'success': False, 'error': 'No image file provided.'})
883
+ file = request.files['image']
884
+ timestamp = int(time.time() * 1000)
885
+ filename = f"replaced_panel_{timestamp}.png"
886
+ save_path = os.path.join(comic_generator.frames_dir, filename)
887
+ file.save(save_path)
888
+ print(f"βœ… Replaced panel with '{filename}' without applying color enhancement.")
889
+ return jsonify({'success': True, 'new_filename': filename})
890
+ except Exception as e:
891
+ traceback.print_exc()
892
+ return jsonify({'success': False, 'error': str(e)})
893
+
894
+ @app.route('/regenerate_frame', methods=['POST'])
895
+ def regenerate_frame_route():
896
+ try:
897
+ data = request.get_json()
898
+ filename = data.get('filename')
899
+ if not filename: return jsonify({'success': False, 'message': 'No filename provided'})
900
+ result = comic_generator.regenerate_frame(filename)
901
+ return jsonify(result)
902
+ except Exception as e:
903
+ traceback.print_exc()
904
+ return jsonify({'success': False, 'message': str(e)})
905
+
906
+ @app.route('/comic')
907
+ def view_comic():
908
+ return send_from_directory('output', 'page.html')
909
+
910
+ @app.route('/output/<path:filename>')
911
+ def output_file(filename):
912
+ return send_from_directory('output', filename)
913
+
914
+ @app.route('/frames/final/<path:filename>')
915
+ def frame_file(filename):
916
+ return send_from_directory('frames/final', filename)
917
+
918
+ if __name__ == '__main__':
919
+ port = int(os.getenv("PORT", 7860))
920
+ print(f"πŸš€ Starting Enhanced Comic Generator on host 0.0.0.0, port {port}")
921
+ app.run(debug=False, host='0.0.0.0', port=port)