tester343 commited on
Commit
42bdbd7
·
verified ·
1 Parent(s): 5ae4e29

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +198 -284
app_enhanced.py CHANGED
@@ -1,35 +1,49 @@
 
1
  import os
2
  import time
3
  import threading
4
- import uuid
5
- import shutil
6
  import json
7
  import traceback
8
  import logging
9
  import string
10
  import random
11
- from concurrent.futures import ThreadPoolExecutor
12
- from flask import Flask, render_template, request, jsonify, send_from_directory, send_file
13
 
14
- # ==========================================
15
- # 🚀 ZEROGPU IMPORTS & SETUP
16
- # ==========================================
17
- import spaces # Must be at the top level
18
- import torch # Import torch explicitly
 
 
 
 
19
 
20
- # We define GPU functions GLOBALLY so the scanner finds them.
21
- # We pass only necessary paths/data to avoid pickling the whole class.
 
 
 
 
 
 
 
22
 
23
- @spaces.GPU(duration=120)
 
 
 
 
 
 
24
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path):
25
- """
26
- Standalone GPU function to handle the heavy lifting.
27
- """
28
  print(f"🚀 GPU Task Started for {video_path}")
29
 
30
- # 1. Imports inside to ensure they run in the GPU environment
31
  import cv2
32
  import srt
 
33
  from backend.keyframes.keyframes import black_bar_crop
34
  from backend.simple_color_enhancer import SimpleColorEnhancer
35
  from backend.quality_color_enhancer import QualityColorEnhancer
@@ -38,202 +52,132 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path):
38
  from backend.ai_enhanced_core import face_detector
39
  from backend.class_def import bubble, panel, Page
40
 
41
- # 2. Analyze Video
42
  cap = cv2.VideoCapture(video_path)
43
- if not cap.isOpened():
44
- raise Exception("Cannot open video")
45
- video_fps = cap.get(cv2.CAP_PROP_FPS) or 25
46
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
47
- duration = total_frames / video_fps
48
  cap.release()
49
 
50
- # 3. Subtitles
51
  user_srt = os.path.join(user_dir, 'subs.srt')
52
  try:
53
  get_real_subtitles(video_path)
54
  if os.path.exists('test1.srt'):
55
  shutil.move('test1.srt', user_srt)
56
  except Exception as e:
57
- print(f"⚠️ Subtitle generation failed: {e}. Creating fallback.")
58
- with open(user_srt, 'w') as f:
59
- f.write("1\n00:00:01,000 --> 00:00:04,000\nHello\n")
60
 
61
  with open(user_srt, 'r', encoding='utf-8') as f:
62
  all_subs = list(srt.parse(f.read()))
63
 
64
- key_moments = [{
65
- 'index': s.index,
66
- 'text': s.content,
67
- 'start': s.start.total_seconds(),
68
- 'end': s.end.total_seconds()
69
- } for s in all_subs]
70
-
71
- # 4. Extract Keyframes
72
  key_moments.sort(key=lambda x: x['start'])
 
 
73
  frame_metadata = {}
74
- frame_count = 0
75
  cap = cv2.VideoCapture(video_path)
 
76
 
77
- max_frames = 48
78
- for i, moment in enumerate(key_moments[:max_frames]):
79
- frame_time = (moment['start'] + moment['end']) / 2
80
- if frame_time > duration: continue
81
 
82
- frame_number = int(frame_time * video_fps)
83
- cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
84
  ret, frame = cap.read()
85
-
86
  if ret:
87
- fname = f"frame_{frame_count:04d}.png"
88
- fpath = os.path.join(frames_dir, fname)
89
- cv2.imwrite(fpath, frame)
90
- frame_metadata[fname] = {
91
- 'time': frame_time,
92
- 'dialogue': moment['text'],
93
- 'start': moment['start'],
94
- 'end': moment['end']
95
- }
96
- frame_count += 1
97
  cap.release()
98
 
99
- with open(metadata_path, 'w') as f:
100
- json.dump(frame_metadata, f, indent=2)
101
-
102
- # 5. Enhance & Crop
103
- try:
104
- black_bar_crop() # Assuming it processes files in place or globally configured
105
- except: pass
106
 
107
- simple_enhancer = SimpleColorEnhancer()
108
- quality_enhancer = QualityColorEnhancer()
 
109
 
 
 
110
  frame_files = sorted([f for f in os.listdir(frames_dir) if f.endswith('.png')])
 
111
  for f in frame_files:
112
- full_p = os.path.join(frames_dir, f)
113
- simple_enhancer.enhance_single(full_p)
114
- quality_enhancer.enhance_single(full_p)
115
 
116
- # 6. Bubbles & Layout
117
  bubbles_list = []
118
  for f in frame_files:
119
- full_p = os.path.join(frames_dir, f)
120
- meta = frame_metadata.get(f, {})
121
- dialogue = meta.get('dialogue', '')
122
-
123
  try:
124
- faces = face_detector.detect_faces(full_p)
125
- lip = face_detector.get_lip_position(full_p, faces[0]) if faces else (-1, -1)
126
- bx, by = ai_bubble_placer.place_bubble_ai(full_p, lip)
127
  bubbles_list.append(bubble(dialog=dialogue, bubble_offset_x=bx, bubble_offset_y=by, lip_x=lip[0], lip_y=lip[1]))
128
  except:
129
  bubbles_list.append(bubble(dialog=dialogue))
130
 
131
- # 7. Generate Pages
132
  try:
133
  from backend.fixed_12_pages_800x1080 import generate_12_pages_800x1080
134
  pages = generate_12_pages_800x1080(frame_files, bubbles_list)
135
  except:
136
- # Fallback layout
137
  pages = []
138
- num_pages = (len(frame_files) + 3) // 4
139
- for i in range(num_pages):
140
- start, end = i * 4, (i + 1) * 4
141
- p_panels = [panel(image=f) for f in frame_files[start:end]]
142
- p_bubbles = bubbles_list[start:end]
143
- if p_panels:
144
- pages.append(Page(panels=p_panels, bubbles=p_bubbles))
145
-
146
- # Return simple data structure to main thread
147
- pages_data = []
148
- for page in pages:
149
- panels = [p.__dict__ if hasattr(p, '__dict__') else p for p in page.panels]
150
- bubbles_data = [b.__dict__ if hasattr(b, '__dict__') else b for b in page.bubbles]
151
- pages_data.append({'panels': panels, 'bubbles': bubbles_data})
152
 
153
- return pages_data
154
 
155
  @spaces.GPU
156
- def regenerate_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
157
  import cv2
158
- from backend.simple_color_enhancer import SimpleColorEnhancer
159
- from backend.quality_color_enhancer import QualityColorEnhancer
160
-
161
  if not os.path.exists(metadata_path): return None
162
  with open(metadata_path, 'r') as f: meta = json.load(f)
163
  if fname not in meta: return None
164
 
165
- current_data = meta[fname]
166
- curr_time = current_data['time'] if isinstance(current_data, dict) else current_data
167
-
168
  cap = cv2.VideoCapture(video_path)
169
  fps = cap.get(cv2.CAP_PROP_FPS) or 25
170
- offset = (1.0 / fps) * (1 if direction == 'forward' else -1)
171
- new_time = max(0, curr_time + offset)
172
 
173
- cap.set(cv2.CAP_PROP_POS_MSEC, new_time * 1000)
174
  ret, frame = cap.read()
175
  cap.release()
176
 
177
  if ret:
178
  path = os.path.join(frames_dir, fname)
179
  cv2.imwrite(path, frame)
 
 
180
  SimpleColorEnhancer().enhance_single(path)
181
- QualityColorEnhancer().enhance_single(path)
182
 
183
- if isinstance(meta[fname], dict): meta[fname]['time'] = new_time
184
- else: meta[fname] = new_time
185
- with open(metadata_path, 'w') as f: json.dump(meta, f, indent=2)
186
- return new_time
187
  return None
188
 
189
- @spaces.GPU
190
- def get_frame_at_ts_gpu(video_path, frames_dir, metadata_path, fname, ts):
191
- import cv2
192
- from backend.simple_color_enhancer import SimpleColorEnhancer
193
- from backend.quality_color_enhancer import QualityColorEnhancer
194
-
195
- cap = cv2.VideoCapture(video_path)
196
- cap.set(cv2.CAP_PROP_POS_MSEC, float(ts) * 1000)
197
- ret, frame = cap.read()
198
- cap.release()
199
-
200
- if ret:
201
- path = os.path.join(frames_dir, fname)
202
- cv2.imwrite(path, frame)
203
- SimpleColorEnhancer().enhance_single(path)
204
- QualityColorEnhancer().enhance_single(path)
205
-
206
- if os.path.exists(metadata_path):
207
- with open(metadata_path, 'r') as f: meta = json.load(f)
208
- if fname in meta:
209
- if isinstance(meta[fname], dict): meta[fname]['time'] = float(ts)
210
- else: meta[fname] = float(ts)
211
- with open(metadata_path, 'w') as f: json.dump(meta, f, indent=2)
212
- return True
213
- return False
214
-
215
- # ==========================================
216
- # ⚙️ CONFIG & DEPENDENCIES
217
- # ==========================================
218
- logging.basicConfig(level=logging.INFO)
219
- logger = logging.getLogger(__name__)
220
-
221
- # --- FLASK APP SETUP ---
222
- app = Flask(__name__)
223
- BASE_USER_DIR = "userdata"
224
- SAVED_COMICS_DIR = "saved_comics"
225
-
226
- os.makedirs(BASE_USER_DIR, exist_ok=True)
227
- os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
228
-
229
- def generate_save_code(length=8):
230
- chars = string.ascii_uppercase + string.digits
231
- while True:
232
- code = ''.join(random.choices(chars, k=length))
233
- if not os.path.exists(os.path.join(SAVED_COMICS_DIR, code)):
234
- return code
235
-
236
- # --- 3. ENHANCED COMIC GENERATOR CLASS (Wrapper) ---
237
  class EnhancedComicGenerator:
238
  def __init__(self, sid):
239
  self.sid = sid
@@ -241,179 +185,149 @@ class EnhancedComicGenerator:
241
  self.video_path = os.path.join(self.user_dir, 'uploaded.mp4')
242
  self.frames_dir = os.path.join(self.user_dir, 'frames')
243
  self.output_dir = os.path.join(self.user_dir, 'output')
244
- self.status_file = os.path.join(self.output_dir, 'status.json')
245
- self.metadata_path = os.path.join(self.frames_dir, 'frame_metadata.json')
246
  os.makedirs(self.frames_dir, exist_ok=True)
247
  os.makedirs(self.output_dir, exist_ok=True)
 
248
 
249
- def update_status(self, message, progress):
250
- try:
251
- with open(self.status_file, 'w') as f:
252
- json.dump({'message': message, 'progress': progress}, f)
253
- except: pass
254
-
255
- def cleanup_previous_run(self):
256
- if os.path.exists(self.frames_dir):
257
- for f in os.listdir(self.frames_dir):
258
- try: os.remove(os.path.join(self.frames_dir, f))
259
- except: pass
260
- if os.path.exists(self.output_dir):
261
- for f in os.listdir(self.output_dir):
262
- if f != 'status.json':
263
- try: os.remove(os.path.join(self.output_dir, f))
264
- except: pass
265
 
266
- def run_process(self):
267
  try:
268
- self.update_status("Starting GPU Process...", 10)
269
-
270
- # CALL GLOBAL GPU FUNCTION
271
- pages_data = generate_comic_gpu(
272
- self.video_path,
273
- self.user_dir,
274
- self.frames_dir,
275
- self.metadata_path
276
- )
277
-
278
- # SAVE RESULTS
279
- with open(os.path.join(self.output_dir, 'pages.json'), 'w', encoding='utf-8') as f:
280
- json.dump(pages_data, f, indent=2)
281
-
282
- self.update_status("Complete!", 100)
283
- print("✅ Comic generated successfully")
284
  except Exception as e:
285
  traceback.print_exc()
286
- self.update_status(f"Error: {str(e)}", -1)
 
 
 
 
287
 
288
- # --- ROUTES ---
 
 
289
  INDEX_HTML = '''
290
  <!DOCTYPE html>
291
  <html lang="en">
292
  <head>
293
- <meta charset="UTF-8">
294
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
295
- <title>Movie to Comic Generator (ZeroGPU)</title>
296
- <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
297
- <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@700&family=Gloria+Hallelujah&family=Lato&display=swap" rel="stylesheet">
298
- <style>
299
- * { box-sizing: border-box; }
300
- body { background-color: #fdf6e3; font-family: 'Lato', sans-serif; color: #3d3d3d; margin: 0; min-height: 100vh; }
301
- #upload-container { display: flex; flex-direction:column; justify-content: center; align-items: center; min-height: 100vh; width: 100%; padding: 20px; }
302
- .upload-box { max-width: 500px; width: 100%; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); text-align: center; }
303
- #editor-container { display: none; padding: 20px; width: 100%; box-sizing: border-box; padding-bottom: 100px; }
304
- h1 { color: #2c3e50; margin-bottom: 20px; font-weight: 600; }
305
- .submit-btn { width: 100%; padding: 15px; background: #e67e22; color: white; border: none; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; transition: 0.2s; }
306
- .file-label { display: block; padding: 15px; background: #2c3e50; color: white; border-radius: 8px; cursor: pointer; font-weight: bold; margin-bottom: 10px; transition:0.2s; }
307
- .comic-page { background: white; width: 600px; height: 400px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); position: relative; overflow: hidden; border: 2px solid #000; padding: 10px; margin: 20px auto;}
308
- .comic-grid { display: grid; grid-template-columns: 285px 285px; grid-template-rows: 185px 185px; gap: 10px; width: 100%; height: 100%; }
309
- .panel { position: relative; overflow: hidden; border: 2px solid #000; background: #eee; cursor: pointer; }
310
- .panel img { width: 100%; height: 100%; object-fit: cover; }
311
- .speech-bubble { position: absolute; display: flex; justify-content: center; align-items: center; width: 150px; height: 80px; z-index: 10; cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold; font-size: 13px; text-align: center; background: #4ECDC4; color: white; border-radius: 20px; padding: 10px;}
312
- .edit-controls { position: fixed; bottom: 20px; right: 20px; width: 260px; background: rgba(44, 62, 80, 0.95); color: white; padding: 15px; border-radius: 8px; }
313
- button { width: 100%; margin-top: 5px; padding: 6px; border-radius: 4px; border: 1px solid #ddd; cursor: pointer; }
314
- </style>
315
  </head>
316
  <body>
317
- <div id="upload-container">
318
- <div class="upload-box">
319
- <h1>🎬 Comic Generator</h1>
320
- <input type="file" id="file-upload" style="display:none" onchange="document.getElementById('fn').innerText=this.files[0].name">
321
- <label for="file-upload" class="file-label">📁 Choose Video File</label>
322
- <span id="fn" style="display:block; margin-bottom:10px;">No file selected</span>
323
- <button class="submit-btn" onclick="upload()">🚀 Generate Comic</button>
324
- <div id="loading" style="display:none; margin-top:20px;">Processing... <span id="prog">0</span>%</div>
325
- </div>
326
- </div>
327
- <div id="editor-container">
328
- <div id="comic-container"></div>
329
- <div class="edit-controls">
330
- <h4>Controls</h4>
331
- <button onclick="saveComic()">💾 Save Comic</button>
332
- <button onclick="location.reload()">🏠 New Comic</button>
333
- </div>
334
- </div>
335
- <script>
336
- let sid = Math.random().toString(36).substring(7);
337
- async function upload() {
338
- const f = document.getElementById('file-upload').files[0];
339
- if(!f) return alert("Select file");
340
- document.getElementById('loading').style.display='block';
341
- const fd = new FormData(); fd.append('file', f);
342
- await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
343
- setInterval(checkStatus, 2000);
344
- }
345
- async function checkStatus() {
346
- const r = await fetch(`/status?sid=${sid}`);
347
- const d = await r.json();
348
- document.getElementById('prog').innerText = d.progress;
349
- if(d.progress >= 100) {
350
- document.getElementById('upload-container').style.display='none';
351
- document.getElementById('editor-container').style.display='block';
352
- loadComic();
353
- }
354
- }
355
- async function loadComic() {
356
- const r = await fetch(`/output/pages.json?sid=${sid}`);
357
- const data = await r.json();
358
- const con = document.getElementById('comic-container');
359
- data.forEach((page, i) => {
360
- const div = document.createElement('div'); div.className='comic-page';
361
- const grid = document.createElement('div'); grid.className='comic-grid';
362
- page.panels.forEach(pan => {
363
- const p = document.createElement('div'); p.className='panel';
364
- p.innerHTML = `<img src="/frames/${pan.image}?sid=${sid}">`;
365
- grid.appendChild(p);
366
- (pan.bubbles||[]).forEach(b => {
367
- const bub = document.createElement('div'); bub.className='speech-bubble';
368
- bub.innerText = b.dialog || "...";
369
- bub.style.left = (b.bubble_offset_x||50)+'px';
370
- bub.style.top = (b.bubble_offset_y||20)+'px';
371
- p.appendChild(bub);
372
- });
373
- });
374
- div.appendChild(grid); con.appendChild(div);
375
  });
376
- }
377
- async function saveComic() { alert("Save not implemented in simple view"); }
378
- </script>
 
 
 
379
  </body>
380
  </html>
381
  '''
382
 
383
  @app.route('/')
384
- def index():
385
- return INDEX_HTML
386
 
387
  @app.route('/uploader', methods=['POST'])
388
  def upload():
389
  sid = request.args.get('sid')
390
- if 'file' not in request.files: return "No file", 400
391
  f = request.files['file']
392
  gen = EnhancedComicGenerator(sid)
393
- gen.cleanup_previous_run()
394
  f.save(gen.video_path)
395
- gen.update_status("Starting...", 5)
396
- threading.Thread(target=gen.run_process).start()
397
  return jsonify({'success': True})
398
 
399
  @app.route('/status')
400
- def get_status():
401
  sid = request.args.get('sid')
402
  path = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
403
  if os.path.exists(path): return send_file(path)
404
- return jsonify({'progress': 0, 'message': "Waiting..."})
405
 
406
  @app.route('/output/<path:filename>')
407
- def get_output(filename):
408
  sid = request.args.get('sid')
409
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
410
 
411
  @app.route('/frames/<path:filename>')
412
- def get_frame(filename):
413
  sid = request.args.get('sid')
414
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
415
 
 
 
 
 
 
 
 
 
416
  if __name__ == '__main__':
417
- port = int(os.getenv("PORT", 7860))
418
- print(f"🚀 Starting on port {port}")
419
- app.run(host='0.0.0.0', port=port)
 
 
1
+ import spaces # <--- MUST BE THE FIRST IMPORT
2
  import os
3
  import time
4
  import threading
 
 
5
  import json
6
  import traceback
7
  import logging
8
  import string
9
  import random
10
+ import shutil
11
+ from flask import Flask, jsonify, request, send_from_directory, send_file
12
 
13
+ # ======================================================
14
+ # 🚀 ZEROGPU DETECTION FUNCTION
15
+ # ======================================================
16
+ # This function exists solely to be detected by Hugging Face
17
+ @spaces.GPU
18
+ def gpu_warmup():
19
+ import torch
20
+ print(f"✅ ZeroGPU Warmup: CUDA Available: {torch.cuda.is_available()}")
21
+ return True
22
 
23
+ # ======================================================
24
+ # 🔧 CONFIGURATION
25
+ # ======================================================
26
+ logging.basicConfig(level=logging.INFO)
27
+ logger = logging.getLogger(__name__)
28
+
29
+ app = Flask(__name__)
30
+ BASE_USER_DIR = "userdata"
31
+ SAVED_COMICS_DIR = "saved_comics"
32
 
33
+ os.makedirs(BASE_USER_DIR, exist_ok=True)
34
+ os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
35
+
36
+ # ======================================================
37
+ # 🧠 MAIN GPU LOGIC
38
+ # ======================================================
39
+ @spaces.GPU(duration=180)
40
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path):
 
 
 
41
  print(f"🚀 GPU Task Started for {video_path}")
42
 
43
+ # Imports inside function to ensure they use the GPU environment
44
  import cv2
45
  import srt
46
+ import numpy as np
47
  from backend.keyframes.keyframes import black_bar_crop
48
  from backend.simple_color_enhancer import SimpleColorEnhancer
49
  from backend.quality_color_enhancer import QualityColorEnhancer
 
52
  from backend.ai_enhanced_core import face_detector
53
  from backend.class_def import bubble, panel, Page
54
 
55
+ # 1. Analyze Video
56
  cap = cv2.VideoCapture(video_path)
57
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25
 
 
58
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
59
+ duration = total_frames / fps
60
  cap.release()
61
 
62
+ # 2. Subtitles
63
  user_srt = os.path.join(user_dir, 'subs.srt')
64
  try:
65
  get_real_subtitles(video_path)
66
  if os.path.exists('test1.srt'):
67
  shutil.move('test1.srt', user_srt)
68
  except Exception as e:
69
+ print(f"Subtitle fallback used: {e}")
70
+ with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:04,000\nHello\n")
 
71
 
72
  with open(user_srt, 'r', encoding='utf-8') as f:
73
  all_subs = list(srt.parse(f.read()))
74
 
75
+ key_moments = [{'index':s.index, 'text':s.content, 'start':s.start.total_seconds(), 'end':s.end.total_seconds()} for s in all_subs]
 
 
 
 
 
 
 
76
  key_moments.sort(key=lambda x: x['start'])
77
+
78
+ # 3. Extract Frames
79
  frame_metadata = {}
 
80
  cap = cv2.VideoCapture(video_path)
81
+ count = 0
82
 
83
+ # Limit to 12 frames for speed/demo purposes on ZeroGPU, increase if needed
84
+ for i, moment in enumerate(key_moments[:24]):
85
+ mid_time = (moment['start'] + moment['end']) / 2
86
+ if mid_time > duration: continue
87
 
88
+ cap.set(cv2.CAP_PROP_POS_FRAMES, int(mid_time * fps))
 
89
  ret, frame = cap.read()
 
90
  if ret:
91
+ fname = f"frame_{count:04d}.png"
92
+ full_p = os.path.join(frames_dir, fname)
93
+ cv2.imwrite(full_p, frame)
94
+ frame_metadata[fname] = {'dialogue': moment['text'], 'time': mid_time}
95
+ count += 1
 
 
 
 
 
96
  cap.release()
97
 
98
+ with open(metadata_path, 'w') as f: json.dump(frame_metadata, f)
 
 
 
 
 
 
99
 
100
+ # 4. Enhance
101
+ try: black_bar_crop()
102
+ except: pass
103
 
104
+ se = SimpleColorEnhancer()
105
+ qe = QualityColorEnhancer()
106
  frame_files = sorted([f for f in os.listdir(frames_dir) if f.endswith('.png')])
107
+
108
  for f in frame_files:
109
+ p = os.path.join(frames_dir, f)
110
+ se.enhance_single(p)
111
+ qe.enhance_single(p)
112
 
113
+ # 5. Bubbles
114
  bubbles_list = []
115
  for f in frame_files:
116
+ p = os.path.join(frames_dir, f)
117
+ dialogue = frame_metadata.get(f, {}).get('dialogue', '')
 
 
118
  try:
119
+ faces = face_detector.detect_faces(p)
120
+ lip = face_detector.get_lip_position(p, faces[0]) if faces else (-1, -1)
121
+ bx, by = ai_bubble_placer.place_bubble_ai(p, lip)
122
  bubbles_list.append(bubble(dialog=dialogue, bubble_offset_x=bx, bubble_offset_y=by, lip_x=lip[0], lip_y=lip[1]))
123
  except:
124
  bubbles_list.append(bubble(dialog=dialogue))
125
 
126
+ # 6. Layout
127
  try:
128
  from backend.fixed_12_pages_800x1080 import generate_12_pages_800x1080
129
  pages = generate_12_pages_800x1080(frame_files, bubbles_list)
130
  except:
 
131
  pages = []
132
+ for i in range((len(frame_files)+3)//4):
133
+ start = i*4
134
+ pg_panels = [panel(image=f) for f in frame_files[start:start+4]]
135
+ pg_bubbles = bubbles_list[start:start+4]
136
+ if pg_panels: pages.append(Page(panels=pg_panels, bubbles=pg_bubbles))
137
+
138
+ # Serialize
139
+ result = []
140
+ for pg in pages:
141
+ p_data = [p.__dict__ if hasattr(p,'__dict__') else p for p in pg.panels]
142
+ b_data = [b.__dict__ if hasattr(b,'__dict__') else b for b in pg.bubbles]
143
+ result.append({'panels': p_data, 'bubbles': b_data})
 
 
144
 
145
+ return result
146
 
147
  @spaces.GPU
148
+ def regen_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
149
  import cv2
150
+ import json
 
 
151
  if not os.path.exists(metadata_path): return None
152
  with open(metadata_path, 'r') as f: meta = json.load(f)
153
  if fname not in meta: return None
154
 
155
+ t = meta[fname]['time'] if isinstance(meta[fname], dict) else meta[fname]
 
 
156
  cap = cv2.VideoCapture(video_path)
157
  fps = cap.get(cv2.CAP_PROP_FPS) or 25
158
+ offset = (1.0/fps) * (1 if direction == 'forward' else -1)
159
+ new_t = max(0, t + offset)
160
 
161
+ cap.set(cv2.CAP_PROP_POS_MSEC, new_t * 1000)
162
  ret, frame = cap.read()
163
  cap.release()
164
 
165
  if ret:
166
  path = os.path.join(frames_dir, fname)
167
  cv2.imwrite(path, frame)
168
+ # Re-enhance
169
+ from backend.simple_color_enhancer import SimpleColorEnhancer
170
  SimpleColorEnhancer().enhance_single(path)
 
171
 
172
+ if isinstance(meta[fname], dict): meta[fname]['time'] = new_t
173
+ else: meta[fname] = new_t
174
+ with open(metadata_path, 'w') as f: json.dump(meta, f)
175
+ return new_t
176
  return None
177
 
178
+ # ======================================================
179
+ # 💻 BACKEND CLASS
180
+ # ======================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  class EnhancedComicGenerator:
182
  def __init__(self, sid):
183
  self.sid = sid
 
185
  self.video_path = os.path.join(self.user_dir, 'uploaded.mp4')
186
  self.frames_dir = os.path.join(self.user_dir, 'frames')
187
  self.output_dir = os.path.join(self.user_dir, 'output')
 
 
188
  os.makedirs(self.frames_dir, exist_ok=True)
189
  os.makedirs(self.output_dir, exist_ok=True)
190
+ self.metadata_path = os.path.join(self.frames_dir, 'frame_metadata.json')
191
 
192
+ def cleanup(self):
193
+ if os.path.exists(self.frames_dir): shutil.rmtree(self.frames_dir)
194
+ if os.path.exists(self.output_dir): shutil.rmtree(self.output_dir)
195
+ os.makedirs(self.frames_dir, exist_ok=True)
196
+ os.makedirs(self.output_dir, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
197
 
198
+ def run(self):
199
  try:
200
+ self.write_status("Processing on GPU...", 10)
201
+ data = generate_comic_gpu(self.video_path, self.user_dir, self.frames_dir, self.metadata_path)
202
+ with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
203
+ json.dump(data, f, indent=2)
204
+ self.write_status("Complete!", 100)
 
 
 
 
 
 
 
 
 
 
 
205
  except Exception as e:
206
  traceback.print_exc()
207
+ self.write_status(f"Error: {str(e)}", -1)
208
+
209
+ def write_status(self, msg, prog):
210
+ with open(os.path.join(self.output_dir, 'status.json'), 'w') as f:
211
+ json.dump({'message': msg, 'progress': prog}, f)
212
 
213
+ # ======================================================
214
+ # 🌐 ROUTES
215
+ # ======================================================
216
  INDEX_HTML = '''
217
  <!DOCTYPE html>
218
  <html lang="en">
219
  <head>
220
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
221
+ <title>ComicGen ZeroGPU</title>
222
+ <style>
223
+ body{font-family:sans-serif;background:#f0f2f5;display:flex;flex-direction:column;align-items:center;padding:20px;}
224
+ .box{background:white;padding:30px;border-radius:10px;box-shadow:0 4px 12px rgba(0,0,0,0.1);text-align:center;max-width:500px;width:100%;}
225
+ button{background:#007bff;color:white;border:none;padding:10px 20px;border-radius:5px;cursor:pointer;font-size:16px;margin-top:10px;}
226
+ button:hover{background:#0056b3;}
227
+ .comic-page{background:white;border:2px solid #333;margin:20px 0;padding:10px;width:100%;max-width:600px;}
228
+ .panel{position:relative;margin-bottom:10px;border:2px solid #000;}
229
+ .panel img{width:100%;display:block;}
230
+ .bubble{position:absolute;background:#fff;border:2px solid #000;border-radius:20px;padding:10px;font-size:12px;text-align:center;min-width:50px;}
231
+ </style>
 
 
 
 
 
 
 
 
 
 
232
  </head>
233
  <body>
234
+ <div class="box" id="upload-box">
235
+ <h1>🎬 Video to Comic</h1>
236
+ <input type="file" id="vid" accept="video/*"><br>
237
+ <button onclick="start()">Generate Comic</button>
238
+ <div id="status" style="margin-top:15px;color:#666;"></div>
239
+ </div>
240
+ <div id="result"></div>
241
+ <script>
242
+ let sid = Math.random().toString(36).substring(7);
243
+ async function start() {
244
+ let f = document.getElementById('vid').files[0];
245
+ if(!f) return alert("Please select a video");
246
+ document.getElementById('status').innerText = "Uploading...";
247
+ let fd = new FormData(); fd.append('file', f);
248
+ let r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
249
+ if(r.ok) {
250
+ document.getElementById('status').innerText = "Queued for GPU...";
251
+ setInterval(check, 2000);
252
+ }
253
+ }
254
+ async function check() {
255
+ let r = await fetch(`/status?sid=${sid}`);
256
+ let d = await r.json();
257
+ document.getElementById('status').innerText = d.message;
258
+ if(d.progress >= 100) {
259
+ document.getElementById('upload-box').style.display = 'none';
260
+ loadPages();
261
+ }
262
+ }
263
+ async function loadPages() {
264
+ let r = await fetch(`/output/pages.json?sid=${sid}`);
265
+ let pages = await r.json();
266
+ let res = document.getElementById('result');
267
+ pages.forEach((pg, i) => {
268
+ let div = document.createElement('div'); div.className='comic-page';
269
+ div.innerHTML = `<h3>Page ${i+1}</h3>`;
270
+ pg.panels.forEach(pan => {
271
+ let pDiv = document.createElement('div'); pDiv.className='panel';
272
+ pDiv.innerHTML = `<img src="/frames/${pan.image}?sid=${sid}">`;
273
+ (pan.bubbles||[]).forEach(b => {
274
+ let bub = document.createElement('div'); bub.className='bubble';
275
+ bub.innerText = b.dialog;
276
+ bub.style.left = (b.bubble_offset_x||10)+'px';
277
+ bub.style.top = (b.bubble_offset_y||10)+'px';
278
+ pDiv.appendChild(bub);
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  });
280
+ div.appendChild(pDiv);
281
+ });
282
+ res.appendChild(div);
283
+ });
284
+ }
285
+ </script>
286
  </body>
287
  </html>
288
  '''
289
 
290
  @app.route('/')
291
+ def home(): return INDEX_HTML
 
292
 
293
  @app.route('/uploader', methods=['POST'])
294
  def upload():
295
  sid = request.args.get('sid')
 
296
  f = request.files['file']
297
  gen = EnhancedComicGenerator(sid)
298
+ gen.cleanup()
299
  f.save(gen.video_path)
300
+ gen.write_status("Starting...", 0)
301
+ threading.Thread(target=gen.run).start()
302
  return jsonify({'success': True})
303
 
304
  @app.route('/status')
305
+ def status():
306
  sid = request.args.get('sid')
307
  path = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
308
  if os.path.exists(path): return send_file(path)
309
+ return jsonify({'progress': 0, 'message': 'Waiting...'})
310
 
311
  @app.route('/output/<path:filename>')
312
+ def files_out(filename):
313
  sid = request.args.get('sid')
314
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
315
 
316
  @app.route('/frames/<path:filename>')
317
+ def files_frames(filename):
318
  sid = request.args.get('sid')
319
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
320
 
321
+ @app.route('/regenerate_frame', methods=['POST'])
322
+ def regen_frame():
323
+ d = request.json
324
+ sid = request.args.get('sid')
325
+ gen = EnhancedComicGenerator(sid)
326
+ res = regen_frame_gpu(gen.video_path, gen.frames_dir, gen.metadata_path, d['filename'], d['direction'])
327
+ return jsonify({'success': res is not None})
328
+
329
  if __name__ == '__main__':
330
+ # Force detection of gpu_warmup
331
+ try: gpu_warmup()
332
+ except: pass
333
+ app.run(host='0.0.0.0', port=7860)