tester343 commited on
Commit
a78e278
·
verified ·
1 Parent(s): dce2b01

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +43 -71
app_enhanced.py CHANGED
@@ -1,14 +1,24 @@
1
  import os
2
- import webbrowser
3
  import time
4
  import threading
5
  import uuid
6
  import shutil
7
  import json
8
  import traceback
9
- from typing import List
10
  from concurrent.futures import ThreadPoolExecutor
11
- from flask import Flask, render_template, request, jsonify, send_from_directory, send_file, session
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  # --- ROBUST IMPORTS WITH FALLBACKS ---
14
  try:
@@ -25,8 +35,8 @@ try:
25
  except Exception as e:
26
  print(f"⚠️ Could not load SimpleColorEnhancer: {e}. This feature will be SKIPPED.")
27
  class SimpleColorEnhancer:
28
- def enhance_batch(self, *args, **kwargs): print("-> Skipping simple color enhancement (module not loaded).")
29
- def enhance_single(self, *args, **kwargs): print("-> Skipping simple color enhancement (module not loaded).")
30
 
31
  try:
32
  from backend.quality_color_enhancer import QualityColorEnhancer
@@ -34,8 +44,8 @@ try:
34
  except Exception as e:
35
  print(f"⚠️ Could not load QualityColorEnhancer: {e}. This feature will be SKIPPED.")
36
  class QualityColorEnhancer:
37
- def batch_enhance(self, *args, **kwargs): print("-> Skipping quality color enhancement (module not loaded).")
38
- def enhance_single(self, *args, **kwargs): print("-> Skipping quality color enhancement (module not loaded).")
39
 
40
  try:
41
  from backend.class_def import bubble, panel, Page
@@ -60,11 +70,10 @@ except Exception as e:
60
 
61
  # --- FLASK APP SETUP ---
62
  app = Flask(__name__)
63
- # Constant secret key ensures sessions survive server restarts if browser keeps cookie
64
- app.secret_key = "SUPER_SECRET_COMIC_KEY_123"
65
- BASE_USER_DIR = "userdata"
66
 
67
- # --- MERGED HTML: UPLOAD UI + EDITOR UI ---
68
  INDEX_HTML = '''
69
  <!DOCTYPE html>
70
  <html lang="en">
@@ -72,7 +81,7 @@ INDEX_HTML = '''
72
  <meta charset="UTF-8">
73
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
74
  <title>Movie to Comic Generator</title>
75
- <!-- EXPORT LIBRARY -->
76
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
77
  <link rel="preconnect" href="https://fonts.googleapis.com">
78
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -87,31 +96,20 @@ INDEX_HTML = '''
87
  min-height: 100vh;
88
  }
89
 
90
- /* Centered Container for Upload */
91
  #upload-container {
92
- display: flex;
93
- justify-content: center;
94
- align-items: center;
95
- min-height: 100vh;
96
- width: 100%;
97
  }
98
 
99
  .upload-box {
100
- max-width: 500px;
101
- width: 100%;
102
- padding: 40px;
103
- background-color: #ffffff;
104
- border-radius: 12px;
105
- box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
106
- text-align: center;
107
  }
108
 
109
- /* Editor Container (Hidden initially) */
110
  #editor-container {
111
  display: none; /* Hidden by default */
112
- padding: 20px;
113
- width: 100%;
114
- box-sizing: border-box;
115
  }
116
 
117
  h1 { color: #2c3e50; margin-bottom: 30px; font-weight: 600; }
@@ -130,14 +128,12 @@ INDEX_HTML = '''
130
  }
131
  .submit-btn:hover { background-color: #d35400; }
132
 
133
- /* Loader */
134
  .loading-view { display: none; flex-direction: column; align-items: center; justify-content: center; }
135
  .loader {
136
  width: 120px; height: 20px; border-radius: 20px;
137
- background:
138
- radial-gradient(circle 10px, #e67e22 100%, transparent 0),
139
- radial-gradient(circle 10px, #e67e22 100%, transparent 0),
140
- radial-gradient(circle 10px, #e67e22 100%, transparent 0);
141
  background-size: 20px 20px; background-position: 0px 50%, 50px 50%, 100px 50%;
142
  background-repeat: no-repeat; animation:-ball 2s infinite linear;
143
  }
@@ -174,7 +170,7 @@ INDEX_HTML = '''
174
  .speech-bubble.selected { outline: 2px dashed #4CAF50; }
175
  .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; }
176
 
177
- /* <<< SPEECH BUBBLE CSS (EXPORT SAFE - GRADIENT METHOD) >>> */
178
  .speech-bubble.speech {
179
  --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
180
  --c: var(--bubble-fill-color, #4ECDC4);
@@ -186,6 +182,7 @@ INDEX_HTML = '''
186
  background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
187
  border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
188
  }
 
189
  .speech-bubble.speech.tail-bottom:before { top: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
190
  .speech-bubble.speech.tail-top { border-radius: min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) var(--r) var(--r) / var(--r); }
191
  .speech-bubble.speech.tail-top:before { bottom: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: scaleY(-1); }
@@ -333,7 +330,7 @@ INDEX_HTML = '''
333
  // SWITCH TO EDITOR VIEW
334
  document.getElementById('upload-container').style.display = 'none';
335
  document.getElementById('editor-container').style.display = 'block';
336
- loadComicData(); // Load the json
337
  } else if (data.progress < 0) {
338
  clearInterval(statusInterval);
339
  statusText.textContent = "An error occurred. Check server logs.";
@@ -447,7 +444,6 @@ INDEX_HTML = '''
447
  document.getElementById('zoom-slider').addEventListener('input', handleZoom);
448
  document.getElementById('tail-slider').addEventListener('input', (e) => slideTail(e.target.value));
449
 
450
- // Color pickers
451
  document.getElementById('bubble-text-color').addEventListener('input', (e) => {
452
  if(currentlySelectedBubble) currentlySelectedBubble.style.setProperty('--bubble-text-color', e.target.value);
453
  });
@@ -476,13 +472,12 @@ INDEX_HTML = '''
476
  panel.classList.add('selected');
477
  currentlySelectedPanel = panel;
478
  selectBubble(null);
479
- resetPanelTransform(); // Optional reset or load state
480
  }
481
 
482
  function selectBubble(bubble) {
483
  if(currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
484
  currentlySelectedBubble = bubble;
485
- const controls = ['bubble-text-color', 'bubble-fill-color', 'bubble-type-select', 'zoom-slider'];
486
  const tailControls = document.getElementById('tail-controls');
487
 
488
  if(bubble) {
@@ -495,13 +490,14 @@ INDEX_HTML = '''
495
  if(bubble.dataset.type === 'speech') tailControls.style.display = 'block';
496
  else tailControls.style.display = 'none';
497
  } else {
498
- controls.forEach(id => { if(document.getElementById(id)) document.getElementById(id).disabled = true; });
 
 
499
  document.getElementById('zoom-slider').disabled = false;
500
  tailControls.style.display = 'none';
501
  }
502
  }
503
 
504
- // --- Drag/Resize/Tail Logic (Same as before) ---
505
  function startDrag(e) {
506
  draggedBubble = e.target.closest('.speech-bubble');
507
  selectBubble(draggedBubble);
@@ -573,7 +569,6 @@ INDEX_HTML = '''
573
  if(currentlySelectedBubble) { currentlySelectedBubble.remove(); selectBubble(null); }
574
  }
575
 
576
- // --- Panel Image Manipulation ---
577
  function startPan(e) {
578
  if(e.button !== 0) return;
579
  const img = e.target;
@@ -617,7 +612,6 @@ INDEX_HTML = '''
617
  updateImageTransform(img);
618
  }
619
 
620
- // --- API Calls ---
621
  function replacePanelImage() {
622
  if (!currentlySelectedPanel) return alert("Select a panel first.");
623
  const img = currentlySelectedPanel.querySelector('img');
@@ -695,7 +689,6 @@ INDEX_HTML = '''
695
 
696
  class EnhancedComicGenerator:
697
  def __init__(self, sid):
698
- # --- PER-USER SESSION DIRECTORIES ---
699
  self.sid = sid
700
  self.user_dir = os.path.join(BASE_USER_DIR, sid)
701
  self.video_path = os.path.join(self.user_dir, 'uploaded.mp4')
@@ -703,7 +696,6 @@ class EnhancedComicGenerator:
703
  self.output_dir = os.path.join(self.user_dir, 'output')
704
  self.status_file = os.path.join(self.output_dir, 'status.json')
705
 
706
- # Create directories if they don't exist
707
  os.makedirs(self.frames_dir, exist_ok=True)
708
  os.makedirs(self.output_dir, exist_ok=True)
709
 
@@ -724,11 +716,9 @@ class EnhancedComicGenerator:
724
  os.remove(os.path.join(self.frames_dir, file))
725
  if os.path.isdir(self.output_dir):
726
  for file in os.listdir(self.output_dir):
727
- if file != 'status.json': # Keep status file briefly
728
- try:
729
- os.remove(os.path.join(self.output_dir, file))
730
  except: pass
731
- # Clean temp srt for this user (could be better handled with tempfile)
732
  srt_file = os.path.join(self.user_dir, 'subs.srt')
733
  if os.path.exists(srt_file): os.remove(srt_file)
734
  print(f"[{self.sid}] ✅ Cleanup complete.")
@@ -736,7 +726,6 @@ class EnhancedComicGenerator:
736
  def regenerate_frame(self, frame_filename, direction):
737
  try:
738
  if not self.video_fps:
739
- # Re-open video to get FPS if lost (unlikely but safe)
740
  cap = cv2.VideoCapture(self.video_path)
741
  self.video_fps = cap.get(cv2.CAP_PROP_FPS)
742
  cap.release()
@@ -772,7 +761,6 @@ class EnhancedComicGenerator:
772
  self._enhance_all_images(single_image_path=new_path)
773
  self._enhance_quality_colors(single_image_path=new_path)
774
 
775
- # Update metadata
776
  if isinstance(frame_to_time[frame_filename], dict):
777
  frame_to_time[frame_filename]['time'] = target_time
778
  else:
@@ -871,17 +859,13 @@ class EnhancedComicGenerator:
871
  cap.release()
872
 
873
  self.update_status("Generating subtitles...", 10)
874
- # IMPORTANT: Adapt get_real_subtitles to support output path if possible
875
- # For now, assuming it generates 'test1.srt' in CWD, we move it
876
  get_real_subtitles(self.video_path)
877
 
878
- # Move the generated SRT to user folder if it exists in root
879
  if os.path.exists('test1.srt'):
880
  user_srt = os.path.join(self.user_dir, 'subs.srt')
881
  shutil.move('test1.srt', user_srt)
882
  else:
883
- user_srt = os.path.join(self.user_dir, 'subs.srt') # Or however your logic handles it
884
- # If get_real_subtitles is hardcoded, this part needs care in your backend
885
 
886
  with open(user_srt, 'r', encoding='utf-8') as f:
887
  all_subs = list(srt.parse(f.read()))
@@ -892,7 +876,7 @@ class EnhancedComicGenerator:
892
  raise Exception("Keyframe extraction failed.")
893
 
894
  self.update_status("Cropping black bars...", 45)
895
- black_x, black_y, _, _ = black_bar_crop() # This might need adaptation to take an image path if it's global
896
 
897
  self.update_status("Enhancing images...", 50)
898
  self._enhance_all_images()
@@ -950,7 +934,6 @@ class EnhancedComicGenerator:
950
  bubble_x, bubble_y = ai_bubble_placer.place_bubble_ai(frame_path, (lip_x, lip_y))
951
  return bubble(bubble_offset_x=bubble_x, bubble_offset_y=bubble_y, lip_x=lip_x, lip_y=lip_y, dialog=dialogue, emotion='normal')
952
  except Exception as e:
953
- # Fallback
954
  return bubble(bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, dialog=dialogue, emotion='normal')
955
 
956
  def _create_ai_bubbles_from_moments(self, black_x, black_y):
@@ -967,7 +950,6 @@ class EnhancedComicGenerator:
967
  return bubbles
968
 
969
  def _generate_pages(self, bubbles):
970
- # Using simple fallback generation logic for stability
971
  pages, frame_files = [], sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
972
  num_pages = (len(frame_files) + 3) // 4
973
  for i in range(num_pages):
@@ -993,10 +975,8 @@ class EnhancedComicGenerator:
993
 
994
  def _copy_template_files(self):
995
  try:
996
- # We don't strictly need to write page.html anymore for the main flow,
997
- # but it is good for debugging or direct /comic access.
998
  with open(os.path.join(self.output_dir, 'page.html'), 'w', encoding='utf-8') as f:
999
- f.write(INDEX_HTML) # We can reuse the main HTML structure for simplicity if accessed directly
1000
  except Exception as e:
1001
  print(f"[{self.sid}] Template creation failed: {e}")
1002
 
@@ -1010,11 +990,7 @@ def index():
1010
 
1011
  @app.route('/uploader', methods=['POST'])
1012
  def upload_file():
1013
- # FIX: Auto-create session if missing due to restart
1014
- if 'sid' not in session:
1015
- session['sid'] = uuid.uuid4().hex
1016
- print(f"⚠️ Session recreated: {session['sid']}")
1017
-
1018
  try:
1019
  f = request.files['file']
1020
  gen = EnhancedComicGenerator(session['sid'])
@@ -1036,8 +1012,6 @@ def status():
1036
  @app.route('/comic')
1037
  def view_comic():
1038
  if 'sid' not in session: return "Session expired", 400
1039
- # For /comic, we can serve the same INDEX_HTML but maybe trigger a load immediately?
1040
- # Or just serve the page.html if generated.
1041
  return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'output'), 'page.html')
1042
 
1043
  @app.route('/output/<path:filename>')
@@ -1050,7 +1024,6 @@ def frame_file(filename):
1050
  if 'sid' not in session: return "Session expired", 400
1051
  return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'frames'), filename)
1052
 
1053
- # Actions that modify state need the session ID to find the correct folders
1054
  @app.route('/regenerate_frame', methods=['POST'])
1055
  def regenerate_frame_route():
1056
  if 'sid' not in session: return jsonify({'success': False, 'message': 'Session expired'})
@@ -1076,7 +1049,6 @@ def goto_timestamp_route():
1076
  return jsonify(gen.get_frame_at_timestamp(data['filename'], float(data['timestamp'])))
1077
 
1078
  if __name__ == '__main__':
1079
- # Ensure base userdata dir exists
1080
  os.makedirs(BASE_USER_DIR, exist_ok=True)
1081
  port = int(os.getenv("PORT", 7860))
1082
  print(f"🚀 Starting Multi-User Comic Generator on port {port}")
 
1
  import os
 
2
  import time
3
  import threading
4
  import uuid
5
  import shutil
6
  import json
7
  import traceback
 
8
  from concurrent.futures import ThreadPoolExecutor
9
+
10
+ # --- ESSENTIAL IMPORTS ---
11
+ # These must be at the top. If they fail, add them to requirements.txt
12
+ try:
13
+ import cv2
14
+ import numpy as np
15
+ from PIL import Image
16
+ import srt
17
+ from flask import Flask, render_template, request, jsonify, send_from_directory, send_file, session
18
+ except ImportError as e:
19
+ print(f"❌ Critical Import Error: {e}")
20
+ print("Please ensure 'flask', 'opencv-python-headless', 'numpy', 'pillow', and 'srt' are in requirements.txt")
21
+ exit(1)
22
 
23
  # --- ROBUST IMPORTS WITH FALLBACKS ---
24
  try:
 
35
  except Exception as e:
36
  print(f"⚠️ Could not load SimpleColorEnhancer: {e}. This feature will be SKIPPED.")
37
  class SimpleColorEnhancer:
38
+ def enhance_batch(self, *args, **kwargs): pass
39
+ def enhance_single(self, *args, **kwargs): pass
40
 
41
  try:
42
  from backend.quality_color_enhancer import QualityColorEnhancer
 
44
  except Exception as e:
45
  print(f"⚠️ Could not load QualityColorEnhancer: {e}. This feature will be SKIPPED.")
46
  class QualityColorEnhancer:
47
+ def batch_enhance(self, *args, **kwargs): pass
48
+ def enhance_single(self, *args, **kwargs): pass
49
 
50
  try:
51
  from backend.class_def import bubble, panel, Page
 
70
 
71
  # --- FLASK APP SETUP ---
72
  app = Flask(__name__)
73
+ app.secret_key = "HUGGINGFACE_SECRET_KEY_XYZ" # Necessary for sessions
74
+ BASE_USER_DIR = "userdata" # Persistent storage location
 
75
 
76
+ # --- MERGED HTML: UPLOAD UI + EDITOR UI + EXPORT FIX + CSS FIX ---
77
  INDEX_HTML = '''
78
  <!DOCTYPE html>
79
  <html lang="en">
 
81
  <meta charset="UTF-8">
82
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
83
  <title>Movie to Comic Generator</title>
84
+ <!-- EXPORT LIBRARY (Supports CSS Masks/Gradients) -->
85
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
86
  <link rel="preconnect" href="https://fonts.googleapis.com">
87
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 
96
  min-height: 100vh;
97
  }
98
 
 
99
  #upload-container {
100
+ display: flex; justify-content: center; align-items: center;
101
+ min-height: 100vh; width: 100%;
 
 
 
102
  }
103
 
104
  .upload-box {
105
+ max-width: 500px; width: 100%; padding: 40px;
106
+ background-color: #ffffff; border-radius: 12px;
107
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); text-align: center;
 
 
 
 
108
  }
109
 
 
110
  #editor-container {
111
  display: none; /* Hidden by default */
112
+ padding: 20px; width: 100%; box-sizing: border-box;
 
 
113
  }
114
 
115
  h1 { color: #2c3e50; margin-bottom: 30px; font-weight: 600; }
 
128
  }
129
  .submit-btn:hover { background-color: #d35400; }
130
 
 
131
  .loading-view { display: none; flex-direction: column; align-items: center; justify-content: center; }
132
  .loader {
133
  width: 120px; height: 20px; border-radius: 20px;
134
+ background: radial-gradient(circle 10px, #e67e22 100%, transparent 0),
135
+ radial-gradient(circle 10px, #e67e22 100%, transparent 0),
136
+ radial-gradient(circle 10px, #e67e22 100%, transparent 0);
 
137
  background-size: 20px 20px; background-position: 0px 50%, 50px 50%, 100px 50%;
138
  background-repeat: no-repeat; animation:-ball 2s infinite linear;
139
  }
 
170
  .speech-bubble.selected { outline: 2px dashed #4CAF50; }
171
  .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; }
172
 
173
+ /* <<< EXACT CSS WITH GRADIENT EXPORT FIX >>> */
174
  .speech-bubble.speech {
175
  --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
176
  --c: var(--bubble-fill-color, #4ECDC4);
 
182
  background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
183
  border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
184
  }
185
+ /* Directional Logic */
186
  .speech-bubble.speech.tail-bottom:before { top: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
187
  .speech-bubble.speech.tail-top { border-radius: min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) var(--r) var(--r) / var(--r); }
188
  .speech-bubble.speech.tail-top:before { bottom: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: scaleY(-1); }
 
330
  // SWITCH TO EDITOR VIEW
331
  document.getElementById('upload-container').style.display = 'none';
332
  document.getElementById('editor-container').style.display = 'block';
333
+ loadComicData();
334
  } else if (data.progress < 0) {
335
  clearInterval(statusInterval);
336
  statusText.textContent = "An error occurred. Check server logs.";
 
444
  document.getElementById('zoom-slider').addEventListener('input', handleZoom);
445
  document.getElementById('tail-slider').addEventListener('input', (e) => slideTail(e.target.value));
446
 
 
447
  document.getElementById('bubble-text-color').addEventListener('input', (e) => {
448
  if(currentlySelectedBubble) currentlySelectedBubble.style.setProperty('--bubble-text-color', e.target.value);
449
  });
 
472
  panel.classList.add('selected');
473
  currentlySelectedPanel = panel;
474
  selectBubble(null);
475
+ resetPanelTransform();
476
  }
477
 
478
  function selectBubble(bubble) {
479
  if(currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
480
  currentlySelectedBubble = bubble;
 
481
  const tailControls = document.getElementById('tail-controls');
482
 
483
  if(bubble) {
 
490
  if(bubble.dataset.type === 'speech') tailControls.style.display = 'block';
491
  else tailControls.style.display = 'none';
492
  } else {
493
+ if(document.getElementById('bubble-text-color')) document.getElementById('bubble-text-color').disabled = true;
494
+ if(document.getElementById('bubble-fill-color')) document.getElementById('bubble-fill-color').disabled = true;
495
+ if(document.getElementById('bubble-type-select')) document.getElementById('bubble-type-select').disabled = true;
496
  document.getElementById('zoom-slider').disabled = false;
497
  tailControls.style.display = 'none';
498
  }
499
  }
500
 
 
501
  function startDrag(e) {
502
  draggedBubble = e.target.closest('.speech-bubble');
503
  selectBubble(draggedBubble);
 
569
  if(currentlySelectedBubble) { currentlySelectedBubble.remove(); selectBubble(null); }
570
  }
571
 
 
572
  function startPan(e) {
573
  if(e.button !== 0) return;
574
  const img = e.target;
 
612
  updateImageTransform(img);
613
  }
614
 
 
615
  function replacePanelImage() {
616
  if (!currentlySelectedPanel) return alert("Select a panel first.");
617
  const img = currentlySelectedPanel.querySelector('img');
 
689
 
690
  class EnhancedComicGenerator:
691
  def __init__(self, sid):
 
692
  self.sid = sid
693
  self.user_dir = os.path.join(BASE_USER_DIR, sid)
694
  self.video_path = os.path.join(self.user_dir, 'uploaded.mp4')
 
696
  self.output_dir = os.path.join(self.user_dir, 'output')
697
  self.status_file = os.path.join(self.output_dir, 'status.json')
698
 
 
699
  os.makedirs(self.frames_dir, exist_ok=True)
700
  os.makedirs(self.output_dir, exist_ok=True)
701
 
 
716
  os.remove(os.path.join(self.frames_dir, file))
717
  if os.path.isdir(self.output_dir):
718
  for file in os.listdir(self.output_dir):
719
+ if file != 'status.json':
720
+ try: os.remove(os.path.join(self.output_dir, file))
 
721
  except: pass
 
722
  srt_file = os.path.join(self.user_dir, 'subs.srt')
723
  if os.path.exists(srt_file): os.remove(srt_file)
724
  print(f"[{self.sid}] ✅ Cleanup complete.")
 
726
  def regenerate_frame(self, frame_filename, direction):
727
  try:
728
  if not self.video_fps:
 
729
  cap = cv2.VideoCapture(self.video_path)
730
  self.video_fps = cap.get(cv2.CAP_PROP_FPS)
731
  cap.release()
 
761
  self._enhance_all_images(single_image_path=new_path)
762
  self._enhance_quality_colors(single_image_path=new_path)
763
 
 
764
  if isinstance(frame_to_time[frame_filename], dict):
765
  frame_to_time[frame_filename]['time'] = target_time
766
  else:
 
859
  cap.release()
860
 
861
  self.update_status("Generating subtitles...", 10)
 
 
862
  get_real_subtitles(self.video_path)
863
 
 
864
  if os.path.exists('test1.srt'):
865
  user_srt = os.path.join(self.user_dir, 'subs.srt')
866
  shutil.move('test1.srt', user_srt)
867
  else:
868
+ user_srt = os.path.join(self.user_dir, 'subs.srt')
 
869
 
870
  with open(user_srt, 'r', encoding='utf-8') as f:
871
  all_subs = list(srt.parse(f.read()))
 
876
  raise Exception("Keyframe extraction failed.")
877
 
878
  self.update_status("Cropping black bars...", 45)
879
+ black_x, black_y, _, _ = black_bar_crop()
880
 
881
  self.update_status("Enhancing images...", 50)
882
  self._enhance_all_images()
 
934
  bubble_x, bubble_y = ai_bubble_placer.place_bubble_ai(frame_path, (lip_x, lip_y))
935
  return bubble(bubble_offset_x=bubble_x, bubble_offset_y=bubble_y, lip_x=lip_x, lip_y=lip_y, dialog=dialogue, emotion='normal')
936
  except Exception as e:
 
937
  return bubble(bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, dialog=dialogue, emotion='normal')
938
 
939
  def _create_ai_bubbles_from_moments(self, black_x, black_y):
 
950
  return bubbles
951
 
952
  def _generate_pages(self, bubbles):
 
953
  pages, frame_files = [], sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
954
  num_pages = (len(frame_files) + 3) // 4
955
  for i in range(num_pages):
 
975
 
976
  def _copy_template_files(self):
977
  try:
 
 
978
  with open(os.path.join(self.output_dir, 'page.html'), 'w', encoding='utf-8') as f:
979
+ f.write(INDEX_HTML)
980
  except Exception as e:
981
  print(f"[{self.sid}] Template creation failed: {e}")
982
 
 
990
 
991
  @app.route('/uploader', methods=['POST'])
992
  def upload_file():
993
+ if 'sid' not in session: session['sid'] = uuid.uuid4().hex
 
 
 
 
994
  try:
995
  f = request.files['file']
996
  gen = EnhancedComicGenerator(session['sid'])
 
1012
  @app.route('/comic')
1013
  def view_comic():
1014
  if 'sid' not in session: return "Session expired", 400
 
 
1015
  return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'output'), 'page.html')
1016
 
1017
  @app.route('/output/<path:filename>')
 
1024
  if 'sid' not in session: return "Session expired", 400
1025
  return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'frames'), filename)
1026
 
 
1027
  @app.route('/regenerate_frame', methods=['POST'])
1028
  def regenerate_frame_route():
1029
  if 'sid' not in session: return jsonify({'success': False, 'message': 'Session expired'})
 
1049
  return jsonify(gen.get_frame_at_timestamp(data['filename'], float(data['timestamp'])))
1050
 
1051
  if __name__ == '__main__':
 
1052
  os.makedirs(BASE_USER_DIR, exist_ok=True)
1053
  port = int(os.getenv("PORT", 7860))
1054
  print(f"🚀 Starting Multi-User Comic Generator on port {port}")