ace-step: stream audio bytes inline; catalog v3 → v4

The pre-fix wrapper at stacks/ace-step/infer-api.py returned a JSON
{output_path: "..."} reference to a file written inside the
container at /app/outputs/. That path was unreachable from outside
the container — every consumer got 134 bytes of JSON-pretending-to-
be-WAV instead of audio. Surfaced by the asset_engine consumer's
end-to-end smoke (althing thread 01KRCJF7NGMXYE9F62Q1A6KFD4 msg 5);
my own earlier smoke missed it because I checked HTTP=200 and stopped
reading instead of inspecting the response body.

Wrapper now reads back the file the pipeline writes and streams the
bytes via fastapi.responses.Response with media_type set from the
audio_format request field (audio/wav | audio/mpeg | audio/flac).
The in-container path is exposed via X-Output-Path header for log
correlation but is no longer load-bearing.

Verified end-to-end against live ace-step on irv-ml1:
  POST /generate  ->  HTTP 200 in 80s
  content-type: audio/wav
  content-length: 945226
  x-output-path: /app/outputs/output_cfe87d1d....wav
  $ file response.wav
  RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit,
    stereo 48000 Hz

Catalog: ace-step bumped version 3 -> 4. Dropped
response.output_field (no longer applicable). reproducibility.notes
expanded to record both the v2 18-arg-tuple fix and this v4
inline-streaming change so the history is auditable from the
catalog itself.

Stale ACEStepOutput Pydantic model left in infer-api.py for now —
unused but small; future cleanup.
This commit is contained in:
vh
2026-05-11 15:48:33 -07:00
parent 7d0a9fa09b
commit f8ecc6c047
2 changed files with 46 additions and 9 deletions
+10 -3
View File
@@ -752,7 +752,7 @@ services:
Apache-2.0 hybrid diffusion+LLM music generation. Multi-minute lyric-aware Apache-2.0 hybrid diffusion+LLM music generation. Multi-minute lyric-aware
songs with vocals + instrumentation. songs with vocals + instrumentation.
category: music category: music
version: 3 version: 4
host: irv-ml1 host: irv-ml1
endpoint: http://10.100.79.3:8210/generate endpoint: http://10.100.79.3:8210/generate
method: POST method: POST
@@ -948,16 +948,23 @@ services:
Wrapper-side cleanup queued — once the upstream model defaults this, Wrapper-side cleanup queued — once the upstream model defaults this,
the catalog field will become optional or be dropped entirely. the catalog field will become optional or be dropped entirely.
response: response:
# As of wrapper version that ships with image local/ace-step:v1
# post 2026-05-11, /generate streams audio bytes inline with
# Content-Type set from the audio_format request field. The
# in-container output_path is exposed via X-Output-Path header
# for log correlation but is no longer load-bearing.
type: audio type: audio
mime_from_field: audio_format mime_from_field: audio_format
output_field: output_path
reproducibility: reproducibility:
seedable: true seedable: true
deterministic: true deterministic: true
notes: > notes: >
actual_seeds parameter exposed; identical seeds + params = identical audio. actual_seeds parameter exposed; identical seeds + params = identical audio.
Local infer-api.py patches upstream's broken 24-arg pipeline signature Local infer-api.py patches upstream's broken 24-arg pipeline signature
(was 18 in upstream — caused crashes with audio_duration in `format` slot). (was 18 in upstream — caused crashes with audio_duration in `format` slot)
AND inline-streams the generated audio bytes (was returning a JSON
path reference to a file inside the container, which was unreachable
from outside).
estimated_latency: estimated_latency:
cold_start_s: 30 cold_start_s: 30
warm_per_unit: "~10–60s depending on audio_duration + infer_step" warm_per_unit: "~10–60s depending on audio_duration + infer_step"
+36 -6
View File
@@ -16,6 +16,7 @@
# audio2audio_enable=False, ref_audio_strength=0.5, no ref audio, no # audio2audio_enable=False, ref_audio_strength=0.5, no ref audio, no
# LoRA.) # LoRA.)
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.responses import Response
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Optional from typing import List, Optional
import os import os
@@ -24,6 +25,15 @@ import uuid
app = FastAPI(title="ACEStep Pipeline API (patched)") 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): class ACEStepInput(BaseModel):
checkpoint_path: str checkpoint_path: str
@@ -78,8 +88,17 @@ def initialize_pipeline(
) )
@app.post("/generate", response_model=ACEStepOutput) @app.post("/generate")
async def generate_audio(input_data: ACEStepInput): 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: try:
model_demo = initialize_pipeline( model_demo = initialize_pipeline(
input_data.checkpoint_path, input_data.checkpoint_path,
@@ -115,14 +134,25 @@ async def generate_audio(input_data: ACEStepInput):
input_data.lora_weight, input_data.lora_weight,
) )
output_path = input_data.output_path or f"/app/outputs/output_{uuid.uuid4().hex}.wav" 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) model_demo(*params, save_path=output_path)
return ACEStepOutput( with open(output_path, "rb") as f:
status="success", audio_bytes = f.read()
output_path=output_path,
message="Audio generated successfully", 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: except Exception as e: