From 3baf63735625f3c8a519bd11a28e5b11e3481ac7 Mon Sep 17 00:00:00 2001 From: Clement Tian on Paperspace Date: Wed, 29 Apr 2026 01:41:26 +0000 Subject: [PATCH] feat: added ACE-Step 1.5 powered remix API --- docs/example-api-calls.md | 51 ++++++++++++++++++++++++++ main.py | 75 +++++++++++++++++++++++++++++++++++++++ mock-server/main.py | 37 +++++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/docs/example-api-calls.md b/docs/example-api-calls.md index 0deb6ac..1e841ec 100644 --- a/docs/example-api-calls.md +++ b/docs/example-api-calls.md @@ -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}` ## Request diff --git a/main.py b/main.py index 528c2df..8ea0fd5 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ import argparse import json import logging import shutil +import tempfile import threading import urllib.parse 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( "/v1/fullsong/result/{task_id}", tags=["fullsong"], diff --git a/mock-server/main.py b/mock-server/main.py index 6e452bf..43f1b5d 100644 --- a/mock-server/main.py +++ b/mock-server/main.py @@ -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( "/v1/fullsong/result/{task_id}", tags=["fullsong"],