sparrow/app/models/schemas/node.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

49 lines
1.1 KiB
Python

# app/models/schemas/node.py — Node Pydantic models
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel
NodeStatus = Literal["pending", "generating", "ready", "error"]
class NodeRow(BaseModel):
id: str
chain_id: str
parent_id: str | None
audio_path: str | None
duration_s: float | None
status: NodeStatus
is_committed: bool
prompt: str
energy: float | None
tempo_feel: float | None
density: float | None
cfg_coef: float
prompt_duration_s: float
error_msg: str | None
created_at: float
children: list["NodeRow"] = []
class BranchRequest(BaseModel):
prompt: str = ""
energy: float | None = None
tempo_feel: float | None = None
density: float | None = None
cfg_coef: float = 3.0
prompt_duration_s: float = 10.0
duration_s: float = 15.0
class StemResult(BaseModel):
node_id: str
vocals: str
drums: str
bass: str
other: str
class ExportRequest(BaseModel):
format: Literal["wav", "mp3"] = "wav"
node_ids: list[str] | None = None # if None, use committed spine