tester343 commited on
Commit
a2964b3
·
verified ·
1 Parent(s): 60d2c02

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +121 -271
app_enhanced.py CHANGED
@@ -17,18 +17,74 @@ from flask import Flask, jsonify, request, send_from_directory, send_file
17
  # ======================================================
18
  # 🚀 ZEROGPU CONFIGURATION
19
  # ======================================================
20
- def update_status_file(path, msg, progress):
21
- """Helper to write status from within GPU function"""
22
- try:
23
- with open(path, 'w') as f:
24
- json.dump({'message': msg, 'progress': progress}, f)
25
- except:
26
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  @spaces.GPU(duration=300)
29
- def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, status_path, target_pages):
30
  print(f"🚀 GPU Task Started: {video_path} | Pages: {target_pages}")
31
- update_status_file(status_path, "Initializing GPU Models...", 10)
32
 
33
  import cv2
34
  import srt
@@ -48,8 +104,6 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, status_p
48
  cap.release()
49
 
50
  user_srt = os.path.join(user_dir, 'subs.srt')
51
- update_status_file(status_path, "Extracting Audio & Subtitles...", 20)
52
-
53
  try:
54
  get_real_subtitles(video_path)
55
  if os.path.exists('test1.srt'):
@@ -65,9 +119,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, status_p
65
  raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
66
 
67
  if target_pages <= 0: target_pages = 1
68
-
69
- # === 5 PANELS PER PAGE ===
70
- panels_per_page = 5
71
  total_panels_needed = target_pages * panels_per_page
72
 
73
  selected_moments = []
@@ -85,8 +137,6 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, status_p
85
  count = 0
86
  frame_files_ordered = []
87
 
88
- update_status_file(status_path, f"Extracting {total_panels_needed} Frames...", 30)
89
-
90
  for i, moment in enumerate(selected_moments):
91
  mid = (moment['start'] + moment['end']) / 2
92
  if mid > duration: mid = duration - 1
@@ -100,23 +150,17 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, status_p
100
  frame_metadata[fname] = {'dialogue': moment['text'], 'time': mid}
101
  frame_files_ordered.append(fname)
102
  count += 1
103
- if count % 2 == 0:
104
- prog = 30 + int((count / total_panels_needed) * 20)
105
- update_status_file(status_path, f"Extracted Frame {count}/{total_panels_needed}", prog)
106
-
107
  cap.release()
108
 
109
  with open(metadata_path, 'w') as f: json.dump(frame_metadata, f, indent=2)
110
-
111
- update_status_file(status_path, "Auto-Cropping...", 55)
112
  try: black_bar_crop()
113
  except: pass
114
 
115
  se = SimpleColorEnhancer()
116
  qe = QualityColorEnhancer()
117
 
118
- for idx, f in enumerate(frame_files_ordered):
119
- update_status_file(status_path, f"Enhancing Image {idx+1}/{len(frame_files_ordered)}", 60 + int((idx / len(frame_files_ordered)) * 20))
120
  p = os.path.join(frames_dir, f)
121
  try: se.enhance_single(p, p)
122
  except: pass
@@ -124,8 +168,6 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, status_p
124
  except: pass
125
 
126
  bubbles_list = []
127
- update_status_file(status_path, "Placing Bubbles...", 85)
128
-
129
  for f in frame_files_ordered:
130
  p = os.path.join(frames_dir, f)
131
  dialogue = frame_metadata.get(f, {}).get('dialogue', '')
@@ -146,8 +188,8 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, status_p
146
 
147
  pages = []
148
  for i in range(target_pages):
149
- start_idx = i * panels_per_page
150
- end_idx = start_idx + panels_per_page
151
  p_frames = frame_files_ordered[start_idx:end_idx]
152
  p_bubbles = bubbles_list[start_idx:end_idx]
153
  if p_frames:
@@ -162,70 +204,6 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, status_p
162
 
163
  return result
164
 
165
- @spaces.GPU
166
- def gpu_warmup():
167
- import torch
168
- print(f"✅ ZeroGPU Warmup: CUDA Available: {torch.cuda.is_available()}")
169
- return True
170
-
171
- # ======================================================
172
- # 💾 PERSISTENT STORAGE CONFIGURATION
173
- # ======================================================
174
- if os.path.exists('/data'):
175
- BASE_STORAGE_PATH = '/data'
176
- print("✅ Using Persistent Storage at /data (Files saved for days/weeks)")
177
- else:
178
- BASE_STORAGE_PATH = '.'
179
- print("⚠️ Using Ephemeral/Local Storage (Files lost on restart)")
180
-
181
- BASE_USER_DIR = os.path.join(BASE_STORAGE_PATH, "userdata")
182
- SAVED_COMICS_DIR = os.path.join(BASE_STORAGE_PATH, "saved_comics")
183
-
184
- os.makedirs(BASE_USER_DIR, exist_ok=True)
185
- os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
186
-
187
- # ======================================================
188
- # 🧱 DATA CLASSES
189
- # ======================================================
190
- def bubble(dialog="", bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, emotion='normal', type='speech'):
191
- return {
192
- 'dialog': dialog,
193
- 'bubble_offset_x': int(bubble_offset_x),
194
- 'bubble_offset_y': int(bubble_offset_y),
195
- 'lip_x': int(lip_x),
196
- 'lip_y': int(lip_y),
197
- 'emotion': emotion,
198
- 'type': type,
199
- 'tail_pos': '50%',
200
- 'classes': f'speech-bubble {type} tail-bottom'
201
- }
202
-
203
- def panel(image=""):
204
- return {'image': image}
205
-
206
- class Page:
207
- def __init__(self, panels, bubbles):
208
- self.panels = panels
209
- self.bubbles = bubbles
210
-
211
- # ======================================================
212
- # 🔧 APP CONFIG
213
- # ======================================================
214
- logging.basicConfig(level=logging.INFO)
215
- logger = logging.getLogger(__name__)
216
-
217
- app = Flask(__name__)
218
-
219
- def generate_save_code(length=8):
220
- chars = string.ascii_uppercase + string.digits
221
- while True:
222
- code = ''.join(random.choices(chars, k=length))
223
- if not os.path.exists(os.path.join(SAVED_COMICS_DIR, code)):
224
- return code
225
-
226
- # ======================================================
227
- # 🧠 GLOBAL GPU FUNCTIONS
228
- # ======================================================
229
  @spaces.GPU
230
  def regen_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
231
  import cv2
@@ -308,10 +286,8 @@ class EnhancedComicGenerator:
308
 
309
  def run(self, target_pages):
310
  try:
311
- self.write_status("Queued for GPU...", 5)
312
- status_file = os.path.join(self.output_dir, 'status.json')
313
- # Pass status_file path to GPU function for real-time updates
314
- data = generate_comic_gpu(self.video_path, self.user_dir, self.frames_dir, self.metadata_path, status_file, int(target_pages))
315
  with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
316
  json.dump(data, f, indent=2)
317
  self.write_status("Complete!", 100)
@@ -356,97 +332,14 @@ INDEX_HTML = '''
356
  @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
357
 
358
  /* COMIC LAYOUT */
359
- .comic-wrapper { max-width: 1200px; margin: 0 auto; display:flex; flex-direction:column; align-items:center; }
360
- .page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; width: 100%; }
361
  .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
362
-
363
- /*
364
- SIZE UPDATE: 1000px width x 712px height
365
- (Matches 350px x 2 tiers + 12px gutter)
366
- Scaled down using transform for viewing
367
- */
368
- .comic-page {
369
- background: white;
370
- width: 1000px;
371
- height: 712px;
372
- box-shadow: 0 4px 10px rgba(0,0,0,0.1);
373
- position: relative;
374
- overflow: hidden;
375
- border: 2px solid #000;
376
- padding: 0;
377
- /* SCALING FOR EDITOR VIEW */
378
- transform-origin: top center;
379
- transform: scale(0.6);
380
- margin-bottom: -280px; /* Compensate for scale empty space */
381
- }
382
-
383
- /* === 5-PANEL TEMPLATE (2 TOP, 3 BOTTOM) === */
384
- /* Background White for 12px Gutter (1.2% width, 1.7% height of 712px) */
385
- .comic-grid { width: 100%; height: 100%; position: relative; background: #ffffff; }
386
-
387
- /* Panel Background White */
388
- .panel { position: absolute; overflow: hidden; background: #ffffff; cursor: pointer; border: 0; display:flex; justify-content:center; align-items:center; }
389
- .panel.selected { z-index: 20; }
390
- .panel.selected img { outline: 3px solid #2196F3; outline-offset: -3px; }
391
-
392
- /*
393
- COORDINATES from Green Text:
394
- Row 1 Split: Top=63.2%, Bottom=59.5%
395
- Row 2 Left Split: Top=33.0%, Bottom=35.7%
396
- Row 2 Right Split: Top=64.8%, Bottom=68.2%
397
- Tier Height: 350px (approx 49.15% of 712px)
398
- */
399
-
400
- /* --- ROW 1 (TOP) --- Height 49.15% */
401
-
402
- /* Panel 1: Right ends at (63.2 - 0.6)% / (59.5 - 0.6)% */
403
- .panel:nth-child(1) {
404
- top: 0; left: 0; height: 49.15%; width: 100%;
405
- clip-path: polygon(0% 0%, 62.6% 0%, 58.9% 100%, 0% 100%);
406
- }
407
- /* Panel 2: Left starts at (63.2 + 0.6)% / (59.5 + 0.6)% */
408
- .panel:nth-child(2) {
409
- top: 0; left: 0; height: 49.15%; width: 100%;
410
- clip-path: polygon(63.8% 0%, 100% 0%, 100% 100%, 60.1% 100%);
411
- }
412
-
413
- /* --- ROW 2 (BOTTOM) --- Top: 50.85% (Gap ~1.7%), Height: 49.15% */
414
-
415
- /* Panel 3: Right ends at (33.0 - 0.6)% / (35.7 - 0.6)% */
416
- .panel:nth-child(3) {
417
- top: 50.85%; left: 0; height: 49.15%; width: 100%;
418
- clip-path: polygon(0% 0%, 32.4% 0%, 35.1% 100%, 0% 100%);
419
- }
420
- /* Panel 4: Left starts (33.0 + 0.6)% / (35.7 + 0.6)%. Right ends (64.8 - 0.6)% / (68.2 - 0.6)% */
421
- .panel:nth-child(4) {
422
- top: 50.85%; left: 0; height: 49.15%; width: 100%;
423
- clip-path: polygon(33.6% 0%, 64.2% 0%, 67.6% 100%, 36.3% 100%);
424
- }
425
- /* Panel 5: Left starts (64.8 + 0.6)% / (68.2 + 0.6)% */
426
- .panel:nth-child(5) {
427
- top: 50.85%; left: 0; height: 49.15%; width: 100%;
428
- clip-path: polygon(65.4% 0%, 100% 0%, 100% 100%, 68.8% 100%);
429
- }
430
-
431
- /* ====================== */
432
-
433
- /*
434
- IMAGE FIT: 'fill' to STRETCH fully into the panel shape
435
- WITHOUT empty space and WITHOUT zooming.
436
- (User accepted distortion over empty space/cutting)
437
- Allowed to be transformed (panned/scaled) via JS if user wants.
438
- */
439
- .panel img {
440
- width: 100%; height: 100%;
441
- object-fit: fill; /* STRETCH: No gaps, no cut */
442
- transition: transform 0.1s ease-out;
443
- transform-origin: center center;
444
- display: block;
445
- }
446
- /* Toggle classes */
447
- .panel img.fit-cover { object-fit: cover !important; }
448
- .panel img.fit-contain { object-fit: contain !important; }
449
-
450
  .panel img.pannable { cursor: grab; }
451
  .panel img.panning { cursor: grabbing; }
452
 
@@ -456,15 +349,30 @@ INDEX_HTML = '''
456
  width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
457
  z-index: 10; cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold;
458
  font-size: 13px; text-align: center;
 
459
  overflow: visible;
460
  line-height: 1.2;
461
  --tail-pos: 50%;
462
  }
463
 
 
464
  .bubble-text {
465
- padding: 0.5em; word-wrap: break-word; white-space: pre-wrap; position: relative;
466
- z-index: 5; pointer-events: none; user-select: none; width: 100%; height: 100%;
467
- overflow: hidden; display: flex; align-items: center; justify-content: center; border-radius: inherit;
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  }
469
 
470
  .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
@@ -491,15 +399,26 @@ INDEX_HTML = '''
491
  .speech-bubble.speech.tail-left:before { right: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: rotate(90deg); transform-origin: top right; }
492
  .speech-bubble.speech.tail-right { border-radius: var(--r); }
493
  .speech-bubble.speech.tail-right:before { left: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: rotate(-90deg); transform-origin: top left; }
494
-
495
- /* Thought/Reaction Styles */
496
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
497
  .speech-bubble.thought::before { display:none; }
498
  .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
499
  .thought-dot-1 { width: 20px; height: 20px; }
500
  .thought-dot-2 { width: 12px; height: 12px; }
 
 
501
  .speech-bubble.thought.pos-bl .thought-dot-1 { left: 20px; bottom: -20px; }
502
  .speech-bubble.thought.pos-bl .thought-dot-2 { left: 10px; bottom: -32px; }
 
 
 
 
 
 
 
 
 
 
503
  .speech-bubble.reaction { background: #FFD700; border: 3px solid #E53935; color: #D32F2F; font-weight: 900; text-transform: uppercase; clip-path: polygon(0% 25%, 17% 21%, 17% 0%, 31% 16%, 50% 4%, 69% 16%, 83% 0%, 83% 21%, 100% 25%, 85% 45%, 95% 62%, 82% 79%, 100% 97%, 79% 89%, 60% 98%, 46% 82%, 27% 95%, 15% 78%, 5% 62%, 15% 45%); }
504
  .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
505
 
@@ -537,7 +456,7 @@ INDEX_HTML = '''
537
  <div class="page-input-group">
538
  <label>📚 Total Comic Pages:</label>
539
  <input type="number" id="page-count" value="4" min="1" max="15" placeholder="e.g. 4 (Video will be divided evenly)">
540
- <small style="color:#666; font-size:11px; display:block; margin-top:5px;">System calculates 5 panels per page.</small>
541
  </div>
542
 
543
  <button class="submit-btn" onclick="upload()">🚀 Generate Comic</button>
@@ -608,7 +527,6 @@ INDEX_HTML = '''
608
 
609
  <div class="control-group">
610
  <label>🖼️ Panel Tools:</label>
611
- <button onclick="toggleFitCover()" class="action-btn">🔄 Toggle Fit/Fill</button>
612
  <button onclick="replacePanelImage()" class="action-btn">🖼️ Replace Image</button>
613
  <div class="button-grid">
614
  <button onclick="adjustFrame('backward')" class="secondary-btn" id="prev-btn">⬅️ Prev</button>
@@ -734,8 +652,7 @@ INDEX_HTML = '''
734
  const pages = [];
735
  document.querySelectorAll('.comic-page').forEach(p => {
736
  const panels = [];
737
- const grid = p.querySelector('.comic-grid');
738
- grid.querySelectorAll('.panel').forEach(pan => {
739
  const img = pan.querySelector('img');
740
  const bubbles = [];
741
  pan.querySelectorAll('.speech-bubble').forEach(b => {
@@ -752,7 +669,6 @@ INDEX_HTML = '''
752
  panels.push({
753
  src: img.src,
754
  zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY,
755
- fit: img.style.objectFit,
756
  bubbles: bubbles
757
  });
758
  });
@@ -772,20 +688,14 @@ INDEX_HTML = '''
772
  const pageWrapper = document.createElement('div'); pageWrapper.className = 'page-wrapper';
773
  const pageTitle = document.createElement('h2'); pageTitle.className = 'page-title'; pageTitle.textContent = `Page ${pageIdx + 1}`;
774
  pageWrapper.appendChild(pageTitle);
775
-
776
  const div = document.createElement('div'); div.className = 'comic-page';
777
  const grid = document.createElement('div'); grid.className = 'comic-grid';
778
-
779
  page.panels.forEach((pan) => {
780
  const pDiv = document.createElement('div'); pDiv.className = 'panel';
781
  pDiv.onclick = (e) => { e.stopPropagation(); selectPanel(pDiv); };
782
  const img = document.createElement('img');
783
  img.src = pan.src.includes('?') ? pan.src : pan.src + `?sid=${sid}`;
784
  img.dataset.zoom = pan.zoom || 100; img.dataset.translateX = pan.tx || 0; img.dataset.translateY = pan.ty || 0;
785
-
786
- // RESTORE FIT STATE
787
- if(pan.fit) img.style.objectFit = pan.fit;
788
-
789
  updateImageTransform(img);
790
  img.onmousedown = (e) => startPan(e, img);
791
  pDiv.appendChild(img);
@@ -972,67 +882,17 @@ INDEX_HTML = '''
972
  document.getElementById('bubble-text-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(true); } });
973
  document.getElementById('bubble-fill-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(true); } });
974
 
975
- function handleZoom(el) {
976
- if(!selectedPanel) return;
977
- const img = selectedPanel.querySelector('img');
978
- img.dataset.zoom = el.value;
979
- updateImageTransform(img);
980
- }
981
  document.getElementById('zoom-slider').addEventListener('change', () => saveDraft(true));
982
-
983
- function startPan(e, img) {
984
- // REMOVED ZOOM CHECK - ALLOW PAN ALWAYS
985
- e.preventDefault();
986
- isPanning = true;
987
- selectedPanel = img.closest('.panel');
988
- panStartX = e.clientX;
989
- panStartY = e.clientY;
990
- panStartTx = parseFloat(img.dataset.translateX || 0);
991
- panStartTy = parseFloat(img.dataset.translateY || 0);
992
- img.classList.add('panning');
993
- }
994
-
995
- function panImage(e) {
996
- if(!isPanning || !selectedPanel) return;
997
- const img = selectedPanel.querySelector('img');
998
- img.dataset.translateX = panStartTx + (e.clientX - panStartX);
999
- img.dataset.translateY = panStartTy + (e.clientY - panStartY);
1000
- updateImageTransform(img);
1001
- }
1002
-
1003
- function updateImageTransform(img) {
1004
- const z = (img.dataset.zoom || 100) / 100;
1005
- const x = img.dataset.translateX || 0;
1006
- const y = img.dataset.translateY || 0;
1007
- img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`;
1008
- }
1009
-
1010
- function resetPanelTransform() {
1011
- if(!selectedPanel) return alert("Select a panel");
1012
- const img = selectedPanel.querySelector('img');
1013
- img.dataset.zoom = 100;
1014
- img.dataset.translateX = 0;
1015
- img.dataset.translateY = 0;
1016
- document.getElementById('zoom-slider').value = 100;
1017
- updateImageTransform(img);
1018
- saveDraft(true);
1019
- }
1020
 
1021
  function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
1022
  async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
1023
  async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
1024
 
1025
- // NEW: Toggle between 'fill' (stretch 100%), 'cover' (crop), and 'contain' (bars)
1026
- function toggleFitCover() {
1027
- if(!selectedPanel) return alert("Select a panel");
1028
- const img = selectedPanel.querySelector('img');
1029
- const current = img.style.objectFit || 'fill';
1030
- if(current === 'fill') img.style.objectFit = 'cover';
1031
- else if(current === 'cover') img.style.objectFit = 'contain';
1032
- else img.style.objectFit = 'fill';
1033
- saveDraft(true);
1034
- }
1035
-
1036
  async function exportComic() {
1037
  const pgs = document.querySelectorAll('.comic-page');
1038
  if(pgs.length === 0) return alert("No pages found");
@@ -1042,13 +902,6 @@ INDEX_HTML = '''
1042
  if(selectedPanel) selectedPanel.classList.remove('selected');
1043
  alert(`Exporting ${pgs.length} page(s)...`);
1044
 
1045
- // --- PREPARE FOR EXPORT: Remove editor scaling ---
1046
- const comicPages = document.querySelectorAll('.comic-page');
1047
- comicPages.forEach(page => {
1048
- page.style.transform = 'none'; // Remove scaling from comic-page
1049
- page.style.marginBottom = '0'; // Remove margin compensation
1050
- });
1051
-
1052
  // --- 0% ERROR FIX ---
1053
  // 1. Lock specific pixel dimensions + 1px buffer to prevent word wrapping
1054
  const bubbles = document.querySelectorAll('.speech-bubble');
@@ -1066,8 +919,8 @@ INDEX_HTML = '''
1066
  for(let i = 0; i < pgs.length; i++) {
1067
  try {
1068
  const u = await htmlToImage.toPng(pgs[i], {
1069
- pixelRatio: 2, // High quality for print
1070
- style: { transform: 'none' } // Ensure no transforms for html-to-image
1071
  });
1072
  const a = document.createElement('a');
1073
  a.href = u;
@@ -1079,11 +932,8 @@ INDEX_HTML = '''
1079
  }
1080
  }
1081
 
1082
- // --- RESTORE EDITOR SCALING AFTER EXPORT ---
1083
- comicPages.forEach(page => {
1084
- page.style.transform = 'scale(0.6)';
1085
- page.style.marginBottom = '-280px';
1086
- });
1087
  }
1088
 
1089
  function goBackToUpload() { if(confirm('Go home? Unsaved changes will be lost.')) { document.getElementById('editor-container').style.display = 'none'; document.getElementById('upload-container').style.display = 'flex'; document.getElementById('loading-view').style.display = 'none'; } }
 
17
  # ======================================================
18
  # 🚀 ZEROGPU CONFIGURATION
19
  # ======================================================
20
+ @spaces.GPU
21
+ def gpu_warmup():
22
+ import torch
23
+ print(f"✅ ZeroGPU Warmup: CUDA Available: {torch.cuda.is_available()}")
24
+ return True
25
+
26
+ # ======================================================
27
+ # 💾 PERSISTENT STORAGE CONFIGURATION
28
+ # ======================================================
29
+ # Checks for Hugging Face Persistent Storage
30
+ if os.path.exists('/data'):
31
+ BASE_STORAGE_PATH = '/data'
32
+ print("✅ Using Persistent Storage at /data (Files saved for days/weeks)")
33
+ else:
34
+ BASE_STORAGE_PATH = '.'
35
+ print("⚠️ Using Ephemeral/Local Storage (Files lost on restart)")
36
+
37
+ BASE_USER_DIR = os.path.join(BASE_STORAGE_PATH, "userdata")
38
+ SAVED_COMICS_DIR = os.path.join(BASE_STORAGE_PATH, "saved_comics")
39
+
40
+ os.makedirs(BASE_USER_DIR, exist_ok=True)
41
+ os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
42
 
43
+ # ======================================================
44
+ # 🧱 DATA CLASSES
45
+ # ======================================================
46
+ def bubble(dialog="", bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, emotion='normal', type='speech'):
47
+ return {
48
+ 'dialog': dialog,
49
+ 'bubble_offset_x': int(bubble_offset_x),
50
+ 'bubble_offset_y': int(bubble_offset_y),
51
+ 'lip_x': int(lip_x),
52
+ 'lip_y': int(lip_y),
53
+ 'emotion': emotion,
54
+ 'type': type,
55
+ 'tail_pos': '50%',
56
+ 'classes': f'speech-bubble {type} tail-bottom'
57
+ }
58
+
59
+ def panel(image=""):
60
+ return {'image': image}
61
+
62
+ class Page:
63
+ def __init__(self, panels, bubbles):
64
+ self.panels = panels
65
+ self.bubbles = bubbles
66
+
67
+ # ======================================================
68
+ # 🔧 APP CONFIG
69
+ # ======================================================
70
+ logging.basicConfig(level=logging.INFO)
71
+ logger = logging.getLogger(__name__)
72
+
73
+ app = Flask(__name__)
74
+
75
+ def generate_save_code(length=8):
76
+ chars = string.ascii_uppercase + string.digits
77
+ while True:
78
+ code = ''.join(random.choices(chars, k=length))
79
+ if not os.path.exists(os.path.join(SAVED_COMICS_DIR, code)):
80
+ return code
81
+
82
+ # ======================================================
83
+ # 🧠 GLOBAL GPU FUNCTIONS
84
+ # ======================================================
85
  @spaces.GPU(duration=300)
86
+ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
87
  print(f"🚀 GPU Task Started: {video_path} | Pages: {target_pages}")
 
88
 
89
  import cv2
90
  import srt
 
104
  cap.release()
105
 
106
  user_srt = os.path.join(user_dir, 'subs.srt')
 
 
107
  try:
108
  get_real_subtitles(video_path)
109
  if os.path.exists('test1.srt'):
 
119
  raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
120
 
121
  if target_pages <= 0: target_pages = 1
122
+ panels_per_page = 4
 
 
123
  total_panels_needed = target_pages * panels_per_page
124
 
125
  selected_moments = []
 
137
  count = 0
138
  frame_files_ordered = []
139
 
 
 
140
  for i, moment in enumerate(selected_moments):
141
  mid = (moment['start'] + moment['end']) / 2
142
  if mid > duration: mid = duration - 1
 
150
  frame_metadata[fname] = {'dialogue': moment['text'], 'time': mid}
151
  frame_files_ordered.append(fname)
152
  count += 1
 
 
 
 
153
  cap.release()
154
 
155
  with open(metadata_path, 'w') as f: json.dump(frame_metadata, f, indent=2)
156
+
 
157
  try: black_bar_crop()
158
  except: pass
159
 
160
  se = SimpleColorEnhancer()
161
  qe = QualityColorEnhancer()
162
 
163
+ for f in frame_files_ordered:
 
164
  p = os.path.join(frames_dir, f)
165
  try: se.enhance_single(p, p)
166
  except: pass
 
168
  except: pass
169
 
170
  bubbles_list = []
 
 
171
  for f in frame_files_ordered:
172
  p = os.path.join(frames_dir, f)
173
  dialogue = frame_metadata.get(f, {}).get('dialogue', '')
 
188
 
189
  pages = []
190
  for i in range(target_pages):
191
+ start_idx = i * 4
192
+ end_idx = start_idx + 4
193
  p_frames = frame_files_ordered[start_idx:end_idx]
194
  p_bubbles = bubbles_list[start_idx:end_idx]
195
  if p_frames:
 
204
 
205
  return result
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  @spaces.GPU
208
  def regen_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
209
  import cv2
 
286
 
287
  def run(self, target_pages):
288
  try:
289
+ self.write_status("Waiting for GPU...", 5)
290
+ data = generate_comic_gpu(self.video_path, self.user_dir, self.frames_dir, self.metadata_path, int(target_pages))
 
 
291
  with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
292
  json.dump(data, f, indent=2)
293
  self.write_status("Complete!", 100)
 
332
  @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
333
 
334
  /* COMIC LAYOUT */
335
+ .comic-wrapper { max-width: 1000px; margin: 0 auto; }
336
+ .page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
337
  .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
338
+ .comic-page { background: white; width: 600px; height: 400px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); position: relative; overflow: hidden; border: 2px solid #000; padding: 10px; }
339
+ .comic-grid { display: grid; grid-template-columns: 285px 285px; grid-template-rows: 185px 185px; gap: 10px; width: 100%; height: 100%; }
340
+ .panel { position: relative; overflow: hidden; border: 2px solid #000; background: #eee; cursor: pointer; }
341
+ .panel.selected { outline: 3px solid #2196F3; outline-offset: -3px; }
342
+ .panel img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.1s ease-out; transform-origin: center center; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  .panel img.pannable { cursor: grab; }
344
  .panel img.panning { cursor: grabbing; }
345
 
 
349
  width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
350
  z-index: 10; cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold;
351
  font-size: 13px; text-align: center;
352
+ /* FIX FOR TAIL VISIBILITY */
353
  overflow: visible;
354
  line-height: 1.2;
355
  --tail-pos: 50%;
356
  }
357
 
358
+ /* FIX FOR 0% ERROR EXPORT: Handle overflow and clipping here */
359
  .bubble-text {
360
+ padding: 0.5em;
361
+ word-wrap: break-word;
362
+ white-space: pre-wrap;
363
+ position: relative;
364
+ z-index: 5;
365
+ pointer-events: none;
366
+ user-select: none;
367
+ width: 100%;
368
+ height: 100%;
369
+ /* Strict overflow hidden to match editor vs export */
370
+ overflow: hidden;
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ /* Ensure border radius matches parent so text doesn't leak corners */
375
+ border-radius: inherit;
376
  }
377
 
378
  .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
 
399
  .speech-bubble.speech.tail-left:before { right: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: rotate(90deg); transform-origin: top right; }
400
  .speech-bubble.speech.tail-right { border-radius: var(--r); }
401
  .speech-bubble.speech.tail-right:before { left: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: rotate(-90deg); transform-origin: top left; }
402
+ /* THOUGHT BUBBLE CSS (Fixed Rotation) */
 
403
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
404
  .speech-bubble.thought::before { display:none; }
405
  .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
406
  .thought-dot-1 { width: 20px; height: 20px; }
407
  .thought-dot-2 { width: 12px; height: 12px; }
408
+
409
+ /* Thought Tail Positions */
410
  .speech-bubble.thought.pos-bl .thought-dot-1 { left: 20px; bottom: -20px; }
411
  .speech-bubble.thought.pos-bl .thought-dot-2 { left: 10px; bottom: -32px; }
412
+
413
+ .speech-bubble.thought.pos-br .thought-dot-1 { right: 20px; bottom: -20px; }
414
+ .speech-bubble.thought.pos-br .thought-dot-2 { right: 10px; bottom: -32px; }
415
+
416
+ .speech-bubble.thought.pos-tr .thought-dot-1 { right: 20px; top: -20px; }
417
+ .speech-bubble.thought.pos-tr .thought-dot-2 { right: 10px; top: -32px; }
418
+
419
+ .speech-bubble.thought.pos-tl .thought-dot-1 { left: 20px; top: -20px; }
420
+ .speech-bubble.thought.pos-tl .thought-dot-2 { left: 10px; top: -32px; }
421
+
422
  .speech-bubble.reaction { background: #FFD700; border: 3px solid #E53935; color: #D32F2F; font-weight: 900; text-transform: uppercase; clip-path: polygon(0% 25%, 17% 21%, 17% 0%, 31% 16%, 50% 4%, 69% 16%, 83% 0%, 83% 21%, 100% 25%, 85% 45%, 95% 62%, 82% 79%, 100% 97%, 79% 89%, 60% 98%, 46% 82%, 27% 95%, 15% 78%, 5% 62%, 15% 45%); }
423
  .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
424
 
 
456
  <div class="page-input-group">
457
  <label>📚 Total Comic Pages:</label>
458
  <input type="number" id="page-count" value="4" min="1" max="15" placeholder="e.g. 4 (Video will be divided evenly)">
459
+ <small style="color:#666; font-size:11px; display:block; margin-top:5px;">System calculates ~4 panels per page.</small>
460
  </div>
461
 
462
  <button class="submit-btn" onclick="upload()">🚀 Generate Comic</button>
 
527
 
528
  <div class="control-group">
529
  <label>🖼️ Panel Tools:</label>
 
530
  <button onclick="replacePanelImage()" class="action-btn">🖼️ Replace Image</button>
531
  <div class="button-grid">
532
  <button onclick="adjustFrame('backward')" class="secondary-btn" id="prev-btn">⬅️ Prev</button>
 
652
  const pages = [];
653
  document.querySelectorAll('.comic-page').forEach(p => {
654
  const panels = [];
655
+ p.querySelectorAll('.panel').forEach(pan => {
 
656
  const img = pan.querySelector('img');
657
  const bubbles = [];
658
  pan.querySelectorAll('.speech-bubble').forEach(b => {
 
669
  panels.push({
670
  src: img.src,
671
  zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY,
 
672
  bubbles: bubbles
673
  });
674
  });
 
688
  const pageWrapper = document.createElement('div'); pageWrapper.className = 'page-wrapper';
689
  const pageTitle = document.createElement('h2'); pageTitle.className = 'page-title'; pageTitle.textContent = `Page ${pageIdx + 1}`;
690
  pageWrapper.appendChild(pageTitle);
 
691
  const div = document.createElement('div'); div.className = 'comic-page';
692
  const grid = document.createElement('div'); grid.className = 'comic-grid';
 
693
  page.panels.forEach((pan) => {
694
  const pDiv = document.createElement('div'); pDiv.className = 'panel';
695
  pDiv.onclick = (e) => { e.stopPropagation(); selectPanel(pDiv); };
696
  const img = document.createElement('img');
697
  img.src = pan.src.includes('?') ? pan.src : pan.src + `?sid=${sid}`;
698
  img.dataset.zoom = pan.zoom || 100; img.dataset.translateX = pan.tx || 0; img.dataset.translateY = pan.ty || 0;
 
 
 
 
699
  updateImageTransform(img);
700
  img.onmousedown = (e) => startPan(e, img);
701
  pDiv.appendChild(img);
 
882
  document.getElementById('bubble-text-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(true); } });
883
  document.getElementById('bubble-fill-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(true); } });
884
 
885
+ function handleZoom(el) { if(!selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.zoom = el.value; updateImageTransform(img); }
 
 
 
 
 
886
  document.getElementById('zoom-slider').addEventListener('change', () => saveDraft(true));
887
+ function startPan(e, img) { if(parseFloat(img.dataset.zoom || 100) <= 100) return; e.preventDefault(); isPanning = true; selectedPanel = img.closest('.panel'); panStartX = e.clientX; panStartY = e.clientY; panStartTx = parseFloat(img.dataset.translateX || 0); panStartTy = parseFloat(img.dataset.translateY || 0); img.classList.add('panning'); }
888
+ function panImage(e) { if(!isPanning || !selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.translateX = panStartTx + (e.clientX - panStartX); img.dataset.translateY = panStartTy + (e.clientY - panStartY); updateImageTransform(img); }
889
+ function updateImageTransform(img) { const z = (img.dataset.zoom || 100) / 100; const x = img.dataset.translateX || 0; const y = img.dataset.translateY || 0; img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`; img.classList.toggle('pannable', z > 1); }
890
+ function resetPanelTransform() { if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); img.dataset.zoom = 100; img.dataset.translateX = 0; img.dataset.translateY = 0; document.getElementById('zoom-slider').value = 100; updateImageTransform(img); saveDraft(true); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
891
 
892
  function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
893
  async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
894
  async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
895
 
 
 
 
 
 
 
 
 
 
 
 
896
  async function exportComic() {
897
  const pgs = document.querySelectorAll('.comic-page');
898
  if(pgs.length === 0) return alert("No pages found");
 
902
  if(selectedPanel) selectedPanel.classList.remove('selected');
903
  alert(`Exporting ${pgs.length} page(s)...`);
904
 
 
 
 
 
 
 
 
905
  // --- 0% ERROR FIX ---
906
  // 1. Lock specific pixel dimensions + 1px buffer to prevent word wrapping
907
  const bubbles = document.querySelectorAll('.speech-bubble');
 
919
  for(let i = 0; i < pgs.length; i++) {
920
  try {
921
  const u = await htmlToImage.toPng(pgs[i], {
922
+ pixelRatio: 2, // High quality
923
+ style: { transform: 'none' }
924
  });
925
  const a = document.createElement('a');
926
  a.href = u;
 
932
  }
933
  }
934
 
935
+ // Optional: Reload page or reset styles if user wants to continue editing immediately
936
+ // In this app, styles are locked until reload, which is safer for the export focus.
 
 
 
937
  }
938
 
939
  function goBackToUpload() { if(confirm('Go home? Unsaved changes will be lost.')) { document.getElementById('editor-container').style.display = 'none'; document.getElementById('upload-container').style.display = 'flex'; document.getElementById('loading-view').style.display = 'none'; } }