| | |
| | """ |
| | 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) |