tester343 commited on
Commit
8b0a8d1
·
verified ·
1 Parent(s): 777f379

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. 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=20, type='speech'):
56
- # Apply classes
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
- all_subs = list(srt.parse(f.read()))
116
- except:
117
- all_subs = []
118
 
119
  valid_subs = [s for s in all_subs if s.content and s.content.strip()]
120
  if valid_subs:
@@ -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) - HD Output
150
  h, w = frame.shape[:2]
151
  sq_dim = max(h, w)
152
  square_img = np.zeros((sq_dim, sq_dim, 3), dtype=np.uint8)
153
  x_off = (sq_dim - w) // 2
154
  y_off = (sq_dim - h) // 2
155
  square_img[y_off:y_off+h, x_off:x_off+w] = frame
156
- # Resize to standard high res (1024x1024)
157
  square_img = cv2.resize(square_img, (1024, 1024))
158
 
159
  fname = f"frame_{count:04d}.png"
160
  p = os.path.join(frames_dir, fname)
161
  cv2.imwrite(p, square_img)
162
 
163
- frame_metadata[fname] = {'dialogue': moment['text'], 'time': mid}
 
 
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: b_type = 'narration'
177
- elif '!' in dialogue: b_type = 'reaction'
178
- elif '?' in dialogue: b_type = 'speech'
 
 
 
179
 
180
- # 🎯 1 BUBBLE PER PANEL PLACEMENT (for 800x800 page)
181
- # Quadrants: TL, TR, BL, BR
182
- # Panel size approx 400x400
183
  pos_idx = i % 4
184
- if pos_idx == 0: # TL
185
- bx, by = 150, 50
186
- elif pos_idx == 1: # TR
187
- bx, by = 550, 50
188
- elif pos_idx == 2: # BL
189
- bx, by = 150, 450
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: 5px solid #ffffff;
351
  }
352
 
353
- /* 🎯 5px White Borders between panels */
354
  .comic-grid {
355
- width: 100%; height: 100%; position: relative;
356
- background: #ffffff; /* White background makes white lines */
357
- --y: 50%; --t1: 100%; --t2: 100%; --b1: 100%; --b2: 100%;
358
- --gap: 5px; /* 5px gap size */
359
  }
360
 
361
  .panel { position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; background: #1a1a1a; cursor: grab; }
@@ -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
- ['nw', 'ne', 'sw', 'se'].forEach(dir => { const handle = document.createElement('div'); handle.className = `resize-handle ${dir}`; handle.onmousedown = (e) => startResize(e, dir); b.appendChild(handle); });
 
 
 
730
 
731
- b.onmousedown = (e) => { if(e.target.classList.contains('resize-handle')) return; e.stopPropagation(); selectBubble(b); dragType = 'bubble'; activeObj = b; dragStart = {x: e.clientX, y: e.clientY}; };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
  b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
733
  return b;
734
  }
@@ -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
- const rect = activeObj.parentElement.getBoundingClientRect();
756
- activeObj.style.left = (e.clientX - rect.left - (activeObj.offsetWidth/2)) + 'px';
757
- activeObj.style.top = (e.clientY - rect.top - (activeObj.offsetHeight/2)) + 'px';
 
 
 
 
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, type: b.dataset.type, colors: { fill: b.style.getPropertyValue('--bubble-fill'), text: b.style.getPropertyValue('--bubble-text') }, tailPos: b.style.getPropertyValue('--tail-pos'), classes: b.className });
872
  });
873
  const panels = [];
874
  grid.querySelectorAll('.panel').forEach(pan => {
@@ -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 == '': return jsonify({'success': False, 'message': 'No file uploaded'}), 400
 
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
- with open(os.path.join(save_dir, 'comic_state.json'), 'w') as f:
963
- json.dump({'originalSid': sid, 'pages': data['pages'], 'savedAt': time.time()}, f)
 
 
 
 
 
 
964
  return jsonify({'success': True, 'code': save_code})
965
- except Exception as e: return jsonify({'success': False, 'message': str(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
- if not os.path.exists(save_dir): return jsonify({'success': False, 'message': 'Code not found'})
 
 
 
972
  try:
973
- with open(os.path.join(save_dir, 'comic_state.json'), 'r') as f: data = json.load(f)
974
- orig_sid = data['originalSid']
975
- saved_frames = os.path.join(save_dir, 'frames')
976
- user_frames = os.path.join(BASE_USER_DIR, orig_sid, 'frames')
977
- os.makedirs(user_frames, exist_ok=True)
978
- for fn in os.listdir(saved_frames):
979
- shutil.copy2(os.path.join(saved_frames, fn), os.path.join(user_frames, fn))
980
- return jsonify({'success': True, 'originalSid': orig_sid, 'pages': data['pages']})
981
- except Exception as e: return jsonify({'success': False, 'message': str(e)})
 
 
 
 
 
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()