Update app_enhanced.py
Browse files- app_enhanced.py +94 -89
app_enhanced.py
CHANGED
|
@@ -52,8 +52,8 @@ def generate_save_code(length=8):
|
|
| 52 |
# ======================================================
|
| 53 |
# 🧱 DATA CLASSES
|
| 54 |
# ======================================================
|
| 55 |
-
def bubble(dialog="", x=50, y=
|
| 56 |
-
#
|
| 57 |
classes = f"speech-bubble {type}"
|
| 58 |
if type == 'speech':
|
| 59 |
classes += " tail-bottom"
|
|
@@ -61,7 +61,7 @@ def bubble(dialog="", x=50, y=20, type='speech'):
|
|
| 61 |
classes += " pos-bl"
|
| 62 |
|
| 63 |
return {
|
| 64 |
-
'dialog': dialog,
|
| 65 |
'bubble_offset_x': int(x),
|
| 66 |
'bubble_offset_y': int(y),
|
| 67 |
'type': type,
|
|
@@ -71,14 +71,6 @@ def bubble(dialog="", x=50, y=20, type='speech'):
|
|
| 71 |
'font': "'Comic Neue', cursive"
|
| 72 |
}
|
| 73 |
|
| 74 |
-
def panel(image=""):
|
| 75 |
-
return {'image': image}
|
| 76 |
-
|
| 77 |
-
class Page:
|
| 78 |
-
def __init__(self, panels, bubbles):
|
| 79 |
-
self.panels = panels
|
| 80 |
-
self.bubbles = bubbles
|
| 81 |
-
|
| 82 |
# ======================================================
|
| 83 |
# 🧠 GPU GENERATION
|
| 84 |
# ======================================================
|
|
@@ -104,17 +96,13 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 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 |
-
|
| 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:
|
|
@@ -128,8 +116,9 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 128 |
|
| 129 |
selected_moments = []
|
| 130 |
if not raw_moments:
|
|
|
|
| 131 |
times = np.linspace(1, max(1, duration-1), total_panels_needed)
|
| 132 |
-
for t in times: selected_moments.append({'text': '', 'start': t, 'end': t+1})
|
| 133 |
elif len(raw_moments) <= total_panels_needed:
|
| 134 |
selected_moments = raw_moments
|
| 135 |
else:
|
|
@@ -146,21 +135,23 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 146 |
cap.set(cv2.CAP_PROP_POS_MSEC, mid * 1000)
|
| 147 |
ret, frame = cap.read()
|
| 148 |
if ret:
|
| 149 |
-
# 🎯 SQUARE PADDING (0% Cut
|
| 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 |
-
#
|
| 157 |
square_img = cv2.resize(square_img, (1024, 1024))
|
| 158 |
|
| 159 |
fname = f"frame_{count:04d}.png"
|
| 160 |
p = os.path.join(frames_dir, fname)
|
| 161 |
cv2.imwrite(p, square_img)
|
| 162 |
|
| 163 |
-
|
|
|
|
|
|
|
| 164 |
frame_files_ordered.append(fname)
|
| 165 |
count += 1
|
| 166 |
cap.release()
|
|
@@ -169,29 +160,25 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 169 |
|
| 170 |
bubbles_list = []
|
| 171 |
for i, f in enumerate(frame_files_ordered):
|
| 172 |
-
dialogue = frame_metadata.get(f, {}).get('dialogue', '')
|
| 173 |
|
| 174 |
# Determine Bubble Type
|
| 175 |
b_type = 'speech'
|
| 176 |
-
if '(' in dialogue:
|
| 177 |
-
|
| 178 |
-
elif '
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
-
#
|
| 181 |
-
# Quadrants: TL, TR, BL, BR
|
| 182 |
-
# Panel size approx 400x400
|
| 183 |
pos_idx = i % 4
|
| 184 |
-
if pos_idx == 0:
|
| 185 |
-
|
| 186 |
-
elif pos_idx ==
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
elif pos_idx == 3: # BR
|
| 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,7 +193,6 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_p
|
|
| 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:
|
|
@@ -340,22 +326,18 @@ INDEX_HTML = '''
|
|
| 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:
|
| 351 |
}
|
| 352 |
|
| 353 |
-
/* 🎯 5px White Borders between panels */
|
| 354 |
.comic-grid {
|
| 355 |
-
width: 100%; height: 100%; position: relative;
|
| 356 |
-
|
| 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; }
|
|
@@ -714,8 +696,8 @@ INDEX_HTML = '''
|
|
| 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,9 +708,26 @@ INDEX_HTML = '''
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 730 |
|
| 731 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
|
| 733 |
return b;
|
| 734 |
}
|
|
@@ -739,8 +738,10 @@ INDEX_HTML = '''
|
|
| 739 |
if(newText !== null) { textSpan.textContent = newText; saveState(); }
|
| 740 |
}
|
| 741 |
|
|
|
|
| 742 |
document.addEventListener('mousemove', (e) => {
|
| 743 |
if(!dragType) return;
|
|
|
|
| 744 |
if(dragType === 'handle') {
|
| 745 |
const rect = activeObj.grid.getBoundingClientRect();
|
| 746 |
let x = (e.clientX - rect.left) / rect.width * 100;
|
|
@@ -752,9 +753,13 @@ INDEX_HTML = '''
|
|
| 752 |
img.dataset.translateY = parseFloat(img.dataset.translateY) + dy;
|
| 753 |
updateImageTransform(img); dragStart = {x: e.clientX, y: e.clientY};
|
| 754 |
} else if(dragType === 'bubble') {
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
} else if(dragType === 'resize') {
|
| 759 |
const dx = e.clientX - activeObj.mx; const dy = e.clientY - activeObj.my;
|
| 760 |
activeObj.b.style.width = (activeObj.startW + dx) + 'px';
|
|
@@ -768,8 +773,6 @@ INDEX_HTML = '''
|
|
| 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,7 +871,7 @@ INDEX_HTML = '''
|
|
| 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,
|
| 872 |
});
|
| 873 |
const panels = [];
|
| 874 |
grid.querySelectorAll('.panel').forEach(pan => {
|
|
@@ -894,7 +897,8 @@ def upload():
|
|
| 894 |
if not sid: return jsonify({'success': False, 'message': 'Missing session ID'}), 400
|
| 895 |
|
| 896 |
file = request.files.get('file')
|
| 897 |
-
if not file or file.filename == '':
|
|
|
|
| 898 |
|
| 899 |
target_pages = request.form.get('target_pages', 4)
|
| 900 |
gen = EnhancedComicGenerator(sid)
|
|
@@ -929,23 +933,6 @@ def regen():
|
|
| 929 |
gen = EnhancedComicGenerator(sid)
|
| 930 |
return jsonify(regen_frame_gpu(gen.video_path, gen.frames_dir, gen.metadata_path, d['filename'], d['direction']))
|
| 931 |
|
| 932 |
-
@app.route('/goto_timestamp', methods=['POST'])
|
| 933 |
-
def go_time():
|
| 934 |
-
sid = request.args.get('sid')
|
| 935 |
-
d = request.get_json()
|
| 936 |
-
gen = EnhancedComicGenerator(sid)
|
| 937 |
-
return jsonify(get_frame_at_ts_gpu(gen.video_path, gen.frames_dir, gen.metadata_path, d['filename'], float(d['timestamp'])))
|
| 938 |
-
|
| 939 |
-
@app.route('/replace_panel', methods=['POST'])
|
| 940 |
-
def rep_panel():
|
| 941 |
-
sid = request.args.get('sid')
|
| 942 |
-
f = request.files['image']
|
| 943 |
-
frames_dir = os.path.join(BASE_USER_DIR, sid, 'frames')
|
| 944 |
-
os.makedirs(frames_dir, exist_ok=True)
|
| 945 |
-
fname = f"replaced_{int(time.time() * 1000)}.png"
|
| 946 |
-
f.save(os.path.join(frames_dir, fname))
|
| 947 |
-
return jsonify({'success': True, 'new_filename': fname})
|
| 948 |
-
|
| 949 |
@app.route('/save_comic', methods=['POST'])
|
| 950 |
def save_comic():
|
| 951 |
sid = request.args.get('sid')
|
|
@@ -954,31 +941,49 @@ def save_comic():
|
|
| 954 |
save_code = generate_save_code()
|
| 955 |
save_dir = os.path.join(SAVED_COMICS_DIR, save_code)
|
| 956 |
os.makedirs(save_dir, exist_ok=True)
|
|
|
|
| 957 |
user_frames_dir = os.path.join(BASE_USER_DIR, sid, 'frames')
|
| 958 |
saved_frames_dir = os.path.join(save_dir, 'frames')
|
|
|
|
| 959 |
if os.path.exists(user_frames_dir):
|
| 960 |
if os.path.exists(saved_frames_dir): shutil.rmtree(saved_frames_dir)
|
| 961 |
shutil.copytree(user_frames_dir, saved_frames_dir)
|
| 962 |
-
|
| 963 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 964 |
return jsonify({'success': True, 'code': save_code})
|
| 965 |
-
except Exception as e:
|
|
|
|
|
|
|
| 966 |
|
| 967 |
@app.route('/load_comic/<code>')
|
| 968 |
def load_comic(code):
|
| 969 |
code = code.upper()
|
| 970 |
save_dir = os.path.join(SAVED_COMICS_DIR, code)
|
| 971 |
-
|
|
|
|
|
|
|
|
|
|
| 972 |
try:
|
| 973 |
-
with open(
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 982 |
|
| 983 |
if __name__ == '__main__':
|
| 984 |
try: gpu_warmup()
|
|
|
|
| 52 |
# ======================================================
|
| 53 |
# 🧱 DATA CLASSES
|
| 54 |
# ======================================================
|
| 55 |
+
def bubble(dialog="...", x=50, y=50, type='speech'):
|
| 56 |
+
# Default styling
|
| 57 |
classes = f"speech-bubble {type}"
|
| 58 |
if type == 'speech':
|
| 59 |
classes += " tail-bottom"
|
|
|
|
| 61 |
classes += " pos-bl"
|
| 62 |
|
| 63 |
return {
|
| 64 |
+
'dialog': dialog if dialog.strip() else "...", # Ensure text is never empty
|
| 65 |
'bubble_offset_x': int(x),
|
| 66 |
'bubble_offset_y': int(y),
|
| 67 |
'type': type,
|
|
|
|
| 71 |
'font': "'Comic Neue', cursive"
|
| 72 |
}
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
# ======================================================
|
| 75 |
# 🧠 GPU GENERATION
|
| 76 |
# ======================================================
|
|
|
|
| 96 |
if os.path.exists('test1.srt'):
|
| 97 |
shutil.move('test1.srt', user_srt)
|
| 98 |
elif not os.path.exists(user_srt):
|
| 99 |
+
with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:04,000\nText\n")
|
|
|
|
| 100 |
except:
|
| 101 |
+
with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:04,000\nText\n")
|
|
|
|
| 102 |
|
| 103 |
with open(user_srt, 'r', encoding='utf-8') as f:
|
| 104 |
+
try: all_subs = list(srt.parse(f.read()))
|
| 105 |
+
except: all_subs = []
|
|
|
|
|
|
|
| 106 |
|
| 107 |
valid_subs = [s for s in all_subs if s.content and s.content.strip()]
|
| 108 |
if valid_subs:
|
|
|
|
| 116 |
|
| 117 |
selected_moments = []
|
| 118 |
if not raw_moments:
|
| 119 |
+
# Create timestamps if no text
|
| 120 |
times = np.linspace(1, max(1, duration-1), total_panels_needed)
|
| 121 |
+
for t in times: selected_moments.append({'text': '...', 'start': t, 'end': t+1})
|
| 122 |
elif len(raw_moments) <= total_panels_needed:
|
| 123 |
selected_moments = raw_moments
|
| 124 |
else:
|
|
|
|
| 135 |
cap.set(cv2.CAP_PROP_POS_MSEC, mid * 1000)
|
| 136 |
ret, frame = cap.read()
|
| 137 |
if ret:
|
| 138 |
+
# 🎯 SQUARE PADDING (0% Cut, Matches Template)
|
| 139 |
h, w = frame.shape[:2]
|
| 140 |
sq_dim = max(h, w)
|
| 141 |
square_img = np.zeros((sq_dim, sq_dim, 3), dtype=np.uint8)
|
| 142 |
x_off = (sq_dim - w) // 2
|
| 143 |
y_off = (sq_dim - h) // 2
|
| 144 |
square_img[y_off:y_off+h, x_off:x_off+w] = frame
|
| 145 |
+
# Standardize size
|
| 146 |
square_img = cv2.resize(square_img, (1024, 1024))
|
| 147 |
|
| 148 |
fname = f"frame_{count:04d}.png"
|
| 149 |
p = os.path.join(frames_dir, fname)
|
| 150 |
cv2.imwrite(p, square_img)
|
| 151 |
|
| 152 |
+
# Ensure text exists
|
| 153 |
+
txt = moment['text'] if moment['text'].strip() else "..."
|
| 154 |
+
frame_metadata[fname] = {'dialogue': txt, 'time': mid}
|
| 155 |
frame_files_ordered.append(fname)
|
| 156 |
count += 1
|
| 157 |
cap.release()
|
|
|
|
| 160 |
|
| 161 |
bubbles_list = []
|
| 162 |
for i, f in enumerate(frame_files_ordered):
|
| 163 |
+
dialogue = frame_metadata.get(f, {}).get('dialogue', '...')
|
| 164 |
|
| 165 |
# Determine Bubble Type
|
| 166 |
b_type = 'speech'
|
| 167 |
+
if '(' in dialogue:
|
| 168 |
+
b_type = 'narration'
|
| 169 |
+
elif '!' in dialogue and len(dialogue) < 15: # Reaction only if short
|
| 170 |
+
b_type = 'reaction'
|
| 171 |
+
else:
|
| 172 |
+
b_type = 'speech'
|
| 173 |
|
| 174 |
+
# Smart Positioning (Center of quadrants)
|
|
|
|
|
|
|
| 175 |
pos_idx = i % 4
|
| 176 |
+
if pos_idx == 0: bx, by = 150, 80
|
| 177 |
+
elif pos_idx == 1: bx, by = 600, 80
|
| 178 |
+
elif pos_idx == 2: bx, by = 150, 600
|
| 179 |
+
elif pos_idx == 3: bx, by = 600, 600
|
| 180 |
+
else: bx, by = 50, 50
|
| 181 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
bubbles_list.append(bubble(dialog=dialogue, x=bx, y=by, type=b_type))
|
| 183 |
|
| 184 |
pages = []
|
|
|
|
| 193 |
img = np.zeros((1024, 1024, 3), dtype=np.uint8); img[:] = (30,30,30)
|
| 194 |
cv2.imwrite(os.path.join(frames_dir, fname), img)
|
| 195 |
p_frames.append(fname)
|
|
|
|
| 196 |
p_bubbles.append(bubble(dialog="", x=-999, y=-999, type='speech'))
|
| 197 |
|
| 198 |
if p_frames:
|
|
|
|
| 326 |
.page-wrapper { display: flex; flex-direction: column; align-items: center; }
|
| 327 |
.page-title { text-align: center; color: #eee; margin-bottom: 10px; font-size: 20px; font-weight: bold; }
|
| 328 |
|
|
|
|
| 329 |
.comic-page {
|
| 330 |
width: 800px;
|
| 331 |
height: 800px;
|
| 332 |
background: white;
|
| 333 |
box-shadow: 0 5px 30px rgba(0,0,0,0.6);
|
| 334 |
position: relative; overflow: hidden;
|
| 335 |
+
border: 6px solid #000;
|
| 336 |
}
|
| 337 |
|
|
|
|
| 338 |
.comic-grid {
|
| 339 |
+
width: 100%; height: 100%; position: relative; background: #000;
|
| 340 |
+
--y: 50%; --t1: 100%; --t2: 100%; --b1: 100%; --b2: 100%; --gap: 3px;
|
|
|
|
|
|
|
| 341 |
}
|
| 342 |
|
| 343 |
.panel { position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; background: #1a1a1a; cursor: grab; }
|
|
|
|
| 696 |
const type = data.type || 'speech';
|
| 697 |
let className = data.classes || `speech-bubble ${type} tail-bottom`;
|
| 698 |
if (type === 'thought' && !className.includes('pos-')) className += ' pos-bl';
|
|
|
|
| 699 |
b.className = className;
|
| 700 |
+
|
| 701 |
b.dataset.type = type;
|
| 702 |
b.style.left = data.left; b.style.top = data.top;
|
| 703 |
if(data.width) b.style.width = data.width;
|
|
|
|
| 708 |
|
| 709 |
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); } }
|
| 710 |
|
| 711 |
+
const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || 'Text'; b.appendChild(textSpan);
|
| 712 |
+
const resizer = document.createElement('div'); resizer.className = 'resize-handle';
|
| 713 |
+
resizer.onmousedown = (e) => { e.stopPropagation(); dragType='resize'; activeObj={b:b, startW:b.offsetWidth, startH:b.offsetHeight, mx:e.clientX, my:e.clientY}; };
|
| 714 |
+
b.appendChild(resizer);
|
| 715 |
|
| 716 |
+
// 🎯 DRAG FIX: Stop propagation to prevent selecting panel behind it
|
| 717 |
+
b.onmousedown = (e) => {
|
| 718 |
+
if(e.target === resizer) return;
|
| 719 |
+
e.stopPropagation();
|
| 720 |
+
e.preventDefault(); // Stop text selection
|
| 721 |
+
selectBubble(b);
|
| 722 |
+
dragType = 'bubble';
|
| 723 |
+
activeObj = b;
|
| 724 |
+
|
| 725 |
+
// Calculate offset so we drag from clicked point, not top-left jump
|
| 726 |
+
dragStart = {
|
| 727 |
+
x: e.clientX - b.getBoundingClientRect().left,
|
| 728 |
+
y: e.clientY - b.getBoundingClientRect().top
|
| 729 |
+
};
|
| 730 |
+
};
|
| 731 |
b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
|
| 732 |
return b;
|
| 733 |
}
|
|
|
|
| 738 |
if(newText !== null) { textSpan.textContent = newText; saveState(); }
|
| 739 |
}
|
| 740 |
|
| 741 |
+
// --- GLOBAL MOUSE EVENTS ---
|
| 742 |
document.addEventListener('mousemove', (e) => {
|
| 743 |
if(!dragType) return;
|
| 744 |
+
|
| 745 |
if(dragType === 'handle') {
|
| 746 |
const rect = activeObj.grid.getBoundingClientRect();
|
| 747 |
let x = (e.clientX - rect.left) / rect.width * 100;
|
|
|
|
| 753 |
img.dataset.translateY = parseFloat(img.dataset.translateY) + dy;
|
| 754 |
updateImageTransform(img); dragStart = {x: e.clientX, y: e.clientY};
|
| 755 |
} else if(dragType === 'bubble') {
|
| 756 |
+
// Correct parent-relative positioning
|
| 757 |
+
const parentRect = activeObj.parentElement.getBoundingClientRect();
|
| 758 |
+
let newX = e.clientX - parentRect.left - dragStart.x;
|
| 759 |
+
let newY = e.clientY - parentRect.top - dragStart.y;
|
| 760 |
+
|
| 761 |
+
activeObj.style.left = newX + 'px';
|
| 762 |
+
activeObj.style.top = newY + 'px';
|
| 763 |
} else if(dragType === 'resize') {
|
| 764 |
const dx = e.clientX - activeObj.mx; const dy = e.clientY - activeObj.my;
|
| 765 |
activeObj.b.style.width = (activeObj.startW + dx) + 'px';
|
|
|
|
| 773 |
dragType = null; activeObj = null;
|
| 774 |
});
|
| 775 |
|
|
|
|
|
|
|
| 776 |
function selectBubble(el) {
|
| 777 |
if(selectedBubble) selectedBubble.classList.remove('selected');
|
| 778 |
selectedBubble = el; el.classList.add('selected');
|
|
|
|
| 871 |
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%' };
|
| 872 |
const bubbles = [];
|
| 873 |
grid.querySelectorAll('.speech-bubble').forEach(b => {
|
| 874 |
+
bubbles.push({ text: b.querySelector('.bubble-text').textContent, left: b.style.left, top: b.style.top, width: b.style.width, height: b.style.height, colors: { fill: b.style.getPropertyValue('--bubble-fill'), text: b.style.getPropertyValue('--bubble-text') } });
|
| 875 |
});
|
| 876 |
const panels = [];
|
| 877 |
grid.querySelectorAll('.panel').forEach(pan => {
|
|
|
|
| 897 |
if not sid: return jsonify({'success': False, 'message': 'Missing session ID'}), 400
|
| 898 |
|
| 899 |
file = request.files.get('file')
|
| 900 |
+
if not file or file.filename == '':
|
| 901 |
+
return jsonify({'success': False, 'message': 'No file uploaded'}), 400
|
| 902 |
|
| 903 |
target_pages = request.form.get('target_pages', 4)
|
| 904 |
gen = EnhancedComicGenerator(sid)
|
|
|
|
| 933 |
gen = EnhancedComicGenerator(sid)
|
| 934 |
return jsonify(regen_frame_gpu(gen.video_path, gen.frames_dir, gen.metadata_path, d['filename'], d['direction']))
|
| 935 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 936 |
@app.route('/save_comic', methods=['POST'])
|
| 937 |
def save_comic():
|
| 938 |
sid = request.args.get('sid')
|
|
|
|
| 941 |
save_code = generate_save_code()
|
| 942 |
save_dir = os.path.join(SAVED_COMICS_DIR, save_code)
|
| 943 |
os.makedirs(save_dir, exist_ok=True)
|
| 944 |
+
|
| 945 |
user_frames_dir = os.path.join(BASE_USER_DIR, sid, 'frames')
|
| 946 |
saved_frames_dir = os.path.join(save_dir, 'frames')
|
| 947 |
+
|
| 948 |
if os.path.exists(user_frames_dir):
|
| 949 |
if os.path.exists(saved_frames_dir): shutil.rmtree(saved_frames_dir)
|
| 950 |
shutil.copytree(user_frames_dir, saved_frames_dir)
|
| 951 |
+
|
| 952 |
+
save_data = {
|
| 953 |
+
'code': save_code,
|
| 954 |
+
'originalSid': sid,
|
| 955 |
+
'pages': data.get('pages', []),
|
| 956 |
+
'savedAt': data.get('savedAt', time.strftime('%Y-%m-%d %H:%M:%S'))
|
| 957 |
+
}
|
| 958 |
+
with open(os.path.join(save_dir, 'comic_state.json'), 'w') as f: json.dump(save_data, f, indent=2)
|
| 959 |
return jsonify({'success': True, 'code': save_code})
|
| 960 |
+
except Exception as e:
|
| 961 |
+
traceback.print_exc()
|
| 962 |
+
return jsonify({'success': False, 'message': str(e)})
|
| 963 |
|
| 964 |
@app.route('/load_comic/<code>')
|
| 965 |
def load_comic(code):
|
| 966 |
code = code.upper()
|
| 967 |
save_dir = os.path.join(SAVED_COMICS_DIR, code)
|
| 968 |
+
state_file = os.path.join(save_dir, 'comic_state.json')
|
| 969 |
+
|
| 970 |
+
if not os.path.exists(state_file): return jsonify({'success': False, 'message': 'Code not found'})
|
| 971 |
+
|
| 972 |
try:
|
| 973 |
+
with open(state_file, 'r') as f: save_data = json.load(f)
|
| 974 |
+
original_sid = save_data.get('originalSid')
|
| 975 |
+
saved_frames_dir = os.path.join(save_dir, 'frames')
|
| 976 |
+
if original_sid and os.path.exists(saved_frames_dir):
|
| 977 |
+
user_frames_dir = os.path.join(BASE_USER_DIR, original_sid, 'frames')
|
| 978 |
+
os.makedirs(user_frames_dir, exist_ok=True)
|
| 979 |
+
for fname in os.listdir(saved_frames_dir):
|
| 980 |
+
src = os.path.join(saved_frames_dir, fname)
|
| 981 |
+
dst = os.path.join(user_frames_dir, fname)
|
| 982 |
+
if not os.path.exists(dst): shutil.copy2(src, dst)
|
| 983 |
+
return jsonify({ 'success': True, 'pages': save_data.get('pages', []), 'originalSid': original_sid, 'savedAt': save_data.get('savedAt') })
|
| 984 |
+
except Exception as e:
|
| 985 |
+
traceback.print_exc()
|
| 986 |
+
return jsonify({'success': False, 'message': str(e)})
|
| 987 |
|
| 988 |
if __name__ == '__main__':
|
| 989 |
try: gpu_warmup()
|