tester343 commited on
Commit
5ae4e29
·
verified ·
1 Parent(s): d38c031

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +302 -1014
app_enhanced.py CHANGED
@@ -12,101 +12,211 @@ from concurrent.futures import ThreadPoolExecutor
12
  from flask import Flask, render_template, request, jsonify, send_from_directory, send_file
13
 
14
  # ==========================================
15
- # 🚀 ZEROGPU CRITICAL SETUP
16
  # ==========================================
17
- import spaces # Must be imported at module level
18
-
19
- # This dummy function guarantees HF detects the GPU requirement
20
- @spaces.GPU
21
- def zero_gpu_warmup():
22
- print("✅ ZeroGPU Active")
23
-
24
- # ==========================================
25
- # ⚙️ CONFIG & DEPENDENCIES
26
- # ==========================================
27
- logging.basicConfig(level=logging.INFO)
28
- logger = logging.getLogger(__name__)
29
-
30
- # --- Core Libraries ---
31
- try:
32
  import cv2
33
- import numpy as np
34
- from PIL import Image
35
  import srt
36
- except ImportError as e:
37
- print(f"❌ CRITICAL ERROR: Missing python library. {e}")
38
- cv2 = None
39
- np = None
40
- Image = None
41
- srt = None
42
-
43
- # --- Backend Imports with Fallbacks ---
44
- def dummy_func(*args, **kwargs):
45
- return 0, 0, None, None
46
-
47
- try:
48
  from backend.keyframes.keyframes import black_bar_crop
49
- print("✅ Black bar cropping module loaded.")
50
- except Exception as e:
51
- print(f"⚠️ Could not load black_bar_crop: {e}. Cropping will be SKIPPED.")
52
- black_bar_crop = dummy_func
53
-
54
- try:
55
  from backend.simple_color_enhancer import SimpleColorEnhancer
56
- print("✅ SimpleColorEnhancer loaded.")
57
- except Exception as e:
58
- print(f"⚠️ Could not load SimpleColorEnhancer: {e}.")
59
- class SimpleColorEnhancer:
60
- def enhance_batch(self, *args, **kwargs): pass
61
- def enhance_single(self, *args, **kwargs): pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
- try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  from backend.quality_color_enhancer import QualityColorEnhancer
65
- print("✅ QualityColorEnhancer loaded.")
66
- except Exception as e:
67
- print(f"⚠️ Could not load QualityColorEnhancer: {e}.")
68
- class QualityColorEnhancer:
69
- def batch_enhance(self, *args, **kwargs): pass
70
- def enhance_single(self, *args, **kwargs): pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- try:
73
- from backend.class_def import bubble, panel, Page
74
- print("✅ Core class definitions loaded.")
75
- except Exception as e:
76
- print(f"⚠️ Using fallback class definitions.")
77
- def bubble(dialog="", bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, emotion='normal'):
78
- return {
79
- 'dialog': dialog,
80
- 'bubble_offset_x': bubble_offset_x,
81
- 'bubble_offset_y': bubble_offset_y,
82
- 'lip_x': lip_x,
83
- 'lip_y': lip_y,
84
- 'emotion': emotion
85
- }
86
- def panel(image=""):
87
- return {'image': image}
88
- class Page:
89
- def __init__(self, panels, bubbles):
90
- self.panels = panels
91
- self.bubbles = bubbles
 
 
 
 
 
92
 
93
- try:
94
- from backend.ai_enhanced_core import image_processor, comic_styler, face_detector, layout_optimizer
95
- from backend.ai_bubble_placement import ai_bubble_placer
96
- from backend.subtitles.subs_real import get_real_subtitles
97
- from backend.keyframes.keyframes_simple import generate_keyframes_simple
98
- print("✅ Core utility modules loaded.")
99
- except Exception as e:
100
- print(f"⚠️ Could not load utility modules: {e}")
101
- def get_real_subtitles(v): pass
102
- def generate_keyframes_simple(*args, **kwargs): pass
103
- class DummyDetector:
104
- def detect_faces(self, p): return []
105
- def get_lip_position(self, p, f): return -1, -1
106
- face_detector = DummyDetector()
107
- class DummyPlacer:
108
- def place_bubble_ai(self, p, l): return 50, 20
109
- ai_bubble_placer = DummyPlacer()
110
 
111
  # --- FLASK APP SETUP ---
112
  app = Flask(__name__)
@@ -117,14 +227,65 @@ os.makedirs(BASE_USER_DIR, exist_ok=True)
117
  os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
118
 
119
  def generate_save_code(length=8):
120
- """Generate a unique save code"""
121
  chars = string.ascii_uppercase + string.digits
122
  while True:
123
  code = ''.join(random.choices(chars, k=length))
124
  if not os.path.exists(os.path.join(SAVED_COMICS_DIR, code)):
125
  return code
126
 
127
- # --- FULL HTML INTERFACE ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  INDEX_HTML = '''
129
  <!DOCTYPE html>
130
  <html lang="en">
@@ -135,997 +296,124 @@ INDEX_HTML = '''
135
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
136
  <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@700&family=Gloria+Hallelujah&family=Lato&display=swap" rel="stylesheet">
137
  <style>
138
- /* --- GLOBAL STYLES --- */
139
  * { box-sizing: border-box; }
140
  body { background-color: #fdf6e3; font-family: 'Lato', sans-serif; color: #3d3d3d; margin: 0; min-height: 100vh; }
141
  #upload-container { display: flex; flex-direction:column; justify-content: center; align-items: center; min-height: 100vh; width: 100%; padding: 20px; }
142
  .upload-box { max-width: 500px; width: 100%; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); text-align: center; }
143
  #editor-container { display: none; padding: 20px; width: 100%; box-sizing: border-box; padding-bottom: 100px; }
144
  h1 { color: #2c3e50; margin-bottom: 20px; font-weight: 600; }
145
- h3 { color: #34495e; margin: 20px 0 10px 0; font-size: 16px; }
146
- .file-input { display: none; }
147
- .file-label { display: block; padding: 15px; background: #2c3e50; color: white; border-radius: 8px; cursor: pointer; font-weight: bold; margin-bottom: 10px; transition:0.2s; }
148
- .file-label:hover { background: #34495e; }
149
  .submit-btn { width: 100%; padding: 15px; background: #e67e22; color: white; border: none; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; transition: 0.2s; }
150
- .submit-btn:hover { background: #d35400; }
151
- .restore-btn { margin-top: 10px; background: #27ae60; color: white; padding: 12px; width: 100%; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
152
- .restore-btn:hover { background: #219a52; }
153
- .load-section { margin-top: 30px; padding-top: 20px; border-top: 2px solid #eee; }
154
- .load-input-group { display: flex; gap: 10px; margin-top: 10px; }
155
- .load-input-group input { flex: 1; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; text-transform: uppercase; letter-spacing: 2px; text-align: center; }
156
- .load-input-group button { padding: 12px 20px; background: #3498db; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
157
- .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; }
158
- @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
159
- .comic-wrapper { max-width: 1000px; margin: 0 auto; }
160
- .page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
161
- .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
162
- .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; }
163
  .comic-grid { display: grid; grid-template-columns: 285px 285px; grid-template-rows: 185px 185px; gap: 10px; width: 100%; height: 100%; }
164
  .panel { position: relative; overflow: hidden; border: 2px solid #000; background: #eee; cursor: pointer; }
165
- .panel.selected { outline: 3px solid #2196F3; outline-offset: -3px; }
166
- .panel img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.1s ease-out; transform-origin: center center; }
167
- .panel img.pannable { cursor: grab; }
168
- .panel img.panning { cursor: grabbing; }
169
- .speech-bubble { position: absolute; display: flex; justify-content: center; align-items: center; width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box; z-index: 10; cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold; font-size: 13px; text-align: center; overflow: visible;}
170
- .bubble-text { padding: 0.8em; word-wrap: break-word; position: relative; z-index: 5; }
171
- .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
172
- .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; }
173
- .speech-bubble.speech { --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em; --c: var(--bubble-fill-color, #4ECDC4); background: var(--c); color: var(--bubble-text-color, #fff); padding: 1em; position: absolute; border-radius: var(--r) var(--r) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) / var(--r); }
174
- .speech-bubble.speech:before { content: ""; position: absolute; width: var(--b); height: var(--h); background: inherit; border-bottom-left-radius: 100%; pointer-events: none; z-index: 1; -webkit-mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%); mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%); }
175
- .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))); }
176
- .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); }
177
- .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); }
178
- .speech-bubble.speech.tail-left { border-radius: var(--r); }
179
- .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; }
180
- .speech-bubble.speech.tail-right { border-radius: var(--r); }
181
- .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; }
182
- .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
183
- .speech-bubble.thought::after { display:none; }
184
- .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
185
- .thought-dot-1 { width: 20px; height: 20px; bottom: -20px; left: 15px; }
186
- .thought-dot-2 { width: 12px; height: 12px; bottom: -32px; left: 5px; }
187
- .speech-bubble.flipped .thought-dot-1 { left: auto; right: 15px; }
188
- .speech-bubble.flipped .thought-dot-2 { left: auto; right: 5px; }
189
- .speech-bubble.flipped-vertical .thought-dot-1 { bottom: auto; top: -20px; }
190
- .speech-bubble.flipped-vertical .thought-dot-2 { bottom: auto; top: -32px; }
191
- .speech-bubble.reaction { background: #FFD700; border: 3px solid #E53935; color: #D32F2F; font-weight: 900; text-transform: uppercase; width: 180px; 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%); }
192
- .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
193
- .speech-bubble.idea { background: linear-gradient(180deg,#FFFDD0 0%, #FFF8B5 100%); border: 2px solid #FFA500; color: #6a4b00; border-radius: 40% 60% 40% 60% / 60% 40% 60% 40%; }
194
- .resize-handle { position: absolute; width: 10px; height: 10px; background: #2196F3; border: 1px solid white; border-radius: 50%; display: none; z-index: 11; }
195
- .speech-bubble.selected .resize-handle { display: block; }
196
- .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
197
- .resize-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
198
- .resize-handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
199
- .resize-handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
200
- .edit-controls { position: fixed; bottom: 20px; right: 20px; width: 260px; background: rgba(44, 62, 80, 0.95); color: white; padding: 15px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); z-index: 900; font-size: 13px; max-height: 90vh; overflow-y: auto; }
201
- .edit-controls h4 { margin: 0 0 10px 0; color: #4ECDC4; text-align: center; }
202
- .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
203
- .control-group label { font-size: 11px; font-weight: bold; display: block; margin-bottom: 3px; }
204
- button, input, select { width: 100%; margin-top: 5px; padding: 6px; border-radius: 4px; border: 1px solid #ddd; cursor: pointer; font-weight: bold; font-size: 12px; }
205
- button:hover { background: #f5f5f5; }
206
- .button-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
207
- .color-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
208
- .color-grid div { text-align: center; }
209
- .color-grid input[type="color"] { height: 30px; padding: 2px; }
210
- .slider-container { display: flex; align-items: center; gap: 5px; margin-top: 5px; }
211
- .slider-container label { min-width: 40px; font-size: 11px; }
212
- .timestamp-controls { display: grid; grid-template-columns: 1fr auto; gap: 5px; }
213
- .timestamp-controls input { color: #333; font-weight: normal; }
214
- .action-btn { background: #4CAF50; color: white; }
215
- .reset-btn { background: #e74c3c; color: white; }
216
- .secondary-btn { background: #f39c12; color: white; }
217
- .export-btn { background: #2196F3; color: white; }
218
- .save-btn { background: #9b59b6; color: white; }
219
- .save-code-display { background: #2ecc71; color: white; padding: 15px; border-radius: 8px; text-align: center; margin-top: 10px; display: none; }
220
- .save-code-display .code { font-size: 24px; font-weight: bold; letter-spacing: 3px; background: white; color: #2ecc71; padding: 10px 20px; border-radius: 4px; display: inline-block; margin: 10px 0; font-family: monospace; }
221
- .save-code-display button { background: white; color: #2ecc71; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; font-weight: bold; margin-top: 5px; }
222
- .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: none; justify-content: center; align-items: center; z-index: 9999; }
223
- .modal-content { background: white; padding: 30px; border-radius: 12px; max-width: 400px; width: 90%; text-align: center; }
224
- .modal-content h2 { color: #2ecc71; margin-bottom: 20px; }
225
- .modal-content .code { font-size: 32px; font-weight: bold; letter-spacing: 4px; background: #f0f0f0; padding: 15px 25px; border-radius: 8px; display: inline-block; margin: 15px 0; font-family: monospace; user-select: all; }
226
- .modal-content p { color: #666; margin: 10px 0; }
227
- .modal-content button { background: #3498db; color: white; border: none; padding: 12px 30px; border-radius: 8px; cursor: pointer; font-weight: bold; font-size: 14px; margin: 5px; }
228
- .modal-content button.close-btn { background: #95a5a6; }
229
  </style>
230
  </head>
231
  <body>
232
- <!-- UPLOAD SCREEN -->
233
  <div id="upload-container">
234
  <div class="upload-box">
235
  <h1>🎬 Comic Generator</h1>
236
- <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fn').innerText=this.files[0].name">
237
  <label for="file-upload" class="file-label">📁 Choose Video File</label>
238
- <span id="fn" style="margin-bottom:10px; display:block; color:#666;">No file selected</span>
239
  <button class="submit-btn" onclick="upload()">🚀 Generate Comic</button>
240
- <button id="restore-draft-btn" class="restore-btn" style="display:none; margin-top:15px;" onclick="restoreDraft()">📂 Restore Unsaved Draft</button>
241
- <div class="load-section">
242
- <h3>📥 Load Saved Comic</h3>
243
- <p style="font-size:12px; color:#888; margin-bottom:10px;">Enter your save code to continue editing</p>
244
- <div class="load-input-group">
245
- <input type="text" id="load-code-input" placeholder="SAVE CODE" maxlength="8" style="text-transform:uppercase;">
246
- <button onclick="loadSavedComic()">Load</button>
247
- </div>
248
- </div>
249
- <div class="loading-view" id="loading-view" style="display:none; margin-top:20px;">
250
- <div class="loader" style="margin:0 auto;"></div>
251
- <p id="status-text" style="margin-top:10px;">Starting...</p>
252
- </div>
253
  </div>
254
  </div>
255
-
256
- <!-- EDITOR SCREEN -->
257
  <div id="editor-container">
258
- <div class="comic-wrapper" id="comic-container"></div>
259
- <input type="file" id="image-uploader" style="display: none;" accept="image/*">
260
  <div class="edit-controls">
261
- <h4>✏️ Interactive Editor</h4>
262
- <div class="control-group">
263
- <label>💾 Save & Load:</label>
264
- <button onclick="saveComic()" class="save-btn">💾 Save Comic (Get Code)</button>
265
- <div id="current-save-code" style="display:none; margin-top:8px; padding:8px; background:#2ecc71; border-radius:4px; text-align:center;">
266
- <span style="font-size:11px;">Current Save Code:</span><br>
267
- <span id="display-save-code" style="font-size:18px; font-weight:bold; letter-spacing:2px;"></span>
268
- <button onclick="copyCode()" style="padding:4px 8px; margin-left:5px; font-size:10px;">📋 Copy</button>
269
- </div>
270
- </div>
271
- <div class="control-group">
272
- <label>💬 Bubble Tools:</label>
273
- <select id="bubble-type-select" onchange="changeBubbleType(this.value)" disabled>
274
- <option value="speech">Speech</option>
275
- <option value="thought">Thought</option>
276
- <option value="reaction">Reaction</option>
277
- <option value="narration">Narration</option>
278
- <option value="idea">Idea</option>
279
- </select>
280
- <select id="font-select" onchange="changeFont(this.value)" disabled>
281
- <option value="'Comic Neue', cursive">Comic Neue</option>
282
- <option value="'Bangers', cursive">Bangers</option>
283
- <option value="'Gloria Hallelujah', cursive">Gloria</option>
284
- <option value="'Lato', sans-serif">Lato</option>
285
- </select>
286
- <div class="color-grid">
287
- <div><label>Text</label><input type="color" id="bubble-text-color" value="#ffffff" disabled></div>
288
- <div><label>Fill</label><input type="color" id="bubble-fill-color" value="#4ECDC4" disabled></div>
289
- </div>
290
- <button onclick="addBubble()" class="action-btn">💬 Add Bubble</button>
291
- <button onclick="deleteBubble()" class="reset-btn">🗑️ Delete Bubble</button>
292
- </div>
293
- <div class="control-group" id="tail-controls" style="display:none;">
294
- <label>📐 Tail Adjustment:</label>
295
- <button onclick="rotateTail()" class="secondary-btn">🔄 Rotate Side</button>
296
- <div class="slider-container">
297
- <label>Pos:</label>
298
- <input type="range" id="tail-slider" min="10" max="90" value="50" oninput="slideTail(this.value)">
299
- </div>
300
- </div>
301
- <div class="control-group">
302
- <label>🖼️ Panel Tools:</label>
303
- <button onclick="replacePanelImage()" class="action-btn">🖼️ Replace Image</button>
304
- <div class="button-grid">
305
- <button onclick="adjustFrame('backward')" class="secondary-btn">⬅️ Prev</button>
306
- <button onclick="adjustFrame('forward')" class="action-btn">Next ➡️</button>
307
- </div>
308
- <div class="timestamp-controls">
309
- <input type="text" id="timestamp-input" placeholder="mm:ss or secs">
310
- <button onclick="gotoTimestamp()" class="action-btn">Go</button>
311
- </div>
312
- </div>
313
- <div class="control-group">
314
- <label>🔍 Zoom & Pan:</label>
315
- <div class="button-grid">
316
- <button onclick="resetPanelTransform()" class="secondary-btn">Reset</button>
317
- <input type="range" id="zoom-slider" min="100" max="300" value="100" step="5" disabled oninput="handleZoom(this)">
318
- </div>
319
- </div>
320
- <div class="control-group">
321
- <button onclick="exportComic()" class="export-btn">📥 Export as PNG</button>
322
- <button onclick="goBackToUpload()" class="reset-btn" style="margin-top:10px;">🏠 Back to Home</button>
323
- </div>
324
  </div>
325
  </div>
326
- <div class="modal-overlay" id="save-modal">
327
- <div class="modal-content">
328
- <h2>✅ Comic Saved!</h2>
329
- <p>Your unique save code is:</p>
330
- <div class="code" id="modal-save-code">XXXXXXXX</div>
331
- <p style="font-size:12px;">Write this code down or copy it.<br>Anyone can load this comic using this code.</p>
332
- <button onclick="copyModalCode()">📋 Copy Code</button>
333
- <button class="close-btn" onclick="closeModal()">Close</button>
334
- </div>
335
- </div>
336
-
337
  <script>
338
- function genUUID(){ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,c=>{var r=Math.random()*16|0,v=c=='x'?r:(r&0x3|0x8);return v.toString(16);}); }
339
- let sid = localStorage.getItem('comic_sid') || genUUID();
340
- localStorage.setItem('comic_sid', sid);
341
-
342
- let currentSaveCode = null;
343
- let interval, selectedBubble = null, selectedPanel = null;
344
- let isDragging = false, isResizing = false, isPanning = false;
345
- let startX, startY, initX, initY, initW, initH;
346
- let panStartX, panStartY, panStartTx, panStartTy;
347
- let resizeHandle = '', originalWidth, originalHeight, originalX, originalY, originalMouseX, originalMouseY;
348
- let currentlyEditing = null;
349
-
350
- if(localStorage.getItem('comic_draft_'+sid)) {
351
- document.getElementById('restore-draft-btn').style.display = 'block';
352
- }
353
-
354
- function showSaveModal(code) { document.getElementById('modal-save-code').textContent = code; document.getElementById('save-modal').style.display = 'flex'; }
355
- function closeModal() { document.getElementById('save-modal').style.display = 'none'; }
356
- function copyModalCode() { const code = document.getElementById('modal-save-code').textContent; navigator.clipboard.writeText(code).then(() => { alert('Code copied to clipboard!'); }); }
357
- function copyCode() { if(currentSaveCode) { navigator.clipboard.writeText(currentSaveCode).then(() => { alert('Code copied!'); }); } }
358
-
359
- async function saveComic() {
360
- const state = getCurrentState();
361
- if(!state || state.length === 0) { alert('No comic to save!'); return; }
362
- try {
363
- const response = await fetch(`/save_comic?sid=${sid}`, {
364
- method: 'POST', headers: {'Content-Type': 'application/json'},
365
- body: JSON.stringify({ pages: state, savedAt: new Date().toISOString() })
366
- });
367
- const data = await response.json();
368
- if(data.success) {
369
- currentSaveCode = data.code;
370
- document.getElementById('display-save-code').textContent = data.code;
371
- document.getElementById('current-save-code').style.display = 'block';
372
- showSaveModal(data.code);
373
- saveDraft();
374
- } else { alert('Failed to save: ' + data.message); }
375
- } catch(e) { console.error(e); alert('Error saving comic'); }
376
- }
377
-
378
- async function loadSavedComic() {
379
- const code = document.getElementById('load-code-input').value.trim().toUpperCase();
380
- if(!code || code.length < 4) { alert('Please enter a valid save code'); return; }
381
- try {
382
- const response = await fetch(`/load_comic/${code}`);
383
- const data = await response.json();
384
- if(data.success) {
385
- currentSaveCode = code;
386
- sid = data.originalSid || sid;
387
- localStorage.setItem('comic_sid', sid);
388
- renderFromState(data.pages);
389
- document.getElementById('upload-container').style.display = 'none';
390
- document.getElementById('editor-container').style.display = 'block';
391
- document.getElementById('display-save-code').textContent = code;
392
- document.getElementById('current-save-code').style.display = 'block';
393
- saveDraft();
394
- } else { alert('Could not load comic: ' + data.message); }
395
- } catch(e) { console.error(e); alert('Error loading comic. Check the code and try again.'); }
396
- }
397
-
398
- function restoreDraft() {
399
- const savedData = localStorage.getItem('comic_draft_'+sid);
400
- if(!savedData) { alert("No draft found."); return; }
401
- try {
402
- const state = JSON.parse(savedData);
403
- if(state.saveCode) {
404
- currentSaveCode = state.saveCode;
405
- document.getElementById('display-save-code').textContent = state.saveCode;
406
- document.getElementById('current-save-code').style.display = 'block';
407
- }
408
- renderFromState(state.pages || state);
409
- document.getElementById('upload-container').style.display = 'none';
410
- document.getElementById('editor-container').style.display = 'block';
411
- } catch(e) { console.error(e); alert("Failed to restore draft."); }
412
- }
413
-
414
- function getCurrentState() {
415
- const pages = [];
416
- document.querySelectorAll('.comic-page').forEach(p => {
417
- const panels = [];
418
- p.querySelectorAll('.panel').forEach(pan => {
419
- const img = pan.querySelector('img');
420
- const bubbles = [];
421
- pan.querySelectorAll('.speech-bubble').forEach(b => {
422
- const textEl = b.querySelector('.bubble-text');
423
- bubbles.push({
424
- text: textEl ? textEl.textContent : '',
425
- left: b.style.left, top: b.style.top,
426
- width: b.style.width, height: b.style.height,
427
- classes: b.className, type: b.dataset.type, font: b.style.fontFamily,
428
- tailPos: b.style.getPropertyValue('--tail-pos'),
429
- colors: { fill: b.style.getPropertyValue('--bubble-fill-color'), text: b.style.getPropertyValue('--bubble-text-color') }
430
- });
431
- });
432
- panels.push({ src: img.src, zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY, bubbles: bubbles });
433
- });
434
- pages.push({ panels: panels });
435
- });
436
- return pages;
437
- }
438
-
439
- function saveDraft() {
440
- const state = { pages: getCurrentState(), saveCode: currentSaveCode, savedAt: new Date().toISOString() };
441
- localStorage.setItem('comic_draft_'+sid, JSON.stringify(state));
442
- }
443
-
444
- function renderFromState(pagesData) {
445
- const con = document.getElementById('comic-container'); con.innerHTML = '';
446
- pagesData.forEach((page, pageIdx) => {
447
- const pageWrapper = document.createElement('div'); pageWrapper.className = 'page-wrapper';
448
- const pageTitle = document.createElement('h2'); pageTitle.className = 'page-title'; pageTitle.textContent = `Page ${pageIdx + 1}`;
449
- pageWrapper.appendChild(pageTitle);
450
- const div = document.createElement('div'); div.className = 'comic-page';
451
- const grid = document.createElement('div'); grid.className = 'comic-grid';
452
- page.panels.forEach((pan) => {
453
- const pDiv = document.createElement('div'); pDiv.className = 'panel';
454
- pDiv.onclick = (e) => { e.stopPropagation(); selectPanel(pDiv); };
455
- const img = document.createElement('img');
456
- img.src = pan.src; img.dataset.zoom = pan.zoom || 100; img.dataset.translateX = pan.tx || 0; img.dataset.translateY = pan.ty || 0;
457
- updateImageTransform(img); img.onmousedown = (e) => startPan(e, img);
458
- pDiv.appendChild(img);
459
- (pan.bubbles || []).forEach(bData => { const b = createBubbleHTML(bData); pDiv.appendChild(b); });
460
- grid.appendChild(pDiv);
461
- });
462
- div.appendChild(grid); pageWrapper.appendChild(div); con.appendChild(pageWrapper);
463
- });
464
- }
465
-
466
  async function upload() {
467
  const f = document.getElementById('file-upload').files[0];
468
- if(!f) return alert("Select a video file first");
469
- sid = genUUID(); localStorage.setItem('comic_sid', sid); currentSaveCode = null;
470
- document.querySelector('.upload-box').style.display='none'; document.getElementById('loading-view').style.display='flex';
471
  const fd = new FormData(); fd.append('file', f);
472
- const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
473
- if(r.ok) interval = setInterval(checkStatus, 2000);
474
- else { alert("Upload failed"); location.reload(); }
475
  }
476
-
477
  async function checkStatus() {
478
- try {
479
- const r = await fetch(`/status?sid=${sid}`); const d = await r.json();
480
- document.getElementById('status-text').innerText = d.message;
481
- if(d.progress >= 100) { clearInterval(interval); document.getElementById('upload-container').style.display='none'; document.getElementById('editor-container').style.display='block'; loadNewComic(); }
482
- else if (d.progress < 0) { clearInterval(interval); document.getElementById('status-text').textContent = "Error: " + d.message; document.querySelector('.loader').style.display = 'none'; }
483
- } catch(e) {}
484
- }
485
-
486
- function loadNewComic() {
487
- fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
488
- const cleanData = data.map((p, pi) => ({
489
- panels: p.panels.map((pan, j) => ({
490
- src: `/frames/${pan.image}?sid=${sid}`,
491
- bubbles: (p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) ? [{
492
- text: p.bubbles[j].dialog, left: (p.bubbles[j].bubble_offset_x || 50) + 'px', top: (p.bubbles[j].bubble_offset_y || 20) + 'px', type: 'speech', tailPos: '50%', colors: { text: '#ffffff', fill: '#4ECDC4' }
493
- }] : []
494
- }))
495
- }));
496
- renderFromState(cleanData); saveDraft();
497
- });
498
- }
499
-
500
- function goBackToUpload() {
501
- if(confirm('Go back to home? Make sure you saved your comic!')) {
502
- document.getElementById('editor-container').style.display = 'none'; document.getElementById('upload-container').style.display = 'flex';
503
- document.querySelector('.upload-box').style.display = 'block'; document.getElementById('loading-view').style.display = 'none'; document.getElementById('current-save-code').style.display = 'none';
504
  }
505
  }
506
-
507
- function createBubbleHTML(data) {
508
- const b = document.createElement('div'); b.dataset.type = data.type || 'speech';
509
- applyBubbleType(b, data.type || 'speech', data.classes);
510
- b.style.left = data.left; b.style.top = data.top;
511
- if(data.width) b.style.width = data.width; if(data.height) b.style.height = data.height; if(data.font) b.style.fontFamily = data.font;
512
- if(data.colors) { b.style.setProperty('--bubble-fill-color', data.colors.fill || '#4ECDC4'); b.style.setProperty('--bubble-text-color', data.colors.text || '#ffffff'); }
513
- if(data.tailPos) b.style.setProperty('--tail-pos', data.tailPos);
514
- const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || ''; b.appendChild(textSpan);
515
- ['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); });
516
- 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; };
517
- b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); }; b.onclick = (e) => { e.stopPropagation(); selectBubble(b); };
518
- return b;
519
- }
520
-
521
- function applyBubbleType(bubble, type, existingClasses) {
522
- bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
523
- let baseClasses = 'speech-bubble ' + type;
524
- if (type === 'speech') baseClasses += ' tail-bottom';
525
- if (existingClasses && existingClasses.includes('selected')) baseClasses += ' selected';
526
- bubble.className = baseClasses; bubble.dataset.type = type;
527
- if (type === 'thought') { for (let i = 1; i <= 2; i++) { const dot = document.createElement('div'); dot.className = `thought-dot thought-dot-${i}`; bubble.appendChild(dot); } }
528
- }
529
-
530
- function editBubbleText(bubble) {
531
- if (currentlyEditing) return; currentlyEditing = bubble;
532
- const textSpan = bubble.querySelector('.bubble-text'); const textarea = document.createElement('textarea');
533
- textarea.value = textSpan.textContent; bubble.appendChild(textarea); textSpan.style.display = 'none'; textarea.focus();
534
- const finishEditing = () => { textSpan.textContent = textarea.value; textarea.remove(); textSpan.style.display = ''; currentlyEditing = null; saveDraft(); };
535
- textarea.addEventListener('blur', finishEditing, { once: true });
536
- textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); } });
537
  }
538
-
539
- document.addEventListener('mousemove', (e) => {
540
- if(isDragging && selectedBubble) { selectedBubble.style.left = (initX + e.clientX - startX) + 'px'; selectedBubble.style.top = (initY + e.clientY - startY) + 'px'; }
541
- if(isResizing && selectedBubble) { resizeBubble(e); }
542
- if(isPanning && selectedPanel) { panImage(e); }
543
- });
544
- document.addEventListener('mouseup', () => { if(isDragging || isResizing || isPanning) saveDraft(); isDragging = false; isResizing = false; isPanning = false; });
545
-
546
- function startResize(e, dir) { e.preventDefault(); e.stopPropagation(); isResizing = true; resizeHandle = dir; const rect = selectedBubble.getBoundingClientRect(); originalWidth = rect.width; originalHeight = rect.height; originalX = selectedBubble.offsetLeft; originalY = selectedBubble.offsetTop; originalMouseX = e.clientX; originalMouseY = e.clientY; }
547
- 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('w')) { selectedBubble.style.width = `${originalWidth - dx}px`; selectedBubble.style.left = `${originalX + dx}px`; } if (resizeHandle.includes('s')) selectedBubble.style.height = `${originalHeight + dy}px`; if (resizeHandle.includes('n')) { selectedBubble.style.height = `${originalHeight - dy}px`; selectedBubble.style.top = `${originalY + dy}px`; } }
548
- function selectBubble(el) { if(selectedBubble) selectedBubble.classList.remove('selected'); if(selectedPanel) { selectedPanel.classList.remove('selected'); selectedPanel = null; } selectedBubble = el; el.classList.add('selected'); const bubbleType = el.dataset.type || 'speech'; document.getElementById('tail-controls').style.display = (bubbleType === 'speech' || bubbleType === 'thought') ? 'block' : 'none'; ['bubble-text-color','bubble-fill-color','bubble-type-select','font-select'].forEach(i => document.getElementById(i).disabled = false); document.getElementById('zoom-slider').disabled = true; document.getElementById('bubble-type-select').value = bubbleType; const styles = window.getComputedStyle(el); const textColor = styles.getPropertyValue('--bubble-text-color').trim() || rgbToHex(styles.color); const fillColor = styles.getPropertyValue('--bubble-fill-color').trim() || rgbToHex(styles.backgroundColor); document.getElementById('bubble-text-color').value = textColor; document.getElementById('bubble-fill-color').value = fillColor; const tailPos = styles.getPropertyValue('--tail-pos').trim(); document.getElementById('tail-slider').value = tailPos ? parseInt(tailPos) : 50; }
549
- function selectPanel(el) { if(selectedPanel) selectedPanel.classList.remove('selected'); if(selectedBubble) { selectedBubble.classList.remove('selected'); selectedBubble = null; } selectedPanel = el; el.classList.add('selected'); const img = el.querySelector('img'); document.getElementById('zoom-slider').disabled = false; document.getElementById('zoom-slider').value = img.dataset.zoom || 100; ['bubble-text-color','bubble-fill-color','bubble-type-select','font-select'].forEach(i => document.getElementById(i).disabled = true); document.getElementById('tail-controls').style.display = 'none'; }
550
- function addBubble() { if(!selectedPanel) return alert("Select a panel first"); const b = createBubbleHTML({ text: "New Text", left: "50px", top: "30px", type: 'speech', colors: { text: '#ffffff', fill: '#4ECDC4' } }); selectedPanel.appendChild(b); selectBubble(b); saveDraft(); }
551
- function deleteBubble() { if(!selectedBubble) return alert("Select a bubble first"); if(confirm("Delete this bubble?")) { selectedBubble.remove(); selectedBubble = null; saveDraft(); } }
552
- function changeBubbleType(type) { if(!selectedBubble) return; applyBubbleType(selectedBubble, type); selectedBubble.classList.add('selected'); document.getElementById('tail-controls').style.display = (type === 'speech' || type === 'thought') ? 'block' : 'none'; saveDraft(); }
553
- function changeFont(font) { if(!selectedBubble) return; selectedBubble.style.fontFamily = font; saveDraft(); }
554
- function rotateTail() { if(!selectedBubble) return; const type = selectedBubble.dataset.type; if(type === 'speech') { const positions = ['tail-bottom', 'tail-right', 'tail-top', 'tail-left']; let current = 0; positions.forEach((pos, i) => { if(selectedBubble.classList.contains(pos)) current = i; }); selectedBubble.classList.remove(positions[current]); selectedBubble.classList.add(positions[(current + 1) % 4]); } else if(type === 'thought') { const isFlippedH = selectedBubble.classList.contains('flipped'); const isFlippedV = selectedBubble.classList.contains('flipped-vertical'); if (!isFlippedH && !isFlippedV) selectedBubble.classList.add('flipped'); else if (isFlippedH && !isFlippedV) selectedBubble.classList.add('flipped-vertical'); else if (isFlippedH && isFlippedV) selectedBubble.classList.remove('flipped'); else selectedBubble.classList.remove('flipped-vertical'); } saveDraft(); }
555
- function slideTail(v) { if(selectedBubble) { selectedBubble.style.setProperty('--tail-pos', v+'%'); saveDraft(); } }
556
- document.getElementById('bubble-text-color').addEventListener('input', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(); } });
557
- document.getElementById('bubble-fill-color').addEventListener('input', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(); } });
558
- function handleZoom(el) { if(!selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.zoom = el.value; updateImageTransform(img); saveDraft(); }
559
- 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'); }
560
- 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); }
561
- 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); }
562
- function resetPanelTransform() { if(!selectedPanel) return alert("Select a panel first"); 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(); }
563
- function replacePanelImage() { if(!selectedPanel) return alert("Select a panel first"); 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'); img.style.opacity = '0.5'; 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}`; resetPanelTransform(); saveDraft(); } else { alert('Error: ' + d.error); } img.style.opacity = '1'; inp.value = ''; }; inp.click(); }
564
- async function adjustFrame(dir) { if(!selectedPanel) return alert("Select a panel first"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; img.style.opacity = '0.5'; 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); } img.style.opacity = '1'; saveDraft(); }
565
- async function gotoTimestamp() { if(!selectedPanel) return alert("Select a panel first"); 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 format"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; img.style.opacity = '0.5'; 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 = ''; resetPanelTransform(); } else { alert('Error: ' + d.message); } img.style.opacity = '1'; saveDraft(); }
566
- async function exportComic() { const pgs = document.querySelectorAll('.comic-page'); if(pgs.length === 0) return alert("No pages found"); 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'; }); alert(`Exporting ${pgs.length} page(s)...`); for(let i = 0; i < pgs.length; i++) { try { const u = await htmlToImage.toPng(pgs[i], {pixelRatio: 3}); 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 = ''; }); }
567
- function rgbToHex(rgb) { if (!rgb || !rgb.startsWith('rgb')) return '#ffffff'; let sep = rgb.indexOf(",") > -1 ? "," : " "; rgb = rgb.substr(4).split(")")[0].split(sep); let r = (+rgb[0]).toString(16), g = (+rgb[1]).toString(16), b = (+rgb[2]).toString(16); if (r.length == 1) r = "0" + r; if (g.length == 1) g = "0" + g; if (b.length == 1) b = "0" + b; return "#" + r + g + b; }
568
  </script>
569
  </body>
570
  </html>
571
  '''
572
 
573
- # --- 3. ENHANCED COMIC GENERATOR CLASS ---
574
- class EnhancedComicGenerator:
575
- def __init__(self, sid):
576
- self.sid = sid
577
- self.user_dir = os.path.join(BASE_USER_DIR, sid)
578
- self.video_path = os.path.join(self.user_dir, 'uploaded.mp4')
579
- self.frames_dir = os.path.join(self.user_dir, 'frames')
580
- self.output_dir = os.path.join(self.user_dir, 'output')
581
- self.status_file = os.path.join(self.output_dir, 'status.json')
582
- self.metadata_path = os.path.join(self.frames_dir, 'frame_metadata.json')
583
- os.makedirs(self.frames_dir, exist_ok=True)
584
- os.makedirs(self.output_dir, exist_ok=True)
585
- self.video_fps = None
586
- self.frame_metadata = {}
587
-
588
- def update_status(self, message, progress):
589
- try:
590
- with open(self.status_file, 'w') as f:
591
- json.dump({'message': message, 'progress': progress}, f)
592
- except:
593
- pass
594
-
595
- def cleanup_previous_run(self):
596
- print(f"🧹 Cleaning up for session {self.sid}...")
597
- if os.path.exists(self.frames_dir):
598
- for f in os.listdir(self.frames_dir):
599
- try:
600
- os.remove(os.path.join(self.frames_dir, f))
601
- except:
602
- pass
603
- if os.path.exists(self.output_dir):
604
- for f in os.listdir(self.output_dir):
605
- if f != 'status.json':
606
- try:
607
- os.remove(os.path.join(self.output_dir, f))
608
- except:
609
- pass
610
- user_srt = os.path.join(self.user_dir, 'subs.srt')
611
- if os.path.exists(user_srt):
612
- os.remove(user_srt)
613
- print("✅ Cleanup complete.")
614
-
615
- def generate_keyframes_from_moments(self, key_moments, max_frames=48):
616
- try:
617
- cap = cv2.VideoCapture(self.video_path)
618
- if not cap.isOpened():
619
- raise Exception("Cannot open video for keyframe extraction")
620
-
621
- fps = self.video_fps
622
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
623
- duration = total_frames / fps
624
-
625
- key_moments.sort(key=lambda x: x['start'])
626
- frame_metadata = {}
627
- frame_count = 0
628
-
629
- for i, moment in enumerate(key_moments[:max_frames]):
630
- self.update_status(f"Extracting frame {i+1}/{min(len(key_moments), max_frames)}...",
631
- 25 + int(20 * (i / min(len(key_moments), max_frames))))
632
-
633
- frame_time = (moment['start'] + moment['end']) / 2
634
- if frame_time > duration:
635
- continue
636
-
637
- frame_number = int(frame_time * fps)
638
- cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
639
- ret, frame = cap.read()
640
-
641
- if ret:
642
- frame_filename = f"frame_{frame_count:04d}.png"
643
- frame_path = os.path.join(self.frames_dir, frame_filename)
644
- cv2.imwrite(frame_path, frame)
645
- frame_metadata[frame_filename] = {
646
- 'time': frame_time,
647
- 'dialogue': moment['text'],
648
- 'start': moment['start'],
649
- 'end': moment['end']
650
- }
651
- frame_count += 1
652
-
653
- cap.release()
654
-
655
- with open(self.metadata_path, 'w') as f:
656
- json.dump(frame_metadata, f, indent=2)
657
-
658
- print(f"✅ Extracted {frame_count} keyframes from video")
659
- return True
660
- except Exception as e:
661
- print(f"❌ Error extracting keyframes: {e}")
662
- traceback.print_exc()
663
- return False
664
-
665
- def _enhance_all_images(self, single_image_path=None):
666
- try:
667
- enhancer = SimpleColorEnhancer()
668
- if single_image_path:
669
- enhancer.enhance_single(single_image_path)
670
- else:
671
- frame_paths = [os.path.join(self.frames_dir, f)
672
- for f in os.listdir(self.frames_dir) if f.endswith('.png')]
673
- with ThreadPoolExecutor() as executor:
674
- list(executor.map(enhancer.enhance_single, frame_paths))
675
- print("✅ Simple color enhancement complete")
676
- except Exception as e:
677
- print(f"⚠️ Simple enhancement failed: {e}")
678
-
679
- def _enhance_quality_colors(self, single_image_path=None):
680
- try:
681
- enhancer = QualityColorEnhancer()
682
- if single_image_path:
683
- enhancer.enhance_single(single_image_path)
684
- else:
685
- frame_paths = [os.path.join(self.frames_dir, f)
686
- for f in os.listdir(self.frames_dir) if f.endswith('.png')]
687
- with ThreadPoolExecutor() as executor:
688
- list(executor.map(enhancer.enhance_single, frame_paths))
689
- print("✅ Quality color enhancement complete")
690
- except Exception as e:
691
- print(f"⚠️ Quality enhancement failed: {e}")
692
-
693
- def _process_bubble_for_frame(self, frame_file):
694
- frame_path = os.path.join(self.frames_dir, frame_file)
695
- meta = self.frame_metadata.get(frame_file, {})
696
- dialogue = meta.get('dialogue', '') if isinstance(meta, dict) else ''
697
-
698
- try:
699
- faces = face_detector.detect_faces(frame_path)
700
- if faces:
701
- lip_x, lip_y = face_detector.get_lip_position(frame_path, faces[0])
702
- else:
703
- lip_x, lip_y = -1, -1
704
- bubble_x, bubble_y = ai_bubble_placer.place_bubble_ai(frame_path, (lip_x, lip_y))
705
- return bubble(
706
- bubble_offset_x=bubble_x,
707
- bubble_offset_y=bubble_y,
708
- lip_x=lip_x,
709
- lip_y=lip_y,
710
- dialog=dialogue,
711
- emotion='normal'
712
- )
713
- except Exception as e:
714
- print(f"-> Could not place bubble for {frame_file}: {e}. Using default.")
715
- return bubble(
716
- bubble_offset_x=50,
717
- bubble_offset_y=20,
718
- lip_x=-1,
719
- lip_y=-1,
720
- dialog=dialogue,
721
- emotion='normal'
722
- )
723
-
724
- def _create_ai_bubbles_from_moments(self):
725
- frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
726
-
727
- if not os.path.exists(self.metadata_path):
728
- return [bubble(dialog="") for _ in frame_files]
729
-
730
- with open(self.metadata_path, 'r') as f:
731
- self.frame_metadata = json.load(f)
732
-
733
- with ThreadPoolExecutor() as executor:
734
- bubbles = list(executor.map(self._process_bubble_for_frame, frame_files))
735
-
736
- return bubbles
737
-
738
- def _generate_pages(self, bubbles_list):
739
- try:
740
- from backend.fixed_12_pages_800x1080 import generate_12_pages_800x1080
741
- frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
742
- return generate_12_pages_800x1080(frame_files, bubbles_list)
743
- except ImportError:
744
- pages = []
745
- frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
746
- num_pages = (len(frame_files) + 3) // 4
747
-
748
- for i in range(num_pages):
749
- start, end = i * 4, (i + 1) * 4
750
- page_panels = [panel(image=f) for f in frame_files[start:end]]
751
- page_bubbles = bubbles_list[start:end]
752
- if page_panels:
753
- pages.append(Page(panels=page_panels, bubbles=page_bubbles))
754
-
755
- return pages
756
-
757
- # ==========================================
758
- # 🚀 ZEROGPU DECORATED FUNCTION
759
- # ==========================================
760
- @spaces.GPU(duration=300)
761
- def generate_comic(self):
762
- start_time = time.time()
763
- try:
764
- if cv2 is None:
765
- raise Exception("OpenCV not installed")
766
-
767
- self.update_status("Cleaning up previous run...", 0)
768
- self.cleanup_previous_run()
769
-
770
- self.update_status("Analyzing video...", 5)
771
- cap = cv2.VideoCapture(self.video_path)
772
- if not cap.isOpened():
773
- raise Exception("Cannot open video")
774
- self.video_fps = cap.get(cv2.CAP_PROP_FPS) or 25
775
- cap.release()
776
- print(f"✅ Video FPS detected: {self.video_fps:.2f}")
777
-
778
- self.update_status("Generating subtitles (this may take a while)...", 10)
779
- user_srt = os.path.join(self.user_dir, 'subs.srt')
780
- try:
781
- get_real_subtitles(self.video_path)
782
- if os.path.exists('test1.srt'):
783
- shutil.move('test1.srt', user_srt)
784
- except Exception as e:
785
- print(f"⚠️ Subtitle generation failed: {e}. Creating fallback.")
786
- with open(user_srt, 'w') as f:
787
- f.write("1\n00:00:01,000 --> 00:00:04,000\nHello\n")
788
-
789
- self.update_status("Parsing subtitles...", 20)
790
- with open(user_srt, 'r', encoding='utf-8') as f:
791
- all_subs = list(srt.parse(f.read()))
792
-
793
- key_moments = [{
794
- 'index': s.index,
795
- 'text': s.content,
796
- 'start': s.start.total_seconds(),
797
- 'end': s.end.total_seconds()
798
- } for s in all_subs]
799
-
800
- self.update_status("Extracting keyframes...", 25)
801
- if not self.generate_keyframes_from_moments(key_moments, max_frames=48):
802
- raise Exception("Keyframe extraction failed")
803
-
804
- self.update_status("Cropping black bars...", 45)
805
- try:
806
- black_x, black_y, _, _ = black_bar_crop()
807
- except:
808
- black_x, black_y = 0, 0
809
-
810
- self.update_status("Enhancing images (GPU)...", 50)
811
- self._enhance_all_images()
812
-
813
- self.update_status("Applying quality color enhancement (GPU)...", 60)
814
- self._enhance_quality_colors()
815
-
816
- self.update_status("Placing speech bubbles...", 75)
817
- bubbles = self._create_ai_bubbles_from_moments()
818
-
819
- self.update_status("Assembling comic pages...", 90)
820
- pages = self._generate_pages(bubbles)
821
-
822
- self.update_status("Saving results...", 95)
823
- self._save_results(pages)
824
-
825
- execution_time = (time.time() - start_time) / 60
826
- print(f"✅ Comic generation completed in {execution_time:.2f} minutes")
827
- self.update_status("Complete!", 100)
828
- return True
829
-
830
- except Exception as e:
831
- print(f"❌ Comic generation failed: {e}")
832
- traceback.print_exc()
833
- self.update_status(f"Error: {str(e)}", -1)
834
- return False
835
-
836
- def _save_results(self, pages):
837
- try:
838
- pages_data = []
839
- for page in pages:
840
- panels = [p.__dict__ if hasattr(p, '__dict__') else p for p in page.panels]
841
- bubbles_data = [b.__dict__ if hasattr(b, '__dict__') else b for b in page.bubbles]
842
- pages_data.append({'panels': panels, 'bubbles': bubbles_data})
843
-
844
- with open(os.path.join(self.output_dir, 'pages.json'), 'w', encoding='utf-8') as f:
845
- json.dump(pages_data, f, indent=2)
846
-
847
- print("✅ Results saved successfully!")
848
- except Exception as e:
849
- print(f"❌ Save results failed: {e}")
850
-
851
- # ==========================================
852
- # 🚀 ZEROGPU DECORATED FUNCTION
853
- # ==========================================
854
- @spaces.GPU
855
- def regenerate_frame(self, fname, direction):
856
- try:
857
- if not os.path.exists(self.metadata_path):
858
- return {"success": False, "message": "Frame metadata missing."}
859
-
860
- with open(self.metadata_path, 'r') as f:
861
- meta = json.load(f)
862
-
863
- if fname not in meta:
864
- return {"success": False, "message": "Panel not linked to video."}
865
-
866
- current_data = meta[fname]
867
- if isinstance(current_data, dict):
868
- curr_time = current_data['time']
869
- else:
870
- curr_time = current_data
871
-
872
- if not self.video_fps:
873
- cap = cv2.VideoCapture(self.video_path)
874
- self.video_fps = cap.get(cv2.CAP_PROP_FPS) or 25
875
- cap.release()
876
-
877
- offset = (1.0 / self.video_fps) * (1 if direction == 'forward' else -1)
878
- new_time = max(0, curr_time + offset)
879
-
880
- cap = cv2.VideoCapture(self.video_path)
881
- cap.set(cv2.CAP_PROP_POS_MSEC, new_time * 1000)
882
- ret, frame = cap.read()
883
- cap.release()
884
-
885
- if ret:
886
- frame_path = os.path.join(self.frames_dir, fname)
887
- cv2.imwrite(frame_path, frame)
888
-
889
- print(f"🎨 Applying enhancements to new frame: {fname}")
890
- self._enhance_all_images(single_image_path=frame_path)
891
- self._enhance_quality_colors(single_image_path=frame_path)
892
-
893
- if isinstance(meta[fname], dict):
894
- meta[fname]['time'] = new_time
895
- else:
896
- meta[fname] = new_time
897
- with open(self.metadata_path, 'w') as f:
898
- json.dump(meta, f, indent=2)
899
-
900
- message = f"Adjusted {direction} to {new_time:.3f}s"
901
- print(f"✅ {message}")
902
- return {"success": True, "message": message}
903
-
904
- return {"success": False, "message": "End of video"}
905
-
906
- except Exception as e:
907
- traceback.print_exc()
908
- return {"success": False, "message": str(e)}
909
-
910
- # ==========================================
911
- # 🚀 ZEROGPU DECORATED FUNCTION
912
- # ==========================================
913
- @spaces.GPU
914
- def get_frame_at_timestamp(self, fname, ts):
915
- try:
916
- cap = cv2.VideoCapture(self.video_path)
917
- if not cap.isOpened():
918
- return {"success": False, "message": "Cannot open video."}
919
-
920
- fps = cap.get(cv2.CAP_PROP_FPS) or 25
921
- duration = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) / fps
922
-
923
- if ts < 0 or ts > duration:
924
- cap.release()
925
- return {"success": False, "message": f"Timestamp must be between 0 and {duration:.2f}s."}
926
-
927
- cap.set(cv2.CAP_PROP_POS_MSEC, float(ts) * 1000)
928
- ret, frame = cap.read()
929
- cap.release()
930
-
931
- if ret:
932
- frame_path = os.path.join(self.frames_dir, fname)
933
- cv2.imwrite(frame_path, frame)
934
-
935
- print(f"🎨 Applying enhancements to frame from timestamp: {fname}")
936
- self._enhance_all_images(single_image_path=frame_path)
937
- self._enhance_quality_colors(single_image_path=frame_path)
938
-
939
- if os.path.exists(self.metadata_path):
940
- with open(self.metadata_path, 'r') as f:
941
- meta = json.load(f)
942
- if fname in meta:
943
- if isinstance(meta[fname], dict):
944
- meta[fname]['time'] = float(ts)
945
- else:
946
- meta[fname] = float(ts)
947
- with open(self.metadata_path, 'w') as f:
948
- json.dump(meta, f, indent=2)
949
-
950
- message = f"Jumped to timestamp {ts:.3f}s"
951
- print(f"✅ {message}")
952
- return {"success": True, "message": message}
953
-
954
- return {"success": False, "message": "Invalid time"}
955
-
956
- except Exception as e:
957
- traceback.print_exc()
958
- return {"success": False, "message": str(e)}
959
-
960
-
961
- # --- ROUTES ---
962
  @app.route('/')
963
  def index():
964
  return INDEX_HTML
965
 
966
- # Health Check Route
967
- @app.route('/health')
968
- def health():
969
- return "OK", 200
970
-
971
  @app.route('/uploader', methods=['POST'])
972
  def upload():
973
  sid = request.args.get('sid')
974
- if not sid:
975
- return jsonify({'success': False, 'message': 'Missing session ID'}), 400
976
-
977
- if 'file' not in request.files or not request.files['file'].filename:
978
- return jsonify({'success': False, 'message': 'No file selected'}), 400
979
-
980
  f = request.files['file']
981
  gen = EnhancedComicGenerator(sid)
982
  gen.cleanup_previous_run()
983
  f.save(gen.video_path)
984
  gen.update_status("Starting...", 5)
985
-
986
- # Run in a thread to allow Flask to respond immediately.
987
- threading.Thread(target=gen.generate_comic).start()
988
- return jsonify({'success': True, 'message': 'Generation started.'})
989
 
990
  @app.route('/status')
991
  def get_status():
992
  sid = request.args.get('sid')
993
- if not sid:
994
- return jsonify({'progress': 0, 'message': 'Missing session ID'})
995
-
996
  path = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
997
- if os.path.exists(path):
998
- return send_file(path)
999
  return jsonify({'progress': 0, 'message': "Waiting..."})
1000
 
1001
  @app.route('/output/<path:filename>')
1002
  def get_output(filename):
1003
  sid = request.args.get('sid')
1004
- if not sid:
1005
- return "Missing session ID", 400
1006
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
1007
 
1008
  @app.route('/frames/<path:filename>')
1009
  def get_frame(filename):
1010
  sid = request.args.get('sid')
1011
- if not sid:
1012
- return "Missing session ID", 400
1013
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
1014
 
1015
- @app.route('/regenerate_frame', methods=['POST'])
1016
- def regen():
1017
- sid = request.args.get('sid')
1018
- if not sid:
1019
- return jsonify({'success': False, 'message': 'Missing session ID'})
1020
-
1021
- d = request.get_json()
1022
- gen = EnhancedComicGenerator(sid)
1023
- return jsonify(gen.regenerate_frame(d['filename'], d['direction']))
1024
-
1025
- @app.route('/goto_timestamp', methods=['POST'])
1026
- def go_time():
1027
- sid = request.args.get('sid')
1028
- if not sid:
1029
- return jsonify({'success': False, 'message': 'Missing session ID'})
1030
-
1031
- d = request.get_json()
1032
- gen = EnhancedComicGenerator(sid)
1033
- return jsonify(gen.get_frame_at_timestamp(d['filename'], float(d['timestamp'])))
1034
-
1035
- @app.route('/replace_panel', methods=['POST'])
1036
- def rep_panel():
1037
- sid = request.args.get('sid')
1038
- if not sid:
1039
- return jsonify({'success': False, 'error': 'Missing session ID'})
1040
-
1041
- if 'image' not in request.files:
1042
- return jsonify({'success': False, 'error': 'No image provided.'})
1043
-
1044
- f = request.files['image']
1045
- frames_dir = os.path.join(BASE_USER_DIR, sid, 'frames')
1046
- os.makedirs(frames_dir, exist_ok=True)
1047
- fname = f"replaced_{int(time.time() * 1000)}.png"
1048
- f.save(os.path.join(frames_dir, fname))
1049
- return jsonify({'success': True, 'new_filename': fname})
1050
-
1051
- @app.route('/save_comic', methods=['POST'])
1052
- def save_comic():
1053
- sid = request.args.get('sid')
1054
- if not sid:
1055
- return jsonify({'success': False, 'message': 'Missing session ID'})
1056
-
1057
- try:
1058
- data = request.get_json()
1059
- save_code = generate_save_code()
1060
- save_dir = os.path.join(SAVED_COMICS_DIR, save_code)
1061
- os.makedirs(save_dir, exist_ok=True)
1062
-
1063
- user_frames_dir = os.path.join(BASE_USER_DIR, sid, 'frames')
1064
- saved_frames_dir = os.path.join(save_dir, 'frames')
1065
-
1066
- if os.path.exists(user_frames_dir):
1067
- if os.path.exists(saved_frames_dir):
1068
- shutil.rmtree(saved_frames_dir)
1069
- shutil.copytree(user_frames_dir, saved_frames_dir)
1070
-
1071
- save_data = {
1072
- 'code': save_code,
1073
- 'originalSid': sid,
1074
- 'pages': data.get('pages', []),
1075
- 'savedAt': data.get('savedAt', time.strftime('%Y-%m-%d %H:%M:%S'))
1076
- }
1077
-
1078
- with open(os.path.join(save_dir, 'comic_state.json'), 'w') as f:
1079
- json.dump(save_data, f, indent=2)
1080
-
1081
- return jsonify({'success': True, 'code': save_code})
1082
- except Exception as e:
1083
- traceback.print_exc()
1084
- return jsonify({'success': False, 'message': str(e)})
1085
-
1086
- @app.route('/load_comic/<code>')
1087
- def load_comic(code):
1088
- code = code.upper()
1089
- save_dir = os.path.join(SAVED_COMICS_DIR, code)
1090
- state_file = os.path.join(save_dir, 'comic_state.json')
1091
-
1092
- if not os.path.exists(state_file):
1093
- return jsonify({'success': False, 'message': 'Save code not found'})
1094
-
1095
- try:
1096
- with open(state_file, 'r') as f:
1097
- save_data = json.load(f)
1098
-
1099
- original_sid = save_data.get('originalSid')
1100
- saved_frames_dir = os.path.join(save_dir, 'frames')
1101
- if original_sid and os.path.exists(saved_frames_dir):
1102
- user_frames_dir = os.path.join(BASE_USER_DIR, original_sid, 'frames')
1103
- os.makedirs(user_frames_dir, exist_ok=True)
1104
- for fname in os.listdir(saved_frames_dir):
1105
- src = os.path.join(saved_frames_dir, fname)
1106
- dst = os.path.join(user_frames_dir, fname)
1107
- if not os.path.exists(dst):
1108
- shutil.copy2(src, dst)
1109
-
1110
- return jsonify({
1111
- 'success': True,
1112
- 'pages': save_data.get('pages', []),
1113
- 'originalSid': original_sid,
1114
- 'savedAt': save_data.get('savedAt')
1115
- })
1116
- except Exception as e:
1117
- traceback.print_exc()
1118
- return jsonify({'success': False, 'message': str(e)})
1119
-
1120
- @app.route('/saved_frames/<code>/<path:filename>')
1121
- def get_saved_frame(code, filename):
1122
- code = code.upper()
1123
- frames_dir = os.path.join(SAVED_COMICS_DIR, code, 'frames')
1124
- if os.path.exists(os.path.join(frames_dir, filename)):
1125
- return send_from_directory(frames_dir, filename)
1126
- return "Frame not found", 404
1127
-
1128
  if __name__ == '__main__':
1129
  port = int(os.getenv("PORT", 7860))
1130
- print(f"🚀 Starting Enhanced Comic Generator on host 0.0.0.0, port {port} (ZeroGPU Enabled)")
1131
- app.run(host='0.0.0.0', port=port, debug=False)
 
12
  from flask import Flask, render_template, request, jsonify, send_from_directory, send_file
13
 
14
  # ==========================================
15
+ # 🚀 ZEROGPU IMPORTS & SETUP
16
  # ==========================================
17
+ import spaces # Must be at the top level
18
+ import torch # Import torch explicitly
19
+
20
+ # We define GPU functions GLOBALLY so the scanner finds them.
21
+ # We pass only necessary paths/data to avoid pickling the whole class.
22
+
23
+ @spaces.GPU(duration=120)
24
+ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path):
25
+ """
26
+ Standalone GPU function to handle the heavy lifting.
27
+ """
28
+ print(f"🚀 GPU Task Started for {video_path}")
29
+
30
+ # 1. Imports inside to ensure they run in the GPU environment
 
31
  import cv2
 
 
32
  import srt
 
 
 
 
 
 
 
 
 
 
 
 
33
  from backend.keyframes.keyframes import black_bar_crop
 
 
 
 
 
 
34
  from backend.simple_color_enhancer import SimpleColorEnhancer
35
+ from backend.quality_color_enhancer import QualityColorEnhancer
36
+ from backend.subtitles.subs_real import get_real_subtitles
37
+ from backend.ai_bubble_placement import ai_bubble_placer
38
+ from backend.ai_enhanced_core import face_detector
39
+ from backend.class_def import bubble, panel, Page
40
+
41
+ # 2. Analyze Video
42
+ cap = cv2.VideoCapture(video_path)
43
+ if not cap.isOpened():
44
+ raise Exception("Cannot open video")
45
+ video_fps = cap.get(cv2.CAP_PROP_FPS) or 25
46
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
47
+ duration = total_frames / video_fps
48
+ cap.release()
49
+
50
+ # 3. Subtitles
51
+ user_srt = os.path.join(user_dir, 'subs.srt')
52
+ try:
53
+ get_real_subtitles(video_path)
54
+ if os.path.exists('test1.srt'):
55
+ shutil.move('test1.srt', user_srt)
56
+ except Exception as e:
57
+ print(f"⚠️ Subtitle generation failed: {e}. Creating fallback.")
58
+ with open(user_srt, 'w') as f:
59
+ f.write("1\n00:00:01,000 --> 00:00:04,000\nHello\n")
60
+
61
+ with open(user_srt, 'r', encoding='utf-8') as f:
62
+ all_subs = list(srt.parse(f.read()))
63
+
64
+ key_moments = [{
65
+ 'index': s.index,
66
+ 'text': s.content,
67
+ 'start': s.start.total_seconds(),
68
+ 'end': s.end.total_seconds()
69
+ } for s in all_subs]
70
+
71
+ # 4. Extract Keyframes
72
+ key_moments.sort(key=lambda x: x['start'])
73
+ frame_metadata = {}
74
+ frame_count = 0
75
+ cap = cv2.VideoCapture(video_path)
76
+
77
+ max_frames = 48
78
+ for i, moment in enumerate(key_moments[:max_frames]):
79
+ frame_time = (moment['start'] + moment['end']) / 2
80
+ if frame_time > duration: continue
81
+
82
+ frame_number = int(frame_time * video_fps)
83
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
84
+ ret, frame = cap.read()
85
+
86
+ if ret:
87
+ fname = f"frame_{frame_count:04d}.png"
88
+ fpath = os.path.join(frames_dir, fname)
89
+ cv2.imwrite(fpath, frame)
90
+ frame_metadata[fname] = {
91
+ 'time': frame_time,
92
+ 'dialogue': moment['text'],
93
+ 'start': moment['start'],
94
+ 'end': moment['end']
95
+ }
96
+ frame_count += 1
97
+ cap.release()
98
+
99
+ with open(metadata_path, 'w') as f:
100
+ json.dump(frame_metadata, f, indent=2)
101
+
102
+ # 5. Enhance & Crop
103
+ try:
104
+ black_bar_crop() # Assuming it processes files in place or globally configured
105
+ except: pass
106
 
107
+ simple_enhancer = SimpleColorEnhancer()
108
+ quality_enhancer = QualityColorEnhancer()
109
+
110
+ frame_files = sorted([f for f in os.listdir(frames_dir) if f.endswith('.png')])
111
+ for f in frame_files:
112
+ full_p = os.path.join(frames_dir, f)
113
+ simple_enhancer.enhance_single(full_p)
114
+ quality_enhancer.enhance_single(full_p)
115
+
116
+ # 6. Bubbles & Layout
117
+ bubbles_list = []
118
+ for f in frame_files:
119
+ full_p = os.path.join(frames_dir, f)
120
+ meta = frame_metadata.get(f, {})
121
+ dialogue = meta.get('dialogue', '')
122
+
123
+ try:
124
+ faces = face_detector.detect_faces(full_p)
125
+ lip = face_detector.get_lip_position(full_p, faces[0]) if faces else (-1, -1)
126
+ bx, by = ai_bubble_placer.place_bubble_ai(full_p, lip)
127
+ bubbles_list.append(bubble(dialog=dialogue, bubble_offset_x=bx, bubble_offset_y=by, lip_x=lip[0], lip_y=lip[1]))
128
+ except:
129
+ bubbles_list.append(bubble(dialog=dialogue))
130
+
131
+ # 7. Generate Pages
132
+ try:
133
+ from backend.fixed_12_pages_800x1080 import generate_12_pages_800x1080
134
+ pages = generate_12_pages_800x1080(frame_files, bubbles_list)
135
+ except:
136
+ # Fallback layout
137
+ pages = []
138
+ num_pages = (len(frame_files) + 3) // 4
139
+ for i in range(num_pages):
140
+ start, end = i * 4, (i + 1) * 4
141
+ p_panels = [panel(image=f) for f in frame_files[start:end]]
142
+ p_bubbles = bubbles_list[start:end]
143
+ if p_panels:
144
+ pages.append(Page(panels=p_panels, bubbles=p_bubbles))
145
+
146
+ # Return simple data structure to main thread
147
+ pages_data = []
148
+ for page in pages:
149
+ panels = [p.__dict__ if hasattr(p, '__dict__') else p for p in page.panels]
150
+ bubbles_data = [b.__dict__ if hasattr(b, '__dict__') else b for b in page.bubbles]
151
+ pages_data.append({'panels': panels, 'bubbles': bubbles_data})
152
+
153
+ return pages_data
154
+
155
+ @spaces.GPU
156
+ def regenerate_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
157
+ import cv2
158
+ from backend.simple_color_enhancer import SimpleColorEnhancer
159
  from backend.quality_color_enhancer import QualityColorEnhancer
160
+
161
+ if not os.path.exists(metadata_path): return None
162
+ with open(metadata_path, 'r') as f: meta = json.load(f)
163
+ if fname not in meta: return None
164
+
165
+ current_data = meta[fname]
166
+ curr_time = current_data['time'] if isinstance(current_data, dict) else current_data
167
+
168
+ cap = cv2.VideoCapture(video_path)
169
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25
170
+ offset = (1.0 / fps) * (1 if direction == 'forward' else -1)
171
+ new_time = max(0, curr_time + offset)
172
+
173
+ cap.set(cv2.CAP_PROP_POS_MSEC, new_time * 1000)
174
+ ret, frame = cap.read()
175
+ cap.release()
176
+
177
+ if ret:
178
+ path = os.path.join(frames_dir, fname)
179
+ cv2.imwrite(path, frame)
180
+ SimpleColorEnhancer().enhance_single(path)
181
+ QualityColorEnhancer().enhance_single(path)
182
+
183
+ if isinstance(meta[fname], dict): meta[fname]['time'] = new_time
184
+ else: meta[fname] = new_time
185
+ with open(metadata_path, 'w') as f: json.dump(meta, f, indent=2)
186
+ return new_time
187
+ return None
188
 
189
+ @spaces.GPU
190
+ def get_frame_at_ts_gpu(video_path, frames_dir, metadata_path, fname, ts):
191
+ import cv2
192
+ from backend.simple_color_enhancer import SimpleColorEnhancer
193
+ from backend.quality_color_enhancer import QualityColorEnhancer
194
+
195
+ cap = cv2.VideoCapture(video_path)
196
+ cap.set(cv2.CAP_PROP_POS_MSEC, float(ts) * 1000)
197
+ ret, frame = cap.read()
198
+ cap.release()
199
+
200
+ if ret:
201
+ path = os.path.join(frames_dir, fname)
202
+ cv2.imwrite(path, frame)
203
+ SimpleColorEnhancer().enhance_single(path)
204
+ QualityColorEnhancer().enhance_single(path)
205
+
206
+ if os.path.exists(metadata_path):
207
+ with open(metadata_path, 'r') as f: meta = json.load(f)
208
+ if fname in meta:
209
+ if isinstance(meta[fname], dict): meta[fname]['time'] = float(ts)
210
+ else: meta[fname] = float(ts)
211
+ with open(metadata_path, 'w') as f: json.dump(meta, f, indent=2)
212
+ return True
213
+ return False
214
 
215
+ # ==========================================
216
+ # ⚙️ CONFIG & DEPENDENCIES
217
+ # ==========================================
218
+ logging.basicConfig(level=logging.INFO)
219
+ logger = logging.getLogger(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
  # --- FLASK APP SETUP ---
222
  app = Flask(__name__)
 
227
  os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
228
 
229
  def generate_save_code(length=8):
 
230
  chars = string.ascii_uppercase + string.digits
231
  while True:
232
  code = ''.join(random.choices(chars, k=length))
233
  if not os.path.exists(os.path.join(SAVED_COMICS_DIR, code)):
234
  return code
235
 
236
+ # --- 3. ENHANCED COMIC GENERATOR CLASS (Wrapper) ---
237
+ class EnhancedComicGenerator:
238
+ def __init__(self, sid):
239
+ self.sid = sid
240
+ self.user_dir = os.path.join(BASE_USER_DIR, sid)
241
+ self.video_path = os.path.join(self.user_dir, 'uploaded.mp4')
242
+ self.frames_dir = os.path.join(self.user_dir, 'frames')
243
+ self.output_dir = os.path.join(self.user_dir, 'output')
244
+ self.status_file = os.path.join(self.output_dir, 'status.json')
245
+ self.metadata_path = os.path.join(self.frames_dir, 'frame_metadata.json')
246
+ os.makedirs(self.frames_dir, exist_ok=True)
247
+ os.makedirs(self.output_dir, exist_ok=True)
248
+
249
+ def update_status(self, message, progress):
250
+ try:
251
+ with open(self.status_file, 'w') as f:
252
+ json.dump({'message': message, 'progress': progress}, f)
253
+ except: pass
254
+
255
+ def cleanup_previous_run(self):
256
+ if os.path.exists(self.frames_dir):
257
+ for f in os.listdir(self.frames_dir):
258
+ try: os.remove(os.path.join(self.frames_dir, f))
259
+ except: pass
260
+ if os.path.exists(self.output_dir):
261
+ for f in os.listdir(self.output_dir):
262
+ if f != 'status.json':
263
+ try: os.remove(os.path.join(self.output_dir, f))
264
+ except: pass
265
+
266
+ def run_process(self):
267
+ try:
268
+ self.update_status("Starting GPU Process...", 10)
269
+
270
+ # CALL GLOBAL GPU FUNCTION
271
+ pages_data = generate_comic_gpu(
272
+ self.video_path,
273
+ self.user_dir,
274
+ self.frames_dir,
275
+ self.metadata_path
276
+ )
277
+
278
+ # SAVE RESULTS
279
+ with open(os.path.join(self.output_dir, 'pages.json'), 'w', encoding='utf-8') as f:
280
+ json.dump(pages_data, f, indent=2)
281
+
282
+ self.update_status("Complete!", 100)
283
+ print("✅ Comic generated successfully")
284
+ except Exception as e:
285
+ traceback.print_exc()
286
+ self.update_status(f"Error: {str(e)}", -1)
287
+
288
+ # --- ROUTES ---
289
  INDEX_HTML = '''
290
  <!DOCTYPE html>
291
  <html lang="en">
 
296
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
297
  <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@700&family=Gloria+Hallelujah&family=Lato&display=swap" rel="stylesheet">
298
  <style>
 
299
  * { box-sizing: border-box; }
300
  body { background-color: #fdf6e3; font-family: 'Lato', sans-serif; color: #3d3d3d; margin: 0; min-height: 100vh; }
301
  #upload-container { display: flex; flex-direction:column; justify-content: center; align-items: center; min-height: 100vh; width: 100%; padding: 20px; }
302
  .upload-box { max-width: 500px; width: 100%; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); text-align: center; }
303
  #editor-container { display: none; padding: 20px; width: 100%; box-sizing: border-box; padding-bottom: 100px; }
304
  h1 { color: #2c3e50; margin-bottom: 20px; font-weight: 600; }
 
 
 
 
305
  .submit-btn { width: 100%; padding: 15px; background: #e67e22; color: white; border: none; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; transition: 0.2s; }
306
+ .file-label { display: block; padding: 15px; background: #2c3e50; color: white; border-radius: 8px; cursor: pointer; font-weight: bold; margin-bottom: 10px; transition:0.2s; }
307
+ .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; margin: 20px auto;}
 
 
 
 
 
 
 
 
 
 
 
308
  .comic-grid { display: grid; grid-template-columns: 285px 285px; grid-template-rows: 185px 185px; gap: 10px; width: 100%; height: 100%; }
309
  .panel { position: relative; overflow: hidden; border: 2px solid #000; background: #eee; cursor: pointer; }
310
+ .panel img { width: 100%; height: 100%; object-fit: cover; }
311
+ .speech-bubble { position: absolute; display: flex; justify-content: center; align-items: center; width: 150px; height: 80px; z-index: 10; cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold; font-size: 13px; text-align: center; background: #4ECDC4; color: white; border-radius: 20px; padding: 10px;}
312
+ .edit-controls { position: fixed; bottom: 20px; right: 20px; width: 260px; background: rgba(44, 62, 80, 0.95); color: white; padding: 15px; border-radius: 8px; }
313
+ button { width: 100%; margin-top: 5px; padding: 6px; border-radius: 4px; border: 1px solid #ddd; cursor: pointer; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  </style>
315
  </head>
316
  <body>
 
317
  <div id="upload-container">
318
  <div class="upload-box">
319
  <h1>🎬 Comic Generator</h1>
320
+ <input type="file" id="file-upload" style="display:none" onchange="document.getElementById('fn').innerText=this.files[0].name">
321
  <label for="file-upload" class="file-label">📁 Choose Video File</label>
322
+ <span id="fn" style="display:block; margin-bottom:10px;">No file selected</span>
323
  <button class="submit-btn" onclick="upload()">🚀 Generate Comic</button>
324
+ <div id="loading" style="display:none; margin-top:20px;">Processing... <span id="prog">0</span>%</div>
 
 
 
 
 
 
 
 
 
 
 
 
325
  </div>
326
  </div>
 
 
327
  <div id="editor-container">
328
+ <div id="comic-container"></div>
 
329
  <div class="edit-controls">
330
+ <h4>Controls</h4>
331
+ <button onclick="saveComic()">💾 Save Comic</button>
332
+ <button onclick="location.reload()">🏠 New Comic</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  </div>
334
  </div>
 
 
 
 
 
 
 
 
 
 
 
335
  <script>
336
+ let sid = Math.random().toString(36).substring(7);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  async function upload() {
338
  const f = document.getElementById('file-upload').files[0];
339
+ if(!f) return alert("Select file");
340
+ document.getElementById('loading').style.display='block';
 
341
  const fd = new FormData(); fd.append('file', f);
342
+ await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
343
+ setInterval(checkStatus, 2000);
 
344
  }
 
345
  async function checkStatus() {
346
+ const r = await fetch(`/status?sid=${sid}`);
347
+ const d = await r.json();
348
+ document.getElementById('prog').innerText = d.progress;
349
+ if(d.progress >= 100) {
350
+ document.getElementById('upload-container').style.display='none';
351
+ document.getElementById('editor-container').style.display='block';
352
+ loadComic();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  }
354
  }
355
+ async function loadComic() {
356
+ const r = await fetch(`/output/pages.json?sid=${sid}`);
357
+ const data = await r.json();
358
+ const con = document.getElementById('comic-container');
359
+ data.forEach((page, i) => {
360
+ const div = document.createElement('div'); div.className='comic-page';
361
+ const grid = document.createElement('div'); grid.className='comic-grid';
362
+ page.panels.forEach(pan => {
363
+ const p = document.createElement('div'); p.className='panel';
364
+ p.innerHTML = `<img src="/frames/${pan.image}?sid=${sid}">`;
365
+ grid.appendChild(p);
366
+ (pan.bubbles||[]).forEach(b => {
367
+ const bub = document.createElement('div'); bub.className='speech-bubble';
368
+ bub.innerText = b.dialog || "...";
369
+ bub.style.left = (b.bubble_offset_x||50)+'px';
370
+ bub.style.top = (b.bubble_offset_y||20)+'px';
371
+ p.appendChild(bub);
372
+ });
373
+ });
374
+ div.appendChild(grid); con.appendChild(div);
375
+ });
 
 
 
 
 
 
 
 
 
 
376
  }
377
+ async function saveComic() { alert("Save not implemented in simple view"); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  </script>
379
  </body>
380
  </html>
381
  '''
382
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  @app.route('/')
384
  def index():
385
  return INDEX_HTML
386
 
 
 
 
 
 
387
  @app.route('/uploader', methods=['POST'])
388
  def upload():
389
  sid = request.args.get('sid')
390
+ if 'file' not in request.files: return "No file", 400
 
 
 
 
 
391
  f = request.files['file']
392
  gen = EnhancedComicGenerator(sid)
393
  gen.cleanup_previous_run()
394
  f.save(gen.video_path)
395
  gen.update_status("Starting...", 5)
396
+ threading.Thread(target=gen.run_process).start()
397
+ return jsonify({'success': True})
 
 
398
 
399
  @app.route('/status')
400
  def get_status():
401
  sid = request.args.get('sid')
 
 
 
402
  path = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
403
+ if os.path.exists(path): return send_file(path)
 
404
  return jsonify({'progress': 0, 'message': "Waiting..."})
405
 
406
  @app.route('/output/<path:filename>')
407
  def get_output(filename):
408
  sid = request.args.get('sid')
 
 
409
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
410
 
411
  @app.route('/frames/<path:filename>')
412
  def get_frame(filename):
413
  sid = request.args.get('sid')
 
 
414
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
415
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  if __name__ == '__main__':
417
  port = int(os.getenv("PORT", 7860))
418
+ print(f"🚀 Starting on port {port}")
419
+ app.run(host='0.0.0.0', port=port)