Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, APIRouter, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import FileResponse | |
| from pydantic import BaseModel, Field, conint | |
| from typing import List, Optional | |
| import os | |
| import sys | |
| import json | |
| from dotenv import load_dotenv, find_dotenv | |
| ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) | |
| if ROOT_DIR not in sys.path: | |
| sys.path.append(ROOT_DIR) | |
| load_dotenv(find_dotenv(), override=True) | |
| class CheckRequest(BaseModel): | |
| ui_text: str = Field(..., description="Paste chatbot response or UI text to review") | |
| primary_color: Optional[str] = Field(None, description="Hex color like #111827 for contrast checks") | |
| background_color: Optional[str] = Field(None, description="Hex background color like #ffffff") | |
| class Finding(BaseModel): | |
| category: str | |
| issue: str | |
| fix: str | |
| class CheckResponse(BaseModel): | |
| score: conint(ge=0, le=100) | |
| summary: str | |
| findings: List[Finding] | |
| quick_fixes: List[str] | |
| from openai import OpenAI | |
| SYSTEM = ( | |
| "You are an Accessibility Checker for conversational AI UIs. Evaluate pasted copy for readability, clarity, bias, and inclusivity; " | |
| "if colors are provided, include contrast guidance (WCAG AA). Provide pragmatic, specific fixes. Keep answers concise and actionable." | |
| ) | |
| class AccessibilityService: | |
| def __init__(self) -> None: | |
| key = (os.getenv("OPENAI_API_KEY") or "").strip().strip('"').strip("'") | |
| if not key: | |
| raise RuntimeError("OPENAI_API_KEY is not set") | |
| base_url = (os.getenv("OPENAI_BASE_URL") or "").strip().strip('"').strip("'") or None | |
| kwargs = {"api_key": key} | |
| if base_url: | |
| kwargs["base_url"] = base_url | |
| self.client = OpenAI(**kwargs) | |
| self.model = os.getenv("OPENAI_MODEL", "gpt-4o-mini") | |
| def check(self, req: CheckRequest) -> CheckResponse: | |
| text_header = (f"Primary: {req.primary_color}\nBackground: {req.background_color}\n" if req.primary_color or req.background_color else "") | |
| text_body = ("UI Text:\n" + req.ui_text.strip()) if req.ui_text else "" | |
| user_parts: List[dict] = [] | |
| if text_header or text_body: | |
| user_parts.append({"type": "text", "text": (text_header + text_body).strip()}) | |
| resp = self.client.chat.completions.create( | |
| model=self.model, | |
| messages=[ | |
| {"role": "system", "content": SYSTEM}, | |
| {"role": "user", "content": user_parts if user_parts else (text_header + text_body)}, | |
| ], | |
| temperature=0.2, | |
| response_format={ | |
| "type": "json_schema", | |
| "json_schema": { | |
| "name": "CheckResponse", | |
| "schema": { | |
| "type": "object", | |
| "additionalProperties": False, | |
| "required": ["score", "summary", "findings", "quick_fixes"], | |
| "properties": { | |
| "score": {"type": "integer", "minimum": 0, "maximum": 100}, | |
| "summary": {"type": "string"}, | |
| "findings": { | |
| "type": "array", | |
| "items": {"type": "object", "required": ["category", "issue", "fix"], "properties": { | |
| "category": {"type": "string"}, | |
| "issue": {"type": "string"}, | |
| "fix": {"type": "string"} | |
| }} | |
| }, | |
| "quick_fixes": {"type": "array", "items": {"type": "string"}} | |
| } | |
| } | |
| } | |
| }, | |
| ) | |
| content = resp.choices[0].message.content | |
| try: | |
| data = json.loads(content) | |
| except Exception: | |
| start = content.find('{'); end = content.rfind('}') | |
| if start != -1 and end != -1 and end > start: | |
| data = json.loads(content[start:end+1]) | |
| else: | |
| raise RuntimeError("Invalid JSON from model") | |
| return CheckResponse.model_validate(data) | |
| app = FastAPI(title="Accessibility Checker", version="0.1.0") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| router = APIRouter(prefix="/access", tags=["access"]) | |
| def index() -> FileResponse: | |
| """Serve the Accessibility Checker page.""" | |
| base = os.path.dirname(__file__) | |
| return FileResponse(os.path.join(base, "static", "index.html")) | |
| def index_no_slash() -> FileResponse: | |
| """Serve the Accessibility Checker page (no trailing slash).""" | |
| base = os.path.dirname(__file__) | |
| return FileResponse(os.path.join(base, "static", "index.html")) | |
| def health() -> dict: | |
| return {"status":"ok","openai_key": bool(os.getenv("OPENAI_API_KEY")), "model": os.getenv("OPENAI_MODEL","gpt-4o-mini")} | |
| def check(payload: CheckRequest) -> CheckResponse: | |
| try: | |
| return AccessibilityService().check(payload) | |
| except Exception as exc: | |
| raise HTTPException(status_code=502, detail=f"Accessibility error: {exc}") | |
| app.include_router(router) | |
| def index() -> FileResponse: | |
| base = os.path.dirname(__file__) | |
| return FileResponse(os.path.join(base, "static", "index.html")) | |