tester343 commited on
Commit
2ccb001
·
verified ·
1 Parent(s): c6e766b

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +114 -110
app_enhanced.py CHANGED
@@ -1,36 +1,27 @@
1
- import spaces # <--- MUST BE LINE 1
2
- from flask import Flask, jsonify, request, send_from_directory, send_file
3
  import os
4
- import time
5
- import threading
6
- import json
7
- import traceback
8
- import string
9
- import random
10
- import shutil
11
- import cv2
12
- import numpy as np
13
 
14
  # ======================================================
15
- # 🧠 ZEROGPU FUNCTIONS (Defined early for detection)
16
  # ======================================================
17
 
18
  @spaces.GPU(duration=120)
19
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages, panels_per_page_req):
20
- """ZeroGPU function to extract frames and metadata"""
21
- cap = cv2.VideoCapture(video_path)
22
- if not cap.isOpened(): raise Exception("Video open failed")
23
 
 
 
24
  fps = cap.get(cv2.CAP_PROP_FPS) or 25
25
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
26
  duration = total_frames / fps
27
 
28
- target_pages = int(target_pages)
29
- panels_per_page = int(panels_per_page_req)
30
- total_panels = target_pages * panels_per_page
31
 
32
- # Simple logic to grab frames at intervals
33
- timestamps = np.linspace(0.5, max(0.5, duration - 0.5), total_panels)
34
  frame_metadata = {}
35
  frame_files = []
36
 
@@ -48,10 +39,9 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
48
  json.dump(frame_metadata, f)
49
 
50
  pages = []
51
- for i in range(target_pages):
52
- start = i * panels_per_page
53
- end = start + panels_per_page
54
- subset = frame_files[start:end]
55
  if subset:
56
  pages.append({
57
  'panels': [{'image': f} for f in subset],
@@ -61,7 +51,8 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
61
 
62
  @spaces.GPU
63
  def regen_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
64
- """Adjusts frame time forward or backward"""
 
65
  with open(metadata_path, 'r') as f: meta = json.load(f)
66
  t = meta[fname]['time']
67
  cap = cv2.VideoCapture(video_path)
@@ -78,37 +69,46 @@ def regen_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
78
  return False
79
 
80
  # ======================================================
81
- # ⚙️ CONFIG & STORAGE
82
  # ======================================================
83
- STORAGE = '/data' if os.path.exists('/data') else '.'
84
- BASE_USER_DIR = os.path.join(STORAGE, "userdata")
 
 
 
 
 
 
 
 
85
  os.makedirs(BASE_USER_DIR, exist_ok=True)
86
 
87
  app = Flask(__name__)
88
 
89
- class ComicGenerator:
90
  def __init__(self, sid):
91
  self.sid = sid
92
  self.dir = os.path.join(BASE_USER_DIR, sid)
93
  self.v_path = os.path.join(self.dir, 'video.mp4')
94
  self.f_dir = os.path.join(self.dir, 'frames')
95
- self.o_dir = os.path.join(self.dir, 'out')
96
- for d in [self.f_dir, self.o_dir]: os.makedirs(d, exist_ok=True)
97
- self.meta_p = os.path.join(self.f_dir, 'meta.json')
 
98
 
99
- def update_status(self, msg, prog):
100
  with open(os.path.join(self.o_dir, 'status.json'), 'w') as f:
101
  json.dump({'message': msg, 'progress': prog}, f)
102
 
103
- def process(self, pgs, ppp):
104
  try:
105
- self.update_status("GPU Processing...", 20)
106
  data = generate_comic_gpu(self.v_path, self.dir, self.f_dir, self.meta_p, pgs, ppp)
107
  with open(os.path.join(self.o_dir, 'pages.json'), 'w') as f:
108
  json.dump(data, f)
109
- self.update_status("Done!", 100)
110
  except Exception as e:
111
- self.update_status(f"Error: {str(e)}", -1)
112
 
113
  # ======================================================
114
  # 🌐 UI TEMPLATE (VERTICAL TILT)
@@ -117,129 +117,134 @@ INDEX_HTML = '''
117
  <!DOCTYPE html><html><head><title>Vertical Tilt Comic</title>
118
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
119
  <style>
120
- body { font-family: sans-serif; background: #eef2f3; padding: 20px; }
121
- .comic-page {
122
  width: 800px; height: 1000px; background: white; margin: 20px auto;
123
- position: relative; border: 6px solid black; overflow: hidden;
124
  }
125
  .grid { width: 100%; height: 100%; position: relative; background: #000; }
126
- .panel { position: absolute; top:0; left:0; width:100%; height:100%; overflow:hidden; }
127
 
128
- /* VERTICAL TILT CLIPPING (Left/Right Split) */
129
- .layout-slant .panel:nth-child(1) {
130
  z-index: 2;
131
- clip-path: polygon(0 0, var(--v-split-top, 45%) 0, var(--v-split-bottom, 55%) 100%, 0 100%);
132
  }
133
- .layout-slant .panel:nth-child(2) {
134
  z-index: 1;
135
- clip-path: polygon(var(--v-split-top, 45%) 0, 100% 0, 100% 100%, var(--v-split-bottom, 55%) 100%);
136
  }
137
 
138
  .panel img { width: 100%; height: 100%; object-fit: cover; }
139
 
140
- /* TILT HANDLES (Top and Bottom) */
141
- .v-handle {
142
- position: absolute; width: 30px; height: 30px; background: #ff4757;
143
- border-radius: 50%; z-index: 100; cursor: ew-resize; border: 3px solid white;
 
144
  }
145
- .v-handle.top { top: -15px; left: var(--v-split-top, 45%); transform: translateX(-50%); }
146
- .v-handle.bottom { bottom: -15px; left: var(--v-split-bottom, 55%); transform: translateX(-50%); }
147
 
148
- .bubble { position: absolute; padding: 10px; background: white; border: 2px solid black; border-radius: 20px; cursor: move; z-index: 50; transform: translate(-50%, -50%); }
149
- .controls { position: fixed; right: 20px; top: 20px; width: 200px; background: white; padding: 15px; border-radius: 10px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
 
 
150
  </style></head><body>
151
 
152
- <div id="upload-view" style="text-align:center; padding-top: 100px;">
153
- <h1>🎬 Vertical Tilt Comic Generator</h1>
154
- <input type="file" id="vid-file"><br><br>
155
- <button onclick="startProcessing()" style="padding:10px 20px; cursor:pointer;">Generate</button>
156
- <p id="status-msg"></p>
 
 
157
  </div>
158
 
159
- <div id="editor-view" style="display:none">
160
- <div id="page-container"></div>
161
  <div class="controls">
162
  <h3>Editor</h3>
163
- <p style="font-size: 12px;">Drag the red circles at the <b>top</b> and <b>bottom</b> to tilt the vertical divider.</p>
164
- <button onclick="exportPNG()">Export Page 1</button>
165
- <button onclick="location.reload()">Start Over</button>
166
  </div>
167
  </div>
168
 
169
  <script>
170
- let sid = "SID_" + Math.random().toString(36).substr(2, 9);
171
- let draggingHandle = null;
172
 
173
- async function startProcessing() {
174
  const file = document.getElementById('vid-file').files[0];
175
- if(!file) return alert("Select a video");
176
  const fd = new FormData(); fd.append('file', file);
177
- document.getElementById('status-msg').innerText = "Uploading & Waiting for ZeroGPU...";
178
 
179
  await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
180
- const timer = setInterval(async () => {
181
  const r = await fetch(`/status?sid=${sid}`);
182
  const d = await r.json();
183
- document.getElementById('status-msg').innerText = d.message;
184
- if(d.progress >= 100) { clearInterval(timer); showEditor(); }
185
  }, 2000);
186
  }
187
 
188
- async function showEditor() {
189
  const r = await fetch(`/output/pages.json?sid=${sid}`);
190
  const data = await r.json();
191
- const container = document.getElementById('page-container');
192
-
193
  data.forEach(p => {
194
- const pg = document.createElement('div');
195
- pg.className = 'comic-page';
196
  const grid = document.createElement('div');
197
- grid.className = 'grid layout-slant';
198
- grid.style.setProperty('--v-split-top', '45%');
199
- grid.style.setProperty('--v-split-bottom', '55%');
200
 
201
- const hTop = document.createElement('div'); hTop.className = 'v-handle top';
202
- const hBot = document.createElement('div'); hBot.className = 'v-handle bottom';
203
 
204
- hTop.onmousedown = () => draggingHandle = { grid, key: '--v-split-top' };
205
- hBot.onmousedown = () => draggingHandle = { grid, key: '--v-split-bottom' };
206
 
207
  p.panels.forEach(pan => {
208
- const pnl = document.createElement('div');
209
- pnl.className = 'panel';
210
- pnl.innerHTML = `<img src="/frames/${pan.image}?sid=${sid}">`;
211
- grid.appendChild(pnl);
212
  });
213
 
214
  grid.appendChild(hTop); grid.appendChild(hBot);
215
- pg.appendChild(grid);
216
- container.appendChild(pg);
217
  });
218
-
219
- document.getElementById('upload-view').style.display = 'none';
220
- document.getElementById('editor-view').style.display = 'block';
221
  }
222
 
223
  window.onmousemove = (e) => {
224
- if(!draggingHandle) return;
225
- const rect = draggingHandle.grid.getBoundingClientRect();
226
  let x = ((e.clientX - rect.left) / rect.width) * 100;
227
- x = Math.max(5, Math.min(95, x));
228
- draggingHandle.grid.style.setProperty(draggingHandle.key, x + '%');
229
  };
230
- window.onmouseup = () => draggingHandle = null;
231
 
232
- async function exportPNG() {
233
- const node = document.querySelector('.comic-page');
234
- const dataUrl = await htmlToImage.toPng(node);
235
- const link = document.createElement('a');
236
- link.download = 'comic-page.png'; link.href = dataUrl; link.click();
 
 
237
  }
238
  </script></body></html>
239
  '''
240
 
241
  # ======================================================
242
- # 🚀 FLASK ROUTES
243
  # ======================================================
244
 
245
  @app.route('/')
@@ -249,24 +254,23 @@ def index(): return INDEX_HTML
249
  def uploader():
250
  sid = request.args.get('sid')
251
  f = request.files['file']
252
- gen = ComicGenerator(sid)
253
  f.save(gen.v_path)
254
- threading.Thread(target=gen.process, args=(2, 2)).start()
255
  return jsonify({'ok': True})
256
 
257
  @app.route('/status')
258
  def get_status():
259
  sid = request.args.get('sid')
260
  try:
261
- with open(os.path.join(BASE_USER_DIR, sid, 'out', 'status.json'), 'r') as f:
262
  return f.read()
263
- except:
264
- return jsonify({'message': 'Initializing...', 'progress': 0})
265
 
266
  @app.route('/output/<path:filename>')
267
  def get_output(filename):
268
  sid = request.args.get('sid')
269
- return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'out'), filename)
270
 
271
  @app.route('/frames/<path:filename>')
272
  def get_frame(filename):
 
1
+ import spaces # <--- CRITICAL: MUST BE LINE 1
 
2
  import os
3
+ import torch
 
 
 
 
 
 
 
 
4
 
5
  # ======================================================
6
+ # 🧠 ZEROGPU FUNCTIONS (Must be at top-level)
7
  # ======================================================
8
 
9
  @spaces.GPU(duration=120)
10
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages, panels_per_page_req):
11
+ import cv2
12
+ import numpy as np
13
+ import json
14
 
15
+ cap = cv2.VideoCapture(video_path)
16
+ if not cap.isOpened(): raise Exception("Cannot open video")
17
  fps = cap.get(cv2.CAP_PROP_FPS) or 25
18
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
19
  duration = total_frames / fps
20
 
21
+ total_panels = int(target_pages) * int(panels_per_page_req)
22
+ # Grab frames at even intervals
23
+ timestamps = np.linspace(1, max(1, duration - 1), total_panels)
24
 
 
 
25
  frame_metadata = {}
26
  frame_files = []
27
 
 
39
  json.dump(frame_metadata, f)
40
 
41
  pages = []
42
+ for i in range(int(target_pages)):
43
+ start = i * int(panels_per_page_req)
44
+ subset = frame_files[start:start + int(panels_per_page_req)]
 
45
  if subset:
46
  pages.append({
47
  'panels': [{'image': f} for f in subset],
 
51
 
52
  @spaces.GPU
53
  def regen_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
54
+ import cv2
55
+ import json
56
  with open(metadata_path, 'r') as f: meta = json.load(f)
57
  t = meta[fname]['time']
58
  cap = cv2.VideoCapture(video_path)
 
69
  return False
70
 
71
  # ======================================================
72
+ # ⚙️ APP & STORAGE SETUP
73
  # ======================================================
74
+ import time
75
+ import threading
76
+ import json
77
+ import string
78
+ import random
79
+ import shutil
80
+ from flask import Flask, jsonify, request, send_from_directory, send_file
81
+
82
+ BASE_STORAGE_PATH = '/data' if os.path.exists('/data') else '.'
83
+ BASE_USER_DIR = os.path.join(BASE_STORAGE_PATH, "userdata")
84
  os.makedirs(BASE_USER_DIR, exist_ok=True)
85
 
86
  app = Flask(__name__)
87
 
88
+ class ComicBackend:
89
  def __init__(self, sid):
90
  self.sid = sid
91
  self.dir = os.path.join(BASE_USER_DIR, sid)
92
  self.v_path = os.path.join(self.dir, 'video.mp4')
93
  self.f_dir = os.path.join(self.dir, 'frames')
94
+ self.o_dir = os.path.join(self.dir, 'output')
95
+ os.makedirs(self.f_dir, exist_ok=True)
96
+ os.makedirs(self.o_dir, exist_ok=True)
97
+ self.meta_p = os.path.join(self.f_dir, 'metadata.json')
98
 
99
+ def write_status(self, msg, prog):
100
  with open(os.path.join(self.o_dir, 'status.json'), 'w') as f:
101
  json.dump({'message': msg, 'progress': prog}, f)
102
 
103
+ def run(self, pgs, ppp):
104
  try:
105
+ self.write_status("Running ZeroGPU Task...", 25)
106
  data = generate_comic_gpu(self.v_path, self.dir, self.f_dir, self.meta_p, pgs, ppp)
107
  with open(os.path.join(self.o_dir, 'pages.json'), 'w') as f:
108
  json.dump(data, f)
109
+ self.write_status("Complete!", 100)
110
  except Exception as e:
111
+ self.write_status(f"Error: {str(e)}", -1)
112
 
113
  # ======================================================
114
  # 🌐 UI TEMPLATE (VERTICAL TILT)
 
117
  <!DOCTYPE html><html><head><title>Vertical Tilt Comic</title>
118
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
119
  <style>
120
+ body { font-family: 'Segoe UI', sans-serif; background: #1a1a1a; color: white; margin: 0; padding: 20px; }
121
+ .page {
122
  width: 800px; height: 1000px; background: white; margin: 20px auto;
123
+ position: relative; border: 8px solid black; overflow: hidden;
124
  }
125
  .grid { width: 100%; height: 100%; position: relative; background: #000; }
126
+ .panel { position: absolute; top:0; left:0; width: 100%; height: 100%; overflow: hidden; }
127
 
128
+ /* VERTICAL SPLIT CLIP-PATH (Left/Right) */
129
+ .layout-vertical .panel:nth-child(1) {
130
  z-index: 2;
131
+ clip-path: polygon(0 0, var(--v-top, 45%) 0, var(--v-bottom, 55%) 100%, 0 100%);
132
  }
133
+ .layout-vertical .panel:nth-child(2) {
134
  z-index: 1;
135
+ clip-path: polygon(var(--v-top, 45%) 0, 100% 0, 100% 100%, var(--v-bottom, 55%) 100%);
136
  }
137
 
138
  .panel img { width: 100%; height: 100%; object-fit: cover; }
139
 
140
+ /* HANDLES ON TOP AND BOTTOM */
141
+ .handle {
142
+ position: absolute; width: 32px; height: 32px; background: #ffdf00;
143
+ border-radius: 50%; z-index: 100; cursor: ew-resize; border: 4px solid #000;
144
+ box-shadow: 0 0 10px rgba(0,0,0,0.5);
145
  }
146
+ .handle.top { top: -16px; left: var(--v-top, 45%); transform: translateX(-50%); }
147
+ .handle.bottom { bottom: -16px; left: var(--v-bottom, 55%); transform: translateX(-50%); }
148
 
149
+ .controls { position: fixed; left: 20px; top: 20px; background: #333; padding: 20px; border-radius: 12px; width: 220px; }
150
+ button { width: 100%; padding: 10px; margin-top: 10px; cursor: pointer; font-weight: bold; border-radius: 5px; border: none; }
151
+ .btn-gen { background: #00ff88; color: #000; }
152
+ #status { color: #00ff88; font-family: monospace; margin-top: 10px; }
153
  </style></head><body>
154
 
155
+ <div id="upload-ui">
156
+ <div style="text-align:center; margin-top: 100px;">
157
+ <h1>🎬 Vertical Tilt Comic Generator</h1>
158
+ <input type="file" id="vid-file"><br><br>
159
+ <button class="btn-gen" onclick="upload()">Generate Comic</button>
160
+ <div id="status">Ready.</div>
161
+ </div>
162
  </div>
163
 
164
+ <div id="editor-ui" style="display:none">
165
+ <div id="page-list"></div>
166
  <div class="controls">
167
  <h3>Editor</h3>
168
+ <p style="font-size: 11px;">Drag the <b style="color:#ffdf00">yellow circles</b> on the TOP and BOTTOM edges to tilt the divider.</p>
169
+ <button onclick="download()">📥 Download PNG</button>
170
+ <button onclick="location.reload()" style="background:#ff4444; color:white;">Reset</button>
171
  </div>
172
  </div>
173
 
174
  <script>
175
+ let sid = "S" + Math.random().toString(36).substr(2, 9);
176
+ let dragging = null;
177
 
178
+ async function upload() {
179
  const file = document.getElementById('vid-file').files[0];
180
+ if(!file) return alert("Select video");
181
  const fd = new FormData(); fd.append('file', file);
182
+ document.getElementById('status').innerText = "Uploading...";
183
 
184
  await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
185
+ const check = setInterval(async () => {
186
  const r = await fetch(`/status?sid=${sid}`);
187
  const d = await r.json();
188
+ document.getElementById('status').innerText = d.message;
189
+ if(d.progress >= 100) { clearInterval(check); initEditor(); }
190
  }, 2000);
191
  }
192
 
193
+ async function initEditor() {
194
  const r = await fetch(`/output/pages.json?sid=${sid}`);
195
  const data = await r.json();
196
+ const list = document.getElementById('page-list');
 
197
  data.forEach(p => {
198
+ const pageDiv = document.createElement('div');
199
+ pageDiv.className = 'page';
200
  const grid = document.createElement('div');
201
+ grid.className = 'grid layout-vertical';
202
+ grid.style.setProperty('--v-top', '45%');
203
+ grid.style.setProperty('--v-bottom', '55%');
204
 
205
+ const hTop = document.createElement('div'); hTop.className = 'handle top';
206
+ const hBot = document.createElement('div'); hBot.className = 'handle bottom';
207
 
208
+ hTop.onmousedown = () => dragging = { grid, key: '--v-top' };
209
+ hBot.onmousedown = () => dragging = { grid, key: '--v-bottom' };
210
 
211
  p.panels.forEach(pan => {
212
+ const div = document.createElement('div');
213
+ div.className = 'panel';
214
+ div.innerHTML = `<img src="/frames/${pan.image}?sid=${sid}">`;
215
+ grid.appendChild(div);
216
  });
217
 
218
  grid.appendChild(hTop); grid.appendChild(hBot);
219
+ pageDiv.appendChild(grid);
220
+ list.appendChild(pageDiv);
221
  });
222
+ document.getElementById('upload-ui').style.display='none';
223
+ document.getElementById('editor-ui').style.display='block';
 
224
  }
225
 
226
  window.onmousemove = (e) => {
227
+ if(!dragging) return;
228
+ const rect = dragging.grid.getBoundingClientRect();
229
  let x = ((e.clientX - rect.left) / rect.width) * 100;
230
+ x = Math.max(5, Math.min(95, x)); // Constraints
231
+ dragging.grid.style.setProperty(dragging.key, x + '%');
232
  };
233
+ window.onmouseup = () => dragging = null;
234
 
235
+ async function download() {
236
+ const pages = document.querySelectorAll('.page');
237
+ for(let i=0; i<pages.length; i++){
238
+ const url = await htmlToImage.toPng(pages[i]);
239
+ const a = document.createElement('a');
240
+ a.download = `comic-page-${i+1}.png`; a.href = url; a.click();
241
+ }
242
  }
243
  </script></body></html>
244
  '''
245
 
246
  # ======================================================
247
+ # 🚀 ROUTES
248
  # ======================================================
249
 
250
  @app.route('/')
 
254
  def uploader():
255
  sid = request.args.get('sid')
256
  f = request.files['file']
257
+ gen = ComicBackend(sid)
258
  f.save(gen.v_path)
259
+ threading.Thread(target=gen.run, args=(2, 2)).start()
260
  return jsonify({'ok': True})
261
 
262
  @app.route('/status')
263
  def get_status():
264
  sid = request.args.get('sid')
265
  try:
266
+ with open(os.path.join(BASE_USER_DIR, sid, 'output', 'status.json'), 'r') as f:
267
  return f.read()
268
+ except: return jsonify({'message': 'Starting...', 'progress': 0})
 
269
 
270
  @app.route('/output/<path:filename>')
271
  def get_output(filename):
272
  sid = request.args.get('sid')
273
+ return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
274
 
275
  @app.route('/frames/<path:filename>')
276
  def get_frame(filename):