Update app_enhanced.py
Browse files- app_enhanced.py +63 -48
app_enhanced.py
CHANGED
|
@@ -12,13 +12,10 @@ import json
|
|
| 12 |
import shutil
|
| 13 |
from typing import List
|
| 14 |
import traceback
|
| 15 |
-
# <<< MODIFICATION START: Import for parallel processing >>>
|
| 16 |
from concurrent.futures import ThreadPoolExecutor
|
| 17 |
-
# <<< MODIFICATION END >>>
|
| 18 |
|
| 19 |
|
| 20 |
# --- ROBUST IMPORTS WITH FALLBACKS ---
|
| 21 |
-
# (Assuming these modules have a method to process a single image, e.g., enhance_single)
|
| 22 |
try:
|
| 23 |
from backend.keyframes.keyframes import black_bar_crop
|
| 24 |
print("✅ Black bar cropping module loaded.")
|
|
@@ -416,9 +413,7 @@ class EnhancedComicGenerator:
|
|
| 416 |
with open('test1.srt', 'r', encoding='utf-8') as f:
|
| 417 |
all_subs = list(srt.parse(f.read()))
|
| 418 |
key_moments = [{'index': s.index, 'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in all_subs]
|
| 419 |
-
# <<< MODIFICATION START: Reduced default max_frames for speed >>>
|
| 420 |
if not self.generate_keyframes_from_moments(self.video_path, key_moments, max_frames=32):
|
| 421 |
-
# <<< MODIFICATION END >>>
|
| 422 |
raise Exception("Keyframe extraction failed.")
|
| 423 |
update_status("Cropping black bars...", 45)
|
| 424 |
black_x, black_y, _, _ = black_bar_crop()
|
|
@@ -441,7 +436,6 @@ class EnhancedComicGenerator:
|
|
| 441 |
update_status(f"Error: {e}", -1)
|
| 442 |
return False
|
| 443 |
|
| 444 |
-
# <<< MODIFICATION START: Parallelized enhancement functions for speed >>>
|
| 445 |
def _enhance_all_images(self, single_image_path=None):
|
| 446 |
try:
|
| 447 |
enhancer = SimpleColorEnhancer()
|
|
@@ -465,11 +459,8 @@ class EnhancedComicGenerator:
|
|
| 465 |
list(executor.map(enhancer.enhance_single, frame_paths))
|
| 466 |
except Exception as e:
|
| 467 |
print(f"⚠️ Quality enhancement failed: {e}")
|
| 468 |
-
# <<< MODIFICATION END >>>
|
| 469 |
|
| 470 |
-
# <<< MODIFICATION START: Parallelized bubble placement for speed >>>
|
| 471 |
def _process_bubble_for_frame(self, frame_file):
|
| 472 |
-
"""Helper function to process a single frame for bubble placement."""
|
| 473 |
frame_path = os.path.join(self.frames_dir, frame_file)
|
| 474 |
dialogue = self.frame_metadata.get(frame_file, {}).get('dialogue', "")
|
| 475 |
try:
|
|
@@ -491,11 +482,9 @@ class EnhancedComicGenerator:
|
|
| 491 |
self.frame_metadata = json.load(f)
|
| 492 |
|
| 493 |
with ThreadPoolExecutor() as executor:
|
| 494 |
-
# Map the processing function to each frame file and collect results
|
| 495 |
bubbles = list(executor.map(self._process_bubble_for_frame, frame_files))
|
| 496 |
|
| 497 |
return bubbles
|
| 498 |
-
# <<< MODIFICATION END >>>
|
| 499 |
|
| 500 |
def _generate_pages(self, bubbles):
|
| 501 |
try:
|
|
@@ -549,36 +538,67 @@ class EnhancedComicGenerator:
|
|
| 549 |
.panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; transition: transform 0.1s ease-out; }
|
| 550 |
.panel img.pannable { cursor: grab; }
|
| 551 |
.panel img.panning { cursor: grabbing; }
|
| 552 |
-
.speech-bubble { position: absolute; display: flex; justify-content: center; align-items: center; width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box;
|
| 553 |
-
.bubble-text { padding:
|
| 554 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; }
|
| 555 |
.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); font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102; }
|
| 556 |
|
| 557 |
-
/* <<< MODIFICATION START: New CSS for 'speech' bubble
|
| 558 |
.speech-bubble.speech {
|
| 559 |
-
/*
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
border-radius:
|
|
|
|
|
|
|
|
|
|
| 564 |
}
|
| 565 |
|
| 566 |
-
.speech-bubble.speech::
|
| 567 |
-
|
|
|
|
| 568 |
position: absolute;
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
}
|
| 576 |
-
|
| 577 |
-
/* 4-WAY CURVY TAIL POSITIONING */
|
| 578 |
-
.speech-bubble.speech.tail-bl::after { bottom: -11px; left: 30px; transform: rotate(45deg); border-radius: 0 0 12px 0; border-left: none; border-top: none;}
|
| 579 |
-
.speech-bubble.speech.tail-br::after { bottom: -11px; right: 30px; transform: rotate(135deg); border-radius: 0 0 12px 0; border-left: none; border-top: none;}
|
| 580 |
-
.speech-bubble.speech.tail-tr::after { top: -11px; right: 30px; transform: rotate(225deg); border-radius: 0 0 12px 0; border-left: none; border-top: none;}
|
| 581 |
-
.speech-bubble.speech.tail-tl::after { top: -11px; left: 30px; transform: rotate(315deg); border-radius: 0 0 12px 0; border-left: none; border-top: none;}
|
| 582 |
/* <<< MODIFICATION END >>> */
|
| 583 |
|
| 584 |
.speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
|
|
@@ -635,7 +655,7 @@ class EnhancedComicGenerator:
|
|
| 635 |
<div class="color-picker-grid">
|
| 636 |
<div>
|
| 637 |
<label for="bubble-text-color">Text</label>
|
| 638 |
-
<input type="color" id="bubble-text-color" value="#
|
| 639 |
</div>
|
| 640 |
<div>
|
| 641 |
<label for="bubble-fill-color">Fill</label>
|
|
@@ -730,17 +750,13 @@ class EnhancedComicGenerator:
|
|
| 730 |
document.getElementById('zoom-slider').addEventListener('input', handleZoom);
|
| 731 |
|
| 732 |
document.getElementById('bubble-text-color').addEventListener('input', (e) => {
|
| 733 |
-
if(currentlySelectedBubble) currentlySelectedBubble.style.color
|
| 734 |
});
|
| 735 |
-
// <<< MODIFICATION START: Re-enabled fill color picker for all bubble types >>>
|
| 736 |
document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
|
| 737 |
if(currentlySelectedBubble) {
|
| 738 |
-
// This now works for all bubbles, including the new 'speech' one.
|
| 739 |
-
// We use a CSS variable to color the bubble and its pseudo-element tail.
|
| 740 |
currentlySelectedBubble.style.setProperty('--bubble-fill-color', e.target.value);
|
| 741 |
}
|
| 742 |
});
|
| 743 |
-
// <<< MODIFICATION END >>>
|
| 744 |
|
| 745 |
document.addEventListener('mousemove', e => { if (isPanning) panImage(e); if (draggedBubble) drag(e); if(isResizing) resizeBubble(e); });
|
| 746 |
document.addEventListener('mouseup', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
|
|
@@ -773,7 +789,7 @@ class EnhancedComicGenerator:
|
|
| 773 |
}
|
| 774 |
|
| 775 |
const rgbToHex = (rgb) => {
|
| 776 |
-
if (!rgb || !rgb.startsWith('rgb')) return '#
|
| 777 |
let sep = rgb.indexOf(",") > -1 ? "," : " ";
|
| 778 |
rgb = rgb.substr(4).split(")")[0].split(sep);
|
| 779 |
let r = (+rgb[0]).toString(16), g = (+rgb[1]).toString(16), b = (+rgb[2]).toString(16);
|
|
@@ -848,15 +864,14 @@ class EnhancedComicGenerator:
|
|
| 848 |
currentlySelectedPanel = null;
|
| 849 |
|
| 850 |
const styles = window.getComputedStyle(currentlySelectedBubble);
|
| 851 |
-
document.getElementById('bubble-text-color')
|
| 852 |
-
|
| 853 |
-
|
|
|
|
| 854 |
const fillColorPicker = document.getElementById('bubble-fill-color');
|
| 855 |
-
fillColorPicker.disabled = false;
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
fillColorPicker.value = currentFill ? currentFill : rgbToHex(styles.backgroundColor);
|
| 859 |
-
// <<< MODIFICATION END >>>
|
| 860 |
|
| 861 |
document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
|
| 862 |
document.getElementById('font-select').value = styles.fontFamily.split(',')[0].replace(/"/g, "").replace(/'/g, "").trim();
|
|
|
|
| 12 |
import shutil
|
| 13 |
from typing import List
|
| 14 |
import traceback
|
|
|
|
| 15 |
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
# --- ROBUST IMPORTS WITH FALLBACKS ---
|
|
|
|
| 19 |
try:
|
| 20 |
from backend.keyframes.keyframes import black_bar_crop
|
| 21 |
print("✅ Black bar cropping module loaded.")
|
|
|
|
| 413 |
with open('test1.srt', 'r', encoding='utf-8') as f:
|
| 414 |
all_subs = list(srt.parse(f.read()))
|
| 415 |
key_moments = [{'index': s.index, 'text': s.content, 'start': s.start.total_seconds(), 'end': s.end.total_seconds()} for s in all_subs]
|
|
|
|
| 416 |
if not self.generate_keyframes_from_moments(self.video_path, key_moments, max_frames=32):
|
|
|
|
| 417 |
raise Exception("Keyframe extraction failed.")
|
| 418 |
update_status("Cropping black bars...", 45)
|
| 419 |
black_x, black_y, _, _ = black_bar_crop()
|
|
|
|
| 436 |
update_status(f"Error: {e}", -1)
|
| 437 |
return False
|
| 438 |
|
|
|
|
| 439 |
def _enhance_all_images(self, single_image_path=None):
|
| 440 |
try:
|
| 441 |
enhancer = SimpleColorEnhancer()
|
|
|
|
| 459 |
list(executor.map(enhancer.enhance_single, frame_paths))
|
| 460 |
except Exception as e:
|
| 461 |
print(f"⚠️ Quality enhancement failed: {e}")
|
|
|
|
| 462 |
|
|
|
|
| 463 |
def _process_bubble_for_frame(self, frame_file):
|
|
|
|
| 464 |
frame_path = os.path.join(self.frames_dir, frame_file)
|
| 465 |
dialogue = self.frame_metadata.get(frame_file, {}).get('dialogue', "")
|
| 466 |
try:
|
|
|
|
| 482 |
self.frame_metadata = json.load(f)
|
| 483 |
|
| 484 |
with ThreadPoolExecutor() as executor:
|
|
|
|
| 485 |
bubbles = list(executor.map(self._process_bubble_for_frame, frame_files))
|
| 486 |
|
| 487 |
return bubbles
|
|
|
|
| 488 |
|
| 489 |
def _generate_pages(self, bubbles):
|
| 490 |
try:
|
|
|
|
| 538 |
.panel img { width: 100%; height: 100%; object-fit: cover; object-position: center; transition: transform 0.1s ease-out; }
|
| 539 |
.panel img.pannable { cursor: grab; }
|
| 540 |
.panel img.panning { cursor: grabbing; }
|
| 541 |
+
.speech-bubble { position: absolute; display: flex; justify-content: center; align-items: center; width: 150px; height: 80px; min-width: 50px; min-height: 30px; box-sizing: border-box; z-index: 10; cursor: move; overflow: visible; font-size: 13px; font-weight: bold; text-align: center; font-family: 'Comic Neue', cursive; }
|
| 542 |
+
.bubble-text { padding: 1em; word-wrap: break-word; }
|
| 543 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; }
|
| 544 |
.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); font: inherit; text-align: center; resize: none; padding: 8px; z-index: 102; }
|
| 545 |
|
| 546 |
+
/* <<< MODIFICATION START: New Advanced CSS for 'speech' bubble >>> */
|
| 547 |
.speech-bubble.speech {
|
| 548 |
+
/* Main bubble properties */
|
| 549 |
+
color: var(--bubble-text-color, #333);
|
| 550 |
+
font-size: 16px;
|
| 551 |
+
padding: 0; /* Padding is handled by the bubble-text span now */
|
| 552 |
+
border-radius: 1.2em;
|
| 553 |
+
background: var(--bubble-fill-color, white);
|
| 554 |
+
box-shadow: 0 0 5px rgba(0,0,0,0.2);
|
| 555 |
+
border: none;
|
| 556 |
}
|
| 557 |
|
| 558 |
+
.speech-bubble.speech::before {
|
| 559 |
+
/* This pseudo-element creates the complex tail */
|
| 560 |
+
content: "";
|
| 561 |
position: absolute;
|
| 562 |
+
background: inherit; /* Inherits the dynamic color from the parent */
|
| 563 |
+
|
| 564 |
+
/* Tail dimensions */
|
| 565 |
+
width: 3em; /* base */
|
| 566 |
+
height: 1.8em; /* height */
|
| 567 |
+
|
| 568 |
+
/* The magic mask that carves the tail shape */
|
| 569 |
+
-webkit-mask: radial-gradient(calc(0.6 * 100%) 105% at 100% 0, #0000 99%, #000 101%);
|
| 570 |
+
mask: radial-gradient(calc(0.6 * 100%) 105% at 100% 0, #0000 99%, #000 101%);
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
/* 4-WAY TAIL ROTATION CLASSES */
|
| 574 |
+
.speech-bubble.speech.tail-bl::before { /* Bottom-Left */
|
| 575 |
+
bottom: -0.9em;
|
| 576 |
+
left: 1.5em;
|
| 577 |
+
transform-origin: bottom left;
|
| 578 |
+
transform: skewX(30deg) rotate(-10deg);
|
| 579 |
+
border-bottom-right-radius: 80%;
|
| 580 |
+
}
|
| 581 |
+
.speech-bubble.speech.tail-br::before { /* Bottom-Right */
|
| 582 |
+
bottom: -0.9em;
|
| 583 |
+
right: 1.5em;
|
| 584 |
+
transform-origin: bottom right;
|
| 585 |
+
transform: skewX(-30deg) rotate(10deg) scaleX(-1);
|
| 586 |
+
border-bottom-left-radius: 80%;
|
| 587 |
+
}
|
| 588 |
+
.speech-bubble.speech.tail-tr::before { /* Top-Right */
|
| 589 |
+
top: -0.9em;
|
| 590 |
+
right: 1.5em;
|
| 591 |
+
transform-origin: top right;
|
| 592 |
+
transform: skewX(30deg) rotate(10deg) scale(-1, -1);
|
| 593 |
+
border-top-left-radius: 80%;
|
| 594 |
+
}
|
| 595 |
+
.speech-bubble.speech.tail-tl::before { /* Top-Left */
|
| 596 |
+
top: -0.9em;
|
| 597 |
+
left: 1.5em;
|
| 598 |
+
transform-origin: top left;
|
| 599 |
+
transform: skewX(-30deg) rotate(-10deg) scaleY(-1);
|
| 600 |
+
border-top-right-radius: 80%;
|
| 601 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
/* <<< MODIFICATION END >>> */
|
| 603 |
|
| 604 |
.speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
|
|
|
|
| 655 |
<div class="color-picker-grid">
|
| 656 |
<div>
|
| 657 |
<label for="bubble-text-color">Text</label>
|
| 658 |
+
<input type="color" id="bubble-text-color" value="#333333" disabled>
|
| 659 |
</div>
|
| 660 |
<div>
|
| 661 |
<label for="bubble-fill-color">Fill</label>
|
|
|
|
| 750 |
document.getElementById('zoom-slider').addEventListener('input', handleZoom);
|
| 751 |
|
| 752 |
document.getElementById('bubble-text-color').addEventListener('input', (e) => {
|
| 753 |
+
if(currentlySelectedBubble) currentlySelectedBubble.style.setProperty('--bubble-text-color', e.target.value);
|
| 754 |
});
|
|
|
|
| 755 |
document.getElementById('bubble-fill-color').addEventListener('input', (e) => {
|
| 756 |
if(currentlySelectedBubble) {
|
|
|
|
|
|
|
| 757 |
currentlySelectedBubble.style.setProperty('--bubble-fill-color', e.target.value);
|
| 758 |
}
|
| 759 |
});
|
|
|
|
| 760 |
|
| 761 |
document.addEventListener('mousemove', e => { if (isPanning) panImage(e); if (draggedBubble) drag(e); if(isResizing) resizeBubble(e); });
|
| 762 |
document.addEventListener('mouseup', e => { if (isPanning) stopPan(e); if (draggedBubble) stopDrag(e); if(isResizing) stopResize(e);});
|
|
|
|
| 789 |
}
|
| 790 |
|
| 791 |
const rgbToHex = (rgb) => {
|
| 792 |
+
if (!rgb || !rgb.startsWith('rgb')) return '#ffffff';
|
| 793 |
let sep = rgb.indexOf(",") > -1 ? "," : " ";
|
| 794 |
rgb = rgb.substr(4).split(")")[0].split(sep);
|
| 795 |
let r = (+rgb[0]).toString(16), g = (+rgb[1]).toString(16), b = (+rgb[2]).toString(16);
|
|
|
|
| 864 |
currentlySelectedPanel = null;
|
| 865 |
|
| 866 |
const styles = window.getComputedStyle(currentlySelectedBubble);
|
| 867 |
+
const textColorPicker = document.getElementById('bubble-text-color');
|
| 868 |
+
const currentTextColor = styles.getPropertyValue('--bubble-text-color').trim();
|
| 869 |
+
textColorPicker.value = currentTextColor ? currentTextColor : rgbToHex(styles.color);
|
| 870 |
+
|
| 871 |
const fillColorPicker = document.getElementById('bubble-fill-color');
|
| 872 |
+
fillColorPicker.disabled = false;
|
| 873 |
+
const currentFillColor = styles.getPropertyValue('--bubble-fill-color').trim();
|
| 874 |
+
fillColorPicker.value = currentFillColor ? currentFillColor : rgbToHex(styles.backgroundColor);
|
|
|
|
|
|
|
| 875 |
|
| 876 |
document.getElementById('bubble-type-select').value = currentlySelectedBubble.dataset.type || 'speech';
|
| 877 |
document.getElementById('font-select').value = styles.fontFamily.split(',')[0].replace(/"/g, "").replace(/'/g, "").trim();
|