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:
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user