Update app_enhanced.py
Browse files- app_enhanced.py +66 -41
app_enhanced.py
CHANGED
|
@@ -14,9 +14,6 @@ from typing import List
|
|
| 14 |
import traceback
|
| 15 |
|
| 16 |
# --- ROBUST IMPORTS WITH FALLBACKS ---
|
| 17 |
-
# This structure prevents the app from crashing if a module is missing.
|
| 18 |
-
# It will log a warning and skip the feature instead.
|
| 19 |
-
|
| 20 |
try:
|
| 21 |
from backend.keyframes.keyframes import black_bar_crop
|
| 22 |
print("✅ Black bar cropping module loaded.")
|
|
@@ -66,7 +63,6 @@ try:
|
|
| 66 |
except Exception as e:
|
| 67 |
print(f"⚠️ Could not load a core utility module: {e}")
|
| 68 |
|
| 69 |
-
# Import smart comic generation
|
| 70 |
try:
|
| 71 |
from backend.emotion_aware_comic import EmotionAwareComicGenerator
|
| 72 |
from backend.story_analyzer import SmartComicGenerator
|
|
@@ -76,7 +72,6 @@ except Exception as e:
|
|
| 76 |
SMART_COMIC_AVAILABLE = False
|
| 77 |
print(f"⚠️ Smart comic generation not available: {e}")
|
| 78 |
|
| 79 |
-
# Import panel extractor
|
| 80 |
try:
|
| 81 |
from backend.panel_extractor import PanelExtractor
|
| 82 |
PANEL_EXTRACTOR_AVAILABLE = True
|
|
@@ -85,7 +80,6 @@ except Exception as e:
|
|
| 85 |
PANEL_EXTRACTOR_AVAILABLE = False
|
| 86 |
print(f"⚠️ Panel extractor not available: {e}")
|
| 87 |
|
| 88 |
-
# Import smart story extractor
|
| 89 |
try:
|
| 90 |
from backend.smart_story_extractor import SmartStoryExtractor
|
| 91 |
STORY_EXTRACTOR_AVAILABLE = True
|
|
@@ -96,7 +90,6 @@ except Exception as e:
|
|
| 96 |
|
| 97 |
app = Flask(__name__)
|
| 98 |
|
| 99 |
-
# Import editor routes
|
| 100 |
try:
|
| 101 |
from comic_editor_server import add_editor_routes
|
| 102 |
add_editor_routes(app)
|
|
@@ -104,7 +97,6 @@ try:
|
|
| 104 |
except Exception as e:
|
| 105 |
print(f"⚠️ Could not load comic editor: {e}")
|
| 106 |
|
| 107 |
-
# Ensure directories exist
|
| 108 |
os.makedirs('video', exist_ok=True)
|
| 109 |
os.makedirs('frames/final', exist_ok=True)
|
| 110 |
os.makedirs('output', exist_ok=True)
|
|
@@ -116,6 +108,7 @@ class EnhancedComicGenerator:
|
|
| 116 |
self.frames_dir = 'frames/final'
|
| 117 |
self.output_dir = 'output'
|
| 118 |
self.apply_comic_style = False
|
|
|
|
| 119 |
|
| 120 |
def cleanup_generated(self):
|
| 121 |
"""Deletes all old files to ensure a fresh start."""
|
|
@@ -155,12 +148,14 @@ class EnhancedComicGenerator:
|
|
| 155 |
except:
|
| 156 |
return 'open'
|
| 157 |
|
| 158 |
-
def regenerate_frame(self, frame_filename):
|
| 159 |
"""
|
| 160 |
-
Regenerate frame by moving
|
| 161 |
-
Updates metadata so repeated clicks keep advancing.
|
| 162 |
"""
|
| 163 |
try:
|
|
|
|
|
|
|
|
|
|
| 164 |
metadata_path = 'frames/frame_metadata.json'
|
| 165 |
if not os.path.exists(metadata_path):
|
| 166 |
return {"success": False, "message": "Frame metadata missing."}
|
|
@@ -170,13 +165,25 @@ class EnhancedComicGenerator:
|
|
| 170 |
|
| 171 |
if frame_filename not in frame_to_time:
|
| 172 |
return {"success": False, "message": "Panel not linked to original video."}
|
| 173 |
-
|
| 174 |
if isinstance(frame_to_time[frame_filename], dict):
|
| 175 |
current_time = frame_to_time[frame_filename]['time']
|
| 176 |
else:
|
| 177 |
current_time = frame_to_time[frame_filename]
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
cap = cv2.VideoCapture(self.video_path)
|
| 182 |
if not cap.isOpened():
|
|
@@ -187,24 +194,27 @@ class EnhancedComicGenerator:
|
|
| 187 |
cap.release()
|
| 188 |
|
| 189 |
if not ret or frame is None:
|
| 190 |
-
return {"success": False, "message": "No
|
| 191 |
|
|
|
|
| 192 |
new_path = os.path.join(self.frames_dir, frame_filename)
|
| 193 |
cv2.imwrite(new_path, frame)
|
| 194 |
|
|
|
|
| 195 |
if isinstance(frame_to_time[frame_filename], dict):
|
| 196 |
frame_to_time[frame_filename]['time'] = target_time
|
| 197 |
else:
|
| 198 |
frame_to_time[frame_filename] = target_time
|
| 199 |
-
|
| 200 |
with open(metadata_path, 'w') as f:
|
| 201 |
json.dump(frame_to_time, f, indent=2)
|
| 202 |
|
| 203 |
-
|
|
|
|
| 204 |
|
| 205 |
return {
|
| 206 |
"success": True,
|
| 207 |
-
"message":
|
| 208 |
"new_filename": frame_filename
|
| 209 |
}
|
| 210 |
|
|
@@ -222,11 +232,8 @@ class EnhancedComicGenerator:
|
|
| 222 |
print("❌ Cannot open video for keyframe extraction")
|
| 223 |
return False
|
| 224 |
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
print("⚠️ Video FPS is 0, defaulting to 25. Keyframe extraction might be inaccurate.")
|
| 228 |
-
fps = 25
|
| 229 |
-
|
| 230 |
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 231 |
duration = total_frames / fps
|
| 232 |
|
|
@@ -300,6 +307,17 @@ class EnhancedComicGenerator:
|
|
| 300 |
self.cleanup_generated()
|
| 301 |
print("🎬 Starting Enhanced Comic Generation...")
|
| 302 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
print("📝 Generating subtitles...")
|
| 304 |
get_real_subtitles(self.video_path)
|
| 305 |
all_subs = []
|
|
@@ -518,6 +536,7 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 518 |
.edit-controls .reset-button { background-color: #e74c3c; }
|
| 519 |
.edit-controls .action-button { background-color: #4CAF50; }
|
| 520 |
.edit-controls .secondary-button { background-color: #f39c12; }
|
|
|
|
| 521 |
</style>
|
| 522 |
</head>
|
| 523 |
<body>
|
|
@@ -537,11 +556,17 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 537 |
</div>
|
| 538 |
<div class="control-group">
|
| 539 |
<button onclick="replacePanelImage()" class="action-button">🖼️ Replace Panel Image</button>
|
| 540 |
-
<button onclick="regenerateFrame()" class="action-button">🔄 Regenerate Frame</button>
|
| 541 |
-
<button onclick="exportPagesToPNG()" class="action-button" style="background-color: #2196F3;">🖨️ Export Pages to PNG</button>
|
| 542 |
</div>
|
| 543 |
<div class="control-group">
|
| 544 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
</div>
|
| 546 |
</div>
|
| 547 |
<script>
|
|
@@ -627,7 +652,7 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 627 |
bubbleDiv.appendChild(textSpan);
|
| 628 |
bubbleDiv.style.left = data.left;
|
| 629 |
bubbleDiv.style.top = data.top;
|
| 630 |
-
applyBubbleType(bubbleDiv, 'speech');
|
| 631 |
return bubbleDiv;
|
| 632 |
}
|
| 633 |
|
|
@@ -658,13 +683,13 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 658 |
if (!currentlySelectedBubble) return alert("Please select a bubble to rotate.");
|
| 659 |
const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
|
| 660 |
const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
|
| 661 |
-
if (!isFlippedH && !isFlippedV) {
|
| 662 |
currentlySelectedBubble.classList.add('flipped');
|
| 663 |
-
} else if (isFlippedH && !isFlippedV) {
|
| 664 |
currentlySelectedBubble.classList.add('flipped-vertical');
|
| 665 |
-
} else if (isFlippedH && isFlippedV) {
|
| 666 |
currentlySelectedBubble.classList.remove('flipped');
|
| 667 |
-
} else {
|
| 668 |
currentlySelectedBubble.classList.remove('flipped-vertical');
|
| 669 |
}
|
| 670 |
}
|
|
@@ -787,9 +812,9 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 787 |
uploader.click();
|
| 788 |
}
|
| 789 |
|
| 790 |
-
function
|
| 791 |
if (!currentlySelectedPanel) {
|
| 792 |
-
alert("Please select a panel first.");
|
| 793 |
return;
|
| 794 |
}
|
| 795 |
const img = currentlySelectedPanel.querySelector('img');
|
|
@@ -800,27 +825,24 @@ body { margin: 0; padding: 20px; background: #f0f0f0; font-family: Arial, sans-s
|
|
| 800 |
filename = filename.split('?')[0];
|
| 801 |
}
|
| 802 |
|
| 803 |
-
if (!confirm(`Regenerate frame "${filename}" with a better version?`)) {
|
| 804 |
-
return;
|
| 805 |
-
}
|
| 806 |
img.style.opacity = '0.5';
|
| 807 |
fetch('/regenerate_frame', {
|
| 808 |
method: 'POST',
|
| 809 |
headers: { 'Content-Type': 'application/json' },
|
| 810 |
-
body: JSON.stringify({ filename: filename })
|
| 811 |
})
|
| 812 |
.then(response => response.json())
|
| 813 |
.then(data => {
|
| 814 |
if (data.success) {
|
| 815 |
img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
|
| 816 |
-
|
| 817 |
} else {
|
| 818 |
alert('Error: ' + data.message);
|
| 819 |
}
|
| 820 |
img.style.opacity = '1';
|
| 821 |
})
|
| 822 |
.catch(error => {
|
| 823 |
-
alert('An error occurred during
|
| 824 |
img.style.opacity = '1';
|
| 825 |
});
|
| 826 |
}
|
|
@@ -896,8 +918,11 @@ def regenerate_frame_route():
|
|
| 896 |
try:
|
| 897 |
data = request.get_json()
|
| 898 |
filename = data.get('filename')
|
| 899 |
-
|
| 900 |
-
|
|
|
|
|
|
|
|
|
|
| 901 |
return jsonify(result)
|
| 902 |
except Exception as e:
|
| 903 |
traceback.print_exc()
|
|
|
|
| 14 |
import traceback
|
| 15 |
|
| 16 |
# --- ROBUST IMPORTS WITH FALLBACKS ---
|
|
|
|
|
|
|
|
|
|
| 17 |
try:
|
| 18 |
from backend.keyframes.keyframes import black_bar_crop
|
| 19 |
print("✅ Black bar cropping module loaded.")
|
|
|
|
| 63 |
except Exception as e:
|
| 64 |
print(f"⚠️ Could not load a core utility module: {e}")
|
| 65 |
|
|
|
|
| 66 |
try:
|
| 67 |
from backend.emotion_aware_comic import EmotionAwareComicGenerator
|
| 68 |
from backend.story_analyzer import SmartComicGenerator
|
|
|
|
| 72 |
SMART_COMIC_AVAILABLE = False
|
| 73 |
print(f"⚠️ Smart comic generation not available: {e}")
|
| 74 |
|
|
|
|
| 75 |
try:
|
| 76 |
from backend.panel_extractor import PanelExtractor
|
| 77 |
PANEL_EXTRACTOR_AVAILABLE = True
|
|
|
|
| 80 |
PANEL_EXTRACTOR_AVAILABLE = False
|
| 81 |
print(f"⚠️ Panel extractor not available: {e}")
|
| 82 |
|
|
|
|
| 83 |
try:
|
| 84 |
from backend.smart_story_extractor import SmartStoryExtractor
|
| 85 |
STORY_EXTRACTOR_AVAILABLE = True
|
|
|
|
| 90 |
|
| 91 |
app = Flask(__name__)
|
| 92 |
|
|
|
|
| 93 |
try:
|
| 94 |
from comic_editor_server import add_editor_routes
|
| 95 |
add_editor_routes(app)
|
|
|
|
| 97 |
except Exception as e:
|
| 98 |
print(f"⚠️ Could not load comic editor: {e}")
|
| 99 |
|
|
|
|
| 100 |
os.makedirs('video', exist_ok=True)
|
| 101 |
os.makedirs('frames/final', exist_ok=True)
|
| 102 |
os.makedirs('output', exist_ok=True)
|
|
|
|
| 108 |
self.frames_dir = 'frames/final'
|
| 109 |
self.output_dir = 'output'
|
| 110 |
self.apply_comic_style = False
|
| 111 |
+
self.video_fps = None # Will store the video's FPS
|
| 112 |
|
| 113 |
def cleanup_generated(self):
|
| 114 |
"""Deletes all old files to ensure a fresh start."""
|
|
|
|
| 148 |
except:
|
| 149 |
return 'open'
|
| 150 |
|
| 151 |
+
def regenerate_frame(self, frame_filename, direction):
|
| 152 |
"""
|
| 153 |
+
Regenerate a frame by moving one frame forward or backward in the video.
|
|
|
|
| 154 |
"""
|
| 155 |
try:
|
| 156 |
+
if not self.video_fps:
|
| 157 |
+
return {"success": False, "message": "Video FPS not found. Please regenerate the comic first."}
|
| 158 |
+
|
| 159 |
metadata_path = 'frames/frame_metadata.json'
|
| 160 |
if not os.path.exists(metadata_path):
|
| 161 |
return {"success": False, "message": "Frame metadata missing."}
|
|
|
|
| 165 |
|
| 166 |
if frame_filename not in frame_to_time:
|
| 167 |
return {"success": False, "message": "Panel not linked to original video."}
|
| 168 |
+
|
| 169 |
if isinstance(frame_to_time[frame_filename], dict):
|
| 170 |
current_time = frame_to_time[frame_filename]['time']
|
| 171 |
else:
|
| 172 |
current_time = frame_to_time[frame_filename]
|
| 173 |
+
|
| 174 |
+
# Calculate the duration of a single frame
|
| 175 |
+
frame_duration = 1.0 / self.video_fps
|
| 176 |
+
|
| 177 |
+
# Calculate the new target time based on direction
|
| 178 |
+
if direction == 'forward':
|
| 179 |
+
target_time = current_time + frame_duration
|
| 180 |
+
elif direction == 'backward':
|
| 181 |
+
target_time = current_time - frame_duration
|
| 182 |
+
else:
|
| 183 |
+
return {"success": False, "message": "Invalid direction specified."}
|
| 184 |
+
|
| 185 |
+
# Ensure the timestamp doesn't go below zero
|
| 186 |
+
target_time = max(0, target_time)
|
| 187 |
|
| 188 |
cap = cv2.VideoCapture(self.video_path)
|
| 189 |
if not cap.isOpened():
|
|
|
|
| 194 |
cap.release()
|
| 195 |
|
| 196 |
if not ret or frame is None:
|
| 197 |
+
return {"success": False, "message": f"No frame available at {target_time:.2f}s."}
|
| 198 |
|
| 199 |
+
# Overwrite the existing frame file
|
| 200 |
new_path = os.path.join(self.frames_dir, frame_filename)
|
| 201 |
cv2.imwrite(new_path, frame)
|
| 202 |
|
| 203 |
+
# Update metadata with the new exact time
|
| 204 |
if isinstance(frame_to_time[frame_filename], dict):
|
| 205 |
frame_to_time[frame_filename]['time'] = target_time
|
| 206 |
else:
|
| 207 |
frame_to_time[frame_filename] = target_time
|
| 208 |
+
|
| 209 |
with open(metadata_path, 'w') as f:
|
| 210 |
json.dump(frame_to_time, f, indent=2)
|
| 211 |
|
| 212 |
+
message = f"Adjusted {direction} to {target_time:.3f}s"
|
| 213 |
+
print(f"✅ {message}")
|
| 214 |
|
| 215 |
return {
|
| 216 |
"success": True,
|
| 217 |
+
"message": message,
|
| 218 |
"new_filename": frame_filename
|
| 219 |
}
|
| 220 |
|
|
|
|
| 232 |
print("❌ Cannot open video for keyframe extraction")
|
| 233 |
return False
|
| 234 |
|
| 235 |
+
# Use the stored FPS
|
| 236 |
+
fps = self.video_fps
|
|
|
|
|
|
|
|
|
|
| 237 |
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 238 |
duration = total_frames / fps
|
| 239 |
|
|
|
|
| 307 |
self.cleanup_generated()
|
| 308 |
print("🎬 Starting Enhanced Comic Generation...")
|
| 309 |
try:
|
| 310 |
+
cap = cv2.VideoCapture(self.video_path)
|
| 311 |
+
if not cap.isOpened():
|
| 312 |
+
print("❌ Cannot open video to get FPS.")
|
| 313 |
+
return False
|
| 314 |
+
self.video_fps = cap.get(cv2.CAP_PROP_FPS)
|
| 315 |
+
if self.video_fps == 0:
|
| 316 |
+
print("⚠️ Video FPS is 0, defaulting to 25.")
|
| 317 |
+
self.video_fps = 25
|
| 318 |
+
cap.release()
|
| 319 |
+
print(f"✅ Video FPS detected: {self.video_fps:.2f}")
|
| 320 |
+
|
| 321 |
print("📝 Generating subtitles...")
|
| 322 |
get_real_subtitles(self.video_path)
|
| 323 |
all_subs = []
|
|
|
|
| 536 |
.edit-controls .reset-button { background-color: #e74c3c; }
|
| 537 |
.edit-controls .action-button { background-color: #4CAF50; }
|
| 538 |
.edit-controls .secondary-button { background-color: #f39c12; }
|
| 539 |
+
.button-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
|
| 540 |
</style>
|
| 541 |
</head>
|
| 542 |
<body>
|
|
|
|
| 556 |
</div>
|
| 557 |
<div class="control-group">
|
| 558 |
<button onclick="replacePanelImage()" class="action-button">🖼️ Replace Panel Image</button>
|
|
|
|
|
|
|
| 559 |
</div>
|
| 560 |
<div class="control-group">
|
| 561 |
+
<label>Adjust Selected Panel Frame:</label>
|
| 562 |
+
<div class="button-grid">
|
| 563 |
+
<button onclick="adjustFrame('backward')" class="secondary-button">⬅️ Previous</button>
|
| 564 |
+
<button onclick="adjustFrame('forward')" class="action-button">Next ➡️</button>
|
| 565 |
+
</div>
|
| 566 |
+
</div>
|
| 567 |
+
<div class="control-group">
|
| 568 |
+
<button onclick="exportPagesToPNG()" class="action-button" style="background-color: #2196F3;">🖨️ Export Pages to PNG</button>
|
| 569 |
+
<button onclick="clearSavedState()" class="reset-button">🔄 Clear Edits & Reset</button>
|
| 570 |
</div>
|
| 571 |
</div>
|
| 572 |
<script>
|
|
|
|
| 652 |
bubbleDiv.appendChild(textSpan);
|
| 653 |
bubbleDiv.style.left = data.left;
|
| 654 |
bubbleDiv.style.top = data.top;
|
| 655 |
+
applyBubbleType(bubbleDiv, 'speech');
|
| 656 |
return bubbleDiv;
|
| 657 |
}
|
| 658 |
|
|
|
|
| 683 |
if (!currentlySelectedBubble) return alert("Please select a bubble to rotate.");
|
| 684 |
const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
|
| 685 |
const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
|
| 686 |
+
if (!isFlippedH && !isFlippedV) {
|
| 687 |
currentlySelectedBubble.classList.add('flipped');
|
| 688 |
+
} else if (isFlippedH && !isFlippedV) {
|
| 689 |
currentlySelectedBubble.classList.add('flipped-vertical');
|
| 690 |
+
} else if (isFlippedH && isFlippedV) {
|
| 691 |
currentlySelectedBubble.classList.remove('flipped');
|
| 692 |
+
} else {
|
| 693 |
currentlySelectedBubble.classList.remove('flipped-vertical');
|
| 694 |
}
|
| 695 |
}
|
|
|
|
| 812 |
uploader.click();
|
| 813 |
}
|
| 814 |
|
| 815 |
+
function adjustFrame(direction) {
|
| 816 |
if (!currentlySelectedPanel) {
|
| 817 |
+
alert("Please select a panel first to adjust its frame.");
|
| 818 |
return;
|
| 819 |
}
|
| 820 |
const img = currentlySelectedPanel.querySelector('img');
|
|
|
|
| 825 |
filename = filename.split('?')[0];
|
| 826 |
}
|
| 827 |
|
|
|
|
|
|
|
|
|
|
| 828 |
img.style.opacity = '0.5';
|
| 829 |
fetch('/regenerate_frame', {
|
| 830 |
method: 'POST',
|
| 831 |
headers: { 'Content-Type': 'application/json' },
|
| 832 |
+
body: JSON.stringify({ filename: filename, direction: direction })
|
| 833 |
})
|
| 834 |
.then(response => response.json())
|
| 835 |
.then(data => {
|
| 836 |
if (data.success) {
|
| 837 |
img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
|
| 838 |
+
console.log(data.message);
|
| 839 |
} else {
|
| 840 |
alert('Error: ' + data.message);
|
| 841 |
}
|
| 842 |
img.style.opacity = '1';
|
| 843 |
})
|
| 844 |
.catch(error => {
|
| 845 |
+
alert('An error occurred during frame adjustment.');
|
| 846 |
img.style.opacity = '1';
|
| 847 |
});
|
| 848 |
}
|
|
|
|
| 918 |
try:
|
| 919 |
data = request.get_json()
|
| 920 |
filename = data.get('filename')
|
| 921 |
+
direction = data.get('direction')
|
| 922 |
+
if not filename or not direction:
|
| 923 |
+
return jsonify({'success': False, 'message': 'Filename or direction missing.'})
|
| 924 |
+
|
| 925 |
+
result = comic_generator.regenerate_frame(filename, direction)
|
| 926 |
return jsonify(result)
|
| 927 |
except Exception as e:
|
| 928 |
traceback.print_exc()
|