Update app_enhanced.py
Browse files- app_enhanced.py +43 -71
app_enhanced.py
CHANGED
|
@@ -1,14 +1,24 @@
|
|
| 1 |
import os
|
| 2 |
-
import webbrowser
|
| 3 |
import time
|
| 4 |
import threading
|
| 5 |
import uuid
|
| 6 |
import shutil
|
| 7 |
import json
|
| 8 |
import traceback
|
| 9 |
-
from typing import List
|
| 10 |
from concurrent.futures import ThreadPoolExecutor
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
# --- ROBUST IMPORTS WITH FALLBACKS ---
|
| 14 |
try:
|
|
@@ -25,8 +35,8 @@ try:
|
|
| 25 |
except Exception as e:
|
| 26 |
print(f"⚠️ Could not load SimpleColorEnhancer: {e}. This feature will be SKIPPED.")
|
| 27 |
class SimpleColorEnhancer:
|
| 28 |
-
def enhance_batch(self, *args, **kwargs):
|
| 29 |
-
def enhance_single(self, *args, **kwargs):
|
| 30 |
|
| 31 |
try:
|
| 32 |
from backend.quality_color_enhancer import QualityColorEnhancer
|
|
@@ -34,8 +44,8 @@ try:
|
|
| 34 |
except Exception as e:
|
| 35 |
print(f"⚠️ Could not load QualityColorEnhancer: {e}. This feature will be SKIPPED.")
|
| 36 |
class QualityColorEnhancer:
|
| 37 |
-
def batch_enhance(self, *args, **kwargs):
|
| 38 |
-
def enhance_single(self, *args, **kwargs):
|
| 39 |
|
| 40 |
try:
|
| 41 |
from backend.class_def import bubble, panel, Page
|
|
@@ -60,11 +70,10 @@ except Exception as e:
|
|
| 60 |
|
| 61 |
# --- FLASK APP SETUP ---
|
| 62 |
app = Flask(__name__)
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
BASE_USER_DIR = "userdata"
|
| 66 |
|
| 67 |
-
# --- MERGED HTML: UPLOAD UI + EDITOR UI ---
|
| 68 |
INDEX_HTML = '''
|
| 69 |
<!DOCTYPE html>
|
| 70 |
<html lang="en">
|
|
@@ -72,7 +81,7 @@ INDEX_HTML = '''
|
|
| 72 |
<meta charset="UTF-8">
|
| 73 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 74 |
<title>Movie to Comic Generator</title>
|
| 75 |
-
<!-- EXPORT LIBRARY -->
|
| 76 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
|
| 77 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 78 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
@@ -87,31 +96,20 @@ INDEX_HTML = '''
|
|
| 87 |
min-height: 100vh;
|
| 88 |
}
|
| 89 |
|
| 90 |
-
/* Centered Container for Upload */
|
| 91 |
#upload-container {
|
| 92 |
-
display: flex;
|
| 93 |
-
|
| 94 |
-
align-items: center;
|
| 95 |
-
min-height: 100vh;
|
| 96 |
-
width: 100%;
|
| 97 |
}
|
| 98 |
|
| 99 |
.upload-box {
|
| 100 |
-
max-width: 500px;
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
background-color: #ffffff;
|
| 104 |
-
border-radius: 12px;
|
| 105 |
-
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
| 106 |
-
text-align: center;
|
| 107 |
}
|
| 108 |
|
| 109 |
-
/* Editor Container (Hidden initially) */
|
| 110 |
#editor-container {
|
| 111 |
display: none; /* Hidden by default */
|
| 112 |
-
padding: 20px;
|
| 113 |
-
width: 100%;
|
| 114 |
-
box-sizing: border-box;
|
| 115 |
}
|
| 116 |
|
| 117 |
h1 { color: #2c3e50; margin-bottom: 30px; font-weight: 600; }
|
|
@@ -130,14 +128,12 @@ INDEX_HTML = '''
|
|
| 130 |
}
|
| 131 |
.submit-btn:hover { background-color: #d35400; }
|
| 132 |
|
| 133 |
-
/* Loader */
|
| 134 |
.loading-view { display: none; flex-direction: column; align-items: center; justify-content: center; }
|
| 135 |
.loader {
|
| 136 |
width: 120px; height: 20px; border-radius: 20px;
|
| 137 |
-
background:
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
radial-gradient(circle 10px, #e67e22 100%, transparent 0);
|
| 141 |
background-size: 20px 20px; background-position: 0px 50%, 50px 50%, 100px 50%;
|
| 142 |
background-repeat: no-repeat; animation:-ball 2s infinite linear;
|
| 143 |
}
|
|
@@ -174,7 +170,7 @@ INDEX_HTML = '''
|
|
| 174 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; }
|
| 175 |
.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; }
|
| 176 |
|
| 177 |
-
/* <<<
|
| 178 |
.speech-bubble.speech {
|
| 179 |
--b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
|
| 180 |
--c: var(--bubble-fill-color, #4ECDC4);
|
|
@@ -186,6 +182,7 @@ INDEX_HTML = '''
|
|
| 186 |
background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
|
| 187 |
border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
|
| 188 |
}
|
|
|
|
| 189 |
.speech-bubble.speech.tail-bottom:before { top: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
|
| 190 |
.speech-bubble.speech.tail-top { 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); }
|
| 191 |
.speech-bubble.speech.tail-top:before { bottom: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: scaleY(-1); }
|
|
@@ -333,7 +330,7 @@ INDEX_HTML = '''
|
|
| 333 |
// SWITCH TO EDITOR VIEW
|
| 334 |
document.getElementById('upload-container').style.display = 'none';
|
| 335 |
document.getElementById('editor-container').style.display = 'block';
|
| 336 |
-
loadComicData();
|
| 337 |
} else if (data.progress < 0) {
|
| 338 |
clearInterval(statusInterval);
|
| 339 |
statusText.textContent = "An error occurred. Check server logs.";
|
|
@@ -447,7 +444,6 @@ INDEX_HTML = '''
|
|
| 447 |
document.getElementById('zoom-slider').addEventListener('input', handleZoom);
|
| 448 |
document.getElementById('tail-slider').addEventListener('input', (e) => slideTail(e.target.value));
|
| 449 |
|
| 450 |
-
// Color pickers
|
| 451 |
document.getElementById('bubble-text-color').addEventListener('input', (e) => {
|
| 452 |
if(currentlySelectedBubble) currentlySelectedBubble.style.setProperty('--bubble-text-color', e.target.value);
|
| 453 |
});
|
|
@@ -476,13 +472,12 @@ INDEX_HTML = '''
|
|
| 476 |
panel.classList.add('selected');
|
| 477 |
currentlySelectedPanel = panel;
|
| 478 |
selectBubble(null);
|
| 479 |
-
resetPanelTransform();
|
| 480 |
}
|
| 481 |
|
| 482 |
function selectBubble(bubble) {
|
| 483 |
if(currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
|
| 484 |
currentlySelectedBubble = bubble;
|
| 485 |
-
const controls = ['bubble-text-color', 'bubble-fill-color', 'bubble-type-select', 'zoom-slider'];
|
| 486 |
const tailControls = document.getElementById('tail-controls');
|
| 487 |
|
| 488 |
if(bubble) {
|
|
@@ -495,13 +490,14 @@ INDEX_HTML = '''
|
|
| 495 |
if(bubble.dataset.type === 'speech') tailControls.style.display = 'block';
|
| 496 |
else tailControls.style.display = 'none';
|
| 497 |
} else {
|
| 498 |
-
|
|
|
|
|
|
|
| 499 |
document.getElementById('zoom-slider').disabled = false;
|
| 500 |
tailControls.style.display = 'none';
|
| 501 |
}
|
| 502 |
}
|
| 503 |
|
| 504 |
-
// --- Drag/Resize/Tail Logic (Same as before) ---
|
| 505 |
function startDrag(e) {
|
| 506 |
draggedBubble = e.target.closest('.speech-bubble');
|
| 507 |
selectBubble(draggedBubble);
|
|
@@ -573,7 +569,6 @@ INDEX_HTML = '''
|
|
| 573 |
if(currentlySelectedBubble) { currentlySelectedBubble.remove(); selectBubble(null); }
|
| 574 |
}
|
| 575 |
|
| 576 |
-
// --- Panel Image Manipulation ---
|
| 577 |
function startPan(e) {
|
| 578 |
if(e.button !== 0) return;
|
| 579 |
const img = e.target;
|
|
@@ -617,7 +612,6 @@ INDEX_HTML = '''
|
|
| 617 |
updateImageTransform(img);
|
| 618 |
}
|
| 619 |
|
| 620 |
-
// --- API Calls ---
|
| 621 |
function replacePanelImage() {
|
| 622 |
if (!currentlySelectedPanel) return alert("Select a panel first.");
|
| 623 |
const img = currentlySelectedPanel.querySelector('img');
|
|
@@ -695,7 +689,6 @@ INDEX_HTML = '''
|
|
| 695 |
|
| 696 |
class EnhancedComicGenerator:
|
| 697 |
def __init__(self, sid):
|
| 698 |
-
# --- PER-USER SESSION DIRECTORIES ---
|
| 699 |
self.sid = sid
|
| 700 |
self.user_dir = os.path.join(BASE_USER_DIR, sid)
|
| 701 |
self.video_path = os.path.join(self.user_dir, 'uploaded.mp4')
|
|
@@ -703,7 +696,6 @@ class EnhancedComicGenerator:
|
|
| 703 |
self.output_dir = os.path.join(self.user_dir, 'output')
|
| 704 |
self.status_file = os.path.join(self.output_dir, 'status.json')
|
| 705 |
|
| 706 |
-
# Create directories if they don't exist
|
| 707 |
os.makedirs(self.frames_dir, exist_ok=True)
|
| 708 |
os.makedirs(self.output_dir, exist_ok=True)
|
| 709 |
|
|
@@ -724,11 +716,9 @@ class EnhancedComicGenerator:
|
|
| 724 |
os.remove(os.path.join(self.frames_dir, file))
|
| 725 |
if os.path.isdir(self.output_dir):
|
| 726 |
for file in os.listdir(self.output_dir):
|
| 727 |
-
if file != 'status.json':
|
| 728 |
-
try:
|
| 729 |
-
os.remove(os.path.join(self.output_dir, file))
|
| 730 |
except: pass
|
| 731 |
-
# Clean temp srt for this user (could be better handled with tempfile)
|
| 732 |
srt_file = os.path.join(self.user_dir, 'subs.srt')
|
| 733 |
if os.path.exists(srt_file): os.remove(srt_file)
|
| 734 |
print(f"[{self.sid}] ✅ Cleanup complete.")
|
|
@@ -736,7 +726,6 @@ class EnhancedComicGenerator:
|
|
| 736 |
def regenerate_frame(self, frame_filename, direction):
|
| 737 |
try:
|
| 738 |
if not self.video_fps:
|
| 739 |
-
# Re-open video to get FPS if lost (unlikely but safe)
|
| 740 |
cap = cv2.VideoCapture(self.video_path)
|
| 741 |
self.video_fps = cap.get(cv2.CAP_PROP_FPS)
|
| 742 |
cap.release()
|
|
@@ -772,7 +761,6 @@ class EnhancedComicGenerator:
|
|
| 772 |
self._enhance_all_images(single_image_path=new_path)
|
| 773 |
self._enhance_quality_colors(single_image_path=new_path)
|
| 774 |
|
| 775 |
-
# Update metadata
|
| 776 |
if isinstance(frame_to_time[frame_filename], dict):
|
| 777 |
frame_to_time[frame_filename]['time'] = target_time
|
| 778 |
else:
|
|
@@ -871,17 +859,13 @@ class EnhancedComicGenerator:
|
|
| 871 |
cap.release()
|
| 872 |
|
| 873 |
self.update_status("Generating subtitles...", 10)
|
| 874 |
-
# IMPORTANT: Adapt get_real_subtitles to support output path if possible
|
| 875 |
-
# For now, assuming it generates 'test1.srt' in CWD, we move it
|
| 876 |
get_real_subtitles(self.video_path)
|
| 877 |
|
| 878 |
-
# Move the generated SRT to user folder if it exists in root
|
| 879 |
if os.path.exists('test1.srt'):
|
| 880 |
user_srt = os.path.join(self.user_dir, 'subs.srt')
|
| 881 |
shutil.move('test1.srt', user_srt)
|
| 882 |
else:
|
| 883 |
-
user_srt = os.path.join(self.user_dir, 'subs.srt')
|
| 884 |
-
# If get_real_subtitles is hardcoded, this part needs care in your backend
|
| 885 |
|
| 886 |
with open(user_srt, 'r', encoding='utf-8') as f:
|
| 887 |
all_subs = list(srt.parse(f.read()))
|
|
@@ -892,7 +876,7 @@ class EnhancedComicGenerator:
|
|
| 892 |
raise Exception("Keyframe extraction failed.")
|
| 893 |
|
| 894 |
self.update_status("Cropping black bars...", 45)
|
| 895 |
-
black_x, black_y, _, _ = black_bar_crop()
|
| 896 |
|
| 897 |
self.update_status("Enhancing images...", 50)
|
| 898 |
self._enhance_all_images()
|
|
@@ -950,7 +934,6 @@ class EnhancedComicGenerator:
|
|
| 950 |
bubble_x, bubble_y = ai_bubble_placer.place_bubble_ai(frame_path, (lip_x, lip_y))
|
| 951 |
return bubble(bubble_offset_x=bubble_x, bubble_offset_y=bubble_y, lip_x=lip_x, lip_y=lip_y, dialog=dialogue, emotion='normal')
|
| 952 |
except Exception as e:
|
| 953 |
-
# Fallback
|
| 954 |
return bubble(bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, dialog=dialogue, emotion='normal')
|
| 955 |
|
| 956 |
def _create_ai_bubbles_from_moments(self, black_x, black_y):
|
|
@@ -967,7 +950,6 @@ class EnhancedComicGenerator:
|
|
| 967 |
return bubbles
|
| 968 |
|
| 969 |
def _generate_pages(self, bubbles):
|
| 970 |
-
# Using simple fallback generation logic for stability
|
| 971 |
pages, frame_files = [], sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
|
| 972 |
num_pages = (len(frame_files) + 3) // 4
|
| 973 |
for i in range(num_pages):
|
|
@@ -993,10 +975,8 @@ class EnhancedComicGenerator:
|
|
| 993 |
|
| 994 |
def _copy_template_files(self):
|
| 995 |
try:
|
| 996 |
-
# We don't strictly need to write page.html anymore for the main flow,
|
| 997 |
-
# but it is good for debugging or direct /comic access.
|
| 998 |
with open(os.path.join(self.output_dir, 'page.html'), 'w', encoding='utf-8') as f:
|
| 999 |
-
f.write(INDEX_HTML)
|
| 1000 |
except Exception as e:
|
| 1001 |
print(f"[{self.sid}] Template creation failed: {e}")
|
| 1002 |
|
|
@@ -1010,11 +990,7 @@ def index():
|
|
| 1010 |
|
| 1011 |
@app.route('/uploader', methods=['POST'])
|
| 1012 |
def upload_file():
|
| 1013 |
-
|
| 1014 |
-
if 'sid' not in session:
|
| 1015 |
-
session['sid'] = uuid.uuid4().hex
|
| 1016 |
-
print(f"⚠️ Session recreated: {session['sid']}")
|
| 1017 |
-
|
| 1018 |
try:
|
| 1019 |
f = request.files['file']
|
| 1020 |
gen = EnhancedComicGenerator(session['sid'])
|
|
@@ -1036,8 +1012,6 @@ def status():
|
|
| 1036 |
@app.route('/comic')
|
| 1037 |
def view_comic():
|
| 1038 |
if 'sid' not in session: return "Session expired", 400
|
| 1039 |
-
# For /comic, we can serve the same INDEX_HTML but maybe trigger a load immediately?
|
| 1040 |
-
# Or just serve the page.html if generated.
|
| 1041 |
return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'output'), 'page.html')
|
| 1042 |
|
| 1043 |
@app.route('/output/<path:filename>')
|
|
@@ -1050,7 +1024,6 @@ def frame_file(filename):
|
|
| 1050 |
if 'sid' not in session: return "Session expired", 400
|
| 1051 |
return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'frames'), filename)
|
| 1052 |
|
| 1053 |
-
# Actions that modify state need the session ID to find the correct folders
|
| 1054 |
@app.route('/regenerate_frame', methods=['POST'])
|
| 1055 |
def regenerate_frame_route():
|
| 1056 |
if 'sid' not in session: return jsonify({'success': False, 'message': 'Session expired'})
|
|
@@ -1076,7 +1049,6 @@ def goto_timestamp_route():
|
|
| 1076 |
return jsonify(gen.get_frame_at_timestamp(data['filename'], float(data['timestamp'])))
|
| 1077 |
|
| 1078 |
if __name__ == '__main__':
|
| 1079 |
-
# Ensure base userdata dir exists
|
| 1080 |
os.makedirs(BASE_USER_DIR, exist_ok=True)
|
| 1081 |
port = int(os.getenv("PORT", 7860))
|
| 1082 |
print(f"🚀 Starting Multi-User Comic Generator on port {port}")
|
|
|
|
| 1 |
import os
|
|
|
|
| 2 |
import time
|
| 3 |
import threading
|
| 4 |
import uuid
|
| 5 |
import shutil
|
| 6 |
import json
|
| 7 |
import traceback
|
|
|
|
| 8 |
from concurrent.futures import ThreadPoolExecutor
|
| 9 |
+
|
| 10 |
+
# --- ESSENTIAL IMPORTS ---
|
| 11 |
+
# These must be at the top. If they fail, add them to requirements.txt
|
| 12 |
+
try:
|
| 13 |
+
import cv2
|
| 14 |
+
import numpy as np
|
| 15 |
+
from PIL import Image
|
| 16 |
+
import srt
|
| 17 |
+
from flask import Flask, render_template, request, jsonify, send_from_directory, send_file, session
|
| 18 |
+
except ImportError as e:
|
| 19 |
+
print(f"❌ Critical Import Error: {e}")
|
| 20 |
+
print("Please ensure 'flask', 'opencv-python-headless', 'numpy', 'pillow', and 'srt' are in requirements.txt")
|
| 21 |
+
exit(1)
|
| 22 |
|
| 23 |
# --- ROBUST IMPORTS WITH FALLBACKS ---
|
| 24 |
try:
|
|
|
|
| 35 |
except Exception as e:
|
| 36 |
print(f"⚠️ Could not load SimpleColorEnhancer: {e}. This feature will be SKIPPED.")
|
| 37 |
class SimpleColorEnhancer:
|
| 38 |
+
def enhance_batch(self, *args, **kwargs): pass
|
| 39 |
+
def enhance_single(self, *args, **kwargs): pass
|
| 40 |
|
| 41 |
try:
|
| 42 |
from backend.quality_color_enhancer import QualityColorEnhancer
|
|
|
|
| 44 |
except Exception as e:
|
| 45 |
print(f"⚠️ Could not load QualityColorEnhancer: {e}. This feature will be SKIPPED.")
|
| 46 |
class QualityColorEnhancer:
|
| 47 |
+
def batch_enhance(self, *args, **kwargs): pass
|
| 48 |
+
def enhance_single(self, *args, **kwargs): pass
|
| 49 |
|
| 50 |
try:
|
| 51 |
from backend.class_def import bubble, panel, Page
|
|
|
|
| 70 |
|
| 71 |
# --- FLASK APP SETUP ---
|
| 72 |
app = Flask(__name__)
|
| 73 |
+
app.secret_key = "HUGGINGFACE_SECRET_KEY_XYZ" # Necessary for sessions
|
| 74 |
+
BASE_USER_DIR = "userdata" # Persistent storage location
|
|
|
|
| 75 |
|
| 76 |
+
# --- MERGED HTML: UPLOAD UI + EDITOR UI + EXPORT FIX + CSS FIX ---
|
| 77 |
INDEX_HTML = '''
|
| 78 |
<!DOCTYPE html>
|
| 79 |
<html lang="en">
|
|
|
|
| 81 |
<meta charset="UTF-8">
|
| 82 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 83 |
<title>Movie to Comic Generator</title>
|
| 84 |
+
<!-- EXPORT LIBRARY (Supports CSS Masks/Gradients) -->
|
| 85 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
|
| 86 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 87 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
|
|
| 96 |
min-height: 100vh;
|
| 97 |
}
|
| 98 |
|
|
|
|
| 99 |
#upload-container {
|
| 100 |
+
display: flex; justify-content: center; align-items: center;
|
| 101 |
+
min-height: 100vh; width: 100%;
|
|
|
|
|
|
|
|
|
|
| 102 |
}
|
| 103 |
|
| 104 |
.upload-box {
|
| 105 |
+
max-width: 500px; width: 100%; padding: 40px;
|
| 106 |
+
background-color: #ffffff; border-radius: 12px;
|
| 107 |
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); text-align: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
}
|
| 109 |
|
|
|
|
| 110 |
#editor-container {
|
| 111 |
display: none; /* Hidden by default */
|
| 112 |
+
padding: 20px; width: 100%; box-sizing: border-box;
|
|
|
|
|
|
|
| 113 |
}
|
| 114 |
|
| 115 |
h1 { color: #2c3e50; margin-bottom: 30px; font-weight: 600; }
|
|
|
|
| 128 |
}
|
| 129 |
.submit-btn:hover { background-color: #d35400; }
|
| 130 |
|
|
|
|
| 131 |
.loading-view { display: none; flex-direction: column; align-items: center; justify-content: center; }
|
| 132 |
.loader {
|
| 133 |
width: 120px; height: 20px; border-radius: 20px;
|
| 134 |
+
background: radial-gradient(circle 10px, #e67e22 100%, transparent 0),
|
| 135 |
+
radial-gradient(circle 10px, #e67e22 100%, transparent 0),
|
| 136 |
+
radial-gradient(circle 10px, #e67e22 100%, transparent 0);
|
|
|
|
| 137 |
background-size: 20px 20px; background-position: 0px 50%, 50px 50%, 100px 50%;
|
| 138 |
background-repeat: no-repeat; animation:-ball 2s infinite linear;
|
| 139 |
}
|
|
|
|
| 170 |
.speech-bubble.selected { outline: 2px dashed #4CAF50; }
|
| 171 |
.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; }
|
| 172 |
|
| 173 |
+
/* <<< EXACT CSS WITH GRADIENT EXPORT FIX >>> */
|
| 174 |
.speech-bubble.speech {
|
| 175 |
--b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
|
| 176 |
--c: var(--bubble-fill-color, #4ECDC4);
|
|
|
|
| 182 |
background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
|
| 183 |
border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
|
| 184 |
}
|
| 185 |
+
/* Directional Logic */
|
| 186 |
.speech-bubble.speech.tail-bottom:before { top: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
|
| 187 |
.speech-bubble.speech.tail-top { 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); }
|
| 188 |
.speech-bubble.speech.tail-top:before { bottom: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: scaleY(-1); }
|
|
|
|
| 330 |
// SWITCH TO EDITOR VIEW
|
| 331 |
document.getElementById('upload-container').style.display = 'none';
|
| 332 |
document.getElementById('editor-container').style.display = 'block';
|
| 333 |
+
loadComicData();
|
| 334 |
} else if (data.progress < 0) {
|
| 335 |
clearInterval(statusInterval);
|
| 336 |
statusText.textContent = "An error occurred. Check server logs.";
|
|
|
|
| 444 |
document.getElementById('zoom-slider').addEventListener('input', handleZoom);
|
| 445 |
document.getElementById('tail-slider').addEventListener('input', (e) => slideTail(e.target.value));
|
| 446 |
|
|
|
|
| 447 |
document.getElementById('bubble-text-color').addEventListener('input', (e) => {
|
| 448 |
if(currentlySelectedBubble) currentlySelectedBubble.style.setProperty('--bubble-text-color', e.target.value);
|
| 449 |
});
|
|
|
|
| 472 |
panel.classList.add('selected');
|
| 473 |
currentlySelectedPanel = panel;
|
| 474 |
selectBubble(null);
|
| 475 |
+
resetPanelTransform();
|
| 476 |
}
|
| 477 |
|
| 478 |
function selectBubble(bubble) {
|
| 479 |
if(currentlySelectedBubble) currentlySelectedBubble.classList.remove('selected');
|
| 480 |
currentlySelectedBubble = bubble;
|
|
|
|
| 481 |
const tailControls = document.getElementById('tail-controls');
|
| 482 |
|
| 483 |
if(bubble) {
|
|
|
|
| 490 |
if(bubble.dataset.type === 'speech') tailControls.style.display = 'block';
|
| 491 |
else tailControls.style.display = 'none';
|
| 492 |
} else {
|
| 493 |
+
if(document.getElementById('bubble-text-color')) document.getElementById('bubble-text-color').disabled = true;
|
| 494 |
+
if(document.getElementById('bubble-fill-color')) document.getElementById('bubble-fill-color').disabled = true;
|
| 495 |
+
if(document.getElementById('bubble-type-select')) document.getElementById('bubble-type-select').disabled = true;
|
| 496 |
document.getElementById('zoom-slider').disabled = false;
|
| 497 |
tailControls.style.display = 'none';
|
| 498 |
}
|
| 499 |
}
|
| 500 |
|
|
|
|
| 501 |
function startDrag(e) {
|
| 502 |
draggedBubble = e.target.closest('.speech-bubble');
|
| 503 |
selectBubble(draggedBubble);
|
|
|
|
| 569 |
if(currentlySelectedBubble) { currentlySelectedBubble.remove(); selectBubble(null); }
|
| 570 |
}
|
| 571 |
|
|
|
|
| 572 |
function startPan(e) {
|
| 573 |
if(e.button !== 0) return;
|
| 574 |
const img = e.target;
|
|
|
|
| 612 |
updateImageTransform(img);
|
| 613 |
}
|
| 614 |
|
|
|
|
| 615 |
function replacePanelImage() {
|
| 616 |
if (!currentlySelectedPanel) return alert("Select a panel first.");
|
| 617 |
const img = currentlySelectedPanel.querySelector('img');
|
|
|
|
| 689 |
|
| 690 |
class EnhancedComicGenerator:
|
| 691 |
def __init__(self, sid):
|
|
|
|
| 692 |
self.sid = sid
|
| 693 |
self.user_dir = os.path.join(BASE_USER_DIR, sid)
|
| 694 |
self.video_path = os.path.join(self.user_dir, 'uploaded.mp4')
|
|
|
|
| 696 |
self.output_dir = os.path.join(self.user_dir, 'output')
|
| 697 |
self.status_file = os.path.join(self.output_dir, 'status.json')
|
| 698 |
|
|
|
|
| 699 |
os.makedirs(self.frames_dir, exist_ok=True)
|
| 700 |
os.makedirs(self.output_dir, exist_ok=True)
|
| 701 |
|
|
|
|
| 716 |
os.remove(os.path.join(self.frames_dir, file))
|
| 717 |
if os.path.isdir(self.output_dir):
|
| 718 |
for file in os.listdir(self.output_dir):
|
| 719 |
+
if file != 'status.json':
|
| 720 |
+
try: os.remove(os.path.join(self.output_dir, file))
|
|
|
|
| 721 |
except: pass
|
|
|
|
| 722 |
srt_file = os.path.join(self.user_dir, 'subs.srt')
|
| 723 |
if os.path.exists(srt_file): os.remove(srt_file)
|
| 724 |
print(f"[{self.sid}] ✅ Cleanup complete.")
|
|
|
|
| 726 |
def regenerate_frame(self, frame_filename, direction):
|
| 727 |
try:
|
| 728 |
if not self.video_fps:
|
|
|
|
| 729 |
cap = cv2.VideoCapture(self.video_path)
|
| 730 |
self.video_fps = cap.get(cv2.CAP_PROP_FPS)
|
| 731 |
cap.release()
|
|
|
|
| 761 |
self._enhance_all_images(single_image_path=new_path)
|
| 762 |
self._enhance_quality_colors(single_image_path=new_path)
|
| 763 |
|
|
|
|
| 764 |
if isinstance(frame_to_time[frame_filename], dict):
|
| 765 |
frame_to_time[frame_filename]['time'] = target_time
|
| 766 |
else:
|
|
|
|
| 859 |
cap.release()
|
| 860 |
|
| 861 |
self.update_status("Generating subtitles...", 10)
|
|
|
|
|
|
|
| 862 |
get_real_subtitles(self.video_path)
|
| 863 |
|
|
|
|
| 864 |
if os.path.exists('test1.srt'):
|
| 865 |
user_srt = os.path.join(self.user_dir, 'subs.srt')
|
| 866 |
shutil.move('test1.srt', user_srt)
|
| 867 |
else:
|
| 868 |
+
user_srt = os.path.join(self.user_dir, 'subs.srt')
|
|
|
|
| 869 |
|
| 870 |
with open(user_srt, 'r', encoding='utf-8') as f:
|
| 871 |
all_subs = list(srt.parse(f.read()))
|
|
|
|
| 876 |
raise Exception("Keyframe extraction failed.")
|
| 877 |
|
| 878 |
self.update_status("Cropping black bars...", 45)
|
| 879 |
+
black_x, black_y, _, _ = black_bar_crop()
|
| 880 |
|
| 881 |
self.update_status("Enhancing images...", 50)
|
| 882 |
self._enhance_all_images()
|
|
|
|
| 934 |
bubble_x, bubble_y = ai_bubble_placer.place_bubble_ai(frame_path, (lip_x, lip_y))
|
| 935 |
return bubble(bubble_offset_x=bubble_x, bubble_offset_y=bubble_y, lip_x=lip_x, lip_y=lip_y, dialog=dialogue, emotion='normal')
|
| 936 |
except Exception as e:
|
|
|
|
| 937 |
return bubble(bubble_offset_x=50, bubble_offset_y=20, lip_x=-1, lip_y=-1, dialog=dialogue, emotion='normal')
|
| 938 |
|
| 939 |
def _create_ai_bubbles_from_moments(self, black_x, black_y):
|
|
|
|
| 950 |
return bubbles
|
| 951 |
|
| 952 |
def _generate_pages(self, bubbles):
|
|
|
|
| 953 |
pages, frame_files = [], sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')])
|
| 954 |
num_pages = (len(frame_files) + 3) // 4
|
| 955 |
for i in range(num_pages):
|
|
|
|
| 975 |
|
| 976 |
def _copy_template_files(self):
|
| 977 |
try:
|
|
|
|
|
|
|
| 978 |
with open(os.path.join(self.output_dir, 'page.html'), 'w', encoding='utf-8') as f:
|
| 979 |
+
f.write(INDEX_HTML)
|
| 980 |
except Exception as e:
|
| 981 |
print(f"[{self.sid}] Template creation failed: {e}")
|
| 982 |
|
|
|
|
| 990 |
|
| 991 |
@app.route('/uploader', methods=['POST'])
|
| 992 |
def upload_file():
|
| 993 |
+
if 'sid' not in session: session['sid'] = uuid.uuid4().hex
|
|
|
|
|
|
|
|
|
|
|
|
|
| 994 |
try:
|
| 995 |
f = request.files['file']
|
| 996 |
gen = EnhancedComicGenerator(session['sid'])
|
|
|
|
| 1012 |
@app.route('/comic')
|
| 1013 |
def view_comic():
|
| 1014 |
if 'sid' not in session: return "Session expired", 400
|
|
|
|
|
|
|
| 1015 |
return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'output'), 'page.html')
|
| 1016 |
|
| 1017 |
@app.route('/output/<path:filename>')
|
|
|
|
| 1024 |
if 'sid' not in session: return "Session expired", 400
|
| 1025 |
return send_from_directory(os.path.join(BASE_USER_DIR, session['sid'], 'frames'), filename)
|
| 1026 |
|
|
|
|
| 1027 |
@app.route('/regenerate_frame', methods=['POST'])
|
| 1028 |
def regenerate_frame_route():
|
| 1029 |
if 'sid' not in session: return jsonify({'success': False, 'message': 'Session expired'})
|
|
|
|
| 1049 |
return jsonify(gen.get_frame_at_timestamp(data['filename'], float(data['timestamp'])))
|
| 1050 |
|
| 1051 |
if __name__ == '__main__':
|
|
|
|
| 1052 |
os.makedirs(BASE_USER_DIR, exist_ok=True)
|
| 1053 |
port = int(os.getenv("PORT", 7860))
|
| 1054 |
print(f"🚀 Starting Multi-User Comic Generator on port {port}")
|