jhh6576 commited on
Commit
3ef5e79
Β·
verified Β·
1 Parent(s): 50808f8

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +787 -40
app_enhanced.py CHANGED
@@ -63,12 +63,9 @@ try:
63
  except Exception as e:
64
  print(f"⚠️ Could not load a core utility module: {e}")
65
 
66
- # (Other optional imports...)
67
-
68
  app = Flask(__name__)
69
 
70
- # (Other optional imports...)
71
-
72
  os.makedirs('video', exist_ok=True)
73
  os.makedirs('frames/final', exist_ok=True)
74
  os.makedirs('output', exist_ok=True)
@@ -83,16 +80,111 @@ class EnhancedComicGenerator:
83
  self.video_fps = None
84
 
85
  def cleanup_generated(self):
86
- # (This function is complete and unchanged)
87
- pass
 
 
 
 
 
 
 
88
 
89
  def detect_eye_state(self, frame_path):
90
- # (This function is complete and unchanged)
91
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  def regenerate_frame(self, frame_filename, direction):
94
- # (This function is complete and unchanged)
95
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  def get_frame_at_timestamp(self, frame_filename, timestamp_seconds):
98
  """
@@ -109,6 +201,7 @@ class EnhancedComicGenerator:
109
 
110
  # Check if timestamp is valid
111
  fps = cap.get(cv2.CAP_PROP_FPS)
 
112
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
113
  duration = total_frames / fps
114
  if timestamp_seconds < 0 or timestamp_seconds > duration:
@@ -122,11 +215,9 @@ class EnhancedComicGenerator:
122
  if not ret or frame is None:
123
  return {"success": False, "message": f"Could not retrieve frame at {timestamp_seconds:.2f}s."}
124
 
125
- # Overwrite the existing frame file
126
  new_path = os.path.join(self.frames_dir, frame_filename)
127
  cv2.imwrite(new_path, frame)
128
 
129
- # Update metadata with the new exact time
130
  with open(metadata_path, 'r') as f:
131
  frame_to_time = json.load(f)
132
 
@@ -149,32 +240,253 @@ class EnhancedComicGenerator:
149
  return {"success": False, "message": str(e)}
150
 
151
  def generate_keyframes_from_moments(self, video_path, key_moments, max_frames=48):
152
- # (This function is complete and unchanged)
153
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
  def generate_comic(self, smart_mode=False, emotion_match=False):
156
- # (This function is complete and unchanged)
157
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  def _enhance_all_images(self, single_image_path=None):
160
- # (This function is complete and unchanged)
161
- pass
 
 
 
 
 
 
 
162
 
163
  def _enhance_quality_colors(self, single_image_path=None):
164
- # (This function is complete and unchanged)
165
- pass
 
 
 
 
 
 
166
 
167
  def _create_ai_bubbles_from_moments(self, black_x, black_y):
168
- # (This function is complete and unchanged)
169
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
  def _generate_pages(self, bubbles):
172
- # (This function is complete and unchanged)
173
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
  def _save_results(self, pages):
176
- # (This function is complete and unchanged)
177
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  def _copy_template_files(self):
180
  """This function contains the complete HTML, CSS, and JavaScript for the interactive editor."""
@@ -222,7 +534,7 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
222
  .speech-bubble.flipped-vertical.thought .thought-dot-2 { bottom: auto; top: -32px; }
223
  .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; }
224
  .edit-controls h4 { margin: 0 0 10px 0; color: #26a69a; text-align: center; }
225
- .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; }
226
  .edit-controls .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
227
  .edit-controls .reset-button { background-color: #e74c3c; }
228
  .edit-controls .action-button { background-color: #4CAF50; }
@@ -230,7 +542,7 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
230
  .button-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
231
  .zoom-controls { display: grid; grid-template-columns: auto 1fr; gap: 5px; align-items: center;}
232
  .timestamp-controls { display: grid; grid-template-columns: 1fr auto; gap: 5px; }
233
- .timestamp-controls input { width: 100%; box-sizing: border-box; padding: 5px; border-radius: 3px; border: 1px solid #ccc; }
234
  </style>
235
  </head>
236
  <body>
@@ -253,7 +565,7 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
253
  <label>Panel Tools (Select Panel):</label>
254
  <button onclick="replacePanelImage()" class="action-button">πŸ–ΌοΈ Replace Image</button>
255
  <div class="button-grid">
256
- <button onclick="adjustFrame('backward')" class="secondary-button">⬅️ Previous Frame</button>
257
  <button onclick="adjustFrame('forward')" class="action-button">Next Frame ➑️</button>
258
  </div>
259
  <div class="timestamp-controls">
@@ -281,9 +593,367 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
281
  .catch(err => { document.getElementById('comic-pages').innerHTML = `<div class="loading">Error: ${err.message}</div>`; });
282
  });
283
 
284
- // All other functions are complete and correct...
285
- // ... renderComic, initializeEditor, etc. ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
 
 
 
 
 
 
287
  function gotoTimestamp() {
288
  if (!currentlySelectedPanel) {
289
  alert("Please select a panel first to jump to a timestamp.");
@@ -299,11 +969,13 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
299
  let parsedSeconds = 0;
300
  if (timeStr.includes(':')) {
301
  const parts = timeStr.split(':');
302
- const minutes = parseInt(parts[0], 10);
303
- const seconds = parseFloat(parts[1]);
304
- if (!isNaN(minutes) && !isNaN(seconds)) {
305
- parsedSeconds = (minutes * 60) + seconds;
306
- } else {
 
 
307
  alert("Invalid time format. Please use 'mm:ss'.");
308
  return;
309
  }
@@ -329,8 +1001,8 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
329
  .then(data => {
330
  if (data.success) {
331
  img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
332
- input.value = ''; // Clear input on success
333
- resetPanelTransform(); // Reset zoom/pan for the new frame
334
  } else {
335
  alert('Error: ' + data.message);
336
  }
@@ -357,8 +1029,71 @@ comic_generator = EnhancedComicGenerator()
357
  def index():
358
  return render_template('index.html')
359
 
360
- # (Other routes are complete and unchanged)
361
- # ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
  @app.route('/goto_timestamp', methods=['POST'])
364
  def goto_timestamp_route():
@@ -375,6 +1110,18 @@ def goto_timestamp_route():
375
  traceback.print_exc()
376
  return jsonify({'success': False, 'message': str(e)})
377
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  if __name__ == '__main__':
379
  port = int(os.getenv("PORT", 7860))
380
  print(f"πŸš€ Starting Enhanced Comic Generator on host 0.0.0.0, port {port}")
 
63
  except Exception as e:
64
  print(f"⚠️ Could not load a core utility module: {e}")
65
 
66
+ # (Other optional imports for smart features)
 
67
  app = Flask(__name__)
68
 
 
 
69
  os.makedirs('video', exist_ok=True)
70
  os.makedirs('frames/final', exist_ok=True)
71
  os.makedirs('output', exist_ok=True)
 
80
  self.video_fps = None
81
 
82
  def cleanup_generated(self):
83
+ """Deletes all old files to ensure a fresh start."""
84
+ print("🧹 Performing full cleanup of previous run...")
85
+ if os.path.isdir(self.frames_dir): shutil.rmtree(self.frames_dir)
86
+ if os.path.isdir(self.output_dir): shutil.rmtree(self.output_dir)
87
+ if os.path.isdir('temp'): shutil.rmtree('temp')
88
+ if os.path.exists('test1.srt'): os.remove('test1.srt')
89
+ os.makedirs(self.frames_dir, exist_ok=True)
90
+ os.makedirs(self.output_dir, exist_ok=True)
91
+ print("βœ… Cleanup complete.")
92
 
93
  def detect_eye_state(self, frame_path):
94
+ """
95
+ Detect if eyes are closed or semi-closed in a frame
96
+ Returns: 'open', 'semi-closed', or 'closed'
97
+ """
98
+ try:
99
+ img = cv2.imread(frame_path)
100
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
101
+ face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
102
+ eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')
103
+ faces = face_cascade.detectMultiScale(gray, 1.3, 5)
104
+ for (x, y, w, h) in faces:
105
+ roi_gray = gray[y:y+h, x:x+w]
106
+ eyes = eye_cascade.detectMultiScale(roi_gray)
107
+ if len(eyes) == 0:
108
+ return 'closed'
109
+ elif len(eyes) == 1:
110
+ return 'semi-closed'
111
+ for (ex, ey, ew, eh) in eyes:
112
+ eye_region = roi_gray[ey:ey+eh, ex:ex+ew]
113
+ vert_var = np.var(eye_region, axis=0).mean()
114
+ if vert_var < 500:
115
+ return 'semi-closed'
116
+ return 'open'
117
+ except:
118
+ return 'open'
119
 
120
  def regenerate_frame(self, frame_filename, direction):
121
+ """
122
+ Regenerate a frame by moving one frame forward or backward in the video.
123
+ """
124
+ try:
125
+ if not self.video_fps:
126
+ return {"success": False, "message": "Video FPS not found. Please regenerate the comic first."}
127
+
128
+ metadata_path = 'frames/frame_metadata.json'
129
+ if not os.path.exists(metadata_path):
130
+ return {"success": False, "message": "Frame metadata missing."}
131
+
132
+ with open(metadata_path, 'r') as f:
133
+ frame_to_time = json.load(f)
134
+
135
+ if frame_filename not in frame_to_time:
136
+ return {"success": False, "message": "Panel not linked to original video."}
137
+
138
+ if isinstance(frame_to_time[frame_filename], dict):
139
+ current_time = frame_to_time[frame_filename]['time']
140
+ else:
141
+ current_time = frame_to_time[frame_filename]
142
+
143
+ frame_duration = 1.0 / self.video_fps
144
+
145
+ if direction == 'forward':
146
+ target_time = current_time + frame_duration
147
+ elif direction == 'backward':
148
+ target_time = current_time - frame_duration
149
+ else:
150
+ return {"success": False, "message": "Invalid direction specified."}
151
+
152
+ target_time = max(0, target_time)
153
+
154
+ cap = cv2.VideoCapture(self.video_path)
155
+ if not cap.isOpened():
156
+ return {"success": False, "message": "Cannot open video."}
157
+
158
+ cap.set(cv2.CAP_PROP_POS_MSEC, target_time * 1000)
159
+ ret, frame = cap.read()
160
+ cap.release()
161
+
162
+ if not ret or frame is None:
163
+ return {"success": False, "message": f"No frame available at {target_time:.2f}s."}
164
+
165
+ new_path = os.path.join(self.frames_dir, frame_filename)
166
+ cv2.imwrite(new_path, frame)
167
+
168
+ if isinstance(frame_to_time[frame_filename], dict):
169
+ frame_to_time[frame_filename]['time'] = target_time
170
+ else:
171
+ frame_to_time[frame_filename] = target_time
172
+
173
+ with open(metadata_path, 'w') as f:
174
+ json.dump(frame_to_time, f, indent=2)
175
+
176
+ message = f"Adjusted {direction} to {target_time:.3f}s"
177
+ print(f"βœ… {message}")
178
+
179
+ return {
180
+ "success": True,
181
+ "message": message,
182
+ "new_filename": frame_filename
183
+ }
184
+
185
+ except Exception as e:
186
+ traceback.print_exc()
187
+ return {"success": False, "message": str(e)}
188
 
189
  def get_frame_at_timestamp(self, frame_filename, timestamp_seconds):
190
  """
 
201
 
202
  # Check if timestamp is valid
203
  fps = cap.get(cv2.CAP_PROP_FPS)
204
+ if fps == 0: fps = 25
205
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
206
  duration = total_frames / fps
207
  if timestamp_seconds < 0 or timestamp_seconds > duration:
 
215
  if not ret or frame is None:
216
  return {"success": False, "message": f"Could not retrieve frame at {timestamp_seconds:.2f}s."}
217
 
 
218
  new_path = os.path.join(self.frames_dir, frame_filename)
219
  cv2.imwrite(new_path, frame)
220
 
 
221
  with open(metadata_path, 'r') as f:
222
  frame_to_time = json.load(f)
223
 
 
240
  return {"success": False, "message": str(e)}
241
 
242
  def generate_keyframes_from_moments(self, video_path, key_moments, max_frames=48):
243
+ """
244
+ Generate frames specifically at the key moments timestamps
245
+ """
246
+ try:
247
+ cap = cv2.VideoCapture(video_path)
248
+ if not cap.isOpened():
249
+ print("❌ Cannot open video for keyframe extraction")
250
+ return False
251
+
252
+ fps = self.video_fps
253
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
254
+ duration = total_frames / fps
255
+
256
+ key_moments.sort(key=lambda x: x['start'])
257
+
258
+ if len(key_moments) > max_frames:
259
+ first_count = min(5, max_frames // 4)
260
+ last_count = min(5, max_frames // 4)
261
+ middle_count = max_frames - first_count - last_count
262
+
263
+ if middle_count > 0 and len(key_moments) > (first_count + last_count):
264
+ first_moments = key_moments[:first_count]
265
+ last_moments = key_moments[-last_count:]
266
+ middle_moments = key_moments[first_count:-last_count]
267
+
268
+ if len(middle_moments) > middle_count:
269
+ step = len(middle_moments) / middle_count
270
+ middle_sampled = [middle_moments[int(i * step)] for i in range(middle_count)]
271
+ else:
272
+ middle_sampled = middle_moments
273
+
274
+ key_moments = first_moments + middle_sampled + last_moments
275
+ else:
276
+ step = len(key_moments) / max_frames
277
+ key_moments = [key_moments[int(i * step)] for i in range(max_frames)]
278
+
279
+ frame_metadata = {}
280
+ frame_count = 0
281
+
282
+ for moment in key_moments:
283
+ frame_time = (moment['start'] + moment['end']) / 2
284
+
285
+ if frame_time > duration:
286
+ continue
287
+
288
+ frame_number = int(frame_time * fps)
289
+
290
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
291
+ ret, frame = cap.read()
292
+
293
+ if ret:
294
+ frame_filename = f"frame_{frame_count:04d}.png"
295
+ frame_path = os.path.join(self.frames_dir, frame_filename)
296
+ cv2.imwrite(frame_path, frame)
297
+
298
+ frame_metadata[frame_filename] = {
299
+ 'time': frame_time,
300
+ 'dialogue': moment['text'],
301
+ 'start': moment['start'],
302
+ 'end': moment['end']
303
+ }
304
+ frame_count += 1
305
+ print(f"πŸ“Έ Extracted frame at {frame_time:.2f}s: {moment['text'][:30]}...")
306
+
307
+ cap.release()
308
+
309
+ with open(os.path.join('frames', 'frame_metadata.json'), 'w') as f:
310
+ json.dump(frame_metadata, f, indent=2)
311
+
312
+ print(f"βœ… Extracted {frame_count} keyframes from video")
313
+ return True
314
+
315
+ except Exception as e:
316
+ print(f"❌ Error extracting keyframes: {e}")
317
+ traceback.print_exc()
318
+ return False
319
 
320
  def generate_comic(self, smart_mode=False, emotion_match=False):
321
+ """Main comic generation pipeline"""
322
+ start_time = time.time()
323
+ self.cleanup_generated()
324
+ print("🎬 Starting Enhanced Comic Generation...")
325
+ try:
326
+ cap = cv2.VideoCapture(self.video_path)
327
+ if not cap.isOpened():
328
+ print("❌ Cannot open video to get FPS.")
329
+ return False
330
+ self.video_fps = cap.get(cv2.CAP_PROP_FPS)
331
+ if self.video_fps == 0:
332
+ print("⚠️ Video FPS is 0, defaulting to 25.")
333
+ self.video_fps = 25
334
+ cap.release()
335
+ print(f"βœ… Video FPS detected: {self.video_fps:.2f}")
336
+
337
+ print("πŸ“ Generating subtitles...")
338
+ get_real_subtitles(self.video_path)
339
+ all_subs = []
340
+ if os.path.exists('test1.srt'):
341
+ with open('test1.srt', 'r', encoding='utf-8') as f:
342
+ all_subs = list(srt.parse(f.read()))
343
+ print(f"βœ… Loaded {len(all_subs)} subtitles")
344
+ else:
345
+ print("❌ Subtitle file (test1.srt) not found!")
346
+ return False
347
+
348
+ # Simplified for robustness
349
+ filtered_subs = all_subs
350
+
351
+ key_moments = [{'index': s.index, 'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in filtered_subs]
352
+
353
+ with open(os.path.join(self.output_dir, 'key_moments.json'), 'w', encoding='utf-8') as f:
354
+ json.dump(key_moments, f, indent=2)
355
+
356
+ print("🎬 Extracting frames at key moments...")
357
+ if not self.generate_keyframes_from_moments(self.video_path, key_moments, max_frames=48):
358
+ print("❌ Keyframe extraction failed.")
359
+ return False
360
+
361
+ print("βœ‚οΈ Cropping black bars...")
362
+ black_x, black_y, _, _ = black_bar_crop()
363
+ print("βœ… Black bars cropped.")
364
+
365
+ print("🎨 Enhancing images...")
366
+ self._enhance_all_images()
367
+ self._enhance_quality_colors()
368
+ print("βœ… Images enhancement step complete.")
369
+
370
+ print("πŸ’¬ Creating AI bubbles with key moment dialogues...")
371
+ bubbles = self._create_ai_bubbles_from_moments(black_x, black_y)
372
+ print(f"βœ… Created {len(bubbles)} bubbles.")
373
+
374
+ print("πŸ“‹ Generating pages...")
375
+ pages = self._generate_pages(bubbles)
376
+ print(f"βœ… Generated {len(pages)} pages.")
377
+
378
+ print("πŸ’Ύ Saving results...")
379
+ self._save_results(pages)
380
+ print("βœ… Results saved.")
381
+
382
+ execution_time = (time.time() - start_time) / 60
383
+ print(f"βœ… Comic generation completed in {execution_time:.2f} minutes")
384
+ return True
385
+ except Exception as e:
386
+ print(f"❌ Comic generation failed: {e}")
387
+ traceback.print_exc()
388
+ return False
389
 
390
  def _enhance_all_images(self, single_image_path=None):
391
+ target_dir = self.frames_dir
392
+ if single_image_path:
393
+ target_dir = os.path.dirname(single_image_path)
394
+ if not os.path.exists(target_dir): return
395
+ try:
396
+ enhancer = SimpleColorEnhancer()
397
+ enhancer.enhance_batch(target_dir)
398
+ except Exception as e:
399
+ print(f"❌ Simple enhancement failed during execution: {e}")
400
 
401
  def _enhance_quality_colors(self, single_image_path=None):
402
+ target_dir = self.frames_dir
403
+ if single_image_path:
404
+ target_dir = os.path.dirname(single_image_path)
405
+ try:
406
+ enhancer = QualityColorEnhancer()
407
+ enhancer.batch_enhance(target_dir)
408
+ except Exception as e:
409
+ print(f"⚠️ Quality enhancement failed during execution: {e}")
410
 
411
  def _create_ai_bubbles_from_moments(self, black_x, black_y):
412
+ bubbles = []
413
+ frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
414
+
415
+ metadata_path = 'frames/frame_metadata.json'
416
+ if not os.path.exists(metadata_path):
417
+ print("⚠️ Frame metadata not found, creating empty bubbles.")
418
+ return [bubble(dialog="") for _ in frame_files]
419
+
420
+ with open(metadata_path, 'r') as f:
421
+ frame_metadata = json.load(f)
422
+
423
+ for frame_file in frame_files:
424
+ frame_path = os.path.join(self.frames_dir, frame_file)
425
+ dialogue = ""
426
+
427
+ if frame_file in frame_metadata:
428
+ dialogue = frame_metadata[frame_file]['dialogue']
429
+
430
+ try:
431
+ lip_x, lip_y = -1, -1
432
+ faces = face_detector.detect_faces(frame_path)
433
+ if faces:
434
+ lip_x, lip_y = face_detector.get_lip_position(frame_path, faces[0])
435
+
436
+ bubble_x, bubble_y = ai_bubble_placer.place_bubble_ai(frame_path, (lip_x, lip_y))
437
+ bubbles.append(bubble(
438
+ bubble_offset_x=bubble_x, bubble_offset_y=bubble_y,
439
+ lip_x=lip_x, lip_y=lip_y, dialog=dialogue, emotion='normal'
440
+ ))
441
+ except Exception as e:
442
+ print(f"-> Could not place bubble for {frame_file} due to error: {e}. Using default.")
443
+ bubbles.append(bubble(
444
+ bubble_offset_x=50, bubble_offset_y=20,
445
+ lip_x=-1, lip_y=-1, dialog=dialogue, emotion='normal'
446
+ ))
447
+ return bubbles
448
 
449
  def _generate_pages(self, bubbles):
450
+ try:
451
+ from backend.fixed_12_pages_800x1080 import generate_12_pages_800x1080
452
+ frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
453
+ return generate_12_pages_800x1080(frame_files, bubbles)
454
+ except ImportError:
455
+ pages = []
456
+ frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
457
+ frames_per_page = 4
458
+ num_pages = (len(frame_files) + frames_per_page - 1) // frames_per_page
459
+ frame_counter = 0
460
+ for i in range(num_pages):
461
+ page_panels, page_bubbles = [], []
462
+ for _ in range(frames_per_page):
463
+ if frame_counter < len(frame_files):
464
+ page_panels.append(panel(
465
+ image=frame_files[frame_counter], row_span=6, col_span=6
466
+ ))
467
+ page_bubbles.append(bubbles[frame_counter] if frame_counter < len(bubbles) else bubble(dialog=""))
468
+ frame_counter += 1
469
+ if page_panels:
470
+ pages.append(Page(panels=page_panels, bubbles=page_bubbles))
471
+ return pages
472
 
473
  def _save_results(self, pages):
474
+ try:
475
+ os.makedirs(self.output_dir, exist_ok=True)
476
+ pages_data = []
477
+ for page in pages:
478
+ page_dict = {
479
+ 'panels': [p if isinstance(p, dict) else p.__dict__ for p in page.panels],
480
+ 'bubbles': [b if isinstance(b, dict) else b.__dict__ for b in page.bubbles]
481
+ }
482
+ pages_data.append(page_dict)
483
+ with open(os.path.join(self.output_dir, 'pages.json'), 'w', encoding='utf-8') as f:
484
+ json.dump(pages_data, f, indent=2)
485
+ self._copy_template_files()
486
+ print("βœ… Results saved successfully!")
487
+ except Exception as e:
488
+ print(f"Save results failed: {e}")
489
+ traceback.print_exc()
490
 
491
  def _copy_template_files(self):
492
  """This function contains the complete HTML, CSS, and JavaScript for the interactive editor."""
 
534
  .speech-bubble.flipped-vertical.thought .thought-dot-2 { bottom: auto; top: -32px; }
535
  .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; }
536
  .edit-controls h4 { margin: 0 0 10px 0; color: #26a69a; text-align: center; }
537
+ .edit-controls button, .edit-controls select, .edit-controls input { 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; }
538
  .edit-controls .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
539
  .edit-controls .reset-button { background-color: #e74c3c; }
540
  .edit-controls .action-button { background-color: #4CAF50; }
 
542
  .button-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
543
  .zoom-controls { display: grid; grid-template-columns: auto 1fr; gap: 5px; align-items: center;}
544
  .timestamp-controls { display: grid; grid-template-columns: 1fr auto; gap: 5px; }
545
+ .timestamp-controls input { color: #333; font-weight: normal; }
546
  </style>
547
  </head>
548
  <body>
 
565
  <label>Panel Tools (Select Panel):</label>
566
  <button onclick="replacePanelImage()" class="action-button">πŸ–ΌοΈ Replace Image</button>
567
  <div class="button-grid">
568
+ <button onclick="adjustFrame('backward')" class="secondary-button">⬅️ Prev Frame</button>
569
  <button onclick="adjustFrame('forward')" class="action-button">Next Frame ➑️</button>
570
  </div>
571
  <div class="timestamp-controls">
 
593
  .catch(err => { document.getElementById('comic-pages').innerHTML = `<div class="loading">Error: ${err.message}</div>`; });
594
  });
595
 
596
+ function renderComic(data) {
597
+ const container = document.getElementById('comic-pages');
598
+ container.innerHTML = '';
599
+ if (!data || data.length === 0) return;
600
+ data.forEach((pageData, pageIndex) => {
601
+ const pageWrapper = document.createElement('div');
602
+ pageWrapper.className = 'page-wrapper';
603
+ const pageTitleEl = document.createElement('h2');
604
+ pageTitleEl.className = 'page-title';
605
+ pageTitleEl.textContent = `Page ${pageIndex + 1}`;
606
+ pageWrapper.appendChild(pageTitleEl);
607
+ const pageDiv = document.createElement('div');
608
+ pageDiv.className = 'comic-page';
609
+ const grid = document.createElement('div');
610
+ grid.className = 'comic-grid';
611
+ pageData.panels.forEach((panelData, panelIndex) => {
612
+ const panelDiv = document.createElement('div');
613
+ panelDiv.className = 'panel';
614
+ const img = document.createElement('img');
615
+ img.src = '/frames/final/' + panelData.image;
616
+ panelDiv.appendChild(img);
617
+ if (pageData.bubbles && pageData.bubbles[panelIndex] && pageData.bubbles[panelIndex].dialog) {
618
+ const bubbleData = pageData.bubbles[panelIndex];
619
+ const bubbleDiv = createBubbleElement({
620
+ id: `initial-${pageIndex}-${panelIndex}`,
621
+ text: bubbleData.dialog || '',
622
+ left: `${bubbleData.bubble_offset_x ?? 50}px`,
623
+ top: `${bubbleData.bubble_offset_y ?? 20}px`,
624
+ });
625
+ panelDiv.appendChild(bubbleDiv);
626
+ }
627
+ grid.appendChild(panelDiv);
628
+ });
629
+ pageDiv.appendChild(grid);
630
+ pageWrapper.appendChild(pageDiv);
631
+ container.appendChild(pageWrapper);
632
+ });
633
+ }
634
+
635
+ let currentlyEditing = null, draggedBubble = null, offset = {x: 0, y: 0};
636
+ let currentlySelectedBubble = null;
637
+ let currentlySelectedPanel = null;
638
+ let isPanning = false, panStartX, panStartY, panStartTranslateX, panStartTranslateY;
639
+
640
+ function initializeEditor() {
641
+ document.querySelectorAll('.panel').forEach(panel => {
642
+ panel.addEventListener('click', e => selectPanel(panel));
643
+ const img = panel.querySelector('img');
644
+ if(img) initializePanelImageEvents(img);
645
+ });
646
+ document.querySelectorAll('.speech-bubble').forEach(b => initializeBubbleEvents(b));
647
+ document.getElementById('zoom-slider').addEventListener('input', handleZoom);
648
+
649
+ document.addEventListener('mousemove', (e) => {
650
+ if (isPanning) panImage(e);
651
+ if (draggedBubble) drag(e);
652
+ });
653
+ document.addEventListener('mouseup', (e) => {
654
+ if (isPanning) stopPan(e);
655
+ if (draggedBubble) stopDrag(e);
656
+ });
657
+ document.addEventListener('mouseleave', (e) => {
658
+ if (isPanning) stopPan(e);
659
+ if (draggedBubble) stopDrag(e);
660
+ });
661
+ }
662
+
663
+ function initializePanelImageEvents(img) {
664
+ img.addEventListener('mousedown', startPan);
665
+ }
666
+
667
+ function initializeBubbleEvents(bubble) {
668
+ bubble.addEventListener('dblclick', e => { e.stopPropagation(); editBubbleText(bubble); });
669
+ bubble.addEventListener('mousedown', e => { e.stopPropagation(); startDrag(e); });
670
+ bubble.addEventListener('click', e => { e.stopPropagation(); selectBubble(bubble); });
671
+ bubble.addEventListener('wheel', e => {
672
+ e.preventDefault();
673
+ const currentWidth = parseFloat(bubble.style.width) || bubble.offsetWidth;
674
+ const newWidth = currentWidth - (e.deltaY > 0 ? 10 : -10);
675
+ if (newWidth >= 60) {
676
+ bubble.style.width = `${newWidth}px`;
677
+ bubble.style.height = 'auto';
678
+ }
679
+ }, { passive: false });
680
+ }
681
+
682
+ function createBubbleElement(data) {
683
+ const bubbleDiv = document.createElement('div');
684
+ bubbleDiv.dataset.id = data.id;
685
+ const textSpan = document.createElement('span');
686
+ textSpan.className = 'bubble-text';
687
+ textSpan.textContent = data.text;
688
+ bubbleDiv.appendChild(textSpan);
689
+ bubbleDiv.style.left = data.left;
690
+ bubbleDiv.style.top = data.top;
691
+ applyBubbleType(bubbleDiv, 'speech');
692
+ return bubbleDiv;
693
+ }
694
+
695
+ function applyBubbleType(bubble, type) {
696
+ bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
697
+ let classesToKeep = 'speech-bubble';
698
+ if (bubble.classList.contains('selected')) classesToKeep += ' selected';
699
+ if (bubble.classList.contains('flipped')) classesToKeep += ' flipped';
700
+ if (bubble.classList.contains('flipped-vertical')) classesToKeep += ' flipped-vertical';
701
+ bubble.className = classesToKeep;
702
+ bubble.classList.add(type);
703
+ bubble.dataset.type = type;
704
+ if (type === 'thought') {
705
+ for (let i = 1; i <= 2; i++) {
706
+ const dot = document.createElement('div');
707
+ dot.className = `thought-dot thought-dot-${i}`;
708
+ bubble.appendChild(dot);
709
+ }
710
+ }
711
+ }
712
+
713
+ function changeBubbleType(type) {
714
+ if (!currentlySelectedBubble) return;
715
+ applyBubbleType(currentlySelectedBubble, type);
716
+ }
717
+
718
+ function rotateBubbleTail() {
719
+ if (!currentlySelectedBubble) { alert("Please select a bubble first."); return; }
720
+ const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
721
+ const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
722
+ if (!isFlippedH && !isFlippedV) {
723
+ currentlySelectedBubble.classList.add('flipped');
724
+ } else if (isFlippedH && !isFlippedV) {
725
+ currentlySelectedBubble.classList.add('flipped-vertical');
726
+ } else if (isFlippedH && isFlippedV) {
727
+ currentlySelectedBubble.classList.remove('flipped');
728
+ } else {
729
+ currentlySelectedBubble.classList.remove('flipped-vertical');
730
+ }
731
+ }
732
+
733
+ function selectPanel(panel) {
734
+ document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
735
+ panel.classList.add('selected');
736
+ currentlySelectedPanel = panel;
737
+ selectBubble(null);
738
+
739
+ const img = currentlySelectedPanel.querySelector('img');
740
+ const zoomSlider = document.getElementById('zoom-slider');
741
+ zoomSlider.value = img.dataset.zoom || 100;
742
+ zoomSlider.disabled = false;
743
+ }
744
+
745
+ function selectBubble(bubble) {
746
+ if (currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
747
+ currentlySelectedBubble = bubble;
748
+ if (currentlySelectedBubble) {
749
+ currentlySelectedBubble.classList.add('selected');
750
+ if(currentlySelectedPanel) currentlySelectedPanel.classList.remove('selected');
751
+ currentlySelectedPanel = null;
752
+ document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
753
+ document.getElementById('zoom-slider').disabled = true;
754
+ }
755
+ }
756
+
757
+ function editBubbleText(bubble) {
758
+ if (currentlyEditing) return;
759
+ currentlyEditing = bubble;
760
+ const textSpan = bubble.querySelector('.bubble-text');
761
+ const currentText = textSpan.textContent;
762
+ textSpan.style.display = 'none';
763
+ bubble.style.height = 'auto';
764
+ const textarea = document.createElement('textarea');
765
+ textarea.value = currentText;
766
+ bubble.appendChild(textarea);
767
+ textarea.focus();
768
+ const finishEditing = () => {
769
+ textSpan.textContent = textarea.value;
770
+ bubble.removeChild(textarea);
771
+ textSpan.style.display = '';
772
+ currentlyEditing = null;
773
+ bubble.style.height = 'auto';
774
+ };
775
+ textarea.addEventListener('blur', finishEditing, { once: true });
776
+ textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); }});
777
+ }
778
+
779
+ function startDrag(e) {
780
+ const bubble = e.target.closest('.speech-bubble');
781
+ if (!bubble || currentlyEditing) return;
782
+ draggedBubble = bubble;
783
+ selectBubble(bubble);
784
+ const rect = bubble.getBoundingClientRect();
785
+ offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
786
+ }
787
+
788
+ function drag(e) {
789
+ if (!draggedBubble) return;
790
+ const parentRect = draggedBubble.parentElement.getBoundingClientRect();
791
+ let x = e.clientX - parentRect.left - offset.x;
792
+ let y = e.clientY - parentRect.top - offset.y;
793
+ draggedBubble.style.left = `${x}px`;
794
+ draggedBubble.style.top = `${y}px`;
795
+ }
796
+
797
+ function stopDrag() {
798
+ draggedBubble = null;
799
+ }
800
+
801
+ function clearSavedState() {
802
+ if (confirm("Reset all edits to the original AI-generated comic?")) {
803
+ localStorage.removeItem('comicEditorState');
804
+ window.location.reload();
805
+ }
806
+ }
807
+
808
+ async function exportPagesToPNG() {
809
+ const pages = document.querySelectorAll('.comic-page');
810
+ if (pages.length === 0) return alert("No pages found.");
811
+ alert(`Starting export of ${pages.length} page(s).`);
812
+ for (let i = 0; i < pages.length; i++) {
813
+ try {
814
+ const canvas = await html2canvas(pages[i], { scale: 2 });
815
+ const link = document.createElement('a');
816
+ link.download = `comic-page-${i + 1}.png`;
817
+ link.href = canvas.toDataURL('image/png');
818
+ link.click();
819
+ } catch (err) {
820
+ alert(`Failed to export page ${i + 1}.`);
821
+ }
822
+ }
823
+ }
824
+
825
+ function replacePanelImage() {
826
+ if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
827
+ const img = currentlySelectedPanel.querySelector('img');
828
+ const uploader = document.getElementById('image-uploader');
829
+ const oneTimeListener = (event) => {
830
+ const file = event.target.files[0];
831
+ if (!file) return;
832
+ const formData = new FormData();
833
+ formData.append('image', file);
834
+ img.style.opacity = '0.5';
835
+ fetch('/replace_panel', { method: 'POST', body: formData })
836
+ .then(response => response.json())
837
+ .then(data => {
838
+ if (data.success) {
839
+ img.src = `/frames/final/${data.new_filename}?t=${new Date().getTime()}`;
840
+ } else { alert('Error replacing image: ' + data.error); }
841
+ img.style.opacity = '1';
842
+ })
843
+ .catch(error => {
844
+ alert('An error occurred during the upload.');
845
+ img.style.opacity = '1';
846
+ });
847
+ uploader.removeEventListener('change', oneTimeListener);
848
+ uploader.value = '';
849
+ };
850
+ uploader.addEventListener('change', oneTimeListener, { once: true });
851
+ uploader.click();
852
+ }
853
+
854
+ function adjustFrame(direction) {
855
+ if (!currentlySelectedPanel) { alert("Please select a panel first to adjust its frame."); return; }
856
+ const img = currentlySelectedPanel.querySelector('img');
857
+ const currentSrc = img.src;
858
+ let filename = currentSrc.substring(currentSrc.lastIndexOf('/') + 1);
859
+ if (filename.includes('?')) { filename = filename.split('?')[0]; }
860
+ img.style.opacity = '0.5';
861
+ fetch('/regenerate_frame', {
862
+ method: 'POST',
863
+ headers: { 'Content-Type': 'application/json' },
864
+ body: JSON.stringify({ filename: filename, direction: direction })
865
+ })
866
+ .then(response => response.json())
867
+ .then(data => {
868
+ if (data.success) {
869
+ img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
870
+ console.log(data.message);
871
+ } else { alert('Error: ' + data.message); }
872
+ img.style.opacity = '1';
873
+ })
874
+ .catch(error => {
875
+ alert('An error occurred during frame adjustment.');
876
+ img.style.opacity = '1';
877
+ });
878
+ }
879
+
880
+ function updateImageTransform(img) {
881
+ const zoom = (img.dataset.zoom || 100) / 100;
882
+ const x = img.dataset.translateX || 0;
883
+ const y = img.dataset.translateY || 0;
884
+ img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${zoom})`;
885
+ if (zoom > 1) { img.classList.add('pannable'); } else { img.classList.remove('pannable'); }
886
+ }
887
+
888
+ function handleZoom(event) {
889
+ if (!currentlySelectedPanel) return;
890
+ const img = currentlySelectedPanel.querySelector('img');
891
+ img.dataset.zoom = event.target.value;
892
+ updateImageTransform(img);
893
+ }
894
+
895
+ function resetPanelTransform() {
896
+ if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
897
+ const img = currentlySelectedPanel.querySelector('img');
898
+ img.dataset.zoom = 100;
899
+ img.dataset.translateX = 0;
900
+ img.dataset.translateY = 0;
901
+ document.getElementById('zoom-slider').value = 100;
902
+ updateImageTransform(img);
903
+ }
904
+
905
+ function startPan(event) {
906
+ if (event.button !== 0) return;
907
+ const img = event.target;
908
+ const zoom = parseFloat(img.dataset.zoom || 100);
909
+ if (zoom <= 100) return;
910
+ event.preventDefault();
911
+ isPanning = true;
912
+ img.classList.add('panning');
913
+ panStartX = event.clientX;
914
+ panStartY = event.clientY;
915
+ panStartTranslateX = parseFloat(img.dataset.translateX || 0);
916
+ panStartTranslateY = parseFloat(img.dataset.translateY || 0);
917
+ }
918
+
919
+ function panImage(event) {
920
+ if (!isPanning || !currentlySelectedPanel) return;
921
+ const img = currentlySelectedPanel.querySelector('img');
922
+ const dx = event.clientX - panStartX;
923
+ const dy = event.clientY - panStartY;
924
+ img.dataset.translateX = panStartTranslateX + dx;
925
+ img.dataset.translateY = panStartTranslateY + dy;
926
+ updateImageTransform(img);
927
+ }
928
+
929
+ function stopPan(event) {
930
+ if (!isPanning) return;
931
+ isPanning = false;
932
+ if (currentlySelectedPanel) {
933
+ const img = currentlySelectedPanel.querySelector('img');
934
+ if(img) img.classList.remove('panning');
935
+ }
936
+ }
937
+
938
+ function addBubbleToPanel() {
939
+ if (!currentlySelectedPanel) {
940
+ alert("Please select a panel first to add a bubble to it.");
941
+ return;
942
+ }
943
+
944
+ const newBubble = createBubbleElement({
945
+ id: `new-bubble-${Date.now()}`,
946
+ text: 'New Text...',
947
+ left: '10%',
948
+ top: '10%'
949
+ });
950
 
951
+ currentlySelectedPanel.appendChild(newBubble);
952
+ initializeBubbleEvents(newBubble);
953
+ selectBubble(newBubble);
954
+ editBubbleText(newBubble);
955
+ }
956
+
957
  function gotoTimestamp() {
958
  if (!currentlySelectedPanel) {
959
  alert("Please select a panel first to jump to a timestamp.");
 
969
  let parsedSeconds = 0;
970
  if (timeStr.includes(':')) {
971
  const parts = timeStr.split(':');
972
+ try {
973
+ const minutes = parseInt(parts[0], 10);
974
+ const seconds = parseFloat(parts[1]);
975
+ if (!isNaN(minutes) && !isNaN(seconds)) {
976
+ parsedSeconds = (minutes * 60) + seconds;
977
+ } else { throw new Error(); }
978
+ } catch {
979
  alert("Invalid time format. Please use 'mm:ss'.");
980
  return;
981
  }
 
1001
  .then(data => {
1002
  if (data.success) {
1003
  img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
1004
+ input.value = '';
1005
+ resetPanelTransform();
1006
  } else {
1007
  alert('Error: ' + data.message);
1008
  }
 
1029
  def index():
1030
  return render_template('index.html')
1031
 
1032
+ @app.route('/uploader', methods=['POST'])
1033
+ def upload_file():
1034
+ try:
1035
+ if 'file' not in request.files or request.files['file'].filename == '':
1036
+ return "❌ No file selected"
1037
+ f = request.files['file']
1038
+ if os.path.exists(comic_generator.video_path):
1039
+ os.remove(comic_generator.video_path)
1040
+ f.save(comic_generator.video_path)
1041
+ success = comic_generator.generate_comic()
1042
+ if success:
1043
+ return "πŸŽ‰ Enhanced Comic Created Successfully! View it at the /comic endpoint."
1044
+ else:
1045
+ return "❌ Comic generation failed. Check the Space logs for details."
1046
+ except Exception as e:
1047
+ traceback.print_exc()
1048
+ return f"❌ An unexpected error occurred: {str(e)}"
1049
+
1050
+ @app.route('/handle_link', methods=['POST'])
1051
+ def handle_link():
1052
+ try:
1053
+ link = request.form.get('link', '')
1054
+ if not link: return "❌ No link provided"
1055
+ import yt_dlp
1056
+ ydl_opts = {'outtmpl': comic_generator.video_path, 'format': 'best[height<=720]'}
1057
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
1058
+ ydl.download([link])
1059
+ success = comic_generator.generate_comic()
1060
+ if success:
1061
+ return "πŸŽ‰ Enhanced Comic Created Successfully! View it at the /comic endpoint."
1062
+ else:
1063
+ return "❌ Comic generation failed. Check the Space logs for details."
1064
+ except Exception as e:
1065
+ traceback.print_exc()
1066
+ return f"❌ An unexpected error occurred: {str(e)}"
1067
+
1068
+ @app.route('/replace_panel', methods=['POST'])
1069
+ def replace_panel():
1070
+ try:
1071
+ if 'image' not in request.files: return jsonify({'success': False, 'error': 'No image file provided.'})
1072
+ file = request.files['image']
1073
+ timestamp = int(time.time() * 1000)
1074
+ filename = f"replaced_panel_{timestamp}.png"
1075
+ save_path = os.path.join(comic_generator.frames_dir, filename)
1076
+ file.save(save_path)
1077
+ print(f"βœ… Replaced panel with '{filename}' without applying color enhancement.")
1078
+ return jsonify({'success': True, 'new_filename': filename})
1079
+ except Exception as e:
1080
+ traceback.print_exc()
1081
+ return jsonify({'success': False, 'error': str(e)})
1082
+
1083
+ @app.route('/regenerate_frame', methods=['POST'])
1084
+ def regenerate_frame_route():
1085
+ try:
1086
+ data = request.get_json()
1087
+ filename = data.get('filename')
1088
+ direction = data.get('direction')
1089
+ if not filename or not direction:
1090
+ return jsonify({'success': False, 'message': 'Filename or direction missing.'})
1091
+
1092
+ result = comic_generator.regenerate_frame(filename, direction)
1093
+ return jsonify(result)
1094
+ except Exception as e:
1095
+ traceback.print_exc()
1096
+ return jsonify({'success': False, 'message': str(e)})
1097
 
1098
  @app.route('/goto_timestamp', methods=['POST'])
1099
  def goto_timestamp_route():
 
1110
  traceback.print_exc()
1111
  return jsonify({'success': False, 'message': str(e)})
1112
 
1113
+ @app.route('/comic')
1114
+ def view_comic():
1115
+ return send_from_directory('output', 'page.html')
1116
+
1117
+ @app.route('/output/<path:filename>')
1118
+ def output_file(filename):
1119
+ return send_from_directory('output', filename)
1120
+
1121
+ @app.route('/frames/final/<path:filename>')
1122
+ def frame_file(filename):
1123
+ return send_from_directory('frames/final', filename)
1124
+
1125
  if __name__ == '__main__':
1126
  port = int(os.getenv("PORT", 7860))
1127
  print(f"πŸš€ Starting Enhanced Comic Generator on host 0.0.0.0, port {port}")