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
52 lines
1.7 KiB
Python
52 lines
1.7 KiB
Python
# app/tiers.py — tier gates for Sparrow
|
|
#
|
|
# BYOK does not apply to Sparrow — "bring your own" means bring your own
|
|
# hardware (self-hosting). is_self_hosted() checks the cf-orch URL target.
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from fastapi import HTTPException
|
|
|
|
|
|
def is_self_hosted() -> bool:
|
|
"""
|
|
True when cf-orch is on localhost or a LAN address.
|
|
|
|
Self-hosted users get unrestricted access equivalent to Paid tier.
|
|
Cloud users are gated by the standard tier model.
|
|
"""
|
|
orch_url = os.environ.get("CF_ORCH_URL", "")
|
|
if not orch_url:
|
|
return True # no orch = local dev mode = self-hosted
|
|
host = orch_url.split("//")[-1].split(":")[0].split("/")[0]
|
|
return host in ("localhost", "127.0.0.1") or host.startswith("10.") or \
|
|
host.startswith("192.168.") or host.startswith("172.")
|
|
|
|
|
|
def require_tier(feature: str, tier: str | None = None) -> None:
|
|
"""
|
|
Gate a feature by tier. Raises 402 if the caller does not qualify.
|
|
|
|
For self-hosted instances all features are unlocked.
|
|
tier=None means the request carries no tier — treat as Free.
|
|
"""
|
|
if is_self_hosted():
|
|
return
|
|
|
|
_TIER_RANK = {"free": 0, "paid": 1, "premium": 2}
|
|
_FEATURE_MIN: dict[str, str] = {
|
|
"stems": "paid",
|
|
"parallel_branch": "paid",
|
|
"session_allocation": "premium",
|
|
"priority_queue": "premium",
|
|
}
|
|
|
|
min_tier = _FEATURE_MIN.get(feature, "free")
|
|
caller_rank = _TIER_RANK.get(tier or "free", 0)
|
|
required_rank = _TIER_RANK[min_tier]
|
|
|
|
if caller_rank < required_rank:
|
|
raise HTTPException(
|
|
status_code=402,
|
|
detail=f"Feature '{feature}' requires {min_tier} tier.",
|
|
)
|