tester343 commited on
Commit
5ccb565
·
verified ·
1 Parent(s): 989bb28

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +68 -96
app_enhanced.py CHANGED
@@ -1,12 +1,9 @@
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
@@ -29,7 +26,7 @@ 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 SERIALIZATION ERROR)
33
  # ======================================================
34
  def sanitize_json(obj):
35
  if isinstance(obj, dict):
@@ -49,10 +46,7 @@ def sanitize_json(obj):
49
  # ======================================================
50
  @spaces.GPU(duration=300)
51
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
52
- # Heavy imports inside function to avoid startup timeout
53
- import cv2
54
- import srt
55
- import numpy as np
56
  from backend.keyframes.keyframes import black_bar_crop
57
  from backend.simple_color_enhancer import SimpleColorEnhancer
58
  from backend.subtitles.subs_real import get_real_subtitles
@@ -71,7 +65,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
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: f.write("1\n00:00:01,000 --> 00:00:05,000\n...\n")
75
 
76
  with open(user_srt, 'r', encoding='utf-8') as f:
77
  try: all_subs = list(srt.parse(f.read()))
@@ -92,7 +86,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
92
  indices = np.linspace(0, len(all_subs) - 1, total_needed, dtype=int)
93
  moments = [{'text': all_subs[i].content, 'start': all_subs[i].start.total_seconds()} for i in indices]
94
 
95
- # 3. Extraction
96
  frame_metadata = {}
97
  cap = cv2.VideoCapture(video_path)
98
  frame_files = []
@@ -113,12 +107,11 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
113
  try: black_bar_crop()
114
  except: pass
115
 
116
- # 4. Enhance and Organize
117
  se = SimpleColorEnhancer()
118
  pages_data = []
119
  for p_idx in range(target_pages):
120
- p_panels = []
121
- p_bubbles = []
122
  start = p_idx * 5
123
  for i in range(start, start + 5):
124
  if i >= len(frame_files): break
@@ -132,18 +125,18 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
132
  faces = face_detector.detect_faces(img_p)
133
  lip = face_detector.get_lip_position(img_p, faces[0]) if faces else (-1, -1)
134
  bx, by = ai_bubble_placer.place_bubble_ai(img_p, lip)
135
- bubble_item = {'dialog': txt, 'x': bx, 'y': by}
136
  except:
137
- bubble_item = {'dialog': txt, 'x': 50, 'y': 25}
138
 
139
- p_panels.append({'image': f_name})
140
- p_bubbles.append(bubble_item)
141
- pages_data.append({'panels': p_panels, 'bubbles': p_bubbles})
142
 
143
  return sanitize_json(pages_data)
144
 
145
  # ======================================================
146
- # 🔧 APP SETUP
147
  # ======================================================
148
  app = Flask(__name__)
149
 
@@ -163,7 +156,7 @@ def uploader():
163
  def task():
164
  try:
165
  with open(os.path.join(o_dir, 'status.json'), 'w') as f:
166
- json.dump({'message': 'Building Geometric Ratios...', 'progress': 30}, f)
167
  data = generate_comic_gpu(vid_p, u_dir, f_dir, os.path.join(f_dir, 'meta.json'), pages)
168
  with open(os.path.join(o_dir, 'pages.json'), 'w') as f: json.dump(data, f)
169
  with open(os.path.join(o_dir, 'status.json'), 'w') as f:
@@ -180,7 +173,7 @@ def status():
180
  sid = request.args.get('sid')
181
  p = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
182
  if os.path.exists(p): return send_file(p)
183
- return jsonify({'progress': 0, 'message': 'Initializing...'})
184
 
185
  @app.route('/frames/<sid>/<path:filename>')
186
  def get_frame(sid, filename):
@@ -191,108 +184,87 @@ def get_output(sid, filename):
191
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
192
 
193
  # ======================================================
194
- # 🌐 HTML (PERFECT THIN TILTED LINES TEMPLATE)
195
  # ======================================================
196
  INDEX_HTML = '''
197
  <!DOCTYPE html><html lang="en">
198
  <head>
199
- <meta charset="UTF-8"><title>High-Fidelity Comic Generator</title>
200
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
201
- <link href="https://fonts.googleapis.com/css2?family=Comic+Neue:wght@700&family=Lato&display=swap" rel="stylesheet">
202
  <style>
203
- :root {
204
- --slant: 35px; /* Consistent Tilt Angle */
205
- --thin-line: 8px; /* Consistent White Gutters */
206
- --page-border: 12px;
207
- }
208
-
209
- body { background: #1a1a1a; font-family: 'Lato', sans-serif; margin: 0; padding: 20px; }
210
- .setup-view { max-width: 450px; margin: 100px auto; background: white; padding: 40px; border-radius: 12px; text-align: center; color: black; }
211
-
212
- /* ⚡ THE PIXEL-PERFECT ASYMMETRICAL TEMPLATE ⚡ */
213
- .comic-page {
214
- background: white; width: 1000px; height: 750px; margin: 30px auto;
215
- border: var(--page-border) solid black; padding: 10px; box-sizing: border-box;
216
- display: grid; gap: var(--thin-line);
217
- grid-template-columns: repeat(6, 1fr);
218
- grid-template-rows: 1.3fr 1fr;
219
- position: relative; overflow: hidden;
220
  }
221
 
222
- .panel { position: relative; background: #000; overflow: hidden; cursor: pointer; border: 3px solid black; }
223
- /* Scale images to cover clip-path areas without showing black backgrounds */
224
- .panel img { width: 115%; height: 115%; object-fit: cover; position: absolute; top: -7.5%; left: -7.5%; object-position: center 15%; pointer-events: none; }
225
 
226
- /* ROW 1: Slant LEFT \ (Panel 1 Wide, Panel 2 Narrow) */
227
- .panel:nth-child(1) {
228
- grid-column: span 4;
229
- clip-path: polygon(0 0, 100% 0, calc(100% - var(--slant)) 100%, 0 100%);
230
- }
231
- .panel:nth-child(2) {
232
- grid-column: span 2;
233
- clip-path: polygon(var(--slant) 0, 100% 0, 100% 100%, 0 100%);
234
  }
235
 
236
- /* ROW 2: Slant RIGHT / (Three equal action panels) */
237
- .panel:nth-child(3) {
238
- grid-column: span 2;
239
- clip-path: polygon(0 0, calc(100% - var(--slant)) 0, 100% 100%, 0 100%);
240
- }
241
- .panel:nth-child(4) {
242
- grid-column: span 2;
243
- clip-path: polygon(var(--slant) 0, calc(100% - var(--slant)) 0, 100% 100%, var(--slant) 100%);
244
- }
245
- .panel:nth-child(5) {
246
- grid-column: span 2;
247
- clip-path: polygon(var(--slant) 0, 100% 0, 100% 100%, var(--slant) 100%);
248
- }
249
 
250
- .panel.selected { outline: 6px solid #f39c12; z-index: 5; filter: contrast(1.1); }
 
 
 
 
 
 
 
 
 
 
 
251
 
252
- /* BUBBLE STYLING - PRECISE WHITE CAPSULE */
253
  .bubble {
254
  position: absolute; background: white; border: 2.5px solid black; border-radius: 25px;
255
- padding: 10px 20px; font-family: 'Comic Neue'; font-weight: bold; font-size: 16px;
256
- color: black; min-width: 120px; text-align: center; cursor: move; z-index: 10;
257
- box-shadow: 4px 4px 0 rgba(0,0,0,0.1);
258
- }
259
- .bubble::after {
260
- content: ""; position: absolute; bottom: -18px; left: 30px;
261
- width: 0; height: 0; border-left: 10px solid transparent;
262
- border-right: 10px solid transparent; border-top: 20px solid black;
263
- }
264
- .bubble::before {
265
- content: ""; position: absolute; bottom: -13px; left: 31px;
266
- width: 0; height: 0; border-left: 9px solid transparent;
267
- border-right: 9px solid transparent; border-top: 17px solid white;
268
- z-index: 2;
269
  }
270
 
271
- .controls { position: fixed; bottom: 20px; right: 20px; background: #000; padding: 25px; border-radius: 12px; width: 240px; border: 1px solid #333; }
272
- button { width: 100%; padding: 12px; margin-top: 10px; cursor: pointer; font-weight: bold; border-radius: 6px; border: none; }
273
  .hidden { display: none; }
274
- .loader { border: 5px solid #333; border-top: 5px solid #e67e22; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; }
275
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
276
  </style>
277
  </head>
278
  <body>
279
- <div id="upload-zone" class="setup-view">
280
- <h1>🎬 High-Fidelity Comic</h1>
281
- <p>Consistent tilted lines & accurate ratios</p>
282
  <input type="file" id="vid" accept="video/mp4"><br><br>
283
  <label>Total Pages: </label><input type="number" id="pg" value="2" style="width:50px">
284
  <br><br>
285
- <button onclick="start()" style="background:#e67e22; color:white;">Generate Comic</button>
286
  <div id="loading" class="hidden"><div class="loader"></div><p id="st">Acquiring GPU...</p></div>
287
  </div>
288
 
289
  <div id="editor-zone" class="hidden">
290
  <div id="output"></div>
291
  <div class="controls">
292
- <h4 style="margin:0; color:white;">Interactive Editor</h4>
293
  <button onclick="addB()" style="background:#2ecc71; color:white;">💬 Add Bubble</button>
294
  <button onclick="exportPNG()" style="background:#3498db; color:white;">📥 Download PNGs</button>
295
- <button onclick="location.reload()" style="background:#555; color:white;">🏠 Reset</button>
296
  </div>
297
  </div>
298
 
@@ -308,21 +280,21 @@ INDEX_HTML = '''
308
  await fetch(`/uploader?sid=${sid}`, {method: 'POST', body: fd});
309
  const itv = setInterval(async () => {
310
  const r = await fetch(`/status?sid=${sid}`); const d = await r.json();
311
- document.getElementById('st').innerText = d.message;
312
  if(d.progress >= 100) { clearInterval(itv); load(); }
313
  }, 2000);
314
  }
315
 
316
  async function load() {
317
  const r = await fetch(`/output/${sid}/pages.json`); const pages = await r.json();
318
- document.getElementById('upload-zone').classList.add('hidden');
319
  document.getElementById('editor-zone').classList.remove('hidden');
320
  const out = document.getElementById('output');
321
  pages.forEach(p => {
322
  const pgDiv = document.createElement('div'); pgDiv.className = 'comic-page';
323
  p.panels.forEach((pan, i) => {
324
- const pDiv = document.createElement('div'); pDiv.className = 'panel';
325
- pDiv.onclick = (e) => { e.stopPropagation(); if(selP) selP.classList.remove('selected'); selP=pDiv; pDiv.classList.add('selected'); };
326
  const img = document.createElement('img'); img.src = `/frames/${sid}/${pan.image}`;
327
  pDiv.appendChild(img);
328
  if(p.bubbles[i]) pDiv.appendChild(createB(p.bubbles[i].dialog, p.bubbles[i].x, p.bubbles[i].y));
@@ -345,7 +317,7 @@ INDEX_HTML = '''
345
  return b;
346
  }
347
 
348
- function addB() { if(selP) selP.appendChild(createB("Dialogue", 60, 60)); else alert("Select a panel first!"); }
349
 
350
  async function exportPNG() {
351
  const pgs = document.querySelectorAll('.comic-page');
 
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 shutil
8
  import cv2
9
  import numpy as np
 
26
  os.makedirs(BASE_USER_DIR, exist_ok=True)
27
 
28
  # ======================================================
29
+ # 🔧 JSON SANITIZER (FIX FOR int64 ERROR)
30
  # ======================================================
31
  def sanitize_json(obj):
32
  if isinstance(obj, dict):
 
46
  # ======================================================
47
  @spaces.GPU(duration=300)
48
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
49
+ # Heavy AI imports inside function to avoid Startup Timeout
 
 
 
50
  from backend.keyframes.keyframes import black_bar_crop
51
  from backend.simple_color_enhancer import SimpleColorEnhancer
52
  from backend.subtitles.subs_real import get_real_subtitles
 
65
  get_real_subtitles(video_path)
66
  if os.path.exists('test1.srt'): shutil.move('test1.srt', user_srt)
67
  except:
68
+ with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:05,000\nDialogue...\n")
69
 
70
  with open(user_srt, 'r', encoding='utf-8') as f:
71
  try: all_subs = list(srt.parse(f.read()))
 
86
  indices = np.linspace(0, len(all_subs) - 1, total_needed, dtype=int)
87
  moments = [{'text': all_subs[i].content, 'start': all_subs[i].start.total_seconds()} for i in indices]
88
 
89
+ # 3. Frame Extraction
90
  frame_metadata = {}
91
  cap = cv2.VideoCapture(video_path)
92
  frame_files = []
 
107
  try: black_bar_crop()
108
  except: pass
109
 
110
+ # 4. Enhance & Assemble
111
  se = SimpleColorEnhancer()
112
  pages_data = []
113
  for p_idx in range(target_pages):
114
+ p_p, p_b = [], []
 
115
  start = p_idx * 5
116
  for i in range(start, start + 5):
117
  if i >= len(frame_files): break
 
125
  faces = face_detector.detect_faces(img_p)
126
  lip = face_detector.get_lip_position(img_p, faces[0]) if faces else (-1, -1)
127
  bx, by = ai_bubble_placer.place_bubble_ai(img_p, lip)
128
+ item = {'dialog': txt, 'x': bx, 'y': by}
129
  except:
130
+ item = {'dialog': txt, 'x': 50, 'y': 25}
131
 
132
+ p_p.append({'image': f_name})
133
+ p_b.append(item)
134
+ pages_data.append({'panels': p_p, 'bubbles': p_b})
135
 
136
  return sanitize_json(pages_data)
137
 
138
  # ======================================================
139
+ # 🔧 APP ENGINE
140
  # ======================================================
141
  app = Flask(__name__)
142
 
 
156
  def task():
157
  try:
158
  with open(os.path.join(o_dir, 'status.json'), 'w') as f:
159
+ json.dump({'message': 'Generating Panels...', 'progress': 30}, f)
160
  data = generate_comic_gpu(vid_p, u_dir, f_dir, os.path.join(f_dir, 'meta.json'), pages)
161
  with open(os.path.join(o_dir, 'pages.json'), 'w') as f: json.dump(data, f)
162
  with open(os.path.join(o_dir, 'status.json'), 'w') as f:
 
173
  sid = request.args.get('sid')
174
  p = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
175
  if os.path.exists(p): return send_file(p)
176
+ return jsonify({'progress': 0})
177
 
178
  @app.route('/frames/<sid>/<path:filename>')
179
  def get_frame(sid, filename):
 
184
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
185
 
186
  # ======================================================
187
+ # 🌐 UI HTML (USING YOUR FINAL COORDINATES)
188
  # ======================================================
189
  INDEX_HTML = '''
190
  <!DOCTYPE html><html lang="en">
191
  <head>
192
+ <meta charset="UTF-8"><title>Elite Geometric Comic Maker</title>
193
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
194
+ <link href="https://fonts.googleapis.com/css2?family=Comic+Neue:wght@700&family=Lato:wght@400;900&display=swap" rel="stylesheet">
195
  <style>
196
+ /* COORDINATES INJECTED FROM YOUR TOOL */
197
+ :root {
198
+ --w: 1000px;
199
+ --h: 700px;
200
+ --tierY: 350px;
201
+ --gut: 5.25px; /* Half of 10.5px Gutter */
202
+
203
+ --r1-topX: 641.2px;
204
+ --r1-botX: 588.2px;
205
+
206
+ --r2L-topX: 284.2px;
207
+ --r2L-botX: 314.2px;
208
+
209
+ --r2R-topX: 618.2px;
210
+ --r2R-botX: 678.2px;
 
 
211
  }
212
 
213
+ body { background: #111; font-family: 'Lato', sans-serif; margin: 0; padding: 20px; color: white; }
214
+ .setup-box { max-width: 450px; margin: 80px auto; background: white; padding: 40px; border-radius: 12px; color: black; text-align: center; }
 
215
 
216
+ .comic-page {
217
+ background: white; width: var(--w); height: var(--h); margin: 40px auto;
218
+ border: 12px solid black; position: relative; overflow: hidden;
 
 
 
 
 
219
  }
220
 
221
+ .panel { position: absolute; background: #000; overflow: hidden; cursor: pointer; }
222
+ .panel img { width: 120%; height: 120%; object-fit: cover; position: absolute; top: -10%; left: -10%; pointer-events: none; }
 
 
 
 
 
 
 
 
 
 
 
223
 
224
+ /* YOUR PRECISE GEOMETRY */
225
+ /* Panel 1 (Top Left) */
226
+ .p1 { top: 0; left: 0; width: 100%; height: var(--tierY); clip-path: polygon(0 0, calc(var(--r1-topX) - var(--gut)) 0, calc(var(--r1-botX) - var(--gut)) var(--tierY), 0 var(--tierY)); }
227
+ /* Panel 2 (Top Right) */
228
+ .p2 { top: 0; left: 0; width: 100%; height: var(--tierY); clip-path: polygon(calc(var(--r1-topX) + var(--gut)) 0, var(--w) 0, var(--w) var(--tierY), calc(var(--r1-botX) + var(--gut)) var(--tierY)); }
229
+
230
+ /* Panel 3 (Bottom Left) */
231
+ .p3 { top: calc(var(--tierY) + var(--gut)); left: 0; width: 100%; height: calc(var(--h) - var(--tierY)); clip-path: polygon(0 0, calc(var(--r2L-topX) - var(--gut)) 0, calc(var(--r2L-botX) - var(--gut)) 100%, 0 100%); }
232
+ /* Panel 4 (Bottom Center) */
233
+ .p4 { top: calc(var(--tierY) + var(--gut)); left: 0; width: 100%; height: calc(var(--h) - var(--tierY)); clip-path: polygon(calc(var(--r2L-topX) + var(--gut)) 0, calc(var(--r2R-topX) - var(--gut)) 0, calc(var(--r2R-botX) - var(--gut)) 100%, calc(var(--r2L-botX) + var(--gut)) 100%); }
234
+ /* Panel 5 (Bottom Right) */
235
+ .p5 { top: calc(var(--tierY) + var(--gut)); left: 0; width: 100%; height: calc(var(--h) - var(--tierY)); clip-path: polygon(calc(var(--r2R-topX) + var(--gut)) 0, var(--w) 0, var(--w) 100%, calc(var(--r2R-botX) + var(--gut)) 100%); }
236
 
 
237
  .bubble {
238
  position: absolute; background: white; border: 2.5px solid black; border-radius: 25px;
239
+ padding: 10px 18px; font-family: 'Comic Neue'; font-weight: bold; font-size: 15px;
240
+ color: black; min-width: 100px; text-align: center; cursor: move; z-index: 10;
 
 
 
 
 
 
 
 
 
 
 
 
241
  }
242
 
243
+ .controls { position: fixed; bottom: 20px; right: 20px; background: #000; padding: 20px; border-radius: 12px; width: 220px; border: 2px solid #333; }
244
+ button { width: 100%; padding: 10px; margin-top: 10px; cursor: pointer; font-weight: bold; border-radius: 6px; border: none; }
245
  .hidden { display: none; }
246
+ .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; }
247
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
248
  </style>
249
  </head>
250
  <body>
251
+ <div id="u-zone" class="setup-box">
252
+ <h1>🎬 Elite Comic Maker</h1>
253
+ <p>Using Final Dragged Coordinates</p>
254
  <input type="file" id="vid" accept="video/mp4"><br><br>
255
  <label>Total Pages: </label><input type="number" id="pg" value="2" style="width:50px">
256
  <br><br>
257
+ <button onclick="start()" style="background:#00d2ff; color:black;">🚀 GENERATE COMIC</button>
258
  <div id="loading" class="hidden"><div class="loader"></div><p id="st">Acquiring GPU...</p></div>
259
  </div>
260
 
261
  <div id="editor-zone" class="hidden">
262
  <div id="output"></div>
263
  <div class="controls">
264
+ <h4 style="margin:0; color:#00d2ff;">EDITOR</h4>
265
  <button onclick="addB()" style="background:#2ecc71; color:white;">💬 Add Bubble</button>
266
  <button onclick="exportPNG()" style="background:#3498db; color:white;">📥 Download PNGs</button>
267
+ <button onclick="location.reload()" style="background:#e74c3c; color:white;">🏠 Reset</button>
268
  </div>
269
  </div>
270
 
 
280
  await fetch(`/uploader?sid=${sid}`, {method: 'POST', body: fd});
281
  const itv = setInterval(async () => {
282
  const r = await fetch(`/status?sid=${sid}`); const d = await r.json();
283
+ document.getElementById('st').innerText = d.message || "Working...";
284
  if(d.progress >= 100) { clearInterval(itv); load(); }
285
  }, 2000);
286
  }
287
 
288
  async function load() {
289
  const r = await fetch(`/output/${sid}/pages.json`); const pages = await r.json();
290
+ document.getElementById('u-zone').classList.add('hidden');
291
  document.getElementById('editor-zone').classList.remove('hidden');
292
  const out = document.getElementById('output');
293
  pages.forEach(p => {
294
  const pgDiv = document.createElement('div'); pgDiv.className = 'comic-page';
295
  p.panels.forEach((pan, i) => {
296
+ const pDiv = document.createElement('div'); pDiv.className = 'panel p' + (i+1);
297
+ pDiv.onclick = (e) => { e.stopPropagation(); selP=pDiv; };
298
  const img = document.createElement('img'); img.src = `/frames/${sid}/${pan.image}`;
299
  pDiv.appendChild(img);
300
  if(p.bubbles[i]) pDiv.appendChild(createB(p.bubbles[i].dialog, p.bubbles[i].x, p.bubbles[i].y));
 
317
  return b;
318
  }
319
 
320
+ function addB() { if(selP) selP.appendChild(createB("Dialogue", 60, 60)); }
321
 
322
  async function exportPNG() {
323
  const pgs = document.querySelectorAll('.comic-page');