"""ASCII Art Generator - FastAPI Web Application.""" import tempfile import time 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, StreamingResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from render import ( PALETTES, PATTERN_TYPES, EFFECT_NAMES, image_to_art, gif_to_art, is_animated_gif, generate_pattern, animate_image, animate_pattern, frames_to_gif, ) 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 CACHE_BUST = str(int(time.time())) 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, "effects": EFFECT_NAMES, "cache_bust": CACHE_BUST, }) @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(2000, 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/animate") async def animate_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"), effect: str = Form("color_cycle"), num_frames: int = Form(20), frame_duration: int = Form(100), export_gif: str = Form("false"), ): dither = dither.lower() in ("true", "1", "yes", "on") double_width = double_width.lower() in ("true", "1", "yes", "on") want_gif = export_gif.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(2000, width)) num_frames = max(6, min(60, num_frames)) frame_duration = max(20, min(500, frame_duration)) if palette not in PALETTES: palette = "wingdings" if effect not in EFFECT_NAMES: effect = "color_cycle" 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: frames = animate_image( tmp_path, width=width, palette_name=palette, bg=bg, double_width=double_width, dither=dither, output_format="html", effect=effect, num_frames=num_frames, frame_duration=frame_duration, return_pil=want_gif, ) if want_gif: pil_frames = [f["pil_frame"] for f in frames] durations = [f["duration"] for f in frames] gif_bytes = frames_to_gif(pil_frames, durations) return StreamingResponse( iter([gif_bytes]), media_type="image/gif", headers={"Content-Disposition": "attachment; filename=ascii-animation.gif"}, ) frame_divs = [] for i, f in enumerate(frames): display = "block" if i == 0 else "none" frame_divs.append( f'