Files
led2/lightsync/main.py
Claude b6aacad9ce feat(06-03): add AI sync backend — heuristic mapper, LLM generator, API endpoint
- 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
2026-04-07 11:45:49 +00:00

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