# app/api/endpoints/export.py — stitch committed spine and serve as download from __future__ import annotations import logging from pathlib import Path from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import FileResponse from app.api.deps import get_conn, get_data_dir from app.models.schemas.node import ExportRequest from app.services import chain as chain_svc from app.services.export import ExportFormat, make_export_path, stitch logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/chains", tags=["export"]) @router.post("/{chain_id}/export") async def export_chain( chain_id: str, body: ExportRequest, conn=Depends(get_conn), data_dir: str = Depends(get_data_dir), ) -> FileResponse: """ Stitch node audio files into a single download. If body.node_ids is provided, those nodes are used in order. Otherwise the committed spine (root → leaf) is used. Raises 409 if any targeted node has no audio. """ if body.node_ids is not None: nodes = [chain_svc.get_node(conn, nid) for nid in body.node_ids] if any(n is None for n in nodes): raise HTTPException(status_code=404, detail="One or more nodes not found.") else: nodes = chain_svc.get_committed_spine(conn, chain_id) if not nodes: raise HTTPException( status_code=404, detail="Chain not found or has no committed nodes.", ) audio_paths = [n["audio_path"] for n in nodes if n] # type: ignore[index] missing = [n["id"] for n in nodes if n and n["audio_path"] is None] # type: ignore[index] if missing: raise HTTPException( status_code=409, detail=f"Nodes {missing} have no audio yet.", ) fmt: ExportFormat = body.format # type: ignore[assignment] output_path = make_export_path(data_dir, chain_id, fmt) try: await stitch(audio_paths, output_path, fmt) except Exception as exc: logger.exception("Export failed for chain %s: %s", chain_id, exc) raise HTTPException(status_code=500, detail=f"Export failed: {exc}") from exc media_type = "audio/wav" if fmt == "wav" else "audio/mpeg" filename = f"sparrow_export_{chain_id[:8]}.{fmt}" return FileResponse(output_path, media_type=media_type, filename=filename)