Update app_enhanced.py
Browse files- app_enhanced.py +111 -56
app_enhanced.py
CHANGED
|
@@ -82,8 +82,8 @@ def generate_save_code(length=8):
|
|
| 82 |
# ๐ง GLOBAL GPU FUNCTIONS
|
| 83 |
# ======================================================
|
| 84 |
@spaces.GPU(duration=300)
|
| 85 |
-
def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
|
| 86 |
-
print(f"๐ GPU Task Started: {video_path} | Pages: {target_pages}")
|
| 87 |
|
| 88 |
import cv2
|
| 89 |
import srt
|
|
@@ -118,9 +118,9 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 118 |
raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
|
| 119 |
|
| 120 |
if target_pages <= 0: target_pages = 1
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
# --- CHANGED: 2 PANELS PER PAGE ---
|
| 123 |
-
panels_per_page = 2
|
| 124 |
total_panels_needed = target_pages * panels_per_page
|
| 125 |
|
| 126 |
selected_moments = []
|
|
@@ -182,9 +182,8 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 182 |
|
| 183 |
pages = []
|
| 184 |
for i in range(target_pages):
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
end_idx = start_idx + 2
|
| 188 |
p_frames = frame_files_ordered[start_idx:end_idx]
|
| 189 |
p_bubbles = bubbles_list[start_idx:end_idx]
|
| 190 |
if p_frames:
|
|
@@ -279,10 +278,10 @@ class EnhancedComicGenerator:
|
|
| 279 |
os.makedirs(self.frames_dir, exist_ok=True)
|
| 280 |
os.makedirs(self.output_dir, exist_ok=True)
|
| 281 |
|
| 282 |
-
def run(self, target_pages):
|
| 283 |
try:
|
| 284 |
self.write_status("Waiting for GPU...", 5)
|
| 285 |
-
data = generate_comic_gpu(self.video_path, self.user_dir, self.frames_dir, self.metadata_path, int(target_pages))
|
| 286 |
with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
|
| 287 |
json.dump(data, f, indent=2)
|
| 288 |
self.write_status("Complete!", 100)
|
|
@@ -331,48 +330,45 @@ INDEX_HTML = '''
|
|
| 331 |
.page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
|
| 332 |
.page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
|
| 333 |
|
| 334 |
-
.comic-page { background: white; width: 600px; height: 400px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); position: relative; overflow: hidden; border: 2px solid #000; padding:
|
| 335 |
|
| 336 |
-
/* ===
|
| 337 |
-
.comic-grid { width: 100%; height: 100%; position: relative; background: #000; }
|
| 338 |
|
| 339 |
-
|
| 340 |
-
.
|
| 341 |
-
.panel.selected img { outline: 3px solid #2196F3; outline-offset: -3px; }
|
| 342 |
|
| 343 |
-
/*
|
| 344 |
-
|
| 345 |
-
Clip: Standard rect top, but bottom slants up from left to right.
|
| 346 |
-
polygon(0 0, 100% 0, 100% 85%, 0 100%)
|
| 347 |
-
*/
|
| 348 |
-
.panel:nth-child(1) {
|
| 349 |
-
top: 0; left: 0; width: 100%; height: 55%;
|
| 350 |
-
clip-path: polygon(0% 0%, 100% 0%, 100% 85%, 0% 100%);
|
| 351 |
-
z-index: 2;
|
| 352 |
-
}
|
| 353 |
|
| 354 |
-
/*
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
.
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
}
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
.panel:nth-child(3), .panel:nth-child(4) { display: none; }
|
| 367 |
-
/* ====================== */
|
| 368 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
.panel img {
|
| 370 |
width: 100%; height: 100%;
|
| 371 |
-
object-fit:
|
| 372 |
transition: transform 0.1s ease-out;
|
| 373 |
transform-origin: center center;
|
| 374 |
pointer-events: auto;
|
| 375 |
}
|
|
|
|
|
|
|
| 376 |
.panel img.pannable { cursor: grab; }
|
| 377 |
.panel img.panning { cursor: grabbing; }
|
| 378 |
|
|
@@ -385,10 +381,7 @@ INDEX_HTML = '''
|
|
| 385 |
overflow: visible;
|
| 386 |
line-height: 1.2;
|
| 387 |
--tail-pos: 50%;
|
| 388 |
-
|
| 389 |
-
/* Default Center Positioning if no coords */
|
| 390 |
-
left: 50%; top: 50%;
|
| 391 |
-
transform: translate(-50%, -50%);
|
| 392 |
}
|
| 393 |
|
| 394 |
.bubble-text {
|
|
@@ -464,10 +457,15 @@ INDEX_HTML = '''
|
|
| 464 |
.modal-content button { background: #3498db; color: white; border: none; padding: 12px 30px; border-radius: 8px; cursor: pointer; font-weight: bold; margin: 5px; }
|
| 465 |
</style>
|
| 466 |
</head> <body> <div id="upload-container"> <div class="upload-box"> <h1>๐ฌ Enhanced Comic Generator</h1> <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fn').innerText=this.files[0].name"> <label for="file-upload" class="file-label">๐ Choose Video File</label> <span id="fn" style="margin-bottom:10px; display:block; color:#666;">No file selected</span>
|
| 467 |
-
<div class="
|
| 468 |
-
<
|
| 469 |
-
|
| 470 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
</div>
|
| 472 |
|
| 473 |
<button class="submit-btn" onclick="upload()">๐ Generate Comic</button>
|
|
@@ -494,6 +492,16 @@ INDEX_HTML = '''
|
|
| 494 |
|
| 495 |
<button onclick="undoLastAction()" class="undo-btn">โฉ๏ธ Undo</button>
|
| 496 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
<div class="control-group">
|
| 498 |
<label>๐พ Save & Load:</label>
|
| 499 |
<button onclick="saveComic()" class="save-btn">๐พ Save Comic</button>
|
|
@@ -552,10 +560,11 @@ INDEX_HTML = '''
|
|
| 552 |
<div class="control-group">
|
| 553 |
<label>๐ Zoom & Pan:</label>
|
| 554 |
<div class="button-grid">
|
|
|
|
| 555 |
<button onclick="resetPanelTransform()" class="secondary-btn">Reset</button>
|
| 556 |
-
<input type="range" id="zoom-slider" min="50" max="500" value="100" step="5" disabled oninput="handleZoom(this)">
|
| 557 |
</div>
|
| 558 |
-
<
|
|
|
|
| 559 |
</div>
|
| 560 |
|
| 561 |
<div class="control-group">
|
|
@@ -663,8 +672,14 @@ INDEX_HTML = '''
|
|
| 663 |
function getCurrentState() {
|
| 664 |
const pages = [];
|
| 665 |
document.querySelectorAll('.comic-page').forEach(p => {
|
| 666 |
-
const panels = [];
|
| 667 |
const grid = p.querySelector('.comic-grid');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
grid.querySelectorAll('.panel').forEach(pan => {
|
| 669 |
const img = pan.querySelector('img');
|
| 670 |
const bubbles = [];
|
|
@@ -677,16 +692,17 @@ INDEX_HTML = '''
|
|
| 677 |
type: b.dataset.type, font: b.style.fontFamily,
|
| 678 |
tailPos: b.style.getPropertyValue('--tail-pos'),
|
| 679 |
colors: { fill: b.style.getPropertyValue('--bubble-fill-color'), text: b.style.getPropertyValue('--bubble-text-color') },
|
| 680 |
-
transform: b.style.transform
|
| 681 |
});
|
| 682 |
});
|
| 683 |
panels.push({
|
| 684 |
src: img.src,
|
| 685 |
zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY,
|
|
|
|
| 686 |
bubbles: bubbles
|
| 687 |
});
|
| 688 |
});
|
| 689 |
-
pages.push({ panels: panels });
|
| 690 |
});
|
| 691 |
return pages;
|
| 692 |
}
|
|
@@ -704,7 +720,10 @@ INDEX_HTML = '''
|
|
| 704 |
pageWrapper.appendChild(pageTitle);
|
| 705 |
|
| 706 |
const div = document.createElement('div'); div.className = 'comic-page';
|
| 707 |
-
const grid = document.createElement('div');
|
|
|
|
|
|
|
|
|
|
| 708 |
|
| 709 |
page.panels.forEach((pan) => {
|
| 710 |
const pDiv = document.createElement('div'); pDiv.className = 'panel';
|
|
@@ -712,6 +731,9 @@ INDEX_HTML = '''
|
|
| 712 |
const img = document.createElement('img');
|
| 713 |
img.src = pan.src.includes('?') ? pan.src : pan.src + `?sid=${sid}`;
|
| 714 |
img.dataset.zoom = pan.zoom || 100; img.dataset.translateX = pan.tx || 0; img.dataset.translateY = pan.ty || 0;
|
|
|
|
|
|
|
|
|
|
| 715 |
updateImageTransform(img);
|
| 716 |
|
| 717 |
// === EVENTS FOR PANNING AND ZOOMING ===
|
|
@@ -726,6 +748,9 @@ INDEX_HTML = '''
|
|
| 726 |
grid.appendChild(pDiv);
|
| 727 |
});
|
| 728 |
div.appendChild(grid); pageWrapper.appendChild(div); con.appendChild(pageWrapper);
|
|
|
|
|
|
|
|
|
|
| 729 |
});
|
| 730 |
selectedBubble = null;
|
| 731 |
selectedPanel = null;
|
|
@@ -736,11 +761,12 @@ INDEX_HTML = '''
|
|
| 736 |
async function upload() {
|
| 737 |
const f = document.getElementById('file-upload').files[0];
|
| 738 |
const pCount = document.getElementById('page-count').value;
|
|
|
|
| 739 |
if(!f) return alert("Select a video");
|
| 740 |
sid = genUUID(); localStorage.setItem('comic_sid', sid); currentSaveCode = null;
|
| 741 |
document.querySelector('.upload-box').style.display='none';
|
| 742 |
document.getElementById('loading-view').style.display='flex';
|
| 743 |
-
const fd = new FormData(); fd.append('file', f); fd.append('target_pages', pCount);
|
| 744 |
const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
|
| 745 |
if(r.ok) interval = setInterval(checkStatus, 2000);
|
| 746 |
else { alert("Upload failed"); location.reload(); }
|
|
@@ -758,8 +784,10 @@ INDEX_HTML = '''
|
|
| 758 |
function loadNewComic() {
|
| 759 |
fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
|
| 760 |
const cleanData = data.map((p, pi) => ({
|
|
|
|
| 761 |
panels: p.panels.map((pan, j) => ({
|
| 762 |
src: `/frames/${pan.image}?sid=${sid}`,
|
|
|
|
| 763 |
bubbles: (p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) ? [{
|
| 764 |
text: p.bubbles[j].dialog,
|
| 765 |
// Backend now sends -1, -1 for center.
|
|
@@ -869,6 +897,9 @@ INDEX_HTML = '''
|
|
| 869 |
document.getElementById('bubble-type-select').disabled = true;
|
| 870 |
document.getElementById('font-select').disabled = true;
|
| 871 |
document.getElementById('tail-controls').style.display = 'none';
|
|
|
|
|
|
|
|
|
|
| 872 |
}
|
| 873 |
|
| 874 |
function addBubble() {
|
|
@@ -973,6 +1004,28 @@ INDEX_HTML = '''
|
|
| 973 |
function updateImageTransform(img) { const z = (img.dataset.zoom || 100) / 100; const x = img.dataset.translateX || 0; const y = img.dataset.translateY || 0; img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`; img.classList.toggle('pannable', true); }
|
| 974 |
function resetPanelTransform() { if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); img.dataset.zoom = 100; img.dataset.translateX = 0; img.dataset.translateY = 0; document.getElementById('zoom-slider').value = 100; updateImageTransform(img); saveDraft(true); }
|
| 975 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 976 |
function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
|
| 977 |
async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
| 978 |
async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
|
@@ -1034,6 +1087,8 @@ def upload():
|
|
| 1034 |
|
| 1035 |
# GET PAGE COUNT FROM FORM
|
| 1036 |
target_pages = request.form.get('target_pages', 4)
|
|
|
|
|
|
|
| 1037 |
|
| 1038 |
f = request.files['file']
|
| 1039 |
gen = EnhancedComicGenerator(sid)
|
|
@@ -1042,7 +1097,7 @@ def upload():
|
|
| 1042 |
gen.write_status("Starting...", 5)
|
| 1043 |
|
| 1044 |
# Run in thread
|
| 1045 |
-
threading.Thread(target=gen.run, args=(target_pages,)).start()
|
| 1046 |
return jsonify({'success': True, 'message': 'Generation started.'})
|
| 1047 |
|
| 1048 |
@app.route('/status')
|
|
|
|
| 82 |
# ๐ง GLOBAL GPU FUNCTIONS
|
| 83 |
# ======================================================
|
| 84 |
@spaces.GPU(duration=300)
|
| 85 |
+
def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages, panels_per_page_req):
|
| 86 |
+
print(f"๐ GPU Task Started: {video_path} | Pages: {target_pages} | Panels/Page: {panels_per_page_req}")
|
| 87 |
|
| 88 |
import cv2
|
| 89 |
import srt
|
|
|
|
| 118 |
raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
|
| 119 |
|
| 120 |
if target_pages <= 0: target_pages = 1
|
| 121 |
+
panels_per_page = int(panels_per_page_req)
|
| 122 |
+
if panels_per_page <= 0: panels_per_page = 2
|
| 123 |
|
|
|
|
|
|
|
| 124 |
total_panels_needed = target_pages * panels_per_page
|
| 125 |
|
| 126 |
selected_moments = []
|
|
|
|
| 182 |
|
| 183 |
pages = []
|
| 184 |
for i in range(target_pages):
|
| 185 |
+
start_idx = i * panels_per_page
|
| 186 |
+
end_idx = start_idx + panels_per_page
|
|
|
|
| 187 |
p_frames = frame_files_ordered[start_idx:end_idx]
|
| 188 |
p_bubbles = bubbles_list[start_idx:end_idx]
|
| 189 |
if p_frames:
|
|
|
|
| 278 |
os.makedirs(self.frames_dir, exist_ok=True)
|
| 279 |
os.makedirs(self.output_dir, exist_ok=True)
|
| 280 |
|
| 281 |
+
def run(self, target_pages, panels_per_page):
|
| 282 |
try:
|
| 283 |
self.write_status("Waiting for GPU...", 5)
|
| 284 |
+
data = generate_comic_gpu(self.video_path, self.user_dir, self.frames_dir, self.metadata_path, int(target_pages), int(panels_per_page))
|
| 285 |
with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
|
| 286 |
json.dump(data, f, indent=2)
|
| 287 |
self.write_status("Complete!", 100)
|
|
|
|
| 330 |
.page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
|
| 331 |
.page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
|
| 332 |
|
| 333 |
+
.comic-page { background: white; width: 600px; height: 400px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); position: relative; overflow: hidden; border: 2px solid #000; padding: 10px; }
|
| 334 |
|
| 335 |
+
/* === ADJUSTABLE LAYOUTS === */
|
| 336 |
+
.comic-grid { width: 100%; height: 100%; position: relative; background: #000; display:grid; gap: 10px; }
|
| 337 |
|
| 338 |
+
/* Layout: Rows (Horizontal Split) - Default */
|
| 339 |
+
.comic-grid.layout-rows { grid-template-columns: 1fr; grid-auto-rows: 1fr; }
|
|
|
|
| 340 |
|
| 341 |
+
/* Layout: Columns (Vertical Split) */
|
| 342 |
+
.comic-grid.layout-cols { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); grid-template-rows: 1fr; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
+
/* Layout: Grid (2x2) */
|
| 345 |
+
.comic-grid.layout-grid { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
|
| 346 |
+
|
| 347 |
+
/* Layout: Slanted (Designer) */
|
| 348 |
+
.comic-grid.layout-slanted { display: block; }
|
| 349 |
+
.comic-grid.layout-slanted .panel { position: absolute; width:100%; left:0; }
|
| 350 |
+
/* Top Panel */
|
| 351 |
+
.comic-grid.layout-slanted .panel:nth-child(1) { top: 0; height: 55%; clip-path: polygon(0% 0%, 100% 0%, 100% 85%, 0% 100%); z-index:2; }
|
| 352 |
+
/* Bottom Panel */
|
| 353 |
+
.comic-grid.layout-slanted .panel:nth-child(2) { top: 45%; height: 55%; clip-path: polygon(0% 15%, 100% 0%, 100% 100%, 0% 100%); z-index:1; }
|
| 354 |
+
/* Hide extra panels in slanted mode */
|
| 355 |
+
.comic-grid.layout-slanted .panel:nth-child(n+3) { display: none; }
|
|
|
|
|
|
|
| 356 |
|
| 357 |
+
|
| 358 |
+
.panel { overflow: hidden; background: #eee; cursor: pointer; border: 2px solid #000; position: relative; }
|
| 359 |
+
.layout-slanted .panel { border: none; background: transparent; }
|
| 360 |
+
.panel.selected { z-index: 20; border-color: #2196F3; }
|
| 361 |
+
|
| 362 |
+
/* Image fitting */
|
| 363 |
.panel img {
|
| 364 |
width: 100%; height: 100%;
|
| 365 |
+
object-fit: contain; /* Default: Show entire image (no cut) */
|
| 366 |
transition: transform 0.1s ease-out;
|
| 367 |
transform-origin: center center;
|
| 368 |
pointer-events: auto;
|
| 369 |
}
|
| 370 |
+
.panel img.fit-cover { object-fit: cover; }
|
| 371 |
+
|
| 372 |
.panel img.pannable { cursor: grab; }
|
| 373 |
.panel img.panning { cursor: grabbing; }
|
| 374 |
|
|
|
|
| 381 |
overflow: visible;
|
| 382 |
line-height: 1.2;
|
| 383 |
--tail-pos: 50%;
|
| 384 |
+
left: 50%; top: 50%; transform: translate(-50%, -50%);
|
|
|
|
|
|
|
|
|
|
| 385 |
}
|
| 386 |
|
| 387 |
.bubble-text {
|
|
|
|
| 457 |
.modal-content button { background: #3498db; color: white; border: none; padding: 12px 30px; border-radius: 8px; cursor: pointer; font-weight: bold; margin: 5px; }
|
| 458 |
</style>
|
| 459 |
</head> <body> <div id="upload-container"> <div class="upload-box"> <h1>๐ฌ Enhanced Comic Generator</h1> <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fn').innerText=this.files[0].name"> <label for="file-upload" class="file-label">๐ Choose Video File</label> <span id="fn" style="margin-bottom:10px; display:block; color:#666;">No file selected</span>
|
| 460 |
+
<div class="button-grid">
|
| 461 |
+
<div class="page-input-group" style="margin:5px 0;">
|
| 462 |
+
<label>๐ Panels / Page:</label>
|
| 463 |
+
<input type="number" id="panels-per-page" value="2" min="1" max="4">
|
| 464 |
+
</div>
|
| 465 |
+
<div class="page-input-group" style="margin:5px 0;">
|
| 466 |
+
<label>๐ Total Pages:</label>
|
| 467 |
+
<input type="number" id="page-count" value="4" min="1" max="15">
|
| 468 |
+
</div>
|
| 469 |
</div>
|
| 470 |
|
| 471 |
<button class="submit-btn" onclick="upload()">๐ Generate Comic</button>
|
|
|
|
| 492 |
|
| 493 |
<button onclick="undoLastAction()" class="undo-btn">โฉ๏ธ Undo</button>
|
| 494 |
|
| 495 |
+
<div class="control-group">
|
| 496 |
+
<label>๐ Layout Style:</label>
|
| 497 |
+
<select id="layout-select" onchange="changeLayout(this.value)">
|
| 498 |
+
<option value="layout-rows">Rows (Horizontal)</option>
|
| 499 |
+
<option value="layout-cols">Cols (Vertical)</option>
|
| 500 |
+
<option value="layout-grid">Grid (2x2)</option>
|
| 501 |
+
<option value="layout-slanted">Slanted (Designer)</option>
|
| 502 |
+
</select>
|
| 503 |
+
</div>
|
| 504 |
+
|
| 505 |
<div class="control-group">
|
| 506 |
<label>๐พ Save & Load:</label>
|
| 507 |
<button onclick="saveComic()" class="save-btn">๐พ Save Comic</button>
|
|
|
|
| 560 |
<div class="control-group">
|
| 561 |
<label>๐ Zoom & Pan:</label>
|
| 562 |
<div class="button-grid">
|
| 563 |
+
<button onclick="toggleFitMode()" class="secondary-btn" id="fit-btn">Fit: Contain</button>
|
| 564 |
<button onclick="resetPanelTransform()" class="secondary-btn">Reset</button>
|
|
|
|
| 565 |
</div>
|
| 566 |
+
<input type="range" id="zoom-slider" min="50" max="500" value="100" step="5" disabled oninput="handleZoom(this)">
|
| 567 |
+
<small style="display:block;margin-top:5px;color:#aaa;">๐ก Scroll on image to Zoom, Drag to Pan.</small>
|
| 568 |
</div>
|
| 569 |
|
| 570 |
<div class="control-group">
|
|
|
|
| 672 |
function getCurrentState() {
|
| 673 |
const pages = [];
|
| 674 |
document.querySelectorAll('.comic-page').forEach(p => {
|
|
|
|
| 675 |
const grid = p.querySelector('.comic-grid');
|
| 676 |
+
// Extract layout class
|
| 677 |
+
let layout = 'layout-rows';
|
| 678 |
+
if(grid.classList.contains('layout-cols')) layout = 'layout-cols';
|
| 679 |
+
if(grid.classList.contains('layout-grid')) layout = 'layout-grid';
|
| 680 |
+
if(grid.classList.contains('layout-slanted')) layout = 'layout-slanted';
|
| 681 |
+
|
| 682 |
+
const panels = [];
|
| 683 |
grid.querySelectorAll('.panel').forEach(pan => {
|
| 684 |
const img = pan.querySelector('img');
|
| 685 |
const bubbles = [];
|
|
|
|
| 692 |
type: b.dataset.type, font: b.style.fontFamily,
|
| 693 |
tailPos: b.style.getPropertyValue('--tail-pos'),
|
| 694 |
colors: { fill: b.style.getPropertyValue('--bubble-fill-color'), text: b.style.getPropertyValue('--bubble-text-color') },
|
| 695 |
+
transform: b.style.transform
|
| 696 |
});
|
| 697 |
});
|
| 698 |
panels.push({
|
| 699 |
src: img.src,
|
| 700 |
zoom: img.dataset.zoom, tx: img.dataset.translateX, ty: img.dataset.translateY,
|
| 701 |
+
fit: img.classList.contains('fit-cover') ? 'cover' : 'contain',
|
| 702 |
bubbles: bubbles
|
| 703 |
});
|
| 704 |
});
|
| 705 |
+
pages.push({ layout: layout, panels: panels });
|
| 706 |
});
|
| 707 |
return pages;
|
| 708 |
}
|
|
|
|
| 720 |
pageWrapper.appendChild(pageTitle);
|
| 721 |
|
| 722 |
const div = document.createElement('div'); div.className = 'comic-page';
|
| 723 |
+
const grid = document.createElement('div');
|
| 724 |
+
|
| 725 |
+
// Restore Layout
|
| 726 |
+
grid.className = 'comic-grid ' + (page.layout || 'layout-rows');
|
| 727 |
|
| 728 |
page.panels.forEach((pan) => {
|
| 729 |
const pDiv = document.createElement('div'); pDiv.className = 'panel';
|
|
|
|
| 731 |
const img = document.createElement('img');
|
| 732 |
img.src = pan.src.includes('?') ? pan.src : pan.src + `?sid=${sid}`;
|
| 733 |
img.dataset.zoom = pan.zoom || 100; img.dataset.translateX = pan.tx || 0; img.dataset.translateY = pan.ty || 0;
|
| 734 |
+
|
| 735 |
+
if(pan.fit === 'cover') img.classList.add('fit-cover');
|
| 736 |
+
|
| 737 |
updateImageTransform(img);
|
| 738 |
|
| 739 |
// === EVENTS FOR PANNING AND ZOOMING ===
|
|
|
|
| 748 |
grid.appendChild(pDiv);
|
| 749 |
});
|
| 750 |
div.appendChild(grid); pageWrapper.appendChild(div); con.appendChild(pageWrapper);
|
| 751 |
+
|
| 752 |
+
// Sync layout dropdown if this is the first page
|
| 753 |
+
if(pageIdx === 0) document.getElementById('layout-select').value = (page.layout || 'layout-rows');
|
| 754 |
});
|
| 755 |
selectedBubble = null;
|
| 756 |
selectedPanel = null;
|
|
|
|
| 761 |
async function upload() {
|
| 762 |
const f = document.getElementById('file-upload').files[0];
|
| 763 |
const pCount = document.getElementById('page-count').value;
|
| 764 |
+
const panelCount = document.getElementById('panels-per-page').value;
|
| 765 |
if(!f) return alert("Select a video");
|
| 766 |
sid = genUUID(); localStorage.setItem('comic_sid', sid); currentSaveCode = null;
|
| 767 |
document.querySelector('.upload-box').style.display='none';
|
| 768 |
document.getElementById('loading-view').style.display='flex';
|
| 769 |
+
const fd = new FormData(); fd.append('file', f); fd.append('target_pages', pCount); fd.append('panels_per_page', panelCount);
|
| 770 |
const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
|
| 771 |
if(r.ok) interval = setInterval(checkStatus, 2000);
|
| 772 |
else { alert("Upload failed"); location.reload(); }
|
|
|
|
| 784 |
function loadNewComic() {
|
| 785 |
fetch(`/output/pages.json?sid=${sid}`).then(r=>r.json()).then(data => {
|
| 786 |
const cleanData = data.map((p, pi) => ({
|
| 787 |
+
layout: 'layout-rows', // Default layout
|
| 788 |
panels: p.panels.map((pan, j) => ({
|
| 789 |
src: `/frames/${pan.image}?sid=${sid}`,
|
| 790 |
+
fit: 'contain', // Default no cut
|
| 791 |
bubbles: (p.bubbles && p.bubbles[j] && p.bubbles[j].dialog) ? [{
|
| 792 |
text: p.bubbles[j].dialog,
|
| 793 |
// Backend now sends -1, -1 for center.
|
|
|
|
| 897 |
document.getElementById('bubble-type-select').disabled = true;
|
| 898 |
document.getElementById('font-select').disabled = true;
|
| 899 |
document.getElementById('tail-controls').style.display = 'none';
|
| 900 |
+
|
| 901 |
+
// Update Fit Button Text
|
| 902 |
+
document.getElementById('fit-btn').innerText = img.classList.contains('fit-cover') ? "Fit: Cover" : "Fit: Contain";
|
| 903 |
}
|
| 904 |
|
| 905 |
function addBubble() {
|
|
|
|
| 1004 |
function updateImageTransform(img) { const z = (img.dataset.zoom || 100) / 100; const x = img.dataset.translateX || 0; const y = img.dataset.translateY || 0; img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`; img.classList.toggle('pannable', true); }
|
| 1005 |
function resetPanelTransform() { if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); img.dataset.zoom = 100; img.dataset.translateX = 0; img.dataset.translateY = 0; document.getElementById('zoom-slider').value = 100; updateImageTransform(img); saveDraft(true); }
|
| 1006 |
|
| 1007 |
+
// === LAYOUT & FIT FUNCTIONS ===
|
| 1008 |
+
function changeLayout(newLayout) {
|
| 1009 |
+
// Apply layout to all pages for consistency
|
| 1010 |
+
document.querySelectorAll('.comic-grid').forEach(g => {
|
| 1011 |
+
g.className = 'comic-grid ' + newLayout;
|
| 1012 |
+
});
|
| 1013 |
+
saveDraft(true);
|
| 1014 |
+
}
|
| 1015 |
+
|
| 1016 |
+
function toggleFitMode() {
|
| 1017 |
+
if(!selectedPanel) return alert("Select a panel");
|
| 1018 |
+
const img = selectedPanel.querySelector('img');
|
| 1019 |
+
if(img.classList.contains('fit-cover')) {
|
| 1020 |
+
img.classList.remove('fit-cover'); // Switch to Contain
|
| 1021 |
+
document.getElementById('fit-btn').innerText = "Fit: Contain";
|
| 1022 |
+
} else {
|
| 1023 |
+
img.classList.add('fit-cover'); // Switch to Cover
|
| 1024 |
+
document.getElementById('fit-btn').innerText = "Fit: Cover";
|
| 1025 |
+
}
|
| 1026 |
+
saveDraft(true);
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
|
| 1030 |
async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
| 1031 |
async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
|
|
|
|
| 1087 |
|
| 1088 |
# GET PAGE COUNT FROM FORM
|
| 1089 |
target_pages = request.form.get('target_pages', 4)
|
| 1090 |
+
# GET PANEL COUNT FROM FORM (Default to 2)
|
| 1091 |
+
panels_per_page = request.form.get('panels_per_page', 2)
|
| 1092 |
|
| 1093 |
f = request.files['file']
|
| 1094 |
gen = EnhancedComicGenerator(sid)
|
|
|
|
| 1097 |
gen.write_status("Starting...", 5)
|
| 1098 |
|
| 1099 |
# Run in thread
|
| 1100 |
+
threading.Thread(target=gen.run, args=(target_pages, panels_per_page)).start()
|
| 1101 |
return jsonify({'success': True, 'message': 'Generation started.'})
|
| 1102 |
|
| 1103 |
@app.route('/status')
|