tester343 commited on
Commit
f461aed
·
verified ·
1 Parent(s): 559291a

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +40 -246
app_enhanced.py CHANGED
@@ -1,4 +1,4 @@
1
- import spaces # <--- CRITICAL: MUST BE THE FIRST IMPORT
2
  import os
3
  import time
4
  import threading
@@ -24,7 +24,7 @@ def gpu_warmup():
24
  return True
25
 
26
  # ======================================================
27
- # 🧱 DATA CLASSES (Defined Locally)
28
  # ======================================================
29
  def bubble(dialog="", bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, emotion='normal', type='speech'):
30
  return {
@@ -68,14 +68,13 @@ def generate_save_code(length=8):
68
  return code
69
 
70
  # ======================================================
71
- # 🧠 GLOBAL GPU FUNCTIONS (The Engine)
72
  # ======================================================
73
 
74
  @spaces.GPU(duration=300)
75
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
76
  print(f"🚀 GPU Task Started: {video_path} | Pages: {target_pages}")
77
 
78
- # --- Local Imports for GPU Context ---
79
  import cv2
80
  import srt
81
  import numpy as np
@@ -106,8 +105,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
106
  with open(user_srt, 'r', encoding='utf-8') as f:
107
  all_subs = list(srt.parse(f.read()))
108
 
109
- # 3. Smart Keyframe Selection (Equal Division Logic)
110
- # Filter out empty subtitles
111
  valid_subs = [s for s in all_subs if s.content.strip()]
112
  raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
113
 
@@ -115,18 +113,14 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
115
  panels_per_page = 4
116
  total_panels_needed = target_pages * panels_per_page
117
 
118
- print(f"📊 Calculating: Needed {total_panels_needed} panels.")
119
-
120
  selected_moments = []
121
  if not raw_moments:
122
- # Fallback if no audio/subs: Just split time evenly
123
  times = np.linspace(1, duration-1, total_panels_needed)
124
  for t in times:
125
  selected_moments.append({'text': '', 'start': t, 'end': t+1})
126
  elif len(raw_moments) <= total_panels_needed:
127
  selected_moments = raw_moments
128
  else:
129
- # EVENLY DISTRIBUTE selection across the entire list
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
 
@@ -146,12 +140,9 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
146
  fname = f"frame_{count:04d}.png"
147
  p = os.path.join(frames_dir, fname)
148
  cv2.imwrite(p, frame)
149
- os.sync() # Ensure write
150
 
151
- frame_metadata[fname] = {
152
- 'dialogue': moment['text'],
153
- 'time': mid
154
- }
155
  frame_files_ordered.append(fname)
156
  count += 1
157
  cap.release()
@@ -165,7 +156,6 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
165
  se = SimpleColorEnhancer()
166
  qe = QualityColorEnhancer()
167
 
168
- print("🎨 Enhancing images...")
169
  for f in frame_files_ordered:
170
  p = os.path.join(frames_dir, f)
171
  try: se.enhance_single(p, p)
@@ -174,14 +164,11 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
174
  except: pass
175
 
176
  # 6. Bubble Placement
177
- print("💬 Placing bubbles...")
178
  bubbles_list = []
179
-
180
  for f in frame_files_ordered:
181
  p = os.path.join(frames_dir, f)
182
  dialogue = frame_metadata.get(f, {}).get('dialogue', '')
183
 
184
- # Heuristic for bubble type
185
  b_type = 'speech'
186
  if '(' in dialogue and ')' in dialogue: b_type = 'narration'
187
  elif '!' in dialogue and dialogue.isupper(): b_type = 'reaction'
@@ -191,36 +178,22 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
191
  faces = face_detector.detect_faces(p)
192
  lip = face_detector.get_lip_position(p, faces[0]) if faces else (-1, -1)
193
  bx, by = ai_bubble_placer.place_bubble_ai(p, lip)
194
-
195
- b = bubble(
196
- dialog=dialogue,
197
- bubble_offset_x=bx,
198
- bubble_offset_y=by,
199
- lip_x=lip[0],
200
- lip_y=lip[1],
201
- type=b_type
202
- )
203
  bubbles_list.append(b)
204
  except:
205
  bubbles_list.append(bubble(dialog=dialogue, type=b_type))
206
 
207
- # 7. Final Layout Construction
208
- print("📄 Assembling pages...")
209
  pages = []
210
-
211
- # Strictly 4 panels per page based on calculation
212
  for i in range(target_pages):
213
  start_idx = i * 4
214
  end_idx = start_idx + 4
215
-
216
  p_frames = frame_files_ordered[start_idx:end_idx]
217
  p_bubbles = bubbles_list[start_idx:end_idx]
218
-
219
  if p_frames:
220
  pg_panels = [panel(image=f) for f in p_frames]
221
  pages.append(Page(panels=pg_panels, bubbles=p_bubbles))
222
 
223
- # Serialize
224
  result = []
225
  for pg in pages:
226
  p_data = [p if isinstance(p, dict) else p.__dict__ for p in pg.panels]
@@ -325,7 +298,7 @@ class EnhancedComicGenerator:
325
  json.dump({'message': msg, 'progress': prog}, f)
326
 
327
  # ======================================================
328
- # 🌐 ROUTES & FULL UI
329
  # ======================================================
330
 
331
  INDEX_HTML = '''
@@ -367,7 +340,6 @@ INDEX_HTML = '''
367
  .loader { width: 120px; height: 20px; background: radial-gradient(circle 10px, #e67e22 100%, transparent 0); background-size: 20px 20px; animation: ball 1s infinite linear; margin: 20px auto; }
368
  @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
369
 
370
- /* COMIC ELEMENTS */
371
  .comic-wrapper { max-width: 1000px; margin: 0 auto; }
372
  .page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
373
  .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
@@ -379,7 +351,7 @@ INDEX_HTML = '''
379
  .panel img.pannable { cursor: grab; }
380
  .panel img.panning { cursor: grabbing; }
381
 
382
- /* BUBBLES */
383
  .speech-bubble {
384
  position: absolute; display: flex; justify-content: center; align-items: center;
385
  width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
@@ -387,11 +359,11 @@ INDEX_HTML = '''
387
  font-size: 13px; text-align: center; overflow: visible;
388
  --tail-pos: 50%;
389
  }
390
- .bubble-text { padding: 0.8em; word-wrap: break-word; position: relative; z-index: 5; }
391
  .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
392
  .speech-bubble textarea { position: absolute; top:0; left:0; width:100%; height:100%; box-sizing:border-box; border:1px solid #4CAF50; background:rgba(255,255,255,0.95); text-align:center; padding:8px; z-index:102; resize:none; font: inherit; }
393
 
394
- /* --- TAIL CSS --- */
395
  .speech-bubble.speech {
396
  --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
397
  background: var(--bubble-fill-color, #4ECDC4);
@@ -405,15 +377,12 @@ INDEX_HTML = '''
405
  -webkit-mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
406
  mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
407
  }
408
- /* Tail Orientations */
409
- .speech-bubble.speech.tail-bottom:before { top: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
410
 
 
411
  .speech-bubble.speech.tail-top { border-radius: min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) var(--r) var(--r) / var(--r); }
412
  .speech-bubble.speech.tail-top:before { bottom: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: scaleY(-1); }
413
-
414
  .speech-bubble.speech.tail-left { border-radius: var(--r); }
415
  .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; }
416
-
417
  .speech-bubble.speech.tail-right { border-radius: var(--r); }
418
  .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; }
419
 
@@ -578,7 +547,7 @@ INDEX_HTML = '''
578
  localStorage.setItem('comic_sid', sid);
579
 
580
  let currentSaveCode = null;
581
- let isProcessing = false; // Lock
582
  let interval, selectedBubble = null, selectedPanel = null;
583
  let isDragging = false, isResizing = false, isPanning = false;
584
  let startX, startY, initX, initY, panStartX, panStartY, panStartTx, panStartTy;
@@ -587,7 +556,6 @@ INDEX_HTML = '''
587
 
588
  if(localStorage.getItem('comic_draft_'+sid)) document.getElementById('restore-draft-btn').style.display = 'block';
589
 
590
- // --- HELPERS ---
591
  function showSaveModal(code) { document.getElementById('modal-save-code').textContent = code; document.getElementById('save-modal').style.display = 'flex'; }
592
  function closeModal() { document.getElementById('save-modal').style.display = 'none'; }
593
  function copyModalCode() { navigator.clipboard.writeText(document.getElementById('modal-save-code').textContent).then(() => alert('Code copied!')); }
@@ -602,7 +570,6 @@ INDEX_HTML = '''
602
  });
603
  }
604
 
605
- // --- CORE ACTIONS ---
606
  async function saveComic() {
607
  const state = getCurrentState();
608
  if(!state || state.length === 0) { alert('No comic to save!'); return; }
@@ -678,9 +645,7 @@ INDEX_HTML = '''
678
  pDiv.onclick = (e) => { e.stopPropagation(); selectPanel(pDiv); };
679
  const img = document.createElement('img');
680
  img.src = pan.src.includes('?') ? pan.src : pan.src + `?sid=${sid}`;
681
- img.dataset.zoom = pan.zoom || 100;
682
- img.dataset.translateX = pan.tx || 0;
683
- img.dataset.translateY = pan.ty || 0;
684
  updateImageTransform(img);
685
  img.onmousedown = (e) => startPan(e, img);
686
  pDiv.appendChild(img);
@@ -696,14 +661,9 @@ INDEX_HTML = '''
696
  const pCount = document.getElementById('page-count').value;
697
  if(!f) return alert("Select a video");
698
  sid = genUUID(); localStorage.setItem('comic_sid', sid); currentSaveCode = null;
699
-
700
  document.querySelector('.upload-box').style.display='none';
701
  document.getElementById('loading-view').style.display='flex';
702
-
703
- const fd = new FormData();
704
- fd.append('file', f);
705
- fd.append('target_pages', pCount);
706
-
707
  const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
708
  if(r.ok) interval = setInterval(checkStatus, 2000);
709
  else { alert("Upload failed"); location.reload(); }
@@ -736,35 +696,27 @@ INDEX_HTML = '''
736
  });
737
  }
738
 
739
- // --- INTERACTIVE ELEMENTS ---
740
  function createBubbleHTML(data) {
741
  const b = document.createElement('div');
742
- // Ensure default classes for tail rendering
743
  const type = data.type || 'speech';
744
  b.className = data.classes || `speech-bubble ${type} tail-bottom`;
745
  b.dataset.type = type;
746
-
747
  b.style.left = data.left; b.style.top = data.top;
748
  if(data.width) b.style.width = data.width; if(data.height) b.style.height = data.height;
749
  if(data.font) b.style.fontFamily = data.font;
750
-
751
- if(data.colors) {
752
- b.style.setProperty('--bubble-fill-color', data.colors.fill || '#4ECDC4');
753
- b.style.setProperty('--bubble-text-color', data.colors.text || '#ffffff');
754
- }
755
  if(data.tailPos) b.style.setProperty('--tail-pos', data.tailPos);
756
 
757
  const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || ''; b.appendChild(textSpan);
758
 
759
- // THOUGHT BUBBLE DOTS
760
- if(type === 'thought') {
761
- for(let i=1; i<=2; i++){
762
- const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; b.appendChild(d);
763
- }
764
- }
765
 
766
  ['nw', 'ne', 'sw', 'se'].forEach(dir => { const handle = document.createElement('div'); handle.className = `resize-handle ${dir}`; handle.onmousedown = (e) => startResize(e, dir); b.appendChild(handle); });
767
- b.onmousedown = (e) => { if(e.target.classList.contains('resize-handle')) return; e.stopPropagation(); selectBubble(b); isDragging = true; startX = e.clientX; startY = e.clientY; initX = b.offsetLeft; initY = b.offsetTop; };
 
 
 
 
768
  b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
769
  return b;
770
  }
@@ -778,16 +730,12 @@ INDEX_HTML = '''
778
  textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); } });
779
  }
780
 
781
- // --- GLOBAL MOUSE EVENTS ---
782
  document.addEventListener('mousemove', (e) => {
783
  if(isDragging && selectedBubble) { selectedBubble.style.left = (initX + e.clientX - startX) + 'px'; selectedBubble.style.top = (initY + e.clientY - startY) + 'px'; }
784
  if(isResizing && selectedBubble) { resizeBubble(e); }
785
  if(isPanning && selectedPanel) { panImage(e); }
786
  });
787
- document.addEventListener('mouseup', () => {
788
- if(isDragging || isResizing || isPanning) saveDraft();
789
- isDragging = false; isResizing = false; isPanning = false;
790
- });
791
 
792
  function startResize(e, dir) { e.preventDefault(); e.stopPropagation(); isResizing = true; resizeHandle = dir; const rect = selectedBubble.getBoundingClientRect(); originalWidth = rect.width; originalHeight = rect.height; originalMouseX = e.clientX; originalMouseY = e.clientY; }
793
  function resizeBubble(e) { if (!isResizing || !selectedBubble) return; const dx = e.clientX - originalMouseX; const dy = e.clientY - originalMouseY; if(resizeHandle.includes('e')) selectedBubble.style.width = (originalWidth + dx)+'px'; if(resizeHandle.includes('s')) selectedBubble.style.height = (originalHeight + dy)+'px'; }
@@ -796,14 +744,11 @@ INDEX_HTML = '''
796
  if(selectedBubble) selectedBubble.classList.remove('selected');
797
  if(selectedPanel) { selectedPanel.classList.remove('selected'); selectedPanel = null; }
798
  selectedBubble = el; el.classList.add('selected');
799
-
800
- // Enable controls
801
  document.getElementById('bubble-type-select').disabled = false;
802
  document.getElementById('font-select').disabled = false;
803
  document.getElementById('bubble-text-color').disabled = false;
804
  document.getElementById('bubble-fill-color').disabled = false;
805
  document.getElementById('tail-controls').style.display = 'block';
806
-
807
  document.getElementById('bubble-type-select').value = el.dataset.type || 'speech';
808
  }
809
 
@@ -811,12 +756,9 @@ INDEX_HTML = '''
811
  if(selectedPanel) selectedPanel.classList.remove('selected');
812
  if(selectedBubble) { selectedBubble.classList.remove('selected'); selectedBubble = null; }
813
  selectedPanel = el; el.classList.add('selected');
814
-
815
- // Enable Zoom
816
  document.getElementById('zoom-slider').disabled = false;
817
  const img = el.querySelector('img');
818
  document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
819
-
820
  document.getElementById('bubble-type-select').disabled = true;
821
  document.getElementById('font-select').disabled = true;
822
  document.getElementById('tail-controls').style.display = 'none';
@@ -836,24 +778,13 @@ INDEX_HTML = '''
836
  function changeBubbleType(type) {
837
  if(!selectedBubble) return;
838
  selectedBubble.dataset.type = type;
839
- // Remove old tail classes
840
- selectedBubble.classList.remove('speech', 'thought', 'reaction', 'narration');
841
- selectedBubble.classList.add(type);
842
-
843
- // Thought bubbles need dots
844
  selectedBubble.querySelectorAll('.thought-dot').forEach(d=>d.remove());
845
- if(type === 'thought') {
846
- for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; selectedBubble.appendChild(d); }
847
- }
848
- saveDraft();
849
- }
850
-
851
- function changeFont(font) {
852
- if(!selectedBubble) return;
853
- selectedBubble.style.fontFamily = font;
854
  saveDraft();
855
  }
856
 
 
857
  function rotateTail() {
858
  if(!selectedBubble) return;
859
  const type = selectedBubble.dataset.type;
@@ -866,160 +797,23 @@ INDEX_HTML = '''
866
  }
867
  saveDraft();
868
  }
 
869
 
870
- function slideTail(v) {
871
- if(selectedBubble) {
872
- selectedBubble.style.setProperty('--tail-pos', v+'%');
873
- saveDraft();
874
- }
875
- }
876
 
877
- document.getElementById('bubble-text-color').addEventListener('input', (e) => {
878
- if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(); }
879
- });
880
- document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
881
- if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(); }
882
- });
883
 
884
- // --- PAN & ZOOM ---
885
- function handleZoom(el) {
886
- if(!selectedPanel) return;
887
- const img = selectedPanel.querySelector('img');
888
- img.dataset.zoom = el.value;
889
- updateImageTransform(img);
890
- saveDraft();
891
- }
892
 
893
- function startPan(e, img) {
894
- if(parseFloat(img.dataset.zoom || 100) <= 100) return;
895
- e.preventDefault();
896
- isPanning = true;
897
- selectedPanel = img.closest('.panel');
898
- panStartX = e.clientX;
899
- panStartY = e.clientY;
900
- panStartTx = parseFloat(img.dataset.translateX || 0);
901
- panStartTy = parseFloat(img.dataset.translateY || 0);
902
- img.classList.add('panning');
903
- }
904
-
905
- function panImage(e) {
906
- if(!isPanning || !selectedPanel) return;
907
- const img = selectedPanel.querySelector('img');
908
- img.dataset.translateX = panStartTx + (e.clientX - panStartX);
909
- img.dataset.translateY = panStartTy + (e.clientY - panStartY);
910
- updateImageTransform(img);
911
- }
912
-
913
- function updateImageTransform(img) {
914
- const z = (img.dataset.zoom || 100) / 100;
915
- const x = img.dataset.translateX || 0;
916
- const y = img.dataset.translateY || 0;
917
- img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`;
918
- img.classList.toggle('pannable', z > 1);
919
- }
920
-
921
- function resetPanelTransform() {
922
- if(!selectedPanel) return alert("Select a panel");
923
- const img = selectedPanel.querySelector('img');
924
- img.dataset.zoom = 100; img.dataset.translateX = 0; img.dataset.translateY = 0;
925
- document.getElementById('zoom-slider').value = 100;
926
- updateImageTransform(img);
927
- saveDraft();
928
- }
929
-
930
- function replacePanelImage() {
931
- if(!selectedPanel) return alert("Select a panel");
932
- const inp = document.getElementById('image-uploader');
933
- inp.onchange = async (e) => {
934
- const fd = new FormData(); fd.append('image', e.target.files[0]);
935
- const img = selectedPanel.querySelector('img');
936
- const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd});
937
- const d = await r.json();
938
- if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(); }
939
- inp.value = '';
940
- };
941
- inp.click();
942
- }
943
-
944
- async function adjustFrame(dir) {
945
- if(isProcessing) return;
946
- if(!selectedPanel) return alert("Select a panel");
947
- const img = selectedPanel.querySelector('img');
948
- let fname = img.src.split('/').pop().split('?')[0];
949
-
950
- setProcessing(true);
951
- img.style.opacity = '0.5';
952
-
953
- try {
954
- const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) });
955
- const d = await r.json();
956
- if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; }
957
- else { alert('Error: ' + d.message); }
958
- } catch(e) { console.error(e); }
959
-
960
- img.style.opacity = '1';
961
- setProcessing(false);
962
- saveDraft();
963
- }
964
-
965
- async function gotoTimestamp() {
966
- if(isProcessing) return;
967
- if(!selectedPanel) return alert("Select a panel");
968
- let v = document.getElementById('timestamp-input').value.trim();
969
- if(!v) return;
970
- if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); }
971
- if(isNaN(v)) return alert("Invalid time");
972
-
973
- const img = selectedPanel.querySelector('img');
974
- let fname = img.src.split('/').pop().split('?')[0];
975
-
976
- setProcessing(true);
977
- img.style.opacity = '0.5';
978
-
979
- try {
980
- const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) });
981
- const d = await r.json();
982
- if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; }
983
- else { alert('Error: ' + d.message); }
984
- } catch(e) { console.error(e); }
985
-
986
- img.style.opacity = '1';
987
- setProcessing(false);
988
- saveDraft();
989
- }
990
-
991
- async function exportComic() {
992
- const pgs = document.querySelectorAll('.comic-page');
993
- if(pgs.length === 0) return alert("No pages found");
994
- alert(`Exporting ${pgs.length} page(s)...`);
995
-
996
- // Temporarily fix bubbles for export
997
- const bubbles = document.querySelectorAll('.speech-bubble');
998
- bubbles.forEach(b => {
999
- const rect = b.getBoundingClientRect();
1000
- b.style.width = rect.width + 'px';
1001
- b.style.height = rect.height + 'px';
1002
- });
1003
-
1004
- for(let i = 0; i < pgs.length; i++) {
1005
- try {
1006
- const u = await htmlToImage.toPng(pgs[i], {pixelRatio: 2});
1007
- const a = document.createElement('a'); a.href = u; a.download = `Comic-Page-${i+1}.png`; a.click();
1008
- }
1009
- catch(err) { console.error(err); alert(`Failed to export page ${i+1}`); }
1010
- }
1011
-
1012
- // Reset
1013
- bubbles.forEach(b => { b.style.width = ''; b.style.height = ''; });
1014
- }
1015
-
1016
- function goBackToUpload() {
1017
- if(confirm('Go home? Unsaved changes will be lost.')) {
1018
- document.getElementById('editor-container').style.display = 'none';
1019
- document.getElementById('upload-container').style.display = 'flex';
1020
- document.getElementById('loading-view').style.display = 'none';
1021
- }
1022
- }
1023
  </script>
1024
  </body>
1025
  </html>
 
1
+ import spaces # <--- CRITICAL: MUST BE FIRST
2
  import os
3
  import time
4
  import threading
 
24
  return True
25
 
26
  # ======================================================
27
+ # 🧱 DATA CLASSES
28
  # ======================================================
29
  def bubble(dialog="", bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, emotion='normal', type='speech'):
30
  return {
 
68
  return code
69
 
70
  # ======================================================
71
+ # 🧠 GLOBAL GPU FUNCTIONS
72
  # ======================================================
73
 
74
  @spaces.GPU(duration=300)
75
  def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
76
  print(f"🚀 GPU Task Started: {video_path} | Pages: {target_pages}")
77
 
 
78
  import cv2
79
  import srt
80
  import numpy as np
 
105
  with open(user_srt, 'r', encoding='utf-8') as f:
106
  all_subs = list(srt.parse(f.read()))
107
 
108
+ # 3. Smart Keyframe Selection
 
109
  valid_subs = [s for s in all_subs if s.content.strip()]
110
  raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
111
 
 
113
  panels_per_page = 4
114
  total_panels_needed = target_pages * panels_per_page
115
 
 
 
116
  selected_moments = []
117
  if not raw_moments:
 
118
  times = np.linspace(1, duration-1, total_panels_needed)
119
  for t in times:
120
  selected_moments.append({'text': '', 'start': t, 'end': t+1})
121
  elif len(raw_moments) <= total_panels_needed:
122
  selected_moments = raw_moments
123
  else:
 
124
  indices = np.linspace(0, len(raw_moments) - 1, total_panels_needed, dtype=int)
125
  selected_moments = [raw_moments[i] for i in indices]
126
 
 
140
  fname = f"frame_{count:04d}.png"
141
  p = os.path.join(frames_dir, fname)
142
  cv2.imwrite(p, frame)
143
+ os.sync()
144
 
145
+ frame_metadata[fname] = {'dialogue': moment['text'], 'time': mid}
 
 
 
146
  frame_files_ordered.append(fname)
147
  count += 1
148
  cap.release()
 
156
  se = SimpleColorEnhancer()
157
  qe = QualityColorEnhancer()
158
 
 
159
  for f in frame_files_ordered:
160
  p = os.path.join(frames_dir, f)
161
  try: se.enhance_single(p, p)
 
164
  except: pass
165
 
166
  # 6. Bubble Placement
 
167
  bubbles_list = []
 
168
  for f in frame_files_ordered:
169
  p = os.path.join(frames_dir, f)
170
  dialogue = frame_metadata.get(f, {}).get('dialogue', '')
171
 
 
172
  b_type = 'speech'
173
  if '(' in dialogue and ')' in dialogue: b_type = 'narration'
174
  elif '!' in dialogue and dialogue.isupper(): b_type = 'reaction'
 
178
  faces = face_detector.detect_faces(p)
179
  lip = face_detector.get_lip_position(p, faces[0]) if faces else (-1, -1)
180
  bx, by = ai_bubble_placer.place_bubble_ai(p, lip)
181
+ b = bubble(dialog=dialogue, bubble_offset_x=bx, bubble_offset_y=by, lip_x=lip[0], lip_y=lip[1], type=b_type)
 
 
 
 
 
 
 
 
182
  bubbles_list.append(b)
183
  except:
184
  bubbles_list.append(bubble(dialog=dialogue, type=b_type))
185
 
186
+ # 7. Final Layout
 
187
  pages = []
 
 
188
  for i in range(target_pages):
189
  start_idx = i * 4
190
  end_idx = start_idx + 4
 
191
  p_frames = frame_files_ordered[start_idx:end_idx]
192
  p_bubbles = bubbles_list[start_idx:end_idx]
 
193
  if p_frames:
194
  pg_panels = [panel(image=f) for f in p_frames]
195
  pages.append(Page(panels=pg_panels, bubbles=p_bubbles))
196
 
 
197
  result = []
198
  for pg in pages:
199
  p_data = [p if isinstance(p, dict) else p.__dict__ for p in pg.panels]
 
298
  json.dump({'message': msg, 'progress': prog}, f)
299
 
300
  # ======================================================
301
+ # 🌐 ROUTES & HTML (FIXED SELECTION LOGIC)
302
  # ======================================================
303
 
304
  INDEX_HTML = '''
 
340
  .loader { width: 120px; height: 20px; background: radial-gradient(circle 10px, #e67e22 100%, transparent 0); background-size: 20px 20px; animation: ball 1s infinite linear; margin: 20px auto; }
341
  @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
342
 
 
343
  .comic-wrapper { max-width: 1000px; margin: 0 auto; }
344
  .page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
345
  .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
 
351
  .panel img.pannable { cursor: grab; }
352
  .panel img.panning { cursor: grabbing; }
353
 
354
+ /* BUBBLES - FIX: pointer-events none on text to fix selection */
355
  .speech-bubble {
356
  position: absolute; display: flex; justify-content: center; align-items: center;
357
  width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
 
359
  font-size: 13px; text-align: center; overflow: visible;
360
  --tail-pos: 50%;
361
  }
362
+ .bubble-text { padding: 0.8em; word-wrap: break-word; position: relative; z-index: 5; pointer-events: none; user-select: none; }
363
  .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
364
  .speech-bubble textarea { position: absolute; top:0; left:0; width:100%; height:100%; box-sizing:border-box; border:1px solid #4CAF50; background:rgba(255,255,255,0.95); text-align:center; padding:8px; z-index:102; resize:none; font: inherit; }
365
 
366
+ /* BUBBLE TYPES & TAILS */
367
  .speech-bubble.speech {
368
  --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
369
  background: var(--bubble-fill-color, #4ECDC4);
 
377
  -webkit-mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
378
  mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
379
  }
 
 
380
 
381
+ .speech-bubble.speech.tail-bottom:before { top: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
382
  .speech-bubble.speech.tail-top { border-radius: min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) var(--r) var(--r) / var(--r); }
383
  .speech-bubble.speech.tail-top:before { bottom: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: scaleY(-1); }
 
384
  .speech-bubble.speech.tail-left { border-radius: var(--r); }
385
  .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; }
 
386
  .speech-bubble.speech.tail-right { border-radius: var(--r); }
387
  .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; }
388
 
 
547
  localStorage.setItem('comic_sid', sid);
548
 
549
  let currentSaveCode = null;
550
+ let isProcessing = false;
551
  let interval, selectedBubble = null, selectedPanel = null;
552
  let isDragging = false, isResizing = false, isPanning = false;
553
  let startX, startY, initX, initY, panStartX, panStartY, panStartTx, panStartTy;
 
556
 
557
  if(localStorage.getItem('comic_draft_'+sid)) document.getElementById('restore-draft-btn').style.display = 'block';
558
 
 
559
  function showSaveModal(code) { document.getElementById('modal-save-code').textContent = code; document.getElementById('save-modal').style.display = 'flex'; }
560
  function closeModal() { document.getElementById('save-modal').style.display = 'none'; }
561
  function copyModalCode() { navigator.clipboard.writeText(document.getElementById('modal-save-code').textContent).then(() => alert('Code copied!')); }
 
570
  });
571
  }
572
 
 
573
  async function saveComic() {
574
  const state = getCurrentState();
575
  if(!state || state.length === 0) { alert('No comic to save!'); return; }
 
645
  pDiv.onclick = (e) => { e.stopPropagation(); selectPanel(pDiv); };
646
  const img = document.createElement('img');
647
  img.src = pan.src.includes('?') ? pan.src : pan.src + `?sid=${sid}`;
648
+ img.dataset.zoom = pan.zoom || 100; img.dataset.translateX = pan.tx || 0; img.dataset.translateY = pan.ty || 0;
 
 
649
  updateImageTransform(img);
650
  img.onmousedown = (e) => startPan(e, img);
651
  pDiv.appendChild(img);
 
661
  const pCount = document.getElementById('page-count').value;
662
  if(!f) return alert("Select a video");
663
  sid = genUUID(); localStorage.setItem('comic_sid', sid); currentSaveCode = null;
 
664
  document.querySelector('.upload-box').style.display='none';
665
  document.getElementById('loading-view').style.display='flex';
666
+ const fd = new FormData(); fd.append('file', f); fd.append('target_pages', pCount);
 
 
 
 
667
  const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
668
  if(r.ok) interval = setInterval(checkStatus, 2000);
669
  else { alert("Upload failed"); location.reload(); }
 
696
  });
697
  }
698
 
 
699
  function createBubbleHTML(data) {
700
  const b = document.createElement('div');
 
701
  const type = data.type || 'speech';
702
  b.className = data.classes || `speech-bubble ${type} tail-bottom`;
703
  b.dataset.type = type;
 
704
  b.style.left = data.left; b.style.top = data.top;
705
  if(data.width) b.style.width = data.width; if(data.height) b.style.height = data.height;
706
  if(data.font) b.style.fontFamily = data.font;
707
+ if(data.colors) { b.style.setProperty('--bubble-fill-color', data.colors.fill || '#4ECDC4'); b.style.setProperty('--bubble-text-color', data.colors.text || '#ffffff'); }
 
 
 
 
708
  if(data.tailPos) b.style.setProperty('--tail-pos', data.tailPos);
709
 
710
  const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || ''; b.appendChild(textSpan);
711
 
712
+ if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; b.appendChild(d); } }
 
 
 
 
 
713
 
714
  ['nw', 'ne', 'sw', 'se'].forEach(dir => { const handle = document.createElement('div'); handle.className = `resize-handle ${dir}`; handle.onmousedown = (e) => startResize(e, dir); b.appendChild(handle); });
715
+ b.onmousedown = (e) => {
716
+ if(e.target.classList.contains('resize-handle')) return;
717
+ e.stopPropagation(); selectBubble(b); isDragging = true; startX = e.clientX; startY = e.clientY; initX = b.offsetLeft; initY = b.offsetTop;
718
+ };
719
+ b.onclick = (e) => { e.stopPropagation(); }; // Critical Fix for selection
720
  b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
721
  return b;
722
  }
 
730
  textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); } });
731
  }
732
 
 
733
  document.addEventListener('mousemove', (e) => {
734
  if(isDragging && selectedBubble) { selectedBubble.style.left = (initX + e.clientX - startX) + 'px'; selectedBubble.style.top = (initY + e.clientY - startY) + 'px'; }
735
  if(isResizing && selectedBubble) { resizeBubble(e); }
736
  if(isPanning && selectedPanel) { panImage(e); }
737
  });
738
+ document.addEventListener('mouseup', () => { if(isDragging || isResizing || isPanning) saveDraft(); isDragging = false; isResizing = false; isPanning = false; });
 
 
 
739
 
740
  function startResize(e, dir) { e.preventDefault(); e.stopPropagation(); isResizing = true; resizeHandle = dir; const rect = selectedBubble.getBoundingClientRect(); originalWidth = rect.width; originalHeight = rect.height; originalMouseX = e.clientX; originalMouseY = e.clientY; }
741
  function resizeBubble(e) { if (!isResizing || !selectedBubble) return; const dx = e.clientX - originalMouseX; const dy = e.clientY - originalMouseY; if(resizeHandle.includes('e')) selectedBubble.style.width = (originalWidth + dx)+'px'; if(resizeHandle.includes('s')) selectedBubble.style.height = (originalHeight + dy)+'px'; }
 
744
  if(selectedBubble) selectedBubble.classList.remove('selected');
745
  if(selectedPanel) { selectedPanel.classList.remove('selected'); selectedPanel = null; }
746
  selectedBubble = el; el.classList.add('selected');
 
 
747
  document.getElementById('bubble-type-select').disabled = false;
748
  document.getElementById('font-select').disabled = false;
749
  document.getElementById('bubble-text-color').disabled = false;
750
  document.getElementById('bubble-fill-color').disabled = false;
751
  document.getElementById('tail-controls').style.display = 'block';
 
752
  document.getElementById('bubble-type-select').value = el.dataset.type || 'speech';
753
  }
754
 
 
756
  if(selectedPanel) selectedPanel.classList.remove('selected');
757
  if(selectedBubble) { selectedBubble.classList.remove('selected'); selectedBubble = null; }
758
  selectedPanel = el; el.classList.add('selected');
 
 
759
  document.getElementById('zoom-slider').disabled = false;
760
  const img = el.querySelector('img');
761
  document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
 
762
  document.getElementById('bubble-type-select').disabled = true;
763
  document.getElementById('font-select').disabled = true;
764
  document.getElementById('tail-controls').style.display = 'none';
 
778
  function changeBubbleType(type) {
779
  if(!selectedBubble) return;
780
  selectedBubble.dataset.type = type;
781
+ selectedBubble.className = 'speech-bubble ' + type + ' selected';
 
 
 
 
782
  selectedBubble.querySelectorAll('.thought-dot').forEach(d=>d.remove());
783
+ if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; selectedBubble.appendChild(d); } }
 
 
 
 
 
 
 
 
784
  saveDraft();
785
  }
786
 
787
+ function changeFont(font) { if(!selectedBubble) return; selectedBubble.style.fontFamily = font; saveDraft(); }
788
  function rotateTail() {
789
  if(!selectedBubble) return;
790
  const type = selectedBubble.dataset.type;
 
797
  }
798
  saveDraft();
799
  }
800
+ function slideTail(v) { if(selectedBubble) { selectedBubble.style.setProperty('--tail-pos', v+'%'); saveDraft(); } }
801
 
802
+ document.getElementById('bubble-text-color').addEventListener('input', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(); } });
803
+ document.getElementById('bubble-fill-color').addEventListener('input', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(); } });
 
 
 
 
804
 
805
+ function handleZoom(el) { if(!selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.zoom = el.value; updateImageTransform(img); saveDraft(); }
806
+ 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'); }
807
+ 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); }
808
+ 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); }
809
+ 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(); }
 
810
 
811
+ 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(); } inp.value = ''; }; inp.click(); }
812
+ 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(); }
813
+ 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(); }
814
+ async function exportComic() { const pgs = document.querySelectorAll('.comic-page'); if(pgs.length === 0) return alert("No pages found"); alert(`Exporting ${pgs.length} page(s)...`); const bubbles = document.querySelectorAll('.speech-bubble'); bubbles.forEach(b => { const rect = b.getBoundingClientRect(); b.style.width = rect.width + 'px'; b.style.height = rect.height + 'px'; }); for(let i = 0; i < pgs.length; i++) { try { const u = await htmlToImage.toPng(pgs[i], {pixelRatio: 2}); const a = document.createElement('a'); a.href = u; a.download = `Comic-Page-${i+1}.png`; a.click(); } catch(err) { console.error(err); alert(`Failed to export page ${i+1}`); } } bubbles.forEach(b => { b.style.width = ''; b.style.height = ''; }); }
 
 
 
 
815
 
816
+ 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'; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
817
  </script>
818
  </body>
819
  </html>