tester343 commited on
Commit
2eb576e
·
verified ·
1 Parent(s): 2036c8c

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +56 -41
app_enhanced.py CHANGED
@@ -24,7 +24,7 @@ def gpu_warmup():
24
  return True
25
 
26
  # ======================================================
27
- # 💾 STORAGE SETUP
28
  # ======================================================
29
  if os.path.exists('/data'):
30
  BASE_STORAGE_PATH = '/data'
@@ -43,7 +43,7 @@ os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
43
  # 🔧 APP CONFIG
44
  # ======================================================
45
  app = Flask(__name__)
46
- app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB Limit
47
 
48
  def generate_save_code(length=8):
49
  chars = string.ascii_uppercase + string.digits
@@ -74,16 +74,16 @@ class Page:
74
  self.bubbles = bubbles
75
 
76
  # ======================================================
77
- # 🧠 GPU GENERATION (FULL TEXT + HD IMAGE)
78
  # ======================================================
79
  @spaces.GPU(duration=300)
80
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
81
- print(f"🚀 Generating HD Comic with TEXT: {video_path}")
82
 
83
  import cv2
84
  import srt
85
  import numpy as np
86
- from backend.subtitles.subs_real import get_real_subtitles # Ensure this backend file exists
87
 
88
  # 1. Video Setup
89
  cap = cv2.VideoCapture(video_path)
@@ -93,7 +93,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
93
  duration = total_frames / fps
94
  cap.release()
95
 
96
- # 2. GENERATE SUBTITLES
97
  user_srt = os.path.join(user_dir, 'subs.srt')
98
  try:
99
  print("🎙️ Extracting subtitles...")
@@ -101,24 +101,22 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
101
  if os.path.exists('test1.srt'):
102
  shutil.move('test1.srt', user_srt)
103
  elif not os.path.exists(user_srt):
104
- with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:04,000\n(No Text)\n")
105
  except Exception as e:
106
  print(f"⚠️ Subtitle error: {e}")
107
- with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:04,000\n(Error)\n")
108
 
109
- # 3. Parse Subtitles
110
  with open(user_srt, 'r', encoding='utf-8') as f:
111
  try: all_subs = list(srt.parse(f.read()))
112
  except: all_subs = []
113
 
114
  valid_subs = [s for s in all_subs if s.content and s.content.strip()]
115
-
116
  if valid_subs:
117
  raw_moments = [{'text': s.content.strip(), 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
118
  else:
119
  raw_moments = []
120
 
121
- # 4. Determine Frames needed (4 per page)
122
  panels_per_page = 4
123
  total_panels_needed = int(target_pages) * panels_per_page
124
 
@@ -132,7 +130,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
132
  indices = np.linspace(0, len(raw_moments) - 1, total_panels_needed, dtype=int)
133
  selected_moments = [raw_moments[i] for i in indices]
134
 
135
- # 5. Extract Frames (HD 1280x720)
136
  frame_metadata = {}
137
  cap = cv2.VideoCapture(video_path)
138
  count = 0
@@ -145,13 +143,29 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
145
  ret, frame = cap.read()
146
 
147
  if ret:
148
- # 🎯 KEEP 16:9 ASPECT RATIO (1280x720)
149
- # This ensures no data is lost. Frontend controls crop/zoom.
150
- frame = cv2.resize(frame, (1280, 720))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  fname = f"frame_{count:04d}.png"
153
  p = os.path.join(frames_dir, fname)
154
- cv2.imwrite(p, frame)
155
 
156
  frame_metadata[fname] = {'dialogue': moment['text'], 'time': mid}
157
  frame_files_ordered.append(fname)
@@ -160,18 +174,17 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
160
  cap.release()
161
  with open(metadata_path, 'w') as f: json.dump(frame_metadata, f, indent=2)
162
 
163
- # 6. Generate Bubbles
164
  bubbles_list = []
165
  for f in frame_files_ordered:
166
  dialogue = frame_metadata.get(f, {}).get('dialogue', '')
167
  b_type = 'speech'
168
  if '(' in dialogue: b_type = 'narration'
169
  elif '!' in dialogue: b_type = 'reaction'
170
-
171
- # Center bubbles initially
172
  bubbles_list.append(bubble(dialog=dialogue, x=50, y=20, type=b_type))
173
 
174
- # 7. Construct Pages
175
  pages = []
176
  for i in range(int(target_pages)):
177
  start_idx = i * 4
@@ -179,10 +192,9 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
179
  p_frames = frame_files_ordered[start_idx:end_idx]
180
  p_bubbles = bubbles_list[start_idx:end_idx]
181
 
182
- # Pad with empty frames if not enough
183
  while len(p_frames) < 4:
184
  fname = f"empty_{i}_{len(p_frames)}.png"
185
- img = np.zeros((720, 1280, 3), dtype=np.uint8); img[:] = (30,30,30)
186
  cv2.imwrite(os.path.join(frames_dir, fname), img)
187
  p_frames.append(fname)
188
  p_bubbles.append(bubble(dialog="", type='speech'))
@@ -191,7 +203,6 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
191
  pg_panels = [panel(image=f) for f in p_frames]
192
  pages.append(Page(panels=pg_panels, bubbles=p_bubbles))
193
 
194
- # 8. Output
195
  result = []
196
  for pg in pages:
197
  p_data = [p if isinstance(p, dict) else p.__dict__ for p in pg.panels]
@@ -219,9 +230,17 @@ def regen_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
219
  cap.release()
220
 
221
  if ret:
222
- frame = cv2.resize(frame, (1280, 720)) # Keep HD
 
 
 
 
 
 
 
 
223
  p = os.path.join(frames_dir, fname)
224
- cv2.imwrite(p, frame)
225
 
226
  if isinstance(meta[fname], dict): meta[fname]['time'] = new_t
227
  else: meta[fname] = new_t
@@ -239,9 +258,16 @@ def get_frame_at_ts_gpu(video_path, frames_dir, metadata_path, fname, ts):
239
  cap.release()
240
 
241
  if ret:
242
- frame = cv2.resize(frame, (1280, 720)) # Keep HD
 
 
 
 
 
 
 
243
  p = os.path.join(frames_dir, fname)
244
- cv2.imwrite(p, frame)
245
  if os.path.exists(metadata_path):
246
  with open(metadata_path, 'r') as f: meta = json.load(f)
247
  if fname in meta:
@@ -342,7 +368,7 @@ INDEX_HTML = '''
342
 
343
  .panel { position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; background: #1a1a1a; cursor: grab; }
344
 
345
- /* IMAGE HANDLING: Object-fit cover fills the square. Pan/Zoom reveals hidden parts. */
346
  .panel img {
347
  width: 100%; height: 100%;
348
  object-fit: cover;
@@ -421,14 +447,12 @@ INDEX_HTML = '''
421
  <input type="text" id="load-code" placeholder="ENTER SAVE CODE" style="width:70%; display:inline-block;">
422
  <button onclick="loadComic()" style="width:25%; display:inline-block; background:#9b59b6; color:white;">Load</button>
423
  </div>
424
-
425
  <div class="loading-view" id="loading-view" style="display:none; margin-top:20px;">
426
  <div class="loader" style="margin:0 auto;"></div>
427
  <p id="status-text" style="margin-top:10px;">Starting...</p>
428
  </div>
429
  </div>
430
  </div>
431
-
432
  <div id="editor-container">
433
  <div class="tip">👉 Drag Blue/Green dots from the RIGHT edge to reveal hidden square panels!</div>
434
  <div class="comic-wrapper" id="comic-container"></div>
@@ -440,7 +464,6 @@ INDEX_HTML = '''
440
  <button onclick="undo()" style="background:#7f8c8d; color:white;">↩️ Undo</button>
441
  <button onclick="saveComic()" class="save-btn">💾 Save Comic</button>
442
  </div>
443
-
444
  <div class="control-group">
445
  <label>💬 Bubbles:</label>
446
  <div class="button-grid">
@@ -452,7 +475,6 @@ INDEX_HTML = '''
452
  <input type="color" id="bub-text" value="#000000" onchange="updateBubbleColor()">
453
  </div>
454
  </div>
455
-
456
  <div class="control-group">
457
  <label>🖼️ Image Control:</label>
458
  <div class="button-grid">
@@ -463,7 +485,7 @@ INDEX_HTML = '''
463
 
464
  <div class="control-group">
465
  <label>🔍 Zoom (Mouse Wheel):</label>
466
- <!-- Min zoom 20 allowed to zoom OUT -->
467
  <input type="range" id="zoom-slider" min="20" max="300" value="100" step="5" oninput="handleZoom(this.value)" disabled>
468
  <button onclick="resetPanelTransform()" class="secondary-btn">Reset View</button>
469
  </div>
@@ -474,7 +496,6 @@ INDEX_HTML = '''
474
  </div>
475
  </div>
476
  </div>
477
-
478
  <div class="modal-overlay" id="save-modal">
479
  <div class="modal-content">
480
  <h2>✅ Comic Saved!</h2>
@@ -482,7 +503,6 @@ INDEX_HTML = '''
482
  <button onclick="closeModal()">Close</button>
483
  </div>
484
  </div>
485
-
486
  <script>
487
  function genUUID(){ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,c=>{var r=Math.random()*16|0,v=c=='x'?r:(r&0x3|0x8);return v.toString(16);}); }
488
  let sid = localStorage.getItem('comic_sid') || genUUID();
@@ -491,7 +511,6 @@ INDEX_HTML = '''
491
  let dragType = null, activeObj = null, dragStart = {x:0, y:0};
492
  let historyStack = [];
493
 
494
- // HISTORY & UNDO
495
  function saveState() {
496
  const state = [];
497
  document.querySelectorAll('.comic-page').forEach(pg => {
@@ -508,9 +527,7 @@ INDEX_HTML = '''
508
  const panels = [];
509
  grid.querySelectorAll('.panel').forEach(pan => {
510
  const img = pan.querySelector('img');
511
- const srcParts = img.src.split('frames/');
512
- const fname = srcParts.length > 1 ? srcParts[1].split('?')[0] : '';
513
- panels.push({ image: fname, zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY });
514
  });
515
  state.push({ layout, bubbles, panels });
516
  });
@@ -558,7 +575,6 @@ INDEX_HTML = '''
558
  setTimeout(() => restoreFromState(JSON.parse(localStorage.getItem('comic_draft_'+sid))), 500);
559
  });
560
  }
561
-
562
  async function upload() {
563
  const f = document.getElementById('file-upload').files[0];
564
  const pCount = document.getElementById('page-count').value;
@@ -590,7 +606,6 @@ INDEX_HTML = '''
590
  renderFromState(cleanData);
591
  saveState();
592
  }
593
-
594
  function renderFromState(pagesData) {
595
  const con = document.getElementById('comic-container'); con.innerHTML = '';
596
  pagesData.forEach((page, pageIdx) => {
 
24
  return True
25
 
26
  # ======================================================
27
+ # 💾 PERSISTENT STORAGE
28
  # ======================================================
29
  if os.path.exists('/data'):
30
  BASE_STORAGE_PATH = '/data'
 
43
  # 🔧 APP CONFIG
44
  # ======================================================
45
  app = Flask(__name__)
46
+ app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB Upload Limit
47
 
48
  def generate_save_code(length=8):
49
  chars = string.ascii_uppercase + string.digits
 
74
  self.bubbles = bubbles
75
 
76
  # ======================================================
77
+ # 🧠 GPU GENERATION (SQUARE PADDING + TEXT)
78
  # ======================================================
79
  @spaces.GPU(duration=300)
80
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
81
+ print(f"🚀 Generating Square Comic: {video_path}")
82
 
83
  import cv2
84
  import srt
85
  import numpy as np
86
+ from backend.subtitles.subs_real import get_real_subtitles
87
 
88
  # 1. Video Setup
89
  cap = cv2.VideoCapture(video_path)
 
93
  duration = total_frames / fps
94
  cap.release()
95
 
96
+ # 2. SUBTITLES
97
  user_srt = os.path.join(user_dir, 'subs.srt')
98
  try:
99
  print("🎙️ Extracting subtitles...")
 
101
  if os.path.exists('test1.srt'):
102
  shutil.move('test1.srt', user_srt)
103
  elif not os.path.exists(user_srt):
104
+ with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:04,000\n...\n")
105
  except Exception as e:
106
  print(f"⚠️ Subtitle error: {e}")
107
+ with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:04,000\n...\n")
108
 
 
109
  with open(user_srt, 'r', encoding='utf-8') as f:
110
  try: all_subs = list(srt.parse(f.read()))
111
  except: all_subs = []
112
 
113
  valid_subs = [s for s in all_subs if s.content and s.content.strip()]
 
114
  if valid_subs:
115
  raw_moments = [{'text': s.content.strip(), 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
116
  else:
117
  raw_moments = []
118
 
119
+ # 3. Frame Selection (4 per page)
120
  panels_per_page = 4
121
  total_panels_needed = int(target_pages) * panels_per_page
122
 
 
130
  indices = np.linspace(0, len(raw_moments) - 1, total_panels_needed, dtype=int)
131
  selected_moments = [raw_moments[i] for i in indices]
132
 
133
+ # 4. Extract & PAD TO SQUARE
134
  frame_metadata = {}
135
  cap = cv2.VideoCapture(video_path)
136
  count = 0
 
143
  ret, frame = cap.read()
144
 
145
  if ret:
146
+ # ----------------------------------------------------
147
+ # 🎯 SQUARE PADDING LOGIC (0% Cut)
148
+ # ----------------------------------------------------
149
+ h, w = frame.shape[:2]
150
+ # Determine square size based on max dimension
151
+ sq_dim = max(h, w)
152
+
153
+ # Create black square canvas
154
+ square_img = np.zeros((sq_dim, sq_dim, 3), dtype=np.uint8)
155
+
156
+ # Calculate centering offsets
157
+ x_off = (sq_dim - w) // 2
158
+ y_off = (sq_dim - h) // 2
159
+
160
+ # Place original frame in center
161
+ square_img[y_off:y_off+h, x_off:x_off+w] = frame
162
+
163
+ # Optionally resize to standard high res (e.g. 1024x1024) to normalize
164
+ square_img = cv2.resize(square_img, (1024, 1024))
165
 
166
  fname = f"frame_{count:04d}.png"
167
  p = os.path.join(frames_dir, fname)
168
+ cv2.imwrite(p, square_img)
169
 
170
  frame_metadata[fname] = {'dialogue': moment['text'], 'time': mid}
171
  frame_files_ordered.append(fname)
 
174
  cap.release()
175
  with open(metadata_path, 'w') as f: json.dump(frame_metadata, f, indent=2)
176
 
177
+ # 5. Bubbles
178
  bubbles_list = []
179
  for f in frame_files_ordered:
180
  dialogue = frame_metadata.get(f, {}).get('dialogue', '')
181
  b_type = 'speech'
182
  if '(' in dialogue: b_type = 'narration'
183
  elif '!' in dialogue: b_type = 'reaction'
184
+ # Position bubbles somewhat centrally to avoid black bars
 
185
  bubbles_list.append(bubble(dialog=dialogue, x=50, y=20, type=b_type))
186
 
187
+ # 6. Pages
188
  pages = []
189
  for i in range(int(target_pages)):
190
  start_idx = i * 4
 
192
  p_frames = frame_files_ordered[start_idx:end_idx]
193
  p_bubbles = bubbles_list[start_idx:end_idx]
194
 
 
195
  while len(p_frames) < 4:
196
  fname = f"empty_{i}_{len(p_frames)}.png"
197
+ img = np.zeros((1024, 1024, 3), dtype=np.uint8); img[:] = (30,30,30)
198
  cv2.imwrite(os.path.join(frames_dir, fname), img)
199
  p_frames.append(fname)
200
  p_bubbles.append(bubble(dialog="", type='speech'))
 
203
  pg_panels = [panel(image=f) for f in p_frames]
204
  pages.append(Page(panels=pg_panels, bubbles=p_bubbles))
205
 
 
206
  result = []
207
  for pg in pages:
208
  p_data = [p if isinstance(p, dict) else p.__dict__ for p in pg.panels]
 
230
  cap.release()
231
 
232
  if ret:
233
+ # Re-apply Square Padding logic
234
+ h, w = frame.shape[:2]
235
+ sq_dim = max(h, w)
236
+ square_img = np.zeros((sq_dim, sq_dim, 3), dtype=np.uint8)
237
+ x_off = (sq_dim - w) // 2
238
+ y_off = (sq_dim - h) // 2
239
+ square_img[y_off:y_off+h, x_off:x_off+w] = frame
240
+ square_img = cv2.resize(square_img, (1024, 1024))
241
+
242
  p = os.path.join(frames_dir, fname)
243
+ cv2.imwrite(p, square_img)
244
 
245
  if isinstance(meta[fname], dict): meta[fname]['time'] = new_t
246
  else: meta[fname] = new_t
 
258
  cap.release()
259
 
260
  if ret:
261
+ h, w = frame.shape[:2]
262
+ sq_dim = max(h, w)
263
+ square_img = np.zeros((sq_dim, sq_dim, 3), dtype=np.uint8)
264
+ x_off = (sq_dim - w) // 2
265
+ y_off = (sq_dim - h) // 2
266
+ square_img[y_off:y_off+h, x_off:x_off+w] = frame
267
+ square_img = cv2.resize(square_img, (1024, 1024))
268
+
269
  p = os.path.join(frames_dir, fname)
270
+ cv2.imwrite(p, square_img)
271
  if os.path.exists(metadata_path):
272
  with open(metadata_path, 'r') as f: meta = json.load(f)
273
  if fname in meta:
 
368
 
369
  .panel { position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; background: #1a1a1a; cursor: grab; }
370
 
371
+ /* IMAGE: Cover ensures it fills. Zoom allows control. */
372
  .panel img {
373
  width: 100%; height: 100%;
374
  object-fit: cover;
 
447
  <input type="text" id="load-code" placeholder="ENTER SAVE CODE" style="width:70%; display:inline-block;">
448
  <button onclick="loadComic()" style="width:25%; display:inline-block; background:#9b59b6; color:white;">Load</button>
449
  </div>
 
450
  <div class="loading-view" id="loading-view" style="display:none; margin-top:20px;">
451
  <div class="loader" style="margin:0 auto;"></div>
452
  <p id="status-text" style="margin-top:10px;">Starting...</p>
453
  </div>
454
  </div>
455
  </div>
 
456
  <div id="editor-container">
457
  <div class="tip">👉 Drag Blue/Green dots from the RIGHT edge to reveal hidden square panels!</div>
458
  <div class="comic-wrapper" id="comic-container"></div>
 
464
  <button onclick="undo()" style="background:#7f8c8d; color:white;">↩️ Undo</button>
465
  <button onclick="saveComic()" class="save-btn">💾 Save Comic</button>
466
  </div>
 
467
  <div class="control-group">
468
  <label>💬 Bubbles:</label>
469
  <div class="button-grid">
 
475
  <input type="color" id="bub-text" value="#000000" onchange="updateBubbleColor()">
476
  </div>
477
  </div>
 
478
  <div class="control-group">
479
  <label>🖼️ Image Control:</label>
480
  <div class="button-grid">
 
485
 
486
  <div class="control-group">
487
  <label>🔍 Zoom (Mouse Wheel):</label>
488
+ <!-- Min zoom 20 allowed to zoom OUT to see whole square image -->
489
  <input type="range" id="zoom-slider" min="20" max="300" value="100" step="5" oninput="handleZoom(this.value)" disabled>
490
  <button onclick="resetPanelTransform()" class="secondary-btn">Reset View</button>
491
  </div>
 
496
  </div>
497
  </div>
498
  </div>
 
499
  <div class="modal-overlay" id="save-modal">
500
  <div class="modal-content">
501
  <h2>✅ Comic Saved!</h2>
 
503
  <button onclick="closeModal()">Close</button>
504
  </div>
505
  </div>
 
506
  <script>
507
  function genUUID(){ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,c=>{var r=Math.random()*16|0,v=c=='x'?r:(r&0x3|0x8);return v.toString(16);}); }
508
  let sid = localStorage.getItem('comic_sid') || genUUID();
 
511
  let dragType = null, activeObj = null, dragStart = {x:0, y:0};
512
  let historyStack = [];
513
 
 
514
  function saveState() {
515
  const state = [];
516
  document.querySelectorAll('.comic-page').forEach(pg => {
 
527
  const panels = [];
528
  grid.querySelectorAll('.panel').forEach(pan => {
529
  const img = pan.querySelector('img');
530
+ panels.push({ zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY });
 
 
531
  });
532
  state.push({ layout, bubbles, panels });
533
  });
 
575
  setTimeout(() => restoreFromState(JSON.parse(localStorage.getItem('comic_draft_'+sid))), 500);
576
  });
577
  }
 
578
  async function upload() {
579
  const f = document.getElementById('file-upload').files[0];
580
  const pCount = document.getElementById('page-count').value;
 
606
  renderFromState(cleanData);
607
  saveState();
608
  }
 
609
  function renderFromState(pagesData) {
610
  const con = document.getElementById('comic-container'); con.innerHTML = '';
611
  pagesData.forEach((page, pageIdx) => {