Full FastAPI backend for the AI music continuation editor:
Services
- chain.py: chain + node CRUD, commit/discard, recursive CTE spine query
- musicgen.py: MusicGenClient with cf-orch allocation + mock mode (CF_MUSICGEN_MOCK=1)
- stems.py: Demucs 4-stem separation subprocess wrapper + mock mode
- export.py: ffmpeg concat demuxer to stitch committed spine into WAV/MP3
API endpoints
- chains: CRUD, multipart audio upload (WAV/MP3/FLAC/OGG/M4A/AIFF)
- nodes: branch creation (202 + BackgroundTasks), commit, discard, audio stream
- gpu: cf-orch capacity status; session allocation stubbed pending cf-orch#43
- stems: Paid-tier stem separation (Demucs, gated via tiers.py)
- export: POST /{chain_id}/export → FileResponse download
- events: SSE stream (node-status events) per chain via asyncio Queue pub/sub
Infrastructure
- lifespan: reads SPARROW_DB_PATH/DATA_DIR at startup (not import time)
- events_store: subscribe/unsubscribe/broadcast pattern for SSE
- CORS: open in dev, SPARROW_CORS_ORIGINS in production
- Background generation opens its own DB connection (WAL-safe)
Tests: 30/30 passing across service units and API integration
73 lines
2.2 KiB
Python
73 lines
2.2 KiB
Python
# app/api/endpoints/gpu.py — cf-orch GPU status and session allocation
|
|
#
|
|
# GET /api/gpu/status — available capacity from cf-orch
|
|
# POST /api/gpu/connect — session-held allocation (Premium tier, stub)
|
|
# DELETE /api/gpu/disconnect — release session allocation (Premium tier, stub)
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/gpu", tags=["gpu"])
|
|
|
|
_ORCH_URL = os.environ.get("CF_ORCH_URL", "").rstrip("/")
|
|
_SERVICE = "cf-musicgen"
|
|
|
|
|
|
@router.get("/status")
|
|
async def gpu_status() -> dict:
|
|
"""
|
|
Return current cf-orch capacity for cf-musicgen.
|
|
|
|
Returns {"available": False, "reason": "..."} if cf-orch is unreachable
|
|
or unconfigured (e.g. mock mode).
|
|
"""
|
|
if not _ORCH_URL:
|
|
return {
|
|
"available": False,
|
|
"reason": "CF_ORCH_URL not configured — running in mock mode.",
|
|
"mock": True,
|
|
}
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
resp = await client.get(
|
|
f"{_ORCH_URL}/api/services/{_SERVICE}/status"
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
except httpx.HTTPStatusError as exc:
|
|
raise HTTPException(
|
|
status_code=502,
|
|
detail=f"cf-orch returned {exc.response.status_code}.",
|
|
) from exc
|
|
except Exception as exc:
|
|
raise HTTPException(
|
|
status_code=502,
|
|
detail=f"cf-orch unreachable: {exc}",
|
|
) from exc
|
|
|
|
|
|
@router.post("/connect", status_code=501)
|
|
async def gpu_connect() -> dict:
|
|
"""
|
|
Session-held GPU allocation (Premium tier).
|
|
|
|
Not yet implemented — tracked in cf-orch #43.
|
|
"""
|
|
raise HTTPException(
|
|
status_code=501,
|
|
detail="Session-held GPU allocation is not yet implemented (cf-orch #43).",
|
|
)
|
|
|
|
|
|
@router.delete("/disconnect", status_code=501)
|
|
async def gpu_disconnect() -> dict:
|
|
"""Release a session-held GPU allocation (Premium tier, stub)."""
|
|
raise HTTPException(
|
|
status_code=501,
|
|
detail="Session-held GPU allocation is not yet implemented (cf-orch #43).",
|
|
)
|