# Patched ACE-Step REST API. # # Upstream's infer-api.py builds an 18-element positional `params` # tuple and splats it into the pipeline. The pipeline's __call__ # actually expects 24 positional args — upstream's tuple is missing # `format` at the head and the audio2audio/lora block at the tail. # The result of the missing `format` head: every subsequent arg shifts # left, the int `audio_duration` lands in `format`'s slot, and the # pipeline blows up with "object of type 'int' has no len()" when it # tries to validate the format string. # # This file copies upstream's behavior verbatim except for the params # tuple, which now matches the canonical 24-arg pipeline signature # inferred from the Gradio handler in acestep/ui/components.py. # (Defaults for the new fields mirror Gradio's defaults: format=wav, # audio2audio_enable=False, ref_audio_strength=0.5, no ref audio, no # LoRA.) from fastapi import FastAPI, HTTPException from fastapi.responses import Response from pydantic import BaseModel from typing import List, Optional import os from acestep.pipeline_ace_step import ACEStepPipeline import uuid app = FastAPI(title="ACEStep Pipeline API (patched)") # audio_format -> Content-Type. The wrapper writes the file in the # format the upstream pipeline supports for the requested extension; # we just need to label the bytes correctly on the wire. _AUDIO_MIME = { "wav": "audio/wav", "mp3": "audio/mpeg", "flac": "audio/flac", } class ACEStepInput(BaseModel): checkpoint_path: str bf16: bool = True torch_compile: bool = False device_id: int = 0 output_path: Optional[str] = None audio_duration: float prompt: str lyrics: str infer_step: int guidance_scale: float scheduler_type: str cfg_type: str omega_scale: float actual_seeds: List[int] guidance_interval: float guidance_interval_decay: float min_guidance_scale: float use_erg_tag: bool use_erg_lyric: bool use_erg_diffusion: bool oss_steps: List[int] guidance_scale_text: float = 0.0 guidance_scale_lyric: float = 0.0 # Optional new fields exposed for completeness — Gradio defaults. # lora_name_or_path: empty string trips HF Hub's repo-id validator # ("Repo id ... cannot ... be empty"), so default to None and only # forward it as a string when actually set. audio_format: str = "wav" audio2audio_enable: bool = False ref_audio_strength: float = 0.5 ref_audio_input: Optional[str] = None lora_name_or_path: Optional[str] = None lora_weight: float = 1.0 class ACEStepOutput(BaseModel): status: str output_path: Optional[str] message: str def initialize_pipeline( checkpoint_path: str, bf16: bool, torch_compile: bool, device_id: int ) -> ACEStepPipeline: os.environ["CUDA_VISIBLE_DEVICES"] = str(device_id) return ACEStepPipeline( checkpoint_dir=checkpoint_path, dtype="bfloat16" if bf16 else "float32", torch_compile=torch_compile, ) @app.post("/generate") async def generate_audio(input_data: ACEStepInput): """Generate music; respond with the audio bytes inline. Pre-2026-05-11 this returned a JSON {output_path} reference to a file inside the container — useless to any external consumer since the path wasn't reachable from outside. Now streams the bytes back directly with the right Content-Type, and the file in /app/outputs/ is incidental bookkeeping (the pipeline writes there as part of its normal flow; we read it back for the wire). """ try: model_demo = initialize_pipeline( input_data.checkpoint_path, input_data.bf16, input_data.torch_compile, input_data.device_id, ) params = ( input_data.audio_format, input_data.audio_duration, input_data.prompt, input_data.lyrics, input_data.infer_step, input_data.guidance_scale, input_data.scheduler_type, input_data.cfg_type, input_data.omega_scale, ", ".join(map(str, input_data.actual_seeds)), input_data.guidance_interval, input_data.guidance_interval_decay, input_data.min_guidance_scale, input_data.use_erg_tag, input_data.use_erg_lyric, input_data.use_erg_diffusion, ", ".join(map(str, input_data.oss_steps)), input_data.guidance_scale_text, input_data.guidance_scale_lyric, input_data.audio2audio_enable, input_data.ref_audio_strength, input_data.ref_audio_input, input_data.lora_name_or_path or "none", input_data.lora_weight, ) ext = input_data.audio_format.lower() output_path = ( input_data.output_path or f"/app/outputs/output_{uuid.uuid4().hex}.{ext}" ) model_demo(*params, save_path=output_path) with open(output_path, "rb") as f: audio_bytes = f.read() return Response( content=audio_bytes, media_type=_AUDIO_MIME.get(ext, "application/octet-stream"), headers={ # Surfaces the in-container path for debugging/log correlation # without putting it on the response body. "X-Output-Path": output_path, }, ) except Exception as e: raise HTTPException(status_code=500, detail=f"Error generating audio: {str(e)}") @app.get("/health") async def health_check(): return {"status": "healthy"} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)