ascii-art/app.py

160 lines
5.3 KiB
Python

"""ASCII Art Generator - FastAPI Web Application."""
import tempfile
import os
from pathlib import Path
from fastapi import FastAPI, File, Form, UploadFile, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from render import (
PALETTES, PATTERN_TYPES, image_to_art, gif_to_art, is_animated_gif,
generate_pattern,
)
app = FastAPI(title="ASCII Art Generator", docs_url=None, redoc_url=None)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
BASE_DIR = Path(__file__).parent
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
templates = Jinja2Templates(directory=BASE_DIR / "templates")
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB
ALLOWED_TYPES = {"image/png", "image/jpeg", "image/gif", "image/webp", "image/bmp"}
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("index.html", {
"request": request,
"palettes": list(PALETTES.keys()),
"palette_previews": {k: v[:12] for k, v in PALETTES.items()},
"patterns": PATTERN_TYPES,
})
@app.get("/health")
async def health():
return {"status": "ok"}
@app.post("/api/internal/provision")
async def provision_space(request: Request):
"""rSpace provisioning webhook — stateless app, just acknowledge."""
body = await request.json()
space = body.get("space", "unknown")
return {"status": "ok", "space": space, "message": f"rcreate space '{space}' ready"}
@app.post("/api/pattern")
async def render_pattern(
pattern: str = Form("plasma"),
width: int = Form(200),
height: int = Form(80),
palette: str = Form("mystic"),
seed: str = Form(""),
output_format: str = Form("html"),
):
width = max(20, min(500, width))
height = max(10, min(250, height))
if pattern not in PATTERN_TYPES:
pattern = "plasma"
if palette not in PALETTES:
palette = "mystic"
seed_val = int(seed) if seed.isdigit() else None
art = generate_pattern(
pattern_type=pattern, width=width, height=height,
palette_name=palette, output_format=output_format, seed=seed_val,
)
if output_format == "plain":
return PlainTextResponse(art)
return HTMLResponse(art)
@app.get("/api/palettes")
async def get_palettes():
return {
"palettes": [
{"name": k, "chars": v, "preview": v[:12]}
for k, v in PALETTES.items()
]
}
@app.post("/api/render")
async def render_art(
file: UploadFile = File(...),
width: int = Form(80),
palette: str = Form("wingdings"),
bg: str = Form("dark"),
dither: str = Form("false"),
double_width: str = Form("false"),
output_format: str = Form("html"),
):
# JS sends booleans as strings
dither = dither.lower() in ("true", "1", "yes", "on")
double_width = double_width.lower() in ("true", "1", "yes", "on")
if file.content_type not in ALLOWED_TYPES:
return JSONResponse({"error": f"Unsupported file type: {file.content_type}"}, 400)
content = await file.read()
if len(content) > MAX_UPLOAD_SIZE:
return JSONResponse({"error": "File too large (max 10MB)"}, 400)
width = max(20, min(500, width))
if palette not in PALETTES:
palette = "wingdings"
if bg not in ("dark", "light"):
bg = "dark"
if output_format not in ("html", "ansi", "plain", "json"):
output_format = "html"
suffix = Path(file.filename or "img.png").suffix
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
tmp.write(content)
tmp_path = tmp.name
try:
animated = is_animated_gif(tmp_path)
if animated:
frames = gif_to_art(
tmp_path, width=width, palette_name=palette,
bg=bg, double_width=double_width, dither=dither,
output_format=output_format if output_format != "json" else "html",
)
if output_format == "json":
return JSONResponse({
"animated": True,
"frames": frames,
"frame_count": len(frames),
})
# For HTML output, wrap each frame
frame_divs = []
for i, f in enumerate(frames):
display = "block" if i == 0 else "none"
frame_divs.append(
f'<div class="art-frame" data-duration="{f["duration"]}" '
f'style="display:{display}">{f["art"]}</div>'
)
html = "\n".join(frame_divs)
return HTMLResponse(html)
else:
art = image_to_art(
tmp_path, width=width, palette_name=palette,
bg=bg, double_width=double_width, dither=dither,
output_format=output_format if output_format != "json" else "html",
)
if output_format == "json":
return JSONResponse({"animated": False, "art": art})
elif output_format == "plain":
return PlainTextResponse(art)
else:
return HTMLResponse(art)
finally:
os.unlink(tmp_path)