from __future__ import annotations import json import os from pathlib import Path from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel ROOT_DIR = Path(__file__).resolve().parents[2] DATA_DIR = ROOT_DIR / "app" / "data" PUBLIC_DIR = ROOT_DIR / "app" / "public" DIST_DIR = ROOT_DIR / "app" / "frontend" / "dist" ALLOWED_DATA_FILES = { "features.json", "faq.json", "route_model_matrix.json", "docs_nav.json", "voice_seed_library.json", "workflow_examples.json", "architecture_layers.json", "site_manifest.json", } CONTRACT_FILES = { "voice_registry": "contracts/voice_registry_shape.json", "route_model_matrix": "contracts/route_model_matrix_shape.json", "seed_voice_library": "contracts/seed_voice_library_shape.json", "provider_config": "contracts/provider_config_shape.json", } def load_json(relative_path: str) -> dict | list: with (DATA_DIR / relative_path).open("r", encoding="utf-8") as handle: return json.load(handle) class DemoRequest(BaseModel): workflow_id: str app = FastAPI(title="Aether Voice Studio Space API", version="1.0.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) if DIST_DIR.exists(): assets_dir = DIST_DIR / "assets" if assets_dir.exists(): app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") if PUBLIC_DIR.exists(): app.mount("/public", StaticFiles(directory=PUBLIC_DIR), name="public") @app.get("/api/health") def health() -> dict: return {"ok": True, "demo_mode": os.getenv("DEMO_MODE", "true").lower() == "true"} @app.get("/api/config") def config() -> dict: return { "demoMode": os.getenv("DEMO_MODE", "true").lower() == "true", "spaceTitle": "Aether Voice Studio", "singlePort": int(os.getenv("PORT", "7860")), } @app.get("/api/content") def content() -> dict: return { "site": load_json("site_manifest.json"), "features": load_json("features.json"), "routes": load_json("route_model_matrix.json"), "docsNav": load_json("docs_nav.json"), "faq": load_json("faq.json"), "seedLibrary": load_json("voice_seed_library.json"), "workflows": load_json("workflow_examples.json"), "architecture": load_json("architecture_layers.json"), } @app.get("/api/data/{filename}") def data_file(filename: str) -> dict | list: if filename not in ALLOWED_DATA_FILES: raise HTTPException(status_code=404, detail="Unknown data file") return load_json(filename) @app.get("/api/contracts") def contracts() -> dict: return { name: { "endpoint": f"/api/contracts/{name}", "download": f"/api/contracts/{name}?download=true", } for name in CONTRACT_FILES } @app.get("/api/contracts/{contract_name}") def contract_file(contract_name: str, download: bool = False): relative_path = CONTRACT_FILES.get(contract_name) if not relative_path: raise HTTPException(status_code=404, detail="Unknown contract") file_path = DATA_DIR / relative_path if download: return FileResponse(file_path, media_type="application/json", filename=file_path.name) return load_json(relative_path) @app.post("/api/demo/simulate") def simulate_demo(request: DemoRequest) -> dict: payloads = load_json("demo_payloads.json") workflow_id = request.workflow_id selected = payloads.get(workflow_id) if selected is None: raise HTTPException(status_code=404, detail="Unknown workflow") return { "demoMode": os.getenv("DEMO_MODE", "true").lower() == "true", "workflowId": workflow_id, "result": selected, } @app.get("/{full_path:path}") def spa_fallback(full_path: str): requested = DIST_DIR / full_path if full_path and requested.exists() and requested.is_file(): return FileResponse(requested) index_file = DIST_DIR / "index.html" if index_file.exists(): return FileResponse(index_file) raise HTTPException(status_code=503, detail="Frontend build is unavailable")