Spaces:
Running
Running
# app.py | |
import os | |
import logging | |
import base64 | |
import textwrap # Import the missing module | |
from flask import Flask, request, render_template, send_file, flash, redirect, url_for, jsonify | |
from mermaid_renderer import MermaidRenderer # Import the refactored class | |
# Configure logging (optional but recommended) | |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
app = Flask(__name__) | |
# Required for flashing messages | |
# In a real deployment, use a persistent, environment-variable-based secret key | |
# Using a fixed key for simplicity here, replace in production | |
app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'a_very_secret_key_change_in_prod') | |
# Initialize the renderer (could potentially be done once at startup) | |
# Handle potential init errors | |
renderer = None | |
try: | |
renderer = MermaidRenderer() | |
logging.info("MermaidRenderer initialized successfully.") | |
except RuntimeError as e: | |
logging.critical(f"Failed to initialize MermaidRenderer: {e}. The rendering endpoint will be unavailable.") | |
# Keep renderer as None to indicate failure | |
def index(): | |
"""Display the main page with the form, including default example code.""" | |
default_mermaid_code = textwrap.dedent(""" | |
mindmap | |
root((mindmap)) | |
Origins | |
Long history | |
::icon(fa fa-book) | |
Popularisation | |
British popular psychology author Tony Buzan | |
Research | |
On effectiveness<br/>and features | |
On Automatic creation | |
Uses | |
Creative techniques | |
Strategic planning | |
Argument mapping | |
Tools | |
Pen and paper | |
Mermaid | |
""").strip() | |
if renderer is None: | |
flash("Error: Mermaid rendering service is unavailable due to initialization failure. Please check server logs.", "error") | |
return render_template('index.html', default_code=default_mermaid_code) | |
def render_mermaid(): | |
""" | |
Handle form submission. | |
Handle form submission for final download. | |
Re-renders the diagram based on form data and sends it as an attachment. | |
""" | |
if renderer is None: | |
flash("Mermaid rendering service is unavailable. Cannot process request.", "error") | |
return redirect(url_for('index')) | |
mermaid_code = request.form.get('mermaid_code', '') | |
output_format = request.form.get('output_format', 'png') | |
theme = request.form.get('theme', 'default') | |
if not mermaid_code.strip(): | |
flash("Mermaid code cannot be empty.", "warning") | |
return redirect(url_for('index')) | |
output_path = None | |
input_path = None | |
try: | |
logging.info(f"Download request: format={output_format}, theme={theme}") | |
if not renderer: | |
raise RuntimeError("Renderer not initialized.") | |
output_path, input_path = renderer.render(mermaid_code, output_format, theme) | |
mime_types = {'png': 'image/png', 'svg': 'image/svg+xml', 'pdf': 'application/pdf'} | |
mime_type = mime_types.get(output_format, 'application/octet-stream') | |
return send_file( | |
output_path, | |
mimetype=mime_type, | |
as_attachment=True, | |
download_name=f'diagram.{output_format}' | |
) | |
except (ValueError, RuntimeError) as e: | |
logging.error(f"Download rendering failed: {e}") | |
flash(f"Error generating download: {e}", "error") | |
return redirect(url_for('index')) | |
except Exception as e: | |
logging.exception("An unexpected error occurred during rendering.") # Log full traceback | |
flash("An unexpected server error occurred. Please try again later.", "error") | |
return redirect(url_for('index')) | |
finally: | |
# IMPORTANT: Clean up temporary files after sending the response or on error | |
if output_path and os.path.exists(output_path): | |
try: | |
# If it was PDF, send_file might hold the handle, but unlinking should still work on Unix-like systems | |
# On Windows, this might cause issues if send_file hasn't finished. | |
# A more robust solution might involve background tasks or different cleanup strategies. | |
os.unlink(output_path) | |
logging.info(f"Cleaned up temporary output file: {output_path}") | |
except OSError as e: | |
logging.error(f"Error deleting temporary output file {output_path}: {e}") | |
if input_path and os.path.exists(input_path): | |
try: | |
os.unlink(input_path) | |
logging.info(f"Cleaned up temporary input file: {input_path}") | |
except OSError as e: | |
logging.error(f"Error deleting temporary input file {input_path}: {e}") | |
def preview_mermaid(): | |
""" | |
Handles asynchronous preview requests. | |
Renders PNG/SVG and returns image data as JSON. | |
""" | |
if renderer is None: | |
return jsonify({"error": "Mermaid rendering service is unavailable."}), 503 | |
data = request.get_json() | |
if not data: | |
return jsonify({"error": "Invalid request data."}), 400 | |
mermaid_code = data.get('mermaid_code', '') | |
# Preview only supports PNG and SVG for embedding | |
output_format = data.get('output_format', 'svg') # Default to SVG for preview | |
if output_format not in ['png', 'svg']: | |
output_format = 'svg' # Force SVG if invalid format requested for preview | |
theme = data.get('theme', 'default') | |
if not mermaid_code.strip(): | |
return jsonify({"error": "Mermaid code cannot be empty."}), 400 | |
output_path = None | |
input_path = None | |
preview_data = None | |
try: | |
logging.info(f"Preview request: format={output_format}, theme={theme}") | |
if not renderer: | |
raise RuntimeError("Renderer not initialized.") # Should be caught above, but defensive | |
output_path, input_path = renderer.render(mermaid_code, output_format, theme) | |
with open(output_path, 'rb') as f: | |
file_content = f.read() | |
if output_format == 'png': | |
preview_data = base64.b64encode(file_content).decode('utf-8') | |
elif output_format == 'svg': | |
try: | |
preview_data = file_content.decode('utf-8') | |
except UnicodeDecodeError: | |
logging.error("SVG content is not valid UTF-8 for preview.") | |
raise RuntimeError("Generated SVG is not valid UTF-8.") | |
return jsonify({ | |
"format": output_format, | |
"data": preview_data | |
}) | |
except (ValueError, RuntimeError) as e: | |
logging.error(f"Preview rendering failed: {e}") | |
return jsonify({"error": f"Error rendering preview: {e}"}), 500 | |
except Exception as e: | |
logging.exception("An unexpected error occurred during preview generation.") | |
return jsonify({"error": "An unexpected server error occurred during preview."}), 500 | |
finally: | |
# Ensure cleanup after preview generation | |
if output_path and os.path.exists(output_path): | |
try: | |
os.unlink(output_path) | |
logging.info(f"Cleaned up temporary output file after download: {output_path}") | |
except OSError as e: | |
logging.error(f"Error deleting temporary output file {output_path} after download: {e}") | |
if input_path and os.path.exists(input_path): | |
try: | |
os.unlink(input_path) | |
logging.info(f"Cleaned up temporary input file after download: {input_path}") | |
except OSError as e: | |
logging.error(f"Error deleting temporary input file {input_path} after download: {e}") | |
def healthz(): | |
return "OK", 200 | |
if __name__ == '__main__': | |
# For local development only (use Gunicorn/Waitress in production) | |
# Use 0.0.0.0 to be accessible on the network | |
# Set debug=False for production-like testing, or True for development features | |
is_debug = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true' | |
# Use port 7860 for Hugging Face Spaces compatibility | |
app.run(debug=is_debug, host='0.0.0.0', port=int(os.environ.get('PORT', 7860))) | |