tester343 commited on
Commit
2918181
·
verified ·
1 Parent(s): a78e278

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +230 -896
app_enhanced.py CHANGED
@@ -6,74 +6,79 @@ import shutil
6
  import json
7
  import traceback
8
  from concurrent.futures import ThreadPoolExecutor
 
9
 
10
- # --- ESSENTIAL IMPORTS ---
11
- # These must be at the top. If they fail, add them to requirements.txt
12
  try:
13
  import cv2
14
  import numpy as np
15
  from PIL import Image
16
  import srt
17
- from flask import Flask, render_template, request, jsonify, send_from_directory, send_file, session
18
  except ImportError as e:
19
- print(f"❌ Critical Import Error: {e}")
20
- print("Please ensure 'flask', 'opencv-python-headless', 'numpy', 'pillow', and 'srt' are in requirements.txt")
21
- exit(1)
 
 
 
 
 
 
 
 
 
22
 
23
- # --- ROBUST IMPORTS WITH FALLBACKS ---
24
  try:
25
  from backend.keyframes.keyframes import black_bar_crop
26
- print("✅ Black bar cropping module loaded.")
27
- except Exception as e:
28
- print(f"⚠️ Could not load black_bar_crop: {e}. Cropping will be SKIPPED.")
29
- def black_bar_crop():
30
- return 0, 0, None, None
31
 
32
  try:
33
  from backend.simple_color_enhancer import SimpleColorEnhancer
34
- print("✅ SimpleColorEnhancer loaded.")
35
- except Exception as e:
36
- print(f"⚠️ Could not load SimpleColorEnhancer: {e}. This feature will be SKIPPED.")
37
  class SimpleColorEnhancer:
38
- def enhance_batch(self, *args, **kwargs): pass
39
- def enhance_single(self, *args, **kwargs): pass
40
 
41
  try:
42
  from backend.quality_color_enhancer import QualityColorEnhancer
43
- print("✅ QualityColorEnhancer loaded.")
44
- except Exception as e:
45
- print(f"⚠️ Could not load QualityColorEnhancer: {e}. This feature will be SKIPPED.")
46
  class QualityColorEnhancer:
47
- def batch_enhance(self, *args, **kwargs): pass
48
- def enhance_single(self, *args, **kwargs): pass
49
 
50
  try:
51
  from backend.class_def import bubble, panel, Page
52
- print("✅ Core class definitions (bubble, panel, Page) loaded.")
53
- except Exception as e:
54
- print(f"⚠️ CRITICAL: Could not load core class definitions: {e}. Using fallback definitions.")
55
  def bubble(**kwargs): return kwargs
56
  def panel(**kwargs): return kwargs
57
  class Page:
58
- def __init__(self, panels, bubbles):
59
- self.panels = panels
60
- self.bubbles = bubbles
61
 
62
  try:
63
- from backend.ai_enhanced_core import image_processor, comic_styler, face_detector, layout_optimizer
64
  from backend.ai_bubble_placement import ai_bubble_placer
65
  from backend.subtitles.subs_real import get_real_subtitles
66
- from backend.keyframes.keyframes_simple import generate_keyframes_simple
67
- print("✅ Core utility modules loaded.")
68
  except Exception as e:
69
- print(f"⚠️ Could not load a core utility module: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  # --- FLASK APP SETUP ---
72
  app = Flask(__name__)
73
- app.secret_key = "HUGGINGFACE_SECRET_KEY_XYZ" # Necessary for sessions
74
- BASE_USER_DIR = "userdata" # Persistent storage location
75
 
76
- # --- MERGED HTML: UPLOAD UI + EDITOR UI + EXPORT FIX + CSS FIX ---
77
  INDEX_HTML = '''
78
  <!DOCTYPE html>
79
  <html lang="en">
@@ -81,605 +86,161 @@ INDEX_HTML = '''
81
  <meta charset="UTF-8">
82
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
83
  <title>Movie to Comic Generator</title>
84
- <!-- EXPORT LIBRARY (Supports CSS Masks/Gradients) -->
85
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
86
- <link rel="preconnect" href="https://fonts.googleapis.com">
87
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
88
- <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@700&family=Gloria+Hallelujah&family=Lato&display=swap" rel="stylesheet">
89
  <style>
90
- /* --- GLOBAL & UPLOAD STYLES --- */
91
- body {
92
- background-color: #fdf6e3;
93
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
94
- color: #3d3d3d;
95
- margin: 0;
96
- min-height: 100vh;
97
- }
98
-
99
- #upload-container {
100
- display: flex; justify-content: center; align-items: center;
101
- min-height: 100vh; width: 100%;
102
- }
103
-
104
- .upload-box {
105
- max-width: 500px; width: 100%; padding: 40px;
106
- background-color: #ffffff; border-radius: 12px;
107
- box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); text-align: center;
108
- }
109
-
110
- #editor-container {
111
- display: none; /* Hidden by default */
112
- padding: 20px; width: 100%; box-sizing: border-box;
113
- }
114
-
115
- h1 { color: #2c3e50; margin-bottom: 30px; font-weight: 600; }
116
- .file-input { display: none; }
117
- .file-label {
118
- display: block; padding: 15px 25px; background-color: #2c3e50; color: white;
119
- border-radius: 8px; cursor: pointer; font-size: 16px; font-weight: 500;
120
- transition: background-color 0.3s ease, transform 0.2s ease;
121
- }
122
- .file-label:hover { background-color: #34495e; transform: translateY(-2px); }
123
- #file-name { display: block; margin-top: 15px; font-style: italic; color: #7f8c8d; }
124
- .submit-btn {
125
- width: 100%; padding: 15px; border: none; border-radius: 8px;
126
- background-color: #e67e22; color: white; font-size: 18px; font-weight: bold;
127
- cursor: pointer; transition: background-color 0.3s ease; margin-top: 20px;
128
- }
129
- .submit-btn:hover { background-color: #d35400; }
130
 
131
- .loading-view { display: none; flex-direction: column; align-items: center; justify-content: center; }
132
- .loader {
133
- width: 120px; height: 20px; border-radius: 20px;
134
- background: radial-gradient(circle 10px, #e67e22 100%, transparent 0),
135
- radial-gradient(circle 10px, #e67e22 100%, transparent 0),
136
- radial-gradient(circle 10px, #e67e22 100%, transparent 0);
137
- background-size: 20px 20px; background-position: 0px 50%, 50px 50%, 100px 50%;
138
- background-repeat: no-repeat; animation:-ball 2s infinite linear;
139
- }
140
- @keyframes -ball {
141
- 0% {background-position: 0px 50% ,50px 50% ,100px 50%}
142
- 16% {background-position: 0px 0% ,50px 50% ,100px 50%}
143
- 33% {background-position: 0px 100% ,50px 0% ,100px 50%}
144
- 50% {background-position: 0px 50% ,50px 100% ,100px 0%}
145
- 66% {background-position: 0px 50% ,50px 50% ,100px 100%}
146
- 83% {background-position: 0px 50% ,50px 50% ,100px 50%}
147
- 100% {background-position: 0px 50% ,50px 50% ,100px 50%}
148
- }
149
- #status-text { margin-top: 25px; color: #34495e; font-weight: 500; font-size: 18px; }
150
-
151
- /* --- COMIC EDITOR STYLES --- */
152
- .comic-container-wrapper { max-width: 1200px; margin: 0 auto; }
153
- .comic-page { background: white; width: 600px; height: 400px; box-shadow: 0 0 10px rgba(0,0,0,0.1); box-sizing: content-box; position: relative; overflow: hidden; border: 1px solid #333; padding: 10px; }
154
- .comic-grid { display: grid; grid-template-columns: 285px 285px; grid-template-rows: 185px 185px; gap: 10px; width: 100%; height: 100%; }
155
- .page-wrapper { margin: 30px auto; width: 622px; display: flex; flex-direction: column; align-items: center; }
156
- .page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
157
 
158
- .panel { position: relative; overflow: hidden; width: 100%; height: 100%; box-sizing: border-box; cursor: pointer; border: 1px solid #333; }
159
- .panel.selected { outline: 3px solid #2196F3; outline-offset: -3px; }
160
- .panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; transition: transform 0.1s ease-out; }
161
- .panel img.pannable { cursor: grab; }
162
- .panel img.panning { cursor: grabbing; }
163
-
164
- .speech-bubble {
165
- position: absolute; display: flex; justify-content: center; align-items: center; width: 150px; height: 80px;
166
- min-width: 50px; min-height: 30px; box-sizing: border-box; z-index: 10; cursor: move; overflow: visible;
167
- font-size: 13px; font-weight: bold; text-align: center; font-family: 'Comic Neue', cursive;
168
- }
169
- .bubble-text { padding: 0.8em; word-wrap: break-word; position: relative; z-index: 5; }
170
- .speech-bubble.selected { outline: 2px dashed #4CAF50; }
171
- .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; }
172
-
173
- /* <<< EXACT CSS WITH GRADIENT EXPORT FIX >>> */
174
  .speech-bubble.speech {
175
  --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
176
  --c: var(--bubble-fill-color, #4ECDC4);
177
  background: var(--c); color: var(--bubble-text-color, #fff); padding: 1em; position: absolute;
178
  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);
 
 
 
179
  }
180
  .speech-bubble.speech:before {
181
  content: ""; position: absolute; width: var(--b); height: var(--h);
182
  background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
183
  border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
184
  }
185
- /* Directional Logic */
186
  .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))); }
187
- .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); }
188
- .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); }
189
- .speech-bubble.speech.tail-left { border-radius: var(--r); }
190
- .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; }
191
- .speech-bubble.speech.tail-right { border-radius: var(--r); }
192
- .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; }
193
-
194
- .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
195
- .speech-bubble.thought::after { display: none; }
196
- .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
197
- .thought-dot-1 { width: 20px; height: 20px; bottom: -20px; left: 15px; }
198
- .thought-dot-2 { width: 12px; height: 12px; bottom: -32px; left: 5px; }
199
- .speech-bubble.flipped .thought-dot-1 { left: auto; right: 15px; }
200
- .speech-bubble.flipped .thought-dot-2 { left: auto; right: 5px; }
201
- .speech-bubble.flipped-vertical .thought-dot-1 { bottom: auto; top: -20px; }
202
- .speech-bubble.flipped-vertical .thought-dot-2 { bottom: auto; top: -32px; }
203
-
204
- .resize-handle { position: absolute; width: 10px; height: 10px; background: #2196F3; border: 1px solid white; border-radius: 50%; display: none; z-index: 11; }
205
- .speech-bubble.selected .resize-handle { display: block; }
206
- .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
207
- .resize-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
208
- .resize-handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
209
- .resize-handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
210
 
211
- .edit-controls { position: fixed; bottom: 20px; right: 20px; background: rgba(44, 62, 80, 0.9); color: white; padding: 10px 15px; border-radius: 8px; font-size: 13px; z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 220px; }
212
- .edit-controls h4 { margin: 0 0 10px 0; color: #26a69a; text-align: center; }
213
- .edit-controls button, .edit-controls select, .edit-controls input { margin-top: 5px; padding: 6px 8px; font-size: 12px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; width: 100%; box-sizing: border-box; }
214
- .edit-controls .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
215
- .edit-controls .reset-button { background-color: #e74c3c; }
216
- .edit-controls .action-button { background-color: #4CAF50; }
217
- .edit-controls .secondary-button { background-color: #f39c12; }
218
- .button-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
219
- .zoom-controls { display: grid; grid-template-columns: auto 1fr; gap: 5px; align-items: center;}
220
- .timestamp-controls { display: grid; grid-template-columns: 1fr auto; gap: 5px; }
221
- .timestamp-controls input { color: #333; font-weight: normal; }
222
- .color-picker-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
223
- .color-picker-grid div { text-align: center; }
224
- .color-picker-grid label { font-size: 11px; }
225
- .color-picker-grid input[type="color"] { height: 25px; padding: 2px; }
226
- .slider-container { display: flex; align-items: center; gap: 5px; margin-top: 5px;}
227
- .slider-container label { font-size: 11px; min-width: 50px;}
228
  </style>
229
  </head>
230
  <body>
231
- <!-- UPLOAD VIEW -->
232
  <div id="upload-container">
233
- <div class="upload-box" id="upload-view">
234
- <h1>🎬 Movie to Comic Generator</h1>
235
- <form id="upload-form">
236
- <label for="file-upload" class="file-label">Choose Video File</label>
237
- <input id="file-upload" class="file-input" type="file" name="file" onchange="updateFileName(this)">
238
- <span id="file-name">No file selected</span>
239
- <button class="submit-btn" type="submit">Generate Comic</button>
240
- </form>
241
- </div>
242
- <div class="loading-view" id="loading-view">
243
- <div class="loader"></div>
244
- <p id="status-text">Starting...</p>
245
  </div>
246
  </div>
247
 
248
- <!-- EDITOR VIEW (Initially Hidden) -->
249
  <div id="editor-container">
250
- <div class="comic-container-wrapper">
251
- <h1 class="comic-title" style="text-align:center;">🎬 Generated Comic</h1>
252
- <div id="comic-pages"><div class="loading">Loading comic...</div></div>
253
- </div>
254
-
255
- <input type="file" id="image-uploader" style="display: none;" accept="image/*">
256
-
257
  <div class="edit-controls">
258
- <h4>✏️ Interactive Editor</h4>
259
- <div class="control-group">
260
- <label>Bubble Tools:</label>
261
- <select id="bubble-type-select" onchange="changeBubbleType(this.value)" disabled>
262
- <option value="speech">Speech</option><option value="thought">Thought</option>
263
- </select>
264
- <div class="color-picker-grid">
265
- <div><label>Text</label><input type="color" id="bubble-text-color" value="#ffffff" disabled></div>
266
- <div><label>Fill</label><input type="color" id="bubble-fill-color" value="#4ECDC4" disabled></div>
267
- </div>
268
- <button onclick="addBubbleToPanel()" class="action-button">💬 Add Bubble</button>
269
- <button onclick="deleteBubble()" class="reset-button">🗑️ Delete Bubble</button>
270
- </div>
271
- <div class="control-group" id="tail-controls" style="display: none;">
272
- <label>Tail Adjustment</label>
273
- <button onclick="rotateBubbleTail()" class="secondary-button">🔄 Rotate Side</button>
274
- <div class="slider-container">
275
- <label>Pos:</label>
276
- <input type="range" id="tail-slider" min="10" max="90" value="50" oninput="slideTail(this.value)">
277
- </div>
278
- </div>
279
- <div class="control-group">
280
- <label>Panel Tools (Select Panel):</label>
281
- <button onclick="replacePanelImage()" class="action-button">🖼️ Replace Image</button>
282
- <div class="button-grid">
283
- <button onclick="adjustFrame('backward')" class="secondary-button">⬅️ Prev</button>
284
- <button onclick="adjustFrame('forward')" class="action-button">Next ➡️</button>
285
- </div>
286
- <div class="timestamp-controls">
287
- <input type="text" id="timestamp-input" placeholder="mm:ss or secs">
288
- <button onclick="gotoTimestamp()" class="action-button">Go</button>
289
- </div>
290
- </div>
291
- <div class="control-group">
292
- <label>Zoom & Pan (Select Panel):</label>
293
- <div class="zoom-controls">
294
- <button onclick="resetPanelTransform()" class="secondary-button" style="padding: 4px 6px;">Reset</button>
295
- <input type="range" id="zoom-slider" min="100" max="300" value="100" step="5" disabled>
296
- </div>
297
- </div>
298
- <div class="control-group">
299
- <button onclick="exportPagesToPNG()" class="action-button" style="background-color: #2196F3;">🖨️ Export Pages</button>
300
- </div>
301
  </div>
302
  </div>
303
 
304
  <script>
305
- let statusInterval;
306
- let currentlyEditing = null, draggedBubble = null, offset = {x: 0, y: 0};
307
- let currentlySelectedBubble = null;
308
- let currentlySelectedPanel = null;
309
- let isPanning = false, panStartX, panStartY, panStartTranslateX, panStartTranslateY;
310
- let isResizing = false, resizeHandle, originalWidth, originalHeight, originalX, originalY, originalMouseX, originalMouseY;
311
-
312
- function updateFileName(input) {
313
- const fileNameSpan = document.getElementById('file-name');
314
- if (input.files && input.files.length > 0) {
315
- fileNameSpan.textContent = input.files[0].name;
316
- } else {
317
- fileNameSpan.textContent = 'No file selected';
318
- }
319
  }
320
 
321
  async function checkStatus() {
322
- try {
323
- const response = await fetch('/status');
324
- const data = await response.json();
325
- const statusText = document.getElementById('status-text');
326
- statusText.textContent = data.message;
327
-
328
- if (data.progress >= 100) {
329
- clearInterval(statusInterval);
330
- // SWITCH TO EDITOR VIEW
331
- document.getElementById('upload-container').style.display = 'none';
332
- document.getElementById('editor-container').style.display = 'block';
333
- loadComicData();
334
- } else if (data.progress < 0) {
335
- clearInterval(statusInterval);
336
- statusText.textContent = "An error occurred. Check server logs.";
337
- statusText.style.color = '#e74c3c';
338
- document.querySelector('.loader').style.display = 'none';
339
- }
340
- } catch (error) {
341
- console.error("Error fetching status:", error);
342
- }
343
- }
344
-
345
- document.getElementById('upload-form').addEventListener('submit', async function(event) {
346
- event.preventDefault();
347
- const fileInput = document.getElementById('file-upload');
348
- if (!fileInput.files || fileInput.files.length === 0) {
349
- alert("Please select a video file first.");
350
- return;
351
- }
352
- document.getElementById('upload-view').style.display = 'none';
353
- document.getElementById('loading-view').style.display = 'flex';
354
- const formData = new FormData();
355
- formData.append('file', fileInput.files[0]);
356
- try {
357
- const response = await fetch('/uploader', { method: 'POST', body: formData });
358
- if (!response.ok) throw new Error('Upload failed!');
359
- statusInterval = setInterval(checkStatus, 2000);
360
- } catch (error) {
361
- console.error("Upload error:", error);
362
- document.getElementById('status-text').textContent = "Failed to start generation.";
363
  }
364
- });
365
-
366
- // --- EDITOR LOGIC ---
367
- function loadComicData() {
368
- fetch('output/pages.json')
369
- .then(res => res.ok ? res.json() : Promise.reject(new Error('Failed')))
370
- .then(data => { renderComic(data); initializeEditor(); })
371
- .catch(err => { document.getElementById('comic-pages').innerHTML = `<div class="loading">Error: ${err.message}</div>`; });
372
  }
373
 
374
- function renderComic(data) {
375
- const container = document.getElementById('comic-pages');
376
- container.innerHTML = '';
377
- data.forEach((pageData, pageIndex) => {
378
- const pageWrapper = document.createElement('div');
379
- pageWrapper.className = 'page-wrapper';
380
- const pageTitleEl = document.createElement('h2');
381
- pageTitleEl.textContent = `Page ${pageIndex + 1}`;
382
- pageWrapper.appendChild(pageTitleEl);
383
- const pageDiv = document.createElement('div');
384
- pageDiv.className = 'comic-page';
385
- const grid = document.createElement('div');
386
- grid.className = 'comic-grid';
387
- pageData.panels.forEach((panelData, panelIndex) => {
388
- const panelDiv = document.createElement('div');
389
- panelDiv.className = 'panel';
390
- const img = document.createElement('img');
391
- img.src = 'frames/' + panelData.image;
392
- panelDiv.appendChild(img);
393
- if (pageData.bubbles && pageData.bubbles[panelIndex] && pageData.bubbles[panelIndex].dialog) {
394
- const bubbleDiv = createBubbleElement({
395
- id: `initial-${pageIndex}-${panelIndex}`,
396
- text: pageData.bubbles[panelIndex].dialog || '',
397
- left: `${pageData.bubbles[panelIndex].bubble_offset_x ?? 50}px`,
398
- top: `${pageData.bubbles[panelIndex].bubble_offset_y ?? 20}px`,
399
- });
400
- panelDiv.appendChild(bubbleDiv);
401
- }
402
- grid.appendChild(panelDiv);
 
 
 
403
  });
404
- pageDiv.appendChild(grid);
405
- pageWrapper.appendChild(pageDiv);
406
- container.appendChild(pageWrapper);
407
  });
408
  }
409
 
410
- function createBubbleElement(data) {
411
- const bubbleDiv = document.createElement('div');
412
- bubbleDiv.dataset.id = data.id;
413
- const textSpan = document.createElement('span');
414
- textSpan.className = 'bubble-text';
415
- textSpan.textContent = data.text;
416
- bubbleDiv.appendChild(textSpan);
417
- bubbleDiv.style.left = data.left;
418
- bubbleDiv.style.top = data.top;
419
- applyBubbleType(bubbleDiv, 'speech');
420
- initializeBubbleEvents(bubbleDiv);
421
- return bubbleDiv;
422
- }
423
-
424
- function applyBubbleType(bubble, type) {
425
- bubble.className = 'speech-bubble ' + type;
426
- if(type === 'speech') bubble.classList.add('tail-bottom');
427
- if(type === 'thought') {
428
- bubble.querySelectorAll('.thought-dot').forEach(e=>e.remove());
429
- for(let i=1; i<=2; i++) {
430
- const d = document.createElement('div');
431
- d.className = `thought-dot thought-dot-${i}`;
432
- bubble.appendChild(d);
433
  }
434
- }
435
- bubble.dataset.type = type;
436
- }
437
-
438
- function initializeEditor() {
439
- document.querySelectorAll('.panel').forEach(panel => {
440
- panel.addEventListener('click', () => selectPanel(panel));
441
- panel.querySelector('img')?.addEventListener('mousedown', startPan);
442
- });
443
- document.querySelectorAll('.speech-bubble').forEach(initializeBubbleEvents);
444
- document.getElementById('zoom-slider').addEventListener('input', handleZoom);
445
- document.getElementById('tail-slider').addEventListener('input', (e) => slideTail(e.target.value));
446
-
447
- document.getElementById('bubble-text-color').addEventListener('input', (e) => {
448
- if(currentlySelectedBubble) currentlySelectedBubble.style.setProperty('--bubble-text-color', e.target.value);
449
- });
450
- document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
451
- if(currentlySelectedBubble) currentlySelectedBubble.style.setProperty('--bubble-fill-color', e.target.value);
452
- });
453
-
454
- document.addEventListener('mousemove', e => { if (isPanning) panImage(e); if (draggedBubble) drag(e); if(isResizing) resizeBubble(e); });
455
- document.addEventListener('mouseup', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
456
- document.addEventListener('mouseleave', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
457
- }
458
-
459
- function initializeBubbleEvents(bubble) {
460
- bubble.addEventListener('mousedown', (e) => { e.stopPropagation(); startDrag(e); });
461
- bubble.addEventListener('click', (e) => { e.stopPropagation(); selectBubble(bubble); });
462
- bubble.addEventListener('dblclick', (e) => { e.stopPropagation(); editBubbleText(bubble); });
463
- ['nw', 'ne', 'sw', 'se'].forEach(dir => {
464
- const h = document.createElement('div'); h.className = `resize-handle ${dir}`;
465
- bubble.appendChild(h);
466
- h.addEventListener('mousedown', (e) => startResize(e, dir));
467
- });
468
- }
469
-
470
- function selectPanel(panel) {
471
- document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
472
- panel.classList.add('selected');
473
- currentlySelectedPanel = panel;
474
- selectBubble(null);
475
- resetPanelTransform();
476
- }
477
-
478
- function selectBubble(bubble) {
479
- if(currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
480
- currentlySelectedBubble = bubble;
481
- const tailControls = document.getElementById('tail-controls');
482
-
483
- if(bubble) {
484
- bubble.classList.add('selected');
485
- document.getElementById('bubble-text-color').disabled = false;
486
- document.getElementById('bubble-fill-color').disabled = false;
487
- document.getElementById('bubble-type-select').disabled = false;
488
- document.getElementById('zoom-slider').disabled = true;
489
-
490
- if(bubble.dataset.type === 'speech') tailControls.style.display = 'block';
491
- else tailControls.style.display = 'none';
492
- } else {
493
- if(document.getElementById('bubble-text-color')) document.getElementById('bubble-text-color').disabled = true;
494
- if(document.getElementById('bubble-fill-color')) document.getElementById('bubble-fill-color').disabled = true;
495
- if(document.getElementById('bubble-type-select')) document.getElementById('bubble-type-select').disabled = true;
496
- document.getElementById('zoom-slider').disabled = false;
497
- tailControls.style.display = 'none';
498
- }
499
- }
500
-
501
- function startDrag(e) {
502
- draggedBubble = e.target.closest('.speech-bubble');
503
- selectBubble(draggedBubble);
504
- const rect = draggedBubble.getBoundingClientRect();
505
- offset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
506
- }
507
- function drag(e) {
508
- if(!draggedBubble) return;
509
- e.preventDefault();
510
- const parent = draggedBubble.parentElement.getBoundingClientRect();
511
- draggedBubble.style.left = (e.clientX - parent.left - offset.x) + 'px';
512
- draggedBubble.style.top = (e.clientY - parent.top - offset.y) + 'px';
513
- }
514
- function stopDrag() { draggedBubble = null; }
515
-
516
- function startResize(e, dir) {
517
- e.preventDefault(); e.stopPropagation();
518
- isResizing = true; resizeHandle = dir;
519
- const b = currentlySelectedBubble;
520
- const r = b.getBoundingClientRect();
521
- originalWidth = r.width; originalHeight = r.height;
522
- originalX = b.offsetLeft; originalY = b.offsetTop;
523
- originalMouseX = e.clientX; originalMouseY = e.clientY;
524
- }
525
- function resizeBubble(e) {
526
- if(!isResizing || !currentlySelectedBubble) return;
527
- const dx = e.clientX - originalMouseX;
528
- const dy = e.clientY - originalMouseY;
529
- const b = currentlySelectedBubble;
530
- if(resizeHandle.includes('e')) b.style.width = (originalWidth + dx) + 'px';
531
- if(resizeHandle.includes('w')) { b.style.width = (originalWidth - dx) + 'px'; b.style.left = (originalX + dx) + 'px'; }
532
- if(resizeHandle.includes('s')) b.style.height = (originalHeight + dy) + 'px';
533
- if(resizeHandle.includes('n')) { b.style.height = (originalHeight - dy) + 'px'; b.style.top = (originalY + dy) + 'px'; }
534
- }
535
- function stopResize() { isResizing = false; }
536
-
537
- function slideTail(val) {
538
- if(currentlySelectedBubble) currentlySelectedBubble.style.setProperty('--tail-pos', val + '%');
539
- }
540
- function rotateBubbleTail() {
541
- if(!currentlySelectedBubble) return;
542
- const b = currentlySelectedBubble;
543
- if(b.classList.contains('tail-bottom')) b.classList.replace('tail-bottom', 'tail-left');
544
- else if(b.classList.contains('tail-left')) b.classList.replace('tail-left', 'tail-top');
545
- else if(b.classList.contains('tail-top')) b.classList.replace('tail-top', 'tail-right');
546
- else b.className = b.className.replace(/tail-\w+/, 'tail-bottom');
547
- }
548
- function changeBubbleType(val) {
549
- if(!currentlySelectedBubble) return;
550
- applyBubbleType(currentlySelectedBubble, val);
551
- selectBubble(currentlySelectedBubble);
552
- }
553
- function editBubbleText(bubble) {
554
- const span = bubble.querySelector('.bubble-text');
555
- const txt = document.createElement('textarea');
556
- txt.value = span.textContent;
557
- bubble.appendChild(txt);
558
- span.style.display = 'none';
559
- txt.focus();
560
- txt.onblur = () => { span.textContent = txt.value; txt.remove(); span.style.display = 'block'; };
561
- }
562
- function addBubbleToPanel() {
563
- if(!currentlySelectedPanel) return alert("Select panel first");
564
- const b = createBubbleElement({id: Date.now(), text: "New Text", left: "20px", top: "20px"});
565
- currentlySelectedPanel.appendChild(b);
566
- selectBubble(b);
567
- }
568
- function deleteBubble() {
569
- if(currentlySelectedBubble) { currentlySelectedBubble.remove(); selectBubble(null); }
570
- }
571
-
572
- function startPan(e) {
573
- if(e.button !== 0) return;
574
- const img = e.target;
575
- if((parseFloat(img.dataset.zoom)||100) <= 100) return;
576
- e.preventDefault(); isPanning = true;
577
- img.classList.add('panning');
578
- panStartX = e.clientX; panStartY = e.clientY;
579
- panStartTranslateX = parseFloat(img.dataset.translateX || 0);
580
- panStartTranslateY = parseFloat(img.dataset.translateY || 0);
581
- }
582
- function panImage(e) {
583
- if(!isPanning || !currentlySelectedPanel) return;
584
- const img = currentlySelectedPanel.querySelector('img');
585
- img.dataset.translateX = panStartTranslateX + (e.clientX - panStartX);
586
- img.dataset.translateY = panStartTranslateY + (e.clientY - panStartY);
587
- updateImageTransform(img);
588
- }
589
- function stopPan() {
590
- if(!isPanning) return;
591
- isPanning = false;
592
- currentlySelectedPanel?.querySelector('img')?.classList.remove('panning');
593
- }
594
- function handleZoom(e) {
595
- if(!currentlySelectedPanel) return;
596
- const img = currentlySelectedPanel.querySelector('img');
597
- img.dataset.zoom = e.target.value;
598
- updateImageTransform(img);
599
- }
600
- function updateImageTransform(img) {
601
- const z = (img.dataset.zoom || 100)/100;
602
- const x = img.dataset.translateX || 0;
603
- const y = img.dataset.translateY || 0;
604
- img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`;
605
- img.classList.toggle('pannable', z>1);
606
- }
607
- function resetPanelTransform() {
608
- if(!currentlySelectedPanel) return;
609
- const img = currentlySelectedPanel.querySelector('img');
610
- img.dataset.zoom=100; img.dataset.translateX=0; img.dataset.translateY=0;
611
- document.getElementById('zoom-slider').value = 100;
612
- updateImageTransform(img);
613
- }
614
-
615
- function replacePanelImage() {
616
- if (!currentlySelectedPanel) return alert("Select a panel first.");
617
- const img = currentlySelectedPanel.querySelector('img');
618
- const uploader = document.getElementById('image-uploader');
619
- uploader.onchange = (event) => {
620
- const file = event.target.files[0];
621
- if (!file) return;
622
- const formData = new FormData();
623
- formData.append('image', file);
624
- img.style.opacity = '0.5';
625
- fetch('/replace_panel', { method: 'POST', body: formData })
626
- .then(res => res.json())
627
- .then(data => {
628
- if (data.success) {
629
- img.src = `frames/${data.new_filename}?t=${Date.now()}`;
630
- resetPanelTransform();
631
- } else alert('Error: ' + data.error);
632
- img.style.opacity = '1';
633
- });
634
- uploader.value = '';
635
  };
636
- uploader.click();
637
- }
638
-
639
- function adjustFrame(dir) {
640
- if (!currentlySelectedPanel) return alert("Select a panel.");
641
- const img = currentlySelectedPanel.querySelector('img');
642
- let filename = img.src.substring(img.src.lastIndexOf('/')+1).split('?')[0];
643
- fetch('/regenerate_frame', {
644
- method: 'POST', headers: {'Content-Type': 'application/json'},
645
- body: JSON.stringify({ filename, direction: dir })
646
- }).then(r=>r.json()).then(d=>{
647
- if(d.success) img.src = `frames/${filename}?t=${Date.now()}`;
648
- else alert(d.message);
649
- });
650
- }
651
-
652
- function gotoTimestamp() {
653
- if (!currentlySelectedPanel) return alert("Select a panel.");
654
- const input = document.getElementById('timestamp-input');
655
- let val = input.value.trim();
656
- if(!val) return;
657
- if(val.includes(':')) {
658
- let parts = val.split(':');
659
- val = parseInt(parts[0])*60 + parseFloat(parts[1]);
660
- } else val = parseFloat(val);
661
-
662
- const img = currentlySelectedPanel.querySelector('img');
663
- let filename = img.src.substring(img.src.lastIndexOf('/')+1).split('?')[0];
664
- fetch('/goto_timestamp', {
665
- method: 'POST', headers: {'Content-Type': 'application/json'},
666
- body: JSON.stringify({ filename, timestamp: val })
667
- }).then(r=>r.json()).then(d=>{
668
- if(d.success) { img.src = `frames/${filename}?t=${Date.now()}`; input.value=''; resetPanelTransform(); }
669
- else alert(d.message);
670
- });
671
  }
672
 
673
- async function exportPagesToPNG() {
674
  const pages = document.querySelectorAll('.comic-page');
675
- for(let i=0; i<pages.length; i++) {
676
- try {
677
- const dataUrl = await htmlToImage.toPng(pages[i], { pixelRatio: 3 });
678
- const link = document.createElement('a');
679
- link.download = `page-${i+1}.png`;
680
- link.href = dataUrl;
681
- link.click();
682
- } catch(e) { console.error(e); alert("Export failed"); }
683
  }
684
  }
685
  </script>
@@ -700,356 +261,129 @@ class EnhancedComicGenerator:
700
  os.makedirs(self.output_dir, exist_ok=True)
701
 
702
  self.video_fps = None
703
- self.frame_metadata = {}
704
 
705
  def update_status(self, message, progress):
706
  try:
707
  with open(self.status_file, 'w') as f:
708
  json.dump({'message': message, 'progress': progress}, f)
709
- except Exception as e:
710
- print(f"Error updating status: {e}")
711
-
712
- def cleanup_generated(self):
713
- print(f"[{self.sid}] 🧹 Performing cleanup...")
714
- if os.path.isdir(self.frames_dir):
715
- for file in os.listdir(self.frames_dir):
716
- os.remove(os.path.join(self.frames_dir, file))
717
- if os.path.isdir(self.output_dir):
718
- for file in os.listdir(self.output_dir):
719
- if file != 'status.json':
720
- try: os.remove(os.path.join(self.output_dir, file))
721
- except: pass
722
- srt_file = os.path.join(self.user_dir, 'subs.srt')
723
- if os.path.exists(srt_file): os.remove(srt_file)
724
- print(f"[{self.sid}] ✅ Cleanup complete.")
725
 
726
- def regenerate_frame(self, frame_filename, direction):
727
  try:
728
- if not self.video_fps:
729
- cap = cv2.VideoCapture(self.video_path)
730
- self.video_fps = cap.get(cv2.CAP_PROP_FPS)
731
- cap.release()
732
-
733
- metadata_path = os.path.join(self.frames_dir, 'frame_metadata.json')
734
- if not os.path.exists(metadata_path):
735
- return {"success": False, "message": "Frame metadata missing."}
736
- with open(metadata_path, 'r') as f:
737
- frame_to_time = json.load(f)
738
-
739
- if frame_filename not in frame_to_time:
740
- return {"success": False, "message": "Panel not linked to video."}
741
-
742
- current_data = frame_to_time[frame_filename]
743
- current_time = current_data['time'] if isinstance(current_data, dict) else current_data
744
-
745
- frame_duration = 1.0 / self.video_fps
746
- target_time = current_time + frame_duration if direction == 'forward' else current_time - frame_duration
747
- target_time = max(0, target_time)
748
 
 
749
  cap = cv2.VideoCapture(self.video_path)
750
- if not cap.isOpened(): return {"success": False, "message": "Cannot open video."}
751
- cap.set(cv2.CAP_PROP_POS_MSEC, target_time * 1000)
752
- ret, frame = cap.read()
753
- cap.release()
754
-
755
- if not ret or frame is None:
756
- return {"success": False, "message": f"No frame at {target_time:.2f}s."}
757
 
758
- new_path = os.path.join(self.frames_dir, frame_filename)
759
- cv2.imwrite(new_path, frame)
760
-
761
- self._enhance_all_images(single_image_path=new_path)
762
- self._enhance_quality_colors(single_image_path=new_path)
763
-
764
- if isinstance(frame_to_time[frame_filename], dict):
765
- frame_to_time[frame_filename]['time'] = target_time
766
- else:
767
- frame_to_time[frame_filename] = target_time
768
-
769
- with open(metadata_path, 'w') as f: json.dump(frame_to_time, f, indent=2)
770
-
771
- return {"success": True, "message": f"Adjusted to {target_time:.3f}s", "new_filename": frame_filename}
772
- except Exception as e:
773
- traceback.print_exc()
774
- return {"success": False, "message": str(e)}
775
 
776
- def get_frame_at_timestamp(self, frame_filename, timestamp_seconds):
777
- try:
778
- metadata_path = os.path.join(self.frames_dir, 'frame_metadata.json')
779
- if not os.path.exists(metadata_path): return {"success": False, "message": "Metadata missing."}
780
-
 
 
 
 
 
 
 
 
 
 
781
  cap = cv2.VideoCapture(self.video_path)
782
- if not cap.isOpened(): return {"success": False, "message": "Cannot open video."}
783
- fps = cap.get(cv2.CAP_PROP_FPS)
784
- if fps == 0: fps = 25
785
- duration = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) / fps
786
 
787
- if timestamp_seconds < 0 or timestamp_seconds > duration:
788
- cap.release()
789
- return {"success": False, "message": f"Timestamp invalid (0-{duration:.2f}s)."}
790
-
791
- cap.set(cv2.CAP_PROP_POS_MSEC, timestamp_seconds * 1000)
792
- ret, frame = cap.read()
793
- cap.release()
794
 
795
- if not ret or frame is None: return {"success": False, "message": "Could not retrieve frame."}
 
796
 
797
- new_path = os.path.join(self.frames_dir, frame_filename)
798
- cv2.imwrite(new_path, frame)
799
-
800
- self._enhance_all_images(single_image_path=new_path)
801
- self._enhance_quality_colors(single_image_path=new_path)
802
-
803
- with open(metadata_path, 'r') as f: frame_to_time = json.load(f)
804
- if frame_filename in frame_to_time:
805
- if isinstance(frame_to_time[frame_filename], dict):
806
- frame_to_time[frame_filename]['time'] = timestamp_seconds
807
- else:
808
- frame_to_time[frame_filename] = timestamp_seconds
809
- with open(metadata_path, 'w') as f: json.dump(frame_to_time, f, indent=2)
810
-
811
- return { "success": True, "message": f"Jumped to {timestamp_seconds:.3f}s" }
812
- except Exception as e:
813
- return {"success": False, "message": str(e)}
814
-
815
- def generate_keyframes_from_moments(self, video_path, key_moments, max_frames=32):
816
- try:
817
- cap = cv2.VideoCapture(video_path)
818
- if not cap.isOpened(): raise Exception("Cannot open video")
819
- fps, total_frames = self.video_fps, int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
820
- duration = total_frames / fps
821
- key_moments.sort(key=lambda x: x['start'])
822
 
823
- frame_metadata, frame_count = {}, 0
824
- for i, moment in enumerate(key_moments):
825
- self.update_status(f"Extracting frame {i+1}/{len(key_moments)}...", 25 + int(20 * (i / len(key_moments))))
826
- frame_time = (moment['start'] + moment['end']) / 2
827
- if frame_time > duration: continue
828
-
829
- frame_number = int(frame_time * fps)
830
- cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
831
  ret, frame = cap.read()
832
  if ret:
833
- frame_filename = f"frame_{frame_count:04d}.png"
834
- frame_path = os.path.join(self.frames_dir, frame_filename)
835
- cv2.imwrite(frame_path, frame)
836
- frame_metadata[frame_filename] = { 'time': frame_time, 'dialogue': moment['text'], 'start': moment['start'], 'end': moment['end'] }
837
- frame_count += 1
838
  cap.release()
839
 
840
- with open(os.path.join(self.frames_dir, 'frame_metadata.json'), 'w') as f:
841
- json.dump(frame_metadata, f, indent=2)
842
- print(f"[{self.sid}] ✅ Extracted {frame_count} keyframes")
843
- return True
844
- except Exception as e:
845
- print(f"[{self.sid}] Error extracting keyframes: {e}")
846
- return False
847
-
848
- def generate_comic(self):
849
- start_time = time.time()
850
- try:
851
- self.update_status("Cleaning up...", 0)
852
- self.cleanup_generated()
853
-
854
- self.update_status("Analyzing video...", 5)
855
- cap = cv2.VideoCapture(self.video_path)
856
- if not cap.isOpened(): raise Exception("Cannot open video to get FPS.")
857
- self.video_fps = cap.get(cv2.CAP_PROP_FPS)
858
- if self.video_fps == 0: self.video_fps = 25
859
- cap.release()
860
-
861
- self.update_status("Generating subtitles...", 10)
862
- get_real_subtitles(self.video_path)
863
-
864
- if os.path.exists('test1.srt'):
865
- user_srt = os.path.join(self.user_dir, 'subs.srt')
866
- shutil.move('test1.srt', user_srt)
867
- else:
868
- user_srt = os.path.join(self.user_dir, 'subs.srt')
869
 
870
- with open(user_srt, 'r', encoding='utf-8') as f:
871
- all_subs = list(srt.parse(f.read()))
872
-
873
- key_moments = [{'index': s.index, 'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in all_subs]
874
-
875
- if not self.generate_keyframes_from_moments(self.video_path, key_moments):
876
- raise Exception("Keyframe extraction failed.")
877
 
878
- self.update_status("Cropping black bars...", 45)
879
- black_x, black_y, _, _ = black_bar_crop()
880
-
881
- self.update_status("Enhancing images...", 50)
882
- self._enhance_all_images()
883
- self._enhance_quality_colors()
884
-
885
- self.update_status("Placing speech bubbles...", 75)
886
- bubbles = self._create_ai_bubbles_from_moments(black_x, black_y)
887
-
888
- self.update_status("Assembling pages...", 90)
889
- pages = self._generate_pages(bubbles)
890
-
891
- self.update_status("Saving...", 95)
892
- self._save_results(pages)
893
-
894
- execution_time = (time.time() - start_time) / 60
895
- print(f"[{self.sid}] ✅ Completed in {execution_time:.2f} min")
896
- self.update_status("Complete!", 100)
897
- return True
898
- except Exception as e:
899
- print(f"[{self.sid}] ❌ Failed: {e}")
900
- traceback.print_exc()
901
- self.update_status(f"Error: {e}", -1)
902
- return False
903
-
904
- def _enhance_all_images(self, single_image_path=None):
905
- try:
906
- enhancer = SimpleColorEnhancer()
907
- if single_image_path:
908
- enhancer.enhance_single(single_image_path)
909
- else:
910
- frame_paths = [os.path.join(self.frames_dir, f) for f in os.listdir(self.frames_dir) if f.endswith('.png')]
911
- with ThreadPoolExecutor() as executor:
912
- list(executor.map(enhancer.enhance_single, frame_paths))
913
- except Exception as e:
914
- print(f"Simple enhancement error: {e}")
915
-
916
- def _enhance_quality_colors(self, single_image_path=None):
917
- try:
918
- enhancer = QualityColorEnhancer()
919
- if single_image_path:
920
- enhancer.enhance_single(single_image_path)
921
- else:
922
- frame_paths = [os.path.join(self.frames_dir, f) for f in os.listdir(self.frames_dir) if f.endswith('.png')]
923
- with ThreadPoolExecutor() as executor:
924
- list(executor.map(enhancer.enhance_single, frame_paths))
925
- except Exception as e:
926
- print(f"Quality enhancement error: {e}")
927
-
928
- def _process_bubble_for_frame(self, frame_file):
929
- frame_path = os.path.join(self.frames_dir, frame_file)
930
- dialogue = self.frame_metadata.get(frame_file, {}).get('dialogue', "")
931
- try:
932
- faces = face_detector.detect_faces(frame_path)
933
- lip_x, lip_y = face_detector.get_lip_position(frame_path, faces[0]) if faces else (-1, -1)
934
- bubble_x, bubble_y = ai_bubble_placer.place_bubble_ai(frame_path, (lip_x, lip_y))
935
- return bubble(bubble_offset_x=bubble_x, bubble_offset_y=bubble_y, lip_x=lip_x, lip_y=lip_y, dialog=dialogue, emotion='normal')
936
- except Exception as e:
937
- return bubble(bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, dialog=dialogue, emotion='normal')
938
-
939
- def _create_ai_bubbles_from_moments(self, black_x, black_y):
940
- frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
941
- metadata_path = os.path.join(self.frames_dir, 'frame_metadata.json')
942
- if not os.path.exists(metadata_path):
943
- return [bubble(dialog="") for _ in frame_files]
944
 
945
- with open(metadata_path, 'r') as f:
946
- self.frame_metadata = json.load(f)
947
 
948
- with ThreadPoolExecutor() as executor:
949
- bubbles = list(executor.map(self._process_bubble_for_frame, frame_files))
950
- return bubbles
951
 
952
- def _generate_pages(self, bubbles):
953
- pages, frame_files = [], sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
954
- num_pages = (len(frame_files) + 3) // 4
955
- for i in range(num_pages):
956
- start, end = i*4, (i+1)*4
957
- page_panels = [panel(image=f) for f in frame_files[start:end]]
958
- page_bubbles = bubbles[start:end]
959
- if page_panels: pages.append(Page(panels=page_panels, bubbles=page_bubbles))
960
- return pages
961
 
962
- def _save_results(self, pages):
963
- try:
964
- pages_data = []
965
- for page in pages:
966
- panels = [p.__dict__ if hasattr(p, '__dict__') else p for p in page.panels]
967
- bubbles_data = [b.__dict__ if hasattr(b, '__dict__') else b for b in page.bubbles]
968
- pages_data.append({'panels': panels, 'bubbles': bubbles_data})
969
-
970
- with open(os.path.join(self.output_dir, 'pages.json'), 'w', encoding='utf-8') as f:
971
- json.dump(pages_data, f, indent=2)
972
- self._copy_template_files()
973
  except Exception as e:
974
- print(f"Save results failed: {e}")
975
-
976
- def _copy_template_files(self):
977
- try:
978
- with open(os.path.join(self.output_dir, 'page.html'), 'w', encoding='utf-8') as f:
979
- f.write(INDEX_HTML)
980
- except Exception as e:
981
- print(f"[{self.sid}] Template creation failed: {e}")
982
-
983
- # --- ROUTES ---
984
 
985
  @app.route('/')
986
  def index():
987
- if 'sid' not in session:
988
- session['sid'] = uuid.uuid4().hex
989
  return INDEX_HTML
990
 
991
  @app.route('/uploader', methods=['POST'])
992
- def upload_file():
993
  if 'sid' not in session: session['sid'] = uuid.uuid4().hex
994
- try:
995
- f = request.files['file']
996
- gen = EnhancedComicGenerator(session['sid'])
997
- f.save(gen.video_path)
998
- threading.Thread(target=gen.generate_comic).start()
999
- return jsonify({'success': True})
1000
- except Exception as e:
1001
- return jsonify({'success': False, 'message': str(e)}), 500
 
 
 
1002
 
1003
  @app.route('/status')
1004
- def status():
1005
- if 'sid' not in session: return jsonify({'message': 'Session expired', 'progress': -1})
1006
  sid = session['sid']
1007
- status_file = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
1008
- if os.path.exists(status_file):
1009
- with open(status_file, 'r') as f: return jsonify(json.load(f))
1010
- return jsonify({'message': 'Initializing...', 'progress': 0})
1011
-
1012
- @app.route('/comic')
1013
- def view_comic():
1014
- if 'sid' not in session: return "Session expired", 400
1015
- return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'output'), 'page.html')
1016
 
1017
  @app.route('/output/<path:filename>')
1018
- def output_file(filename):
1019
- if 'sid' not in session: return "Session expired", 400
1020
  return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'output'), filename)
1021
 
1022
  @app.route('/frames/<path:filename>')
1023
- def frame_file(filename):
1024
- if 'sid' not in session: return "Session expired", 400
1025
  return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'frames'), filename)
1026
 
1027
- @app.route('/regenerate_frame', methods=['POST'])
1028
- def regenerate_frame_route():
1029
- if 'sid' not in session: return jsonify({'success': False, 'message': 'Session expired'})
1030
- data = request.get_json()
1031
- gen = EnhancedComicGenerator(session['sid'])
1032
- return jsonify(gen.regenerate_frame(data['filename'], data['direction']))
1033
-
1034
- @app.route('/replace_panel', methods=['POST'])
1035
- def replace_panel():
1036
- if 'sid' not in session: return jsonify({'success': False, 'error': 'Session expired'})
1037
- if 'image' not in request.files: return jsonify({'success': False, 'error': 'No image'})
1038
- file = request.files['image']
1039
- gen = EnhancedComicGenerator(session['sid'])
1040
- filename = f"replaced_{int(time.time())}.png"
1041
- file.save(os.path.join(gen.frames_dir, filename))
1042
- return jsonify({'success': True, 'new_filename': filename})
1043
-
1044
- @app.route('/goto_timestamp', methods=['POST'])
1045
- def goto_timestamp_route():
1046
- if 'sid' not in session: return jsonify({'success': False, 'message': 'Session expired'})
1047
- data = request.get_json()
1048
- gen = EnhancedComicGenerator(session['sid'])
1049
- return jsonify(gen.get_frame_at_timestamp(data['filename'], float(data['timestamp'])))
1050
-
1051
  if __name__ == '__main__':
1052
  os.makedirs(BASE_USER_DIR, exist_ok=True)
1053
- port = int(os.getenv("PORT", 7860))
1054
- print(f"🚀 Starting Multi-User Comic Generator on port {port}")
1055
- app.run(debug=False, host='0.0.0.0', port=port)
 
6
  import json
7
  import traceback
8
  from concurrent.futures import ThreadPoolExecutor
9
+ from flask import Flask, render_template, request, jsonify, send_from_directory, send_file, session
10
 
11
+ # --- 1. CORE DEPENDENCY CHECKS ---
 
12
  try:
13
  import cv2
14
  import numpy as np
15
  from PIL import Image
16
  import srt
 
17
  except ImportError as e:
18
+ print(f"❌ CRITICAL ERROR: Missing python library. {e}")
19
+ # We define dummy variables to let the app start, but generation will fail with a clear message
20
+ cv2 = None
21
+ np = None
22
+ Image = None
23
+ srt = None
24
+
25
+ # --- 2. BACKEND MODULE IMPORT WITH FALLBACKS ---
26
+ # This ensures the app doesn't crash if a specific backend file is missing
27
+ def dummy_function(*args, **kwargs):
28
+ print("⚠️ Warning: Function not loaded correctly.")
29
+ return 0, 0, None, None
30
 
 
31
  try:
32
  from backend.keyframes.keyframes import black_bar_crop
33
+ except Exception:
34
+ black_bar_crop = dummy_function
 
 
 
35
 
36
  try:
37
  from backend.simple_color_enhancer import SimpleColorEnhancer
38
+ except Exception:
 
 
39
  class SimpleColorEnhancer:
40
+ def enhance_single(self, *args): pass
 
41
 
42
  try:
43
  from backend.quality_color_enhancer import QualityColorEnhancer
44
+ except Exception:
 
 
45
  class QualityColorEnhancer:
46
+ def enhance_single(self, *args): pass
 
47
 
48
  try:
49
  from backend.class_def import bubble, panel, Page
50
+ except Exception:
 
 
51
  def bubble(**kwargs): return kwargs
52
  def panel(**kwargs): return kwargs
53
  class Page:
54
+ def __init__(self, panels, bubbles): self.panels, self.bubbles = panels, bubbles
 
 
55
 
56
  try:
57
+ from backend.ai_enhanced_core import image_processor, face_detector
58
  from backend.ai_bubble_placement import ai_bubble_placer
59
  from backend.subtitles.subs_real import get_real_subtitles
 
 
60
  except Exception as e:
61
+ print(f"⚠️ Backend Import Error: {e}")
62
+ # Define dummy get_real_subtitles to prevent NameError
63
+ def get_real_subtitles(video_path):
64
+ raise Exception("Subtitle module failed to load. Check backend/subtitles/subs_real.py")
65
+
66
+ # Define dummy face detector
67
+ class DummyDetector:
68
+ def detect_faces(self, p): return []
69
+ def get_lip_position(self, p, f): return -1, -1
70
+ face_detector = DummyDetector()
71
+
72
+ # Define dummy bubble placer
73
+ class DummyPlacer:
74
+ def place_bubble_ai(self, p, l): return 50, 20
75
+ ai_bubble_placer = DummyPlacer()
76
 
77
  # --- FLASK APP SETUP ---
78
  app = Flask(__name__)
79
+ app.secret_key = "HF_SPACE_SECRET_KEY_555"
80
+ BASE_USER_DIR = "userdata"
81
 
 
82
  INDEX_HTML = '''
83
  <!DOCTYPE html>
84
  <html lang="en">
 
86
  <meta charset="UTF-8">
87
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
88
  <title>Movie to Comic Generator</title>
 
89
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
90
+ <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@700&display=swap" rel="stylesheet">
 
 
91
  <style>
92
+ body { background-color: #fdf6e3; font-family: system-ui, sans-serif; color: #3d3d3d; margin: 0; min-height: 100vh; }
93
+ #upload-container { display: flex; justify-content: center; align-items: center; min-height: 100vh; width: 100%; }
94
+ .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; }
95
+ #editor-container { display: none; padding: 20px; width: 100%; box-sizing: border-box; }
96
+ .file-label { display: block; padding: 15px; background: #2c3e50; color: white; border-radius: 8px; cursor: pointer; margin-bottom: 10px; }
97
+ .submit-btn { width: 100%; padding: 15px; background: #e67e22; color: white; border: none; border-radius: 8px; font-size: 18px; cursor: pointer; font-weight: bold; }
98
+ .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; }
99
+ @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
+ /* COMIC STYLES */
102
+ .comic-page { background: white; width: 600px; height: 400px; position: relative; overflow: hidden; border: 2px solid #000; margin: 0 auto 20px; }
103
+ .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; }
104
+ .panel { position: relative; overflow: hidden; border: 2px solid #000; background: #eee; }
105
+ .panel img { width: 100%; height: 100%; object-fit: cover; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
+ /* SPEECH BUBBLE - EXPORT SAFE GRADIENT */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  .speech-bubble.speech {
109
  --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
110
  --c: var(--bubble-fill-color, #4ECDC4);
111
  background: var(--c); color: var(--bubble-text-color, #fff); padding: 1em; position: absolute;
112
  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);
113
+ font-family: 'Comic Neue', cursive; font-weight: bold; font-size: 13px; text-align: center;
114
+ min-width: 60px; min-height: 40px; display: flex; align-items: center; justify-content: center;
115
+ cursor: move; z-index: 10;
116
  }
117
  .speech-bubble.speech:before {
118
  content: ""; position: absolute; width: var(--b); height: var(--h);
119
  background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
120
  border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
121
  }
 
122
  .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))); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
+ .speech-bubble.selected { outline: 2px dashed #333; }
125
+ .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; }
126
+ .edit-controls button { width: 100%; margin-top: 5px; padding: 8px; cursor: pointer; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  </style>
128
  </head>
129
  <body>
 
130
  <div id="upload-container">
131
+ <div class="upload-box">
132
+ <h1>🎬 Comic Generator</h1>
133
+ <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fname').innerText=this.files[0].name">
134
+ <label for="file-upload" class="file-label">Choose Video</label>
135
+ <span id="fname">No file</span>
136
+ <button class="submit-btn" onclick="upload()">Generate</button>
137
+ <div id="loading" style="display:none;">
138
+ <div class="loader"></div>
139
+ <p id="status">Uploading...</p>
140
+ </div>
 
 
141
  </div>
142
  </div>
143
 
 
144
  <div id="editor-container">
145
+ <div id="comic-pages"></div>
 
 
 
 
 
 
146
  <div class="edit-controls">
147
+ <h4>Editor</h4>
148
+ <button onclick="exportToPng()">💾 Export PNG</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  </div>
150
  </div>
151
 
152
  <script>
153
+ let interval;
154
+ async function upload() {
155
+ const file = document.getElementById('file-upload').files[0];
156
+ if(!file) return alert("Select file");
157
+
158
+ const fd = new FormData();
159
+ fd.append('file', file);
160
+
161
+ document.querySelector('.upload-box').style.display='none';
162
+ document.getElementById('loading').style.display='block';
163
+
164
+ const res = await fetch('/uploader', {method:'POST', body:fd});
165
+ if(res.ok) interval = setInterval(checkStatus, 2000);
166
+ else { alert("Upload failed"); location.reload(); }
167
  }
168
 
169
  async function checkStatus() {
170
+ const res = await fetch('/status');
171
+ const data = await res.json();
172
+ document.getElementById('status').innerText = data.message;
173
+
174
+ if(data.progress >= 100) {
175
+ clearInterval(interval);
176
+ document.getElementById('upload-container').style.display='none';
177
+ document.getElementById('editor-container').style.display='block';
178
+ loadComic();
179
+ } else if(data.progress < 0) {
180
+ clearInterval(interval);
181
+ alert("Error: " + data.message);
182
+ location.reload();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  }
 
 
 
 
 
 
 
 
184
  }
185
 
186
+ function loadComic() {
187
+ fetch('output/pages.json').then(r=>r.json()).then(data => {
188
+ const c = document.getElementById('comic-pages');
189
+ data.forEach((p, i) => {
190
+ const div = document.createElement('div');
191
+ div.className = 'comic-page';
192
+ div.id = 'page-'+i;
193
+ const grid = document.createElement('div');
194
+ grid.className = 'comic-grid';
195
+
196
+ p.panels.forEach((pan, j) => {
197
+ const pDiv = document.createElement('div');
198
+ pDiv.className = 'panel';
199
+ const img = document.createElement('img');
200
+ img.src = 'frames/' + pan.image;
201
+ pDiv.appendChild(img);
202
+
203
+ // Add bubble
204
+ if(p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) {
205
+ const b = document.createElement('div');
206
+ b.className = 'speech-bubble speech tail-bottom';
207
+ b.innerText = p.bubbles[j].dialog;
208
+ // Safe defaults
209
+ b.style.left = (p.bubbles[j].bubble_offset_x || 50) + 'px';
210
+ b.style.top = (p.bubbles[j].bubble_offset_y || 20) + 'px';
211
+ pDiv.appendChild(b);
212
+ makeInteractive(b);
213
+ }
214
+ grid.appendChild(pDiv);
215
+ });
216
+ div.appendChild(grid);
217
+ c.appendChild(div);
218
  });
 
 
 
219
  });
220
  }
221
 
222
+ function makeInteractive(el) {
223
+ el.onmousedown = function(e) {
224
+ e.stopPropagation();
225
+ // Simple drag logic
226
+ const offX = e.clientX - el.offsetLeft;
227
+ const offY = e.clientY - el.offsetTop;
228
+ document.onmousemove = function(ev) {
229
+ el.style.left = (ev.clientX - offX) + 'px';
230
+ el.style.top = (ev.clientY - offY) + 'px';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  }
232
+ document.onmouseup = function() { document.onmousemove = null; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  }
235
 
236
+ async function exportToPng() {
237
  const pages = document.querySelectorAll('.comic-page');
238
+ for(let p of pages) {
239
+ const url = await htmlToImage.toPng(p, {pixelRatio: 2});
240
+ const a = document.createElement('a');
241
+ a.download = 'comic.png';
242
+ a.href = url;
243
+ a.click();
 
 
244
  }
245
  }
246
  </script>
 
261
  os.makedirs(self.output_dir, exist_ok=True)
262
 
263
  self.video_fps = None
 
264
 
265
  def update_status(self, message, progress):
266
  try:
267
  with open(self.status_file, 'w') as f:
268
  json.dump({'message': message, 'progress': progress}, f)
269
+ except: pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
+ def generate_comic(self):
272
  try:
273
+ if cv2 is None: raise Exception("OpenCV not installed on server.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
+ self.update_status("Processing Video...", 10)
276
  cap = cv2.VideoCapture(self.video_path)
277
+ if not cap.isOpened(): raise Exception("Invalid Video File")
 
 
 
 
 
 
278
 
279
+ self.video_fps = cap.get(cv2.CAP_PROP_FPS)
280
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
281
+ duration = total_frames / self.video_fps
282
+ cap.release()
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
+ # --- GENERATE SUBTITLES ---
285
+ self.update_status("Extracting Dialogue...", 30)
286
+ try:
287
+ get_real_subtitles(self.video_path) # Generates 'test1.srt' in cwd usually
288
+ # Move it to user dir
289
+ if os.path.exists('test1.srt'):
290
+ shutil.move('test1.srt', os.path.join(self.user_dir, 'subs.srt'))
291
+ except Exception as e:
292
+ print(f"Subtitle Error: {e}")
293
+ # Create dummy srt if failed
294
+ with open(os.path.join(self.user_dir, 'subs.srt'), 'w') as f:
295
+ f.write("1\n00:00:01,000 --> 00:00:04,000\nHello World!\n")
296
+
297
+ # --- EXTRACT FRAMES ---
298
+ self.update_status("Creating Panels...", 50)
299
  cap = cv2.VideoCapture(self.video_path)
 
 
 
 
300
 
301
+ # Parse SRT
302
+ subs_path = os.path.join(self.user_dir, 'subs.srt')
303
+ with open(subs_path, 'r', encoding='utf-8') as f:
304
+ subs = list(srt.parse(f.read()))
 
 
 
305
 
306
+ frame_files = []
307
+ bubbles = []
308
 
309
+ # Limit to 12 frames for speed
310
+ step = max(1, len(subs) // 12)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
+ for i, sub in enumerate(subs[::step]):
313
+ mid_time = (sub.start.total_seconds() + sub.end.total_seconds()) / 2
314
+ cap.set(cv2.CAP_PROP_POS_MSEC, mid_time * 1000)
 
 
 
 
 
315
  ret, frame = cap.read()
316
  if ret:
317
+ fname = f"frame_{i}.png"
318
+ cv2.imwrite(os.path.join(self.frames_dir, fname), frame)
319
+ frame_files.append(fname)
320
+ # Simple bubble placement
321
+ bubbles.append(bubble(dialog=sub.content, bubble_offset_x=50, bubble_offset_y=20))
322
  cap.release()
323
 
324
+ # --- GENERATE PAGES ---
325
+ self.update_status("Assembling...", 80)
326
+ pages_data = []
327
+ # Create 4-panel pages
328
+ for i in range(0, len(frame_files), 4):
329
+ batch_files = frame_files[i:i+4]
330
+ batch_bubbles = bubbles[i:i+4]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
+ panels = [{'image': f} for f in batch_files]
333
+ b_data = [b.__dict__ if hasattr(b, '__dict__') else b for b in batch_bubbles]
 
 
 
 
 
334
 
335
+ pages_data.append({'panels': panels, 'bubbles': b_data})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
+ with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
338
+ json.dump(pages_data, f)
339
 
340
+ # Save HTML for direct viewing
341
+ with open(os.path.join(self.output_dir, 'page.html'), 'w') as f:
342
+ f.write(INDEX_HTML)
343
 
344
+ self.update_status("Done!", 100)
 
 
 
 
 
 
 
 
345
 
 
 
 
 
 
 
 
 
 
 
 
346
  except Exception as e:
347
+ traceback.print_exc()
348
+ self.update_status(f"Failed: {str(e)}", -1)
 
 
 
 
 
 
 
 
349
 
350
  @app.route('/')
351
  def index():
352
+ if 'sid' not in session: session['sid'] = uuid.uuid4().hex
 
353
  return INDEX_HTML
354
 
355
  @app.route('/uploader', methods=['POST'])
356
+ def upload():
357
  if 'sid' not in session: session['sid'] = uuid.uuid4().hex
358
+ sid = session['sid']
359
+ if 'file' not in request.files: return "No file", 400
360
+
361
+ gen = EnhancedComicGenerator(sid)
362
+ request.files['file'].save(gen.video_path)
363
+
364
+ # Initial status
365
+ gen.update_status("Starting...", 5)
366
+
367
+ threading.Thread(target=gen.generate_comic).start()
368
+ return jsonify({'success': True})
369
 
370
  @app.route('/status')
371
+ def get_status():
372
+ if 'sid' not in session: return jsonify({'progress': -1, 'message': "Session Lost"})
373
  sid = session['sid']
374
+ status_path = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
375
+ if os.path.exists(status_path):
376
+ return send_file(status_path)
377
+ return jsonify({'progress': 0, 'message': "Waiting..."})
 
 
 
 
 
378
 
379
  @app.route('/output/<path:filename>')
380
+ def get_output(filename):
 
381
  return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'output'), filename)
382
 
383
  @app.route('/frames/<path:filename>')
384
+ def get_frame(filename):
 
385
  return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'frames'), filename)
386
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  if __name__ == '__main__':
388
  os.makedirs(BASE_USER_DIR, exist_ok=True)
389
+ app.run(host='0.0.0.0', port=7860)