tester343 commited on
Commit
d87d09e
·
verified ·
1 Parent(s): e258333

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +421 -146
app_enhanced.py CHANGED
@@ -6,6 +6,8 @@ import shutil
6
  import json
7
  import traceback
8
  import logging
 
 
9
  from concurrent.futures import ThreadPoolExecutor
10
  from flask import Flask, render_template, request, jsonify, send_from_directory, send_file
11
 
@@ -41,7 +43,7 @@ try:
41
  from backend.simple_color_enhancer import SimpleColorEnhancer
42
  print("✅ SimpleColorEnhancer loaded.")
43
  except Exception as e:
44
- print(f"⚠️ Could not load SimpleColorEnhancer: {e}. This feature will be SKIPPED.")
45
  class SimpleColorEnhancer:
46
  def enhance_batch(self, *args, **kwargs): pass
47
  def enhance_single(self, *args, **kwargs): pass
@@ -50,16 +52,16 @@ try:
50
  from backend.quality_color_enhancer import QualityColorEnhancer
51
  print("✅ QualityColorEnhancer loaded.")
52
  except Exception as e:
53
- print(f"⚠️ Could not load QualityColorEnhancer: {e}. This feature will be SKIPPED.")
54
  class QualityColorEnhancer:
55
  def batch_enhance(self, *args, **kwargs): pass
56
  def enhance_single(self, *args, **kwargs): pass
57
 
58
  try:
59
  from backend.class_def import bubble, panel, Page
60
- print("✅ Core class definitions (bubble, panel, Page) loaded.")
61
  except Exception as e:
62
- print(f"⚠️ CRITICAL: Could not load core class definitions: {e}. Using fallback definitions.")
63
  def bubble(dialog="", bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, emotion='normal'):
64
  return {
65
  'dialog': dialog,
@@ -83,7 +85,7 @@ try:
83
  from backend.keyframes.keyframes_simple import generate_keyframes_simple
84
  print("✅ Core utility modules loaded.")
85
  except Exception as e:
86
- print(f"⚠️ Could not load a core utility module: {e}")
87
  def get_real_subtitles(v): pass
88
  def generate_keyframes_simple(*args, **kwargs): pass
89
  class DummyDetector:
@@ -97,6 +99,19 @@ except Exception as e:
97
  # --- FLASK APP SETUP ---
98
  app = Flask(__name__)
99
  BASE_USER_DIR = "userdata"
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  # --- FULL HTML INTERFACE ---
102
  INDEX_HTML = '''
@@ -114,21 +129,29 @@ INDEX_HTML = '''
114
  body { background-color: #fdf6e3; font-family: 'Lato', sans-serif; color: #3d3d3d; margin: 0; min-height: 100vh; }
115
 
116
  /* --- UPLOAD VIEW --- */
117
- #upload-container { display: flex; flex-direction:column; justify-content: center; align-items: center; min-height: 100vh; width: 100%; }
118
  .upload-box { max-width: 500px; width: 100%; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); text-align: center; }
119
 
120
  /* --- EDITOR VIEW --- */
121
  #editor-container { display: none; padding: 20px; width: 100%; box-sizing: border-box; padding-bottom: 100px; }
122
 
123
  /* --- BUTTONS & INPUTS --- */
124
- h1 { color: #2c3e50; margin-bottom: 30px; font-weight: 600; }
 
125
  .file-input { display: none; }
126
  .file-label { display: block; padding: 15px; background: #2c3e50; color: white; border-radius: 8px; cursor: pointer; font-weight: bold; margin-bottom: 10px; transition:0.2s; }
127
  .file-label:hover { background: #34495e; }
128
  .submit-btn { width: 100%; padding: 15px; background: #e67e22; color: white; border: none; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; transition: 0.2s; }
129
  .submit-btn:hover { background: #d35400; }
130
 
131
- .restore-btn { margin-top: 15px; background: #27ae60; color: white; padding: 10px; width: 100%; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; display: none; }
 
 
 
 
 
 
 
132
 
133
  .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; }
134
  @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
@@ -157,7 +180,7 @@ INDEX_HTML = '''
157
  .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
158
  .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; }
159
 
160
- /* --- EXACT "SHARK FIN" CSS WITH MASK --- */
161
  .speech-bubble.speech {
162
  --b: 3em;
163
  --h: 1.8em;
@@ -181,10 +204,7 @@ INDEX_HTML = '''
181
  mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
182
  }
183
 
184
- /* BOTTOM TAIL (Default) */
185
  .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))); }
186
-
187
- /* TOP TAIL */
188
  .speech-bubble.speech.tail-top {
189
  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);
190
  }
@@ -192,22 +212,17 @@ INDEX_HTML = '''
192
  bottom: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b)));
193
  transform: scaleY(-1);
194
  }
195
-
196
- /* LEFT TAIL */
197
  .speech-bubble.speech.tail-left { border-radius: var(--r); }
198
  .speech-bubble.speech.tail-left:before {
199
  right: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b)));
200
  transform: rotate(90deg); transform-origin: top right;
201
  }
202
-
203
- /* RIGHT TAIL */
204
  .speech-bubble.speech.tail-right { border-radius: var(--r); }
205
  .speech-bubble.speech.tail-right:before {
206
  left: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b)));
207
  transform: rotate(-90deg); transform-origin: top left;
208
  }
209
 
210
- /* THOUGHT BUBBLE */
211
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
212
  .speech-bubble.thought::after { display:none; }
213
  .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
@@ -218,24 +233,18 @@ INDEX_HTML = '''
218
  .speech-bubble.flipped-vertical .thought-dot-1 { bottom: auto; top: -20px; }
219
  .speech-bubble.flipped-vertical .thought-dot-2 { bottom: auto; top: -32px; }
220
 
221
- /* REACTION BUBBLE */
222
  .speech-bubble.reaction {
223
  background: #FFD700; border: 3px solid #E53935; color: #D32F2F;
224
  font-weight: 900; text-transform: uppercase; width: 180px;
225
  clip-path: polygon(0% 25%, 17% 21%, 17% 0%, 31% 16%, 50% 4%, 69% 16%, 83% 0%, 83% 21%, 100% 25%, 85% 45%, 95% 62%, 82% 79%, 100% 97%, 79% 89%, 60% 98%, 46% 82%, 27% 95%, 15% 78%, 5% 62%, 15% 45%);
226
  }
227
-
228
- /* NARRATION BOX */
229
  .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
230
-
231
- /* IDEA BUBBLE */
232
  .speech-bubble.idea {
233
  background: linear-gradient(180deg,#FFFDD0 0%, #FFF8B5 100%);
234
  border: 2px solid #FFA500; color: #6a4b00;
235
  border-radius: 40% 60% 40% 60% / 60% 40% 60% 40%;
236
  }
237
 
238
- /* RESIZE HANDLES */
239
  .resize-handle { position: absolute; width: 10px; height: 10px; background: #2196F3; border: 1px solid white; border-radius: 50%; display: none; z-index: 11; }
240
  .speech-bubble.selected .resize-handle { display: block; }
241
  .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
@@ -245,9 +254,10 @@ INDEX_HTML = '''
245
 
246
  /* FLOATING TOOLBAR */
247
  .edit-controls {
248
- position: fixed; bottom: 20px; right: 20px; width: 250px;
249
  background: rgba(44, 62, 80, 0.95); color: white; padding: 15px; border-radius: 8px;
250
  box-shadow: 0 5px 15px rgba(0,0,0,0.3); z-index: 900; font-size: 13px;
 
251
  }
252
  .edit-controls h4 { margin: 0 0 10px 0; color: #4ECDC4; text-align: center; }
253
  .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
@@ -266,6 +276,49 @@ INDEX_HTML = '''
266
  .reset-btn { background: #e74c3c; color: white; }
267
  .secondary-btn { background: #f39c12; color: white; }
268
  .export-btn { background: #2196F3; color: white; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  </style>
270
  </head>
271
  <body>
@@ -273,11 +326,27 @@ INDEX_HTML = '''
273
  <div id="upload-container">
274
  <div class="upload-box">
275
  <h1>🎬 Comic Generator</h1>
 
 
276
  <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fn').innerText=this.files[0].name">
277
- <label for="file-upload" class="file-label">Choose Video</label>
278
  <span id="fn" style="margin-bottom:10px; display:block; color:#666;">No file selected</span>
279
- <button class="submit-btn" onclick="upload()">Generate Comic</button>
280
- <button id="restore-btn" class="restore-btn" onclick="restoreSession()">📂 Restore Unsaved Session</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
  <div class="loading-view" id="loading-view" style="display:none; margin-top:20px;">
283
  <div class="loader" style="margin:0 auto;"></div>
@@ -295,9 +364,20 @@ INDEX_HTML = '''
295
  <div class="edit-controls">
296
  <h4>✏️ Interactive Editor</h4>
297
 
 
 
 
 
 
 
 
 
 
 
 
298
  <!-- Bubble Controls -->
299
  <div class="control-group">
300
- <label>Bubble Tools:</label>
301
  <select id="bubble-type-select" onchange="changeBubbleType(this.value)" disabled>
302
  <option value="speech">Speech</option>
303
  <option value="thought">Thought</option>
@@ -327,7 +407,7 @@ INDEX_HTML = '''
327
 
328
  <!-- Tail Controls -->
329
  <div class="control-group" id="tail-controls" style="display:none;">
330
- <label>Tail Adjustment:</label>
331
  <button onclick="rotateTail()" class="secondary-btn">🔄 Rotate Side</button>
332
  <div class="slider-container">
333
  <label>Pos:</label>
@@ -337,7 +417,7 @@ INDEX_HTML = '''
337
 
338
  <!-- Panel Controls -->
339
  <div class="control-group">
340
- <label>Panel Tools:</label>
341
  <button onclick="replacePanelImage()" class="action-btn">🖼️ Replace Image</button>
342
  <div class="button-grid">
343
  <button onclick="adjustFrame('backward')" class="secondary-btn">⬅️ Prev</button>
@@ -351,7 +431,7 @@ INDEX_HTML = '''
351
 
352
  <!-- Zoom Controls -->
353
  <div class="control-group">
354
- <label>Zoom & Pan:</label>
355
  <div class="button-grid">
356
  <button onclick="resetPanelTransform()" class="secondary-btn">Reset</button>
357
  <input type="range" id="zoom-slider" min="100" max="300" value="100" step="5" disabled oninput="handleZoom(this)">
@@ -360,13 +440,24 @@ INDEX_HTML = '''
360
 
361
  <!-- Actions -->
362
  <div class="control-group">
363
- <button onclick="saveLocal()" class="secondary-btn">💾 Save Draft</button>
364
- <button onclick="exportComic()" class="export-btn">📥 Export Pages</button>
365
- <button onclick="clearAndReset()" class="reset-btn" style="margin-top:10px;">🔄 Clear & Reset</button>
366
  </div>
367
  </div>
368
  </div>
369
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  <script>
371
  // --- SESSION LOGIC ---
372
  function genUUID(){ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,c=>{var r=Math.random()*16|0,v=c=='x'?r:(r&0x3|0x8);return v.toString(16);}); }
@@ -374,10 +465,7 @@ INDEX_HTML = '''
374
  localStorage.setItem('comic_sid', sid);
375
  console.log("Session ID:", sid);
376
 
377
- if(localStorage.getItem('comic_autosave_'+sid)) {
378
- document.getElementById('restore-btn').style.display = 'block';
379
- }
380
-
381
  let interval, selectedBubble = null, selectedPanel = null;
382
  let isDragging = false, isResizing = false, isPanning = false;
383
  let startX, startY, initX, initY, initW, initH;
@@ -385,18 +473,175 @@ INDEX_HTML = '''
385
  let resizeHandle = '', originalWidth, originalHeight, originalX, originalY, originalMouseX, originalMouseY;
386
  let currentlyEditing = null;
387
 
388
- // --- RESTORE FUNCTION ---
389
- function restoreSession() {
390
- const savedData = localStorage.getItem('comic_autosave_'+sid);
391
- if(!savedData) return alert("No saved session found.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  try {
393
  const state = JSON.parse(savedData);
394
- renderFromState(state);
 
 
 
 
 
395
  document.getElementById('upload-container').style.display = 'none';
396
  document.getElementById('editor-container').style.display = 'block';
397
- } catch(e) { console.error(e); alert("Failed to restore session."); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  }
399
 
 
 
 
 
 
 
 
 
 
 
 
400
  function renderFromState(pagesData) {
401
  const con = document.getElementById('comic-container');
402
  con.innerHTML = '';
@@ -427,7 +672,6 @@ INDEX_HTML = '''
427
  img.onmousedown = (e) => startPan(e, img);
428
  pDiv.appendChild(img);
429
 
430
- // Restore bubbles
431
  (pan.bubbles || []).forEach(bData => {
432
  const b = createBubbleHTML(bData);
433
  pDiv.appendChild(b);
@@ -443,7 +687,13 @@ INDEX_HTML = '''
443
  // --- UPLOAD LOGIC ---
444
  async function upload() {
445
  const f = document.getElementById('file-upload').files[0];
446
- if(!f) return alert("Select file");
 
 
 
 
 
 
447
  document.querySelector('.upload-box').style.display='none';
448
  document.getElementById('loading-view').style.display='flex';
449
  const fd = new FormData(); fd.append('file', f);
@@ -486,16 +736,26 @@ INDEX_HTML = '''
486
  }))
487
  }));
488
  renderFromState(cleanData);
489
- saveLocal();
490
  });
491
  }
492
 
 
 
 
 
 
 
 
 
 
 
 
493
  // --- BUBBLE CREATION ---
494
  function createBubbleHTML(data) {
495
  const b = document.createElement('div');
496
  b.dataset.type = data.type || 'speech';
497
 
498
- // Apply type-specific classes
499
  applyBubbleType(b, data.type || 'speech', data.classes);
500
 
501
  b.style.left = data.left;
@@ -515,7 +775,6 @@ INDEX_HTML = '''
515
  textSpan.textContent = data.text || '';
516
  b.appendChild(textSpan);
517
 
518
- // Add resize handles
519
  ['nw', 'ne', 'sw', 'se'].forEach(dir => {
520
  const handle = document.createElement('div');
521
  handle.className = `resize-handle ${dir}`;
@@ -523,7 +782,6 @@ INDEX_HTML = '''
523
  b.appendChild(handle);
524
  });
525
 
526
- // Drag
527
  b.onmousedown = (e) => {
528
  if(e.target.classList.contains('resize-handle')) return;
529
  e.stopPropagation(); selectBubble(b);
@@ -531,19 +789,13 @@ INDEX_HTML = '''
531
  initX = b.offsetLeft; initY = b.offsetTop;
532
  };
533
 
534
- // Edit Text
535
- b.ondblclick = (e) => {
536
- e.stopPropagation();
537
- editBubbleText(b);
538
- };
539
-
540
  b.onclick = (e) => { e.stopPropagation(); selectBubble(b); };
541
 
542
  return b;
543
  }
544
 
545
  function applyBubbleType(bubble, type, existingClasses) {
546
- // Remove thought dots if any
547
  bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
548
 
549
  let baseClasses = 'speech-bubble ' + type;
@@ -576,7 +828,7 @@ INDEX_HTML = '''
576
  textarea.remove();
577
  textSpan.style.display = '';
578
  currentlyEditing = null;
579
- saveLocal();
580
  };
581
  textarea.addEventListener('blur', finishEditing, { once: true });
582
  textarea.addEventListener('keydown', e => {
@@ -602,7 +854,7 @@ INDEX_HTML = '''
602
  });
603
 
604
  document.addEventListener('mouseup', () => {
605
- if(isDragging || isResizing || isPanning) saveLocal();
606
  isDragging = false; isResizing = false; isPanning = false;
607
  });
608
 
@@ -645,7 +897,6 @@ INDEX_HTML = '''
645
  selectedBubble = el;
646
  el.classList.add('selected');
647
 
648
- // Sync Controls
649
  const bubbleType = el.dataset.type || 'speech';
650
  document.getElementById('tail-controls').style.display = (bubbleType === 'speech' || bubbleType === 'thought') ? 'block' : 'none';
651
 
@@ -653,14 +904,12 @@ INDEX_HTML = '''
653
  document.getElementById('zoom-slider').disabled = true;
654
  document.getElementById('bubble-type-select').value = bubbleType;
655
 
656
- // Sync colors
657
  const styles = window.getComputedStyle(el);
658
  const textColor = styles.getPropertyValue('--bubble-text-color').trim() || rgbToHex(styles.color);
659
  const fillColor = styles.getPropertyValue('--bubble-fill-color').trim() || rgbToHex(styles.backgroundColor);
660
  document.getElementById('bubble-text-color').value = textColor;
661
  document.getElementById('bubble-fill-color').value = fillColor;
662
 
663
- // Sync tail slider
664
  const tailPos = styles.getPropertyValue('--tail-pos').trim();
665
  document.getElementById('tail-slider').value = tailPos ? parseInt(tailPos) : 50;
666
  }
@@ -689,7 +938,7 @@ INDEX_HTML = '''
689
  });
690
  selectedPanel.appendChild(b);
691
  selectBubble(b);
692
- saveLocal();
693
  }
694
 
695
  function deleteBubble() {
@@ -697,7 +946,7 @@ INDEX_HTML = '''
697
  if(confirm("Delete this bubble?")) {
698
  selectedBubble.remove();
699
  selectedBubble = null;
700
- saveLocal();
701
  }
702
  }
703
 
@@ -706,13 +955,13 @@ INDEX_HTML = '''
706
  applyBubbleType(selectedBubble, type);
707
  selectedBubble.classList.add('selected');
708
  document.getElementById('tail-controls').style.display = (type === 'speech' || type === 'thought') ? 'block' : 'none';
709
- saveLocal();
710
  }
711
 
712
  function changeFont(font) {
713
  if(!selectedBubble) return;
714
  selectedBubble.style.fontFamily = font;
715
- saveLocal();
716
  }
717
 
718
  function rotateTail() {
@@ -733,27 +982,26 @@ INDEX_HTML = '''
733
  else if (isFlippedH && isFlippedV) selectedBubble.classList.remove('flipped');
734
  else selectedBubble.classList.remove('flipped-vertical');
735
  }
736
- saveLocal();
737
  }
738
 
739
  function slideTail(v) {
740
  if(selectedBubble) {
741
  selectedBubble.style.setProperty('--tail-pos', v+'%');
742
- saveLocal();
743
  }
744
  }
745
 
746
- // Color handlers
747
  document.getElementById('bubble-text-color').addEventListener('input', (e) => {
748
  if(selectedBubble) {
749
  selectedBubble.style.setProperty('--bubble-text-color', e.target.value);
750
- saveLocal();
751
  }
752
  });
753
  document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
754
  if(selectedBubble) {
755
  selectedBubble.style.setProperty('--bubble-fill-color', e.target.value);
756
- saveLocal();
757
  }
758
  });
759
 
@@ -763,7 +1011,7 @@ INDEX_HTML = '''
763
  const img = selectedPanel.querySelector('img');
764
  img.dataset.zoom = el.value;
765
  updateImageTransform(img);
766
- saveLocal();
767
  }
768
 
769
  function startPan(e, img) {
@@ -802,7 +1050,7 @@ INDEX_HTML = '''
802
  img.dataset.translateY = 0;
803
  document.getElementById('zoom-slider').value = 100;
804
  updateImageTransform(img);
805
- saveLocal();
806
  }
807
 
808
  // --- BACKEND ACTIONS ---
@@ -819,7 +1067,7 @@ INDEX_HTML = '''
819
  if(d.success) {
820
  img.src = `/frames/${d.new_filename}?sid=${sid}`;
821
  resetPanelTransform();
822
- saveLocal();
823
  } else {
824
  alert('Error: ' + d.error);
825
  }
@@ -845,7 +1093,7 @@ INDEX_HTML = '''
845
  alert('Error: ' + d.message);
846
  }
847
  img.style.opacity = '1';
848
- saveLocal();
849
  }
850
 
851
  async function gotoTimestamp() {
@@ -878,7 +1126,7 @@ INDEX_HTML = '''
878
  alert('Error: ' + d.message);
879
  }
880
  img.style.opacity = '1';
881
- saveLocal();
882
  }
883
 
884
  // --- EXPORT ---
@@ -886,7 +1134,6 @@ INDEX_HTML = '''
886
  const pgs = document.querySelectorAll('.comic-page');
887
  if(pgs.length === 0) return alert("No pages found");
888
 
889
- // Freeze dimensions
890
  const bubbles = document.querySelectorAll('.speech-bubble');
891
  bubbles.forEach(b => {
892
  const rect = b.getBoundingClientRect();
@@ -908,59 +1155,12 @@ INDEX_HTML = '''
908
  }
909
  }
910
 
911
- // Unfreeze
912
  bubbles.forEach(b => {
913
  b.style.width = '';
914
  b.style.height = '';
915
  });
916
  }
917
 
918
- // --- SAVE & RESET ---
919
- function saveLocal() {
920
- const pages = [];
921
- document.querySelectorAll('.comic-page').forEach(p => {
922
- const panels = [];
923
- p.querySelectorAll('.panel').forEach(pan => {
924
- const img = pan.querySelector('img');
925
- const bubbles = [];
926
- pan.querySelectorAll('.speech-bubble').forEach(b => {
927
- const textEl = b.querySelector('.bubble-text');
928
- bubbles.push({
929
- text: textEl ? textEl.textContent : '',
930
- left: b.style.left,
931
- top: b.style.top,
932
- width: b.style.width,
933
- height: b.style.height,
934
- classes: b.className,
935
- type: b.dataset.type,
936
- font: b.style.fontFamily,
937
- tailPos: b.style.getPropertyValue('--tail-pos'),
938
- colors: {
939
- fill: b.style.getPropertyValue('--bubble-fill-color'),
940
- text: b.style.getPropertyValue('--bubble-text-color')
941
- }
942
- });
943
- });
944
- panels.push({
945
- src: img.src,
946
- zoom: img.dataset.zoom,
947
- tx: img.dataset.translateX,
948
- ty: img.dataset.translateY,
949
- bubbles: bubbles
950
- });
951
- });
952
- pages.push({ panels: panels });
953
- });
954
- localStorage.setItem('comic_autosave_'+sid, JSON.stringify(pages));
955
- }
956
-
957
- function clearAndReset() {
958
- if(confirm("Clear all edits and reset?")) {
959
- localStorage.removeItem('comic_autosave_'+sid);
960
- location.reload();
961
- }
962
- }
963
-
964
  // Helper
965
  function rgbToHex(rgb) {
966
  if (!rgb || !rgb.startsWith('rgb')) return '#ffffff';
@@ -991,7 +1191,6 @@ class EnhancedComicGenerator:
991
  os.makedirs(self.output_dir, exist_ok=True)
992
  self.video_fps = None
993
  self.frame_metadata = {}
994
- self.apply_comic_style = False
995
 
996
  def update_status(self, message, progress):
997
  try:
@@ -1015,14 +1214,12 @@ class EnhancedComicGenerator:
1015
  os.remove(os.path.join(self.output_dir, f))
1016
  except:
1017
  pass
1018
- # Also clean temp srt
1019
  user_srt = os.path.join(self.user_dir, 'subs.srt')
1020
  if os.path.exists(user_srt):
1021
  os.remove(user_srt)
1022
  print("✅ Cleanup complete.")
1023
 
1024
  def generate_keyframes_from_moments(self, key_moments, max_frames=48):
1025
- """Extract keyframes based on subtitle moments"""
1026
  try:
1027
  cap = cv2.VideoCapture(self.video_path)
1028
  if not cap.isOpened():
@@ -1073,7 +1270,6 @@ class EnhancedComicGenerator:
1073
  return False
1074
 
1075
  def _enhance_all_images(self, single_image_path=None):
1076
- """Apply simple color enhancement"""
1077
  try:
1078
  enhancer = SimpleColorEnhancer()
1079
  if single_image_path:
@@ -1088,7 +1284,6 @@ class EnhancedComicGenerator:
1088
  print(f"⚠️ Simple enhancement failed: {e}")
1089
 
1090
  def _enhance_quality_colors(self, single_image_path=None):
1091
- """Apply quality color enhancement"""
1092
  try:
1093
  enhancer = QualityColorEnhancer()
1094
  if single_image_path:
@@ -1103,7 +1298,6 @@ class EnhancedComicGenerator:
1103
  print(f"⚠️ Quality enhancement failed: {e}")
1104
 
1105
  def _process_bubble_for_frame(self, frame_file):
1106
- """Process AI bubble placement for a single frame"""
1107
  frame_path = os.path.join(self.frames_dir, frame_file)
1108
  meta = self.frame_metadata.get(frame_file, {})
1109
  dialogue = meta.get('dialogue', '') if isinstance(meta, dict) else ''
@@ -1135,7 +1329,6 @@ class EnhancedComicGenerator:
1135
  )
1136
 
1137
  def _create_ai_bubbles_from_moments(self):
1138
- """Create AI-placed bubbles for all frames"""
1139
  frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
1140
 
1141
  if not os.path.exists(self.metadata_path):
@@ -1150,13 +1343,11 @@ class EnhancedComicGenerator:
1150
  return bubbles
1151
 
1152
  def _generate_pages(self, bubbles_list):
1153
- """Generate comic pages from frames and bubbles"""
1154
  try:
1155
  from backend.fixed_12_pages_800x1080 import generate_12_pages_800x1080
1156
  frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
1157
  return generate_12_pages_800x1080(frame_files, bubbles_list)
1158
  except ImportError:
1159
- # Fallback: simple 4-panel pages
1160
  pages = []
1161
  frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
1162
  num_pages = (len(frame_files) + 3) // 4
@@ -1171,7 +1362,6 @@ class EnhancedComicGenerator:
1171
  return pages
1172
 
1173
  def generate_comic(self):
1174
- """Main comic generation pipeline"""
1175
  start_time = time.time()
1176
  try:
1177
  if cv2 is None:
@@ -1247,7 +1437,6 @@ class EnhancedComicGenerator:
1247
  return False
1248
 
1249
  def _save_results(self, pages):
1250
- """Save comic pages data to JSON"""
1251
  try:
1252
  pages_data = []
1253
  for page in pages:
@@ -1263,7 +1452,6 @@ class EnhancedComicGenerator:
1263
  print(f"❌ Save results failed: {e}")
1264
 
1265
  def regenerate_frame(self, fname, direction):
1266
- """Regenerate a frame by stepping forward/backward"""
1267
  try:
1268
  if not os.path.exists(self.metadata_path):
1269
  return {"success": False, "message": "Frame metadata missing."}
@@ -1274,24 +1462,20 @@ class EnhancedComicGenerator:
1274
  if fname not in meta:
1275
  return {"success": False, "message": "Panel not linked to video."}
1276
 
1277
- # Get current time
1278
  current_data = meta[fname]
1279
  if isinstance(current_data, dict):
1280
  curr_time = current_data['time']
1281
  else:
1282
  curr_time = current_data
1283
 
1284
- # Get video FPS
1285
  if not self.video_fps:
1286
  cap = cv2.VideoCapture(self.video_path)
1287
  self.video_fps = cap.get(cv2.CAP_PROP_FPS) or 25
1288
  cap.release()
1289
 
1290
- # Calculate new time
1291
  offset = (1.0 / self.video_fps) * (1 if direction == 'forward' else -1)
1292
  new_time = max(0, curr_time + offset)
1293
 
1294
- # Extract new frame
1295
  cap = cv2.VideoCapture(self.video_path)
1296
  cap.set(cv2.CAP_PROP_POS_MSEC, new_time * 1000)
1297
  ret, frame = cap.read()
@@ -1301,12 +1485,10 @@ class EnhancedComicGenerator:
1301
  frame_path = os.path.join(self.frames_dir, fname)
1302
  cv2.imwrite(frame_path, frame)
1303
 
1304
- # Apply enhancements
1305
  print(f"🎨 Applying enhancements to new frame: {fname}")
1306
  self._enhance_all_images(single_image_path=frame_path)
1307
  self._enhance_quality_colors(single_image_path=frame_path)
1308
 
1309
- # Update metadata
1310
  if isinstance(meta[fname], dict):
1311
  meta[fname]['time'] = new_time
1312
  else:
@@ -1325,7 +1507,6 @@ class EnhancedComicGenerator:
1325
  return {"success": False, "message": str(e)}
1326
 
1327
  def get_frame_at_timestamp(self, fname, ts):
1328
- """Get a specific frame at a given timestamp"""
1329
  try:
1330
  cap = cv2.VideoCapture(self.video_path)
1331
  if not cap.isOpened():
@@ -1346,12 +1527,10 @@ class EnhancedComicGenerator:
1346
  frame_path = os.path.join(self.frames_dir, fname)
1347
  cv2.imwrite(frame_path, frame)
1348
 
1349
- # Apply enhancements
1350
  print(f"🎨 Applying enhancements to frame from timestamp: {fname}")
1351
  self._enhance_all_images(single_image_path=frame_path)
1352
  self._enhance_quality_colors(single_image_path=frame_path)
1353
 
1354
- # Update metadata
1355
  if os.path.exists(self.metadata_path):
1356
  with open(self.metadata_path, 'r') as f:
1357
  meta = json.load(f)
@@ -1452,13 +1631,109 @@ def rep_panel():
1452
  return jsonify({'success': False, 'error': 'No image provided.'})
1453
 
1454
  f = request.files['image']
 
 
1455
  fname = f"replaced_{int(time.time() * 1000)}.png"
1456
- f.save(os.path.join(BASE_USER_DIR, sid, 'frames', fname))
1457
  return jsonify({'success': True, 'new_filename': fname})
1458
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1459
 
1460
  if __name__ == '__main__':
1461
  os.makedirs(BASE_USER_DIR, exist_ok=True)
 
1462
  port = int(os.getenv("PORT", 7860))
1463
  print(f"🚀 Starting Enhanced Comic Generator on host 0.0.0.0, port {port}")
 
 
1464
  app.run(host='0.0.0.0', port=port, debug=False)
 
6
  import json
7
  import traceback
8
  import logging
9
+ import string
10
+ import random
11
  from concurrent.futures import ThreadPoolExecutor
12
  from flask import Flask, render_template, request, jsonify, send_from_directory, send_file
13
 
 
43
  from backend.simple_color_enhancer import SimpleColorEnhancer
44
  print("✅ SimpleColorEnhancer loaded.")
45
  except Exception as e:
46
+ print(f"⚠️ Could not load SimpleColorEnhancer: {e}.")
47
  class SimpleColorEnhancer:
48
  def enhance_batch(self, *args, **kwargs): pass
49
  def enhance_single(self, *args, **kwargs): pass
 
52
  from backend.quality_color_enhancer import QualityColorEnhancer
53
  print("✅ QualityColorEnhancer loaded.")
54
  except Exception as e:
55
+ print(f"⚠️ Could not load QualityColorEnhancer: {e}.")
56
  class QualityColorEnhancer:
57
  def batch_enhance(self, *args, **kwargs): pass
58
  def enhance_single(self, *args, **kwargs): pass
59
 
60
  try:
61
  from backend.class_def import bubble, panel, Page
62
+ print("✅ Core class definitions loaded.")
63
  except Exception as e:
64
+ print(f"⚠️ Using fallback class definitions.")
65
  def bubble(dialog="", bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, emotion='normal'):
66
  return {
67
  'dialog': dialog,
 
85
  from backend.keyframes.keyframes_simple import generate_keyframes_simple
86
  print("✅ Core utility modules loaded.")
87
  except Exception as e:
88
+ print(f"⚠️ Could not load utility modules: {e}")
89
  def get_real_subtitles(v): pass
90
  def generate_keyframes_simple(*args, **kwargs): pass
91
  class DummyDetector:
 
99
  # --- FLASK APP SETUP ---
100
  app = Flask(__name__)
101
  BASE_USER_DIR = "userdata"
102
+ SAVED_COMICS_DIR = "saved_comics"
103
+
104
+ # Create directories
105
+ os.makedirs(BASE_USER_DIR, exist_ok=True)
106
+ os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
107
+
108
+ def generate_save_code(length=8):
109
+ """Generate a unique save code"""
110
+ chars = string.ascii_uppercase + string.digits
111
+ while True:
112
+ code = ''.join(random.choices(chars, k=length))
113
+ if not os.path.exists(os.path.join(SAVED_COMICS_DIR, code)):
114
+ return code
115
 
116
  # --- FULL HTML INTERFACE ---
117
  INDEX_HTML = '''
 
129
  body { background-color: #fdf6e3; font-family: 'Lato', sans-serif; color: #3d3d3d; margin: 0; min-height: 100vh; }
130
 
131
  /* --- UPLOAD VIEW --- */
132
+ #upload-container { display: flex; flex-direction:column; justify-content: center; align-items: center; min-height: 100vh; width: 100%; padding: 20px; }
133
  .upload-box { max-width: 500px; width: 100%; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12); text-align: center; }
134
 
135
  /* --- EDITOR VIEW --- */
136
  #editor-container { display: none; padding: 20px; width: 100%; box-sizing: border-box; padding-bottom: 100px; }
137
 
138
  /* --- BUTTONS & INPUTS --- */
139
+ h1 { color: #2c3e50; margin-bottom: 20px; font-weight: 600; }
140
+ h3 { color: #34495e; margin: 20px 0 10px 0; font-size: 16px; }
141
  .file-input { display: none; }
142
  .file-label { display: block; padding: 15px; background: #2c3e50; color: white; border-radius: 8px; cursor: pointer; font-weight: bold; margin-bottom: 10px; transition:0.2s; }
143
  .file-label:hover { background: #34495e; }
144
  .submit-btn { width: 100%; padding: 15px; background: #e67e22; color: white; border: none; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; transition: 0.2s; }
145
  .submit-btn:hover { background: #d35400; }
146
 
147
+ .restore-btn { margin-top: 10px; background: #27ae60; color: white; padding: 12px; width: 100%; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
148
+ .restore-btn:hover { background: #219a52; }
149
+
150
+ .load-section { margin-top: 30px; padding-top: 20px; border-top: 2px solid #eee; }
151
+ .load-input-group { display: flex; gap: 10px; margin-top: 10px; }
152
+ .load-input-group input { flex: 1; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; text-transform: uppercase; letter-spacing: 2px; text-align: center; }
153
+ .load-input-group button { padding: 12px 20px; background: #3498db; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
154
+ .load-input-group button:hover { background: #2980b9; }
155
 
156
  .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; }
157
  @keyframes ball { 0%{background-position:0 50%} 100%{background-position:100px 50%} }
 
180
  .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 100; }
181
  .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; }
182
 
183
+ /* --- SPEECH BUBBLE CSS --- */
184
  .speech-bubble.speech {
185
  --b: 3em;
186
  --h: 1.8em;
 
204
  mask: radial-gradient(calc(var(--t)*100%) 105% at 100% 0,#0000 99%,#000 101%);
205
  }
206
 
 
207
  .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))); }
 
 
208
  .speech-bubble.speech.tail-top {
209
  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);
210
  }
 
212
  bottom: 100%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b)));
213
  transform: scaleY(-1);
214
  }
 
 
215
  .speech-bubble.speech.tail-left { border-radius: var(--r); }
216
  .speech-bubble.speech.tail-left:before {
217
  right: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b)));
218
  transform: rotate(90deg); transform-origin: top right;
219
  }
 
 
220
  .speech-bubble.speech.tail-right { border-radius: var(--r); }
221
  .speech-bubble.speech.tail-right:before {
222
  left: 100%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b)));
223
  transform: rotate(-90deg); transform-origin: top left;
224
  }
225
 
 
226
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
227
  .speech-bubble.thought::after { display:none; }
228
  .thought-dot { position: absolute; background-color: white; border: 2px solid #555; border-radius: 50%; z-index: -1; }
 
233
  .speech-bubble.flipped-vertical .thought-dot-1 { bottom: auto; top: -20px; }
234
  .speech-bubble.flipped-vertical .thought-dot-2 { bottom: auto; top: -32px; }
235
 
 
236
  .speech-bubble.reaction {
237
  background: #FFD700; border: 3px solid #E53935; color: #D32F2F;
238
  font-weight: 900; text-transform: uppercase; width: 180px;
239
  clip-path: polygon(0% 25%, 17% 21%, 17% 0%, 31% 16%, 50% 4%, 69% 16%, 83% 0%, 83% 21%, 100% 25%, 85% 45%, 95% 62%, 82% 79%, 100% 97%, 79% 89%, 60% 98%, 46% 82%, 27% 95%, 15% 78%, 5% 62%, 15% 45%);
240
  }
 
 
241
  .speech-bubble.narration { background: #FAFAFA; border: 2px solid #BDBDBD; color: #424242; border-radius: 3px; }
 
 
242
  .speech-bubble.idea {
243
  background: linear-gradient(180deg,#FFFDD0 0%, #FFF8B5 100%);
244
  border: 2px solid #FFA500; color: #6a4b00;
245
  border-radius: 40% 60% 40% 60% / 60% 40% 60% 40%;
246
  }
247
 
 
248
  .resize-handle { position: absolute; width: 10px; height: 10px; background: #2196F3; border: 1px solid white; border-radius: 50%; display: none; z-index: 11; }
249
  .speech-bubble.selected .resize-handle { display: block; }
250
  .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
 
254
 
255
  /* FLOATING TOOLBAR */
256
  .edit-controls {
257
+ position: fixed; bottom: 20px; right: 20px; width: 260px;
258
  background: rgba(44, 62, 80, 0.95); color: white; padding: 15px; border-radius: 8px;
259
  box-shadow: 0 5px 15px rgba(0,0,0,0.3); z-index: 900; font-size: 13px;
260
+ max-height: 90vh; overflow-y: auto;
261
  }
262
  .edit-controls h4 { margin: 0 0 10px 0; color: #4ECDC4; text-align: center; }
263
  .control-group { margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; }
 
276
  .reset-btn { background: #e74c3c; color: white; }
277
  .secondary-btn { background: #f39c12; color: white; }
278
  .export-btn { background: #2196F3; color: white; }
279
+ .save-btn { background: #9b59b6; color: white; }
280
+
281
+ /* SAVE CODE DISPLAY */
282
+ .save-code-display {
283
+ background: #2ecc71; color: white; padding: 15px; border-radius: 8px;
284
+ text-align: center; margin-top: 10px; display: none;
285
+ }
286
+ .save-code-display .code {
287
+ font-size: 24px; font-weight: bold; letter-spacing: 3px;
288
+ background: white; color: #2ecc71; padding: 10px 20px;
289
+ border-radius: 4px; display: inline-block; margin: 10px 0;
290
+ font-family: monospace;
291
+ }
292
+ .save-code-display button {
293
+ background: white; color: #2ecc71; border: none;
294
+ padding: 8px 15px; border-radius: 4px; cursor: pointer;
295
+ font-weight: bold; margin-top: 5px;
296
+ }
297
+
298
+ /* MODAL */
299
+ .modal-overlay {
300
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
301
+ background: rgba(0,0,0,0.7); display: none; justify-content: center;
302
+ align-items: center; z-index: 9999;
303
+ }
304
+ .modal-content {
305
+ background: white; padding: 30px; border-radius: 12px;
306
+ max-width: 400px; width: 90%; text-align: center;
307
+ }
308
+ .modal-content h2 { color: #2ecc71; margin-bottom: 20px; }
309
+ .modal-content .code {
310
+ font-size: 32px; font-weight: bold; letter-spacing: 4px;
311
+ background: #f0f0f0; padding: 15px 25px; border-radius: 8px;
312
+ display: inline-block; margin: 15px 0; font-family: monospace;
313
+ user-select: all;
314
+ }
315
+ .modal-content p { color: #666; margin: 10px 0; }
316
+ .modal-content button {
317
+ background: #3498db; color: white; border: none;
318
+ padding: 12px 30px; border-radius: 8px; cursor: pointer;
319
+ font-weight: bold; font-size: 14px; margin: 5px;
320
+ }
321
+ .modal-content button.close-btn { background: #95a5a6; }
322
  </style>
323
  </head>
324
  <body>
 
326
  <div id="upload-container">
327
  <div class="upload-box">
328
  <h1>🎬 Comic Generator</h1>
329
+
330
+ <!-- New Comic Section -->
331
  <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fn').innerText=this.files[0].name">
332
+ <label for="file-upload" class="file-label">📁 Choose Video File</label>
333
  <span id="fn" style="margin-bottom:10px; display:block; color:#666;">No file selected</span>
334
+ <button class="submit-btn" onclick="upload()">🚀 Generate Comic</button>
335
+
336
+ <!-- Restore Draft Section -->
337
+ <button id="restore-draft-btn" class="restore-btn" style="display:none; margin-top:15px;" onclick="restoreDraft()">
338
+ 📂 Restore Unsaved Draft
339
+ </button>
340
+
341
+ <!-- Load Saved Comic Section -->
342
+ <div class="load-section">
343
+ <h3>📥 Load Saved Comic</h3>
344
+ <p style="font-size:12px; color:#888; margin-bottom:10px;">Enter your save code to continue editing</p>
345
+ <div class="load-input-group">
346
+ <input type="text" id="load-code-input" placeholder="SAVE CODE" maxlength="8" style="text-transform:uppercase;">
347
+ <button onclick="loadSavedComic()">Load</button>
348
+ </div>
349
+ </div>
350
 
351
  <div class="loading-view" id="loading-view" style="display:none; margin-top:20px;">
352
  <div class="loader" style="margin:0 auto;"></div>
 
364
  <div class="edit-controls">
365
  <h4>✏️ Interactive Editor</h4>
366
 
367
+ <!-- Save/Load Section -->
368
+ <div class="control-group">
369
+ <label>💾 Save & Load:</label>
370
+ <button onclick="saveComic()" class="save-btn">💾 Save Comic (Get Code)</button>
371
+ <div id="current-save-code" style="display:none; margin-top:8px; padding:8px; background:#2ecc71; border-radius:4px; text-align:center;">
372
+ <span style="font-size:11px;">Current Save Code:</span><br>
373
+ <span id="display-save-code" style="font-size:18px; font-weight:bold; letter-spacing:2px;"></span>
374
+ <button onclick="copyCode()" style="padding:4px 8px; margin-left:5px; font-size:10px;">📋 Copy</button>
375
+ </div>
376
+ </div>
377
+
378
  <!-- Bubble Controls -->
379
  <div class="control-group">
380
+ <label>💬 Bubble Tools:</label>
381
  <select id="bubble-type-select" onchange="changeBubbleType(this.value)" disabled>
382
  <option value="speech">Speech</option>
383
  <option value="thought">Thought</option>
 
407
 
408
  <!-- Tail Controls -->
409
  <div class="control-group" id="tail-controls" style="display:none;">
410
+ <label>📐 Tail Adjustment:</label>
411
  <button onclick="rotateTail()" class="secondary-btn">🔄 Rotate Side</button>
412
  <div class="slider-container">
413
  <label>Pos:</label>
 
417
 
418
  <!-- Panel Controls -->
419
  <div class="control-group">
420
+ <label>🖼️ Panel Tools:</label>
421
  <button onclick="replacePanelImage()" class="action-btn">🖼️ Replace Image</button>
422
  <div class="button-grid">
423
  <button onclick="adjustFrame('backward')" class="secondary-btn">⬅️ Prev</button>
 
431
 
432
  <!-- Zoom Controls -->
433
  <div class="control-group">
434
+ <label>🔍 Zoom & Pan:</label>
435
  <div class="button-grid">
436
  <button onclick="resetPanelTransform()" class="secondary-btn">Reset</button>
437
  <input type="range" id="zoom-slider" min="100" max="300" value="100" step="5" disabled oninput="handleZoom(this)">
 
440
 
441
  <!-- Actions -->
442
  <div class="control-group">
443
+ <button onclick="exportComic()" class="export-btn">📥 Export as PNG</button>
444
+ <button onclick="goBackToUpload()" class="reset-btn" style="margin-top:10px;">🏠 Back to Home</button>
 
445
  </div>
446
  </div>
447
  </div>
448
 
449
+ <!-- SAVE CODE MODAL -->
450
+ <div class="modal-overlay" id="save-modal">
451
+ <div class="modal-content">
452
+ <h2>✅ Comic Saved!</h2>
453
+ <p>Your unique save code is:</p>
454
+ <div class="code" id="modal-save-code">XXXXXXXX</div>
455
+ <p style="font-size:12px;">Write this code down or copy it.<br>Anyone can load this comic using this code.</p>
456
+ <button onclick="copyModalCode()">📋 Copy Code</button>
457
+ <button class="close-btn" onclick="closeModal()">Close</button>
458
+ </div>
459
+ </div>
460
+
461
  <script>
462
  // --- SESSION LOGIC ---
463
  function genUUID(){ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,c=>{var r=Math.random()*16|0,v=c=='x'?r:(r&0x3|0x8);return v.toString(16);}); }
 
465
  localStorage.setItem('comic_sid', sid);
466
  console.log("Session ID:", sid);
467
 
468
+ let currentSaveCode = null;
 
 
 
469
  let interval, selectedBubble = null, selectedPanel = null;
470
  let isDragging = false, isResizing = false, isPanning = false;
471
  let startX, startY, initX, initY, initW, initH;
 
473
  let resizeHandle = '', originalWidth, originalHeight, originalX, originalY, originalMouseX, originalMouseY;
474
  let currentlyEditing = null;
475
 
476
+ // Check for draft on load
477
+ if(localStorage.getItem('comic_draft_'+sid)) {
478
+ document.getElementById('restore-draft-btn').style.display = 'block';
479
+ }
480
+
481
+ // --- MODAL FUNCTIONS ---
482
+ function showSaveModal(code) {
483
+ document.getElementById('modal-save-code').textContent = code;
484
+ document.getElementById('save-modal').style.display = 'flex';
485
+ }
486
+ function closeModal() {
487
+ document.getElementById('save-modal').style.display = 'none';
488
+ }
489
+ function copyModalCode() {
490
+ const code = document.getElementById('modal-save-code').textContent;
491
+ navigator.clipboard.writeText(code).then(() => {
492
+ alert('Code copied to clipboard!');
493
+ });
494
+ }
495
+ function copyCode() {
496
+ if(currentSaveCode) {
497
+ navigator.clipboard.writeText(currentSaveCode).then(() => {
498
+ alert('Code copied!');
499
+ });
500
+ }
501
+ }
502
+
503
+ // --- SAVE COMIC TO SERVER ---
504
+ async function saveComic() {
505
+ const state = getCurrentState();
506
+ if(!state || state.length === 0) {
507
+ alert('No comic to save!');
508
+ return;
509
+ }
510
+
511
+ try {
512
+ const response = await fetch(`/save_comic?sid=${sid}`, {
513
+ method: 'POST',
514
+ headers: {'Content-Type': 'application/json'},
515
+ body: JSON.stringify({
516
+ pages: state,
517
+ savedAt: new Date().toISOString()
518
+ })
519
+ });
520
+
521
+ const data = await response.json();
522
+ if(data.success) {
523
+ currentSaveCode = data.code;
524
+ document.getElementById('display-save-code').textContent = data.code;
525
+ document.getElementById('current-save-code').style.display = 'block';
526
+ showSaveModal(data.code);
527
+ // Also save to local draft
528
+ saveDraft();
529
+ } else {
530
+ alert('Failed to save: ' + data.message);
531
+ }
532
+ } catch(e) {
533
+ console.error(e);
534
+ alert('Error saving comic');
535
+ }
536
+ }
537
+
538
+ // --- LOAD SAVED COMIC ---
539
+ async function loadSavedComic() {
540
+ const code = document.getElementById('load-code-input').value.trim().toUpperCase();
541
+ if(!code || code.length < 4) {
542
+ alert('Please enter a valid save code');
543
+ return;
544
+ }
545
+
546
+ try {
547
+ const response = await fetch(`/load_comic/${code}`);
548
+ const data = await response.json();
549
+
550
+ if(data.success) {
551
+ currentSaveCode = code;
552
+ sid = data.originalSid || sid;
553
+ localStorage.setItem('comic_sid', sid);
554
+
555
+ renderFromState(data.pages);
556
+ document.getElementById('upload-container').style.display = 'none';
557
+ document.getElementById('editor-container').style.display = 'block';
558
+
559
+ document.getElementById('display-save-code').textContent = code;
560
+ document.getElementById('current-save-code').style.display = 'block';
561
+
562
+ saveDraft();
563
+ } else {
564
+ alert('Could not load comic: ' + data.message);
565
+ }
566
+ } catch(e) {
567
+ console.error(e);
568
+ alert('Error loading comic. Check the code and try again.');
569
+ }
570
+ }
571
+
572
+ // --- RESTORE DRAFT (Local Storage) ---
573
+ function restoreDraft() {
574
+ const savedData = localStorage.getItem('comic_draft_'+sid);
575
+ if(!savedData) {
576
+ alert("No draft found.");
577
+ return;
578
+ }
579
  try {
580
  const state = JSON.parse(savedData);
581
+ if(state.saveCode) {
582
+ currentSaveCode = state.saveCode;
583
+ document.getElementById('display-save-code').textContent = state.saveCode;
584
+ document.getElementById('current-save-code').style.display = 'block';
585
+ }
586
+ renderFromState(state.pages || state);
587
  document.getElementById('upload-container').style.display = 'none';
588
  document.getElementById('editor-container').style.display = 'block';
589
+ } catch(e) {
590
+ console.error(e);
591
+ alert("Failed to restore draft.");
592
+ }
593
+ }
594
+
595
+ // --- GET CURRENT STATE ---
596
+ function getCurrentState() {
597
+ const pages = [];
598
+ document.querySelectorAll('.comic-page').forEach(p => {
599
+ const panels = [];
600
+ p.querySelectorAll('.panel').forEach(pan => {
601
+ const img = pan.querySelector('img');
602
+ const bubbles = [];
603
+ pan.querySelectorAll('.speech-bubble').forEach(b => {
604
+ const textEl = b.querySelector('.bubble-text');
605
+ bubbles.push({
606
+ text: textEl ? textEl.textContent : '',
607
+ left: b.style.left,
608
+ top: b.style.top,
609
+ width: b.style.width,
610
+ height: b.style.height,
611
+ classes: b.className,
612
+ type: b.dataset.type,
613
+ font: b.style.fontFamily,
614
+ tailPos: b.style.getPropertyValue('--tail-pos'),
615
+ colors: {
616
+ fill: b.style.getPropertyValue('--bubble-fill-color'),
617
+ text: b.style.getPropertyValue('--bubble-text-color')
618
+ }
619
+ });
620
+ });
621
+ panels.push({
622
+ src: img.src,
623
+ zoom: img.dataset.zoom,
624
+ tx: img.dataset.translateX,
625
+ ty: img.dataset.translateY,
626
+ bubbles: bubbles
627
+ });
628
+ });
629
+ pages.push({ panels: panels });
630
+ });
631
+ return pages;
632
  }
633
 
634
+ // --- SAVE DRAFT TO LOCAL STORAGE ---
635
+ function saveDraft() {
636
+ const state = {
637
+ pages: getCurrentState(),
638
+ saveCode: currentSaveCode,
639
+ savedAt: new Date().toISOString()
640
+ };
641
+ localStorage.setItem('comic_draft_'+sid, JSON.stringify(state));
642
+ }
643
+
644
+ // --- RENDER FROM STATE ---
645
  function renderFromState(pagesData) {
646
  const con = document.getElementById('comic-container');
647
  con.innerHTML = '';
 
672
  img.onmousedown = (e) => startPan(e, img);
673
  pDiv.appendChild(img);
674
 
 
675
  (pan.bubbles || []).forEach(bData => {
676
  const b = createBubbleHTML(bData);
677
  pDiv.appendChild(b);
 
687
  // --- UPLOAD LOGIC ---
688
  async function upload() {
689
  const f = document.getElementById('file-upload').files[0];
690
+ if(!f) return alert("Select a video file first");
691
+
692
+ // Generate new session for new upload
693
+ sid = genUUID();
694
+ localStorage.setItem('comic_sid', sid);
695
+ currentSaveCode = null;
696
+
697
  document.querySelector('.upload-box').style.display='none';
698
  document.getElementById('loading-view').style.display='flex';
699
  const fd = new FormData(); fd.append('file', f);
 
736
  }))
737
  }));
738
  renderFromState(cleanData);
739
+ saveDraft();
740
  });
741
  }
742
 
743
+ // --- GO BACK TO UPLOAD ---
744
+ function goBackToUpload() {
745
+ if(confirm('Go back to home? Make sure you saved your comic!')) {
746
+ document.getElementById('editor-container').style.display = 'none';
747
+ document.getElementById('upload-container').style.display = 'flex';
748
+ document.querySelector('.upload-box').style.display = 'block';
749
+ document.getElementById('loading-view').style.display = 'none';
750
+ document.getElementById('current-save-code').style.display = 'none';
751
+ }
752
+ }
753
+
754
  // --- BUBBLE CREATION ---
755
  function createBubbleHTML(data) {
756
  const b = document.createElement('div');
757
  b.dataset.type = data.type || 'speech';
758
 
 
759
  applyBubbleType(b, data.type || 'speech', data.classes);
760
 
761
  b.style.left = data.left;
 
775
  textSpan.textContent = data.text || '';
776
  b.appendChild(textSpan);
777
 
 
778
  ['nw', 'ne', 'sw', 'se'].forEach(dir => {
779
  const handle = document.createElement('div');
780
  handle.className = `resize-handle ${dir}`;
 
782
  b.appendChild(handle);
783
  });
784
 
 
785
  b.onmousedown = (e) => {
786
  if(e.target.classList.contains('resize-handle')) return;
787
  e.stopPropagation(); selectBubble(b);
 
789
  initX = b.offsetLeft; initY = b.offsetTop;
790
  };
791
 
792
+ b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
 
 
 
 
 
793
  b.onclick = (e) => { e.stopPropagation(); selectBubble(b); };
794
 
795
  return b;
796
  }
797
 
798
  function applyBubbleType(bubble, type, existingClasses) {
 
799
  bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
800
 
801
  let baseClasses = 'speech-bubble ' + type;
 
828
  textarea.remove();
829
  textSpan.style.display = '';
830
  currentlyEditing = null;
831
+ saveDraft();
832
  };
833
  textarea.addEventListener('blur', finishEditing, { once: true });
834
  textarea.addEventListener('keydown', e => {
 
854
  });
855
 
856
  document.addEventListener('mouseup', () => {
857
+ if(isDragging || isResizing || isPanning) saveDraft();
858
  isDragging = false; isResizing = false; isPanning = false;
859
  });
860
 
 
897
  selectedBubble = el;
898
  el.classList.add('selected');
899
 
 
900
  const bubbleType = el.dataset.type || 'speech';
901
  document.getElementById('tail-controls').style.display = (bubbleType === 'speech' || bubbleType === 'thought') ? 'block' : 'none';
902
 
 
904
  document.getElementById('zoom-slider').disabled = true;
905
  document.getElementById('bubble-type-select').value = bubbleType;
906
 
 
907
  const styles = window.getComputedStyle(el);
908
  const textColor = styles.getPropertyValue('--bubble-text-color').trim() || rgbToHex(styles.color);
909
  const fillColor = styles.getPropertyValue('--bubble-fill-color').trim() || rgbToHex(styles.backgroundColor);
910
  document.getElementById('bubble-text-color').value = textColor;
911
  document.getElementById('bubble-fill-color').value = fillColor;
912
 
 
913
  const tailPos = styles.getPropertyValue('--tail-pos').trim();
914
  document.getElementById('tail-slider').value = tailPos ? parseInt(tailPos) : 50;
915
  }
 
938
  });
939
  selectedPanel.appendChild(b);
940
  selectBubble(b);
941
+ saveDraft();
942
  }
943
 
944
  function deleteBubble() {
 
946
  if(confirm("Delete this bubble?")) {
947
  selectedBubble.remove();
948
  selectedBubble = null;
949
+ saveDraft();
950
  }
951
  }
952
 
 
955
  applyBubbleType(selectedBubble, type);
956
  selectedBubble.classList.add('selected');
957
  document.getElementById('tail-controls').style.display = (type === 'speech' || type === 'thought') ? 'block' : 'none';
958
+ saveDraft();
959
  }
960
 
961
  function changeFont(font) {
962
  if(!selectedBubble) return;
963
  selectedBubble.style.fontFamily = font;
964
+ saveDraft();
965
  }
966
 
967
  function rotateTail() {
 
982
  else if (isFlippedH && isFlippedV) selectedBubble.classList.remove('flipped');
983
  else selectedBubble.classList.remove('flipped-vertical');
984
  }
985
+ saveDraft();
986
  }
987
 
988
  function slideTail(v) {
989
  if(selectedBubble) {
990
  selectedBubble.style.setProperty('--tail-pos', v+'%');
991
+ saveDraft();
992
  }
993
  }
994
 
 
995
  document.getElementById('bubble-text-color').addEventListener('input', (e) => {
996
  if(selectedBubble) {
997
  selectedBubble.style.setProperty('--bubble-text-color', e.target.value);
998
+ saveDraft();
999
  }
1000
  });
1001
  document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
1002
  if(selectedBubble) {
1003
  selectedBubble.style.setProperty('--bubble-fill-color', e.target.value);
1004
+ saveDraft();
1005
  }
1006
  });
1007
 
 
1011
  const img = selectedPanel.querySelector('img');
1012
  img.dataset.zoom = el.value;
1013
  updateImageTransform(img);
1014
+ saveDraft();
1015
  }
1016
 
1017
  function startPan(e, img) {
 
1050
  img.dataset.translateY = 0;
1051
  document.getElementById('zoom-slider').value = 100;
1052
  updateImageTransform(img);
1053
+ saveDraft();
1054
  }
1055
 
1056
  // --- BACKEND ACTIONS ---
 
1067
  if(d.success) {
1068
  img.src = `/frames/${d.new_filename}?sid=${sid}`;
1069
  resetPanelTransform();
1070
+ saveDraft();
1071
  } else {
1072
  alert('Error: ' + d.error);
1073
  }
 
1093
  alert('Error: ' + d.message);
1094
  }
1095
  img.style.opacity = '1';
1096
+ saveDraft();
1097
  }
1098
 
1099
  async function gotoTimestamp() {
 
1126
  alert('Error: ' + d.message);
1127
  }
1128
  img.style.opacity = '1';
1129
+ saveDraft();
1130
  }
1131
 
1132
  // --- EXPORT ---
 
1134
  const pgs = document.querySelectorAll('.comic-page');
1135
  if(pgs.length === 0) return alert("No pages found");
1136
 
 
1137
  const bubbles = document.querySelectorAll('.speech-bubble');
1138
  bubbles.forEach(b => {
1139
  const rect = b.getBoundingClientRect();
 
1155
  }
1156
  }
1157
 
 
1158
  bubbles.forEach(b => {
1159
  b.style.width = '';
1160
  b.style.height = '';
1161
  });
1162
  }
1163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1164
  // Helper
1165
  function rgbToHex(rgb) {
1166
  if (!rgb || !rgb.startsWith('rgb')) return '#ffffff';
 
1191
  os.makedirs(self.output_dir, exist_ok=True)
1192
  self.video_fps = None
1193
  self.frame_metadata = {}
 
1194
 
1195
  def update_status(self, message, progress):
1196
  try:
 
1214
  os.remove(os.path.join(self.output_dir, f))
1215
  except:
1216
  pass
 
1217
  user_srt = os.path.join(self.user_dir, 'subs.srt')
1218
  if os.path.exists(user_srt):
1219
  os.remove(user_srt)
1220
  print("✅ Cleanup complete.")
1221
 
1222
  def generate_keyframes_from_moments(self, key_moments, max_frames=48):
 
1223
  try:
1224
  cap = cv2.VideoCapture(self.video_path)
1225
  if not cap.isOpened():
 
1270
  return False
1271
 
1272
  def _enhance_all_images(self, single_image_path=None):
 
1273
  try:
1274
  enhancer = SimpleColorEnhancer()
1275
  if single_image_path:
 
1284
  print(f"⚠️ Simple enhancement failed: {e}")
1285
 
1286
  def _enhance_quality_colors(self, single_image_path=None):
 
1287
  try:
1288
  enhancer = QualityColorEnhancer()
1289
  if single_image_path:
 
1298
  print(f"⚠️ Quality enhancement failed: {e}")
1299
 
1300
  def _process_bubble_for_frame(self, frame_file):
 
1301
  frame_path = os.path.join(self.frames_dir, frame_file)
1302
  meta = self.frame_metadata.get(frame_file, {})
1303
  dialogue = meta.get('dialogue', '') if isinstance(meta, dict) else ''
 
1329
  )
1330
 
1331
  def _create_ai_bubbles_from_moments(self):
 
1332
  frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
1333
 
1334
  if not os.path.exists(self.metadata_path):
 
1343
  return bubbles
1344
 
1345
  def _generate_pages(self, bubbles_list):
 
1346
  try:
1347
  from backend.fixed_12_pages_800x1080 import generate_12_pages_800x1080
1348
  frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
1349
  return generate_12_pages_800x1080(frame_files, bubbles_list)
1350
  except ImportError:
 
1351
  pages = []
1352
  frame_files = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
1353
  num_pages = (len(frame_files) + 3) // 4
 
1362
  return pages
1363
 
1364
  def generate_comic(self):
 
1365
  start_time = time.time()
1366
  try:
1367
  if cv2 is None:
 
1437
  return False
1438
 
1439
  def _save_results(self, pages):
 
1440
  try:
1441
  pages_data = []
1442
  for page in pages:
 
1452
  print(f"❌ Save results failed: {e}")
1453
 
1454
  def regenerate_frame(self, fname, direction):
 
1455
  try:
1456
  if not os.path.exists(self.metadata_path):
1457
  return {"success": False, "message": "Frame metadata missing."}
 
1462
  if fname not in meta:
1463
  return {"success": False, "message": "Panel not linked to video."}
1464
 
 
1465
  current_data = meta[fname]
1466
  if isinstance(current_data, dict):
1467
  curr_time = current_data['time']
1468
  else:
1469
  curr_time = current_data
1470
 
 
1471
  if not self.video_fps:
1472
  cap = cv2.VideoCapture(self.video_path)
1473
  self.video_fps = cap.get(cv2.CAP_PROP_FPS) or 25
1474
  cap.release()
1475
 
 
1476
  offset = (1.0 / self.video_fps) * (1 if direction == 'forward' else -1)
1477
  new_time = max(0, curr_time + offset)
1478
 
 
1479
  cap = cv2.VideoCapture(self.video_path)
1480
  cap.set(cv2.CAP_PROP_POS_MSEC, new_time * 1000)
1481
  ret, frame = cap.read()
 
1485
  frame_path = os.path.join(self.frames_dir, fname)
1486
  cv2.imwrite(frame_path, frame)
1487
 
 
1488
  print(f"🎨 Applying enhancements to new frame: {fname}")
1489
  self._enhance_all_images(single_image_path=frame_path)
1490
  self._enhance_quality_colors(single_image_path=frame_path)
1491
 
 
1492
  if isinstance(meta[fname], dict):
1493
  meta[fname]['time'] = new_time
1494
  else:
 
1507
  return {"success": False, "message": str(e)}
1508
 
1509
  def get_frame_at_timestamp(self, fname, ts):
 
1510
  try:
1511
  cap = cv2.VideoCapture(self.video_path)
1512
  if not cap.isOpened():
 
1527
  frame_path = os.path.join(self.frames_dir, fname)
1528
  cv2.imwrite(frame_path, frame)
1529
 
 
1530
  print(f"🎨 Applying enhancements to frame from timestamp: {fname}")
1531
  self._enhance_all_images(single_image_path=frame_path)
1532
  self._enhance_quality_colors(single_image_path=frame_path)
1533
 
 
1534
  if os.path.exists(self.metadata_path):
1535
  with open(self.metadata_path, 'r') as f:
1536
  meta = json.load(f)
 
1631
  return jsonify({'success': False, 'error': 'No image provided.'})
1632
 
1633
  f = request.files['image']
1634
+ frames_dir = os.path.join(BASE_USER_DIR, sid, 'frames')
1635
+ os.makedirs(frames_dir, exist_ok=True)
1636
  fname = f"replaced_{int(time.time() * 1000)}.png"
1637
+ f.save(os.path.join(frames_dir, fname))
1638
  return jsonify({'success': True, 'new_filename': fname})
1639
 
1640
+ # --- SAVE COMIC ENDPOINT ---
1641
+ @app.route('/save_comic', methods=['POST'])
1642
+ def save_comic():
1643
+ sid = request.args.get('sid')
1644
+ if not sid:
1645
+ return jsonify({'success': False, 'message': 'Missing session ID'})
1646
+
1647
+ try:
1648
+ data = request.get_json()
1649
+
1650
+ # Generate unique save code
1651
+ save_code = generate_save_code()
1652
+ save_dir = os.path.join(SAVED_COMICS_DIR, save_code)
1653
+ os.makedirs(save_dir, exist_ok=True)
1654
+
1655
+ # Copy frames from user directory to saved directory
1656
+ user_frames_dir = os.path.join(BASE_USER_DIR, sid, 'frames')
1657
+ saved_frames_dir = os.path.join(save_dir, 'frames')
1658
+
1659
+ if os.path.exists(user_frames_dir):
1660
+ if os.path.exists(saved_frames_dir):
1661
+ shutil.rmtree(saved_frames_dir)
1662
+ shutil.copytree(user_frames_dir, saved_frames_dir)
1663
+
1664
+ # Save the comic state
1665
+ save_data = {
1666
+ 'code': save_code,
1667
+ 'originalSid': sid,
1668
+ 'pages': data.get('pages', []),
1669
+ 'savedAt': data.get('savedAt', time.strftime('%Y-%m-%d %H:%M:%S'))
1670
+ }
1671
+
1672
+ with open(os.path.join(save_dir, 'comic_state.json'), 'w') as f:
1673
+ json.dump(save_data, f, indent=2)
1674
+
1675
+ print(f"✅ Comic saved with code: {save_code}")
1676
+ return jsonify({'success': True, 'code': save_code})
1677
+
1678
+ except Exception as e:
1679
+ traceback.print_exc()
1680
+ return jsonify({'success': False, 'message': str(e)})
1681
+
1682
+ # --- LOAD COMIC ENDPOINT ---
1683
+ @app.route('/load_comic/<code>')
1684
+ def load_comic(code):
1685
+ code = code.upper()
1686
+ save_dir = os.path.join(SAVED_COMICS_DIR, code)
1687
+ state_file = os.path.join(save_dir, 'comic_state.json')
1688
+
1689
+ if not os.path.exists(state_file):
1690
+ return jsonify({'success': False, 'message': 'Save code not found'})
1691
+
1692
+ try:
1693
+ with open(state_file, 'r') as f:
1694
+ save_data = json.load(f)
1695
+
1696
+ original_sid = save_data.get('originalSid')
1697
+
1698
+ # Copy frames to user directory if needed
1699
+ saved_frames_dir = os.path.join(save_dir, 'frames')
1700
+ if original_sid and os.path.exists(saved_frames_dir):
1701
+ user_frames_dir = os.path.join(BASE_USER_DIR, original_sid, 'frames')
1702
+ os.makedirs(user_frames_dir, exist_ok=True)
1703
+
1704
+ # Copy files that don't exist
1705
+ for fname in os.listdir(saved_frames_dir):
1706
+ src = os.path.join(saved_frames_dir, fname)
1707
+ dst = os.path.join(user_frames_dir, fname)
1708
+ if not os.path.exists(dst):
1709
+ shutil.copy2(src, dst)
1710
+
1711
+ return jsonify({
1712
+ 'success': True,
1713
+ 'pages': save_data.get('pages', []),
1714
+ 'originalSid': original_sid,
1715
+ 'savedAt': save_data.get('savedAt')
1716
+ })
1717
+
1718
+ except Exception as e:
1719
+ traceback.print_exc()
1720
+ return jsonify({'success': False, 'message': str(e)})
1721
+
1722
+ # --- SERVE SAVED COMIC FRAMES ---
1723
+ @app.route('/saved_frames/<code>/<path:filename>')
1724
+ def get_saved_frame(code, filename):
1725
+ code = code.upper()
1726
+ frames_dir = os.path.join(SAVED_COMICS_DIR, code, 'frames')
1727
+ if os.path.exists(os.path.join(frames_dir, filename)):
1728
+ return send_from_directory(frames_dir, filename)
1729
+ return "Frame not found", 404
1730
+
1731
 
1732
  if __name__ == '__main__':
1733
  os.makedirs(BASE_USER_DIR, exist_ok=True)
1734
+ os.makedirs(SAVED_COMICS_DIR, exist_ok=True)
1735
  port = int(os.getenv("PORT", 7860))
1736
  print(f"🚀 Starting Enhanced Comic Generator on host 0.0.0.0, port {port}")
1737
+ print(f"📁 User data directory: {BASE_USER_DIR}")
1738
+ print(f"💾 Saved comics directory: {SAVED_COMICS_DIR}")
1739
  app.run(host='0.0.0.0', port=port, debug=False)