tester343 commited on
Commit
75c73c6
·
verified ·
1 Parent(s): b5ffd51

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +173 -606
app_enhanced.py CHANGED
@@ -9,11 +9,9 @@ import logging
9
  from concurrent.futures import ThreadPoolExecutor
10
  from flask import Flask, render_template, request, jsonify, send_from_directory, send_file
11
 
12
- # --- 0. SUPPRESS HUGGING FACE WARNINGS ---
13
- import warnings
14
- warnings.filterwarnings("ignore", category=UserWarning)
15
- os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
16
- logging.getLogger("transformers").setLevel(logging.ERROR)
17
 
18
  # --- 1. CORE DEPENDENCY CHECKS ---
19
  try:
@@ -21,56 +19,42 @@ try:
21
  import numpy as np
22
  from PIL import Image
23
  import srt
 
24
  except ImportError as e:
25
  print(f"❌ CRITICAL ERROR: Missing python library. {e}")
26
- cv2 = None
27
- np = None
28
- Image = None
29
- srt = None
30
 
31
- # --- 2. BACKEND MODULE IMPORTS (WITH ROBUST FALLBACKS) ---
32
- def dummy_black_bar_crop(): return 0, 0, None, None
33
 
34
  try:
35
  from backend.keyframes.keyframes import black_bar_crop
36
- print("✅ Black bar cropping module loaded.")
37
- except Exception:
38
- black_bar_crop = dummy_black_bar_crop
39
 
40
  try:
41
  from backend.simple_color_enhancer import SimpleColorEnhancer
42
- print("✅ SimpleColorEnhancer loaded.")
43
- except Exception:
44
  class SimpleColorEnhancer:
45
  def enhance_single(self, *args): pass
46
-
47
- try:
48
- from backend.quality_color_enhancer import QualityColorEnhancer
49
- print("✅ QualityColorEnhancer loaded.")
50
- except Exception:
51
  class QualityColorEnhancer:
52
  def enhance_single(self, *args): pass
53
 
54
  try:
55
  from backend.class_def import bubble, panel, Page
56
- print("✅ Core class definitions loaded.")
57
- except Exception:
58
- def bubble(dialog="", bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, emotion='normal'):
59
- return {
60
- 'dialog': dialog, 'bubble_offset_x': bubble_offset_x, 'bubble_offset_y': bubble_offset_y,
61
- 'lip_x': lip_x, 'lip_y': lip_y, 'emotion': emotion
62
- }
63
- def panel(image=""): return {'image': image}
64
  class Page:
65
  def __init__(self, panels, bubbles): self.panels, self.bubbles = panels, bubbles
66
 
67
  try:
68
- from backend.ai_enhanced_core import image_processor, comic_styler, face_detector, layout_optimizer
69
  from backend.ai_bubble_placement import ai_bubble_placer
70
  from backend.subtitles.subs_real import get_real_subtitles
71
- from backend.keyframes.keyframes_simple import generate_keyframes_simple
72
- print("✅ Core utility modules loaded.")
73
- except Exception:
74
  def get_real_subtitles(v): pass
75
  class DummyDetector:
76
  def detect_faces(self, p): return []
@@ -80,12 +64,10 @@ except Exception:
80
  def place_bubble_ai(self, p, l): return 50, 20
81
  ai_bubble_placer = DummyPlacer()
82
 
83
-
84
- # --- FLASK APP SETUP ---
85
  app = Flask(__name__)
86
  BASE_USER_DIR = "userdata"
87
 
88
- # --- HTML INTERFACE ---
89
  INDEX_HTML = '''
90
  <!DOCTYPE html>
91
  <html lang="en">
@@ -96,199 +78,76 @@ INDEX_HTML = '''
96
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
97
  <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@700&family=Lato&display=swap" rel="stylesheet">
98
  <style>
99
- /* GLOBAL STYLES */
100
  body { background-color: #fdf6e3; font-family: 'Lato', sans-serif; color: #3d3d3d; margin: 0; min-height: 100vh; }
101
-
102
- /* UPLOAD VIEW */
103
  #upload-container { display: flex; justify-content: center; align-items: center; min-height: 100vh; width: 100%; }
104
  .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; }
105
- .file-input { display: none; }
106
- .file-label { display: block; padding: 15px 25px; background-color: #2c3e50; color: white; border-radius: 8px; cursor: pointer; margin-bottom: 15px; font-weight: bold; transition: 0.3s; }
107
- .file-label:hover { background-color: #34495e; transform: translateY(-2px); }
108
- .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.3s; }
109
- .submit-btn:hover { background-color: #d35400; }
110
-
111
- .loading-view { display: none; flex-direction: column; align-items: center; margin-top: 20px; }
112
- .loader { width: 120px; height: 20px; background: radial-gradient(circle 10px, #e67e22 100%, transparent 0); background-size: 20px 20px; animation: ball 1s infinite linear; }
113
- @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
114
- #status-text { margin-top: 20px; font-size: 18px; font-weight: 500; }
115
-
116
- /* EDITOR VIEW */
117
  #editor-container { display: none; padding: 20px; width: 100%; box-sizing: border-box; }
118
- .comic-container-wrapper { max-width: 1200px; margin: 0 auto; padding-bottom: 100px; }
119
- .comic-page { background: white; width: 600px; height: 400px; box-shadow: 0 0 10px rgba(0,0,0,0.1); position: relative; overflow: hidden; border: 1px solid #333; padding: 10px; margin: 0 auto 30px; }
120
- .comic-grid { display: grid; grid-template-columns: 285px 285px; grid-template-rows: 185px 185px; gap: 10px; width: 100%; height: 100%; }
121
- .page-title { text-align: center; margin-bottom: 10px; font-family: 'Bangers', cursive; letter-spacing: 1px; }
122
 
123
- .panel { position: relative; overflow: hidden; width: 100%; height: 100%; border: 1px solid #333; cursor: pointer; background: #eee; }
124
- .panel.selected { outline: 3px solid #2196F3; outline-offset: -3px; }
125
- .panel img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.1s ease-out; }
126
- .panel img.pannable { cursor: grab; }
127
- .panel img.panning { cursor: grabbing; }
128
 
129
- /* SPEECH BUBBLE (SHARK FIN CSS) */
130
- .speech-bubble {
131
- position: absolute; display: flex; justify-content: center; align-items: center;
132
- width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
133
- z-index: 10; cursor: move; overflow: visible; font-family: 'Comic Neue', cursive;
134
- font-weight: bold; font-size: 14px; text-align: center;
135
- }
136
- .bubble-text { padding: 0.8em; word-wrap: break-word; position: relative; z-index: 5; }
137
- .speech-bubble.selected { outline: 2px dashed #4CAF50; }
138
- .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); font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102; }
139
-
140
- /* Exact CSS Logic provided for Shark Fin Tail (adapted for export) */
141
  .speech-bubble.speech {
142
- --b: 3em; /* tail base width */
143
- --h: 1.8em; /* tail height */
144
- --t: 0.6; /* thickness */
145
- --p: var(--tail-pos, 50%); /* Slider position */
146
- --r: 1.2em; /* radius */
147
- --c: var(--bubble-fill-color, #4ECDC4); /* color */
148
-
149
- background: var(--c);
150
- color: var(--bubble-text-color, #fff);
151
- padding: 1em;
152
- position: absolute;
153
  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);
 
 
 
154
  }
155
-
156
  .speech-bubble.speech:before {
157
  content: ""; position: absolute; width: var(--b); height: var(--h);
158
  background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
159
  border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
160
  }
161
-
162
- /* Tail Rotations */
163
  .speech-bubble.speech.tail-bottom:before { top: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
164
-
165
- .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); }
166
  .speech-bubble.speech.tail-top:before { bottom: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: scaleY(-1); }
167
-
168
- .speech-bubble.speech.tail-left { border-radius: var(--r); }
169
  .speech-bubble.speech.tail-left:before { right: 99%; 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; }
170
-
171
- .speech-bubble.speech.tail-right { border-radius: var(--r); }
172
  .speech-bubble.speech.tail-right:before { left: 99%; 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; }
173
 
174
- /* Thought Bubble */
175
- .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
176
- .speech-bubble.thought::after { display: none; }
177
- .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
178
- .thought-dot-1 { width: 20px; height: 20px; bottom: -20px; left: 15px; }
179
- .thought-dot-2 { width: 12px; height: 12px; bottom: -32px; left: 5px; }
180
-
181
- /* Resize Handles */
182
- .resize-handle { position: absolute; width: 10px; height: 10px; background: #2196F3; border: 1px solid white; border-radius: 50%; display: none; z-index: 11; }
183
- .speech-bubble.selected .resize-handle { display: block; }
184
- .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
185
- .resize-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
186
- .resize-handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
187
- .resize-handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
188
-
189
- /* Controls */
190
- .edit-controls {
191
- position: fixed; bottom: 20px; right: 20px; width: 220px;
192
- background: rgba(44, 62, 80, 0.95); color: white; padding: 15px;
193
- border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); z-index: 1000;
194
- }
195
- .edit-controls h4 { margin: 0 0 10px 0; color: #4ECDC4; text-align: center; }
196
- .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
197
- button, select, input { width: 100%; margin-top: 5px; padding: 6px; border-radius: 4px; border: none; cursor: pointer; font-weight: bold; }
198
- .action-button { background-color: #4CAF50; color: white; }
199
- .secondary-button { background-color: #f39c12; color: white; }
200
- .reset-button { background-color: #e74c3c; color: white; }
201
- .button-grid, .zoom-controls, .timestamp-controls, .color-picker-grid { display: grid; gap: 5px; }
202
- .button-grid { grid-template-columns: 1fr 1fr; }
203
- .zoom-controls { grid-template-columns: auto 1fr; align-items: center; }
204
- .timestamp-controls { grid-template-columns: 1fr auto; }
205
- .color-picker-grid { grid-template-columns: 1fr 1fr; }
206
- .slider-container { display: flex; align-items: center; gap: 5px; margin-top: 5px; }
207
- .slider-container label { font-size: 11px; min-width: 30px; }
208
  </style>
209
  </head>
210
  <body>
211
- <!-- UPLOAD SECTION -->
212
  <div id="upload-container">
213
  <div class="upload-box">
214
- <h1>🎬 Movie to Comic</h1>
215
- <form id="upload-form">
216
- <label for="file-upload" class="file-label">Choose Video File</label>
217
- <input id="file-upload" class="file-input" type="file" accept="video/*" onchange="document.getElementById('fname').innerText=this.files[0].name">
218
- <span id="fname" style="display:block; margin-bottom:10px; color:#7f8c8d; font-style:italic;">No file selected</span>
219
- <button class="submit-btn" type="submit">Generate Comic</button>
220
- </form>
221
- <div class="loading-view" id="loading-view">
222
  <div class="loader"></div>
223
- <p id="status-text">Starting...</p>
224
  </div>
225
  </div>
226
  </div>
227
 
228
- <!-- EDITOR SECTION -->
229
  <div id="editor-container">
230
- <div class="comic-container-wrapper">
231
- <h1 class="comic-title" style="text-align:center;">🎬 Generated Comic</h1>
232
- <div id="comic-pages"></div>
233
- </div>
234
-
235
- <input type="file" id="image-uploader" style="display: none;" accept="image/*">
236
-
237
  <div class="edit-controls">
238
- <h4>✏️ Editor</h4>
239
-
240
- <!-- Bubble Controls -->
241
- <div class="control-group">
242
- <label>Bubble:</label>
243
- <select id="bubble-type-select" onchange="changeBubbleType(this.value)" disabled>
244
- <option value="speech">Speech</option><option value="thought">Thought</option>
245
- </select>
246
- <div class="color-picker-grid">
247
- <div><small>Text</small><input type="color" id="bubble-text-color" value="#ffffff" disabled></div>
248
- <div><small>Fill</small><input type="color" id="bubble-fill-color" value="#4ECDC4" disabled></div>
249
- </div>
250
- <button onclick="addBubbleToPanel()" class="action-button">💬 Add Bubble</button>
251
- <button onclick="deleteBubble()" class="reset-button">🗑️ Delete</button>
252
- </div>
253
-
254
- <!-- Tail Controls -->
255
- <div class="control-group" id="tail-controls" style="display: none;">
256
- <label>Tail:</label>
257
- <button onclick="rotateBubbleTail()" class="secondary-button">🔄 Rotate Side</button>
258
- <div class="slider-container">
259
- <label>Pos</label>
260
- <input type="range" id="tail-slider" min="10" max="90" value="50" oninput="slideTail(this.value)">
261
- </div>
262
- </div>
263
-
264
- <!-- Panel Controls -->
265
- <div class="control-group">
266
- <label>Panel:</label>
267
- <button onclick="replacePanelImage()" class="action-button">🖼️ Replace Image</button>
268
- <div class="button-grid">
269
- <button onclick="adjustFrame('backward')" class="secondary-button">⬅️ Prev</button>
270
- <button onclick="adjustFrame('forward')" class="action-button">Next ➡️</button>
271
- </div>
272
- <div class="timestamp-controls">
273
- <input type="text" id="timestamp-input" placeholder="mm:ss">
274
- <button onclick="gotoTimestamp()" class="action-button">Go</button>
275
- </div>
276
- <div class="zoom-controls" style="margin-top:5px;">
277
- <button onclick="resetPanelTransform()" class="reset-button" style="font-size:10px; padding:2px;">Reset</button>
278
- <input type="range" id="zoom-slider" min="100" max="300" value="100" step="5" disabled>
279
- </div>
280
- </div>
281
-
282
- <!-- Export -->
283
- <div class="control-group">
284
- <button onclick="exportPagesToPNG()" class="action-button" style="background-color: #2196F3;">💾 Export PNG</button>
285
- <button onclick="location.reload()" class="reset-button">↺ Start Over</button>
286
- </div>
287
  </div>
288
  </div>
289
 
290
  <script>
291
- // --- CLIENT SIDE SESSION MANAGEMENT ---
292
  function generateUUID() {
293
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
294
  var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
@@ -299,328 +158,118 @@ INDEX_HTML = '''
299
  if(!sid) { sid = generateUUID(); localStorage.setItem('comic_sid', sid); }
300
  console.log("SID:", sid);
301
 
302
- let statusInterval;
303
- let currentlySelectedBubble = null, currentlySelectedPanel = null;
304
- let draggedBubble = null, offset = {x:0, y:0};
305
- let isPanning = false, panStartX, panStartY, panStartTx, panStartTy;
306
- let isResizing = false, resizeHandle, origW, origH, origX, origY, origMX, origMY;
307
 
308
- // --- UPLOAD FLOW ---
309
- document.getElementById('upload-form').addEventListener('submit', async (e) => {
310
- e.preventDefault();
311
  const file = document.getElementById('file-upload').files[0];
312
- if(!file) return alert("Please select a file");
313
 
314
- document.getElementById('upload-view').style.display = 'none';
315
- document.getElementById('loading-view').style.display = 'flex';
316
 
317
- const fd = new FormData(); fd.append('file', file);
 
318
 
319
  try {
320
  const res = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
321
- if(res.ok) statusInterval = setInterval(checkStatus, 2000);
322
- else { alert("Upload failed"); location.reload(); }
323
- } catch(e) { console.error(e); alert("Error uploading"); }
324
- });
 
 
 
 
 
 
 
 
325
 
326
  async function checkStatus() {
327
  try {
328
  const res = await fetch(`/status?sid=${sid}`);
329
  const data = await res.json();
330
- document.getElementById('status-text').textContent = data.message;
331
 
332
  if(data.progress >= 100) {
333
- clearInterval(statusInterval);
334
- document.getElementById('upload-container').style.display = 'none';
335
- document.getElementById('editor-container').style.display = 'block';
336
  loadComic();
337
  } else if(data.progress < 0) {
338
- clearInterval(statusInterval);
339
- alert("Error: " + data.message);
340
  location.reload();
341
  }
342
- } catch(e) {}
343
  }
344
 
345
- // --- EDITOR LOGIC ---
346
  function loadComic() {
347
  fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
348
- const container = document.getElementById('comic-pages');
349
- container.innerHTML = '';
350
- data.forEach((page, i) => {
351
- const wrapper = document.createElement('div');
352
- wrapper.className = 'comic-page';
353
- wrapper.id = 'page-'+i;
354
-
355
  const grid = document.createElement('div');
356
  grid.className = 'comic-grid';
357
 
358
- page.panels.forEach((p, j) => {
359
  const pDiv = document.createElement('div');
360
  pDiv.className = 'panel';
361
- pDiv.onclick = (e) => { e.stopPropagation(); selectPanel(pDiv); };
362
-
363
  const img = document.createElement('img');
364
- img.src = `/frames/${p.image}?sid=${sid}`;
365
- img.onmousedown = startPan;
366
  pDiv.appendChild(img);
367
 
368
- // Bubbles
369
- if(page.bubbles && page.bubbles[j] && page.bubbles[j].dialog) {
370
- const b = createBubbleElement({
371
- text: page.bubbles[j].dialog,
372
- left: (page.bubbles[j].bubble_offset_x||50)+'px',
373
- top: (page.bubbles[j].bubble_offset_y||20)+'px'
374
- });
375
  pDiv.appendChild(b);
 
376
  }
377
  grid.appendChild(pDiv);
378
  });
379
- wrapper.appendChild(grid);
380
-
381
- // Title wrapper
382
- const outer = document.createElement('div');
383
- outer.className = 'page-wrapper';
384
- outer.innerHTML = `<h2 class="page-title">Page ${i+1}</h2>`;
385
- outer.appendChild(wrapper);
386
- container.appendChild(outer);
387
- });
388
-
389
- // Setup Global Listeners
390
- document.addEventListener('mousemove', (e) => {
391
- if(isPanning) panImage(e);
392
- if(draggedBubble) drag(e);
393
- if(isResizing) resizeBubble(e);
394
- });
395
- document.addEventListener('mouseup', () => {
396
- stopPan(); stopDrag(); stopResize();
397
- });
398
-
399
- // Color Listeners
400
- document.getElementById('bubble-text-color').addEventListener('input', (e) => {
401
- if(currentlySelectedBubble) currentlySelectedBubble.style.setProperty('--bubble-text-color', e.target.value);
402
- });
403
- document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
404
- if(currentlySelectedBubble) currentlySelectedBubble.style.setProperty('--bubble-fill-color', e.target.value);
405
  });
406
- // Slider Listener
407
- document.getElementById('zoom-slider').addEventListener('input', handleZoom);
408
- document.getElementById('tail-slider').addEventListener('input', (e) => slideTail(e.target.value));
409
  });
410
  }
411
 
412
- function createBubbleElement(d) {
413
- const b = document.createElement('div');
414
- b.className = 'speech-bubble speech tail-bottom';
415
- b.style.left = d.left; b.style.top = d.top;
416
- b.dataset.type = 'speech';
417
-
418
- const span = document.createElement('span');
419
- span.className = 'bubble-text';
420
- span.textContent = d.text;
421
- b.appendChild(span);
422
-
423
- // Handles
424
- ['nw','ne','sw','se'].forEach(dir => {
425
- const h = document.createElement('div'); h.className = `resize-handle ${dir}`;
426
- h.onmousedown = (e) => startResize(e, dir);
427
- b.appendChild(h);
428
- });
429
-
430
- b.onmousedown = startDrag;
431
- b.onclick = (e) => { e.stopPropagation(); selectBubble(b); };
432
- b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
433
- return b;
434
- }
435
-
436
- function selectPanel(el) {
437
- document.querySelectorAll('.panel.selected').forEach(e=>e.classList.remove('selected'));
438
- el.classList.add('selected');
439
- currentlySelectedPanel = el;
440
- selectBubble(null);
441
- resetPanelTransform(true); // Load zoom state
442
- }
443
-
444
- function selectBubble(el) {
445
- if(currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
446
- currentlySelectedBubble = el;
447
- const tailControls = document.getElementById('tail-controls');
448
-
449
- if(el) {
450
  el.classList.add('selected');
451
- document.getElementById('bubble-text-color').disabled = false;
452
- document.getElementById('bubble-fill-color').disabled = false;
453
- document.getElementById('bubble-type-select').disabled = false;
454
- document.getElementById('zoom-slider').disabled = true;
455
- tailControls.style.display = (el.dataset.type === 'speech') ? 'block' : 'none';
456
- } else {
457
- document.getElementById('bubble-text-color').disabled = true;
458
- document.getElementById('bubble-fill-color').disabled = true;
459
- document.getElementById('bubble-type-select').disabled = true;
460
- document.getElementById('zoom-slider').disabled = false;
461
- tailControls.style.display = 'none';
462
- }
463
  }
464
 
465
- // --- BUBBLE ACTIONS ---
466
- function startDrag(e) {
467
- const b = e.target.closest('.speech-bubble');
468
- if(!b || e.target.classList.contains('resize-handle')) return;
469
- draggedBubble = b; selectBubble(b);
470
- const r = b.getBoundingClientRect();
471
- offset = {x: e.clientX - r.left, y: e.clientY - r.top};
472
- }
473
- function drag(e) {
474
- if(!draggedBubble) return;
475
- e.preventDefault();
476
- const p = draggedBubble.parentElement.getBoundingClientRect();
477
- draggedBubble.style.left = (e.clientX - p.left - offset.x) + 'px';
478
- draggedBubble.style.top = (e.clientY - p.top - offset.y) + 'px';
479
- }
480
- function stopDrag() { draggedBubble = null; }
481
-
482
- function startResize(e, dir) {
483
- e.stopPropagation(); e.preventDefault();
484
- isResizing = true; resizeHandle = dir;
485
- const b = currentlySelectedBubble;
486
- const r = b.getBoundingClientRect();
487
- origW = r.width; origH = r.height; origX = b.offsetLeft; origY = b.offsetTop;
488
- origMX = e.clientX; origMY = e.clientY;
489
- }
490
- function resizeBubble(e) {
491
- if(!isResizing) return;
492
- const dx = e.clientX - origMX; const dy = e.clientY - origMY;
493
- const b = currentlySelectedBubble;
494
- if(resizeHandle.includes('e')) b.style.width = (origW + dx) + 'px';
495
- if(resizeHandle.includes('w')) { b.style.width = (origW - dx) + 'px'; b.style.left = (origX + dx) + 'px'; }
496
- if(resizeHandle.includes('s')) b.style.height = (origH + dy) + 'px';
497
- if(resizeHandle.includes('n')) { b.style.height = (origH - dy) + 'px'; b.style.top = (origY + dy) + 'px'; }
498
- }
499
- function stopResize() { isResizing = false; }
500
-
501
- function slideTail(val) { if(currentlySelectedBubble) currentlySelectedBubble.style.setProperty('--tail-pos', val+'%'); }
502
- function rotateBubbleTail() {
503
- if(!currentlySelectedBubble) return;
504
- const b = currentlySelectedBubble;
505
- if(b.classList.contains('tail-bottom')) b.classList.replace('tail-bottom','tail-left');
506
- else if(b.classList.contains('tail-left')) b.classList.replace('tail-left','tail-top');
507
- else if(b.classList.contains('tail-top')) b.classList.replace('tail-top','tail-right');
508
- else b.className = b.className.replace(/tail-\w+/,'tail-bottom');
509
- }
510
- function changeBubbleType(val) {
511
- if(!currentlySelectedBubble) return;
512
- currentlySelectedBubble.className = `speech-bubble ${val} tail-bottom selected`;
513
- currentlySelectedBubble.dataset.type = val;
514
- selectBubble(currentlySelectedBubble); // refresh UI
515
- }
516
- function editBubbleText(b) {
517
- const span = b.querySelector('.bubble-text');
518
- const txt = document.createElement('textarea');
519
- txt.value = span.textContent;
520
- b.appendChild(txt); span.style.display='none';
521
- txt.focus();
522
- txt.onblur = () => { span.textContent = txt.value; txt.remove(); span.style.display='block'; };
523
- }
524
- function addBubbleToPanel() {
525
- if(!currentlySelectedPanel) return alert("Select a panel first");
526
- const b = createBubbleElement({text:"Text...", left:"20px", top:"20px"});
527
- currentlySelectedPanel.appendChild(b);
528
- selectBubble(b);
529
- }
530
- function deleteBubble() { if(currentlySelectedBubble) { currentlySelectedBubble.remove(); selectBubble(null); } }
531
-
532
- // --- PANEL ACTIONS ---
533
- function startPan(e) {
534
- if(e.button!==0) return;
535
- const img = e.target;
536
- if((parseFloat(img.dataset.zoom)||100) <= 100) return;
537
- e.preventDefault(); isPanning = true;
538
- img.classList.add('panning');
539
- panStartX = e.clientX; panStartY = e.clientY;
540
- panStartTx = parseFloat(img.dataset.translateX||0);
541
- panStartTy = parseFloat(img.dataset.translateY||0);
542
- }
543
- function panImage(e) {
544
- if(!isPanning || !currentlySelectedPanel) return;
545
- const img = currentlySelectedPanel.querySelector('img');
546
- img.dataset.translateX = panStartTx + (e.clientX - panStartX);
547
- img.dataset.translateY = panStartTy + (e.clientY - panStartY);
548
- updateImageTransform(img);
549
- }
550
- function stopPan() { isPanning = false; currentlySelectedPanel?.querySelector('img')?.classList.remove('panning'); }
551
- function handleZoom(e) {
552
- if(!currentlySelectedPanel) return;
553
- const img = currentlySelectedPanel.querySelector('img');
554
- img.dataset.zoom = e.target.value;
555
- updateImageTransform(img);
556
- }
557
- function updateImageTransform(img) {
558
- const z = (img.dataset.zoom||100)/100;
559
- const x = img.dataset.translateX||0; const y = img.dataset.translateY||0;
560
- img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`;
561
- img.classList.toggle('pannable', z>1);
562
- }
563
- function resetPanelTransform(loadOnly=false) {
564
- if(!currentlySelectedPanel) return;
565
- const img = currentlySelectedPanel.querySelector('img');
566
- if(!loadOnly) {
567
- img.dataset.zoom=100; img.dataset.translateX=0; img.dataset.translateY=0;
568
  }
569
- document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
570
- updateImageTransform(img);
571
  }
572
 
573
- // --- API Calls ---
574
- function replacePanelImage() {
575
- if (!currentlySelectedPanel) return alert("Select panel");
576
- const img = currentlySelectedPanel.querySelector('img');
577
- const inp = document.getElementById('image-uploader');
578
- inp.onchange = async (e) => {
579
- const f = e.target.files[0]; if(!f) return;
580
- const fd = new FormData(); fd.append('image', f);
581
- img.style.opacity=0.5;
582
- const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd});
583
- const d = await r.json();
584
- if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}&t=${Date.now()}`; resetPanelTransform(); }
585
- else alert(d.error);
586
- img.style.opacity=1;
587
- };
588
- inp.click();
589
- }
590
- async function adjustFrame(dir) {
591
- if(!currentlySelectedPanel) return alert("Select panel");
592
- const img = currentlySelectedPanel.querySelector('img');
593
- let fname = img.src.split('/').pop().split('?')[0];
594
- const r = await fetch(`/regenerate_frame?sid=${sid}`, {
595
- method:'POST', headers:{'Content-Type':'application/json'},
596
- body: JSON.stringify({filename:fname, direction:dir})
597
- });
598
- const d = await r.json();
599
- if(d.success) img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`;
600
- else alert(d.message);
601
- }
602
- async function gotoTimestamp() {
603
- if(!currentlySelectedPanel) return alert("Select panel");
604
- const img = currentlySelectedPanel.querySelector('img');
605
- let val = document.getElementById('timestamp-input').value;
606
- if(val.includes(':')) { const p=val.split(':'); val = parseInt(p[0])*60 + parseFloat(p[1]); }
607
- let fname = img.src.split('/').pop().split('?')[0];
608
- const r = await fetch(`/goto_timestamp?sid=${sid}`, {
609
- method:'POST', headers:{'Content-Type':'application/json'},
610
- body: JSON.stringify({filename:fname, timestamp:val})
611
- });
612
- const d = await r.json();
613
- if(d.success) img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`;
614
- else alert(d.message);
615
- }
616
- async function exportPagesToPNG() {
617
- const pgs = document.querySelectorAll('.comic-page');
618
- for(let i=0; i<pgs.length; i++) {
619
- try {
620
- const url = await htmlToImage.toPng(pgs[i], {pixelRatio:3});
621
- const a = document.createElement('a');
622
- a.download = `comic-page-${i+1}.png`; a.href=url; a.click();
623
- } catch(e) { console.error(e); alert("Export failed"); }
624
  }
625
  }
626
  </script>
@@ -628,7 +277,6 @@ INDEX_HTML = '''
628
  </html>
629
  '''
630
 
631
- # --- BACKEND LOGIC ---
632
  class EnhancedComicGenerator:
633
  def __init__(self, sid):
634
  self.sid = sid
@@ -642,57 +290,63 @@ class EnhancedComicGenerator:
642
  os.makedirs(self.output_dir, exist_ok=True)
643
 
644
  self.video_fps = None
645
- self.frame_metadata = {}
646
 
647
  def update_status(self, message, progress):
648
  try:
649
  with open(self.status_file, 'w') as f:
650
  json.dump({'message': message, 'progress': progress}, f)
651
  except: pass
652
-
653
- # --- CLEANUP: Deletes OLD processing files on NEW upload ---
654
  def cleanup_previous_run(self):
655
- print(f"[{self.sid}] 🧹 Cleaning previous run...")
656
- if os.path.exists(self.frames_dir):
657
- for f in os.listdir(self.frames_dir):
658
- try: os.remove(os.path.join(self.frames_dir, f))
659
- except: pass
660
- if os.path.exists(self.output_dir):
661
- for f in os.listdir(self.output_dir):
662
- try: os.remove(os.path.join(self.output_dir, f))
663
- except: pass
664
- srt_file = os.path.join(self.user_dir, 'subs.srt')
665
- if os.path.exists(srt_file): os.remove(srt_file)
 
666
 
667
  def generate_comic(self):
668
  try:
669
- if cv2 is None: raise Exception("OpenCV missing on server.")
 
670
 
671
- self.update_status("Processing Video...", 5)
672
  cap = cv2.VideoCapture(self.video_path)
673
- if not cap.isOpened(): raise Exception("Invalid Video")
 
674
  self.video_fps = cap.get(cv2.CAP_PROP_FPS) or 25
675
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
676
  cap.release()
677
 
678
  # 1. Subtitles
679
- self.update_status("Extracting Dialogue...", 20)
680
  user_srt = os.path.join(self.user_dir, 'subs.srt')
681
  try:
682
- get_real_subtitles(self.video_path)
683
- if os.path.exists('test1.srt'): shutil.move('test1.srt', user_srt)
684
- except:
685
- with open(user_srt, 'w') as f: f.write("1\n00:00:00,000 --> 00:00:05,000\n...\n")
686
-
687
- # 2. Keyframes
688
- self.update_status("Generating Panels...", 40)
689
- with open(user_srt, 'r', encoding='utf-8') as f: subs = list(srt.parse(f.read()))
 
 
 
 
690
 
691
  cap = cv2.VideoCapture(self.video_path)
692
  frame_files = []
693
  bubbles = []
694
 
695
- limit_subs = subs[:12] # Limit to 12 panels for demo
 
696
 
697
  for i, sub in enumerate(limit_subs):
698
  mid = (sub.start.total_seconds() + sub.end.total_seconds()) / 2
@@ -702,24 +356,15 @@ class EnhancedComicGenerator:
702
  fname = f"frame_{i}.png"
703
  cv2.imwrite(os.path.join(self.frames_dir, fname), frame)
704
  frame_files.append(fname)
705
- self.frame_metadata[fname] = mid
706
-
707
  bubbles.append(bubble(
708
  dialog=sub.content,
709
- bubble_offset_x=50, bubble_offset_y=20,
710
  lip_x=-1, lip_y=-1, emotion='normal'
711
  ))
712
  cap.release()
713
 
714
- with open(os.path.join(self.frames_dir, 'frame_metadata.json'), 'w') as f:
715
- json.dump(self.frame_metadata, f)
716
-
717
- # 3. Enhance
718
- self.update_status("Enhancing...", 70)
719
- self._enhance_all_images()
720
- self._enhance_quality_colors()
721
-
722
- # 4. Assemble
723
  self.update_status("Finalizing...", 90)
724
  pages_data = []
725
  for i in range(0, len(frame_files), 4):
@@ -731,80 +376,17 @@ class EnhancedComicGenerator:
731
 
732
  with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
733
  json.dump(pages_data, f)
 
 
 
 
734
 
735
  self.update_status("Done!", 100)
 
736
 
737
  except Exception as e:
738
  traceback.print_exc()
739
- self.update_status(f"Error: {str(e)}", -1)
740
-
741
- # --- Helper Methods for Editor ---
742
- def regenerate_frame(self, fname, direction):
743
- try:
744
- meta_path = os.path.join(self.frames_dir, 'frame_metadata.json')
745
- if not os.path.exists(meta_path): return {"success":False, "message":"No metadata"}
746
- with open(meta_path,'r') as f: meta = json.load(f)
747
-
748
- if fname not in meta: return {"success":False, "message":"Frame not found"}
749
- curr_time = meta[fname]
750
-
751
- if not self.video_fps:
752
- cap = cv2.VideoCapture(self.video_path); self.video_fps = cap.get(cv2.CAP_PROP_FPS); cap.release()
753
-
754
- offset = (1.0/self.video_fps) * (1 if direction=='forward' else -1)
755
- new_time = max(0, curr_time + offset)
756
-
757
- cap = cv2.VideoCapture(self.video_path)
758
- cap.set(cv2.CAP_PROP_POS_MSEC, new_time*1000)
759
- ret, frame = cap.read()
760
- cap.release()
761
-
762
- if ret:
763
- cv2.imwrite(os.path.join(self.frames_dir, fname), frame)
764
- meta[fname] = new_time
765
- with open(meta_path,'w') as f: json.dump(meta, f)
766
- self._enhance_all_images(single_image_path=os.path.join(self.frames_dir, fname))
767
- self._enhance_quality_colors(single_image_path=os.path.join(self.frames_dir, fname))
768
- return {"success":True}
769
- return {"success":False, "message":"End of video"}
770
- except Exception as e: return {"success":False, "message":str(e)}
771
-
772
- def get_frame_at_timestamp(self, fname, ts):
773
- try:
774
- cap = cv2.VideoCapture(self.video_path)
775
- cap.set(cv2.CAP_PROP_POS_MSEC, float(ts)*1000)
776
- ret, frame = cap.read()
777
- cap.release()
778
- if ret:
779
- cv2.imwrite(os.path.join(self.frames_dir, fname), frame)
780
- meta_path = os.path.join(self.frames_dir, 'frame_metadata.json')
781
- if os.path.exists(meta_path):
782
- with open(meta_path,'r') as f: meta = json.load(f)
783
- meta[fname] = float(ts)
784
- with open(meta_path,'w') as f: json.dump(meta, f)
785
- self._enhance_all_images(single_image_path=os.path.join(self.frames_dir, fname))
786
- self._enhance_quality_colors(single_image_path=os.path.join(self.frames_dir, fname))
787
- return {"success":True}
788
- return {"success":False, "message":"Invalid time"}
789
- except Exception as e: return {"success":False, "message":str(e)}
790
-
791
- def _enhance_all_images(self, single_image_path=None):
792
- try:
793
- enhancer = SimpleColorEnhancer()
794
- if single_image_path: enhancer.enhance_single(single_image_path)
795
- else:
796
- paths = [os.path.join(self.frames_dir, f) for f in os.listdir(self.frames_dir) if f.endswith('.png')]
797
- with ThreadPoolExecutor() as ex: list(ex.map(enhancer.enhance_single, paths))
798
- except: pass
799
-
800
- def _enhance_quality_colors(self, single_image_path=None):
801
- try:
802
- enhancer = QualityColorEnhancer()
803
- if single_image_path: enhancer.enhance_single(single_image_path)
804
- else:
805
- paths = [os.path.join(self.frames_dir, f) for f in os.listdir(self.frames_dir) if f.endswith('.png')]
806
- with ThreadPoolExecutor() as ex: list(ex.map(enhancer.enhance_single, paths))
807
- except: pass
808
 
809
  # --- ROUTES ---
810
  @app.route('/')
@@ -813,22 +395,27 @@ def index(): return INDEX_HTML
813
  @app.route('/uploader', methods=['POST'])
814
  def upload():
815
  sid = request.args.get('sid')
816
- if not sid: return "Missing SID", 400
 
 
 
 
817
  f = request.files['file']
818
- gen = EnhancedComicGenerator(sid)
819
 
820
- # CLEAN OLD FILES
821
  gen.cleanup_previous_run()
822
-
823
  f.save(gen.video_path)
 
824
  gen.update_status("Starting...", 5)
825
  threading.Thread(target=gen.generate_comic).start()
 
826
  return jsonify({'success': True})
827
 
828
  @app.route('/status')
829
  def get_status():
830
  sid = request.args.get('sid')
831
  if not sid: return jsonify({'progress': -1, 'message': "No SID"})
 
832
  path = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
833
  if os.path.exists(path): return send_file(path)
834
  return jsonify({'progress': 0, 'message': "Waiting..."})
@@ -843,28 +430,8 @@ def get_frame(filename):
843
  sid = request.args.get('sid')
844
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
845
 
846
- @app.route('/regenerate_frame', methods=['POST'])
847
- def regen_frame():
848
- sid = request.args.get('sid')
849
- d = request.get_json()
850
- return jsonify(EnhancedComicGenerator(sid).regenerate_frame(d['filename'], d['direction']))
851
-
852
- @app.route('/goto_timestamp', methods=['POST'])
853
- def go_time():
854
- sid = request.args.get('sid')
855
- d = request.get_json()
856
- return jsonify(EnhancedComicGenerator(sid).get_frame_at_timestamp(d['filename'], d['timestamp']))
857
-
858
- @app.route('/replace_panel', methods=['POST'])
859
- def rep_panel():
860
- sid = request.args.get('sid')
861
- f = request.files['image']
862
- gen = EnhancedComicGenerator(sid)
863
- fname = f"replaced_{int(time.time())}.png"
864
- f.save(os.path.join(gen.frames_dir, fname))
865
- return jsonify({'success': True, 'new_filename': fname})
866
-
867
  if __name__ == '__main__':
868
  os.makedirs(BASE_USER_DIR, exist_ok=True)
869
  port = int(os.getenv("PORT", 7860))
 
870
  app.run(host='0.0.0.0', port=port)
 
9
  from concurrent.futures import ThreadPoolExecutor
10
  from flask import Flask, render_template, request, jsonify, send_from_directory, send_file
11
 
12
+ # --- 0. LOGGING SETUP ---
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
 
 
15
 
16
  # --- 1. CORE DEPENDENCY CHECKS ---
17
  try:
 
19
  import numpy as np
20
  from PIL import Image
21
  import srt
22
+ print("✅ OpenCV, Numpy, Pillow, SRT loaded successfully.")
23
  except ImportError as e:
24
  print(f"❌ CRITICAL ERROR: Missing python library. {e}")
25
+ print("⚠️ Please ensure 'opencv-python-headless' is in requirements.txt")
26
+ cv2 = None; np = None; Image = None; srt = None
 
 
27
 
28
+ # --- 2. BACKEND IMPORTS WITH DUMMIES ---
29
+ def dummy_func(*args, **kwargs): return 0, 0, None, None
30
 
31
  try:
32
  from backend.keyframes.keyframes import black_bar_crop
33
+ except:
34
+ black_bar_crop = dummy_func
 
35
 
36
  try:
37
  from backend.simple_color_enhancer import SimpleColorEnhancer
38
+ from backend.quality_color_enhancer import QualityColorEnhancer
39
+ except:
40
  class SimpleColorEnhancer:
41
  def enhance_single(self, *args): pass
 
 
 
 
 
42
  class QualityColorEnhancer:
43
  def enhance_single(self, *args): pass
44
 
45
  try:
46
  from backend.class_def import bubble, panel, Page
47
+ except:
48
+ def bubble(**kwargs): return kwargs
49
+ def panel(**kwargs): return kwargs
 
 
 
 
 
50
  class Page:
51
  def __init__(self, panels, bubbles): self.panels, self.bubbles = panels, bubbles
52
 
53
  try:
54
+ from backend.ai_enhanced_core import image_processor, face_detector
55
  from backend.ai_bubble_placement import ai_bubble_placer
56
  from backend.subtitles.subs_real import get_real_subtitles
57
+ except:
 
 
58
  def get_real_subtitles(v): pass
59
  class DummyDetector:
60
  def detect_faces(self, p): return []
 
64
  def place_bubble_ai(self, p, l): return 50, 20
65
  ai_bubble_placer = DummyPlacer()
66
 
67
+ # --- FLASK SETUP ---
 
68
  app = Flask(__name__)
69
  BASE_USER_DIR = "userdata"
70
 
 
71
  INDEX_HTML = '''
72
  <!DOCTYPE html>
73
  <html lang="en">
 
78
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
79
  <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@700&family=Lato&display=swap" rel="stylesheet">
80
  <style>
 
81
  body { background-color: #fdf6e3; font-family: 'Lato', sans-serif; color: #3d3d3d; margin: 0; min-height: 100vh; }
 
 
82
  #upload-container { display: flex; justify-content: center; align-items: center; min-height: 100vh; width: 100%; }
83
  .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; }
 
 
 
 
 
 
 
 
 
 
 
 
84
  #editor-container { display: none; padding: 20px; width: 100%; box-sizing: border-box; }
85
+ .file-label { display: block; padding: 15px; background: #2c3e50; color: white; border-radius: 8px; cursor: pointer; margin-bottom: 10px; }
86
+ .submit-btn { width: 100%; padding: 15px; background: #e67e22; color: white; border: none; border-radius: 8px; font-size: 18px; cursor: pointer; font-weight: bold; }
87
+ .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; }
88
+ @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
89
 
90
+ /* COMIC STYLES */
91
+ .comic-page { background: white; width: 600px; height: 400px; position: relative; overflow: hidden; border: 2px solid #000; margin: 0 auto 20px; }
92
+ .comic-grid { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 10px; width: 100%; height: 100%; padding: 10px; box-sizing: border-box; }
93
+ .panel { position: relative; overflow: hidden; border: 2px solid #000; background: #eee; }
94
+ .panel img { width: 100%; height: 100%; object-fit: cover; }
95
 
96
+ /* EXACT USER CSS FOR SPEECH BUBBLE (EXPORT SAFE) */
 
 
 
 
 
 
 
 
 
 
 
97
  .speech-bubble.speech {
98
+ --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
99
+ --c: var(--bubble-fill-color, #4ECDC4);
100
+ background: var(--c); color: var(--bubble-text-color, #fff); padding: 1em; position: absolute;
 
 
 
 
 
 
 
 
101
  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);
102
+ font-family: 'Comic Neue', cursive; font-weight: bold; font-size: 14px; text-align: center;
103
+ min-width: 80px; min-height: 40px; display: flex; align-items: center; justify-content: center;
104
+ cursor: move; z-index: 10;
105
  }
 
106
  .speech-bubble.speech:before {
107
  content: ""; position: absolute; width: var(--b); height: var(--h);
108
  background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
109
  border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
110
  }
 
 
111
  .speech-bubble.speech.tail-bottom:before { top: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
 
 
112
  .speech-bubble.speech.tail-top:before { bottom: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: scaleY(-1); }
 
 
113
  .speech-bubble.speech.tail-left:before { right: 99%; 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; }
 
 
114
  .speech-bubble.speech.tail-right:before { left: 99%; 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; }
115
 
116
+ .speech-bubble.selected { outline: 2px dashed #333; }
117
+ .edit-controls { position: fixed; bottom: 20px; right: 20px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.2); width: 200px; }
118
+ .edit-controls button { width: 100%; margin-top: 5px; padding: 8px; cursor: pointer; }
119
+ .slider-container { margin-top:10px; }
120
+ .slider-container input { width: 100%; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  </style>
122
  </head>
123
  <body>
 
124
  <div id="upload-container">
125
  <div class="upload-box">
126
+ <h1>🎬 Comic Generator</h1>
127
+ <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fname').innerText=this.files[0].name">
128
+ <label for="file-upload" class="file-label">Choose Video</label>
129
+ <span id="fname">No file</span>
130
+ <button class="submit-btn" onclick="upload()">Generate</button>
131
+ <div id="loading" style="display:none;">
 
 
132
  <div class="loader"></div>
133
+ <p id="status">Starting...</p>
134
  </div>
135
  </div>
136
  </div>
137
 
 
138
  <div id="editor-container">
139
+ <div id="comic-pages"></div>
 
 
 
 
 
 
140
  <div class="edit-controls">
141
+ <h4>Editor</h4>
142
+ <label>Tail Pos:</label>
143
+ <div class="slider-container"><input type="range" id="tail-slider" min="10" max="90" value="50" oninput="slideTail(this.value)"></div>
144
+ <button onclick="exportToPng()">💾 Export PNG</button>
145
+ <button onclick="location.reload()" style="color:red; margin-top:10px;">↺ Reset</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  </div>
147
  </div>
148
 
149
  <script>
150
+ // --- SESSION MANAGEMENT ---
151
  function generateUUID() {
152
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
153
  var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
 
158
  if(!sid) { sid = generateUUID(); localStorage.setItem('comic_sid', sid); }
159
  console.log("SID:", sid);
160
 
161
+ let interval;
162
+ let currentlySelectedBubble = null;
 
 
 
163
 
164
+ async function upload() {
 
 
165
  const file = document.getElementById('file-upload').files[0];
166
+ if(!file) return alert("Select file");
167
 
168
+ const fd = new FormData();
169
+ fd.append('file', file);
170
 
171
+ document.querySelector('.upload-box').style.display='none';
172
+ document.getElementById('loading').style.display='block';
173
 
174
  try {
175
  const res = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
176
+ if(res.ok) {
177
+ interval = setInterval(checkStatus, 2000);
178
+ } else {
179
+ const err = await res.json();
180
+ alert("Upload Error: " + (err.message || "Unknown"));
181
+ location.reload();
182
+ }
183
+ } catch(e) {
184
+ console.error(e);
185
+ alert("Connection Failed. Check logs.");
186
+ }
187
+ }
188
 
189
  async function checkStatus() {
190
  try {
191
  const res = await fetch(`/status?sid=${sid}`);
192
  const data = await res.json();
193
+ document.getElementById('status').innerText = data.message;
194
 
195
  if(data.progress >= 100) {
196
+ clearInterval(interval);
197
+ document.getElementById('upload-container').style.display='none';
198
+ document.getElementById('editor-container').style.display='block';
199
  loadComic();
200
  } else if(data.progress < 0) {
201
+ clearInterval(interval);
202
+ alert("Generation Error: " + data.message);
203
  location.reload();
204
  }
205
+ } catch(e) { console.error("Status check failed", e); }
206
  }
207
 
 
208
  function loadComic() {
209
  fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
210
+ const c = document.getElementById('comic-pages');
211
+ data.forEach((p, i) => {
212
+ const div = document.createElement('div');
213
+ div.className = 'comic-page';
214
+ div.id = 'page-'+i;
 
 
215
  const grid = document.createElement('div');
216
  grid.className = 'comic-grid';
217
 
218
+ p.panels.forEach((pan, j) => {
219
  const pDiv = document.createElement('div');
220
  pDiv.className = 'panel';
 
 
221
  const img = document.createElement('img');
222
+ img.src = `/frames/${pan.image}?sid=${sid}`;
 
223
  pDiv.appendChild(img);
224
 
225
+ if(p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) {
226
+ const b = document.createElement('div');
227
+ b.className = 'speech-bubble speech tail-bottom';
228
+ b.innerText = p.bubbles[j].dialog;
229
+ b.style.left = (p.bubbles[j].bubble_offset_x || 50) + 'px';
230
+ b.style.top = (p.bubbles[j].bubble_offset_y || 20) + 'px';
 
231
  pDiv.appendChild(b);
232
+ makeInteractive(b);
233
  }
234
  grid.appendChild(pDiv);
235
  });
236
+ div.appendChild(grid);
237
+ c.appendChild(div);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  });
 
 
 
239
  });
240
  }
241
 
242
+ function makeInteractive(el) {
243
+ el.onmousedown = function(e) {
244
+ e.stopPropagation();
245
+ currentlySelectedBubble = el;
246
+ document.querySelectorAll('.speech-bubble').forEach(b => b.classList.remove('selected'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  el.classList.add('selected');
248
+
249
+ const offX = e.clientX - el.offsetLeft;
250
+ const offY = e.clientY - el.offsetTop;
251
+ document.onmousemove = function(ev) {
252
+ el.style.left = (ev.clientX - offX) + 'px';
253
+ el.style.top = (ev.clientY - offY) + 'px';
254
+ }
255
+ document.onmouseup = function() { document.onmousemove = null; }
256
+ };
 
 
 
257
  }
258
 
259
+ function slideTail(val) {
260
+ if(currentlySelectedBubble) {
261
+ currentlySelectedBubble.style.setProperty('--tail-pos', val + '%');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  }
 
 
263
  }
264
 
265
+ async function exportToPng() {
266
+ const pages = document.querySelectorAll('.comic-page');
267
+ for(let p of pages) {
268
+ const url = await htmlToImage.toPng(p, {pixelRatio: 2});
269
+ const a = document.createElement('a');
270
+ a.download = 'comic.png';
271
+ a.href = url;
272
+ a.click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  }
274
  }
275
  </script>
 
277
  </html>
278
  '''
279
 
 
280
  class EnhancedComicGenerator:
281
  def __init__(self, sid):
282
  self.sid = sid
 
290
  os.makedirs(self.output_dir, exist_ok=True)
291
 
292
  self.video_fps = None
 
293
 
294
  def update_status(self, message, progress):
295
  try:
296
  with open(self.status_file, 'w') as f:
297
  json.dump({'message': message, 'progress': progress}, f)
298
  except: pass
299
+
 
300
  def cleanup_previous_run(self):
301
+ # Remove old frames/output files but keep directories
302
+ for folder in [self.frames_dir, self.output_dir]:
303
+ if os.path.exists(folder):
304
+ for filename in os.listdir(folder):
305
+ file_path = os.path.join(folder, filename)
306
+ try:
307
+ if os.path.isfile(file_path) or os.path.islink(file_path):
308
+ os.unlink(file_path)
309
+ elif os.path.isdir(file_path):
310
+ shutil.rmtree(file_path)
311
+ except Exception as e:
312
+ print(f'Failed to delete {file_path}. Reason: {e}')
313
 
314
  def generate_comic(self):
315
  try:
316
+ print(f"[{self.sid}] Generation thread started.")
317
+ if cv2 is None: raise Exception("OpenCV not installed on server.")
318
 
319
+ self.update_status("Processing Video...", 10)
320
  cap = cv2.VideoCapture(self.video_path)
321
+ if not cap.isOpened(): raise Exception("Invalid Video File")
322
+
323
  self.video_fps = cap.get(cv2.CAP_PROP_FPS) or 25
324
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
325
  cap.release()
326
 
327
  # 1. Subtitles
328
+ self.update_status("Extracting Dialogue...", 30)
329
  user_srt = os.path.join(self.user_dir, 'subs.srt')
330
  try:
331
+ get_real_subtitles(self.video_path) # Should produce test1.srt
332
+ if os.path.exists('test1.srt'):
333
+ shutil.move('test1.srt', user_srt)
334
+ except Exception as e:
335
+ print(f"Subtitle generation warning: {e}")
336
+ if not os.path.exists(user_srt):
337
+ with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:04,000\nComic Scene 1\n")
338
+
339
+ # 2. Extract Frames
340
+ self.update_status("Generating Panels...", 50)
341
+ with open(user_srt, 'r', encoding='utf-8') as f:
342
+ subs = list(srt.parse(f.read()))
343
 
344
  cap = cv2.VideoCapture(self.video_path)
345
  frame_files = []
346
  bubbles = []
347
 
348
+ # Simple sampling logic
349
+ limit_subs = subs[:12]
350
 
351
  for i, sub in enumerate(limit_subs):
352
  mid = (sub.start.total_seconds() + sub.end.total_seconds()) / 2
 
356
  fname = f"frame_{i}.png"
357
  cv2.imwrite(os.path.join(self.frames_dir, fname), frame)
358
  frame_files.append(fname)
359
+ # Pass all necessary args to prevent TypeError
 
360
  bubbles.append(bubble(
361
  dialog=sub.content,
362
+ bubble_offset_x=50, bubble_offset_y=20,
363
  lip_x=-1, lip_y=-1, emotion='normal'
364
  ))
365
  cap.release()
366
 
367
+ # 3. Assemble
 
 
 
 
 
 
 
 
368
  self.update_status("Finalizing...", 90)
369
  pages_data = []
370
  for i in range(0, len(frame_files), 4):
 
376
 
377
  with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
378
  json.dump(pages_data, f)
379
+
380
+ # Also save the HTML template for debug purposes
381
+ with open(os.path.join(self.output_dir, 'page.html'), 'w') as f:
382
+ f.write(INDEX_HTML)
383
 
384
  self.update_status("Done!", 100)
385
+ print(f"[{self.sid}] Generation Complete.")
386
 
387
  except Exception as e:
388
  traceback.print_exc()
389
+ self.update_status(f"Failed: {str(e)}", -1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
  # --- ROUTES ---
392
  @app.route('/')
 
395
  @app.route('/uploader', methods=['POST'])
396
  def upload():
397
  sid = request.args.get('sid')
398
+ if not sid: return jsonify({'message': 'No SID provided'}), 400
399
+
400
+ print(f"[SERVER] Upload received for SID: {sid}")
401
+
402
+ if 'file' not in request.files: return jsonify({'message': 'No file part'}), 400
403
  f = request.files['file']
 
404
 
405
+ gen = EnhancedComicGenerator(sid)
406
  gen.cleanup_previous_run()
 
407
  f.save(gen.video_path)
408
+
409
  gen.update_status("Starting...", 5)
410
  threading.Thread(target=gen.generate_comic).start()
411
+
412
  return jsonify({'success': True})
413
 
414
  @app.route('/status')
415
  def get_status():
416
  sid = request.args.get('sid')
417
  if not sid: return jsonify({'progress': -1, 'message': "No SID"})
418
+
419
  path = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
420
  if os.path.exists(path): return send_file(path)
421
  return jsonify({'progress': 0, 'message': "Waiting..."})
 
430
  sid = request.args.get('sid')
431
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  if __name__ == '__main__':
434
  os.makedirs(BASE_USER_DIR, exist_ok=True)
435
  port = int(os.getenv("PORT", 7860))
436
+ print(f"🚀 Server starting on port {port}")
437
  app.run(host='0.0.0.0', port=port)