| 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") |
|
|