tester343 commited on
Commit
6b724b1
ยท
verified ยท
1 Parent(s): d95669b

Update app_enhanced.py

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