160 lines
5.3 KiB
Python
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)
|