- Create lightsync/api/sync.py with POST /api/sync/generate and GET /api/sync/llm-available - heuristic_generate: intensity-to-animation mapping, chroma-to-color, beat-snapped block placement - llm_generate: Anthropic claude-3-haiku with lazy init, JSON parse + validation - section_range filtering for per-section generation (D-11) - Register sync.router with prefix /api/sync in main.py
76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
import os
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from lightsync.devices.registry import DeviceRegistry
|
|
from lightsync.store.show_store import ShowStore
|
|
from lightsync.protocol.udp_sender import UDPSender
|
|
|
|
# Module-level containers populated in lifespan
|
|
registry: DeviceRegistry | None = None
|
|
show_store: ShowStore | None = None
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
global registry, show_store
|
|
# Startup
|
|
registry = DeviceRegistry(Path("devices.json"))
|
|
await registry.load()
|
|
show_store = ShowStore(Path("shows"))
|
|
show_store.ensure_dir()
|
|
|
|
# Beat analysis cache: filepath -> {"tempo": float, "beats": [float, ...]}
|
|
app.state.beats = {}
|
|
# Segments cache: "filepath:n_segments" -> [{start, end, label}, ...]
|
|
app.state.segments = {}
|
|
|
|
# UDP sender for device communication (Phase 3)
|
|
udp_sender = UDPSender()
|
|
await udp_sender.start()
|
|
app.state.udp_sender = udp_sender
|
|
|
|
yield
|
|
|
|
# Shutdown
|
|
await app.state.udp_sender.stop()
|
|
await registry.save()
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
app = FastAPI(title="LightSync", lifespan=lifespan, redirect_slashes=False)
|
|
|
|
# API routes (registered before static mount — see Pattern 7)
|
|
from lightsync.api import shows, devices, ws, audio, timeline, sync
|
|
app.include_router(shows.router, prefix="/api/shows")
|
|
app.include_router(devices.router, prefix="/api/devices")
|
|
app.include_router(ws.router)
|
|
app.include_router(audio.router, prefix="/api/audio")
|
|
app.include_router(timeline.router, prefix="/api")
|
|
app.include_router(sync.router, prefix="/api/sync")
|
|
|
|
# Serve uploaded audio files so <audio src=...> can load them
|
|
shows_dir = Path("/app/shows")
|
|
shows_dir.mkdir(parents=True, exist_ok=True)
|
|
app.mount("/shows", StaticFiles(directory=str(shows_dir)), name="shows")
|
|
|
|
# Static file serving — no-cache headers to prevent stale JS/CSS
|
|
frontend_dir = Path(__file__).parent / "frontend"
|
|
|
|
@app.get("/{filename:path}")
|
|
async def static_files(request: Request, filename: str):
|
|
path = frontend_dir / (filename or "index.html")
|
|
if not path.exists() or not path.is_file():
|
|
path = frontend_dir / "index.html"
|
|
response = FileResponse(path)
|
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
return response
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|