tester343 commited on
Commit
617eba6
·
verified ·
1 Parent(s): 1b90095

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +91 -184
app_enhanced.py CHANGED
@@ -1,55 +1,37 @@
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():
21
  import torch
22
  return torch.cuda.is_available()
23
 
24
- # ======================================================
25
- # 💾 STORAGE SETUP
26
- # ======================================================
27
  BASE_STORAGE_PATH = '/data' if os.path.exists('/data') else '.'
28
  BASE_USER_DIR = os.path.join(BASE_STORAGE_PATH, "userdata")
29
  os.makedirs(BASE_USER_DIR, exist_ok=True)
30
 
31
- # ======================================================
32
- # 🔧 JSON SANITIZER (FIX FOR int64 ERROR)
33
- # ======================================================
34
  def sanitize_json(obj):
35
- if isinstance(obj, dict):
36
- return {k: sanitize_json(v) for k, v in obj.items()}
37
- elif isinstance(obj, list):
38
- return [sanitize_json(v) for v in obj]
39
- elif isinstance(obj, (np.int64, np.int32, np.int16)):
40
- return int(obj)
41
- elif isinstance(obj, (np.float64, np.float32)):
42
- return float(obj)
43
- elif isinstance(obj, np.ndarray):
44
- return sanitize_json(obj.tolist())
45
  return obj
46
 
47
  # ======================================================
48
- # 🧠 CORE GPU GENERATOR
49
  # ======================================================
50
  @spaces.GPU(duration=300)
51
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
52
- # Heavy AI imports inside function to avoid Startup Timeout
53
  from backend.keyframes.keyframes import black_bar_crop
54
  from backend.simple_color_enhancer import SimpleColorEnhancer
55
  from backend.subtitles.subs_real import get_real_subtitles
@@ -62,11 +44,11 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
62
  duration = total_frames / fps
63
  cap.release()
64
 
65
- # 1. Subtitles
66
  user_srt = os.path.join(user_dir, 'subs.srt')
67
  try:
68
  get_real_subtitles(video_path)
69
- if os.path.exists('test1.srt'): shutil.move('test1.srt', user_srt)
70
  except:
71
  with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:05,000\nDialogue...\n")
72
 
@@ -74,11 +56,8 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
74
  try: all_subs = list(srt.parse(f.read()))
75
  except: all_subs = []
76
 
77
- # 2. 5-Panel Math
78
  panels_per_page = 5
79
- target_pages = int(target_pages)
80
- total_needed = target_pages * panels_per_page
81
-
82
  indices = np.linspace(0, len(all_subs) - 1, total_needed, dtype=int) if all_subs else range(total_needed)
83
 
84
  frame_metadata = {}
@@ -90,21 +69,18 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
90
  ret, frame = cap.read()
91
  if ret:
92
  fname = f"frame_{i:04d}.png"
93
- p = os.path.join(frames_dir, fname)
94
- cv2.imwrite(p, frame)
95
  frame_metadata[fname] = {'dialogue': all_subs[idx].content if all_subs else "", 'time': t}
96
  frame_files.append(fname)
97
  cap.release()
98
 
99
- with open(metadata_path, 'w') as f:
100
- json.dump(sanitize_json(frame_metadata), f)
101
-
102
  try: black_bar_crop()
103
  except: pass
104
 
105
  se = SimpleColorEnhancer()
106
  pages_data = []
107
- for p_idx in range(target_pages):
108
  p_p, p_b = [], []
109
  start = p_idx * 5
110
  for i in range(start, start + 5):
@@ -113,22 +89,19 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
113
  img_p = os.path.join(frames_dir, f_name)
114
  try: se.enhance_single(img_p, img_p)
115
  except: pass
116
-
117
  txt = frame_metadata[f_name]['dialogue']
118
  try:
119
  faces = face_detector.detect_faces(img_p)
120
  lip = face_detector.get_lip_position(img_p, faces[0]) if faces else (-1, -1)
121
  bx, by = ai_bubble_placer.place_bubble_ai(img_p, lip)
122
  item = {'dialog': txt, 'x': bx, 'y': by}
123
- except:
124
- item = {'dialog': txt, 'x': 50, 'y': 25}
125
  p_p.append({'image': f_name}); p_b.append(item)
126
  pages_data.append({'panels': p_p, 'bubbles': p_b})
127
-
128
  return sanitize_json(pages_data)
129
 
130
  # ======================================================
131
- # 🔧 APP ENGINE
132
  # ======================================================
133
  app = Flask(__name__)
134
 
@@ -141,185 +114,119 @@ def uploader():
141
  u_dir = os.path.join(BASE_USER_DIR, sid)
142
  f_dir = os.path.join(u_dir, 'frames'); o_dir = os.path.join(u_dir, 'output')
143
  os.makedirs(f_dir, exist_ok=True); os.makedirs(o_dir, exist_ok=True)
144
- vid_p = os.path.join(u_dir, 'video.mp4')
145
- request.files['file'].save(vid_p)
146
- pages = request.form.get('pages', 2)
147
-
148
  def task():
149
  try:
150
- with open(os.path.join(o_dir, 'status.json'), 'w') as f:
151
- json.dump({'message': 'Drafting Parallel Geometry...', 'progress': 30}, f)
152
- data = generate_comic_gpu(vid_p, u_dir, f_dir, os.path.join(f_dir, 'meta.json'), pages)
153
- with open(os.path.join(o_dir, 'pages.json'), 'w') as f: json.dump(data, f)
154
- with open(os.path.join(o_dir, 'status.json'), 'w') as f:
155
- json.dump({'message': 'Complete', 'progress': 100}, f)
156
  except Exception as e:
157
- with open(os.path.join(o_dir, 'status.json'), 'w') as f:
158
- json.dump({'message': f'Error: {str(e)}', 'progress': -1}, f)
159
 
160
  threading.Thread(target=task).start()
161
- return jsonify({'success': True})
162
 
163
  @app.route('/status')
164
  def status():
165
- sid = request.args.get('sid')
166
- p = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
167
  return send_file(p) if os.path.exists(p) else jsonify({'progress': 0})
168
 
169
- @app.route('/frames/<sid>/<path:filename>')
170
- def get_frame(sid, filename): return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
171
 
172
- @app.route('/output/<sid>/<path:filename>')
173
- def get_output(sid, filename): return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
174
 
175
- # ======================================================
176
- # 🌐 HTML (MATHEMATICALLY PARALLEL TILT TEMPLATE)
177
- # ======================================================
178
  INDEX_HTML = '''
179
- <!DOCTYPE html><html lang="en">
180
- <head>
181
- <meta charset="UTF-8"><title>Geometric Consistency Generator</title>
182
- <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
183
- <link href="https://fonts.googleapis.com/css2?family=Comic+Neue:wght@700&family=Lato:wght@400;900&display=swap" rel="stylesheet">
184
- <style>
185
- :root {
186
- --slant: 35px; /* Fixed angle for all panels */
187
- --hairline: 1.5px; /* Minimal consistent gap */
188
- --page-border: 12px;
189
- }
190
- body { background: #111; font-family: 'Lato', sans-serif; margin: 0; padding: 20px; }
191
- .setup-box { max-width: 450px; margin: 80px auto; background: white; padding: 40px; border-radius: 12px; color: black; text-align: center; }
192
-
193
- /* THE PERFECT PARALLEL TEMPLATE ⚡ */
194
- .comic-page {
195
- background: white; width: 1000px; height: 750px; margin: 40px auto;
196
- border: var(--page-border) solid black; padding: 3px; box-sizing: border-box;
197
- display: grid; gap: var(--hairline);
198
- grid-template-columns: repeat(6, 1fr);
199
- grid-template-rows: 1.35fr 1fr; /* Straight horizontal line between tiers */
200
- position: relative; overflow: hidden;
201
- }
202
-
203
- .panel { position: relative; background: #000; overflow: hidden; cursor: pointer; border: 1.5px solid black; }
204
- .panel img { width: 115%; height: 115%; object-fit: cover; position: absolute; top: -7.5%; left: -7.5%; object-position: center 15%; pointer-events: none; }
205
-
206
- /* ROW 1: Slant Left \ (Dividers are perfectly parallel) */
207
- .panel:nth-child(1) {
208
- grid-column: span 4;
209
- clip-path: polygon(0 0, 100% 0, calc(100% - var(--slant)) 100%, 0 100%);
210
- }
211
- .panel:nth-child(2) {
212
- grid-column: span 2;
213
- clip-path: polygon(var(--slant) 0, 100% 0, 100% 100%, 0 100%);
214
- }
215
-
216
- /* ROW 2: Slant Right / (Dividers are perfectly parallel) */
217
- .panel:nth-child(3) {
218
- grid-column: span 2;
219
- clip-path: polygon(0 0, calc(100% - var(--slant)) 0, 100% 100%, 0 100%);
220
- }
221
- .panel:nth-child(4) {
222
- grid-column: span 2;
223
- clip-path: polygon(var(--slant) 0, calc(100% - var(--slant)) 0, 100% 100%, var(--slant) 100%);
224
- }
225
- .panel:nth-child(5) {
226
- grid-column: span 2;
227
- clip-path: polygon(var(--slant) 0, 100% 0, 100% 100%, var(--slant) 100%);
228
- }
229
-
230
- .panel.selected { outline: 8px solid #00d2ff; z-index: 5; filter: brightness(1.1); }
231
-
232
- .bubble {
233
- position: absolute; background: white; border: 2.5px solid black; border-radius: 25px;
234
- padding: 10px 20px; font-family: 'Comic Neue'; font-weight: bold; font-size: 15px;
235
- color: black; min-width: 110px; text-align: center; cursor: move; z-index: 10;
236
- box-shadow: 4px 4px 0 rgba(0,0,0,0.1);
237
- }
238
- .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; }
239
- .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; }
240
-
241
- .controls { position: fixed; bottom: 20px; right: 20px; background: #000; padding: 25px; border-radius: 12px; width: 240px; border: 2px solid #333; }
242
- button { width: 100%; padding: 12px; margin-top: 10px; cursor: pointer; font-weight: bold; border-radius: 6px; border: none; }
243
- .hidden { display: none; }
244
- .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; }
245
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
246
- </style>
247
- </head>
248
  <body>
249
- <div id="upload-zone" class="setup-box">
250
- <h1>🎬 Elite Comic Maker</h1>
251
- <p>Consistent Straight & Slanted Geometry</p>
252
- <input type="file" id="vid" accept="video/mp4"><br><br>
253
- <label>Total Pages: </label><input type="number" id="pg" value="2" style="width:50px">
254
- <br><br>
255
- <button onclick="start()" style="background:#e67e22; color:white;">GENERATE COMIC</button>
256
- <div id="loading" class="hidden"><div class="loader"></div><p id="st">Acquiring GPU...</p></div>
257
  </div>
258
- <div id="editor-zone" class="hidden">
259
- <div id="output"></div>
260
  <div class="controls">
261
- <h4 style="margin:0; color:#00d2ff;">EDITOR</h4>
262
- <button onclick="addB()" style="background:#2ecc71; color:white;">💬 Add Bubble</button>
263
- <button onclick="exportPNG()" style="background:#3498db; color:white;">📥 Download PNGs</button>
264
- <button onclick="location.reload()" style="background:#e74c3c; color:white;">🏠 Reset</button>
265
  </div>
266
  </div>
267
  <script>
268
- let sid = 's' + Math.random().toString(36).substr(2,9);
269
- let selP = null;
270
  async function start() {
271
- const f = document.getElementById('vid').files[0];
272
- if(!f) return alert("Select video!");
273
- document.getElementById('loading').classList.remove('hidden');
274
- const fd = new FormData(); fd.append('file', f); fd.append('pages', document.getElementById('pg').value);
275
  await fetch(`/uploader?sid=${sid}`, {method: 'POST', body: fd});
276
  const itv = setInterval(async () => {
277
  const r = await fetch(`/status?sid=${sid}`); const d = await r.json();
278
- document.getElementById('st').innerText = d.message || "Working...";
279
  if(d.progress >= 100) { clearInterval(itv); load(); }
280
  }, 2000);
281
  }
282
  async function load() {
283
- const r = await fetch(`/output/${sid}/pages.json`); const pages = await r.json();
284
- document.getElementById('upload-zone').classList.add('hidden');
285
- document.getElementById('editor-zone').classList.remove('hidden');
286
- const out = document.getElementById('output');
287
  pages.forEach(p => {
288
- const pgDiv = document.createElement('div'); pgDiv.className = 'comic-page';
289
  p.panels.forEach((pan, i) => {
290
- const pDiv = document.createElement('div'); pDiv.className = 'panel';
291
- pDiv.onclick = (e) => { e.stopPropagation(); if(selP) selP.classList.remove('selected'); selP=pDiv; pDiv.classList.add('selected'); };
292
- const img = document.createElement('img'); img.src = `/frames/${sid}/${pan.image}`;
293
- pDiv.appendChild(img);
294
- if(p.bubbles[i]) pDiv.appendChild(createB(p.bubbles[i].dialog, p.bubbles[i].x, p.bubbles[i].y));
295
- pgDiv.appendChild(pDiv);
 
 
 
296
  });
297
- out.appendChild(pgDiv);
298
  });
299
  }
300
- function createB(txt, x, y) {
301
- const b = document.createElement('div'); b.className = 'bubble';
302
- b.innerText = txt || '...'; b.style.left = (x || 50) + 'px'; b.style.top = (y || 20) + 'px';
303
- b.onmousedown = (e) => {
304
- e.stopPropagation();
305
- let ox = e.clientX - b.offsetLeft, oy = e.clientY - b.offsetTop;
306
- document.onmousemove = (ev) => { b.style.left=(ev.clientX-ox)+'px'; b.style.top=(ev.clientY-oy)+'px'; };
307
- document.onmouseup = () => { document.onmousemove = null; };
308
- };
309
- b.ondblclick = () => { let n = prompt("Edit text:", b.innerText); if(n) b.innerText = n; };
310
- return b;
311
- }
312
- function addB() { if(selP) selP.appendChild(createB("Dialogue", 60, 60)); }
313
  async function exportPNG() {
314
  const pgs = document.querySelectorAll('.comic-page');
315
  for(let pg of pgs) {
316
- const url = await htmlToImage.toPng(pg, {pixelRatio: 2});
317
- const l = document.createElement('a'); l.download='Elite_Comic.png'; l.href=url; l.click();
318
  }
319
  }
320
- </script>
321
- </body></html>
322
- '''
323
 
324
  if __name__ == '__main__':
325
  try: gpu_warmup()
 
1
+ import spaces # MUST BE FIRST
2
  import os
3
  import time
4
  import threading
5
  import json
 
 
 
 
 
6
  import cv2
7
  import numpy as np
8
  import srt
9
  from flask import Flask, jsonify, request, send_from_directory, send_file
10
 
11
  # ======================================================
12
+ # 🚀 ZEROGPU & STORAGE
13
  # ======================================================
14
  @spaces.GPU
15
  def gpu_warmup():
16
  import torch
17
  return torch.cuda.is_available()
18
 
 
 
 
19
  BASE_STORAGE_PATH = '/data' if os.path.exists('/data') else '.'
20
  BASE_USER_DIR = os.path.join(BASE_STORAGE_PATH, "userdata")
21
  os.makedirs(BASE_USER_DIR, exist_ok=True)
22
 
 
 
 
23
  def sanitize_json(obj):
24
+ if isinstance(obj, (np.int64, np.int32, np.int16)): return int(obj)
25
+ if isinstance(obj, (np.float64, np.float32)): return float(obj)
26
+ if isinstance(obj, dict): return {k: sanitize_json(v) for k, v in obj.items()}
27
+ if isinstance(obj, list): return [sanitize_json(v) for v in obj]
 
 
 
 
 
 
28
  return obj
29
 
30
  # ======================================================
31
+ # 🧠 THE ENGINE
32
  # ======================================================
33
  @spaces.GPU(duration=300)
34
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
 
35
  from backend.keyframes.keyframes import black_bar_crop
36
  from backend.simple_color_enhancer import SimpleColorEnhancer
37
  from backend.subtitles.subs_real import get_real_subtitles
 
44
  duration = total_frames / fps
45
  cap.release()
46
 
47
+ # Subtitles
48
  user_srt = os.path.join(user_dir, 'subs.srt')
49
  try:
50
  get_real_subtitles(video_path)
51
+ if os.path.exists('test1.srt'): os.rename('test1.srt', user_srt)
52
  except:
53
  with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:05,000\nDialogue...\n")
54
 
 
56
  try: all_subs = list(srt.parse(f.read()))
57
  except: all_subs = []
58
 
 
59
  panels_per_page = 5
60
+ total_needed = int(target_pages) * panels_per_page
 
 
61
  indices = np.linspace(0, len(all_subs) - 1, total_needed, dtype=int) if all_subs else range(total_needed)
62
 
63
  frame_metadata = {}
 
69
  ret, frame = cap.read()
70
  if ret:
71
  fname = f"frame_{i:04d}.png"
72
+ cv2.imwrite(os.path.join(frames_dir, fname), frame)
 
73
  frame_metadata[fname] = {'dialogue': all_subs[idx].content if all_subs else "", 'time': t}
74
  frame_files.append(fname)
75
  cap.release()
76
 
77
+ with open(metadata_path, 'w') as f: json.dump(sanitize_json(frame_metadata), f)
 
 
78
  try: black_bar_crop()
79
  except: pass
80
 
81
  se = SimpleColorEnhancer()
82
  pages_data = []
83
+ for p_idx in range(int(target_pages)):
84
  p_p, p_b = [], []
85
  start = p_idx * 5
86
  for i in range(start, start + 5):
 
89
  img_p = os.path.join(frames_dir, f_name)
90
  try: se.enhance_single(img_p, img_p)
91
  except: pass
 
92
  txt = frame_metadata[f_name]['dialogue']
93
  try:
94
  faces = face_detector.detect_faces(img_p)
95
  lip = face_detector.get_lip_position(img_p, faces[0]) if faces else (-1, -1)
96
  bx, by = ai_bubble_placer.place_bubble_ai(img_p, lip)
97
  item = {'dialog': txt, 'x': bx, 'y': by}
98
+ except: item = {'dialog': txt, 'x': 50, 'y': 25}
 
99
  p_p.append({'image': f_name}); p_b.append(item)
100
  pages_data.append({'panels': p_p, 'bubbles': p_b})
 
101
  return sanitize_json(pages_data)
102
 
103
  # ======================================================
104
+ # 🌐 FLASK & HTML
105
  # ======================================================
106
  app = Flask(__name__)
107
 
 
114
  u_dir = os.path.join(BASE_USER_DIR, sid)
115
  f_dir = os.path.join(u_dir, 'frames'); o_dir = os.path.join(u_dir, 'output')
116
  os.makedirs(f_dir, exist_ok=True); os.makedirs(o_dir, exist_ok=True)
117
+ request.files['file'].save(os.path.join(u_dir, 'video.mp4'))
118
+
 
 
119
  def task():
120
  try:
121
+ data = generate_comic_gpu(os.path.join(u_dir, 'video.mp4'), u_dir, f_dir, os.path.join(f_dir, 'm.json'), request.form.get('pages', 2))
122
+ with open(os.path.join(o_dir, 'p.json'), 'w') as f: json.dump(data, f)
123
+ with open(os.path.join(o_dir, 's.json'), 'w') as f: json.dump({'progress': 100}, f)
 
 
 
124
  except Exception as e:
125
+ with open(os.path.join(o_dir, 's.json'), 'w') as f: json.dump({'progress': -1, 'msg': str(e)}, f)
 
126
 
127
  threading.Thread(target=task).start()
128
+ return jsonify({'ok': True})
129
 
130
  @app.route('/status')
131
  def status():
132
+ p = os.path.join(BASE_USER_DIR, request.args.get('sid'), 'output', 's.json')
 
133
  return send_file(p) if os.path.exists(p) else jsonify({'progress': 0})
134
 
135
+ @app.route('/frames/<sid>/<path:f>')
136
+ def get_frame(sid, f): return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), f)
137
 
138
+ @app.route('/output/<sid>/<path:f>')
139
+ def get_output(sid, f): return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), f)
140
 
 
 
 
141
  INDEX_HTML = '''
142
+ <!DOCTYPE html><html><head><meta charset="UTF-8">
143
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
144
+ <style>
145
+ :root {
146
+ --slant: 40px; /* MATHEMATICAL PARALLEL OFFSET */
147
+ --hairline: 0.1px; /* SUB-PIXEL PRECISION GAP */
148
+ }
149
+ body { background: #000; font-family: sans-serif; padding: 20px; color: white; }
150
+
151
+ /* THE PERFECT GEOMETRIC TEMPLATE */
152
+ .comic-page {
153
+ background: white; width: 1000px; height: 750px; margin: auto;
154
+ border: 12px solid black; padding: 2px; display: grid; gap: var(--hairline);
155
+ grid-template-columns: repeat(6, 1fr); grid-template-rows: 1.35fr 1fr;
156
+ position: relative; overflow: hidden;
157
+ }
158
+ .panel { position: relative; background: #000; overflow: hidden; border: 1px solid black; }
159
+
160
+ /* TECHNIQUE: OVERSCAN & HEADROOM PROTECTION */
161
+ .panel img {
162
+ width: 115%; height: 115%; object-fit: cover;
163
+ position: absolute; top: -7.5%; left: -7.5%;
164
+ object-position: center 15%; /* PROTECTS FACES FROM TOP CLIPPING */
165
+ }
166
+
167
+ /* ROW 1: PARALLEL SLANT LEFT \ */
168
+ .panel:nth-child(1) { grid-column: span 4; clip-path: polygon(0 0, 100% 0, calc(100% - var(--slant)) 100%, 0 100%); }
169
+ .panel:nth-child(2) { grid-column: span 2; clip-path: polygon(var(--slant) 0, 100% 0, 100% 100%, 0 100%); }
170
+
171
+ /* ROW 2: PARALLEL SLANT RIGHT / */
172
+ .panel:nth-child(3) { grid-column: span 2; clip-path: polygon(0 0, calc(100% - var(--slant)) 0, 100% 100%, 0 100%); }
173
+ .panel:nth-child(4) { grid-column: span 2; clip-path: polygon(var(--slant) 0, calc(100% - var(--slant)) 0, 100% 100%, var(--slant) 100%); }
174
+ .panel:nth-child(5) { grid-column: span 2; clip-path: polygon(var(--slant) 0, 100% 0, 100% 100%, var(--slant) 100%); }
175
+
176
+ .bubble { position: absolute; background: white; border: 2.5px solid black; border-radius: 25px; padding: 10px; color: black; font-weight: bold; cursor: move; z-index: 99; text-align: center; min-width: 100px;}
177
+ .controls { position: fixed; bottom: 20px; right: 20px; background: #222; padding: 20px; border-radius: 10px; }
178
+ .hidden { display: none; }
179
+ </style></head>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  <body>
181
+ <div id="u-zone" style="text-align:center; margin-top:100px;">
182
+ <h1>Elite Geometric Generator</h1>
183
+ <input type="file" id="vid"><br><br>
184
+ <button onclick="start()" style="padding:10px 20px; background:cyan; font-weight:bold;">GENERATE PERFECT COMIC</button>
 
 
 
 
185
  </div>
186
+ <div id="e-zone" class="hidden">
187
+ <div id="out"></div>
188
  <div class="controls">
189
+ <button onclick="exportPNG()" style="background:lime; padding:10px; width:100%;">DOWNLOAD PNG</button>
 
 
 
190
  </div>
191
  </div>
192
  <script>
193
+ let sid = 's'+Math.random().toString(36).substr(2,9);
 
194
  async function start() {
195
+ const fd = new FormData(); fd.append('file', document.getElementById('vid').files[0]);
 
 
 
196
  await fetch(`/uploader?sid=${sid}`, {method: 'POST', body: fd});
197
  const itv = setInterval(async () => {
198
  const r = await fetch(`/status?sid=${sid}`); const d = await r.json();
 
199
  if(d.progress >= 100) { clearInterval(itv); load(); }
200
  }, 2000);
201
  }
202
  async function load() {
203
+ const r = await fetch(`/output/${sid}/p.json`); const pages = await r.json();
204
+ document.getElementById('u-zone').classList.add('hidden');
205
+ document.getElementById('e-zone').classList.remove('hidden');
 
206
  pages.forEach(p => {
207
+ const pg = document.createElement('div'); pg.className='comic-page';
208
  p.panels.forEach((pan, i) => {
209
+ const div = document.createElement('div'); div.className='panel';
210
+ const img = document.createElement('img'); img.src=`/frames/${sid}/${pan.image}`;
211
+ div.appendChild(img);
212
+ if(p.bubbles[i]) {
213
+ const b = document.createElement('div'); b.className='bubble'; b.innerText=p.bubbles[i].dialog;
214
+ b.style.left=p.bubbles[i].x+'px'; b.style.top=p.bubbles[i].y+'px';
215
+ div.appendChild(b);
216
+ }
217
+ pg.appendChild(div);
218
  });
219
+ document.getElementById('out').appendChild(pg);
220
  });
221
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  async function exportPNG() {
223
  const pgs = document.querySelectorAll('.comic-page');
224
  for(let pg of pgs) {
225
+ const url = await htmlToImage.toPng(pg, {pixelRatio: 3});
226
+ const l = document.createElement('a'); l.download='Page.png'; l.href=url; l.click();
227
  }
228
  }
229
+ </script></body></html>'''
 
 
230
 
231
  if __name__ == '__main__':
232
  try: gpu_warmup()