tester343 commited on
Commit
a8edaa9
·
verified ·
1 Parent(s): 69db5d6

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +248 -192
app_enhanced.py CHANGED
@@ -10,11 +10,11 @@ import torch
10
  from flask import Flask, jsonify, request, send_from_directory, send_file
11
 
12
  # ======================================================
13
- # 🚀 ZEROGPU WARMUP (FIXES RUNTIME ERROR)
14
  # ======================================================
15
  @spaces.GPU
16
  def gpu_warmup():
17
- """Dummy function to trigger ZeroGPU detection at startup."""
18
  print(f"✅ GPU Warmup: CUDA Available = {torch.cuda.is_available()}")
19
  return True
20
 
@@ -23,7 +23,6 @@ def gpu_warmup():
23
  # ======================================================
24
  app = Flask(__name__)
25
 
26
- # Persistent storage check
27
  if os.path.exists('/data'):
28
  BASE_STORAGE_PATH = '/data'
29
  else:
@@ -37,17 +36,14 @@ os.makedirs(BASE_USER_DIR, exist_ok=True)
37
  # ======================================================
38
 
39
  def create_placeholder_image(text, filename, output_dir):
40
- """Creates a backup image if video fails to read."""
41
- # Create dark grey image
42
  img = np.zeros((800, 800, 3), dtype=np.uint8)
43
- img[:] = (40, 40, 40)
44
 
45
- # Add text
46
  font = cv2.FONT_HERSHEY_SIMPLEX
47
  cv2.putText(img, text, (50, 400), font, 1.5, (200, 200, 200), 3, cv2.LINE_AA)
48
-
49
- # Add border
50
- cv2.rectangle(img, (0,0), (800,800), (100,100,100), 20)
51
 
52
  path = os.path.join(output_dir, filename)
53
  cv2.imwrite(path, img)
@@ -55,17 +51,11 @@ def create_placeholder_image(text, filename, output_dir):
55
 
56
  @spaces.GPU(duration=120)
57
  def generate_comic_gpu(video_path, frames_dir, target_pages):
58
- """
59
- Extracts 4 frames per page (2x2 Grid).
60
- """
61
- print(f"🚀 Starting GPU generation for {video_path}")
62
-
63
- # 1. Setup
64
  if os.path.exists(frames_dir): shutil.rmtree(frames_dir)
65
  os.makedirs(frames_dir, exist_ok=True)
66
 
67
  cap = cv2.VideoCapture(video_path)
68
- fps = 25
69
  total_frames = 0
70
  duration = 0
71
 
@@ -74,72 +64,57 @@ def generate_comic_gpu(video_path, frames_dir, target_pages):
74
  fps = cap.get(cv2.CAP_PROP_FPS) or 25
75
  duration = total_frames / fps
76
  else:
77
- print("❌ Video load failed. Using placeholders.")
78
 
79
- # 2. Calculate frames needed (4 per page)
80
  panels_per_page = 4
81
  total_panels_needed = int(target_pages) * panels_per_page
82
-
83
  frame_files_ordered = []
84
 
85
- # 3. Extract Frames
86
- if duration > 0 and total_frames > 0:
87
  times = np.linspace(1, max(1, duration - 1), total_panels_needed)
88
-
89
  for i, t in enumerate(times):
90
  cap.set(cv2.CAP_PROP_POS_MSEC, t * 1000)
91
  ret, frame = cap.read()
92
  fname = f"frame_{i:04d}.png"
93
-
94
  if ret and frame is not None:
95
- # Crop to square-ish for 2x2 grid
96
  h, w = frame.shape[:2]
97
  min_dim = min(h, w)
98
- start_x = (w - min_dim) // 2
99
- start_y = (h - min_dim) // 2
100
- # Crop center square
101
- frame = frame[start_y:start_y+min_dim, start_x:start_x+min_dim]
102
-
103
- # Resize for consistency
104
  frame = cv2.resize(frame, (800, 800))
105
-
106
  cv2.imwrite(os.path.join(frames_dir, fname), frame)
107
  frame_files_ordered.append(fname)
108
  else:
109
- create_placeholder_image(f"Error {t:.1f}s", fname, frames_dir)
110
  frame_files_ordered.append(fname)
111
  cap.release()
112
  else:
113
- # Fallback loop
114
  for i in range(total_panels_needed):
115
  fname = f"placeholder_{i}.png"
116
  create_placeholder_image(f"Panel {i+1}", fname, frames_dir)
117
  frame_files_ordered.append(fname)
118
 
119
- # 4. Build Page Data
120
  pages_data = []
121
  for i in range(int(target_pages)):
122
  start = i * panels_per_page
123
  end = start + panels_per_page
124
  p_frames = frame_files_ordered[start:end]
125
 
126
- # Fill missing if any
127
  while len(p_frames) < 4:
128
  fname = f"extra_{len(p_frames)}.png"
129
  create_placeholder_image("Empty", fname, frames_dir)
130
  p_frames.append(fname)
131
 
132
  pg_panels = [{'image': f} for f in p_frames]
133
-
134
  pg_bubbles = []
135
  if i == 0:
136
- pg_bubbles.append({'dialog': "Drag the RED DOT\nto resize panels!", 'x': '50%', 'y': '50%'})
137
 
138
  pages_data.append({
139
  'panels': pg_panels,
140
- 'bubbles': pg_bubbles,
141
- 'splitX': '50%', # Center X
142
- 'splitY': '50%' # Center Y
143
  })
144
 
145
  return pages_data
@@ -151,19 +126,16 @@ class ComicGenHost:
151
  self.video_path = os.path.join(self.user_dir, 'video.mp4')
152
  self.frames_dir = os.path.join(self.user_dir, 'frames')
153
  self.output_dir = os.path.join(self.user_dir, 'output')
154
-
155
  os.makedirs(self.user_dir, exist_ok=True)
156
  os.makedirs(self.frames_dir, exist_ok=True)
157
  os.makedirs(self.output_dir, exist_ok=True)
158
 
159
  def run(self, pages):
160
  try:
161
- self.write_status("Generating...", 30)
162
  data = generate_comic_gpu(self.video_path, self.frames_dir, pages)
163
-
164
  with open(os.path.join(self.output_dir, 'data.json'), 'w') as f:
165
  json.dump(data, f)
166
-
167
  self.write_status("Ready", 100)
168
  except Exception as e:
169
  traceback.print_exc()
@@ -182,211 +154,241 @@ INDEX_HTML = '''
182
  <head>
183
  <meta charset="UTF-8">
184
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
185
- <title>4-Panel Adjustable Comic</title>
186
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
187
  <link href="https://fonts.googleapis.com/css2?family=Bangers&display=swap" rel="stylesheet">
188
  <style>
189
- body { background: #121212; color: #eee; font-family: sans-serif; margin: 0; text-align: center; }
190
 
191
- /* UPLOAD SCREEN */
192
- #upload-view { padding: 50px; }
193
  .box { background: #1e1e1e; display: inline-block; padding: 40px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); }
194
- button { background: #e74c3c; border: none; padding: 10px 20px; font-weight: bold; cursor: pointer; border-radius: 4px; font-size: 16px; margin-top: 10px; color: white; }
195
  button:hover { background: #c0392b; }
196
- input { padding: 10px; margin: 5px; border-radius: 4px; border: 1px solid #555; background: #333; color: white; }
197
-
198
- /* EDITOR SCREEN */
199
- #editor-view { display: none; padding: 20px; }
200
- .comic-container { display: flex; flex-wrap: wrap; justify-content: center; gap: 40px; margin-top: 20px; padding-bottom: 80px; }
201
 
202
- /* COMIC PAGE STYLE */
 
 
 
203
  .comic-page {
204
  width: 600px; height: 800px;
205
  background: white;
206
  border: 4px solid #000;
207
  position: relative;
208
- box-shadow: 0 0 20px rgba(0,0,0,0.5);
209
- user-select: none;
210
  overflow: hidden;
211
  }
212
 
213
- /* 2x2 GRID LOGIC */
214
  .comic-grid {
215
  width: 100%; height: 100%;
216
  position: relative;
217
- background: #000; /* The "Gap" color */
218
- --x: 50%;
219
- --y: 50%;
220
- --gap: 4px; /* Thickness of divider */
 
 
221
  }
222
 
223
  .panel {
224
- position: absolute;
225
  overflow: hidden;
226
- background: #333;
227
- box-sizing: border-box;
228
- border: 2px solid #000;
229
  }
230
-
 
231
  .panel img {
232
  width: 100%; height: 100%;
233
  object-fit: cover;
234
- pointer-events: auto;
 
235
  }
 
236
 
237
- /* DYNAMIC PANEL SIZING */
238
  /* Top Left */
239
- .panel:nth-child(1) { left: 0; top: 0; width: calc(var(--x) - var(--gap)/2); height: calc(var(--y) - var(--gap)/2); }
 
 
 
240
  /* Top Right */
241
- .panel:nth-child(2) { left: calc(var(--x) + var(--gap)/2); top: 0; width: calc(100% - var(--x) - var(--gap)/2); height: calc(var(--y) - var(--gap)/2); }
 
 
 
242
  /* Bottom Left */
243
- .panel:nth-child(3) { left: 0; top: calc(var(--y) + var(--gap)/2); width: calc(var(--x) - var(--gap)/2); height: calc(100% - var(--y) - var(--gap)/2); }
 
 
 
244
  /* Bottom Right */
245
- .panel:nth-child(4) { left: calc(var(--x) + var(--gap)/2); top: calc(var(--y) + var(--gap)/2); width: calc(100% - var(--x) - var(--gap)/2); height: calc(100% - var(--y) - var(--gap)/2); }
 
 
 
246
 
247
- /* CENTRAL HANDLE (RED DOT) */
248
- .grid-handle {
249
  position: absolute;
250
- width: 24px; height: 24px;
251
- background: #e74c3c;
252
- border: 3px solid white;
253
- border-radius: 50%;
254
  left: var(--x); top: var(--y);
255
  transform: translate(-50%, -50%);
256
- cursor: move;
257
- z-index: 999;
258
- box-shadow: 0 4px 10px rgba(0,0,0,0.5);
259
- pointer-events: auto;
 
 
 
 
 
 
 
 
 
260
  }
261
- .grid-handle:hover { transform: translate(-50%, -50%) scale(1.2); }
262
 
263
  /* BUBBLES */
264
  .bubble {
265
  position: absolute;
266
  background: white; color: black;
267
- padding: 10px 15px; border-radius: 20px;
268
- font-family: 'Bangers', cursive;
269
- letter-spacing: 1px;
270
- border: 2px solid black;
271
- z-index: 100; cursor: move;
272
  transform: translate(-50%, -50%);
273
- min-width: 50px; text-align: center;
274
- }
275
- .bubble:after {
276
- content: ''; position: absolute;
277
- bottom: -10px; left: 50%; transform: translateX(-50%);
278
- border-width: 10px 10px 0; border-style: solid;
279
- border-color: black transparent transparent transparent;
280
  }
281
 
282
- /* CONTROLS */
283
  .toolbar {
284
  position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
285
  background: #333; padding: 10px 20px; border-radius: 50px;
286
- display: flex; gap: 15px; z-index: 1000; box-shadow: 0 5px 20px rgba(0,0,0,0.6);
287
  }
288
- .toolbar button { margin-top: 0; font-size: 14px; }
289
  </style>
290
  </head>
291
  <body>
292
 
293
  <div id="upload-view">
294
  <div class="box">
295
- <h1>🎞️ 4-Panel Comic Maker</h1>
296
- <p>Upload a video to create a 2x2 Comic Page.</p>
297
- <input type="file" id="fileIn" accept="video/*"><br>
298
  <label>Pages:</label> <input type="number" id="pgCount" value="1" min="1" max="5" style="width:50px;">
299
- <br>
300
  <button onclick="startUpload()">🚀 Generate</button>
301
- <p id="status" style="color: #bbb; margin-top:10px;"></p>
302
  </div>
303
  </div>
304
 
305
  <div id="editor-view">
306
- <h2>Drag the <span style="color:#e74c3c">Red Dot</span> to resize panels!</h2>
307
  <div class="comic-container" id="container"></div>
308
-
309
  <div class="toolbar">
310
- <button onclick="addBubble()">💬 Add Text</button>
311
- <button onclick="downloadAll()">💾 Download</button>
312
- <button style="background:#555" onclick="location.reload()">↺ New</button>
 
 
 
313
  </div>
314
  </div>
315
 
316
  <script>
317
  let sid = 'S' + Date.now();
318
- let dragItem = null;
319
- let activeEl = null;
 
 
 
 
320
 
321
  async function startUpload() {
322
  let f = document.getElementById('fileIn').files[0];
323
- if(!f) return alert("Select a video.");
324
-
325
  let fd = new FormData();
326
  fd.append('file', f);
327
  fd.append('pages', document.getElementById('pgCount').value);
328
-
329
- document.getElementById('status').innerText = "Uploading & Processing...";
330
-
331
  let r = await fetch(`/upload?sid=${sid}`, {method:'POST', body:fd});
332
- if(r.ok) monitorStatus();
333
  }
334
 
335
- function monitorStatus() {
336
  let t = setInterval(async () => {
337
  let r = await fetch(`/status?sid=${sid}`);
338
  let d = await r.json();
339
  document.getElementById('status').innerText = d.message;
340
- if(d.progress === 100) {
341
- clearInterval(t);
342
- loadEditor();
343
- }
344
  }, 1000);
345
  }
346
 
347
  async function loadEditor() {
348
  document.getElementById('upload-view').style.display='none';
349
  document.getElementById('editor-view').style.display='block';
350
-
351
  let r = await fetch(`/output/data.json?sid=${sid}`);
352
  let data = await r.json();
353
-
354
  let con = document.getElementById('container');
355
  con.innerHTML = '';
356
-
357
- data.forEach((pg, i) => {
358
- let page = document.createElement('div');
359
- page.className = 'comic-page';
360
 
361
  let grid = document.createElement('div');
362
  grid.className = 'comic-grid';
363
- grid.style.setProperty('--x', pg.splitX || '50%');
364
- grid.style.setProperty('--y', pg.splitY || '50%');
365
 
366
- // 4 Panels
367
- pg.panels.forEach(pan => {
368
  let div = document.createElement('div');
369
  div.className = 'panel';
370
- // Timestamp avoids caching blank images
371
- div.innerHTML = `<img src="/frames/${pan.image}?sid=${sid}&t=${Date.now()}">`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  grid.appendChild(div);
373
  });
374
-
375
- // Handle
376
- let handle = document.createElement('div');
377
- handle.className = 'grid-handle';
378
- handle.onmousedown = (e) => {
379
  e.stopPropagation();
380
- dragItem = 'handle';
381
- activeEl = { handle: handle, grid: grid };
382
  };
383
- grid.appendChild(handle);
384
-
 
 
 
 
 
 
 
 
 
385
  // Bubbles
386
  if(pg.bubbles) pg.bubbles.forEach(b => createBubble(b.dialog, grid));
387
-
388
- page.appendChild(grid);
389
- con.appendChild(page);
390
  });
391
  }
392
 
@@ -396,53 +398,116 @@ INDEX_HTML = '''
396
  b.contentEditable = true;
397
  b.innerText = txt || "Text";
398
  b.style.left = '50%'; b.style.top = '50%';
399
-
400
  b.onmousedown = (e) => {
401
- if(e.target !== b) return;
402
  e.stopPropagation();
403
- dragItem = 'bubble';
404
- activeEl = b;
405
  };
406
-
407
  if(!parent) parent = document.querySelector('.comic-grid');
408
- if(parent) parent.appendChild(b);
409
  }
410
-
411
  window.addBubble = () => createBubble("New Text");
412
 
413
- // DRAGGING LOGIC
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  document.addEventListener('mousemove', (e) => {
415
- if(!dragItem) return;
416
 
417
- if(dragItem === 'handle') {
418
- let rect = activeEl.grid.getBoundingClientRect();
419
- let x = e.clientX - rect.left;
420
- let y = e.clientY - rect.top;
 
 
 
 
 
421
 
422
- // Constraints (10% to 90%)
423
- let px = Math.max(10, Math.min(90, (x / rect.width) * 100));
424
- let py = Math.max(10, Math.min(90, (y / rect.height) * 100));
425
 
426
- activeEl.grid.style.setProperty('--x', px + '%');
427
- activeEl.grid.style.setProperty('--y', py + '%');
428
  }
429
- else if(dragItem === 'bubble') {
430
- let rect = activeEl.parentElement.getBoundingClientRect();
431
- activeEl.style.left = (e.clientX - rect.left) + 'px';
432
- activeEl.style.top = (e.clientY - rect.top) + 'px';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  }
434
- });
435
 
436
- document.addEventListener('mouseup', () => {
437
- dragItem = null;
438
- activeEl = null;
 
 
 
439
  });
440
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  window.downloadAll = async () => {
442
  let pgs = document.querySelectorAll('.comic-page');
443
  for(let i=0; i<pgs.length; i++) {
444
- let handles = pgs[i].querySelectorAll('.grid-handle');
445
- handles.forEach(h => h.style.display = 'none');
 
446
 
447
  let url = await htmlToImage.toPng(pgs[i]);
448
  let a = document.createElement('a');
@@ -450,7 +515,7 @@ INDEX_HTML = '''
450
  a.href = url;
451
  a.click();
452
 
453
- handles.forEach(h => h.style.display = 'block');
454
  }
455
  };
456
  </script>
@@ -459,37 +524,32 @@ INDEX_HTML = '''
459
  '''
460
 
461
  # ======================================================
462
- # 🔌 FLASK ROUTES
463
  # ======================================================
464
  @app.route('/')
465
- def index():
466
- return INDEX_HTML
467
 
468
  @app.route('/upload', methods=['POST'])
469
  def upload():
470
  sid = request.args.get('sid')
471
  f = request.files['file']
472
  pages = request.form.get('pages', 1)
473
-
474
  host = ComicGenHost(sid)
475
  f.save(host.video_path)
476
-
477
  threading.Thread(target=host.run, args=(pages,)).start()
478
- return jsonify({'ok': True})
479
 
480
  @app.route('/status')
481
  def status():
482
  sid = request.args.get('sid')
483
  p = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
484
  if os.path.exists(p): return send_file(p)
485
- return jsonify({'progress': 0, 'message': 'Waiting...'})
486
 
487
  @app.route('/frames/<path:filename>')
488
  def frames(filename):
489
  sid = request.args.get('sid')
490
- resp = send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
491
- resp.headers['Cache-Control'] = 'no-store'
492
- return resp
493
 
494
  @app.route('/output/<path:filename>')
495
  def output(filename):
@@ -497,10 +557,6 @@ def output(filename):
497
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
498
 
499
  if __name__ == '__main__':
500
- # 🚀 EXPLICIT WARMUP CALL TO REGISTER FUNCTION
501
- try:
502
- gpu_warmup()
503
- except Exception as e:
504
- print(f"⚠️ Warmup ignored (Normal if not on GPU yet): {e}")
505
-
506
  app.run(host='0.0.0.0', port=7860)
 
10
  from flask import Flask, jsonify, request, send_from_directory, send_file
11
 
12
  # ======================================================
13
+ # 🚀 ZEROGPU WARMUP
14
  # ======================================================
15
  @spaces.GPU
16
  def gpu_warmup():
17
+ """Dummy function to trigger ZeroGPU detection."""
18
  print(f"✅ GPU Warmup: CUDA Available = {torch.cuda.is_available()}")
19
  return True
20
 
 
23
  # ======================================================
24
  app = Flask(__name__)
25
 
 
26
  if os.path.exists('/data'):
27
  BASE_STORAGE_PATH = '/data'
28
  else:
 
36
  # ======================================================
37
 
38
  def create_placeholder_image(text, filename, output_dir):
39
+ """Fallback image generator."""
 
40
  img = np.zeros((800, 800, 3), dtype=np.uint8)
41
+ img[:] = (30, 30, 30) # Dark grey
42
 
43
+ # Text
44
  font = cv2.FONT_HERSHEY_SIMPLEX
45
  cv2.putText(img, text, (50, 400), font, 1.5, (200, 200, 200), 3, cv2.LINE_AA)
46
+ cv2.rectangle(img, (0,0), (800,800), (100,100,100), 10)
 
 
47
 
48
  path = os.path.join(output_dir, filename)
49
  cv2.imwrite(path, img)
 
51
 
52
  @spaces.GPU(duration=120)
53
  def generate_comic_gpu(video_path, frames_dir, target_pages):
54
+ """Extracts 4 frames per page (2x2 Grid)."""
 
 
 
 
 
55
  if os.path.exists(frames_dir): shutil.rmtree(frames_dir)
56
  os.makedirs(frames_dir, exist_ok=True)
57
 
58
  cap = cv2.VideoCapture(video_path)
 
59
  total_frames = 0
60
  duration = 0
61
 
 
64
  fps = cap.get(cv2.CAP_PROP_FPS) or 25
65
  duration = total_frames / fps
66
  else:
67
+ print("❌ Video load failed.")
68
 
 
69
  panels_per_page = 4
70
  total_panels_needed = int(target_pages) * panels_per_page
 
71
  frame_files_ordered = []
72
 
73
+ if duration > 0:
 
74
  times = np.linspace(1, max(1, duration - 1), total_panels_needed)
 
75
  for i, t in enumerate(times):
76
  cap.set(cv2.CAP_PROP_POS_MSEC, t * 1000)
77
  ret, frame = cap.read()
78
  fname = f"frame_{i:04d}.png"
 
79
  if ret and frame is not None:
80
+ # Square crop
81
  h, w = frame.shape[:2]
82
  min_dim = min(h, w)
83
+ sx = (w - min_dim) // 2
84
+ sy = (h - min_dim) // 2
85
+ frame = frame[sy:sy+min_dim, sx:sx+min_dim]
 
 
 
86
  frame = cv2.resize(frame, (800, 800))
 
87
  cv2.imwrite(os.path.join(frames_dir, fname), frame)
88
  frame_files_ordered.append(fname)
89
  else:
90
+ create_placeholder_image(f"Err {t:.1f}s", fname, frames_dir)
91
  frame_files_ordered.append(fname)
92
  cap.release()
93
  else:
 
94
  for i in range(total_panels_needed):
95
  fname = f"placeholder_{i}.png"
96
  create_placeholder_image(f"Panel {i+1}", fname, frames_dir)
97
  frame_files_ordered.append(fname)
98
 
 
99
  pages_data = []
100
  for i in range(int(target_pages)):
101
  start = i * panels_per_page
102
  end = start + panels_per_page
103
  p_frames = frame_files_ordered[start:end]
104
 
 
105
  while len(p_frames) < 4:
106
  fname = f"extra_{len(p_frames)}.png"
107
  create_placeholder_image("Empty", fname, frames_dir)
108
  p_frames.append(fname)
109
 
110
  pg_panels = [{'image': f} for f in p_frames]
 
111
  pg_bubbles = []
112
  if i == 0:
113
+ pg_bubbles.append({'dialog': "Red Dot = Move Center\nBlue Dot = Tilt Line", 'x': '50%', 'y': '50%'})
114
 
115
  pages_data.append({
116
  'panels': pg_panels,
117
+ 'bubbles': pg_bubbles
 
 
118
  })
119
 
120
  return pages_data
 
126
  self.video_path = os.path.join(self.user_dir, 'video.mp4')
127
  self.frames_dir = os.path.join(self.user_dir, 'frames')
128
  self.output_dir = os.path.join(self.user_dir, 'output')
 
129
  os.makedirs(self.user_dir, exist_ok=True)
130
  os.makedirs(self.frames_dir, exist_ok=True)
131
  os.makedirs(self.output_dir, exist_ok=True)
132
 
133
  def run(self, pages):
134
  try:
135
+ self.write_status("Processing Video...", 20)
136
  data = generate_comic_gpu(self.video_path, self.frames_dir, pages)
 
137
  with open(os.path.join(self.output_dir, 'data.json'), 'w') as f:
138
  json.dump(data, f)
 
139
  self.write_status("Ready", 100)
140
  except Exception as e:
141
  traceback.print_exc()
 
154
  <head>
155
  <meta charset="UTF-8">
156
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
157
+ <title>Advanced Comic Maker</title>
158
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
159
  <link href="https://fonts.googleapis.com/css2?family=Bangers&display=swap" rel="stylesheet">
160
  <style>
161
+ body { background: #121212; color: #eee; font-family: sans-serif; margin: 0; user-select: none; }
162
 
163
+ /* LAYOUT */
164
+ #upload-view { padding: 50px; text-align: center; }
165
  .box { background: #1e1e1e; display: inline-block; padding: 40px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); }
166
+ button { background: #e74c3c; border: none; padding: 10px 20px; font-weight: bold; cursor: pointer; border-radius: 4px; font-size: 16px; color: white; margin: 5px; }
167
  button:hover { background: #c0392b; }
 
 
 
 
 
168
 
169
+ #editor-view { display: none; padding: 20px; text-align: center; }
170
+ .comic-container { display: flex; flex-wrap: wrap; justify-content: center; gap: 40px; margin-top: 20px; padding-bottom: 100px; }
171
+
172
+ /* PAGE */
173
  .comic-page {
174
  width: 600px; height: 800px;
175
  background: white;
176
  border: 4px solid #000;
177
  position: relative;
178
+ box-shadow: 0 0 30px rgba(0,0,0,0.5);
 
179
  overflow: hidden;
180
  }
181
 
182
+ /* GRID LOGIC with Clip Path */
183
  .comic-grid {
184
  width: 100%; height: 100%;
185
  position: relative;
186
+ background: #000;
187
+ --x: 50%; /* Center X */
188
+ --y: 50%; /* Center Y */
189
+ --xt: 50%; /* Top X (Tilt) */
190
+ --xb: 50%; /* Bottom X (Tilt) */
191
+ --gap: 3px;
192
  }
193
 
194
  .panel {
195
+ position: absolute; top: 0; left: 0; width: 100%; height: 100%;
196
  overflow: hidden;
197
+ background: #222;
 
 
198
  }
199
+
200
+ /* IMAGE ZOOM/PAN */
201
  .panel img {
202
  width: 100%; height: 100%;
203
  object-fit: cover;
204
+ transform-origin: center;
205
+ cursor: grab;
206
  }
207
+ .panel img:active { cursor: grabbing; }
208
 
209
+ /* CLIP PATHS FOR TILTED PANELS */
210
  /* Top Left */
211
+ .panel:nth-child(1) {
212
+ clip-path: polygon(0 0, calc(var(--xt) - var(--gap)), calc(var(--x) - var(--gap)) calc(var(--y) - var(--gap)), 0 calc(var(--y) - var(--gap)));
213
+ z-index: 1;
214
+ }
215
  /* Top Right */
216
+ .panel:nth-child(2) {
217
+ clip-path: polygon(calc(var(--xt) + var(--gap)) 0, 100% 0, 100% calc(var(--y) - var(--gap)), calc(var(--x) + var(--gap)) calc(var(--y) - var(--gap)));
218
+ z-index: 1;
219
+ }
220
  /* Bottom Left */
221
+ .panel:nth-child(3) {
222
+ clip-path: polygon(0 calc(var(--y) + var(--gap)), calc(var(--x) - var(--gap)) calc(var(--y) + var(--gap)), calc(var(--xb) - var(--gap)) 100%, 0 100%);
223
+ z-index: 1;
224
+ }
225
  /* Bottom Right */
226
+ .panel:nth-child(4) {
227
+ clip-path: polygon(calc(var(--x) + var(--gap)) calc(var(--y) + var(--gap)), 100% calc(var(--y) + var(--gap)), 100% 100%, calc(var(--xb) + var(--gap)) 100%);
228
+ z-index: 1;
229
+ }
230
 
231
+ /* HANDLES */
232
+ .handle-center {
233
  position: absolute;
234
+ width: 20px; height: 20px;
235
+ background: #e74c3c; /* RED */
236
+ border: 2px solid white; border-radius: 50%;
 
237
  left: var(--x); top: var(--y);
238
  transform: translate(-50%, -50%);
239
+ cursor: move; z-index: 999;
240
+ box-shadow: 0 0 5px black;
241
+ }
242
+
243
+ .handle-tilt {
244
+ position: absolute;
245
+ width: 16px; height: 16px;
246
+ background: #3498db; /* BLUE */
247
+ border: 2px solid white; border-radius: 50%;
248
+ left: var(--xt); top: 0%;
249
+ transform: translate(-50%, 50%); /* Just below top edge */
250
+ cursor: ew-resize; z-index: 999;
251
+ box-shadow: 0 0 5px black;
252
  }
 
253
 
254
  /* BUBBLES */
255
  .bubble {
256
  position: absolute;
257
  background: white; color: black;
258
+ padding: 8px 12px; border-radius: 15px;
259
+ font-family: 'Bangers', cursive; letter-spacing: 1px;
260
+ border: 2px solid black; z-index: 100; cursor: move;
 
 
261
  transform: translate(-50%, -50%);
262
+ text-align: center; min-width: 60px;
263
+ box-shadow: 4px 4px 0 rgba(0,0,0,0.2);
 
 
 
 
 
264
  }
265
 
 
266
  .toolbar {
267
  position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
268
  background: #333; padding: 10px 20px; border-radius: 50px;
269
+ display: flex; gap: 10px; z-index: 1000; box-shadow: 0 5px 20px rgba(0,0,0,0.6);
270
  }
271
+ .info-tip { color: #888; margin-top: 10px; font-size: 0.9em; }
272
  </style>
273
  </head>
274
  <body>
275
 
276
  <div id="upload-view">
277
  <div class="box">
278
+ <h1>🎞️ Advanced Comic Maker</h1>
279
+ <p>Zoom, Pan, Tilt & Resize!</p>
280
+ <input type="file" id="fileIn" accept="video/*"><br><br>
281
  <label>Pages:</label> <input type="number" id="pgCount" value="1" min="1" max="5" style="width:50px;">
282
+ <br><br>
283
  <button onclick="startUpload()">🚀 Generate</button>
284
+ <p id="status" style="color:#aaa; margin-top:10px;"></p>
285
  </div>
286
  </div>
287
 
288
  <div id="editor-view">
 
289
  <div class="comic-container" id="container"></div>
 
290
  <div class="toolbar">
291
+ <button onclick="addBubble()">💬 Text</button>
292
+ <button onclick="downloadAll()">💾 Save</button>
293
+ <button style="background:#555" onclick="location.reload()">↺ Reset</button>
294
+ </div>
295
+ <div class="info-tip">
296
+ <b>Controls:</b> Red Dot = Resize Center | Blue Dot = Tilt | Scroll Image = Zoom | Drag Image = Pan
297
  </div>
298
  </div>
299
 
300
  <script>
301
  let sid = 'S' + Date.now();
302
+ let dragType = null; // 'center', 'tilt', 'bubble', 'pan'
303
+ let activeObj = null;
304
+ let dragStart = {x:0, y:0};
305
+
306
+ // IMAGE STATE STORAGE: { imgId: {scale:1, tx:0, ty:0} }
307
+ let imgStates = new Map();
308
 
309
  async function startUpload() {
310
  let f = document.getElementById('fileIn').files[0];
311
+ if(!f) return alert("Select video");
 
312
  let fd = new FormData();
313
  fd.append('file', f);
314
  fd.append('pages', document.getElementById('pgCount').value);
315
+ document.getElementById('status').innerText = "Uploading...";
 
 
316
  let r = await fetch(`/upload?sid=${sid}`, {method:'POST', body:fd});
317
+ if(r.ok) checkStatus();
318
  }
319
 
320
+ function checkStatus() {
321
  let t = setInterval(async () => {
322
  let r = await fetch(`/status?sid=${sid}`);
323
  let d = await r.json();
324
  document.getElementById('status').innerText = d.message;
325
+ if(d.progress >= 100) { clearInterval(t); loadEditor(); }
 
 
 
326
  }, 1000);
327
  }
328
 
329
  async function loadEditor() {
330
  document.getElementById('upload-view').style.display='none';
331
  document.getElementById('editor-view').style.display='block';
 
332
  let r = await fetch(`/output/data.json?sid=${sid}`);
333
  let data = await r.json();
 
334
  let con = document.getElementById('container');
335
  con.innerHTML = '';
336
+
337
+ data.forEach(pg => {
338
+ let pDiv = document.createElement('div');
339
+ pDiv.className = 'comic-page';
340
 
341
  let grid = document.createElement('div');
342
  grid.className = 'comic-grid';
 
 
343
 
344
+ // Panels
345
+ pg.panels.forEach((pan, idx) => {
346
  let div = document.createElement('div');
347
  div.className = 'panel';
348
+ let img = document.createElement('img');
349
+ img.src = `/frames/${pan.image}?sid=${sid}`;
350
+ img.id = `img-${Math.random().toString(36).substr(2,9)}`;
351
+
352
+ // Initialize State
353
+ imgStates.set(img.id, {s: 1, tx: 0, ty: 0});
354
+
355
+ // Zoom Listener
356
+ img.onwheel = (e) => handleZoom(e, img);
357
+ // Pan Listener (Start)
358
+ img.onmousedown = (e) => {
359
+ e.preventDefault(); e.stopPropagation();
360
+ dragType = 'pan';
361
+ activeObj = img;
362
+ dragStart = {x: e.clientX, y: e.clientY};
363
+ };
364
+
365
+ div.appendChild(img);
366
  grid.appendChild(div);
367
  });
368
+
369
+ // Center Handle (Red)
370
+ let hc = document.createElement('div');
371
+ hc.className = 'handle-center';
372
+ hc.onmousedown = (e) => {
373
  e.stopPropagation();
374
+ dragType = 'center'; activeObj = grid;
 
375
  };
376
+ grid.appendChild(hc);
377
+
378
+ // Tilt Handle (Blue)
379
+ let ht = document.createElement('div');
380
+ ht.className = 'handle-tilt';
381
+ ht.onmousedown = (e) => {
382
+ e.stopPropagation();
383
+ dragType = 'tilt'; activeObj = grid;
384
+ };
385
+ grid.appendChild(ht);
386
+
387
  // Bubbles
388
  if(pg.bubbles) pg.bubbles.forEach(b => createBubble(b.dialog, grid));
389
+
390
+ pDiv.appendChild(grid);
391
+ con.appendChild(pDiv);
392
  });
393
  }
394
 
 
398
  b.contentEditable = true;
399
  b.innerText = txt || "Text";
400
  b.style.left = '50%'; b.style.top = '50%';
 
401
  b.onmousedown = (e) => {
 
402
  e.stopPropagation();
403
+ dragType = 'bubble'; activeObj = b;
 
404
  };
 
405
  if(!parent) parent = document.querySelector('.comic-grid');
406
+ parent.appendChild(b);
407
  }
 
408
  window.addBubble = () => createBubble("New Text");
409
 
410
+ // === INTERACTION LOGIC ===
411
+
412
+ // Zoom
413
+ function handleZoom(e, img) {
414
+ e.preventDefault();
415
+ let st = imgStates.get(img.id);
416
+ let delta = e.deltaY * -0.001;
417
+ st.s = Math.min(Math.max(0.5, st.s + delta), 5); // Limit Zoom 0.5x to 5x
418
+ updateImgTransform(img);
419
+ }
420
+
421
+ function updateImgTransform(img) {
422
+ let st = imgStates.get(img.id);
423
+ img.style.transform = `translate(${st.tx}px, ${st.ty}px) scale(${st.s})`;
424
+ }
425
+
426
+ // Global Move
427
  document.addEventListener('mousemove', (e) => {
428
+ if(!dragType) return;
429
 
430
+ // 1. Resize Center (Red Dot)
431
+ if(dragType === 'center') {
432
+ let rect = activeObj.getBoundingClientRect();
433
+ let x = (e.clientX - rect.left) / rect.width * 100;
434
+ let y = (e.clientY - rect.top) / rect.height * 100;
435
+
436
+ // Allow 0 to 100%
437
+ x = Math.max(0, Math.min(100, x));
438
+ y = Math.max(0, Math.min(100, y));
439
 
440
+ activeObj.style.setProperty('--x', x + '%');
441
+ activeObj.style.setProperty('--y', y + '%');
 
442
 
443
+ // Recalculate Tilt Bottom (Geometry)
444
+ updateTiltGeometry(activeObj);
445
  }
446
+
447
+ // 2. Tilt (Blue Dot)
448
+ else if(dragType === 'tilt') {
449
+ let rect = activeObj.getBoundingClientRect();
450
+ let x = (e.clientX - rect.left) / rect.width * 100;
451
+ x = Math.max(0, Math.min(100, x)); // 0-100%
452
+
453
+ activeObj.style.setProperty('--xt', x + '%');
454
+ updateTiltGeometry(activeObj);
455
+ }
456
+
457
+ // 3. Pan Image
458
+ else if(dragType === 'pan') {
459
+ let dx = e.clientX - dragStart.x;
460
+ let dy = e.clientY - dragStart.y;
461
+ let st = imgStates.get(activeObj.id);
462
+ st.tx += dx;
463
+ st.ty += dy;
464
+ dragStart = {x: e.clientX, y: e.clientY};
465
+ updateImgTransform(activeObj);
466
  }
 
467
 
468
+ // 4. Move Bubble
469
+ else if(dragType === 'bubble') {
470
+ let rect = activeObj.parentElement.getBoundingClientRect();
471
+ activeObj.style.left = (e.clientX - rect.left) + 'px';
472
+ activeObj.style.top = (e.clientY - rect.top) + 'px';
473
+ }
474
  });
475
 
476
+ document.addEventListener('mouseup', () => { dragType = null; activeObj = null; });
477
+
478
+ // Calculates the Bottom X based on Top X and Center X to form a straight line
479
+ function updateTiltGeometry(grid) {
480
+ // Need to read computed styles roughly or rely on inline styles
481
+ // Since we set properties on the style attribute, we can parse them
482
+ let cx = parseFloat(grid.style.getPropertyValue('--x')) || 50;
483
+ let cy = parseFloat(grid.style.getPropertyValue('--y')) || 50;
484
+ let xt = parseFloat(grid.style.getPropertyValue('--xt')) || 50;
485
+
486
+ // Math: We have Point Top (xt, 0) and Point Center (cx, cy).
487
+ // We want Point Bottom (xb, 100) to be on the same line.
488
+ // Slope m = (cy - 0) / (cx - xt) = cy / (cx - xt)
489
+ // Line eq: y - 0 = m * (x - xt) => y = m(x - xt)
490
+ // At bottom, y = 100.
491
+ // 100 = (cy / (cx - xt)) * (xb - xt)
492
+ // 100 * (cx - xt) / cy = xb - xt
493
+ // xb = xt + (100/cy) * (cx - xt)
494
+
495
+ if(cy === 0) cy = 0.1; // Avoid divide by zero
496
+
497
+ let xb = xt + (100 / cy) * (cx - xt);
498
+
499
+ // Clamp visually so it doesn't fly off screen too wildly
500
+ // xb = Math.max(-50, Math.min(150, xb));
501
+
502
+ grid.style.setProperty('--xb', xb + '%');
503
+ }
504
+
505
  window.downloadAll = async () => {
506
  let pgs = document.querySelectorAll('.comic-page');
507
  for(let i=0; i<pgs.length; i++) {
508
+ // Hide handles
509
+ let handles = pgs[i].querySelectorAll('.handle-center, .handle-tilt');
510
+ handles.forEach(h => h.style.display='none');
511
 
512
  let url = await htmlToImage.toPng(pgs[i]);
513
  let a = document.createElement('a');
 
515
  a.href = url;
516
  a.click();
517
 
518
+ handles.forEach(h => h.style.display='block');
519
  }
520
  };
521
  </script>
 
524
  '''
525
 
526
  # ======================================================
527
+ # 🔌 ROUTES
528
  # ======================================================
529
  @app.route('/')
530
+ def index(): return INDEX_HTML
 
531
 
532
  @app.route('/upload', methods=['POST'])
533
  def upload():
534
  sid = request.args.get('sid')
535
  f = request.files['file']
536
  pages = request.form.get('pages', 1)
 
537
  host = ComicGenHost(sid)
538
  f.save(host.video_path)
 
539
  threading.Thread(target=host.run, args=(pages,)).start()
540
+ return jsonify({'ok':True})
541
 
542
  @app.route('/status')
543
  def status():
544
  sid = request.args.get('sid')
545
  p = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
546
  if os.path.exists(p): return send_file(p)
547
+ return jsonify({'progress':0, 'message':'Waiting...'})
548
 
549
  @app.route('/frames/<path:filename>')
550
  def frames(filename):
551
  sid = request.args.get('sid')
552
+ return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
 
 
553
 
554
  @app.route('/output/<path:filename>')
555
  def output(filename):
 
557
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
558
 
559
  if __name__ == '__main__':
560
+ try: gpu_warmup()
561
+ except: pass
 
 
 
 
562
  app.run(host='0.0.0.0', port=7860)