Update app_enhanced.py
Browse files- app_enhanced.py +60 -76
app_enhanced.py
CHANGED
|
@@ -47,14 +47,12 @@ def create_placeholder_image(text, filename, output_dir):
|
|
| 47 |
|
| 48 |
@spaces.GPU(duration=120)
|
| 49 |
def generate_comic_gpu(video_path, frames_dir, target_pages):
|
| 50 |
-
"""Extracts frames WITHOUT cropping
|
| 51 |
if os.path.exists(frames_dir): shutil.rmtree(frames_dir)
|
| 52 |
os.makedirs(frames_dir, exist_ok=True)
|
| 53 |
|
| 54 |
cap = cv2.VideoCapture(video_path)
|
| 55 |
-
if not cap.isOpened():
|
| 56 |
-
print("❌ Video load failed.")
|
| 57 |
-
return []
|
| 58 |
|
| 59 |
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 60 |
fps = cap.get(cv2.CAP_PROP_FPS) or 25
|
|
@@ -70,22 +68,9 @@ def generate_comic_gpu(video_path, frames_dir, target_pages):
|
|
| 70 |
cap.set(cv2.CAP_PROP_POS_MSEC, t * 1000)
|
| 71 |
ret, frame = cap.read()
|
| 72 |
fname = f"frame_{i:04d}.png"
|
| 73 |
-
|
| 74 |
if ret and frame is not None:
|
| 75 |
-
#
|
| 76 |
-
# Maintains visual data.
|
| 77 |
-
# We resize to a standard width (e.g. 800) but keep aspect ratio relative
|
| 78 |
-
# or just force a high res fit.
|
| 79 |
-
|
| 80 |
-
# Let's resize to 800xWidth to ensure it fits nicely in panels
|
| 81 |
-
# but we won't cut pixels off.
|
| 82 |
-
h, w = frame.shape[:2]
|
| 83 |
-
|
| 84 |
-
# Simple Resize (Distortion is usually okay for comics,
|
| 85 |
-
# but keeping aspect ratio is better. Let's just standard resize
|
| 86 |
-
# so the frontend Object-Fit handles the rest).
|
| 87 |
frame = cv2.resize(frame, (800, 1000))
|
| 88 |
-
|
| 89 |
cv2.imwrite(os.path.join(frames_dir, fname), frame)
|
| 90 |
frame_files_ordered.append(fname)
|
| 91 |
else:
|
|
@@ -112,7 +97,7 @@ def generate_comic_gpu(video_path, frames_dir, target_pages):
|
|
| 112 |
pg_panels = [{'image': f} for f in p_frames]
|
| 113 |
pg_bubbles = []
|
| 114 |
if i == 0:
|
| 115 |
-
pg_bubbles.append({'dialog': "
|
| 116 |
|
| 117 |
pages_data.append({
|
| 118 |
'panels': pg_panels,
|
|
@@ -156,7 +141,7 @@ INDEX_HTML = '''
|
|
| 156 |
<head>
|
| 157 |
<meta charset="UTF-8">
|
| 158 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 159 |
-
<title>
|
| 160 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
|
| 161 |
<link href="https://fonts.googleapis.com/css2?family=Bangers&display=swap" rel="stylesheet">
|
| 162 |
<style>
|
|
@@ -184,13 +169,19 @@ INDEX_HTML = '''
|
|
| 184 |
width: 100%; height: 100%;
|
| 185 |
position: relative;
|
| 186 |
background: #000;
|
| 187 |
-
/* Coordinate System */
|
| 188 |
-
--x: 50%; /* Center X */
|
| 189 |
-
--y: 50%; /* Center Y */
|
| 190 |
-
--xt: 50%; /* Top Edge X */
|
| 191 |
-
--xb: 50%; /* Bottom Edge X */
|
| 192 |
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
}
|
| 195 |
|
| 196 |
.panel {
|
|
@@ -200,50 +191,54 @@ INDEX_HTML = '''
|
|
| 200 |
|
| 201 |
.panel img {
|
| 202 |
width: 100%; height: 100%;
|
| 203 |
-
object-fit: cover;
|
| 204 |
transform-origin: center;
|
| 205 |
cursor: grab;
|
| 206 |
}
|
| 207 |
.panel img:active { cursor: grabbing; }
|
| 208 |
|
| 209 |
/* === DYNAMIC CLIP PATHS === */
|
| 210 |
-
|
|
|
|
| 211 |
.panel:nth-child(1) {
|
| 212 |
clip-path: polygon(
|
| 213 |
0 0,
|
| 214 |
-
calc(var(--
|
| 215 |
-
calc(var(--
|
| 216 |
0 calc(var(--y) - var(--gap))
|
| 217 |
);
|
| 218 |
z-index: 1;
|
| 219 |
}
|
| 220 |
-
|
|
|
|
| 221 |
.panel:nth-child(2) {
|
| 222 |
clip-path: polygon(
|
| 223 |
-
calc(var(--
|
| 224 |
100% 0,
|
| 225 |
100% calc(var(--y) - var(--gap)),
|
| 226 |
-
calc(var(--
|
| 227 |
);
|
| 228 |
z-index: 1;
|
| 229 |
}
|
| 230 |
-
|
|
|
|
| 231 |
.panel:nth-child(3) {
|
| 232 |
clip-path: polygon(
|
| 233 |
0 calc(var(--y) + var(--gap)),
|
| 234 |
-
calc(var(--
|
| 235 |
-
calc(var(--
|
| 236 |
0 100%
|
| 237 |
);
|
| 238 |
z-index: 1;
|
| 239 |
}
|
| 240 |
-
|
|
|
|
| 241 |
.panel:nth-child(4) {
|
| 242 |
clip-path: polygon(
|
| 243 |
-
calc(var(--
|
| 244 |
100% calc(var(--y) + var(--gap)),
|
| 245 |
100% 100%,
|
| 246 |
-
calc(var(--
|
| 247 |
);
|
| 248 |
z-index: 1;
|
| 249 |
}
|
|
@@ -253,14 +248,18 @@ INDEX_HTML = '''
|
|
| 253 |
position: absolute; width: 22px; height: 22px;
|
| 254 |
border: 2px solid white; border-radius: 50%;
|
| 255 |
transform: translate(-50%, -50%);
|
| 256 |
-
z-index: 999; cursor:
|
| 257 |
box-shadow: 0 2px 5px rgba(0,0,0,0.8);
|
| 258 |
}
|
| 259 |
.handle:hover { transform: translate(-50%, -50%) scale(1.2); }
|
| 260 |
|
| 261 |
-
|
| 262 |
-
.h-
|
| 263 |
-
.h-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
|
| 265 |
/* BUBBLES */
|
| 266 |
.bubble {
|
|
@@ -284,8 +283,8 @@ INDEX_HTML = '''
|
|
| 284 |
|
| 285 |
<div id="upload-view">
|
| 286 |
<div class="box">
|
| 287 |
-
<h1>🎞️
|
| 288 |
-
<p>
|
| 289 |
<input type="file" id="fileIn" accept="video/*"><br><br>
|
| 290 |
<label>Pages:</label> <input type="number" id="pgCount" value="1" min="1" max="5" style="width:50px;">
|
| 291 |
<br><br>
|
|
@@ -363,15 +362,13 @@ INDEX_HTML = '''
|
|
| 363 |
grid.appendChild(div);
|
| 364 |
});
|
| 365 |
|
| 366 |
-
// Handles
|
| 367 |
-
//
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
grid.append(hc, ht, hb);
|
| 375 |
|
| 376 |
if(pg.bubbles) pg.bubbles.forEach(b => createBubble(b.dialog, grid));
|
| 377 |
pDiv.appendChild(grid);
|
|
@@ -379,12 +376,13 @@ INDEX_HTML = '''
|
|
| 379 |
});
|
| 380 |
}
|
| 381 |
|
| 382 |
-
function createHandle(cls, grid,
|
| 383 |
let h = document.createElement('div');
|
| 384 |
h.className = `handle ${cls}`;
|
| 385 |
h.onmousedown = (e) => {
|
| 386 |
e.stopPropagation();
|
| 387 |
-
dragType =
|
|
|
|
| 388 |
};
|
| 389 |
return h;
|
| 390 |
}
|
|
@@ -421,27 +419,13 @@ INDEX_HTML = '''
|
|
| 421 |
document.addEventListener('mousemove', (e) => {
|
| 422 |
if(!dragType) return;
|
| 423 |
|
| 424 |
-
// 1.
|
| 425 |
-
if(dragType === '
|
| 426 |
-
let rect = activeObj.getBoundingClientRect();
|
| 427 |
-
let x = (e.clientX - rect.left) / rect.width * 100;
|
| 428 |
-
let y = (e.clientY - rect.top) / rect.height * 100;
|
| 429 |
-
activeObj.style.setProperty('--x', clamp(x)+'%');
|
| 430 |
-
activeObj.style.setProperty('--y', clamp(y)+'%');
|
| 431 |
-
}
|
| 432 |
-
// 2. Top Tilt (Moves Top Edge X)
|
| 433 |
-
else if(dragType === 'top') {
|
| 434 |
-
let rect = activeObj.getBoundingClientRect();
|
| 435 |
-
let x = (e.clientX - rect.left) / rect.width * 100;
|
| 436 |
-
activeObj.style.setProperty('--xt', clamp(x)+'%');
|
| 437 |
-
}
|
| 438 |
-
// 3. Bottom Tilt (Moves Bottom Edge X)
|
| 439 |
-
else if(dragType === 'bottom') {
|
| 440 |
-
let rect = activeObj.getBoundingClientRect();
|
| 441 |
let x = (e.clientX - rect.left) / rect.width * 100;
|
| 442 |
-
activeObj.style.setProperty(
|
| 443 |
}
|
| 444 |
-
//
|
| 445 |
else if(dragType === 'pan') {
|
| 446 |
let dx = e.clientX - dragStart.x;
|
| 447 |
let dy = e.clientY - dragStart.y;
|
|
@@ -450,7 +434,7 @@ INDEX_HTML = '''
|
|
| 450 |
dragStart = {x: e.clientX, y: e.clientY};
|
| 451 |
updateImgTransform(activeObj);
|
| 452 |
}
|
| 453 |
-
//
|
| 454 |
else if(dragType === 'bubble') {
|
| 455 |
let rect = activeObj.parentElement.getBoundingClientRect();
|
| 456 |
activeObj.style.left = (e.clientX - rect.left) + 'px';
|
|
|
|
| 47 |
|
| 48 |
@spaces.GPU(duration=120)
|
| 49 |
def generate_comic_gpu(video_path, frames_dir, target_pages):
|
| 50 |
+
"""Extracts frames WITHOUT cropping (Full Content)."""
|
| 51 |
if os.path.exists(frames_dir): shutil.rmtree(frames_dir)
|
| 52 |
os.makedirs(frames_dir, exist_ok=True)
|
| 53 |
|
| 54 |
cap = cv2.VideoCapture(video_path)
|
| 55 |
+
if not cap.isOpened(): return []
|
|
|
|
|
|
|
| 56 |
|
| 57 |
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 58 |
fps = cap.get(cv2.CAP_PROP_FPS) or 25
|
|
|
|
| 68 |
cap.set(cv2.CAP_PROP_POS_MSEC, t * 1000)
|
| 69 |
ret, frame = cap.read()
|
| 70 |
fname = f"frame_{i:04d}.png"
|
|
|
|
| 71 |
if ret and frame is not None:
|
| 72 |
+
# Resize to standard width, keep content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
frame = cv2.resize(frame, (800, 1000))
|
|
|
|
| 74 |
cv2.imwrite(os.path.join(frames_dir, fname), frame)
|
| 75 |
frame_files_ordered.append(fname)
|
| 76 |
else:
|
|
|
|
| 97 |
pg_panels = [{'image': f} for f in p_frames]
|
| 98 |
pg_bubbles = []
|
| 99 |
if i == 0:
|
| 100 |
+
pg_bubbles.append({'dialog': "Blue Handles = Top Row\nGreen Handles = Bot Row", 'x': '50%', 'y': '50%'})
|
| 101 |
|
| 102 |
pages_data.append({
|
| 103 |
'panels': pg_panels,
|
|
|
|
| 141 |
<head>
|
| 142 |
<meta charset="UTF-8">
|
| 143 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 144 |
+
<title>Independent Tilt Comic</title>
|
| 145 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
|
| 146 |
<link href="https://fonts.googleapis.com/css2?family=Bangers&display=swap" rel="stylesheet">
|
| 147 |
<style>
|
|
|
|
| 169 |
width: 100%; height: 100%;
|
| 170 |
position: relative;
|
| 171 |
background: #000;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
+
/* INDEPENDENT VARIABLES */
|
| 174 |
+
--y: 50%; /* Horizontal Split (Fixed) */
|
| 175 |
+
|
| 176 |
+
/* Top Row Line: t1 (top) -> t2 (middle) */
|
| 177 |
+
--t1: 50%;
|
| 178 |
+
--t2: 50%;
|
| 179 |
+
|
| 180 |
+
/* Bottom Row Line: b1 (middle) -> b2 (bottom) */
|
| 181 |
+
--b1: 50%;
|
| 182 |
+
--b2: 50%;
|
| 183 |
+
|
| 184 |
+
--gap: 3px;
|
| 185 |
}
|
| 186 |
|
| 187 |
.panel {
|
|
|
|
| 191 |
|
| 192 |
.panel img {
|
| 193 |
width: 100%; height: 100%;
|
| 194 |
+
object-fit: cover;
|
| 195 |
transform-origin: center;
|
| 196 |
cursor: grab;
|
| 197 |
}
|
| 198 |
.panel img:active { cursor: grabbing; }
|
| 199 |
|
| 200 |
/* === DYNAMIC CLIP PATHS === */
|
| 201 |
+
|
| 202 |
+
/* 1. TOP LEFT: (0,0) -> (t1,0) -> (t2, y) -> (0, y) */
|
| 203 |
.panel:nth-child(1) {
|
| 204 |
clip-path: polygon(
|
| 205 |
0 0,
|
| 206 |
+
calc(var(--t1) - var(--gap)) 0,
|
| 207 |
+
calc(var(--t2) - var(--gap)) calc(var(--y) - var(--gap)),
|
| 208 |
0 calc(var(--y) - var(--gap))
|
| 209 |
);
|
| 210 |
z-index: 1;
|
| 211 |
}
|
| 212 |
+
|
| 213 |
+
/* 2. TOP RIGHT: (t1,0) -> (100,0) -> (100, y) -> (t2, y) */
|
| 214 |
.panel:nth-child(2) {
|
| 215 |
clip-path: polygon(
|
| 216 |
+
calc(var(--t1) + var(--gap)) 0,
|
| 217 |
100% 0,
|
| 218 |
100% calc(var(--y) - var(--gap)),
|
| 219 |
+
calc(var(--t2) + var(--gap)) calc(var(--y) - var(--gap))
|
| 220 |
);
|
| 221 |
z-index: 1;
|
| 222 |
}
|
| 223 |
+
|
| 224 |
+
/* 3. BOTTOM LEFT: (0, y) -> (b1, y) -> (b2, 100) -> (0, 100) */
|
| 225 |
.panel:nth-child(3) {
|
| 226 |
clip-path: polygon(
|
| 227 |
0 calc(var(--y) + var(--gap)),
|
| 228 |
+
calc(var(--b1) - var(--gap)) calc(var(--y) + var(--gap)),
|
| 229 |
+
calc(var(--b2) - var(--gap)) 100%,
|
| 230 |
0 100%
|
| 231 |
);
|
| 232 |
z-index: 1;
|
| 233 |
}
|
| 234 |
+
|
| 235 |
+
/* 4. BOTTOM RIGHT: (b1, y) -> (100, y) -> (100, 100) -> (b2, 100) */
|
| 236 |
.panel:nth-child(4) {
|
| 237 |
clip-path: polygon(
|
| 238 |
+
calc(var(--b1) + var(--gap)) calc(var(--y) + var(--gap)),
|
| 239 |
100% calc(var(--y) + var(--gap)),
|
| 240 |
100% 100%,
|
| 241 |
+
calc(var(--b2) + var(--gap)) 100%
|
| 242 |
);
|
| 243 |
z-index: 1;
|
| 244 |
}
|
|
|
|
| 248 |
position: absolute; width: 22px; height: 22px;
|
| 249 |
border: 2px solid white; border-radius: 50%;
|
| 250 |
transform: translate(-50%, -50%);
|
| 251 |
+
z-index: 999; cursor: ew-resize;
|
| 252 |
box-shadow: 0 2px 5px rgba(0,0,0,0.8);
|
| 253 |
}
|
| 254 |
.handle:hover { transform: translate(-50%, -50%) scale(1.2); }
|
| 255 |
|
| 256 |
+
/* Blue for Top Row */
|
| 257 |
+
.h-t1 { background: #3498db; left: var(--t1); top: 0%; margin-top: 10px; }
|
| 258 |
+
.h-t2 { background: #3498db; left: var(--t2); top: 50%; margin-top: -12px; }
|
| 259 |
+
|
| 260 |
+
/* Green for Bottom Row */
|
| 261 |
+
.h-b1 { background: #2ecc71; left: var(--b1); top: 50%; margin-top: 12px; }
|
| 262 |
+
.h-b2 { background: #2ecc71; left: var(--b2); top: 100%; margin-top: -10px; }
|
| 263 |
|
| 264 |
/* BUBBLES */
|
| 265 |
.bubble {
|
|
|
|
| 283 |
|
| 284 |
<div id="upload-view">
|
| 285 |
<div class="box">
|
| 286 |
+
<h1>🎞️ Independent Row Tilt Comic</h1>
|
| 287 |
+
<p>Control Top & Bottom Layouts Separately!</p>
|
| 288 |
<input type="file" id="fileIn" accept="video/*"><br><br>
|
| 289 |
<label>Pages:</label> <input type="number" id="pgCount" value="1" min="1" max="5" style="width:50px;">
|
| 290 |
<br><br>
|
|
|
|
| 362 |
grid.appendChild(div);
|
| 363 |
});
|
| 364 |
|
| 365 |
+
// Handles (Independent)
|
| 366 |
+
// Top Row
|
| 367 |
+
grid.append(createHandle('h-t1', grid, 't1'));
|
| 368 |
+
grid.append(createHandle('h-t2', grid, 't2'));
|
| 369 |
+
// Bot Row
|
| 370 |
+
grid.append(createHandle('h-b1', grid, 'b1'));
|
| 371 |
+
grid.append(createHandle('h-b2', grid, 'b2'));
|
|
|
|
|
|
|
| 372 |
|
| 373 |
if(pg.bubbles) pg.bubbles.forEach(b => createBubble(b.dialog, grid));
|
| 374 |
pDiv.appendChild(grid);
|
|
|
|
| 376 |
});
|
| 377 |
}
|
| 378 |
|
| 379 |
+
function createHandle(cls, grid, varName) {
|
| 380 |
let h = document.createElement('div');
|
| 381 |
h.className = `handle ${cls}`;
|
| 382 |
h.onmousedown = (e) => {
|
| 383 |
e.stopPropagation();
|
| 384 |
+
dragType = 'handle';
|
| 385 |
+
activeObj = { grid: grid, var: varName };
|
| 386 |
};
|
| 387 |
return h;
|
| 388 |
}
|
|
|
|
| 419 |
document.addEventListener('mousemove', (e) => {
|
| 420 |
if(!dragType) return;
|
| 421 |
|
| 422 |
+
// 1. Handles (Horizontal Only)
|
| 423 |
+
if(dragType === 'handle') {
|
| 424 |
+
let rect = activeObj.grid.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
let x = (e.clientX - rect.left) / rect.width * 100;
|
| 426 |
+
activeObj.grid.style.setProperty(`--${activeObj.var}`, clamp(x)+'%');
|
| 427 |
}
|
| 428 |
+
// 2. Pan Image
|
| 429 |
else if(dragType === 'pan') {
|
| 430 |
let dx = e.clientX - dragStart.x;
|
| 431 |
let dy = e.clientY - dragStart.y;
|
|
|
|
| 434 |
dragStart = {x: e.clientX, y: e.clientY};
|
| 435 |
updateImgTransform(activeObj);
|
| 436 |
}
|
| 437 |
+
// 3. Bubble
|
| 438 |
else if(dragType === 'bubble') {
|
| 439 |
let rect = activeObj.parentElement.getBoundingClientRect();
|
| 440 |
activeObj.style.left = (e.clientX - rect.left) + 'px';
|