Update app_enhanced.py
Browse files- app_enhanced.py +45 -34
app_enhanced.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import spaces
|
| 2 |
import os
|
| 3 |
import time
|
| 4 |
import threading
|
|
@@ -28,8 +28,10 @@ def gpu_warmup():
|
|
| 28 |
# ======================================================
|
| 29 |
if os.path.exists('/data'):
|
| 30 |
BASE_STORAGE_PATH = '/data'
|
|
|
|
| 31 |
else:
|
| 32 |
BASE_STORAGE_PATH = '.'
|
|
|
|
| 33 |
|
| 34 |
BASE_USER_DIR = os.path.join(BASE_STORAGE_PATH, "userdata")
|
| 35 |
SAVED_COMICS_DIR = os.path.join(BASE_STORAGE_PATH, "saved_comics")
|
|
@@ -51,7 +53,7 @@ def generate_save_code(length=8):
|
|
| 51 |
# 🧱 DATA CLASSES
|
| 52 |
# ======================================================
|
| 53 |
def bubble(dialog="", x=50, y=20, type='speech'):
|
| 54 |
-
#
|
| 55 |
classes = f"speech-bubble {type}"
|
| 56 |
if type == 'speech':
|
| 57 |
classes += " tail-bottom"
|
|
@@ -102,15 +104,19 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 102 |
if os.path.exists('test1.srt'):
|
| 103 |
shutil.move('test1.srt', user_srt)
|
| 104 |
elif not os.path.exists(user_srt):
|
| 105 |
-
with open(user_srt, 'w') as f:
|
|
|
|
| 106 |
except:
|
| 107 |
-
with open(user_srt, 'w') as f:
|
|
|
|
| 108 |
|
| 109 |
with open(user_srt, 'r', encoding='utf-8') as f:
|
| 110 |
-
try:
|
| 111 |
-
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
valid_subs = [s for s in all_subs if s.content.strip()]
|
| 114 |
if valid_subs:
|
| 115 |
raw_moments = [{'text': s.content.strip(), 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
|
| 116 |
else:
|
|
@@ -140,14 +146,14 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 140 |
cap.set(cv2.CAP_PROP_POS_MSEC, mid * 1000)
|
| 141 |
ret, frame = cap.read()
|
| 142 |
if ret:
|
| 143 |
-
# 🎯 SQUARE PADDING (0% Cut)
|
| 144 |
h, w = frame.shape[:2]
|
| 145 |
sq_dim = max(h, w)
|
| 146 |
square_img = np.zeros((sq_dim, sq_dim, 3), dtype=np.uint8)
|
| 147 |
x_off = (sq_dim - w) // 2
|
| 148 |
y_off = (sq_dim - h) // 2
|
| 149 |
square_img[y_off:y_off+h, x_off:x_off+w] = frame
|
| 150 |
-
# Resize to standard high res
|
| 151 |
square_img = cv2.resize(square_img, (1024, 1024))
|
| 152 |
|
| 153 |
fname = f"frame_{count:04d}.png"
|
|
@@ -185,7 +191,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 185 |
bx, by = 550, 450
|
| 186 |
else:
|
| 187 |
bx, by = 50, 50
|
| 188 |
-
|
| 189 |
bubbles_list.append(bubble(dialog=dialogue, x=bx, y=by, type=b_type))
|
| 190 |
|
| 191 |
pages = []
|
|
@@ -200,21 +206,14 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 200 |
img = np.zeros((1024, 1024, 3), dtype=np.uint8); img[:] = (30,30,30)
|
| 201 |
cv2.imwrite(os.path.join(frames_dir, fname), img)
|
| 202 |
p_frames.append(fname)
|
| 203 |
-
# Add
|
| 204 |
p_bubbles.append(bubble(dialog="", x=-999, y=-999, type='speech'))
|
| 205 |
|
| 206 |
if p_frames:
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
result = []
|
| 212 |
-
for pg in pages:
|
| 213 |
-
p_data = [p if isinstance(p, dict) else p.__dict__ for p in pg.panels]
|
| 214 |
-
b_data = [b if isinstance(b, dict) else b.__dict__ for b in pg.bubbles]
|
| 215 |
-
result.append({'panels': p_data, 'bubbles': b_data})
|
| 216 |
-
|
| 217 |
-
return result
|
| 218 |
|
| 219 |
@spaces.GPU
|
| 220 |
def regen_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
|
|
@@ -341,22 +340,34 @@ INDEX_HTML = '''
|
|
| 341 |
.page-wrapper { display: flex; flex-direction: column; align-items: center; }
|
| 342 |
.page-title { text-align: center; color: #eee; margin-bottom: 10px; font-size: 20px; font-weight: bold; }
|
| 343 |
|
|
|
|
| 344 |
.comic-page {
|
| 345 |
width: 800px;
|
| 346 |
height: 800px;
|
| 347 |
background: white;
|
| 348 |
box-shadow: 0 5px 30px rgba(0,0,0,0.6);
|
| 349 |
position: relative; overflow: hidden;
|
| 350 |
-
border:
|
| 351 |
}
|
| 352 |
|
|
|
|
| 353 |
.comic-grid {
|
| 354 |
-
width: 100%; height: 100%; position: relative;
|
| 355 |
-
|
|
|
|
|
|
|
| 356 |
}
|
| 357 |
|
| 358 |
.panel { position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; background: #1a1a1a; cursor: grab; }
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
.panel img.panning { cursor: grabbing; transition: none; }
|
| 361 |
.panel.selected { outline: 4px solid #3498db; z-index: 5; }
|
| 362 |
|
|
@@ -368,12 +379,13 @@ INDEX_HTML = '''
|
|
| 368 |
|
| 369 |
/* Handles */
|
| 370 |
.handle { position: absolute; width: 26px; height: 26px; border: 3px solid white; border-radius: 50%; transform: translate(-50%, -50%); z-index: 101; cursor: ew-resize; box-shadow: 0 2px 5px rgba(0,0,0,0.8); }
|
|
|
|
| 371 |
.h-t1 { background: #3498db; left: var(--t1); top: 0%; margin-top: 15px; }
|
| 372 |
.h-t2 { background: #3498db; left: var(--t2); top: 50%; margin-top: -15px; }
|
| 373 |
.h-b1 { background: #2ecc71; left: var(--b1); top: 50%; margin-top: 15px; }
|
| 374 |
.h-b2 { background: #2ecc71; left: var(--b2); top: 100%; margin-top: -15px; }
|
| 375 |
|
| 376 |
-
/* SPEECH BUBBLES
|
| 377 |
.speech-bubble {
|
| 378 |
position: absolute; display: flex; justify-content: center; align-items: center;
|
| 379 |
min-width: 60px; min-height: 40px; box-sizing: border-box;
|
|
@@ -702,8 +714,8 @@ INDEX_HTML = '''
|
|
| 702 |
const type = data.type || 'speech';
|
| 703 |
let className = data.classes || `speech-bubble ${type} tail-bottom`;
|
| 704 |
if (type === 'thought' && !className.includes('pos-')) className += ' pos-bl';
|
| 705 |
-
b.className = className;
|
| 706 |
|
|
|
|
| 707 |
b.dataset.type = type;
|
| 708 |
b.style.left = data.left; b.style.top = data.top;
|
| 709 |
if(data.width) b.style.width = data.width;
|
|
@@ -714,12 +726,9 @@ INDEX_HTML = '''
|
|
| 714 |
|
| 715 |
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); } }
|
| 716 |
|
| 717 |
-
const
|
| 718 |
-
const resizer = document.createElement('div'); resizer.className = 'resize-handle';
|
| 719 |
-
resizer.onmousedown = (e) => { e.stopPropagation(); dragType='resize'; activeObj={b:b, startW:b.offsetWidth, startH:b.offsetHeight, mx:e.clientX, my:e.clientY}; };
|
| 720 |
-
b.appendChild(resizer);
|
| 721 |
|
| 722 |
-
b.onmousedown = (e) => { if(e.target
|
| 723 |
b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
|
| 724 |
return b;
|
| 725 |
}
|
|
@@ -759,6 +768,8 @@ INDEX_HTML = '''
|
|
| 759 |
dragType = null; activeObj = null;
|
| 760 |
});
|
| 761 |
|
|
|
|
|
|
|
| 762 |
function selectBubble(el) {
|
| 763 |
if(selectedBubble) selectedBubble.classList.remove('selected');
|
| 764 |
selectedBubble = el; el.classList.add('selected');
|
|
@@ -857,7 +868,7 @@ INDEX_HTML = '''
|
|
| 857 |
const layout = { t1: grid.style.getPropertyValue('--t1')||'100%', t2: grid.style.getPropertyValue('--t2')||'100%', b1: grid.style.getPropertyValue('--b1')||'100%', b2: grid.style.getPropertyValue('--b2')||'100%' };
|
| 858 |
const bubbles = [];
|
| 859 |
grid.querySelectorAll('.speech-bubble').forEach(b => {
|
| 860 |
-
bubbles.push({ text: b.querySelector('.bubble-text').textContent, left: b.style.left, top: b.style.top, width: b.style.width, height: b.style.height, type: b.dataset.type, colors: { fill: b.style.getPropertyValue('--bubble-fill'), text: b.style.getPropertyValue('--bubble-text') }, tailPos: b.style.getPropertyValue('--tail-pos') });
|
| 861 |
});
|
| 862 |
const panels = [];
|
| 863 |
grid.querySelectorAll('.panel').forEach(pan => {
|
|
|
|
| 1 |
+
import spaces # <--- CRITICAL: MUST BE THE FIRST IMPORT
|
| 2 |
import os
|
| 3 |
import time
|
| 4 |
import threading
|
|
|
|
| 28 |
# ======================================================
|
| 29 |
if os.path.exists('/data'):
|
| 30 |
BASE_STORAGE_PATH = '/data'
|
| 31 |
+
print("✅ Using Persistent Storage at /data")
|
| 32 |
else:
|
| 33 |
BASE_STORAGE_PATH = '.'
|
| 34 |
+
print("⚠️ Using Ephemeral Storage")
|
| 35 |
|
| 36 |
BASE_USER_DIR = os.path.join(BASE_STORAGE_PATH, "userdata")
|
| 37 |
SAVED_COMICS_DIR = os.path.join(BASE_STORAGE_PATH, "saved_comics")
|
|
|
|
| 53 |
# 🧱 DATA CLASSES
|
| 54 |
# ======================================================
|
| 55 |
def bubble(dialog="", x=50, y=20, type='speech'):
|
| 56 |
+
# Apply classes
|
| 57 |
classes = f"speech-bubble {type}"
|
| 58 |
if type == 'speech':
|
| 59 |
classes += " tail-bottom"
|
|
|
|
| 104 |
if os.path.exists('test1.srt'):
|
| 105 |
shutil.move('test1.srt', user_srt)
|
| 106 |
elif not os.path.exists(user_srt):
|
| 107 |
+
with open(user_srt, 'w') as f:
|
| 108 |
+
f.write("1\n00:00:01,000 --> 00:00:04,000\n...\n")
|
| 109 |
except:
|
| 110 |
+
with open(user_srt, 'w') as f:
|
| 111 |
+
f.write("1\n00:00:01,000 --> 00:00:04,000\n...\n")
|
| 112 |
|
| 113 |
with open(user_srt, 'r', encoding='utf-8') as f:
|
| 114 |
+
try:
|
| 115 |
+
all_subs = list(srt.parse(f.read()))
|
| 116 |
+
except:
|
| 117 |
+
all_subs = []
|
| 118 |
|
| 119 |
+
valid_subs = [s for s in all_subs if s.content and s.content.strip()]
|
| 120 |
if valid_subs:
|
| 121 |
raw_moments = [{'text': s.content.strip(), 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
|
| 122 |
else:
|
|
|
|
| 146 |
cap.set(cv2.CAP_PROP_POS_MSEC, mid * 1000)
|
| 147 |
ret, frame = cap.read()
|
| 148 |
if ret:
|
| 149 |
+
# 🎯 SQUARE PADDING (0% Cut) - HD Output
|
| 150 |
h, w = frame.shape[:2]
|
| 151 |
sq_dim = max(h, w)
|
| 152 |
square_img = np.zeros((sq_dim, sq_dim, 3), dtype=np.uint8)
|
| 153 |
x_off = (sq_dim - w) // 2
|
| 154 |
y_off = (sq_dim - h) // 2
|
| 155 |
square_img[y_off:y_off+h, x_off:x_off+w] = frame
|
| 156 |
+
# Resize to standard high res (1024x1024)
|
| 157 |
square_img = cv2.resize(square_img, (1024, 1024))
|
| 158 |
|
| 159 |
fname = f"frame_{count:04d}.png"
|
|
|
|
| 191 |
bx, by = 550, 450
|
| 192 |
else:
|
| 193 |
bx, by = 50, 50
|
| 194 |
+
|
| 195 |
bubbles_list.append(bubble(dialog=dialogue, x=bx, y=by, type=b_type))
|
| 196 |
|
| 197 |
pages = []
|
|
|
|
| 206 |
img = np.zeros((1024, 1024, 3), dtype=np.uint8); img[:] = (30,30,30)
|
| 207 |
cv2.imwrite(os.path.join(frames_dir, fname), img)
|
| 208 |
p_frames.append(fname)
|
| 209 |
+
# Add dummy bubble to keep count synced, but off-screen or empty
|
| 210 |
p_bubbles.append(bubble(dialog="", x=-999, y=-999, type='speech'))
|
| 211 |
|
| 212 |
if p_frames:
|
| 213 |
+
pg_panels = [{'image': f} for f in p_frames]
|
| 214 |
+
pages.append({'panels': pg_panels, 'bubbles': p_bubbles})
|
| 215 |
+
|
| 216 |
+
return pages
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
@spaces.GPU
|
| 219 |
def regen_frame_gpu(video_path, frames_dir, metadata_path, fname, direction):
|
|
|
|
| 340 |
.page-wrapper { display: flex; flex-direction: column; align-items: center; }
|
| 341 |
.page-title { text-align: center; color: #eee; margin-bottom: 10px; font-size: 20px; font-weight: bold; }
|
| 342 |
|
| 343 |
+
/* 🎯 5px White Border on Page */
|
| 344 |
.comic-page {
|
| 345 |
width: 800px;
|
| 346 |
height: 800px;
|
| 347 |
background: white;
|
| 348 |
box-shadow: 0 5px 30px rgba(0,0,0,0.6);
|
| 349 |
position: relative; overflow: hidden;
|
| 350 |
+
border: 5px solid #ffffff;
|
| 351 |
}
|
| 352 |
|
| 353 |
+
/* 🎯 5px White Borders between panels */
|
| 354 |
.comic-grid {
|
| 355 |
+
width: 100%; height: 100%; position: relative;
|
| 356 |
+
background: #ffffff; /* White background makes white lines */
|
| 357 |
+
--y: 50%; --t1: 100%; --t2: 100%; --b1: 100%; --b2: 100%;
|
| 358 |
+
--gap: 5px; /* 5px gap size */
|
| 359 |
}
|
| 360 |
|
| 361 |
.panel { position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; background: #1a1a1a; cursor: grab; }
|
| 362 |
+
|
| 363 |
+
/* IMAGE: Cover ensures it fills. Zoom allows control. */
|
| 364 |
+
.panel img {
|
| 365 |
+
width: 100%; height: 100%;
|
| 366 |
+
object-fit: cover;
|
| 367 |
+
transform-origin: center;
|
| 368 |
+
transition: transform 0.05s ease-out;
|
| 369 |
+
display: block;
|
| 370 |
+
}
|
| 371 |
.panel img.panning { cursor: grabbing; transition: none; }
|
| 372 |
.panel.selected { outline: 4px solid #3498db; z-index: 5; }
|
| 373 |
|
|
|
|
| 379 |
|
| 380 |
/* Handles */
|
| 381 |
.handle { position: absolute; width: 26px; height: 26px; border: 3px solid white; border-radius: 50%; transform: translate(-50%, -50%); z-index: 101; cursor: ew-resize; box-shadow: 0 2px 5px rgba(0,0,0,0.8); }
|
| 382 |
+
.handle:hover { transform: translate(-50%, -50%) scale(1.3); }
|
| 383 |
.h-t1 { background: #3498db; left: var(--t1); top: 0%; margin-top: 15px; }
|
| 384 |
.h-t2 { background: #3498db; left: var(--t2); top: 50%; margin-top: -15px; }
|
| 385 |
.h-b1 { background: #2ecc71; left: var(--b1); top: 50%; margin-top: 15px; }
|
| 386 |
.h-b2 { background: #2ecc71; left: var(--b2); top: 100%; margin-top: -15px; }
|
| 387 |
|
| 388 |
+
/* SPEECH BUBBLES */
|
| 389 |
.speech-bubble {
|
| 390 |
position: absolute; display: flex; justify-content: center; align-items: center;
|
| 391 |
min-width: 60px; min-height: 40px; box-sizing: border-box;
|
|
|
|
| 714 |
const type = data.type || 'speech';
|
| 715 |
let className = data.classes || `speech-bubble ${type} tail-bottom`;
|
| 716 |
if (type === 'thought' && !className.includes('pos-')) className += ' pos-bl';
|
|
|
|
| 717 |
|
| 718 |
+
b.className = className;
|
| 719 |
b.dataset.type = type;
|
| 720 |
b.style.left = data.left; b.style.top = data.top;
|
| 721 |
if(data.width) b.style.width = data.width;
|
|
|
|
| 726 |
|
| 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) => { if(e.target.classList.contains('resize-handle')) return; e.stopPropagation(); selectBubble(b); dragType = 'bubble'; activeObj = b; dragStart = {x: e.clientX, y: e.clientY}; };
|
| 732 |
b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
|
| 733 |
return b;
|
| 734 |
}
|
|
|
|
| 768 |
dragType = null; activeObj = null;
|
| 769 |
});
|
| 770 |
|
| 771 |
+
function startResize(e, dir) { e.preventDefault(); e.stopPropagation(); dragType = 'resize'; activeObj = { b: e.target.parentElement, startW: e.target.parentElement.offsetWidth, startH: e.target.parentElement.offsetHeight, mx: e.clientX, my: e.clientY }; }
|
| 772 |
+
|
| 773 |
function selectBubble(el) {
|
| 774 |
if(selectedBubble) selectedBubble.classList.remove('selected');
|
| 775 |
selectedBubble = el; el.classList.add('selected');
|
|
|
|
| 868 |
const layout = { t1: grid.style.getPropertyValue('--t1')||'100%', t2: grid.style.getPropertyValue('--t2')||'100%', b1: grid.style.getPropertyValue('--b1')||'100%', b2: grid.style.getPropertyValue('--b2')||'100%' };
|
| 869 |
const bubbles = [];
|
| 870 |
grid.querySelectorAll('.speech-bubble').forEach(b => {
|
| 871 |
+
bubbles.push({ text: b.querySelector('.bubble-text').textContent, left: b.style.left, top: b.style.top, width: b.style.width, height: b.style.height, type: b.dataset.type, colors: { fill: b.style.getPropertyValue('--bubble-fill'), text: b.style.getPropertyValue('--bubble-text') }, tailPos: b.style.getPropertyValue('--tail-pos'), classes: b.className });
|
| 872 |
});
|
| 873 |
const panels = [];
|
| 874 |
grid.querySelectorAll('.panel').forEach(pan => {
|