tester343 commited on
Commit
d828036
·
verified ·
1 Parent(s): 2918181

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +111 -93
app_enhanced.py CHANGED
@@ -6,7 +6,7 @@ import shutil
6
  import json
7
  import traceback
8
  from concurrent.futures import ThreadPoolExecutor
9
- from flask import Flask, render_template, request, jsonify, send_from_directory, send_file, session
10
 
11
  # --- 1. CORE DEPENDENCY CHECKS ---
12
  try:
@@ -16,17 +16,15 @@ try:
16
  import srt
17
  except ImportError as e:
18
  print(f"❌ CRITICAL ERROR: Missing python library. {e}")
19
- # We define dummy variables to let the app start, but generation will fail with a clear message
20
  cv2 = None
21
  np = None
22
  Image = None
23
  srt = None
24
 
25
- # --- 2. BACKEND MODULE IMPORT WITH FALLBACKS ---
26
- # This ensures the app doesn't crash if a specific backend file is missing
27
- def dummy_function(*args, **kwargs):
28
- print("⚠️ Warning: Function not loaded correctly.")
29
- return 0, 0, None, None
30
 
31
  try:
32
  from backend.keyframes.keyframes import black_bar_crop
@@ -54,31 +52,27 @@ except Exception:
54
  def __init__(self, panels, bubbles): self.panels, self.bubbles = panels, bubbles
55
 
56
  try:
57
- from backend.ai_enhanced_core import image_processor, face_detector
58
  from backend.ai_bubble_placement import ai_bubble_placer
59
  from backend.subtitles.subs_real import get_real_subtitles
60
- except Exception as e:
61
- print(f"⚠️ Backend Import Error: {e}")
62
- # Define dummy get_real_subtitles to prevent NameError
63
- def get_real_subtitles(video_path):
64
- raise Exception("Subtitle module failed to load. Check backend/subtitles/subs_real.py")
65
-
66
- # Define dummy face detector
67
  class DummyDetector:
68
  def detect_faces(self, p): return []
69
  def get_lip_position(self, p, f): return -1, -1
70
  face_detector = DummyDetector()
71
-
72
- # Define dummy bubble placer
73
  class DummyPlacer:
74
  def place_bubble_ai(self, p, l): return 50, 20
75
  ai_bubble_placer = DummyPlacer()
76
 
 
77
  # --- FLASK APP SETUP ---
78
  app = Flask(__name__)
79
- app.secret_key = "HF_SPACE_SECRET_KEY_555"
80
  BASE_USER_DIR = "userdata"
81
 
 
82
  INDEX_HTML = '''
83
  <!DOCTYPE html>
84
  <html lang="en">
@@ -99,31 +93,32 @@ INDEX_HTML = '''
99
  @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
100
 
101
  /* COMIC STYLES */
102
- .comic-page { background: white; width: 600px; height: 400px; position: relative; overflow: hidden; border: 2px solid #000; margin: 0 auto 20px; }
103
  .comic-grid { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 10px; width: 100%; height: 100%; padding: 10px; box-sizing: border-box; }
104
  .panel { position: relative; overflow: hidden; border: 2px solid #000; background: #eee; }
105
  .panel img { width: 100%; height: 100%; object-fit: cover; }
106
 
107
- /* SPEECH BUBBLE - EXPORT SAFE GRADIENT */
108
  .speech-bubble.speech {
109
- --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
110
- --c: var(--bubble-fill-color, #4ECDC4);
111
- background: var(--c); color: var(--bubble-text-color, #fff); padding: 1em; position: absolute;
112
  border-radius: var(--r) var(--r) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) / var(--r);
113
- font-family: 'Comic Neue', cursive; font-weight: bold; font-size: 13px; text-align: center;
114
- min-width: 60px; min-height: 40px; display: flex; align-items: center; justify-content: center;
115
  cursor: move; z-index: 10;
116
  }
 
117
  .speech-bubble.speech:before {
118
  content: ""; position: absolute; width: var(--b); height: var(--h);
119
  background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
120
  border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
 
121
  }
122
- .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))); }
123
 
124
- .speech-bubble.selected { outline: 2px dashed #333; }
125
  .edit-controls { position: fixed; bottom: 20px; right: 20px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.2); width: 200px; }
126
- .edit-controls button { width: 100%; margin-top: 5px; padding: 8px; cursor: pointer; }
 
127
  </style>
128
  </head>
129
  <body>
@@ -132,7 +127,7 @@ INDEX_HTML = '''
132
  <h1>🎬 Comic Generator</h1>
133
  <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fname').innerText=this.files[0].name">
134
  <label for="file-upload" class="file-label">Choose Video</label>
135
- <span id="fname">No file</span>
136
  <button class="submit-btn" onclick="upload()">Generate</button>
137
  <div id="loading" style="display:none;">
138
  <div class="loader"></div>
@@ -144,13 +139,31 @@ INDEX_HTML = '''
144
  <div id="editor-container">
145
  <div id="comic-pages"></div>
146
  <div class="edit-controls">
147
- <h4>Editor</h4>
148
  <button onclick="exportToPng()">💾 Export PNG</button>
 
149
  </div>
150
  </div>
151
 
152
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  let interval;
 
154
  async function upload() {
155
  const file = document.getElementById('file-upload').files[0];
156
  if(!file) return alert("Select file");
@@ -161,13 +174,14 @@ INDEX_HTML = '''
161
  document.querySelector('.upload-box').style.display='none';
162
  document.getElementById('loading').style.display='block';
163
 
164
- const res = await fetch('/uploader', {method:'POST', body:fd});
 
165
  if(res.ok) interval = setInterval(checkStatus, 2000);
166
  else { alert("Upload failed"); location.reload(); }
167
  }
168
 
169
  async function checkStatus() {
170
- const res = await fetch('/status');
171
  const data = await res.json();
172
  document.getElementById('status').innerText = data.message;
173
 
@@ -184,8 +198,10 @@ INDEX_HTML = '''
184
  }
185
 
186
  function loadComic() {
187
- fetch('output/pages.json').then(r=>r.json()).then(data => {
 
188
  const c = document.getElementById('comic-pages');
 
189
  data.forEach((p, i) => {
190
  const div = document.createElement('div');
191
  div.className = 'comic-page';
@@ -197,15 +213,14 @@ INDEX_HTML = '''
197
  const pDiv = document.createElement('div');
198
  pDiv.className = 'panel';
199
  const img = document.createElement('img');
200
- img.src = 'frames/' + pan.image;
 
201
  pDiv.appendChild(img);
202
 
203
- // Add bubble
204
  if(p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) {
205
  const b = document.createElement('div');
206
- b.className = 'speech-bubble speech tail-bottom';
207
  b.innerText = p.bubbles[j].dialog;
208
- // Safe defaults
209
  b.style.left = (p.bubbles[j].bubble_offset_x || 50) + 'px';
210
  b.style.top = (p.bubbles[j].bubble_offset_y || 20) + 'px';
211
  pDiv.appendChild(b);
@@ -216,13 +231,12 @@ INDEX_HTML = '''
216
  div.appendChild(grid);
217
  c.appendChild(div);
218
  });
219
- });
220
  }
221
 
222
  function makeInteractive(el) {
223
  el.onmousedown = function(e) {
224
  e.stopPropagation();
225
- // Simple drag logic
226
  const offX = e.clientX - el.offsetLeft;
227
  const offY = e.clientY - el.offsetTop;
228
  document.onmousemove = function(ev) {
@@ -235,12 +249,14 @@ INDEX_HTML = '''
235
 
236
  async function exportToPng() {
237
  const pages = document.querySelectorAll('.comic-page');
238
- for(let p of pages) {
239
- const url = await htmlToImage.toPng(p, {pixelRatio: 2});
240
- const a = document.createElement('a');
241
- a.download = 'comic.png';
242
- a.href = url;
243
- a.click();
 
 
244
  }
245
  }
246
  </script>
@@ -268,37 +284,38 @@ class EnhancedComicGenerator:
268
  json.dump({'message': message, 'progress': progress}, f)
269
  except: pass
270
 
 
 
 
 
 
271
  def generate_comic(self):
272
  try:
273
- if cv2 is None: raise Exception("OpenCV not installed on server.")
274
 
275
  self.update_status("Processing Video...", 10)
276
  cap = cv2.VideoCapture(self.video_path)
277
- if not cap.isOpened(): raise Exception("Invalid Video File")
278
 
279
- self.video_fps = cap.get(cv2.CAP_PROP_FPS)
280
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
281
- duration = total_frames / self.video_fps
282
  cap.release()
283
 
284
- # --- GENERATE SUBTITLES ---
285
  self.update_status("Extracting Dialogue...", 30)
286
  try:
287
- get_real_subtitles(self.video_path) # Generates 'test1.srt' in cwd usually
288
- # Move it to user dir
289
  if os.path.exists('test1.srt'):
290
  shutil.move('test1.srt', os.path.join(self.user_dir, 'subs.srt'))
291
- except Exception as e:
292
- print(f"Subtitle Error: {e}")
293
- # Create dummy srt if failed
294
  with open(os.path.join(self.user_dir, 'subs.srt'), 'w') as f:
295
- f.write("1\n00:00:01,000 --> 00:00:04,000\nHello World!\n")
296
 
297
- # --- EXTRACT FRAMES ---
298
- self.update_status("Creating Panels...", 50)
299
  cap = cv2.VideoCapture(self.video_path)
300
 
301
- # Parse SRT
302
  subs_path = os.path.join(self.user_dir, 'subs.srt')
303
  with open(subs_path, 'r', encoding='utf-8') as f:
304
  subs = list(srt.parse(f.read()))
@@ -306,84 +323,85 @@ class EnhancedComicGenerator:
306
  frame_files = []
307
  bubbles = []
308
 
309
- # Limit to 12 frames for speed
310
- step = max(1, len(subs) // 12)
311
 
312
- for i, sub in enumerate(subs[::step]):
313
- mid_time = (sub.start.total_seconds() + sub.end.total_seconds()) / 2
314
- cap.set(cv2.CAP_PROP_POS_MSEC, mid_time * 1000)
315
  ret, frame = cap.read()
316
  if ret:
317
  fname = f"frame_{i}.png"
318
  cv2.imwrite(os.path.join(self.frames_dir, fname), frame)
319
  frame_files.append(fname)
320
- # Simple bubble placement
321
  bubbles.append(bubble(dialog=sub.content, bubble_offset_x=50, bubble_offset_y=20))
322
  cap.release()
323
-
324
- # --- GENERATE PAGES ---
325
- self.update_status("Assembling...", 80)
326
  pages_data = []
327
- # Create 4-panel pages
328
  for i in range(0, len(frame_files), 4):
329
- batch_files = frame_files[i:i+4]
330
- batch_bubbles = bubbles[i:i+4]
331
-
332
- panels = [{'image': f} for f in batch_files]
333
- b_data = [b.__dict__ if hasattr(b, '__dict__') else b for b in batch_bubbles]
334
-
335
  pages_data.append({'panels': panels, 'bubbles': b_data})
336
 
337
  with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
338
  json.dump(pages_data, f)
339
-
340
- # Save HTML for direct viewing
341
- with open(os.path.join(self.output_dir, 'page.html'), 'w') as f:
342
- f.write(INDEX_HTML)
343
-
344
  self.update_status("Done!", 100)
345
 
346
  except Exception as e:
347
  traceback.print_exc()
348
- self.update_status(f"Failed: {str(e)}", -1)
 
 
 
349
 
350
  @app.route('/')
351
  def index():
352
- if 'sid' not in session: session['sid'] = uuid.uuid4().hex
353
  return INDEX_HTML
354
 
355
  @app.route('/uploader', methods=['POST'])
356
  def upload():
357
- if 'sid' not in session: session['sid'] = uuid.uuid4().hex
358
- sid = session['sid']
 
359
  if 'file' not in request.files: return "No file", 400
360
 
361
  gen = EnhancedComicGenerator(sid)
 
362
  request.files['file'].save(gen.video_path)
363
 
364
- # Initial status
365
  gen.update_status("Starting...", 5)
366
-
367
  threading.Thread(target=gen.generate_comic).start()
368
  return jsonify({'success': True})
369
 
370
  @app.route('/status')
371
  def get_status():
372
- if 'sid' not in session: return jsonify({'progress': -1, 'message': "Session Lost"})
373
- sid = session['sid']
 
374
  status_path = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
375
  if os.path.exists(status_path):
376
  return send_file(status_path)
377
- return jsonify({'progress': 0, 'message': "Waiting..."})
378
 
379
  @app.route('/output/<path:filename>')
380
  def get_output(filename):
381
- return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'output'), filename)
 
 
382
 
383
  @app.route('/frames/<path:filename>')
384
  def get_frame(filename):
385
- return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'frames'), filename)
 
 
386
 
387
  if __name__ == '__main__':
388
  os.makedirs(BASE_USER_DIR, exist_ok=True)
389
- app.run(host='0.0.0.0', port=7860)
 
 
6
  import json
7
  import traceback
8
  from concurrent.futures import ThreadPoolExecutor
9
+ from flask import Flask, render_template, request, jsonify, send_from_directory, send_file
10
 
11
  # --- 1. CORE DEPENDENCY CHECKS ---
12
  try:
 
16
  import srt
17
  except ImportError as e:
18
  print(f"❌ CRITICAL ERROR: Missing python library. {e}")
19
+ # Dummy definitions to allow app to start and show error in UI
20
  cv2 = None
21
  np = None
22
  Image = None
23
  srt = None
24
 
25
+ # --- 2. BACKEND MODULE IMPORTS (WITH DUMMY FALLBACKS) ---
26
+ # This allows the app to run even if complex backend modules fail to load
27
+ def dummy_function(*args, **kwargs): return 0, 0, None, None
 
 
28
 
29
  try:
30
  from backend.keyframes.keyframes import black_bar_crop
 
52
  def __init__(self, panels, bubbles): self.panels, self.bubbles = panels, bubbles
53
 
54
  try:
55
+ from backend.ai_enhanced_core import image_processor, comic_styler, face_detector, layout_optimizer
56
  from backend.ai_bubble_placement import ai_bubble_placer
57
  from backend.subtitles.subs_real import get_real_subtitles
58
+ from backend.keyframes.keyframes_simple import generate_keyframes_simple
59
+ except Exception:
60
+ # If backend fails, define minimal dummies to prevent crash
61
+ def get_real_subtitles(v): pass
 
 
 
62
  class DummyDetector:
63
  def detect_faces(self, p): return []
64
  def get_lip_position(self, p, f): return -1, -1
65
  face_detector = DummyDetector()
 
 
66
  class DummyPlacer:
67
  def place_bubble_ai(self, p, l): return 50, 20
68
  ai_bubble_placer = DummyPlacer()
69
 
70
+
71
  # --- FLASK APP SETUP ---
72
  app = Flask(__name__)
 
73
  BASE_USER_DIR = "userdata"
74
 
75
+ # --- HTML ---
76
  INDEX_HTML = '''
77
  <!DOCTYPE html>
78
  <html lang="en">
 
93
  @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
94
 
95
  /* COMIC STYLES */
96
+ .comic-page { background: white; width: 600px; height: 400px; position: relative; overflow: hidden; border: 2px solid #000; margin: 0 auto 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
97
  .comic-grid { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 10px; width: 100%; height: 100%; padding: 10px; box-sizing: border-box; }
98
  .panel { position: relative; overflow: hidden; border: 2px solid #000; background: #eee; }
99
  .panel img { width: 100%; height: 100%; object-fit: cover; }
100
 
101
+ /* EXACT SHARK FIN SPEECH BUBBLE CSS */
102
  .speech-bubble.speech {
103
+ --b: 3em; --h: 1.8em; --t: 0.6; --p: 50%; --r: 1.2em;
104
+ --c: #4ECDC4;
105
+ background: var(--c); color: #fff; padding: 1em; position: absolute;
106
  border-radius: var(--r) var(--r) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) / var(--r);
107
+ font-family: 'Comic Neue', cursive; font-weight: bold; font-size: 14px; text-align: center;
108
+ min-width: 80px; min-height: 40px; display: flex; align-items: center; justify-content: center;
109
  cursor: move; z-index: 10;
110
  }
111
+ /* Export-Safe Gradient instead of Mask */
112
  .speech-bubble.speech:before {
113
  content: ""; position: absolute; width: var(--b); height: var(--h);
114
  background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
115
  border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
116
+ top: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b)));
117
  }
 
118
 
 
119
  .edit-controls { position: fixed; bottom: 20px; right: 20px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.2); width: 200px; }
120
+ .edit-controls button { width: 100%; margin-top: 5px; padding: 8px; cursor: pointer; background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; }
121
+ .edit-controls button:hover { background: #e0e0e0; }
122
  </style>
123
  </head>
124
  <body>
 
127
  <h1>🎬 Comic Generator</h1>
128
  <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fname').innerText=this.files[0].name">
129
  <label for="file-upload" class="file-label">Choose Video</label>
130
+ <span id="fname">No file selected</span>
131
  <button class="submit-btn" onclick="upload()">Generate</button>
132
  <div id="loading" style="display:none;">
133
  <div class="loader"></div>
 
139
  <div id="editor-container">
140
  <div id="comic-pages"></div>
141
  <div class="edit-controls">
142
+ <h4>Editor Tools</h4>
143
  <button onclick="exportToPng()">💾 Export PNG</button>
144
+ <button onclick="location.reload()" style="color:red;">↺ Start Over</button>
145
  </div>
146
  </div>
147
 
148
  <script>
149
+ // CLIENT-SIDE SESSION ID GENERATION (Fixes "Session Lost" on Hugging Face)
150
+ function generateUUID() {
151
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
152
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
153
+ return v.toString(16);
154
+ });
155
+ }
156
+
157
+ // Get or create SID
158
+ let sid = localStorage.getItem('comic_sid');
159
+ if(!sid) {
160
+ sid = generateUUID();
161
+ localStorage.setItem('comic_sid', sid);
162
+ }
163
+ console.log("Using Session ID:", sid);
164
+
165
  let interval;
166
+
167
  async function upload() {
168
  const file = document.getElementById('file-upload').files[0];
169
  if(!file) return alert("Select file");
 
174
  document.querySelector('.upload-box').style.display='none';
175
  document.getElementById('loading').style.display='block';
176
 
177
+ // Pass SID in URL
178
+ const res = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
179
  if(res.ok) interval = setInterval(checkStatus, 2000);
180
  else { alert("Upload failed"); location.reload(); }
181
  }
182
 
183
  async function checkStatus() {
184
+ const res = await fetch(`/status?sid=${sid}`);
185
  const data = await res.json();
186
  document.getElementById('status').innerText = data.message;
187
 
 
198
  }
199
 
200
  function loadComic() {
201
+ // Fetch pages.json using SID
202
+ fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
203
  const c = document.getElementById('comic-pages');
204
+ c.innerHTML = ''; // clear
205
  data.forEach((p, i) => {
206
  const div = document.createElement('div');
207
  div.className = 'comic-page';
 
213
  const pDiv = document.createElement('div');
214
  pDiv.className = 'panel';
215
  const img = document.createElement('img');
216
+ // Fetch image using SID
217
+ img.src = `/frames/${pan.image}?sid=${sid}`;
218
  pDiv.appendChild(img);
219
 
 
220
  if(p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) {
221
  const b = document.createElement('div');
222
+ b.className = 'speech-bubble speech';
223
  b.innerText = p.bubbles[j].dialog;
 
224
  b.style.left = (p.bubbles[j].bubble_offset_x || 50) + 'px';
225
  b.style.top = (p.bubbles[j].bubble_offset_y || 20) + 'px';
226
  pDiv.appendChild(b);
 
231
  div.appendChild(grid);
232
  c.appendChild(div);
233
  });
234
+ }).catch(e => console.error(e));
235
  }
236
 
237
  function makeInteractive(el) {
238
  el.onmousedown = function(e) {
239
  e.stopPropagation();
 
240
  const offX = e.clientX - el.offsetLeft;
241
  const offY = e.clientY - el.offsetTop;
242
  document.onmousemove = function(ev) {
 
249
 
250
  async function exportToPng() {
251
  const pages = document.querySelectorAll('.comic-page');
252
+ for(let i=0; i<pages.length; i++) {
253
+ try {
254
+ const url = await htmlToImage.toPng(pages[i], {pixelRatio: 3});
255
+ const a = document.createElement('a');
256
+ a.download = `comic-page-${i+1}.png`;
257
+ a.href = url;
258
+ a.click();
259
+ } catch(e) { console.error(e); alert("Export failed"); }
260
  }
261
  }
262
  </script>
 
284
  json.dump({'message': message, 'progress': progress}, f)
285
  except: pass
286
 
287
+ def cleanup(self):
288
+ # Simple cleanup
289
+ if os.path.exists(self.frames_dir): shutil.rmtree(self.frames_dir)
290
+ os.makedirs(self.frames_dir, exist_ok=True)
291
+
292
  def generate_comic(self):
293
  try:
294
+ if cv2 is None: raise Exception("OpenCV missing on server.")
295
 
296
  self.update_status("Processing Video...", 10)
297
  cap = cv2.VideoCapture(self.video_path)
298
+ if not cap.isOpened(): raise Exception("Invalid Video")
299
 
300
+ self.video_fps = cap.get(cv2.CAP_PROP_FPS) or 25
 
 
301
  cap.release()
302
 
303
+ # 1. Subtitles
304
  self.update_status("Extracting Dialogue...", 30)
305
  try:
306
+ get_real_subtitles(self.video_path) # Creates test1.srt
307
+ # Move to user dir if created in root
308
  if os.path.exists('test1.srt'):
309
  shutil.move('test1.srt', os.path.join(self.user_dir, 'subs.srt'))
310
+ except:
311
+ # Fallback dummy sub
 
312
  with open(os.path.join(self.user_dir, 'subs.srt'), 'w') as f:
313
+ f.write("1\n00:00:01,000 --> 00:00:04,000\nSample Text\n")
314
 
315
+ # 2. Extract Frames
316
+ self.update_status("Generating Panels...", 50)
317
  cap = cv2.VideoCapture(self.video_path)
318
 
 
319
  subs_path = os.path.join(self.user_dir, 'subs.srt')
320
  with open(subs_path, 'r', encoding='utf-8') as f:
321
  subs = list(srt.parse(f.read()))
 
323
  frame_files = []
324
  bubbles = []
325
 
326
+ # Limit frames to avoid timeout
327
+ limit_subs = subs[:12]
328
 
329
+ for i, sub in enumerate(limit_subs):
330
+ mid = (sub.start.total_seconds() + sub.end.total_seconds()) / 2
331
+ cap.set(cv2.CAP_PROP_POS_MSEC, mid * 1000)
332
  ret, frame = cap.read()
333
  if ret:
334
  fname = f"frame_{i}.png"
335
  cv2.imwrite(os.path.join(self.frames_dir, fname), frame)
336
  frame_files.append(fname)
 
337
  bubbles.append(bubble(dialog=sub.content, bubble_offset_x=50, bubble_offset_y=20))
338
  cap.release()
339
+
340
+ # 3. Assemble
341
+ self.update_status("Finalizing...", 80)
342
  pages_data = []
 
343
  for i in range(0, len(frame_files), 4):
344
+ batch_f = frame_files[i:i+4]
345
+ batch_b = bubbles[i:i+4]
346
+ panels = [{'image': f} for f in batch_f]
347
+ # safe object conversion
348
+ b_data = [b.__dict__ if hasattr(b, '__dict__') else b for b in batch_b]
 
349
  pages_data.append({'panels': panels, 'bubbles': b_data})
350
 
351
  with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
352
  json.dump(pages_data, f)
353
+
 
 
 
 
354
  self.update_status("Done!", 100)
355
 
356
  except Exception as e:
357
  traceback.print_exc()
358
+ self.update_status(f"Error: {str(e)}", -1)
359
+
360
+
361
+ # --- ROUTES (SID REQUIRED IN QUERY PARAMS) ---
362
 
363
  @app.route('/')
364
  def index():
 
365
  return INDEX_HTML
366
 
367
  @app.route('/uploader', methods=['POST'])
368
  def upload():
369
+ sid = request.args.get('sid')
370
+ if not sid: return "Missing SID", 400
371
+
372
  if 'file' not in request.files: return "No file", 400
373
 
374
  gen = EnhancedComicGenerator(sid)
375
+ gen.cleanup() # Clean previous run
376
  request.files['file'].save(gen.video_path)
377
 
 
378
  gen.update_status("Starting...", 5)
 
379
  threading.Thread(target=gen.generate_comic).start()
380
  return jsonify({'success': True})
381
 
382
  @app.route('/status')
383
  def get_status():
384
+ sid = request.args.get('sid')
385
+ if not sid: return jsonify({'message': 'No SID', 'progress': -1})
386
+
387
  status_path = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
388
  if os.path.exists(status_path):
389
  return send_file(status_path)
390
+ return jsonify({'message': 'Waiting...', 'progress': 0})
391
 
392
  @app.route('/output/<path:filename>')
393
  def get_output(filename):
394
+ sid = request.args.get('sid')
395
+ if not sid: return "No SID", 400
396
+ return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
397
 
398
  @app.route('/frames/<path:filename>')
399
  def get_frame(filename):
400
+ sid = request.args.get('sid')
401
+ if not sid: return "No SID", 400
402
+ return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
403
 
404
  if __name__ == '__main__':
405
  os.makedirs(BASE_USER_DIR, exist_ok=True)
406
+ port = int(os.getenv("PORT", 7860))
407
+ app.run(host='0.0.0.0', port=port)