"""
server_app.py — gr.Server entrypoint with carousel front page and API endpoints.
Serves:
GET / → HTML carousel (first slide embeds /access via iframe)
GET /access → Mounted Color-UX-Access Gradio app (same as app.py)
POST /api/generate_gallery → api_generate_gallery_from_bytes → JSON
POST /api/analyze_vlm → api_analyze_cvd_grid_from_bytes → JSON
POST /api/wcag_report → api_report_from_json → Markdown
"""
# GUARDRAIL: Do not add self-modifying or patch scripts.
# Implement changes directly in this file or in helper modules with tests.
# Root-level *fix*.py / apply_*.py files are prohibited.
from __future__ import annotations
import os
import base64
from typing import List
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
import gradio as gr
# Import the existing Gradio Blocks app from app.py (must not modify app.py)
from app import demo
# Import backend wrappers
from backend_api import (
api_generate_gallery_from_bytes,
api_analyze_cvd_grid_from_bytes,
api_report_from_json,
)
# ── Carousel HTML ─────────────────────────────────────────────────────────────
_CAROUSEL_HTML = """
NARWALL Tech — Accessibility Suite
Color-UX-Access
Colorblind accessibility testing — screenshot → CVD simulation → WCAG report
Tool Slide 2
Coming soon — additional accessibility testing tools
"""
# ── gr.Server setup ────────────────────────────────────────────────────────────
# Create the Server — this is a FastAPI subclass with Gradio's API engine
app = gr.Server(title="NARWALL Tech — Accessibility Suite")
# Mount the existing Color-UX-Access Blocks app at /access
# This serves the same UI as `uv run app.py`
app.mount("/access", demo, name="color_ux_access")
# ── Custom routes ─────────────────────────────────────────────────────────────
@app.get("/")
async def root():
"""Serve the HTML carousel page at /."""
return HTMLResponse(content=_CAROUSEL_HTML, media_type="text/html")
# ── API endpoints ─────────────────────────────────────────────────────────────
async def _generate_gallery_handler(data: dict) -> dict:
"""Handler shared by @app.api and FastAPI POST /api/generate_gallery."""
b64 = data.get("image", "")
if not b64:
return {"error": "Missing 'image' field in request body", "items": []}
try:
image_bytes = base64.b64decode(b64)
except Exception as e:
return {"error": f"Could not decode base64 image: {e}", "items": []}
try:
gallery = api_generate_gallery_from_bytes(image_bytes)
except ValueError as e:
return {"error": str(e), "items": []}
items = []
for img_bytes, label in gallery:
items.append({
"image": base64.b64encode(img_bytes).decode("utf-8"),
"label": label,
})
return {"items": items}
async def _analyze_vlm_handler(data: dict) -> dict:
"""Handler shared by @app.api and FastAPI POST /api/analyze_vlm."""
items = data.get("items", [])
if not items:
return {"error": "Empty gallery — upload an image first.", "findings": [], "passes": False}
gallery = []
for item in items:
try:
img_bytes = base64.b64decode(item.get("image", ""))
label = item.get("label", "Unknown")
gallery.append((img_bytes, label))
except Exception:
continue
if not gallery:
return {"error": "No valid images in gallery", "findings": [], "passes": False}
return api_analyze_cvd_grid_from_bytes(gallery)
# @app.api — Gradio client API (for programmatic callers via gradio_client)
@app.api(name="generate_gallery")
async def generate_gallery_endpoint(data: dict) -> dict:
return await _generate_gallery_handler(data)
@app.api(name="analyze_vlm")
async def analyze_vlm_endpoint(data: dict) -> dict:
return await _analyze_vlm_handler(data)
@app.api(name="wcag_report")
async def wcag_report_endpoint(data: dict) -> str:
return api_report_from_json(data)
# FastAPI routes — accessible via HTTP/TestClient at /api/*
@app.post("/api/generate_gallery")
async def fastapi_generate_gallery(data: dict) -> dict:
"""POST /api/generate_gallery — same as @app.api generate_gallery for HTTP clients."""
return await _generate_gallery_handler(data)
@app.post("/api/analyze_vlm")
async def fastapi_analyze_vlm(data: dict) -> dict:
"""POST /api/analyze_vlm — same as @app.api analyze_vlm for HTTP clients."""
return await _analyze_vlm_handler(data)
@app.post("/api/wcag_report")
async def fastapi_wcag_report(data: dict) -> str:
"""POST /api/wcag_report — same as @app.api wcag_report for HTTP clients."""
return api_report_from_json(data)
# ── Local development entrypoint ──────────────────────────────────────────────
if __name__ == "__main__":
port = int(os.environ.get("PORT", "7860"))
app.launch(server_name="0.0.0.0", server_port=port)