tester343 commited on
Commit
bc2542d
·
verified ·
1 Parent(s): 7ca93e7

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +84 -271
app_enhanced.py CHANGED
@@ -66,7 +66,7 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, status_p
66
 
67
  if target_pages <= 0: target_pages = 1
68
 
69
- # === 5 PANELS PER PAGE ===
70
  panels_per_page = 5
71
  total_panels_needed = target_pages * panels_per_page
72
 
@@ -100,7 +100,6 @@ def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, status_p
100
  frame_metadata[fname] = {'dialogue': moment['text'], 'time': mid}
101
  frame_files_ordered.append(fname)
102
  count += 1
103
- # Update progress periodically
104
  if count % 2 == 0:
105
  prog = 30 + int((count / total_panels_needed) * 20)
106
  update_status_file(status_path, f"Extracted Frame {count}/{total_panels_needed}", prog)
@@ -311,7 +310,6 @@ class EnhancedComicGenerator:
311
  try:
312
  self.write_status("Queued for GPU...", 5)
313
  status_file = os.path.join(self.output_dir, 'status.json')
314
- # Pass status_file path to GPU function for real-time updates
315
  data = generate_comic_gpu(self.video_path, self.user_dir, self.frames_dir, self.metadata_path, status_file, int(target_pages))
316
  with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
317
  json.dump(data, f, indent=2)
@@ -341,29 +339,16 @@ INDEX_HTML = '''
341
  /* =====================================================
342
  GLOBAL RESET — ABSOLUTE BASELINE
343
  ===================================================== */
344
- * {
345
- margin: 0;
346
- padding: 0;
347
- box-sizing: border-box;
348
- }
349
-
350
- html, body {
351
- background: #f0f0f0;
352
- font-family: system-ui, Arial, sans-serif;
353
- }
354
 
355
  /* =====================================================
356
- PAGE WRAPPER (NO SCALE, EVER)
357
  ===================================================== */
358
- #workspace {
359
- padding: 20px;
360
- display: flex;
361
- flex-direction: column;
362
- align-items: center;
363
- }
364
 
365
  /* =====================================================
366
- COMIC PAGE — TRUE RESOLUTION
367
  1000px Width x 712px Height
368
  ===================================================== */
369
  .comic-page {
@@ -378,88 +363,72 @@ html, body {
378
  }
379
 
380
  .page-title {
381
- font-size: 24px;
382
- font-weight: bold;
383
- margin-bottom: 10px;
384
- color: #333;
385
  }
386
 
387
  /* =====================================================
388
  PANELS — 5 PANEL SLANTED LAYOUT
 
389
  ===================================================== */
390
  .panel {
391
  position: absolute;
392
- /* Removed border to allow gutters to be pure white */
393
- /* border: 2px solid #000; */
394
  overflow: hidden;
395
- background: #fff; /* White background for gaps */
 
 
396
  }
397
 
398
  /*
399
- Reference: 1000px width x 712px height.
400
- Gutter: 12px horizontal (1.2%), 12px vertical (1.7%).
401
- Half Gutter: 0.6% Horz, 0.85% Vert.
402
-
403
- COORDINATES from Green Text:
404
- Row 1 Split: Top=63.2%, Bottom=59.5%
405
- Row 2 Left Split: Top=33.0%, Bottom=35.7%
406
- Row 2 Right Split: Top=64.8%, Bottom=68.2%
407
- Tier Height: 350px (approx 49.15% of 712px)
408
  */
409
 
410
  /* --- ROW 1 (TOP) --- Height 49.15% */
411
-
412
- /* Panel 1: Right ends at (63.2 - 0.6)% / (59.5 - 0.6)% */
413
- .panel:nth-child(1) {
414
  top: 0; left: 0; height: 49.15%; width: 100%;
415
  clip-path: polygon(0% 0%, 62.6% 0%, 58.9% 100%, 0% 100%);
416
  }
417
- /* Panel 2: Left starts at (63.2 + 0.6)% / (59.5 + 0.6)% */
418
- .panel:nth-child(2) {
419
  top: 0; left: 0; height: 49.15%; width: 100%;
420
  clip-path: polygon(63.8% 0%, 100% 0%, 100% 100%, 60.1% 100%);
421
  }
422
 
423
  /* --- ROW 2 (BOTTOM) --- Top: 50.85% (Gap ~1.7%), Height: 49.15% */
424
-
425
- /* Panel 3: Right ends at (33.0 - 0.6)% / (35.7 - 0.6)% */
426
- .panel:nth-child(3) {
427
  top: 50.85%; left: 0; height: 49.15%; width: 100%;
428
  clip-path: polygon(0% 0%, 32.4% 0%, 35.1% 100%, 0% 100%);
429
  }
430
- /* Panel 4: Left starts (33.0 + 0.6)% / (35.7 + 0.6)%. Right ends (64.8 - 0.6)% / (68.2 - 0.6)% */
431
- .panel:nth-child(4) {
432
  top: 50.85%; left: 0; height: 49.15%; width: 100%;
433
  clip-path: polygon(33.6% 0%, 64.2% 0%, 67.6% 100%, 36.3% 100%);
434
  }
435
- /* Panel 5: Left starts (64.8 + 0.6)% / (68.2 + 0.6)% */
436
- .panel:nth-child(5) {
437
  top: 50.85%; left: 0; height: 49.15%; width: 100%;
438
  clip-path: polygon(65.4% 0%, 100% 0%, 100% 100%, 68.8% 100%);
439
  }
440
 
441
  /* =====================================================
442
- PANEL IMAGES — FINAL RULES
443
  ===================================================== */
444
  .panel img {
445
  width: 100%;
446
  height: 100%;
447
- object-fit: contain; /* 🔒 NEVER CROP */
448
- transform: none !important; /* 🔒 NEVER SCALE */
449
  display: block;
450
- background: #fff; /* White bars if ratio mismatch */
451
  image-rendering: auto;
452
  }
453
 
454
  /* =====================================================
455
- SPEECH / THOUGHT BUBBLES
456
  ===================================================== */
457
- .bubble, .speech-bubble {
458
  position: absolute;
459
  max-width: 260px;
460
  background: #4ECDC4;
461
  color: white;
462
- border: 2px solid transparent;
463
  border-radius: 18px;
464
  padding: 10px 14px;
465
  font-size: 14px;
@@ -470,115 +439,61 @@ html, body {
470
  font-weight: bold;
471
  text-align: center;
472
  }
473
-
474
- /* Tail logic handled via JS classes in previous logic, simplifying here for "True Size" */
475
- .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 101; }
 
476
 
477
  /* =====================================================
478
- TOOLBAR & UI
479
  ===================================================== */
480
- .toolbar {
481
- width: 1000px;
482
- margin: 0 auto 20px auto;
483
- display: flex;
484
- gap: 10px;
485
- background: white;
486
- padding: 10px;
487
- border-radius: 8px;
488
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
489
- justify-content: center;
490
- }
491
-
492
- .toolbar button {
493
- padding: 8px 16px;
494
- border: 1px solid #ddd;
495
- background: #fff;
496
- cursor: pointer;
497
- font-weight: bold;
498
- border-radius: 4px;
499
- }
500
-
501
- .toolbar button:hover {
502
- background: #f0f0f0;
503
- }
504
 
505
- #upload-container { display: flex; flex-direction:column; justify-content: center; align-items: center; min-height: 100vh; width: 100%; }
506
  .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; }
507
- .file-input { display: none; }
508
- .file-label { display: block; padding: 15px; background: #2c3e50; color: white; border-radius: 8px; cursor: pointer; font-weight: bold; margin-bottom: 10px; }
509
- .submit-btn { width: 100%; padding: 15px; background: #e67e22; color: white; border: none; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; margin-top:10px; }
510
  .loader { width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 20px auto; }
511
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
512
-
513
  </style>
514
  </head>
515
 
516
  <body>
517
 
518
- <!-- UPLOAD SECTION -->
519
  <div id="upload-container">
520
  <div class="upload-box">
521
  <h1>🎬 CineComic Editor</h1>
522
- <input type="file" id="file-upload" class="file-input" onchange="document.getElementById('fn').innerText=this.files[0].name">
523
- <label for="file-upload" class="file-label">📁 Choose Video File</label>
524
- <span id="fn" style="display:block; margin-bottom:10px; color:#666;">No file selected</span>
525
-
526
- <div style="margin: 20px 0; text-align:left;">
527
- <label style="font-weight:bold; display:block; margin-bottom:5px;">📚 Pages:</label>
528
- <input type="number" id="page-count" value="4" min="1" max="15" style="width:100%; padding:10px; border-radius:5px; border:1px solid #ccc;">
529
- </div>
530
-
531
- <button class="submit-btn" onclick="upload()">🚀 Generate Comic</button>
532
-
533
  <div id="loading-view" style="display:none; margin-top:20px;">
534
  <div class="loader"></div>
535
- <p id="status-text">Processing...</p>
536
- </div>
537
-
538
- <div style="margin-top:20px; border-top:1px solid #eee; padding-top:20px;">
539
- <input type="text" id="load-code-input" placeholder="SAVE CODE" maxlength="8" style="padding:10px; width:150px; text-align:center; text-transform:uppercase;">
540
- <button onclick="loadSavedComic()" style="padding:10px 20px; cursor:pointer;">Load</button>
541
  </div>
542
  </div>
543
  </div>
544
 
545
- <!-- EDITOR WORKSPACE -->
546
  <div id="workspace" style="display:none;">
547
  <div class="toolbar">
548
- <button onclick="saveComic()">💾 Save</button>
549
  <button onclick="exportComic()">📥 Export PNG</button>
550
- <button onclick="location.reload()">🏠 Home</button>
551
  </div>
552
-
553
  <div id="comic-container"></div>
554
  </div>
555
 
556
  <script>
557
- /* =====================================================
558
- LOGIC & API
559
- ===================================================== */
560
  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);}); }
561
- let sid = localStorage.getItem('comic_sid') || genUUID();
562
- localStorage.setItem('comic_sid', sid);
563
  let interval;
564
 
565
- // --- GLOBAL IMAGE POLICY ---
566
- function forceImageRules(img) {
567
- img.style.objectFit = 'contain';
568
- img.style.transform = 'none';
569
- img.style.width = '100%';
570
- img.style.height = '100%';
571
- img.style.display = 'block';
572
- img.removeAttribute('data-zoom');
573
- }
574
-
575
- // --- UPLOAD ---
576
  async function upload() {
577
  const f = document.getElementById('file-upload').files[0];
578
  const pCount = document.getElementById('page-count').value;
579
  if(!f) return alert("Select a video");
580
 
581
- sid = genUUID(); localStorage.setItem('comic_sid', sid);
582
  document.querySelector('.upload-box').style.display='none';
583
  document.getElementById('loading-view').style.display='block';
584
 
@@ -588,7 +503,6 @@ async function upload() {
588
 
589
  const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
590
  if(r.ok) interval = setInterval(checkStatus, 2000);
591
- else { alert("Upload failed"); location.reload(); }
592
  }
593
 
594
  async function checkStatus() {
@@ -614,77 +528,69 @@ function renderComic(data) {
614
  const container = document.getElementById('comic-container');
615
  container.innerHTML = '';
616
 
617
- // Transform backend data format if needed, assume it matches expected 'panels'
618
- // Backend returns list of pages, each with 'panels' array
619
-
620
  data.forEach((pageData, idx) => {
621
- // Wrapper for title
622
- const wrapper = document.createElement('div');
623
  const title = document.createElement('div');
624
  title.className = 'page-title';
625
  title.innerText = `Page ${idx + 1}`;
626
- wrapper.appendChild(title);
627
 
628
  const pageDiv = document.createElement('div');
629
  pageDiv.className = 'comic-page';
630
 
631
- // Render 5 panels strictly
632
  pageData.panels.forEach((p, i) => {
633
- if(i >= 5) return; // Only template supports 5
634
 
635
  const panelDiv = document.createElement('div');
636
  panelDiv.className = 'panel';
 
637
 
638
  const img = document.createElement('img');
639
  img.src = `/frames/${p.image}?sid=${sid}`;
640
- forceImageRules(img); // APPLY TRUE SIZE RULES
641
 
642
  panelDiv.appendChild(img);
643
 
644
- // Add bubbles if any
645
- if(pageData.bubbles && pageData.bubbles[i]) {
646
- const bData = pageData.bubbles[i];
647
- if(bData.dialog) {
648
- const bubble = document.createElement('div');
649
- bubble.className = `speech-bubble ${bData.type || 'speech'}`;
650
- bubble.style.left = (bData.bubble_offset_x || 50) + 'px';
651
- bubble.style.top = (bData.bubble_offset_y || 20) + 'px';
652
- bubble.innerText = bData.dialog;
653
-
654
- // Simple drag logic for bubbles
655
- bubble.onmousedown = function(e) {
656
- e.stopPropagation();
657
- let shiftX = e.clientX - bubble.getBoundingClientRect().left;
658
- let shiftY = e.clientY - bubble.getBoundingClientRect().top;
659
-
660
- function moveAt(pageX, pageY) {
661
- // Relative to pageDiv
662
- let rect = pageDiv.getBoundingClientRect();
663
- bubble.style.left = pageX - shiftX - rect.left + 'px';
664
- bubble.style.top = pageY - shiftY - rect.top + 'px';
665
- }
666
-
667
- function onMouseMove(event) { moveAt(event.pageX, event.pageY); }
668
- document.addEventListener('mousemove', onMouseMove);
669
- bubble.onmouseup = function() {
670
- document.removeEventListener('mousemove', onMouseMove);
671
- bubble.onmouseup = null;
672
- };
673
- };
674
-
675
- panelDiv.appendChild(bubble);
676
- }
677
  }
678
 
679
  pageDiv.appendChild(panelDiv);
680
  });
681
 
682
- wrapper.appendChild(pageDiv);
683
- container.appendChild(wrapper);
684
  });
685
  }
686
 
687
- // --- EXPORT ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
  async function exportComic() {
689
  const pgs = document.querySelectorAll('.comic-page');
690
  for(let i=0; i<pgs.length; i++) {
@@ -694,27 +600,6 @@ async function exportComic() {
694
  } catch(e) { console.error(e); }
695
  }
696
  }
697
-
698
- // --- SAVE / LOAD SKELETON ---
699
- async function saveComic() {
700
- // Logic similar to previous but simplified data structure
701
- alert("Save implemented on backend, frontend state gathering required.");
702
- }
703
- async function loadSavedComic() {
704
- const code = document.getElementById('load-code-input').value;
705
- const r = await fetch(`/load_comic/${code}`);
706
- const d = await r.json();
707
- if(d.success) {
708
- sid = d.originalSid;
709
- document.getElementById('upload-container').style.display='none';
710
- document.getElementById('workspace').style.display='flex';
711
- // Need to adapt saved data structure to renderComic
712
- // renderComic(d.pages);
713
- alert("Loaded! (Rendering logic needs full state match)");
714
- location.reload(); // Quick fix for demo to reload state from server files
715
- }
716
- }
717
-
718
  </script>
719
  </body>
720
  </html>
@@ -731,16 +616,13 @@ def upload():
731
  if 'file' not in request.files or not request.files['file'].filename:
732
  return jsonify({'success': False, 'message': 'No file selected'}), 400
733
 
734
- # GET PAGE COUNT FROM FORM
735
  target_pages = request.form.get('target_pages', 4)
736
-
737
  f = request.files['file']
738
  gen = EnhancedComicGenerator(sid)
739
  gen.cleanup()
740
  f.save(gen.video_path)
741
  gen.write_status("Starting...", 5)
742
 
743
- # Run in thread
744
  threading.Thread(target=gen.run, args=(target_pages,)).start()
745
  return jsonify({'success': True, 'message': 'Generation started.'})
746
 
@@ -761,82 +643,13 @@ def get_frame(filename):
761
  sid = request.args.get('sid')
762
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
763
 
764
- @app.route('/regenerate_frame', methods=['POST'])
765
- def regen():
766
- sid = request.args.get('sid')
767
- d = request.get_json()
768
- gen = EnhancedComicGenerator(sid)
769
- return jsonify(regen_frame_gpu(gen.video_path, gen.frames_dir, gen.metadata_path, d['filename'], d['direction']))
770
-
771
- @app.route('/goto_timestamp', methods=['POST'])
772
- def go_time():
773
- sid = request.args.get('sid')
774
- d = request.get_json()
775
- gen = EnhancedComicGenerator(sid)
776
- return jsonify(get_frame_at_ts_gpu(gen.video_path, gen.frames_dir, gen.metadata_path, d['filename'], float(d['timestamp'])))
777
-
778
- @app.route('/replace_panel', methods=['POST'])
779
- def rep_panel():
780
- sid = request.args.get('sid')
781
- if 'image' not in request.files: return jsonify({'success': False, 'error': 'No image'})
782
- f = request.files['image']
783
- frames_dir = os.path.join(BASE_USER_DIR, sid, 'frames')
784
- os.makedirs(frames_dir, exist_ok=True)
785
- fname = f"replaced_{int(time.time() * 1000)}.png"
786
- f.save(os.path.join(frames_dir, fname))
787
- return jsonify({'success': True, 'new_filename': fname})
788
 
789
  @app.route('/save_comic', methods=['POST'])
790
  def save_comic():
791
- sid = request.args.get('sid')
792
- try:
793
- data = request.get_json()
794
- save_code = generate_save_code()
795
- save_dir = os.path.join(SAVED_COMICS_DIR, save_code)
796
- os.makedirs(save_dir, exist_ok=True)
797
-
798
- user_frames_dir = os.path.join(BASE_USER_DIR, sid, 'frames')
799
- saved_frames_dir = os.path.join(save_dir, 'frames')
800
-
801
- if os.path.exists(user_frames_dir):
802
- if os.path.exists(saved_frames_dir): shutil.rmtree(saved_frames_dir)
803
- shutil.copytree(user_frames_dir, saved_frames_dir)
804
-
805
- save_data = {
806
- 'code': save_code,
807
- 'originalSid': sid,
808
- 'pages': data.get('pages', []),
809
- 'savedAt': data.get('savedAt', time.strftime('%Y-%m-%d %H:%M:%S'))
810
- }
811
- with open(os.path.join(save_dir, 'comic_state.json'), 'w') as f: json.dump(save_data, f, indent=2)
812
- return jsonify({'success': True, 'code': save_code})
813
- except Exception as e:
814
- traceback.print_exc()
815
- return jsonify({'success': False, 'message': str(e)})
816
-
817
- @app.route('/load_comic/<code>')
818
- def load_comic(code):
819
- code = code.upper()
820
- save_dir = os.path.join(SAVED_COMICS_DIR, code)
821
- state_file = os.path.join(save_dir, 'comic_state.json')
822
-
823
- if not os.path.exists(state_file): return jsonify({'success': False, 'message': 'Save code not found'})
824
-
825
- try:
826
- with open(state_file, 'r') as f: save_data = json.load(f)
827
- original_sid = save_data.get('originalSid')
828
- saved_frames_dir = os.path.join(save_dir, 'frames')
829
- if original_sid and os.path.exists(saved_frames_dir):
830
- user_frames_dir = os.path.join(BASE_USER_DIR, original_sid, 'frames')
831
- os.makedirs(user_frames_dir, exist_ok=True)
832
- for fname in os.listdir(saved_frames_dir):
833
- src = os.path.join(saved_frames_dir, fname)
834
- dst = os.path.join(user_frames_dir, fname)
835
- if not os.path.exists(dst): shutil.copy2(src, dst)
836
- return jsonify({ 'success': True, 'pages': save_data.get('pages', []), 'originalSid': original_sid, 'savedAt': save_data.get('savedAt') })
837
- except Exception as e:
838
- traceback.print_exc()
839
- return jsonify({'success': False, 'message': str(e)})
840
 
841
  if __name__ == '__main__':
842
  try: gpu_warmup()
 
66
 
67
  if target_pages <= 0: target_pages = 1
68
 
69
+ # === 5 PANELS PER PAGE (Fixed Template) ===
70
  panels_per_page = 5
71
  total_panels_needed = target_pages * panels_per_page
72
 
 
100
  frame_metadata[fname] = {'dialogue': moment['text'], 'time': mid}
101
  frame_files_ordered.append(fname)
102
  count += 1
 
103
  if count % 2 == 0:
104
  prog = 30 + int((count / total_panels_needed) * 20)
105
  update_status_file(status_path, f"Extracted Frame {count}/{total_panels_needed}", prog)
 
310
  try:
311
  self.write_status("Queued for GPU...", 5)
312
  status_file = os.path.join(self.output_dir, 'status.json')
 
313
  data = generate_comic_gpu(self.video_path, self.user_dir, self.frames_dir, self.metadata_path, status_file, int(target_pages))
314
  with open(os.path.join(self.output_dir, 'pages.json'), 'w') as f:
315
  json.dump(data, f, indent=2)
 
339
  /* =====================================================
340
  GLOBAL RESET — ABSOLUTE BASELINE
341
  ===================================================== */
342
+ * { margin: 0; padding: 0; box-sizing: border-box; }
343
+ html, body { background: #f0f0f0; font-family: system-ui, Arial, sans-serif; }
 
 
 
 
 
 
 
 
344
 
345
  /* =====================================================
346
+ PAGE WRAPPER
347
  ===================================================== */
348
+ #workspace { padding: 20px; display: flex; flex-direction: column; align-items: center; }
 
 
 
 
 
349
 
350
  /* =====================================================
351
+ COMIC PAGE — TRUE RESOLUTION (NO CSS SCALING)
352
  1000px Width x 712px Height
353
  ===================================================== */
354
  .comic-page {
 
363
  }
364
 
365
  .page-title {
366
+ font-size: 24px; font-weight: bold; margin-bottom: 10px; color: #333;
 
 
 
367
  }
368
 
369
  /* =====================================================
370
  PANELS — 5 PANEL SLANTED LAYOUT
371
+ Method: Absolute Position + Clip Path + White Gaps
372
  ===================================================== */
373
  .panel {
374
  position: absolute;
 
 
375
  overflow: hidden;
376
+ background: #fff; /* White background creates the 'gap' effect */
377
+ cursor: pointer;
378
+ border: 0;
379
  }
380
 
381
  /*
382
+ TEMPLATE DEFINITION:
383
+ Based on 1000px x 712px Canvas.
384
+ 12px Gutter ~ 1.2% Width, 1.7% Height.
 
 
 
 
 
 
385
  */
386
 
387
  /* --- ROW 1 (TOP) --- Height 49.15% */
388
+ .panel[data-pos="1"] { /* Top Left */
 
 
389
  top: 0; left: 0; height: 49.15%; width: 100%;
390
  clip-path: polygon(0% 0%, 62.6% 0%, 58.9% 100%, 0% 100%);
391
  }
392
+ .panel[data-pos="2"] { /* Top Right */
 
393
  top: 0; left: 0; height: 49.15%; width: 100%;
394
  clip-path: polygon(63.8% 0%, 100% 0%, 100% 100%, 60.1% 100%);
395
  }
396
 
397
  /* --- ROW 2 (BOTTOM) --- Top: 50.85% (Gap ~1.7%), Height: 49.15% */
398
+ .panel[data-pos="3"] { /* Bottom Left */
 
 
399
  top: 50.85%; left: 0; height: 49.15%; width: 100%;
400
  clip-path: polygon(0% 0%, 32.4% 0%, 35.1% 100%, 0% 100%);
401
  }
402
+ .panel[data-pos="4"] { /* Bottom Middle */
 
403
  top: 50.85%; left: 0; height: 49.15%; width: 100%;
404
  clip-path: polygon(33.6% 0%, 64.2% 0%, 67.6% 100%, 36.3% 100%);
405
  }
406
+ .panel[data-pos="5"] { /* Bottom Right */
 
407
  top: 50.85%; left: 0; height: 49.15%; width: 100%;
408
  clip-path: polygon(65.4% 0%, 100% 0%, 100% 100%, 68.8% 100%);
409
  }
410
 
411
  /* =====================================================
412
+ PANEL IMAGES — STRICT RULES
413
  ===================================================== */
414
  .panel img {
415
  width: 100%;
416
  height: 100%;
417
+ object-fit: contain; /* 🔒 100% FIT, NO CROP */
418
+ transform: none !important; /* 🔒 NO ZOOM, NO SCALE */
419
  display: block;
420
+ background: #fff; /* White bars if aspect ratio mismatch */
421
  image-rendering: auto;
422
  }
423
 
424
  /* =====================================================
425
+ SPEECH BUBBLES
426
  ===================================================== */
427
+ .speech-bubble {
428
  position: absolute;
429
  max-width: 260px;
430
  background: #4ECDC4;
431
  color: white;
 
432
  border-radius: 18px;
433
  padding: 10px 14px;
434
  font-size: 14px;
 
439
  font-weight: bold;
440
  text-align: center;
441
  }
442
+ .speech-bubble:after {
443
+ content: ""; position: absolute; bottom: -10px; left: 20px;
444
+ border-width: 10px 10px 0; border-style: solid; border-color: #4ECDC4 transparent;
445
+ }
446
 
447
  /* =====================================================
448
+ UI
449
  ===================================================== */
450
+ .toolbar { width: 1000px; margin: 0 auto 20px auto; display: flex; gap: 10px; justify-content: center; }
451
+ button { padding: 8px 16px; border: 1px solid #ddd; background: #fff; cursor: pointer; font-weight: bold; border-radius: 4px; }
452
+ button:hover { background: #eee; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
 
454
+ #upload-container { display: flex; flex-direction:column; justify-content: center; align-items: center; min-height: 100vh; }
455
  .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; }
 
 
 
456
  .loader { width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 20px auto; }
457
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
 
458
  </style>
459
  </head>
460
 
461
  <body>
462
 
 
463
  <div id="upload-container">
464
  <div class="upload-box">
465
  <h1>🎬 CineComic Editor</h1>
466
+ <input type="file" id="file-upload" style="margin-bottom:10px;">
467
+ <br>
468
+ <label>Pages: <input type="number" id="page-count" value="4" min="1" max="15" style="width:50px;"></label>
469
+ <br><br>
470
+ <button onclick="upload()">🚀 Generate</button>
 
 
 
 
 
 
471
  <div id="loading-view" style="display:none; margin-top:20px;">
472
  <div class="loader"></div>
473
+ <p id="status-text">Starting...</p>
 
 
 
 
 
474
  </div>
475
  </div>
476
  </div>
477
 
 
478
  <div id="workspace" style="display:none;">
479
  <div class="toolbar">
 
480
  <button onclick="exportComic()">📥 Export PNG</button>
481
+ <button onclick="location.reload()">🏠 Reset</button>
482
  </div>
 
483
  <div id="comic-container"></div>
484
  </div>
485
 
486
  <script>
487
+ // --- LOGIC ---
 
 
488
  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);}); }
489
+ let sid = genUUID();
 
490
  let interval;
491
 
 
 
 
 
 
 
 
 
 
 
 
492
  async function upload() {
493
  const f = document.getElementById('file-upload').files[0];
494
  const pCount = document.getElementById('page-count').value;
495
  if(!f) return alert("Select a video");
496
 
 
497
  document.querySelector('.upload-box').style.display='none';
498
  document.getElementById('loading-view').style.display='block';
499
 
 
503
 
504
  const r = await fetch(`/uploader?sid=${sid}`, {method:'POST', body:fd});
505
  if(r.ok) interval = setInterval(checkStatus, 2000);
 
506
  }
507
 
508
  async function checkStatus() {
 
528
  const container = document.getElementById('comic-container');
529
  container.innerHTML = '';
530
 
 
 
 
531
  data.forEach((pageData, idx) => {
 
 
532
  const title = document.createElement('div');
533
  title.className = 'page-title';
534
  title.innerText = `Page ${idx + 1}`;
535
+ container.appendChild(title);
536
 
537
  const pageDiv = document.createElement('div');
538
  pageDiv.className = 'comic-page';
539
 
540
+ // Render 5 Panels
541
  pageData.panels.forEach((p, i) => {
542
+ if(i >= 5) return;
543
 
544
  const panelDiv = document.createElement('div');
545
  panelDiv.className = 'panel';
546
+ panelDiv.dataset.pos = i + 1; // 1-5 for CSS selection
547
 
548
  const img = document.createElement('img');
549
  img.src = `/frames/${p.image}?sid=${sid}`;
550
+ // STRICT NO-ZOOM RULE APPLIED VIA CSS
551
 
552
  panelDiv.appendChild(img);
553
 
554
+ // Bubbles
555
+ if(pageData.bubbles && pageData.bubbles[i] && pageData.bubbles[i].dialog) {
556
+ const b = document.createElement('div');
557
+ b.className = 'speech-bubble';
558
+ b.style.left = (pageData.bubbles[i].bubble_offset_x || 50) + 'px';
559
+ b.style.top = (pageData.bubbles[i].bubble_offset_y || 20) + 'px';
560
+ b.innerText = pageData.bubbles[i].dialog;
561
+ makeDraggable(b);
562
+ panelDiv.appendChild(b);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
  }
564
 
565
  pageDiv.appendChild(panelDiv);
566
  });
567
 
568
+ container.appendChild(pageDiv);
 
569
  });
570
  }
571
 
572
+ function makeDraggable(el) {
573
+ el.onmousedown = function(e) {
574
+ e.stopPropagation();
575
+ let shiftX = e.clientX - el.getBoundingClientRect().left;
576
+ let shiftY = e.clientY - el.getBoundingClientRect().top;
577
+
578
+ function moveAt(pageX, pageY) {
579
+ let parent = el.parentElement;
580
+ let rect = parent.getBoundingClientRect();
581
+ el.style.left = pageX - shiftX - rect.left + 'px';
582
+ el.style.top = pageY - shiftY - rect.top + 'px';
583
+ }
584
+
585
+ function onMouseMove(event) { moveAt(event.pageX, event.pageY); }
586
+ document.addEventListener('mousemove', onMouseMove);
587
+ el.onmouseup = function() {
588
+ document.removeEventListener('mousemove', onMouseMove);
589
+ el.onmouseup = null;
590
+ };
591
+ };
592
+ }
593
+
594
  async function exportComic() {
595
  const pgs = document.querySelectorAll('.comic-page');
596
  for(let i=0; i<pgs.length; i++) {
 
600
  } catch(e) { console.error(e); }
601
  }
602
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
  </script>
604
  </body>
605
  </html>
 
616
  if 'file' not in request.files or not request.files['file'].filename:
617
  return jsonify({'success': False, 'message': 'No file selected'}), 400
618
 
 
619
  target_pages = request.form.get('target_pages', 4)
 
620
  f = request.files['file']
621
  gen = EnhancedComicGenerator(sid)
622
  gen.cleanup()
623
  f.save(gen.video_path)
624
  gen.write_status("Starting...", 5)
625
 
 
626
  threading.Thread(target=gen.run, args=(target_pages,)).start()
627
  return jsonify({'success': True, 'message': 'Generation started.'})
628
 
 
643
  sid = request.args.get('sid')
644
  return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
645
 
646
+ @app.route('/load_comic/<code>')
647
+ def load_comic(code):
648
+ return jsonify({'success': False, 'message': 'Not implemented in this minimal version'})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
 
650
  @app.route('/save_comic', methods=['POST'])
651
  def save_comic():
652
+ return jsonify({'success': False, 'message': 'Not implemented in this minimal version'})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
 
654
  if __name__ == '__main__':
655
  try: gpu_warmup()