Update app_enhanced.py
Browse files- app_enhanced.py +44 -12
app_enhanced.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import spaces # <--- CRITICAL: MUST BE FIRST
|
| 2 |
import os
|
| 3 |
import time
|
| 4 |
import threading
|
|
@@ -116,8 +116,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 116 |
selected_moments = []
|
| 117 |
if not raw_moments:
|
| 118 |
times = np.linspace(1, duration-1, total_panels_needed)
|
| 119 |
-
for t in times:
|
| 120 |
-
selected_moments.append({'text': '', 'start': t, 'end': t+1})
|
| 121 |
elif len(raw_moments) <= total_panels_needed:
|
| 122 |
selected_moments = raw_moments
|
| 123 |
else:
|
|
@@ -133,7 +132,6 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 133 |
for i, moment in enumerate(selected_moments):
|
| 134 |
mid = (moment['start'] + moment['end']) / 2
|
| 135 |
if mid > duration: mid = duration - 1
|
| 136 |
-
|
| 137 |
cap.set(cv2.CAP_PROP_POS_FRAMES, int(mid * fps))
|
| 138 |
ret, frame = cap.read()
|
| 139 |
if ret:
|
|
@@ -141,7 +139,6 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 141 |
p = os.path.join(frames_dir, fname)
|
| 142 |
cv2.imwrite(p, frame)
|
| 143 |
os.sync()
|
| 144 |
-
|
| 145 |
frame_metadata[fname] = {'dialogue': moment['text'], 'time': mid}
|
| 146 |
frame_files_ordered.append(fname)
|
| 147 |
count += 1
|
|
@@ -298,7 +295,7 @@ class EnhancedComicGenerator:
|
|
| 298 |
json.dump({'message': msg, 'progress': prog}, f)
|
| 299 |
|
| 300 |
# ======================================================
|
| 301 |
-
# 🌐 ROUTES &
|
| 302 |
# ======================================================
|
| 303 |
|
| 304 |
INDEX_HTML = '''
|
|
@@ -340,6 +337,7 @@ INDEX_HTML = '''
|
|
| 340 |
.loader { width: 120px; height: 20px; background: radial-gradient(circle 10px, #e67e22 100%, transparent 0); background-size: 20px 20px; animation: ball 1s infinite linear; margin: 20px auto; }
|
| 341 |
@keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
|
| 342 |
|
|
|
|
| 343 |
.comic-wrapper { max-width: 1000px; margin: 0 auto; }
|
| 344 |
.page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
|
| 345 |
.page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
|
|
@@ -351,7 +349,7 @@ INDEX_HTML = '''
|
|
| 351 |
.panel img.pannable { cursor: grab; }
|
| 352 |
.panel img.panning { cursor: grabbing; }
|
| 353 |
|
| 354 |
-
/* BUBBLES
|
| 355 |
.speech-bubble {
|
| 356 |
position: absolute; display: flex; justify-content: center; align-items: center;
|
| 357 |
width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
|
|
@@ -359,11 +357,12 @@ INDEX_HTML = '''
|
|
| 359 |
font-size: 13px; text-align: center; overflow: visible;
|
| 360 |
--tail-pos: 50%;
|
| 361 |
}
|
|
|
|
| 362 |
.bubble-text { padding: 0.8em; word-wrap: break-word; position: relative; z-index: 5; pointer-events: none; user-select: none; }
|
| 363 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
|
| 364 |
.speech-bubble textarea { position: absolute; top:0; left:0; width:100%; height:100%; box-sizing:border-box; border:1px solid #4CAF50; background:rgba(255,255,255,0.95); text-align:center; padding:8px; z-index:102; resize:none; font: inherit; }
|
| 365 |
|
| 366 |
-
/* BUBBLE
|
| 367 |
.speech-bubble.speech {
|
| 368 |
--b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
|
| 369 |
background: var(--bubble-fill-color, #4ECDC4);
|
|
@@ -386,11 +385,25 @@ INDEX_HTML = '''
|
|
| 386 |
.speech-bubble.speech.tail-right { border-radius: var(--r); }
|
| 387 |
.speech-bubble.speech.tail-right:before { left: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: rotate(-90deg); transform-origin: top left; }
|
| 388 |
|
|
|
|
| 389 |
.speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
|
| 390 |
-
.speech-bubble.thought::before { display:none; }
|
| 391 |
.thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
|
| 392 |
-
.thought-dot-1 { width: 20px; height: 20px;
|
| 393 |
-
.thought-dot-2 { width: 12px; height: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
|
| 395 |
.speech-bubble.reaction { background: #FFD700; border: 3px solid #E53935; color: #D32F2F; font-weight: 900; text-transform: uppercase; clip-path: polygon(0% 25%, 17% 21%, 17% 0%, 31% 16%, 50% 4%, 69% 16%, 83% 0%, 83% 21%, 100% 25%, 85% 45%, 95% 62%, 82% 79%, 100% 97%, 79% 89%, 60% 98%, 46% 82%, 27% 95%, 15% 78%, 5% 62%, 15% 45%); }
|
| 396 |
.speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
|
|
@@ -700,6 +713,8 @@ INDEX_HTML = '''
|
|
| 700 |
const b = document.createElement('div');
|
| 701 |
const type = data.type || 'speech';
|
| 702 |
b.className = data.classes || `speech-bubble ${type} tail-bottom`;
|
|
|
|
|
|
|
| 703 |
b.dataset.type = type;
|
| 704 |
b.style.left = data.left; b.style.top = data.top;
|
| 705 |
if(data.width) b.style.width = data.width; if(data.height) b.style.height = data.height;
|
|
@@ -712,11 +727,12 @@ INDEX_HTML = '''
|
|
| 712 |
if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; b.appendChild(d); } }
|
| 713 |
|
| 714 |
['nw', 'ne', 'sw', 'se'].forEach(dir => { const handle = document.createElement('div'); handle.className = `resize-handle ${dir}`; handle.onmousedown = (e) => startResize(e, dir); b.appendChild(handle); });
|
|
|
|
| 715 |
b.onmousedown = (e) => {
|
| 716 |
if(e.target.classList.contains('resize-handle')) return;
|
| 717 |
e.stopPropagation(); selectBubble(b); isDragging = true; startX = e.clientX; startY = e.clientY; initX = b.offsetLeft; initY = b.offsetTop;
|
| 718 |
};
|
| 719 |
-
b.onclick = (e) => { e.stopPropagation(); };
|
| 720 |
b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
|
| 721 |
return b;
|
| 722 |
}
|
|
@@ -779,24 +795,40 @@ INDEX_HTML = '''
|
|
| 779 |
if(!selectedBubble) return;
|
| 780 |
selectedBubble.dataset.type = type;
|
| 781 |
selectedBubble.className = 'speech-bubble ' + type + ' selected';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
selectedBubble.querySelectorAll('.thought-dot').forEach(d=>d.remove());
|
| 783 |
if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; selectedBubble.appendChild(d); } }
|
| 784 |
saveDraft();
|
| 785 |
}
|
| 786 |
|
| 787 |
function changeFont(font) { if(!selectedBubble) return; selectedBubble.style.fontFamily = font; saveDraft(); }
|
|
|
|
| 788 |
function rotateTail() {
|
| 789 |
if(!selectedBubble) return;
|
| 790 |
const type = selectedBubble.dataset.type;
|
|
|
|
| 791 |
if(type === 'speech') {
|
| 792 |
const positions = ['tail-bottom', 'tail-right', 'tail-top', 'tail-left'];
|
| 793 |
let current = 0;
|
| 794 |
positions.forEach((pos, i) => { if(selectedBubble.classList.contains(pos)) current = i; });
|
| 795 |
selectedBubble.classList.remove(positions[current]);
|
| 796 |
selectedBubble.classList.add(positions[(current + 1) % 4]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 797 |
}
|
| 798 |
saveDraft();
|
| 799 |
}
|
|
|
|
| 800 |
function slideTail(v) { if(selectedBubble) { selectedBubble.style.setProperty('--tail-pos', v+'%'); saveDraft(); } }
|
| 801 |
|
| 802 |
document.getElementById('bubble-text-color').addEventListener('input', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(); } });
|
|
|
|
| 1 |
+
import spaces # <--- CRITICAL: MUST BE THE FIRST IMPORT
|
| 2 |
import os
|
| 3 |
import time
|
| 4 |
import threading
|
|
|
|
| 116 |
selected_moments = []
|
| 117 |
if not raw_moments:
|
| 118 |
times = np.linspace(1, duration-1, total_panels_needed)
|
| 119 |
+
for t in times: selected_moments.append({'text': '', 'start': t, 'end': t+1})
|
|
|
|
| 120 |
elif len(raw_moments) <= total_panels_needed:
|
| 121 |
selected_moments = raw_moments
|
| 122 |
else:
|
|
|
|
| 132 |
for i, moment in enumerate(selected_moments):
|
| 133 |
mid = (moment['start'] + moment['end']) / 2
|
| 134 |
if mid > duration: mid = duration - 1
|
|
|
|
| 135 |
cap.set(cv2.CAP_PROP_POS_FRAMES, int(mid * fps))
|
| 136 |
ret, frame = cap.read()
|
| 137 |
if ret:
|
|
|
|
| 139 |
p = os.path.join(frames_dir, fname)
|
| 140 |
cv2.imwrite(p, frame)
|
| 141 |
os.sync()
|
|
|
|
| 142 |
frame_metadata[fname] = {'dialogue': moment['text'], 'time': mid}
|
| 143 |
frame_files_ordered.append(fname)
|
| 144 |
count += 1
|
|
|
|
| 295 |
json.dump({'message': msg, 'progress': prog}, f)
|
| 296 |
|
| 297 |
# ======================================================
|
| 298 |
+
# 🌐 ROUTES & FULL UI
|
| 299 |
# ======================================================
|
| 300 |
|
| 301 |
INDEX_HTML = '''
|
|
|
|
| 337 |
.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; }
|
| 338 |
@keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
|
| 339 |
|
| 340 |
+
/* COMIC LAYOUT */
|
| 341 |
.comic-wrapper { max-width: 1000px; margin: 0 auto; }
|
| 342 |
.page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
|
| 343 |
.page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
|
|
|
|
| 349 |
.panel img.pannable { cursor: grab; }
|
| 350 |
.panel img.panning { cursor: grabbing; }
|
| 351 |
|
| 352 |
+
/* SPEECH BUBBLES */
|
| 353 |
.speech-bubble {
|
| 354 |
position: absolute; display: flex; justify-content: center; align-items: center;
|
| 355 |
width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
|
|
|
|
| 357 |
font-size: 13px; text-align: center; overflow: visible;
|
| 358 |
--tail-pos: 50%;
|
| 359 |
}
|
| 360 |
+
/* POINTER EVENTS NONE ON TEXT TO FIX SELECTION BUG */
|
| 361 |
.bubble-text { padding: 0.8em; word-wrap: break-word; position: relative; z-index: 5; pointer-events: none; user-select: none; }
|
| 362 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
|
| 363 |
.speech-bubble textarea { position: absolute; top:0; left:0; width:100%; height:100%; box-sizing:border-box; border:1px solid #4CAF50; background:rgba(255,255,255,0.95); text-align:center; padding:8px; z-index:102; resize:none; font: inherit; }
|
| 364 |
|
| 365 |
+
/* SPEECH BUBBLE CSS (Tails) */
|
| 366 |
.speech-bubble.speech {
|
| 367 |
--b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
|
| 368 |
background: var(--bubble-fill-color, #4ECDC4);
|
|
|
|
| 385 |
.speech-bubble.speech.tail-right { border-radius: var(--r); }
|
| 386 |
.speech-bubble.speech.tail-right:before { left: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: rotate(-90deg); transform-origin: top left; }
|
| 387 |
|
| 388 |
+
/* THOUGHT BUBBLE CSS (Fixed Rotation) */
|
| 389 |
.speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
|
| 390 |
+
.speech-bubble.thought::before { display:none; }
|
| 391 |
.thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
|
| 392 |
+
.thought-dot-1 { width: 20px; height: 20px; }
|
| 393 |
+
.thought-dot-2 { width: 12px; height: 12px; }
|
| 394 |
+
|
| 395 |
+
/* Thought Tail Positions */
|
| 396 |
+
.speech-bubble.thought.pos-bl .thought-dot-1 { left: 20px; bottom: -20px; }
|
| 397 |
+
.speech-bubble.thought.pos-bl .thought-dot-2 { left: 10px; bottom: -32px; }
|
| 398 |
+
|
| 399 |
+
.speech-bubble.thought.pos-br .thought-dot-1 { right: 20px; bottom: -20px; }
|
| 400 |
+
.speech-bubble.thought.pos-br .thought-dot-2 { right: 10px; bottom: -32px; }
|
| 401 |
+
|
| 402 |
+
.speech-bubble.thought.pos-tr .thought-dot-1 { right: 20px; top: -20px; }
|
| 403 |
+
.speech-bubble.thought.pos-tr .thought-dot-2 { right: 10px; top: -32px; }
|
| 404 |
+
|
| 405 |
+
.speech-bubble.thought.pos-tl .thought-dot-1 { left: 20px; top: -20px; }
|
| 406 |
+
.speech-bubble.thought.pos-tl .thought-dot-2 { left: 10px; top: -32px; }
|
| 407 |
|
| 408 |
.speech-bubble.reaction { background: #FFD700; border: 3px solid #E53935; color: #D32F2F; font-weight: 900; text-transform: uppercase; clip-path: polygon(0% 25%, 17% 21%, 17% 0%, 31% 16%, 50% 4%, 69% 16%, 83% 0%, 83% 21%, 100% 25%, 85% 45%, 95% 62%, 82% 79%, 100% 97%, 79% 89%, 60% 98%, 46% 82%, 27% 95%, 15% 78%, 5% 62%, 15% 45%); }
|
| 409 |
.speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
|
|
|
|
| 713 |
const b = document.createElement('div');
|
| 714 |
const type = data.type || 'speech';
|
| 715 |
b.className = data.classes || `speech-bubble ${type} tail-bottom`;
|
| 716 |
+
if (type === 'thought' && !b.className.includes('pos-')) b.className += ' pos-bl'; // Default position for thought
|
| 717 |
+
|
| 718 |
b.dataset.type = type;
|
| 719 |
b.style.left = data.left; b.style.top = data.top;
|
| 720 |
if(data.width) b.style.width = data.width; if(data.height) b.style.height = data.height;
|
|
|
|
| 727 |
if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; b.appendChild(d); } }
|
| 728 |
|
| 729 |
['nw', 'ne', 'sw', 'se'].forEach(dir => { const handle = document.createElement('div'); handle.className = `resize-handle ${dir}`; handle.onmousedown = (e) => startResize(e, dir); b.appendChild(handle); });
|
| 730 |
+
|
| 731 |
b.onmousedown = (e) => {
|
| 732 |
if(e.target.classList.contains('resize-handle')) return;
|
| 733 |
e.stopPropagation(); selectBubble(b); isDragging = true; startX = e.clientX; startY = e.clientY; initX = b.offsetLeft; initY = b.offsetTop;
|
| 734 |
};
|
| 735 |
+
b.onclick = (e) => { e.stopPropagation(); };
|
| 736 |
b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
|
| 737 |
return b;
|
| 738 |
}
|
|
|
|
| 795 |
if(!selectedBubble) return;
|
| 796 |
selectedBubble.dataset.type = type;
|
| 797 |
selectedBubble.className = 'speech-bubble ' + type + ' selected';
|
| 798 |
+
|
| 799 |
+
// Default tail for thought is pos-bl if not set
|
| 800 |
+
if(type === 'thought') selectedBubble.classList.add('pos-bl');
|
| 801 |
+
else selectedBubble.classList.add('tail-bottom'); // Default for speech
|
| 802 |
+
|
| 803 |
selectedBubble.querySelectorAll('.thought-dot').forEach(d=>d.remove());
|
| 804 |
if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; selectedBubble.appendChild(d); } }
|
| 805 |
saveDraft();
|
| 806 |
}
|
| 807 |
|
| 808 |
function changeFont(font) { if(!selectedBubble) return; selectedBubble.style.fontFamily = font; saveDraft(); }
|
| 809 |
+
|
| 810 |
function rotateTail() {
|
| 811 |
if(!selectedBubble) return;
|
| 812 |
const type = selectedBubble.dataset.type;
|
| 813 |
+
|
| 814 |
if(type === 'speech') {
|
| 815 |
const positions = ['tail-bottom', 'tail-right', 'tail-top', 'tail-left'];
|
| 816 |
let current = 0;
|
| 817 |
positions.forEach((pos, i) => { if(selectedBubble.classList.contains(pos)) current = i; });
|
| 818 |
selectedBubble.classList.remove(positions[current]);
|
| 819 |
selectedBubble.classList.add(positions[(current + 1) % 4]);
|
| 820 |
+
}
|
| 821 |
+
else if (type === 'thought') {
|
| 822 |
+
// Cycle specifically through the 4 CSS positions we defined
|
| 823 |
+
const positions = ['pos-bl', 'pos-br', 'pos-tr', 'pos-tl'];
|
| 824 |
+
let current = 0;
|
| 825 |
+
positions.forEach((pos, i) => { if(selectedBubble.classList.contains(pos)) current = i; });
|
| 826 |
+
selectedBubble.classList.remove(positions[current]);
|
| 827 |
+
selectedBubble.classList.add(positions[(current + 1) % 4]);
|
| 828 |
}
|
| 829 |
saveDraft();
|
| 830 |
}
|
| 831 |
+
|
| 832 |
function slideTail(v) { if(selectedBubble) { selectedBubble.style.setProperty('--tail-pos', v+'%'); saveDraft(); } }
|
| 833 |
|
| 834 |
document.getElementById('bubble-text-color').addEventListener('input', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(); } });
|