sparrow/app/main.py
pyr0ball a6f60c9e07 feat: implement Sparrow backend (v0.1.0)
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
2026-04-17 15:22:37 -07:00

66 lines
1.7 KiB
Python

# app/main.py — Sparrow FastAPI application factory
from __future__ import annotations
import logging
import os
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.routes import router
from app.db.store import get_connection, run_migrations
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
# Read env at startup — not at import time — so tests can inject values
db_path = os.environ.get("SPARROW_DB_PATH", "data/sparrow.db")
data_dir = os.environ.get("SPARROW_DATA_DIR", "data")
env = os.environ.get("SPARROW_ENV", "development")
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
Path(data_dir).mkdir(parents=True, exist_ok=True)
conn = get_connection(db_path)
run_migrations(conn)
app.state.conn = conn
app.state.data_dir = data_dir
logger.info("Sparrow started (env=%s, db=%s, data=%s)", env, db_path, data_dir)
yield
conn.close()
logger.info("Sparrow shut down.")
def create_app() -> FastAPI:
env = os.environ.get("SPARROW_ENV", "development")
app = FastAPI(
title="Sparrow",
description="AI music continuation — branching chain editor",
version="0.1.0",
lifespan=lifespan,
)
# CORS: open in dev, tighten in production via env
origins = (
["*"] if env == "development"
else os.environ.get("SPARROW_CORS_ORIGINS", "").split(",")
)
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(router)
return app
app = create_app()