# app/api/endpoints/stems.py — 4-stem separation (Paid tier) from __future__ import annotations import logging from pathlib import Path from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from app.api.deps import get_conn, get_data_dir from app.models.schemas.node import StemResult from app.services import chain as chain_svc from app.services import stems as stems_svc from app.tiers import require_tier logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/stems", tags=["stems"]) @router.post("/{node_id}", response_model=StemResult, status_code=202) async def separate_stems( node_id: str, background_tasks: BackgroundTasks, tier: str | None = None, conn=Depends(get_conn), data_dir: str = Depends(get_data_dir), ) -> dict: """ Run 4-stem separation (Demucs htdemucs) on a ready node. Requires Paid tier. Returns stem file paths immediately — paths will be valid once the background task completes. Poll GET /api/nodes/{node_id} or listen on the chain SSE stream for completion signals. tier: passed as query param, e.g. ?tier=paid """ require_tier("stems", tier) node = chain_svc.get_node(conn, node_id) if node is None: raise HTTPException(status_code=404, detail="Node not found.") if node["audio_path"] is None: raise HTTPException(status_code=409, detail="Node has no audio.") stems_dir = stems_svc.make_stems_dir(data_dir, node["chain_id"], node_id) stem_paths = stems_svc._stem_paths(stems_dir, Path(node["audio_path"]).stem) background_tasks.add_task( _run_separation, source_audio_path=node["audio_path"], stems_dir=stems_dir, ) return StemResult( node_id=node_id, vocals=stem_paths["vocals"], drums=stem_paths["drums"], bass=stem_paths["bass"], other=stem_paths["other"], ) async def _run_separation(source_audio_path: str, stems_dir: str) -> None: try: await stems_svc.separate(source_audio_path, stems_dir) logger.info("Stems complete: %s", stems_dir) except Exception as exc: logger.exception("Stem separation failed: %s", exc)