Update app_enhanced.py
Browse files- app_enhanced.py +40 -246
app_enhanced.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import spaces # <--- CRITICAL: MUST BE
|
| 2 |
import os
|
| 3 |
import time
|
| 4 |
import threading
|
|
@@ -24,7 +24,7 @@ def gpu_warmup():
|
|
| 24 |
return True
|
| 25 |
|
| 26 |
# ======================================================
|
| 27 |
-
# 🧱 DATA CLASSES
|
| 28 |
# ======================================================
|
| 29 |
def bubble(dialog="", bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, emotion='normal', type='speech'):
|
| 30 |
return {
|
|
@@ -68,14 +68,13 @@ def generate_save_code(length=8):
|
|
| 68 |
return code
|
| 69 |
|
| 70 |
# ======================================================
|
| 71 |
-
# 🧠 GLOBAL GPU FUNCTIONS
|
| 72 |
# ======================================================
|
| 73 |
|
| 74 |
@spaces.GPU(duration=300)
|
| 75 |
def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
|
| 76 |
print(f"🚀 GPU Task Started: {video_path} | Pages: {target_pages}")
|
| 77 |
|
| 78 |
-
# --- Local Imports for GPU Context ---
|
| 79 |
import cv2
|
| 80 |
import srt
|
| 81 |
import numpy as np
|
|
@@ -106,8 +105,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 106 |
with open(user_srt, 'r', encoding='utf-8') as f:
|
| 107 |
all_subs = list(srt.parse(f.read()))
|
| 108 |
|
| 109 |
-
# 3. Smart Keyframe Selection
|
| 110 |
-
# Filter out empty subtitles
|
| 111 |
valid_subs = [s for s in all_subs if s.content.strip()]
|
| 112 |
raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
|
| 113 |
|
|
@@ -115,18 +113,14 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 115 |
panels_per_page = 4
|
| 116 |
total_panels_needed = target_pages * panels_per_page
|
| 117 |
|
| 118 |
-
print(f"📊 Calculating: Needed {total_panels_needed} panels.")
|
| 119 |
-
|
| 120 |
selected_moments = []
|
| 121 |
if not raw_moments:
|
| 122 |
-
# Fallback if no audio/subs: Just split time evenly
|
| 123 |
times = np.linspace(1, duration-1, total_panels_needed)
|
| 124 |
for t in times:
|
| 125 |
selected_moments.append({'text': '', 'start': t, 'end': t+1})
|
| 126 |
elif len(raw_moments) <= total_panels_needed:
|
| 127 |
selected_moments = raw_moments
|
| 128 |
else:
|
| 129 |
-
# EVENLY DISTRIBUTE selection across the entire list
|
| 130 |
indices = np.linspace(0, len(raw_moments) - 1, total_panels_needed, dtype=int)
|
| 131 |
selected_moments = [raw_moments[i] for i in indices]
|
| 132 |
|
|
@@ -146,12 +140,9 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 146 |
fname = f"frame_{count:04d}.png"
|
| 147 |
p = os.path.join(frames_dir, fname)
|
| 148 |
cv2.imwrite(p, frame)
|
| 149 |
-
os.sync()
|
| 150 |
|
| 151 |
-
frame_metadata[fname] = {
|
| 152 |
-
'dialogue': moment['text'],
|
| 153 |
-
'time': mid
|
| 154 |
-
}
|
| 155 |
frame_files_ordered.append(fname)
|
| 156 |
count += 1
|
| 157 |
cap.release()
|
|
@@ -165,7 +156,6 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 165 |
se = SimpleColorEnhancer()
|
| 166 |
qe = QualityColorEnhancer()
|
| 167 |
|
| 168 |
-
print("🎨 Enhancing images...")
|
| 169 |
for f in frame_files_ordered:
|
| 170 |
p = os.path.join(frames_dir, f)
|
| 171 |
try: se.enhance_single(p, p)
|
|
@@ -174,14 +164,11 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 174 |
except: pass
|
| 175 |
|
| 176 |
# 6. Bubble Placement
|
| 177 |
-
print("💬 Placing bubbles...")
|
| 178 |
bubbles_list = []
|
| 179 |
-
|
| 180 |
for f in frame_files_ordered:
|
| 181 |
p = os.path.join(frames_dir, f)
|
| 182 |
dialogue = frame_metadata.get(f, {}).get('dialogue', '')
|
| 183 |
|
| 184 |
-
# Heuristic for bubble type
|
| 185 |
b_type = 'speech'
|
| 186 |
if '(' in dialogue and ')' in dialogue: b_type = 'narration'
|
| 187 |
elif '!' in dialogue and dialogue.isupper(): b_type = 'reaction'
|
|
@@ -191,36 +178,22 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 191 |
faces = face_detector.detect_faces(p)
|
| 192 |
lip = face_detector.get_lip_position(p, faces[0]) if faces else (-1, -1)
|
| 193 |
bx, by = ai_bubble_placer.place_bubble_ai(p, lip)
|
| 194 |
-
|
| 195 |
-
b = bubble(
|
| 196 |
-
dialog=dialogue,
|
| 197 |
-
bubble_offset_x=bx,
|
| 198 |
-
bubble_offset_y=by,
|
| 199 |
-
lip_x=lip[0],
|
| 200 |
-
lip_y=lip[1],
|
| 201 |
-
type=b_type
|
| 202 |
-
)
|
| 203 |
bubbles_list.append(b)
|
| 204 |
except:
|
| 205 |
bubbles_list.append(bubble(dialog=dialogue, type=b_type))
|
| 206 |
|
| 207 |
-
# 7. Final Layout
|
| 208 |
-
print("📄 Assembling pages...")
|
| 209 |
pages = []
|
| 210 |
-
|
| 211 |
-
# Strictly 4 panels per page based on calculation
|
| 212 |
for i in range(target_pages):
|
| 213 |
start_idx = i * 4
|
| 214 |
end_idx = start_idx + 4
|
| 215 |
-
|
| 216 |
p_frames = frame_files_ordered[start_idx:end_idx]
|
| 217 |
p_bubbles = bubbles_list[start_idx:end_idx]
|
| 218 |
-
|
| 219 |
if p_frames:
|
| 220 |
pg_panels = [panel(image=f) for f in p_frames]
|
| 221 |
pages.append(Page(panels=pg_panels, bubbles=p_bubbles))
|
| 222 |
|
| 223 |
-
# Serialize
|
| 224 |
result = []
|
| 225 |
for pg in pages:
|
| 226 |
p_data = [p if isinstance(p, dict) else p.__dict__ for p in pg.panels]
|
|
@@ -325,7 +298,7 @@ class EnhancedComicGenerator:
|
|
| 325 |
json.dump({'message': msg, 'progress': prog}, f)
|
| 326 |
|
| 327 |
# ======================================================
|
| 328 |
-
# 🌐 ROUTES &
|
| 329 |
# ======================================================
|
| 330 |
|
| 331 |
INDEX_HTML = '''
|
|
@@ -367,7 +340,6 @@ INDEX_HTML = '''
|
|
| 367 |
.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; }
|
| 368 |
@keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
|
| 369 |
|
| 370 |
-
/* COMIC ELEMENTS */
|
| 371 |
.comic-wrapper { max-width: 1000px; margin: 0 auto; }
|
| 372 |
.page-wrapper { margin: 30px auto; display: flex; flex-direction: column; align-items: center; }
|
| 373 |
.page-title { text-align: center; color: #333; margin-bottom: 10px; font-size: 18px; font-weight: bold; }
|
|
@@ -379,7 +351,7 @@ INDEX_HTML = '''
|
|
| 379 |
.panel img.pannable { cursor: grab; }
|
| 380 |
.panel img.panning { cursor: grabbing; }
|
| 381 |
|
| 382 |
-
/* BUBBLES */
|
| 383 |
.speech-bubble {
|
| 384 |
position: absolute; display: flex; justify-content: center; align-items: center;
|
| 385 |
width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
|
|
@@ -387,11 +359,11 @@ INDEX_HTML = '''
|
|
| 387 |
font-size: 13px; text-align: center; overflow: visible;
|
| 388 |
--tail-pos: 50%;
|
| 389 |
}
|
| 390 |
-
.bubble-text { padding: 0.8em; word-wrap: break-word; position: relative; z-index: 5; }
|
| 391 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
|
| 392 |
.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; }
|
| 393 |
|
| 394 |
-
/*
|
| 395 |
.speech-bubble.speech {
|
| 396 |
--b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
|
| 397 |
background: var(--bubble-fill-color, #4ECDC4);
|
|
@@ -405,15 +377,12 @@ INDEX_HTML = '''
|
|
| 405 |
-webkit-mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
|
| 406 |
mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
|
| 407 |
}
|
| 408 |
-
/* Tail Orientations */
|
| 409 |
-
.speech-bubble.speech.tail-bottom:before { top: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
|
| 410 |
|
|
|
|
| 411 |
.speech-bubble.speech.tail-top { border-radius: min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) var(--r) var(--r) / var(--r); }
|
| 412 |
.speech-bubble.speech.tail-top:before { bottom: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: scaleY(-1); }
|
| 413 |
-
|
| 414 |
.speech-bubble.speech.tail-left { border-radius: var(--r); }
|
| 415 |
.speech-bubble.speech.tail-left:before { right: 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 right; }
|
| 416 |
-
|
| 417 |
.speech-bubble.speech.tail-right { border-radius: var(--r); }
|
| 418 |
.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; }
|
| 419 |
|
|
@@ -578,7 +547,7 @@ INDEX_HTML = '''
|
|
| 578 |
localStorage.setItem('comic_sid', sid);
|
| 579 |
|
| 580 |
let currentSaveCode = null;
|
| 581 |
-
let isProcessing = false;
|
| 582 |
let interval, selectedBubble = null, selectedPanel = null;
|
| 583 |
let isDragging = false, isResizing = false, isPanning = false;
|
| 584 |
let startX, startY, initX, initY, panStartX, panStartY, panStartTx, panStartTy;
|
|
@@ -587,7 +556,6 @@ INDEX_HTML = '''
|
|
| 587 |
|
| 588 |
if(localStorage.getItem('comic_draft_'+sid)) document.getElementById('restore-draft-btn').style.display = 'block';
|
| 589 |
|
| 590 |
-
// --- HELPERS ---
|
| 591 |
function showSaveModal(code) { document.getElementById('modal-save-code').textContent = code; document.getElementById('save-modal').style.display = 'flex'; }
|
| 592 |
function closeModal() { document.getElementById('save-modal').style.display = 'none'; }
|
| 593 |
function copyModalCode() { navigator.clipboard.writeText(document.getElementById('modal-save-code').textContent).then(() => alert('Code copied!')); }
|
|
@@ -602,7 +570,6 @@ INDEX_HTML = '''
|
|
| 602 |
});
|
| 603 |
}
|
| 604 |
|
| 605 |
-
// --- CORE ACTIONS ---
|
| 606 |
async function saveComic() {
|
| 607 |
const state = getCurrentState();
|
| 608 |
if(!state || state.length === 0) { alert('No comic to save!'); return; }
|
|
@@ -678,9 +645,7 @@ INDEX_HTML = '''
|
|
| 678 |
pDiv.onclick = (e) => { e.stopPropagation(); selectPanel(pDiv); };
|
| 679 |
const img = document.createElement('img');
|
| 680 |
img.src = pan.src.includes('?') ? pan.src : pan.src + `?sid=${sid}`;
|
| 681 |
-
img.dataset.zoom = pan.zoom || 100;
|
| 682 |
-
img.dataset.translateX = pan.tx || 0;
|
| 683 |
-
img.dataset.translateY = pan.ty || 0;
|
| 684 |
updateImageTransform(img);
|
| 685 |
img.onmousedown = (e) => startPan(e, img);
|
| 686 |
pDiv.appendChild(img);
|
|
@@ -696,14 +661,9 @@ INDEX_HTML = '''
|
|
| 696 |
const pCount = document.getElementById('page-count').value;
|
| 697 |
if(!f) return alert("Select a video");
|
| 698 |
sid = genUUID(); localStorage.setItem('comic_sid', sid); currentSaveCode = null;
|
| 699 |
-
|
| 700 |
document.querySelector('.upload-box').style.display='none';
|
| 701 |
document.getElementById('loading-view').style.display='flex';
|
| 702 |
-
|
| 703 |
-
const fd = new FormData();
|
| 704 |
-
fd.append('file', f);
|
| 705 |
-
fd.append('target_pages', pCount);
|
| 706 |
-
|
| 707 |
const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
|
| 708 |
if(r.ok) interval = setInterval(checkStatus, 2000);
|
| 709 |
else { alert("Upload failed"); location.reload(); }
|
|
@@ -736,35 +696,27 @@ INDEX_HTML = '''
|
|
| 736 |
});
|
| 737 |
}
|
| 738 |
|
| 739 |
-
// --- INTERACTIVE ELEMENTS ---
|
| 740 |
function createBubbleHTML(data) {
|
| 741 |
const b = document.createElement('div');
|
| 742 |
-
// Ensure default classes for tail rendering
|
| 743 |
const type = data.type || 'speech';
|
| 744 |
b.className = data.classes || `speech-bubble ${type} tail-bottom`;
|
| 745 |
b.dataset.type = type;
|
| 746 |
-
|
| 747 |
b.style.left = data.left; b.style.top = data.top;
|
| 748 |
if(data.width) b.style.width = data.width; if(data.height) b.style.height = data.height;
|
| 749 |
if(data.font) b.style.fontFamily = data.font;
|
| 750 |
-
|
| 751 |
-
if(data.colors) {
|
| 752 |
-
b.style.setProperty('--bubble-fill-color', data.colors.fill || '#4ECDC4');
|
| 753 |
-
b.style.setProperty('--bubble-text-color', data.colors.text || '#ffffff');
|
| 754 |
-
}
|
| 755 |
if(data.tailPos) b.style.setProperty('--tail-pos', data.tailPos);
|
| 756 |
|
| 757 |
const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || ''; b.appendChild(textSpan);
|
| 758 |
|
| 759 |
-
|
| 760 |
-
if(type === 'thought') {
|
| 761 |
-
for(let i=1; i<=2; i++){
|
| 762 |
-
const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; b.appendChild(d);
|
| 763 |
-
}
|
| 764 |
-
}
|
| 765 |
|
| 766 |
['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); });
|
| 767 |
-
b.onmousedown = (e) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 768 |
b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
|
| 769 |
return b;
|
| 770 |
}
|
|
@@ -778,16 +730,12 @@ INDEX_HTML = '''
|
|
| 778 |
textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); } });
|
| 779 |
}
|
| 780 |
|
| 781 |
-
// --- GLOBAL MOUSE EVENTS ---
|
| 782 |
document.addEventListener('mousemove', (e) => {
|
| 783 |
if(isDragging && selectedBubble) { selectedBubble.style.left = (initX + e.clientX - startX) + 'px'; selectedBubble.style.top = (initY + e.clientY - startY) + 'px'; }
|
| 784 |
if(isResizing && selectedBubble) { resizeBubble(e); }
|
| 785 |
if(isPanning && selectedPanel) { panImage(e); }
|
| 786 |
});
|
| 787 |
-
document.addEventListener('mouseup', () => {
|
| 788 |
-
if(isDragging || isResizing || isPanning) saveDraft();
|
| 789 |
-
isDragging = false; isResizing = false; isPanning = false;
|
| 790 |
-
});
|
| 791 |
|
| 792 |
function startResize(e, dir) { e.preventDefault(); e.stopPropagation(); isResizing = true; resizeHandle = dir; const rect = selectedBubble.getBoundingClientRect(); originalWidth = rect.width; originalHeight = rect.height; originalMouseX = e.clientX; originalMouseY = e.clientY; }
|
| 793 |
function resizeBubble(e) { if (!isResizing || !selectedBubble) return; const dx = e.clientX - originalMouseX; const dy = e.clientY - originalMouseY; if(resizeHandle.includes('e')) selectedBubble.style.width = (originalWidth + dx)+'px'; if(resizeHandle.includes('s')) selectedBubble.style.height = (originalHeight + dy)+'px'; }
|
|
@@ -796,14 +744,11 @@ INDEX_HTML = '''
|
|
| 796 |
if(selectedBubble) selectedBubble.classList.remove('selected');
|
| 797 |
if(selectedPanel) { selectedPanel.classList.remove('selected'); selectedPanel = null; }
|
| 798 |
selectedBubble = el; el.classList.add('selected');
|
| 799 |
-
|
| 800 |
-
// Enable controls
|
| 801 |
document.getElementById('bubble-type-select').disabled = false;
|
| 802 |
document.getElementById('font-select').disabled = false;
|
| 803 |
document.getElementById('bubble-text-color').disabled = false;
|
| 804 |
document.getElementById('bubble-fill-color').disabled = false;
|
| 805 |
document.getElementById('tail-controls').style.display = 'block';
|
| 806 |
-
|
| 807 |
document.getElementById('bubble-type-select').value = el.dataset.type || 'speech';
|
| 808 |
}
|
| 809 |
|
|
@@ -811,12 +756,9 @@ INDEX_HTML = '''
|
|
| 811 |
if(selectedPanel) selectedPanel.classList.remove('selected');
|
| 812 |
if(selectedBubble) { selectedBubble.classList.remove('selected'); selectedBubble = null; }
|
| 813 |
selectedPanel = el; el.classList.add('selected');
|
| 814 |
-
|
| 815 |
-
// Enable Zoom
|
| 816 |
document.getElementById('zoom-slider').disabled = false;
|
| 817 |
const img = el.querySelector('img');
|
| 818 |
document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
|
| 819 |
-
|
| 820 |
document.getElementById('bubble-type-select').disabled = true;
|
| 821 |
document.getElementById('font-select').disabled = true;
|
| 822 |
document.getElementById('tail-controls').style.display = 'none';
|
|
@@ -836,24 +778,13 @@ INDEX_HTML = '''
|
|
| 836 |
function changeBubbleType(type) {
|
| 837 |
if(!selectedBubble) return;
|
| 838 |
selectedBubble.dataset.type = type;
|
| 839 |
-
|
| 840 |
-
selectedBubble.classList.remove('speech', 'thought', 'reaction', 'narration');
|
| 841 |
-
selectedBubble.classList.add(type);
|
| 842 |
-
|
| 843 |
-
// Thought bubbles need dots
|
| 844 |
selectedBubble.querySelectorAll('.thought-dot').forEach(d=>d.remove());
|
| 845 |
-
if(type === 'thought') {
|
| 846 |
-
for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; selectedBubble.appendChild(d); }
|
| 847 |
-
}
|
| 848 |
-
saveDraft();
|
| 849 |
-
}
|
| 850 |
-
|
| 851 |
-
function changeFont(font) {
|
| 852 |
-
if(!selectedBubble) return;
|
| 853 |
-
selectedBubble.style.fontFamily = font;
|
| 854 |
saveDraft();
|
| 855 |
}
|
| 856 |
|
|
|
|
| 857 |
function rotateTail() {
|
| 858 |
if(!selectedBubble) return;
|
| 859 |
const type = selectedBubble.dataset.type;
|
|
@@ -866,160 +797,23 @@ INDEX_HTML = '''
|
|
| 866 |
}
|
| 867 |
saveDraft();
|
| 868 |
}
|
|
|
|
| 869 |
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
selectedBubble.style.setProperty('--tail-pos', v+'%');
|
| 873 |
-
saveDraft();
|
| 874 |
-
}
|
| 875 |
-
}
|
| 876 |
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
});
|
| 883 |
|
| 884 |
-
|
| 885 |
-
function
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
img.dataset.zoom = el.value;
|
| 889 |
-
updateImageTransform(img);
|
| 890 |
-
saveDraft();
|
| 891 |
-
}
|
| 892 |
|
| 893 |
-
function
|
| 894 |
-
if(parseFloat(img.dataset.zoom || 100) <= 100) return;
|
| 895 |
-
e.preventDefault();
|
| 896 |
-
isPanning = true;
|
| 897 |
-
selectedPanel = img.closest('.panel');
|
| 898 |
-
panStartX = e.clientX;
|
| 899 |
-
panStartY = e.clientY;
|
| 900 |
-
panStartTx = parseFloat(img.dataset.translateX || 0);
|
| 901 |
-
panStartTy = parseFloat(img.dataset.translateY || 0);
|
| 902 |
-
img.classList.add('panning');
|
| 903 |
-
}
|
| 904 |
-
|
| 905 |
-
function panImage(e) {
|
| 906 |
-
if(!isPanning || !selectedPanel) return;
|
| 907 |
-
const img = selectedPanel.querySelector('img');
|
| 908 |
-
img.dataset.translateX = panStartTx + (e.clientX - panStartX);
|
| 909 |
-
img.dataset.translateY = panStartTy + (e.clientY - panStartY);
|
| 910 |
-
updateImageTransform(img);
|
| 911 |
-
}
|
| 912 |
-
|
| 913 |
-
function updateImageTransform(img) {
|
| 914 |
-
const z = (img.dataset.zoom || 100) / 100;
|
| 915 |
-
const x = img.dataset.translateX || 0;
|
| 916 |
-
const y = img.dataset.translateY || 0;
|
| 917 |
-
img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`;
|
| 918 |
-
img.classList.toggle('pannable', z > 1);
|
| 919 |
-
}
|
| 920 |
-
|
| 921 |
-
function resetPanelTransform() {
|
| 922 |
-
if(!selectedPanel) return alert("Select a panel");
|
| 923 |
-
const img = selectedPanel.querySelector('img');
|
| 924 |
-
img.dataset.zoom = 100; img.dataset.translateX = 0; img.dataset.translateY = 0;
|
| 925 |
-
document.getElementById('zoom-slider').value = 100;
|
| 926 |
-
updateImageTransform(img);
|
| 927 |
-
saveDraft();
|
| 928 |
-
}
|
| 929 |
-
|
| 930 |
-
function replacePanelImage() {
|
| 931 |
-
if(!selectedPanel) return alert("Select a panel");
|
| 932 |
-
const inp = document.getElementById('image-uploader');
|
| 933 |
-
inp.onchange = async (e) => {
|
| 934 |
-
const fd = new FormData(); fd.append('image', e.target.files[0]);
|
| 935 |
-
const img = selectedPanel.querySelector('img');
|
| 936 |
-
const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd});
|
| 937 |
-
const d = await r.json();
|
| 938 |
-
if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(); }
|
| 939 |
-
inp.value = '';
|
| 940 |
-
};
|
| 941 |
-
inp.click();
|
| 942 |
-
}
|
| 943 |
-
|
| 944 |
-
async function adjustFrame(dir) {
|
| 945 |
-
if(isProcessing) return;
|
| 946 |
-
if(!selectedPanel) return alert("Select a panel");
|
| 947 |
-
const img = selectedPanel.querySelector('img');
|
| 948 |
-
let fname = img.src.split('/').pop().split('?')[0];
|
| 949 |
-
|
| 950 |
-
setProcessing(true);
|
| 951 |
-
img.style.opacity = '0.5';
|
| 952 |
-
|
| 953 |
-
try {
|
| 954 |
-
const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) });
|
| 955 |
-
const d = await r.json();
|
| 956 |
-
if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; }
|
| 957 |
-
else { alert('Error: ' + d.message); }
|
| 958 |
-
} catch(e) { console.error(e); }
|
| 959 |
-
|
| 960 |
-
img.style.opacity = '1';
|
| 961 |
-
setProcessing(false);
|
| 962 |
-
saveDraft();
|
| 963 |
-
}
|
| 964 |
-
|
| 965 |
-
async function gotoTimestamp() {
|
| 966 |
-
if(isProcessing) return;
|
| 967 |
-
if(!selectedPanel) return alert("Select a panel");
|
| 968 |
-
let v = document.getElementById('timestamp-input').value.trim();
|
| 969 |
-
if(!v) return;
|
| 970 |
-
if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); }
|
| 971 |
-
if(isNaN(v)) return alert("Invalid time");
|
| 972 |
-
|
| 973 |
-
const img = selectedPanel.querySelector('img');
|
| 974 |
-
let fname = img.src.split('/').pop().split('?')[0];
|
| 975 |
-
|
| 976 |
-
setProcessing(true);
|
| 977 |
-
img.style.opacity = '0.5';
|
| 978 |
-
|
| 979 |
-
try {
|
| 980 |
-
const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) });
|
| 981 |
-
const d = await r.json();
|
| 982 |
-
if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; }
|
| 983 |
-
else { alert('Error: ' + d.message); }
|
| 984 |
-
} catch(e) { console.error(e); }
|
| 985 |
-
|
| 986 |
-
img.style.opacity = '1';
|
| 987 |
-
setProcessing(false);
|
| 988 |
-
saveDraft();
|
| 989 |
-
}
|
| 990 |
-
|
| 991 |
-
async function exportComic() {
|
| 992 |
-
const pgs = document.querySelectorAll('.comic-page');
|
| 993 |
-
if(pgs.length === 0) return alert("No pages found");
|
| 994 |
-
alert(`Exporting ${pgs.length} page(s)...`);
|
| 995 |
-
|
| 996 |
-
// Temporarily fix bubbles for export
|
| 997 |
-
const bubbles = document.querySelectorAll('.speech-bubble');
|
| 998 |
-
bubbles.forEach(b => {
|
| 999 |
-
const rect = b.getBoundingClientRect();
|
| 1000 |
-
b.style.width = rect.width + 'px';
|
| 1001 |
-
b.style.height = rect.height + 'px';
|
| 1002 |
-
});
|
| 1003 |
-
|
| 1004 |
-
for(let i = 0; i < pgs.length; i++) {
|
| 1005 |
-
try {
|
| 1006 |
-
const u = await htmlToImage.toPng(pgs[i], {pixelRatio: 2});
|
| 1007 |
-
const a = document.createElement('a'); a.href = u; a.download = `Comic-Page-${i+1}.png`; a.click();
|
| 1008 |
-
}
|
| 1009 |
-
catch(err) { console.error(err); alert(`Failed to export page ${i+1}`); }
|
| 1010 |
-
}
|
| 1011 |
-
|
| 1012 |
-
// Reset
|
| 1013 |
-
bubbles.forEach(b => { b.style.width = ''; b.style.height = ''; });
|
| 1014 |
-
}
|
| 1015 |
-
|
| 1016 |
-
function goBackToUpload() {
|
| 1017 |
-
if(confirm('Go home? Unsaved changes will be lost.')) {
|
| 1018 |
-
document.getElementById('editor-container').style.display = 'none';
|
| 1019 |
-
document.getElementById('upload-container').style.display = 'flex';
|
| 1020 |
-
document.getElementById('loading-view').style.display = 'none';
|
| 1021 |
-
}
|
| 1022 |
-
}
|
| 1023 |
</script>
|
| 1024 |
</body>
|
| 1025 |
</html>
|
|
|
|
| 1 |
+
import spaces # <--- CRITICAL: MUST BE FIRST
|
| 2 |
import os
|
| 3 |
import time
|
| 4 |
import threading
|
|
|
|
| 24 |
return True
|
| 25 |
|
| 26 |
# ======================================================
|
| 27 |
+
# 🧱 DATA CLASSES
|
| 28 |
# ======================================================
|
| 29 |
def bubble(dialog="", bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, emotion='normal', type='speech'):
|
| 30 |
return {
|
|
|
|
| 68 |
return code
|
| 69 |
|
| 70 |
# ======================================================
|
| 71 |
+
# 🧠 GLOBAL GPU FUNCTIONS
|
| 72 |
# ======================================================
|
| 73 |
|
| 74 |
@spaces.GPU(duration=300)
|
| 75 |
def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
|
| 76 |
print(f"🚀 GPU Task Started: {video_path} | Pages: {target_pages}")
|
| 77 |
|
|
|
|
| 78 |
import cv2
|
| 79 |
import srt
|
| 80 |
import numpy as np
|
|
|
|
| 105 |
with open(user_srt, 'r', encoding='utf-8') as f:
|
| 106 |
all_subs = list(srt.parse(f.read()))
|
| 107 |
|
| 108 |
+
# 3. Smart Keyframe Selection
|
|
|
|
| 109 |
valid_subs = [s for s in all_subs if s.content.strip()]
|
| 110 |
raw_moments = [{'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in valid_subs]
|
| 111 |
|
|
|
|
| 113 |
panels_per_page = 4
|
| 114 |
total_panels_needed = target_pages * panels_per_page
|
| 115 |
|
|
|
|
|
|
|
| 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:
|
|
|
|
| 124 |
indices = np.linspace(0, len(raw_moments) - 1, total_panels_needed, dtype=int)
|
| 125 |
selected_moments = [raw_moments[i] for i in indices]
|
| 126 |
|
|
|
|
| 140 |
fname = f"frame_{count:04d}.png"
|
| 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
|
| 148 |
cap.release()
|
|
|
|
| 156 |
se = SimpleColorEnhancer()
|
| 157 |
qe = QualityColorEnhancer()
|
| 158 |
|
|
|
|
| 159 |
for f in frame_files_ordered:
|
| 160 |
p = os.path.join(frames_dir, f)
|
| 161 |
try: se.enhance_single(p, p)
|
|
|
|
| 164 |
except: pass
|
| 165 |
|
| 166 |
# 6. Bubble Placement
|
|
|
|
| 167 |
bubbles_list = []
|
|
|
|
| 168 |
for f in frame_files_ordered:
|
| 169 |
p = os.path.join(frames_dir, f)
|
| 170 |
dialogue = frame_metadata.get(f, {}).get('dialogue', '')
|
| 171 |
|
|
|
|
| 172 |
b_type = 'speech'
|
| 173 |
if '(' in dialogue and ')' in dialogue: b_type = 'narration'
|
| 174 |
elif '!' in dialogue and dialogue.isupper(): b_type = 'reaction'
|
|
|
|
| 178 |
faces = face_detector.detect_faces(p)
|
| 179 |
lip = face_detector.get_lip_position(p, faces[0]) if faces else (-1, -1)
|
| 180 |
bx, by = ai_bubble_placer.place_bubble_ai(p, lip)
|
| 181 |
+
b = bubble(dialog=dialogue, bubble_offset_x=bx, bubble_offset_y=by, lip_x=lip[0], lip_y=lip[1], type=b_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
bubbles_list.append(b)
|
| 183 |
except:
|
| 184 |
bubbles_list.append(bubble(dialog=dialogue, type=b_type))
|
| 185 |
|
| 186 |
+
# 7. Final Layout
|
|
|
|
| 187 |
pages = []
|
|
|
|
|
|
|
| 188 |
for i in range(target_pages):
|
| 189 |
start_idx = i * 4
|
| 190 |
end_idx = start_idx + 4
|
|
|
|
| 191 |
p_frames = frame_files_ordered[start_idx:end_idx]
|
| 192 |
p_bubbles = bubbles_list[start_idx:end_idx]
|
|
|
|
| 193 |
if p_frames:
|
| 194 |
pg_panels = [panel(image=f) for f in p_frames]
|
| 195 |
pages.append(Page(panels=pg_panels, bubbles=p_bubbles))
|
| 196 |
|
|
|
|
| 197 |
result = []
|
| 198 |
for pg in pages:
|
| 199 |
p_data = [p if isinstance(p, dict) else p.__dict__ for p in pg.panels]
|
|
|
|
| 298 |
json.dump({'message': msg, 'progress': prog}, f)
|
| 299 |
|
| 300 |
# ======================================================
|
| 301 |
+
# 🌐 ROUTES & HTML (FIXED SELECTION LOGIC)
|
| 302 |
# ======================================================
|
| 303 |
|
| 304 |
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 |
.panel img.pannable { cursor: grab; }
|
| 352 |
.panel img.panning { cursor: grabbing; }
|
| 353 |
|
| 354 |
+
/* BUBBLES - FIX: pointer-events none on text to fix selection */
|
| 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 |
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 TYPES & TAILS */
|
| 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);
|
|
|
|
| 377 |
-webkit-mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
|
| 378 |
mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
|
| 379 |
}
|
|
|
|
|
|
|
| 380 |
|
| 381 |
+
.speech-bubble.speech.tail-bottom:before { top: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
|
| 382 |
.speech-bubble.speech.tail-top { border-radius: min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) var(--r) var(--r) / var(--r); }
|
| 383 |
.speech-bubble.speech.tail-top:before { bottom: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: scaleY(-1); }
|
|
|
|
| 384 |
.speech-bubble.speech.tail-left { border-radius: var(--r); }
|
| 385 |
.speech-bubble.speech.tail-left:before { right: 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 right; }
|
|
|
|
| 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 |
|
|
|
|
| 547 |
localStorage.setItem('comic_sid', sid);
|
| 548 |
|
| 549 |
let currentSaveCode = null;
|
| 550 |
+
let isProcessing = false;
|
| 551 |
let interval, selectedBubble = null, selectedPanel = null;
|
| 552 |
let isDragging = false, isResizing = false, isPanning = false;
|
| 553 |
let startX, startY, initX, initY, panStartX, panStartY, panStartTx, panStartTy;
|
|
|
|
| 556 |
|
| 557 |
if(localStorage.getItem('comic_draft_'+sid)) document.getElementById('restore-draft-btn').style.display = 'block';
|
| 558 |
|
|
|
|
| 559 |
function showSaveModal(code) { document.getElementById('modal-save-code').textContent = code; document.getElementById('save-modal').style.display = 'flex'; }
|
| 560 |
function closeModal() { document.getElementById('save-modal').style.display = 'none'; }
|
| 561 |
function copyModalCode() { navigator.clipboard.writeText(document.getElementById('modal-save-code').textContent).then(() => alert('Code copied!')); }
|
|
|
|
| 570 |
});
|
| 571 |
}
|
| 572 |
|
|
|
|
| 573 |
async function saveComic() {
|
| 574 |
const state = getCurrentState();
|
| 575 |
if(!state || state.length === 0) { alert('No comic to save!'); return; }
|
|
|
|
| 645 |
pDiv.onclick = (e) => { e.stopPropagation(); selectPanel(pDiv); };
|
| 646 |
const img = document.createElement('img');
|
| 647 |
img.src = pan.src.includes('?') ? pan.src : pan.src + `?sid=${sid}`;
|
| 648 |
+
img.dataset.zoom = pan.zoom || 100; img.dataset.translateX = pan.tx || 0; img.dataset.translateY = pan.ty || 0;
|
|
|
|
|
|
|
| 649 |
updateImageTransform(img);
|
| 650 |
img.onmousedown = (e) => startPan(e, img);
|
| 651 |
pDiv.appendChild(img);
|
|
|
|
| 661 |
const pCount = document.getElementById('page-count').value;
|
| 662 |
if(!f) return alert("Select a video");
|
| 663 |
sid = genUUID(); localStorage.setItem('comic_sid', sid); currentSaveCode = null;
|
|
|
|
| 664 |
document.querySelector('.upload-box').style.display='none';
|
| 665 |
document.getElementById('loading-view').style.display='flex';
|
| 666 |
+
const fd = new FormData(); fd.append('file', f); fd.append('target_pages', pCount);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 667 |
const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
|
| 668 |
if(r.ok) interval = setInterval(checkStatus, 2000);
|
| 669 |
else { alert("Upload failed"); location.reload(); }
|
|
|
|
| 696 |
});
|
| 697 |
}
|
| 698 |
|
|
|
|
| 699 |
function createBubbleHTML(data) {
|
| 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;
|
| 706 |
if(data.font) b.style.fontFamily = data.font;
|
| 707 |
+
if(data.colors) { b.style.setProperty('--bubble-fill-color', data.colors.fill || '#4ECDC4'); b.style.setProperty('--bubble-text-color', data.colors.text || '#ffffff'); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
if(data.tailPos) b.style.setProperty('--tail-pos', data.tailPos);
|
| 709 |
|
| 710 |
const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || ''; b.appendChild(textSpan);
|
| 711 |
|
| 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(); }; // Critical Fix for selection
|
| 720 |
b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
|
| 721 |
return b;
|
| 722 |
}
|
|
|
|
| 730 |
textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); textarea.blur(); } });
|
| 731 |
}
|
| 732 |
|
|
|
|
| 733 |
document.addEventListener('mousemove', (e) => {
|
| 734 |
if(isDragging && selectedBubble) { selectedBubble.style.left = (initX + e.clientX - startX) + 'px'; selectedBubble.style.top = (initY + e.clientY - startY) + 'px'; }
|
| 735 |
if(isResizing && selectedBubble) { resizeBubble(e); }
|
| 736 |
if(isPanning && selectedPanel) { panImage(e); }
|
| 737 |
});
|
| 738 |
+
document.addEventListener('mouseup', () => { if(isDragging || isResizing || isPanning) saveDraft(); isDragging = false; isResizing = false; isPanning = false; });
|
|
|
|
|
|
|
|
|
|
| 739 |
|
| 740 |
function startResize(e, dir) { e.preventDefault(); e.stopPropagation(); isResizing = true; resizeHandle = dir; const rect = selectedBubble.getBoundingClientRect(); originalWidth = rect.width; originalHeight = rect.height; originalMouseX = e.clientX; originalMouseY = e.clientY; }
|
| 741 |
function resizeBubble(e) { if (!isResizing || !selectedBubble) return; const dx = e.clientX - originalMouseX; const dy = e.clientY - originalMouseY; if(resizeHandle.includes('e')) selectedBubble.style.width = (originalWidth + dx)+'px'; if(resizeHandle.includes('s')) selectedBubble.style.height = (originalHeight + dy)+'px'; }
|
|
|
|
| 744 |
if(selectedBubble) selectedBubble.classList.remove('selected');
|
| 745 |
if(selectedPanel) { selectedPanel.classList.remove('selected'); selectedPanel = null; }
|
| 746 |
selectedBubble = el; el.classList.add('selected');
|
|
|
|
|
|
|
| 747 |
document.getElementById('bubble-type-select').disabled = false;
|
| 748 |
document.getElementById('font-select').disabled = false;
|
| 749 |
document.getElementById('bubble-text-color').disabled = false;
|
| 750 |
document.getElementById('bubble-fill-color').disabled = false;
|
| 751 |
document.getElementById('tail-controls').style.display = 'block';
|
|
|
|
| 752 |
document.getElementById('bubble-type-select').value = el.dataset.type || 'speech';
|
| 753 |
}
|
| 754 |
|
|
|
|
| 756 |
if(selectedPanel) selectedPanel.classList.remove('selected');
|
| 757 |
if(selectedBubble) { selectedBubble.classList.remove('selected'); selectedBubble = null; }
|
| 758 |
selectedPanel = el; el.classList.add('selected');
|
|
|
|
|
|
|
| 759 |
document.getElementById('zoom-slider').disabled = false;
|
| 760 |
const img = el.querySelector('img');
|
| 761 |
document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
|
|
|
|
| 762 |
document.getElementById('bubble-type-select').disabled = true;
|
| 763 |
document.getElementById('font-select').disabled = true;
|
| 764 |
document.getElementById('tail-controls').style.display = 'none';
|
|
|
|
| 778 |
function changeBubbleType(type) {
|
| 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;
|
|
|
|
| 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(); } });
|
| 803 |
+
document.getElementById('bubble-fill-color').addEventListener('input', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(); } });
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
|
| 805 |
+
function handleZoom(el) { if(!selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.zoom = el.value; updateImageTransform(img); saveDraft(); }
|
| 806 |
+
function startPan(e, img) { if(parseFloat(img.dataset.zoom || 100) <= 100) return; e.preventDefault(); isPanning = true; selectedPanel = img.closest('.panel'); panStartX = e.clientX; panStartY = e.clientY; panStartTx = parseFloat(img.dataset.translateX || 0); panStartTy = parseFloat(img.dataset.translateY || 0); img.classList.add('panning'); }
|
| 807 |
+
function panImage(e) { if(!isPanning || !selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.translateX = panStartTx + (e.clientX - panStartX); img.dataset.translateY = panStartTy + (e.clientY - panStartY); updateImageTransform(img); }
|
| 808 |
+
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', z > 1); }
|
| 809 |
+
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(); }
|
|
|
|
| 810 |
|
| 811 |
+
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(); } inp.value = ''; }; inp.click(); }
|
| 812 |
+
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(); }
|
| 813 |
+
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(); }
|
| 814 |
+
async function exportComic() { const pgs = document.querySelectorAll('.comic-page'); if(pgs.length === 0) return alert("No pages found"); alert(`Exporting ${pgs.length} page(s)...`); const bubbles = document.querySelectorAll('.speech-bubble'); bubbles.forEach(b => { const rect = b.getBoundingClientRect(); b.style.width = rect.width + 'px'; b.style.height = rect.height + 'px'; }); for(let i = 0; i < pgs.length; i++) { try { const u = await htmlToImage.toPng(pgs[i], {pixelRatio: 2}); const a = document.createElement('a'); a.href = u; a.download = `Comic-Page-${i+1}.png`; a.click(); } catch(err) { console.error(err); alert(`Failed to export page ${i+1}`); } } bubbles.forEach(b => { b.style.width = ''; b.style.height = ''; }); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 815 |
|
| 816 |
+
function goBackToUpload() { if(confirm('Go home? Unsaved changes will be lost.')) { document.getElementById('editor-container').style.display = 'none'; document.getElementById('upload-container').style.display = 'flex'; document.getElementById('loading-view').style.display = 'none'; } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
</script>
|
| 818 |
</body>
|
| 819 |
</html>
|