tester343 commited on
Commit
4643773
·
verified ·
1 Parent(s): d327aca

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +167 -238
app_enhanced.py CHANGED
@@ -1,25 +1,20 @@
1
- # ======================================================
2
- # 🚀 IMPORTS (CRITICAL: spaces first)
3
- # ======================================================
4
- import spaces # Must be first for GPU context
5
  import os
6
  import time
7
  import threading
8
  import json
 
9
  import logging
10
- import shutil
11
- import random
12
  import string
13
- import traceback
14
-
15
  import cv2
16
  import numpy as np
17
  import srt
18
-
19
- from flask import Flask, jsonify, request, send_file, send_from_directory
20
 
21
  # ======================================================
22
- # 🖥️ GPU WARMUP
23
  # ======================================================
24
  @spaces.GPU
25
  def gpu_warmup():
@@ -48,7 +43,7 @@ def sanitize_json(obj):
48
  return obj
49
 
50
  # ======================================================
51
- # 🧠 CORE GPU GENERATOR (COMIC + PANEL DESCRIPTIONS)
52
  # ======================================================
53
  @spaces.GPU(duration=300)
54
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
@@ -58,93 +53,75 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
58
  from backend.ai_bubble_placement import ai_bubble_placer
59
  from backend.ai_enhanced_core import face_detector
60
 
61
- # Video metadata
62
  cap = cv2.VideoCapture(video_path)
63
  fps = cap.get(cv2.CAP_PROP_FPS) or 25
64
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
65
  duration = total_frames / fps
66
  cap.release()
67
 
68
- # Subtitles extraction
69
  user_srt = os.path.join(user_dir, 'subs.srt')
70
  try:
71
  get_real_subtitles(video_path)
72
  if os.path.exists('test1.srt'): shutil.move('test1.srt', user_srt)
73
  except:
74
- with open(user_srt, 'w') as f:
75
- f.write("1\n00:00:01,000 --> 00:00:05,000\n...\n")
76
 
77
  with open(user_srt, 'r', encoding='utf-8') as f:
78
  try: all_subs = list(srt.parse(f.read()))
79
  except: all_subs = []
80
 
81
- # Panel mapping
82
  panels_per_page = 5
83
  target_pages = int(target_pages)
84
  total_needed = target_pages * panels_per_page
85
- indices = np.linspace(0, len(all_subs)-1, total_needed, dtype=int) if all_subs else range(total_needed)
86
-
87
  frame_metadata = {}
88
  cap = cv2.VideoCapture(video_path)
89
  frame_files = []
90
 
91
- # Generate frames + attach visual panel description
92
- PANEL_DESCRIPTIONS = [
93
- "Top panel: Wide view of mother scolding children near lunchboxes, warm indoor light, rule-of-thirds composition.",
94
- "Bottom left: Medium-close shot of Chinki explaining herself, simplified background, soft cooler tones.",
95
- "Bottom center: Medium shot of mother gesturing firmly, neutral lighting, minimal props emphasized.",
96
- "Bottom right: Children together, contrite body language, warmer soft light, emotional focal point."
97
- ]
98
-
99
  for i, idx in enumerate(indices):
100
  t = all_subs[idx].start.total_seconds() if all_subs else (i * (duration/total_needed))
101
- cap.set(cv2.CAP_PROP_POS_MSEC, t*1000)
102
  ret, frame = cap.read()
103
  if ret:
104
  fname = f"frame_{i:04d}.png"
105
- fpath = os.path.join(frames_dir, fname)
106
- cv2.imwrite(fpath, frame)
107
- desc = PANEL_DESCRIPTIONS[i % len(PANEL_DESCRIPTIONS)]
108
- frame_metadata[fname] = {'description': desc, 'time': t}
109
  frame_files.append(fname)
110
  cap.release()
111
 
112
- try: black_bar_crop()
 
113
  except: pass
114
 
115
- # Compositional enhancement
116
  se = SimpleColorEnhancer()
117
  pages_data = []
118
-
119
  for p_idx in range(target_pages):
120
- panels, bubbles = [], []
121
- start = p_idx * panels_per_page
122
- for i in range(start, start + panels_per_page):
123
  if i >= len(frame_files): break
124
  f_name = frame_files[i]
125
- img_path = os.path.join(frames_dir, f_name)
126
- try: se.enhance_single(img_path, img_path)
127
  except: pass
128
-
129
- # Optional bubble placement
130
- txt = ""
131
  try:
132
- faces = face_detector.detect_faces(img_path)
133
- lip = face_detector.get_lip_position(img_path, faces[0]) if faces else (-1, -1)
134
- bx, by = ai_bubble_placer.place_bubble_ai(img_path, lip)
135
- bubble_item = {'dialog': txt, 'x': bx, 'y': by}
136
- except: bubble_item = {'dialog': txt, 'x': 50, 'y': 25}
 
 
 
137
 
138
- panels.append({'image': f_name, 'description': frame_metadata[f_name]['description']})
139
- bubbles.append(bubble_item)
140
- pages_data.append({'panels': panels, 'bubbles': bubbles})
141
-
142
- # Save metadata
143
- with open(metadata_path, 'w') as f: json.dump(sanitize_json(frame_metadata), f)
144
  return sanitize_json(pages_data)
145
 
146
  # ======================================================
147
- # 🔧 FLASK APP
148
  # ======================================================
149
  app = Flask(__name__)
150
 
@@ -155,10 +132,8 @@ def index(): return INDEX_HTML
155
  def uploader():
156
  sid = request.args.get('sid')
157
  u_dir = os.path.join(BASE_USER_DIR, sid)
158
- f_dir = os.path.join(u_dir, 'frames')
159
- o_dir = os.path.join(u_dir, 'output')
160
- os.makedirs(f_dir, exist_ok=True)
161
- os.makedirs(o_dir, exist_ok=True)
162
  vid_p = os.path.join(u_dir, 'video.mp4')
163
  request.files['file'].save(vid_p)
164
  pages = request.form.get('pages', 2)
@@ -166,7 +141,7 @@ def uploader():
166
  def task():
167
  try:
168
  with open(os.path.join(o_dir, 'status.json'), 'w') as f:
169
- json.dump({'message': 'Processing...', 'progress': 30}, f)
170
  data = generate_comic_gpu(vid_p, u_dir, f_dir, os.path.join(f_dir, 'meta.json'), pages)
171
  with open(os.path.join(o_dir, 'pages.json'), 'w') as f: json.dump(data, f)
172
  with open(os.path.join(o_dir, 'status.json'), 'w') as f:
@@ -185,198 +160,152 @@ def status():
185
  return send_file(p) if os.path.exists(p) else jsonify({'progress': 0})
186
 
187
  @app.route('/frames/<sid>/<path:filename>')
188
- def get_frame(sid, filename):
189
- return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
190
 
191
  @app.route('/output/<sid>/<path:filename>')
192
- def get_output(sid, filename):
193
- return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
194
 
195
  # ======================================================
196
- # 🖼️ HTML + JS
197
  # ======================================================
198
- INDEX_HTML = '''<!DOCTYPE html>
199
- <html lang="en">
200
  <head>
201
- <meta charset="UTF-8">
202
- <title>Elite 5-Panel Comic Generator</title>
203
- <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
204
- <link href="https://fonts.googleapis.com/css2?family=Comic+Neue:wght@700&family=Lato:wght@400;900&display=swap" rel="stylesheet">
205
- <style>
206
- :root {
207
- --slant: 40px;
208
- --gutter: 0.1px;
209
- --border-page: 12px;
210
- }
211
- body { background: #000; font-family: 'Lato', sans-serif; margin: 0; padding: 20px; color: white; }
212
- .setup-box { max-width: 450px; margin: 80px auto; background: white; padding: 40px; border-radius: 12px; color: black; text-align: center; }
213
-
214
- .comic-page {
215
- background: white; width: 1000px; height: 750px; margin: 40px auto;
216
- border: var(--border-page) solid black; padding: 5px; box-sizing: border-box;
217
- display: grid; gap: var(--gutter);
218
- grid-template-columns: repeat(6, 1fr);
219
- grid-template-rows: 1.35fr 1fr;
220
- position: relative; overflow: hidden;
221
- }
222
-
223
- .panel { position: relative; background: #000; overflow: hidden; cursor: pointer; border: 1.5px solid black; }
224
- .panel img { width: 118%; height: 118%; object-fit: cover; position: absolute; top: -9%; left: -9%; object-position: center 15%; pointer-events: none; }
225
-
226
- /* Top and bottom panel clipping */
227
- .panel:nth-child(1) { grid-column: span 4; clip-path: polygon(0 0, 100% 0, calc(100% - var(--slant)) 100%, 0 100%); }
228
- .panel:nth-child(2) { grid-column: span 2; clip-path: polygon(var(--slant) 0, 100% 0, 100% 100%, 0 100%); }
229
- .panel:nth-child(3) { grid-column: span 2; clip-path: polygon(0 0, calc(100% - var(--slant)) 0, 100% 100%, 0 100%); }
230
- .panel:nth-child(4) { grid-column: span 2; clip-path: polygon(var(--slant) 0, calc(100% - var(--slant)) 0, 100% 100%, var(--slant) 100%); }
231
- .panel:nth-child(5) { grid-column: span 2; clip-path: polygon(var(--slant) 0, 100% 0, 100% 100%, var(--slant) 100%); }
232
-
233
- .panel.selected { outline: 8px solid #00d2ff; z-index: 5; filter: brightness(1.1); }
234
-
235
- /* Bubble styling */
236
- .bubble {
237
- position: absolute; background: white; border: 2.5px solid black; border-radius: 25px;
238
- padding: 10px 20px; font-family: 'Comic Neue'; font-weight: bold; font-size: 15px;
239
- color: black; min-width: 110px; text-align: center; cursor: move; z-index: 10;
240
- }
241
- .bubble::after {
242
- content: ""; position: absolute; bottom: -18px; left: 30px;
243
- width: 0; height: 0; border-left: 10px solid transparent;
244
- border-right: 10px solid transparent; border-top: 20px solid black;
245
- }
246
- .bubble::before {
247
- content: ""; position: absolute; bottom: -13px; left: 31px;
248
- width: 0; height: 0; border-left: 9px solid transparent;
249
- border-right: 9px solid transparent; border-top: 17px solid white;
250
- z-index: 2;
251
- }
252
-
253
- /* Description tooltip */
254
- .desc-tooltip {
255
- position: absolute;
256
- bottom: 0; left: 0;
257
- width: 100%; background: rgba(0,0,0,0.6); color: #fff;
258
- font-size: 14px; padding: 5px; text-align: center;
259
- box-sizing: border-box; pointer-events: none;
260
- opacity: 0; transition: opacity 0.2s;
261
- }
262
- .panel:hover .desc-tooltip { opacity: 1; }
263
-
264
- .controls { position: fixed; bottom: 20px; right: 20px; background: #000; padding: 25px; border-radius: 12px; width: 240px; border: 2px solid #333; }
265
- button { width: 100%; padding: 12px; margin-top: 10px; cursor: pointer; font-weight: bold; border-radius: 6px; border: none; }
266
- .hidden { display: none; }
267
- .loader { border: 5px solid #333; border-top: 5px solid #00d2ff; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; }
268
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
269
- </style>
270
  </head>
271
  <body>
272
- <div id="upload-zone" class="setup-box">
273
- <h1>🎬 Elite Comic Maker</h1>
274
- <p>Visual Panel Descriptions Applied</p>
275
- <input type="file" id="vid" accept="video/mp4"><br><br>
276
- <label>Total Pages: </label><input type="number" id="pg" value="2" style="width:50px">
277
- <br><br>
278
- <button onclick="start()" style="background:#00d2ff; color:black;">🚀 GENERATE PERFECT COMIC</button>
279
- <div id="loading" class="hidden"><div class="loader"></div><p id="st">Processing Composition...</p></div>
280
- </div>
281
-
282
- <div id="editor-zone" class="hidden">
283
- <div id="output"></div>
284
- <div class="controls">
285
- <h4 style="margin:0; color:#00d2ff;">EDITOR TOOLS</h4>
286
- <button onclick="addB()" style="background:#2ecc71; color:white;">💬 Add Text Capsule</button>
287
- <button onclick="exportPNG()" style="background:#3498db; color:white;">📥 Download Pages</button>
288
- <button onclick="location.reload()" style="background:#e74c3c; color:white;">🏠 Start New</button>
289
  </div>
290
- </div>
291
-
292
  <script>
293
- let sid = 's' + Math.random().toString(36).substr(2,9);
294
- let selP = null;
295
-
296
- async function start() {
297
- const f = document.getElementById('vid').files[0];
298
- if(!f) return alert("Select a video!");
299
- document.getElementById('loading').classList.remove('hidden');
300
- const fd = new FormData();
301
- fd.append('file', f);
302
- fd.append('pages', document.getElementById('pg').value);
303
- await fetch(`/uploader?sid=${sid}`, {method: 'POST', body: fd});
304
- const itv = setInterval(async () => {
305
- const r = await fetch(`/status?sid=${sid}`);
306
- const d = await r.json();
307
- document.getElementById('st').innerText = d.message || "Working...";
308
- if(d.progress >= 100) { clearInterval(itv); load(); }
309
- }, 2000);
310
- }
311
-
312
- async function load() {
313
- const r = await fetch(`/output/${sid}/pages.json`);
314
- const pages = await r.json();
315
- document.getElementById('upload-zone').classList.add('hidden');
316
- document.getElementById('editor-zone').classList.remove('hidden');
317
- const out = document.getElementById('output');
318
-
319
- pages.forEach(p => {
320
- const pgDiv = document.createElement('div'); pgDiv.className = 'comic-page';
321
- p.panels.forEach((pan, i) => {
322
- const pDiv = document.createElement('div'); pDiv.className = 'panel';
323
- pDiv.onclick = (e) => {
324
- e.stopPropagation();
325
- if(selP) selP.classList.remove('selected');
326
- selP=pDiv;
327
- pDiv.classList.add('selected');
328
- };
329
- const img = document.createElement('img'); img.src = `/frames/${sid}/${pan.image}`;
330
- pDiv.appendChild(img);
331
-
332
- // Add panel description overlay
333
- const desc = document.createElement('div');
334
- desc.className = 'desc-tooltip';
335
- desc.innerText = pan.description || '';
336
- pDiv.appendChild(desc);
337
-
338
- // Optional existing bubbles
339
- if(p.bubbles[i]) pDiv.appendChild(createB(p.bubbles[i].dialog, p.bubbles[i].x, p.bubbles[i].y));
340
- pgDiv.appendChild(pDiv);
341
  });
342
- out.appendChild(pgDiv);
343
- });
344
- }
345
-
346
- function createB(txt, x, y) {
347
- const b = document.createElement('div'); b.className = 'bubble';
348
- b.innerText = txt || '...'; b.style.left = (x || 50) + 'px'; b.style.top = (y || 20) + 'px';
349
- b.onmousedown = (e) => {
350
- e.stopPropagation();
351
- let ox = e.clientX - b.offsetLeft, oy = e.clientY - b.offsetTop;
352
- document.onmousemove = (ev) => { b.style.left=(ev.clientX-ox)+'px'; b.style.top=(ev.clientY-oy)+'px'; };
353
- document.onmouseup = () => { document.onmousemove = null; };
354
- };
355
- b.ondblclick = () => { let n = prompt("Edit Text:", b.innerText); if(n) b.innerText = n; };
356
- return b;
357
- }
358
-
359
- function addB() {
360
- if(selP) selP.appendChild(createB("Enter Dialogue", 60, 60));
361
- else alert("Select a panel first!");
362
- }
363
-
364
- async function exportPNG() {
365
- const pgs = document.querySelectorAll('.comic-page');
366
- for(let pg of pgs) {
367
- const url = await htmlToImage.toPng(pg, {pixelRatio: 2});
368
- const l = document.createElement('a'); l.download='Elite_Comic_Page.png'; l.href=url; l.click();
369
  }
370
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  </script>
372
- </body>
373
- </html>
374
- ''' # Keep your existing HTML/JS intact; optional: add descriptions in UI
375
 
376
- # ======================================================
377
- # 🚀 RUN APP
378
- # ======================================================
379
  if __name__ == '__main__':
380
  try: gpu_warmup()
381
  except: pass
382
- app.run(host='0.0.0.0', port=7860)
 
1
+ import spaces # <--- CRITICAL: 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
  import cv2
12
  import numpy as np
13
  import srt
14
+ from flask import Flask, jsonify, request, send_from_directory, send_file
 
15
 
16
  # ======================================================
17
+ # 🚀 ZEROGPU CONFIGURATION
18
  # ======================================================
19
  @spaces.GPU
20
  def gpu_warmup():
 
43
  return obj
44
 
45
  # ======================================================
46
+ # 🧠 CORE GPU GENERATOR
47
  # ======================================================
48
  @spaces.GPU(duration=300)
49
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
 
53
  from backend.ai_bubble_placement import ai_bubble_placer
54
  from backend.ai_enhanced_core import face_detector
55
 
 
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
  user_srt = os.path.join(user_dir, 'subs.srt')
63
  try:
64
  get_real_subtitles(video_path)
65
  if os.path.exists('test1.srt'): shutil.move('test1.srt', user_srt)
66
  except:
67
+ with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:05,000\n...\n")
 
68
 
69
  with open(user_srt, 'r', encoding='utf-8') as f:
70
  try: all_subs = list(srt.parse(f.read()))
71
  except: all_subs = []
72
 
 
73
  panels_per_page = 5
74
  target_pages = int(target_pages)
75
  total_needed = target_pages * panels_per_page
76
+ indices = np.linspace(0, len(all_subs) - 1, total_needed, dtype=int) if all_subs else range(total_needed)
77
+
78
  frame_metadata = {}
79
  cap = cv2.VideoCapture(video_path)
80
  frame_files = []
81
 
 
 
 
 
 
 
 
 
82
  for i, idx in enumerate(indices):
83
  t = all_subs[idx].start.total_seconds() if all_subs else (i * (duration/total_needed))
84
+ cap.set(cv2.CAP_PROP_POS_MSEC, t * 1000)
85
  ret, frame = cap.read()
86
  if ret:
87
  fname = f"frame_{i:04d}.png"
88
+ p = os.path.join(frames_dir, fname)
89
+ cv2.imwrite(p, frame)
90
+ frame_metadata[fname] = {'dialogue': all_subs[idx].content if all_subs else "", 'time': t}
 
91
  frame_files.append(fname)
92
  cap.release()
93
 
94
+ with open(metadata_path, 'w') as f: json.dump(sanitize_json(frame_metadata), f)
95
+ try: black_bar_crop()
96
  except: pass
97
 
 
98
  se = SimpleColorEnhancer()
99
  pages_data = []
 
100
  for p_idx in range(target_pages):
101
+ p_p, p_b = [], []
102
+ start = p_idx * 5
103
+ for i in range(start, start + 5):
104
  if i >= len(frame_files): break
105
  f_name = frame_files[i]
106
+ img_p = os.path.join(frames_dir, f_name)
107
+ try: se.enhance_single(img_p, img_p)
108
  except: pass
109
+
110
+ txt = frame_metadata[f_name]['dialogue']
 
111
  try:
112
+ faces = face_detector.detect_faces(img_p)
113
+ lip = face_detector.get_lip_position(img_p, faces[0]) if faces else (-1, -1)
114
+ bx, by = ai_bubble_placer.place_bubble_ai(img_p, lip)
115
+ item = {'dialog': txt, 'x': bx, 'y': by}
116
+ except:
117
+ item = {'dialog': txt, 'x': 50, 'y': 25}
118
+ p_p.append({'image': f_name}); p_b.append(item)
119
+ pages_data.append({'panels': p_p, 'bubbles': p_b})
120
 
 
 
 
 
 
 
121
  return sanitize_json(pages_data)
122
 
123
  # ======================================================
124
+ # 🔧 APP ENGINE
125
  # ======================================================
126
  app = Flask(__name__)
127
 
 
132
  def uploader():
133
  sid = request.args.get('sid')
134
  u_dir = os.path.join(BASE_USER_DIR, sid)
135
+ f_dir = os.path.join(u_dir, 'frames'); o_dir = os.path.join(u_dir, 'output')
136
+ os.makedirs(f_dir, exist_ok=True); os.makedirs(o_dir, exist_ok=True)
 
 
137
  vid_p = os.path.join(u_dir, 'video.mp4')
138
  request.files['file'].save(vid_p)
139
  pages = request.form.get('pages', 2)
 
141
  def task():
142
  try:
143
  with open(os.path.join(o_dir, 'status.json'), 'w') as f:
144
+ json.dump({'message': 'Generating Tilted Frames...', 'progress': 30}, f)
145
  data = generate_comic_gpu(vid_p, u_dir, f_dir, os.path.join(f_dir, 'meta.json'), pages)
146
  with open(os.path.join(o_dir, 'pages.json'), 'w') as f: json.dump(data, f)
147
  with open(os.path.join(o_dir, 'status.json'), 'w') as f:
 
160
  return send_file(p) if os.path.exists(p) else jsonify({'progress': 0})
161
 
162
  @app.route('/frames/<sid>/<path:filename>')
163
+ def get_frame(sid, filename): return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
 
164
 
165
  @app.route('/output/<sid>/<path:filename>')
166
+ def get_output(sid, filename): return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
 
167
 
168
  # ======================================================
169
+ # 🖼️ UI HTML (FULL IMAGE PRESERVATION + 0.1px DESIGN LINES)
170
  # ======================================================
171
+ INDEX_HTML = '''
172
+ <!DOCTYPE html><html lang="en">
173
  <head>
174
+ <meta charset="UTF-8"><title>Pro Full-Frame Comic Maker</title>
175
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
176
+ <link href="https://fonts.googleapis.com/css2?family=Comic+Neue:wght@700&family=Lato:wght@400;900&display=swap" rel="stylesheet">
177
+ <style>
178
+ :root {
179
+ --slant: 28px; /* Balanced tilt: visible but not destructive */
180
+ --gutter: 0.1px;
181
+ --border-page: 12px;
182
+ }
183
+ body { background: #000; font-family: 'Lato', sans-serif; margin: 0; padding: 20px; }
184
+ .setup-box { max-width: 450px; margin: 80px auto; background: white; padding: 40px; border-radius: 12px; color: black; text-align: center; }
185
+
186
+ /* ⚡ THE FULL-FRAME GEOMETRIC TEMPLATE ⚡ */
187
+ .comic-page {
188
+ background: white; width: 1000px; height: 750px; margin: 40px auto;
189
+ border: var(--border-page) solid black; padding: 4px; box-sizing: border-box;
190
+ display: grid; gap: var(--gutter);
191
+ grid-template-columns: repeat(6, 1fr);
192
+ grid-template-rows: 1.3fr 1fr;
193
+ position: relative; overflow: hidden;
194
+ }
195
+
196
+ .panel { position: relative; background: #000; overflow: hidden; cursor: pointer; border: 1.5px solid black; }
197
+
198
+ /* IMAGE PRESERVATION: Minimal zoom (105%) and top-bias positioning */
199
+ .panel img {
200
+ width: 108%; height: 108%; object-fit: cover;
201
+ position: absolute; top: -4%; left: -4%;
202
+ object-position: center 10%; /* Ensures faces at the top aren't cut */
203
+ pointer-events: none;
204
+ }
205
+
206
+ /* ROW 1: Design Lines - Tilt Left \ */
207
+ .panel:nth-child(1) { grid-column: span 4; clip-path: polygon(0 0, 100% 0, calc(100% - var(--slant)) 100%, 0 100%); }
208
+ .panel:nth-child(2) { grid-column: span 2; clip-path: polygon(var(--slant) 0, 100% 0, 100% 100%, 0 100%); }
209
+
210
+ /* ROW 2: Design Lines - Tilt Right / (Preserving Fullness) */
211
+ .panel:nth-child(3) { grid-column: span 2; clip-path: polygon(0 0, calc(100% - var(--slant)) 0, 100% 100%, 0 100%); }
212
+ .panel:nth-child(4) { grid-column: span 2; clip-path: polygon(var(--slant) 0, calc(100% - var(--slant)) 0, 100% 100%, var(--slant) 100%); }
213
+ .panel:nth-child(5) { grid-column: span 2; clip-path: polygon(var(--slant) 0, 100% 0, 100% 100%, var(--slant) 100%); }
214
+
215
+ .panel.selected { outline: 8px solid #00d2ff; z-index: 5; filter: brightness(1.1); }
216
+
217
+ .bubble {
218
+ position: absolute; background: white; border: 2.5px solid black; border-radius: 25px;
219
+ padding: 10px 20px; font-family: 'Comic Neue'; font-weight: bold; font-size: 15px;
220
+ color: black; min-width: 110px; text-align: center; cursor: move; z-index: 10;
221
+ }
222
+ .bubble::after { content: ""; position: absolute; bottom: -18px; left: 30px; width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; border-top: 20px solid black; }
223
+ .bubble::before { content: ""; position: absolute; bottom: -13px; left: 31px; width: 0; height: 0; border-left: 9px solid transparent; border-right: 9px solid transparent; border-top: 17px solid white; z-index: 2; }
224
+
225
+ .controls { position: fixed; bottom: 20px; right: 20px; background: #000; padding: 25px; border-radius: 12px; width: 240px; border: 2px solid #333; }
226
+ button { width: 100%; padding: 12px; margin-top: 10px; cursor: pointer; font-weight: bold; border-radius: 6px; border: none; }
227
+ .hidden { display: none; }
228
+ .loader { border: 5px solid #333; border-top: 5px solid #00d2ff; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; }
229
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
230
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
231
  </head>
232
  <body>
233
+ <div id="upload-zone" class="setup-box">
234
+ <h1>🎬 Elite Comic Maker</h1>
235
+ <p>Full Image Preservation + 0.1px Design Lines</p>
236
+ <input type="file" id="vid" accept="video/mp4"><br><br>
237
+ <label>Total Pages: </label><input type="number" id="pg" value="2" style="width:50px">
238
+ <br><br>
239
+ <button onclick="start()" style="background:#00d2ff; color:black;">🚀 GENERATE COMIC</button>
240
+ <div id="loading" class="hidden"><div class="loader"></div><p id="st">Processing Composition...</p></div>
241
+ </div>
242
+ <div id="editor-zone" class="hidden">
243
+ <div id="output"></div>
244
+ <div class="controls">
245
+ <h4 style="margin:0; color:#00d2ff;">EDITOR</h4>
246
+ <button onclick="addB()" style="background:#2ecc71; color:white;">💬 Add Bubble</button>
247
+ <button onclick="exportPNG()" style="background:#3498db; color:white;">📥 Download PNGs</button>
248
+ <button onclick="location.reload()" style="background:#e74c3c; color:white;">🏠 Reset</button>
249
+ </div>
250
  </div>
 
 
251
  <script>
252
+ let sid = 's' + Math.random().toString(36).substr(2,9);
253
+ let selP = null;
254
+ async function start() {
255
+ const f = document.getElementById('vid').files[0];
256
+ if(!f) return alert("Select video!");
257
+ document.getElementById('loading').classList.remove('hidden');
258
+ const fd = new FormData(); fd.append('file', f); fd.append('pages', document.getElementById('pg').value);
259
+ await fetch(`/uploader?sid=${sid}`, {method: 'POST', body: fd});
260
+ const itv = setInterval(async () => {
261
+ const r = await fetch(`/status?sid=${sid}`); const d = await r.json();
262
+ document.getElementById('st').innerText = d.message || "Working...";
263
+ if(d.progress >= 100) { clearInterval(itv); load(); }
264
+ }, 2000);
265
+ }
266
+ async function load() {
267
+ const r = await fetch(`/output/${sid}/pages.json`); const pages = await r.json();
268
+ document.getElementById('upload-zone').classList.add('hidden');
269
+ document.getElementById('editor-zone').classList.remove('hidden');
270
+ const out = document.getElementById('output');
271
+ pages.forEach(p => {
272
+ const pgDiv = document.createElement('div'); pgDiv.className = 'comic-page';
273
+ p.panels.forEach((pan, i) => {
274
+ const pDiv = document.createElement('div'); pDiv.className = 'panel';
275
+ pDiv.onclick = (e) => { e.stopPropagation(); if(selP) selP.classList.remove('selected'); selP=pDiv; pDiv.classList.add('selected'); };
276
+ const img = document.createElement('img'); img.src = `/frames/${sid}/${pan.image}`;
277
+ pDiv.appendChild(img);
278
+ if(p.bubbles[i]) pDiv.appendChild(createB(p.bubbles[i].dialog, p.bubbles[i].x, p.bubbles[i].y));
279
+ pgDiv.appendChild(pDiv);
280
+ });
281
+ out.appendChild(pgDiv);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  }
284
+ function createB(txt, x, y) {
285
+ const b = document.createElement('div'); b.className = 'bubble';
286
+ b.innerText = txt || '...'; b.style.left = (x || 50) + 'px'; b.style.top = (y || 20) + 'px';
287
+ b.onmousedown = (e) => {
288
+ e.stopPropagation();
289
+ let ox = e.clientX - b.offsetLeft, oy = e.clientY - b.offsetTop;
290
+ document.onmousemove = (ev) => { b.style.left=(ev.clientX-ox)+'px'; b.style.top=(ev.clientY-oy)+'px'; };
291
+ document.onmouseup = () => { document.onmousemove = null; };
292
+ };
293
+ b.ondblclick = () => { let n = prompt("Edit:", b.innerText); if(n) b.innerText = n; };
294
+ return b;
295
+ }
296
+ function addB() { if(selP) selP.appendChild(createB("Dialogue", 60, 60)); }
297
+ async function exportPNG() {
298
+ const pgs = document.querySelectorAll('.comic-page');
299
+ for(let pg of pgs) {
300
+ const url = await htmlToImage.toPng(pg, {pixelRatio: 2});
301
+ const l = document.createElement('a'); l.download='Pro_Comic.png'; l.href=url; l.click();
302
+ }
303
+ }
304
  </script>
305
+ </body></html>
306
+ '''
 
307
 
 
 
 
308
  if __name__ == '__main__':
309
  try: gpu_warmup()
310
  except: pass
311
+ app.run(host='0.0.0.0', port=7860)