Update app_enhanced.py
Browse files- app_enhanced.py +111 -93
app_enhanced.py
CHANGED
|
@@ -6,7 +6,7 @@ import shutil
|
|
| 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
|
| 10 |
|
| 11 |
# --- 1. CORE DEPENDENCY CHECKS ---
|
| 12 |
try:
|
|
@@ -16,17 +16,15 @@ try:
|
|
| 16 |
import srt
|
| 17 |
except ImportError as e:
|
| 18 |
print(f"❌ CRITICAL ERROR: Missing python library. {e}")
|
| 19 |
-
#
|
| 20 |
cv2 = None
|
| 21 |
np = None
|
| 22 |
Image = None
|
| 23 |
srt = None
|
| 24 |
|
| 25 |
-
# --- 2. BACKEND MODULE
|
| 26 |
-
# This
|
| 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
|
|
@@ -54,31 +52,27 @@ except Exception:
|
|
| 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 |
-
|
| 61 |
-
|
| 62 |
-
#
|
| 63 |
-
def get_real_subtitles(
|
| 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">
|
|
@@ -99,31 +93,32 @@ INDEX_HTML = '''
|
|
| 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 |
-
/*
|
| 108 |
.speech-bubble.speech {
|
| 109 |
-
--b: 3em; --h: 1.8em; --t: 0.6; --p:
|
| 110 |
-
--c:
|
| 111 |
-
background: var(--c); color:
|
| 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:
|
| 114 |
-
min-width:
|
| 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>
|
|
@@ -132,7 +127,7 @@ INDEX_HTML = '''
|
|
| 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>
|
|
@@ -144,13 +139,31 @@ INDEX_HTML = '''
|
|
| 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");
|
|
@@ -161,13 +174,14 @@ INDEX_HTML = '''
|
|
| 161 |
document.querySelector('.upload-box').style.display='none';
|
| 162 |
document.getElementById('loading').style.display='block';
|
| 163 |
|
| 164 |
-
|
|
|
|
| 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(
|
| 171 |
const data = await res.json();
|
| 172 |
document.getElementById('status').innerText = data.message;
|
| 173 |
|
|
@@ -184,8 +198,10 @@ INDEX_HTML = '''
|
|
| 184 |
}
|
| 185 |
|
| 186 |
function loadComic() {
|
| 187 |
-
|
|
|
|
| 188 |
const c = document.getElementById('comic-pages');
|
|
|
|
| 189 |
data.forEach((p, i) => {
|
| 190 |
const div = document.createElement('div');
|
| 191 |
div.className = 'comic-page';
|
|
@@ -197,15 +213,14 @@ INDEX_HTML = '''
|
|
| 197 |
const pDiv = document.createElement('div');
|
| 198 |
pDiv.className = 'panel';
|
| 199 |
const img = document.createElement('img');
|
| 200 |
-
|
|
|
|
| 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
|
| 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);
|
|
@@ -216,13 +231,12 @@ INDEX_HTML = '''
|
|
| 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) {
|
|
@@ -235,12 +249,14 @@ INDEX_HTML = '''
|
|
| 235 |
|
| 236 |
async function exportToPng() {
|
| 237 |
const pages = document.querySelectorAll('.comic-page');
|
| 238 |
-
for(let
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
| 244 |
}
|
| 245 |
}
|
| 246 |
</script>
|
|
@@ -268,37 +284,38 @@ class EnhancedComicGenerator:
|
|
| 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
|
| 274 |
|
| 275 |
self.update_status("Processing Video...", 10)
|
| 276 |
cap = cv2.VideoCapture(self.video_path)
|
| 277 |
-
if not cap.isOpened(): raise Exception("Invalid Video
|
| 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 |
-
#
|
| 285 |
self.update_status("Extracting Dialogue...", 30)
|
| 286 |
try:
|
| 287 |
-
get_real_subtitles(self.video_path) #
|
| 288 |
-
# Move
|
| 289 |
if os.path.exists('test1.srt'):
|
| 290 |
shutil.move('test1.srt', os.path.join(self.user_dir, 'subs.srt'))
|
| 291 |
-
except
|
| 292 |
-
|
| 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\
|
| 296 |
|
| 297 |
-
#
|
| 298 |
-
self.update_status("
|
| 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()))
|
|
@@ -306,84 +323,85 @@ class EnhancedComicGenerator:
|
|
| 306 |
frame_files = []
|
| 307 |
bubbles = []
|
| 308 |
|
| 309 |
-
# Limit to
|
| 310 |
-
|
| 311 |
|
| 312 |
-
for i, sub in enumerate(
|
| 313 |
-
|
| 314 |
-
cap.set(cv2.CAP_PROP_POS_MSEC,
|
| 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 |
-
#
|
| 325 |
-
self.update_status("
|
| 326 |
pages_data = []
|
| 327 |
-
# Create 4-panel pages
|
| 328 |
for i in range(0, len(frame_files), 4):
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
b_data = [b.__dict__ if hasattr(b, '__dict__') else b for b in
|
| 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"
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 358 |
-
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 |
-
|
| 373 |
-
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({'
|
| 378 |
|
| 379 |
@app.route('/output/<path:filename>')
|
| 380 |
def get_output(filename):
|
| 381 |
-
|
|
|
|
|
|
|
| 382 |
|
| 383 |
@app.route('/frames/<path:filename>')
|
| 384 |
def get_frame(filename):
|
| 385 |
-
|
|
|
|
|
|
|
| 386 |
|
| 387 |
if __name__ == '__main__':
|
| 388 |
os.makedirs(BASE_USER_DIR, exist_ok=True)
|
| 389 |
-
|
|
|
|
|
|
| 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
|
| 10 |
|
| 11 |
# --- 1. CORE DEPENDENCY CHECKS ---
|
| 12 |
try:
|
|
|
|
| 16 |
import srt
|
| 17 |
except ImportError as e:
|
| 18 |
print(f"❌ CRITICAL ERROR: Missing python library. {e}")
|
| 19 |
+
# Dummy definitions to allow app to start and show error in UI
|
| 20 |
cv2 = None
|
| 21 |
np = None
|
| 22 |
Image = None
|
| 23 |
srt = None
|
| 24 |
|
| 25 |
+
# --- 2. BACKEND MODULE IMPORTS (WITH DUMMY FALLBACKS) ---
|
| 26 |
+
# This allows the app to run even if complex backend modules fail to load
|
| 27 |
+
def dummy_function(*args, **kwargs): return 0, 0, None, None
|
|
|
|
|
|
|
| 28 |
|
| 29 |
try:
|
| 30 |
from backend.keyframes.keyframes import black_bar_crop
|
|
|
|
| 52 |
def __init__(self, panels, bubbles): self.panels, self.bubbles = panels, bubbles
|
| 53 |
|
| 54 |
try:
|
| 55 |
+
from backend.ai_enhanced_core import image_processor, comic_styler, face_detector, layout_optimizer
|
| 56 |
from backend.ai_bubble_placement import ai_bubble_placer
|
| 57 |
from backend.subtitles.subs_real import get_real_subtitles
|
| 58 |
+
from backend.keyframes.keyframes_simple import generate_keyframes_simple
|
| 59 |
+
except Exception:
|
| 60 |
+
# If backend fails, define minimal dummies to prevent crash
|
| 61 |
+
def get_real_subtitles(v): pass
|
|
|
|
|
|
|
|
|
|
| 62 |
class DummyDetector:
|
| 63 |
def detect_faces(self, p): return []
|
| 64 |
def get_lip_position(self, p, f): return -1, -1
|
| 65 |
face_detector = DummyDetector()
|
|
|
|
|
|
|
| 66 |
class DummyPlacer:
|
| 67 |
def place_bubble_ai(self, p, l): return 50, 20
|
| 68 |
ai_bubble_placer = DummyPlacer()
|
| 69 |
|
| 70 |
+
|
| 71 |
# --- FLASK APP SETUP ---
|
| 72 |
app = Flask(__name__)
|
|
|
|
| 73 |
BASE_USER_DIR = "userdata"
|
| 74 |
|
| 75 |
+
# --- HTML ---
|
| 76 |
INDEX_HTML = '''
|
| 77 |
<!DOCTYPE html>
|
| 78 |
<html lang="en">
|
|
|
|
| 93 |
@keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
|
| 94 |
|
| 95 |
/* COMIC STYLES */
|
| 96 |
+
.comic-page { background: white; width: 600px; height: 400px; position: relative; overflow: hidden; border: 2px solid #000; margin: 0 auto 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
|
| 97 |
.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; }
|
| 98 |
.panel { position: relative; overflow: hidden; border: 2px solid #000; background: #eee; }
|
| 99 |
.panel img { width: 100%; height: 100%; object-fit: cover; }
|
| 100 |
|
| 101 |
+
/* EXACT SHARK FIN SPEECH BUBBLE CSS */
|
| 102 |
.speech-bubble.speech {
|
| 103 |
+
--b: 3em; --h: 1.8em; --t: 0.6; --p: 50%; --r: 1.2em;
|
| 104 |
+
--c: #4ECDC4;
|
| 105 |
+
background: var(--c); color: #fff; padding: 1em; position: absolute;
|
| 106 |
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);
|
| 107 |
+
font-family: 'Comic Neue', cursive; font-weight: bold; font-size: 14px; text-align: center;
|
| 108 |
+
min-width: 80px; min-height: 40px; display: flex; align-items: center; justify-content: center;
|
| 109 |
cursor: move; z-index: 10;
|
| 110 |
}
|
| 111 |
+
/* Export-Safe Gradient instead of Mask */
|
| 112 |
.speech-bubble.speech:before {
|
| 113 |
content: ""; position: absolute; width: var(--b); height: var(--h);
|
| 114 |
background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
|
| 115 |
border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
|
| 116 |
+
top: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b)));
|
| 117 |
}
|
|
|
|
| 118 |
|
|
|
|
| 119 |
.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; }
|
| 120 |
+
.edit-controls button { width: 100%; margin-top: 5px; padding: 8px; cursor: pointer; background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; }
|
| 121 |
+
.edit-controls button:hover { background: #e0e0e0; }
|
| 122 |
</style>
|
| 123 |
</head>
|
| 124 |
<body>
|
|
|
|
| 127 |
<h1>🎬 Comic Generator</h1>
|
| 128 |
<input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fname').innerText=this.files[0].name">
|
| 129 |
<label for="file-upload" class="file-label">Choose Video</label>
|
| 130 |
+
<span id="fname">No file selected</span>
|
| 131 |
<button class="submit-btn" onclick="upload()">Generate</button>
|
| 132 |
<div id="loading" style="display:none;">
|
| 133 |
<div class="loader"></div>
|
|
|
|
| 139 |
<div id="editor-container">
|
| 140 |
<div id="comic-pages"></div>
|
| 141 |
<div class="edit-controls">
|
| 142 |
+
<h4>Editor Tools</h4>
|
| 143 |
<button onclick="exportToPng()">💾 Export PNG</button>
|
| 144 |
+
<button onclick="location.reload()" style="color:red;">↺ Start Over</button>
|
| 145 |
</div>
|
| 146 |
</div>
|
| 147 |
|
| 148 |
<script>
|
| 149 |
+
// CLIENT-SIDE SESSION ID GENERATION (Fixes "Session Lost" on Hugging Face)
|
| 150 |
+
function generateUUID() {
|
| 151 |
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
| 152 |
+
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
| 153 |
+
return v.toString(16);
|
| 154 |
+
});
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// Get or create SID
|
| 158 |
+
let sid = localStorage.getItem('comic_sid');
|
| 159 |
+
if(!sid) {
|
| 160 |
+
sid = generateUUID();
|
| 161 |
+
localStorage.setItem('comic_sid', sid);
|
| 162 |
+
}
|
| 163 |
+
console.log("Using Session ID:", sid);
|
| 164 |
+
|
| 165 |
let interval;
|
| 166 |
+
|
| 167 |
async function upload() {
|
| 168 |
const file = document.getElementById('file-upload').files[0];
|
| 169 |
if(!file) return alert("Select file");
|
|
|
|
| 174 |
document.querySelector('.upload-box').style.display='none';
|
| 175 |
document.getElementById('loading').style.display='block';
|
| 176 |
|
| 177 |
+
// Pass SID in URL
|
| 178 |
+
const res = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
|
| 179 |
if(res.ok) interval = setInterval(checkStatus, 2000);
|
| 180 |
else { alert("Upload failed"); location.reload(); }
|
| 181 |
}
|
| 182 |
|
| 183 |
async function checkStatus() {
|
| 184 |
+
const res = await fetch(`/status?sid=${sid}`);
|
| 185 |
const data = await res.json();
|
| 186 |
document.getElementById('status').innerText = data.message;
|
| 187 |
|
|
|
|
| 198 |
}
|
| 199 |
|
| 200 |
function loadComic() {
|
| 201 |
+
// Fetch pages.json using SID
|
| 202 |
+
fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
|
| 203 |
const c = document.getElementById('comic-pages');
|
| 204 |
+
c.innerHTML = ''; // clear
|
| 205 |
data.forEach((p, i) => {
|
| 206 |
const div = document.createElement('div');
|
| 207 |
div.className = 'comic-page';
|
|
|
|
| 213 |
const pDiv = document.createElement('div');
|
| 214 |
pDiv.className = 'panel';
|
| 215 |
const img = document.createElement('img');
|
| 216 |
+
// Fetch image using SID
|
| 217 |
+
img.src = `/frames/${pan.image}?sid=${sid}`;
|
| 218 |
pDiv.appendChild(img);
|
| 219 |
|
|
|
|
| 220 |
if(p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) {
|
| 221 |
const b = document.createElement('div');
|
| 222 |
+
b.className = 'speech-bubble speech';
|
| 223 |
b.innerText = p.bubbles[j].dialog;
|
|
|
|
| 224 |
b.style.left = (p.bubbles[j].bubble_offset_x || 50) + 'px';
|
| 225 |
b.style.top = (p.bubbles[j].bubble_offset_y || 20) + 'px';
|
| 226 |
pDiv.appendChild(b);
|
|
|
|
| 231 |
div.appendChild(grid);
|
| 232 |
c.appendChild(div);
|
| 233 |
});
|
| 234 |
+
}).catch(e => console.error(e));
|
| 235 |
}
|
| 236 |
|
| 237 |
function makeInteractive(el) {
|
| 238 |
el.onmousedown = function(e) {
|
| 239 |
e.stopPropagation();
|
|
|
|
| 240 |
const offX = e.clientX - el.offsetLeft;
|
| 241 |
const offY = e.clientY - el.offsetTop;
|
| 242 |
document.onmousemove = function(ev) {
|
|
|
|
| 249 |
|
| 250 |
async function exportToPng() {
|
| 251 |
const pages = document.querySelectorAll('.comic-page');
|
| 252 |
+
for(let i=0; i<pages.length; i++) {
|
| 253 |
+
try {
|
| 254 |
+
const url = await htmlToImage.toPng(pages[i], {pixelRatio: 3});
|
| 255 |
+
const a = document.createElement('a');
|
| 256 |
+
a.download = `comic-page-${i+1}.png`;
|
| 257 |
+
a.href = url;
|
| 258 |
+
a.click();
|
| 259 |
+
} catch(e) { console.error(e); alert("Export failed"); }
|
| 260 |
}
|
| 261 |
}
|
| 262 |
</script>
|
|
|
|
| 284 |
json.dump({'message': message, 'progress': progress}, f)
|
| 285 |
except: pass
|
| 286 |
|
| 287 |
+
def cleanup(self):
|
| 288 |
+
# Simple cleanup
|
| 289 |
+
if os.path.exists(self.frames_dir): shutil.rmtree(self.frames_dir)
|
| 290 |
+
os.makedirs(self.frames_dir, exist_ok=True)
|
| 291 |
+
|
| 292 |
def generate_comic(self):
|
| 293 |
try:
|
| 294 |
+
if cv2 is None: raise Exception("OpenCV missing on server.")
|
| 295 |
|
| 296 |
self.update_status("Processing Video...", 10)
|
| 297 |
cap = cv2.VideoCapture(self.video_path)
|
| 298 |
+
if not cap.isOpened(): raise Exception("Invalid Video")
|
| 299 |
|
| 300 |
+
self.video_fps = cap.get(cv2.CAP_PROP_FPS) or 25
|
|
|
|
|
|
|
| 301 |
cap.release()
|
| 302 |
|
| 303 |
+
# 1. Subtitles
|
| 304 |
self.update_status("Extracting Dialogue...", 30)
|
| 305 |
try:
|
| 306 |
+
get_real_subtitles(self.video_path) # Creates test1.srt
|
| 307 |
+
# Move to user dir if created in root
|
| 308 |
if os.path.exists('test1.srt'):
|
| 309 |
shutil.move('test1.srt', os.path.join(self.user_dir, 'subs.srt'))
|
| 310 |
+
except:
|
| 311 |
+
# Fallback dummy sub
|
|
|
|
| 312 |
with open(os.path.join(self.user_dir, 'subs.srt'), 'w') as f:
|
| 313 |
+
f.write("1\n00:00:01,000 --> 00:00:04,000\nSample Text\n")
|
| 314 |
|
| 315 |
+
# 2. Extract Frames
|
| 316 |
+
self.update_status("Generating Panels...", 50)
|
| 317 |
cap = cv2.VideoCapture(self.video_path)
|
| 318 |
|
|
|
|
| 319 |
subs_path = os.path.join(self.user_dir, 'subs.srt')
|
| 320 |
with open(subs_path, 'r', encoding='utf-8') as f:
|
| 321 |
subs = list(srt.parse(f.read()))
|
|
|
|
| 323 |
frame_files = []
|
| 324 |
bubbles = []
|
| 325 |
|
| 326 |
+
# Limit frames to avoid timeout
|
| 327 |
+
limit_subs = subs[:12]
|
| 328 |
|
| 329 |
+
for i, sub in enumerate(limit_subs):
|
| 330 |
+
mid = (sub.start.total_seconds() + sub.end.total_seconds()) / 2
|
| 331 |
+
cap.set(cv2.CAP_PROP_POS_MSEC, mid * 1000)
|
| 332 |
ret, frame = cap.read()
|
| 333 |
if ret:
|
| 334 |
fname = f"frame_{i}.png"
|
| 335 |
cv2.imwrite(os.path.join(self.frames_dir, fname), frame)
|
| 336 |
frame_files.append(fname)
|
|
|
|
| 337 |
bubbles.append(bubble(dialog=sub.content, bubble_offset_x=50, bubble_offset_y=20))
|
| 338 |
cap.release()
|
| 339 |
+
|
| 340 |
+
# 3. Assemble
|
| 341 |
+
self.update_status("Finalizing...", 80)
|
| 342 |
pages_data = []
|
|
|
|
| 343 |
for i in range(0, len(frame_files), 4):
|
| 344 |
+
batch_f = frame_files[i:i+4]
|
| 345 |
+
batch_b = bubbles[i:i+4]
|
| 346 |
+
panels = [{'image': f} for f in batch_f]
|
| 347 |
+
# safe object conversion
|
| 348 |
+
b_data = [b.__dict__ if hasattr(b, '__dict__') else b for b in batch_b]
|
|
|
|
| 349 |
pages_data.append({'panels': panels, 'bubbles': b_data})
|
| 350 |
|
| 351 |
with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
|
| 352 |
json.dump(pages_data, f)
|
| 353 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
self.update_status("Done!", 100)
|
| 355 |
|
| 356 |
except Exception as e:
|
| 357 |
traceback.print_exc()
|
| 358 |
+
self.update_status(f"Error: {str(e)}", -1)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
# --- ROUTES (SID REQUIRED IN QUERY PARAMS) ---
|
| 362 |
|
| 363 |
@app.route('/')
|
| 364 |
def index():
|
|
|
|
| 365 |
return INDEX_HTML
|
| 366 |
|
| 367 |
@app.route('/uploader', methods=['POST'])
|
| 368 |
def upload():
|
| 369 |
+
sid = request.args.get('sid')
|
| 370 |
+
if not sid: return "Missing SID", 400
|
| 371 |
+
|
| 372 |
if 'file' not in request.files: return "No file", 400
|
| 373 |
|
| 374 |
gen = EnhancedComicGenerator(sid)
|
| 375 |
+
gen.cleanup() # Clean previous run
|
| 376 |
request.files['file'].save(gen.video_path)
|
| 377 |
|
|
|
|
| 378 |
gen.update_status("Starting...", 5)
|
|
|
|
| 379 |
threading.Thread(target=gen.generate_comic).start()
|
| 380 |
return jsonify({'success': True})
|
| 381 |
|
| 382 |
@app.route('/status')
|
| 383 |
def get_status():
|
| 384 |
+
sid = request.args.get('sid')
|
| 385 |
+
if not sid: return jsonify({'message': 'No SID', 'progress': -1})
|
| 386 |
+
|
| 387 |
status_path = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
|
| 388 |
if os.path.exists(status_path):
|
| 389 |
return send_file(status_path)
|
| 390 |
+
return jsonify({'message': 'Waiting...', 'progress': 0})
|
| 391 |
|
| 392 |
@app.route('/output/<path:filename>')
|
| 393 |
def get_output(filename):
|
| 394 |
+
sid = request.args.get('sid')
|
| 395 |
+
if not sid: return "No SID", 400
|
| 396 |
+
return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
|
| 397 |
|
| 398 |
@app.route('/frames/<path:filename>')
|
| 399 |
def get_frame(filename):
|
| 400 |
+
sid = request.args.get('sid')
|
| 401 |
+
if not sid: return "No SID", 400
|
| 402 |
+
return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
|
| 403 |
|
| 404 |
if __name__ == '__main__':
|
| 405 |
os.makedirs(BASE_USER_DIR, exist_ok=True)
|
| 406 |
+
port = int(os.getenv("PORT", 7860))
|
| 407 |
+
app.run(host='0.0.0.0', port=port)
|