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

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +77 -44
app_enhanced.py CHANGED
@@ -24,7 +24,7 @@ def gpu_warmup():
24
  return True
25
 
26
  # ======================================================
27
- # 💾 PERSISTENT STORAGE
28
  # ======================================================
29
  if os.path.exists('/data'):
30
  BASE_STORAGE_PATH = '/data'
@@ -39,6 +39,19 @@ SAVED_COMICS_DIR = os.path.join(BASE_STORAGE_PATH, "saved_comics")
39
  os.makedirs(BASE_USER_DIR, exist_ok=True)
40
  os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  # ======================================================
43
  # 🧱 DATA CLASSES
44
  # ======================================================
@@ -60,32 +73,17 @@ class Page:
60
  self.panels = panels
61
  self.bubbles = bubbles
62
 
63
- # ======================================================
64
- # 🔧 APP CONFIG
65
- # ======================================================
66
- app = Flask(__name__)
67
- app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024
68
-
69
- def generate_save_code(length=8):
70
- chars = string.ascii_uppercase + string.digits
71
- while True:
72
- code = ''.join(random.choices(chars, k=length))
73
- if not os.path.exists(os.path.join(SAVED_COMICS_DIR, code)):
74
- return code
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 Comic with TEXT: {video_path}")
82
 
83
  import cv2
84
  import srt
85
  import numpy as np
86
- # Import backend modules for text extraction
87
- from backend.subtitles.subs_real import get_real_subtitles
88
- # Optional: AI placement (can be slow, using center fallback for speed/visibility)
89
 
90
  # 1. Video Setup
91
  cap = cv2.VideoCapture(video_path)
@@ -95,16 +93,14 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
95
  duration = total_frames / fps
96
  cap.release()
97
 
98
- # 2. GENERATE SUBTITLES (The Real Logic)
99
  user_srt = os.path.join(user_dir, 'subs.srt')
100
  try:
101
  print("🎙️ Extracting subtitles...")
102
  get_real_subtitles(video_path)
103
- # Move the generated SRT to user dir
104
  if os.path.exists('test1.srt'):
105
  shutil.move('test1.srt', user_srt)
106
  elif not os.path.exists(user_srt):
107
- # Fallback if extractor failed silently
108
  with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:04,000\n(No Text)\n")
109
  except Exception as e:
110
  print(f"⚠️ Subtitle error: {e}")
@@ -117,30 +113,26 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
117
 
118
  valid_subs = [s for s in all_subs if s.content and s.content.strip()]
119
 
120
- # Create "Moments" from subtitles
121
  if valid_subs:
122
  raw_moments = [{'text': s.content.strip(), 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
123
  else:
124
- # If no speech, create time-based moments
125
  raw_moments = []
126
 
127
- # 4. Determine Frames needed
128
  panels_per_page = 4
129
  total_panels_needed = int(target_pages) * panels_per_page
130
 
131
  selected_moments = []
132
  if not raw_moments:
133
- # Time based distribution if no text
134
  times = np.linspace(1, max(1, duration-1), total_panels_needed)
135
  for t in times: selected_moments.append({'text': '', 'start': t, 'end': t+1})
136
  elif len(raw_moments) <= total_panels_needed:
137
  selected_moments = raw_moments
138
  else:
139
- # Sample moments evenly
140
  indices = np.linspace(0, len(raw_moments) - 1, total_panels_needed, dtype=int)
141
  selected_moments = [raw_moments[i] for i in indices]
142
 
143
- # 5. Extract Frames (HD) & Metadata
144
  frame_metadata = {}
145
  cap = cv2.VideoCapture(video_path)
146
  count = 0
@@ -153,9 +145,8 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
153
  ret, frame = cap.read()
154
 
155
  if ret:
156
- # ----------------------------------------------------
157
- # 🎯 HD EXTRACTION (1280x720) - Preserves quality
158
- # ----------------------------------------------------
159
  frame = cv2.resize(frame, (1280, 720))
160
 
161
  fname = f"frame_{count:04d}.png"
@@ -169,20 +160,16 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
169
  cap.release()
170
  with open(metadata_path, 'w') as f: json.dump(frame_metadata, f, indent=2)
171
 
172
- # 6. Generate Bubbles with Text
173
  bubbles_list = []
174
  for f in frame_files_ordered:
175
  dialogue = frame_metadata.get(f, {}).get('dialogue', '')
176
-
177
- # Determine Type
178
  b_type = 'speech'
179
  if '(' in dialogue: b_type = 'narration'
180
- elif '!' in dialogue and dialogue.isupper(): b_type = 'reaction'
181
- elif '?' in dialogue: b_type = 'speech'
182
 
183
- # Place Bubble (Center Top 50px, 20px) to ensure visibility in Crop
184
- # Users can drag it later.
185
- bubbles_list.append(bubble(dialog=dialogue, x=50, y=50, type=b_type))
186
 
187
  # 7. Construct Pages
188
  pages = []
@@ -192,7 +179,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
192
  p_frames = frame_files_ordered[start_idx:end_idx]
193
  p_bubbles = bubbles_list[start_idx:end_idx]
194
 
195
- # Pad with empty
196
  while len(p_frames) < 4:
197
  fname = f"empty_{i}_{len(p_frames)}.png"
198
  img = np.zeros((720, 1280, 3), dtype=np.uint8); img[:] = (30,30,30)
@@ -217,7 +204,6 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
217
  def regen_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
218
  import cv2
219
  import json
220
- # (Same fast regen logic)
221
  if not os.path.exists(metadata_path): return {"success": False, "message": "No metadata"}
222
  with open(metadata_path, 'r') as f: meta = json.load(f)
223
  if fname not in meta: return {"success": False, "message": "Frame not found"}
@@ -243,6 +229,28 @@ def regen_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
243
  return {"success": True, "message": f"Time: {new_t:.2f}s"}
244
  return {"success": False, "message": "End of video"}
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  # ======================================================
247
  # 💻 BACKEND CLASS
248
  # ======================================================
@@ -324,6 +332,8 @@ INDEX_HTML = '''
324
  width: 100%; height: 100%;
325
  position: relative;
326
  background: #000;
 
 
327
  --y: 50%;
328
  --t1: 100%; --t2: 100%; /* Hidden Right by default */
329
  --b1: 100%; --b2: 100%; /* Hidden Right by default */
@@ -331,7 +341,15 @@ INDEX_HTML = '''
331
  }
332
 
333
  .panel { position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; background: #1a1a1a; cursor: grab; }
334
- .panel img { width: 100%; height: 100%; object-fit: cover; transform-origin: center; transition: transform 0.05s ease-out; display: block; }
 
 
 
 
 
 
 
 
335
  .panel img.panning { cursor: grabbing; transition: none; }
336
  .panel.selected { outline: 4px solid #2196F3; z-index: 5; }
337
 
@@ -445,7 +463,8 @@ INDEX_HTML = '''
445
 
446
  <div class="control-group">
447
  <label>🔍 Zoom (Mouse Wheel):</label>
448
- <input type="range" id="zoom-slider" min="100" max="300" value="100" step="5" oninput="handleZoom(this.value)" disabled>
 
449
  <button onclick="resetPanelTransform()" class="secondary-btn">Reset View</button>
450
  </div>
451
 
@@ -489,7 +508,9 @@ INDEX_HTML = '''
489
  const panels = [];
490
  grid.querySelectorAll('.panel').forEach(pan => {
491
  const img = pan.querySelector('img');
492
- panels.push({ zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY });
 
 
493
  });
494
  state.push({ layout, bubbles, panels });
495
  });
@@ -584,7 +605,19 @@ INDEX_HTML = '''
584
  img.src = pan.src.includes('?') ? pan.src : pan.src + `?sid=${sid}`;
585
  img.dataset.zoom = 100; img.dataset.translateX = 0; img.dataset.translateY = 0;
586
  img.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); selectPanel(pDiv); dragType = 'pan'; activeObj = img; dragStart = {x:e.clientX, y:e.clientY}; img.classList.add('panning'); };
587
- img.onwheel = (e) => { e.preventDefault(); let zoom = parseFloat(img.dataset.zoom); zoom += e.deltaY * -0.1; zoom = Math.min(Math.max(100, zoom), 300); img.dataset.zoom = zoom; updateImageTransform(img); if(selectedPanel === pDiv) document.getElementById('zoom-slider').value = zoom; saveState(); };
 
 
 
 
 
 
 
 
 
 
 
 
588
  pDiv.appendChild(img); grid.appendChild(pDiv);
589
  });
590
 
 
24
  return True
25
 
26
  # ======================================================
27
+ # 💾 STORAGE SETUP
28
  # ======================================================
29
  if os.path.exists('/data'):
30
  BASE_STORAGE_PATH = '/data'
 
39
  os.makedirs(BASE_USER_DIR, exist_ok=True)
40
  os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
41
 
42
+ # ======================================================
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
50
+ while True:
51
+ code = ''.join(random.choices(chars, k=length))
52
+ if not os.path.exists(os.path.join(SAVED_COMICS_DIR, code)):
53
+ return code
54
+
55
  # ======================================================
56
  # 🧱 DATA CLASSES
57
  # ======================================================
 
73
  self.panels = panels
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
  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...")
100
  get_real_subtitles(video_path)
 
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}")
 
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
 
125
  selected_moments = []
126
  if not raw_moments:
 
127
  times = np.linspace(1, max(1, duration-1), total_panels_needed)
128
  for t in times: selected_moments.append({'text': '', 'start': t, 'end': t+1})
129
  elif len(raw_moments) <= total_panels_needed:
130
  selected_moments = raw_moments
131
  else:
 
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
  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"
 
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 = []
 
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)
 
204
  def regen_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
205
  import cv2
206
  import json
 
207
  if not os.path.exists(metadata_path): return {"success": False, "message": "No metadata"}
208
  with open(metadata_path, 'r') as f: meta = json.load(f)
209
  if fname not in meta: return {"success": False, "message": "Frame not found"}
 
229
  return {"success": True, "message": f"Time: {new_t:.2f}s"}
230
  return {"success": False, "message": "End of video"}
231
 
232
+ @spaces.GPU
233
+ def get_frame_at_ts_gpu(video_path, frames_dir, metadata_path, fname, ts):
234
+ import cv2
235
+ import json
236
+ cap = cv2.VideoCapture(video_path)
237
+ cap.set(cv2.CAP_PROP_POS_MSEC, float(ts) * 1000)
238
+ ret, frame = cap.read()
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:
248
+ if isinstance(meta[fname], dict): meta[fname]['time'] = float(ts)
249
+ else: meta[fname] = float(ts)
250
+ with open(metadata_path, 'w') as f: json.dump(meta, f, indent=2)
251
+ return {"success": True, "message": f"Jumped to {ts}s"}
252
+ return {"success": False, "message": "Invalid timestamp"}
253
+
254
  # ======================================================
255
  # 💻 BACKEND CLASS
256
  # ======================================================
 
332
  width: 100%; height: 100%;
333
  position: relative;
334
  background: #000;
335
+
336
+ /* Grid Variables */
337
  --y: 50%;
338
  --t1: 100%; --t2: 100%; /* Hidden Right by default */
339
  --b1: 100%; --b2: 100%; /* Hidden Right by default */
 
341
  }
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;
349
+ transform-origin: center;
350
+ transition: transform 0.05s ease-out;
351
+ display: block;
352
+ }
353
  .panel img.panning { cursor: grabbing; transition: none; }
354
  .panel.selected { outline: 4px solid #2196F3; z-index: 5; }
355
 
 
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>
470
 
 
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
  });
 
605
  img.src = pan.src.includes('?') ? pan.src : pan.src + `?sid=${sid}`;
606
  img.dataset.zoom = 100; img.dataset.translateX = 0; img.dataset.translateY = 0;
607
  img.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); selectPanel(pDiv); dragType = 'pan'; activeObj = img; dragStart = {x:e.clientX, y:e.clientY}; img.classList.add('panning'); };
608
+
609
+ // 🚀 ZOOM WHEEL LOGIC (Min zoom 20%)
610
+ img.onwheel = (e) => {
611
+ e.preventDefault();
612
+ let zoom = parseFloat(img.dataset.zoom);
613
+ zoom += e.deltaY * -0.1;
614
+ zoom = Math.min(Math.max(20, zoom), 300); // Allow zoom out to 20%
615
+ img.dataset.zoom = zoom;
616
+ updateImageTransform(img);
617
+ if(selectedPanel === pDiv) document.getElementById('zoom-slider').value = zoom;
618
+ saveState();
619
+ };
620
+
621
  pDiv.appendChild(img); grid.appendChild(pDiv);
622
  });
623