feat: added ACE-Step 1.5 powered remix API
This commit is contained in:
@@ -60,6 +60,57 @@ curl -X 'POST' \
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# `/v1/fullsong/remix`
|
||||||
|
|
||||||
|
## Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X 'POST' \
|
||||||
|
'http://127.0.0.1:8000/v1/fullsong/remix' \
|
||||||
|
-H 'accept: application/json' \
|
||||||
|
-F 'audio_file=@source.mp3;type=audio/mpeg' \
|
||||||
|
-F 'caption=Genre: Lo-fi hip hop. Style: Chill, mellow, dusty samples. Mood: Relaxed, nostalgic.' \
|
||||||
|
-F 'lyrics=[Verse 1]
|
||||||
|
Your lyrics here...' \
|
||||||
|
-F 'audio_cover_strength=0.5' \
|
||||||
|
-F 'cover_noise_strength=0.2'
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional form fields (omit to use defaults):
|
||||||
|
|
||||||
|
| Field | Type | Default | Description |
|
||||||
|
|-------|------|---------|-------------|
|
||||||
|
| `caption` | string | — | Style description for the remix |
|
||||||
|
| `lyrics` | string | — | New lyrics (leave unset to keep original feel) |
|
||||||
|
| `instrumental` | bool | — | Suppress vocals |
|
||||||
|
| `inference_steps` | int | — | Diffusion steps |
|
||||||
|
| `guidance_scale` | float | — | CFG guidance strength |
|
||||||
|
| `use_random_seed` | bool | — | Randomise seed |
|
||||||
|
| `seed` | int | — | Fixed seed for reproducibility |
|
||||||
|
| `thinking` | bool | — | Enable chain-of-thought reasoning |
|
||||||
|
| `batch_size` | int | — | Number of outputs |
|
||||||
|
| `audio_format` | string | — | Output format (`mp3`, `wav`, …) |
|
||||||
|
| `audio_cover_strength` | float | `0.5` | Remix Strength: 0=creative freedom, 1=faithful to source |
|
||||||
|
| `cover_noise_strength` | float | `0.2` | Cover Strength (Melody Retention): 0=pure style transfer, 0.1–0.25 recommended |
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"task_id": "a1b2c3d4-0000-0000-0000-000000000000",
|
||||||
|
"status": "queued",
|
||||||
|
"queue_position": 1
|
||||||
|
},
|
||||||
|
"code": 200,
|
||||||
|
"error": null,
|
||||||
|
"timestamp": 1776227065582,
|
||||||
|
"extra": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Poll `/v1/fullsong/result/{task_id}` and download via `/v1/fullsong/audio/{task_id}` — same flow as `/v1/fullsong/generate`.
|
||||||
|
|
||||||
# `/v1/fullsong/result/{task_id}`
|
# `/v1/fullsong/result/{task_id}`
|
||||||
|
|
||||||
## Request
|
## Request
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import argparse
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import uuid
|
import uuid
|
||||||
@@ -208,6 +209,80 @@ async def fullsong_generate(req: FullsongGenerateRequest, request: Request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
"/v1/fullsong/remix",
|
||||||
|
tags=["fullsong"],
|
||||||
|
summary="Submit a remix/cover task (ACE-Step 1.5)",
|
||||||
|
)
|
||||||
|
async def fullsong_remix(
|
||||||
|
request: Request,
|
||||||
|
audio_file: UploadFile = File(...),
|
||||||
|
caption: Optional[str] = Form(None),
|
||||||
|
lyrics: Optional[str] = Form(None),
|
||||||
|
instrumental: Optional[bool] = Form(None),
|
||||||
|
inference_steps: Optional[int] = Form(None),
|
||||||
|
guidance_scale: Optional[float] = Form(None),
|
||||||
|
use_random_seed: Optional[bool] = Form(None),
|
||||||
|
seed: Optional[int] = Form(None),
|
||||||
|
thinking: Optional[bool] = Form(None),
|
||||||
|
batch_size: Optional[int] = Form(None),
|
||||||
|
audio_format: Optional[str] = Form(None),
|
||||||
|
audio_cover_strength: float = Form(0.5),
|
||||||
|
cover_noise_strength: float = Form(0.2),
|
||||||
|
):
|
||||||
|
"""Upload a source audio file and reinterpret it in a new style.
|
||||||
|
|
||||||
|
- `audio_file`: source audio (MP3, WAV, FLAC, …)
|
||||||
|
- `caption`: style description for the remix
|
||||||
|
- `lyrics`: new lyrics (or leave unset to keep existing)
|
||||||
|
- `audio_cover_strength`: how closely the remix follows the source structure (0=creative, 1=faithful; default 0.5)
|
||||||
|
- `cover_noise_strength`: melody retention level (0=pure style transfer, 0.1–0.25 recommended; default 0.2)
|
||||||
|
|
||||||
|
Returns `{"data": {"task_id": "...", "status": "queued", ...}}`.
|
||||||
|
Poll `/v1/fullsong/result/{task_id}`, then download via `/v1/fullsong/audio/{task_id}`.
|
||||||
|
"""
|
||||||
|
_require("fullsong")
|
||||||
|
suffix = Path(audio_file.filename or "audio").suffix or ".mp3"
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=suffix, prefix="kg_remix_", delete=False) as tmp:
|
||||||
|
shutil.copyfileobj(audio_file.file, tmp)
|
||||||
|
src_audio_path = tmp.name
|
||||||
|
|
||||||
|
payload: dict = {
|
||||||
|
"task_type": "cover",
|
||||||
|
"src_audio_path": src_audio_path,
|
||||||
|
"audio_cover_strength": audio_cover_strength,
|
||||||
|
"cover_noise_strength": cover_noise_strength,
|
||||||
|
}
|
||||||
|
for key, val in [
|
||||||
|
("caption", caption),
|
||||||
|
("lyrics", lyrics),
|
||||||
|
("instrumental", instrumental),
|
||||||
|
("inference_steps", inference_steps),
|
||||||
|
("guidance_scale", guidance_scale),
|
||||||
|
("use_random_seed", use_random_seed),
|
||||||
|
("seed", seed),
|
||||||
|
("thinking", thinking),
|
||||||
|
("batch_size", batch_size),
|
||||||
|
("audio_format", audio_format),
|
||||||
|
]:
|
||||||
|
if val is not None:
|
||||||
|
payload[key] = val
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = await request.app.state.http_client.post(
|
||||||
|
f"{ACESTEP_BASE_URL}/release_task",
|
||||||
|
json=payload,
|
||||||
|
)
|
||||||
|
except httpx.ConnectError:
|
||||||
|
raise HTTPException(503, "Sub-service unreachable — is the model loaded?")
|
||||||
|
return Response(
|
||||||
|
content=resp.content,
|
||||||
|
status_code=resp.status_code,
|
||||||
|
headers={k: v for k, v in resp.headers.items() if k.lower() not in ("transfer-encoding",)},
|
||||||
|
media_type=resp.headers.get("content-type"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get(
|
@app.get(
|
||||||
"/v1/fullsong/result/{task_id}",
|
"/v1/fullsong/result/{task_id}",
|
||||||
tags=["fullsong"],
|
tags=["fullsong"],
|
||||||
|
|||||||
@@ -211,6 +211,43 @@ async def fullsong_generate(req: FullsongGenerateRequest):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
"/v1/fullsong/remix",
|
||||||
|
tags=["fullsong"],
|
||||||
|
summary="Submit a remix/cover task (ACE-Step 1.5)",
|
||||||
|
)
|
||||||
|
async def fullsong_remix(
|
||||||
|
audio_file: UploadFile = File(...),
|
||||||
|
caption: Optional[str] = Form(None),
|
||||||
|
lyrics: Optional[str] = Form(None),
|
||||||
|
instrumental: Optional[bool] = Form(None),
|
||||||
|
inference_steps: Optional[int] = Form(None),
|
||||||
|
guidance_scale: Optional[float] = Form(None),
|
||||||
|
use_random_seed: Optional[bool] = Form(None),
|
||||||
|
seed: Optional[int] = Form(None),
|
||||||
|
thinking: Optional[bool] = Form(None),
|
||||||
|
batch_size: Optional[int] = Form(None),
|
||||||
|
audio_format: Optional[str] = Form(None),
|
||||||
|
audio_cover_strength: float = Form(0.5),
|
||||||
|
cover_noise_strength: float = Form(0.2),
|
||||||
|
):
|
||||||
|
"""Mock remix endpoint — accepts the same multipart form as the real server."""
|
||||||
|
_require("fullsong")
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
_tasks[task_id] = {"created_at": time.time(), "model_filename": None}
|
||||||
|
return {
|
||||||
|
"data": {
|
||||||
|
"task_id": task_id,
|
||||||
|
"status": "queued",
|
||||||
|
"queue_position": 1,
|
||||||
|
},
|
||||||
|
"code": 200,
|
||||||
|
"error": None,
|
||||||
|
"timestamp": int(time.time() * 1000),
|
||||||
|
"extra": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get(
|
@app.get(
|
||||||
"/v1/fullsong/result/{task_id}",
|
"/v1/fullsong/result/{task_id}",
|
||||||
tags=["fullsong"],
|
tags=["fullsong"],
|
||||||
|
|||||||
Reference in New Issue
Block a user