|
|
|
|
|
""" |
|
|
Comic Editor Server - Interactive bubble editing |
|
|
""" |
|
|
|
|
|
from flask import Flask, render_template, request, jsonify, send_from_directory |
|
|
import json |
|
|
import os |
|
|
from pathlib import Path |
|
|
|
|
|
app = Flask(__name__) |
|
|
|
|
|
|
|
|
COMIC_DATA_PATH = 'output/comic_data.json' |
|
|
FRAMES_DIR = 'frames/final' |
|
|
|
|
|
@app.route('/') |
|
|
def index(): |
|
|
"""Redirect to editor""" |
|
|
return render_template('comic_editor.html') |
|
|
|
|
|
@app.route('/editor') |
|
|
def editor(): |
|
|
"""Comic editor page""" |
|
|
return render_template('comic_editor.html') |
|
|
|
|
|
@app.route('/load_comic') |
|
|
def load_comic(): |
|
|
"""Load existing comic data""" |
|
|
try: |
|
|
|
|
|
if os.path.exists(COMIC_DATA_PATH): |
|
|
with open(COMIC_DATA_PATH, 'r') as f: |
|
|
data = json.load(f) |
|
|
return jsonify(data) |
|
|
else: |
|
|
|
|
|
data = generate_comic_data() |
|
|
return jsonify(data) |
|
|
except Exception as e: |
|
|
return jsonify({'error': str(e)}), 500 |
|
|
|
|
|
@app.route('/save_comic', methods=['POST']) |
|
|
def save_comic(): |
|
|
"""Save comic data""" |
|
|
try: |
|
|
data = request.json |
|
|
|
|
|
|
|
|
os.makedirs('output', exist_ok=True) |
|
|
|
|
|
|
|
|
with open(COMIC_DATA_PATH, 'w') as f: |
|
|
json.dump(data, f, indent=2) |
|
|
|
|
|
|
|
|
generate_static_html(data) |
|
|
|
|
|
return jsonify({'success': True, 'message': 'Comic saved successfully!'}) |
|
|
except Exception as e: |
|
|
return jsonify({'error': str(e)}), 500 |
|
|
|
|
|
@app.route('/frames/<path:filename>') |
|
|
def serve_frame(filename): |
|
|
"""Serve frame images""" |
|
|
return send_from_directory('frames/final', filename) |
|
|
|
|
|
@app.route('/export_comic') |
|
|
def export_comic(): |
|
|
"""Export comic as static HTML""" |
|
|
try: |
|
|
if os.path.exists(COMIC_DATA_PATH): |
|
|
with open(COMIC_DATA_PATH, 'r') as f: |
|
|
data = json.load(f) |
|
|
|
|
|
html = generate_static_html(data) |
|
|
return html, 200, {'Content-Type': 'text/html'} |
|
|
else: |
|
|
return "No comic data found", 404 |
|
|
except Exception as e: |
|
|
return str(e), 500 |
|
|
|
|
|
def generate_comic_data(): |
|
|
"""Generate comic data from existing frames and subtitles""" |
|
|
|
|
|
frames = sorted([f for f in os.listdir(FRAMES_DIR) if f.endswith('.png')]) |
|
|
|
|
|
|
|
|
subtitles = [] |
|
|
if os.path.exists('test1.srt'): |
|
|
import srt |
|
|
with open('test1.srt', 'r') as f: |
|
|
subtitles = list(srt.parse(f.read())) |
|
|
|
|
|
|
|
|
page_width = 800 |
|
|
page_height = 1080 |
|
|
panel_width = 380 |
|
|
panel_height = 280 |
|
|
padding = 10 |
|
|
|
|
|
pages = [] |
|
|
current_page = { |
|
|
'width': page_width, |
|
|
'height': page_height, |
|
|
'panels': [], |
|
|
'bubbles': [] |
|
|
} |
|
|
|
|
|
|
|
|
positions = [ |
|
|
(padding, padding), |
|
|
(page_width - panel_width - padding, padding), |
|
|
(padding, padding + panel_height + 20), |
|
|
(page_width - panel_width - padding, padding + panel_height + 20) |
|
|
] |
|
|
|
|
|
for i, frame in enumerate(frames[:16]): |
|
|
panel_index = i % 4 |
|
|
|
|
|
|
|
|
x, y = positions[panel_index] |
|
|
current_page['panels'].append({ |
|
|
'x': x, |
|
|
'y': y, |
|
|
'width': panel_width, |
|
|
'height': panel_height, |
|
|
'image': f'/frames/{frame}' |
|
|
}) |
|
|
|
|
|
|
|
|
if i < len(subtitles): |
|
|
text = subtitles[i].content.strip() |
|
|
else: |
|
|
text = f"Panel {i+1}" |
|
|
|
|
|
current_page['bubbles'].append({ |
|
|
'id': f'bubble_{i}', |
|
|
'x': x + 20, |
|
|
'y': y + 20, |
|
|
'width': 150, |
|
|
'height': 60, |
|
|
'text': text, |
|
|
'panelIndex': panel_index |
|
|
}) |
|
|
|
|
|
|
|
|
if panel_index == 3 and i < len(frames) - 1: |
|
|
pages.append(current_page) |
|
|
current_page = { |
|
|
'width': page_width, |
|
|
'height': page_height, |
|
|
'panels': [], |
|
|
'bubbles': [] |
|
|
} |
|
|
|
|
|
|
|
|
if current_page['panels']: |
|
|
pages.append(current_page) |
|
|
|
|
|
return {'pages': pages} |
|
|
|
|
|
def generate_static_html(data): |
|
|
"""Generate static HTML from comic data""" |
|
|
html = """ |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>My Comic</title> |
|
|
<style> |
|
|
body { |
|
|
margin: 0; |
|
|
padding: 20px; |
|
|
background: #f0f0f0; |
|
|
font-family: Arial, sans-serif; |
|
|
} |
|
|
.comic-page { |
|
|
position: relative; |
|
|
background: white; |
|
|
margin: 20px auto; |
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1); |
|
|
} |
|
|
.comic-panel { |
|
|
position: absolute; |
|
|
border: 2px solid #333; |
|
|
overflow: hidden; |
|
|
} |
|
|
.comic-panel img { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
object-fit: contain; |
|
|
background: #000; |
|
|
} |
|
|
.speech-bubble { |
|
|
position: absolute; |
|
|
background: white; |
|
|
border: 3px solid #333; |
|
|
border-radius: 20px; |
|
|
padding: 15px; |
|
|
font-family: 'Comic Sans MS', cursive; |
|
|
font-size: 14px; |
|
|
font-weight: bold; |
|
|
text-align: center; |
|
|
z-index: 10; |
|
|
} |
|
|
.bubble-tail { |
|
|
position: absolute; |
|
|
bottom: -15px; |
|
|
left: 20px; |
|
|
width: 0; |
|
|
height: 0; |
|
|
border-left: 15px solid transparent; |
|
|
border-right: 5px solid transparent; |
|
|
border-top: 20px solid #333; |
|
|
transform: rotate(-20deg); |
|
|
} |
|
|
.bubble-tail::after { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
bottom: 3px; |
|
|
left: -12px; |
|
|
width: 0; |
|
|
height: 0; |
|
|
border-left: 12px solid transparent; |
|
|
border-right: 4px solid transparent; |
|
|
border-top: 16px solid white; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
""" |
|
|
|
|
|
for page_idx, page in enumerate(data['pages']): |
|
|
html += f'<div class="comic-page" style="width:{page["width"]}px;height:{page["height"]}px;">\n' |
|
|
|
|
|
|
|
|
for panel in page['panels']: |
|
|
html += f'''<div class="comic-panel" style="left:{panel["x"]}px;top:{panel["y"]}px;width:{panel["width"]}px;height:{panel["height"]}px;"> |
|
|
<img src="{panel["image"]}"> |
|
|
</div>\n''' |
|
|
|
|
|
|
|
|
for bubble in page['bubbles']: |
|
|
html += f'''<div class="speech-bubble" style="left:{bubble["x"]}px;top:{bubble["y"]}px;width:{bubble["width"]}px;height:{bubble["height"]}px;"> |
|
|
{bubble["text"]} |
|
|
<div class="bubble-tail"></div> |
|
|
</div>\n''' |
|
|
|
|
|
html += '</div>\n' |
|
|
|
|
|
html += """ |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
|
|
|
with open('output/comic_static.html', 'w') as f: |
|
|
f.write(html) |
|
|
|
|
|
return html |
|
|
|
|
|
|
|
|
def add_editor_routes(existing_app): |
|
|
"""Add editor routes to existing Flask app""" |
|
|
existing_app.route('/editor')(editor) |
|
|
existing_app.route('/load_comic')(load_comic) |
|
|
existing_app.route('/save_comic', methods=['POST'])(save_comic) |
|
|
existing_app.route('/export_comic')(export_comic) |
|
|
|
|
|
|
|
|
@existing_app.route('/api/load_comic') |
|
|
def api_load_comic(): |
|
|
"""API endpoint for loading comic data""" |
|
|
return load_comic() |
|
|
|
|
|
print("✅ Comic editor routes added!") |
|
|
|
|
|
if __name__ == '__main__': |
|
|
print("🎨 Starting Comic Editor Server...") |
|
|
print("📝 Visit http://localhost:5001/editor to edit your comic") |
|
|
app.run(debug=True, port=5001) |