diff --git a/docs/example-api-calls.md b/docs/example-api-calls.md index 1e841ec..34ac6f9 100644 --- a/docs/example-api-calls.md +++ b/docs/example-api-calls.md @@ -111,6 +111,59 @@ Optional form fields (omit to use defaults): Poll `/v1/fullsong/result/{task_id}` and download via `/v1/fullsong/audio/{task_id}` — same flow as `/v1/fullsong/generate`. +# `/v1/fullsong/repaint` + +## Request + +```bash +curl -X 'POST' \ + 'http://127.0.0.1:8000/v1/fullsong/repaint' \ + -H 'accept: application/json' \ + -F 'audio_file=@source.mp3;type=audio/mpeg' \ + -F 'caption=Genre: Jazz piano. Style: Smooth, mellow, intimate.' \ + -F 'repainting_start=30.0' \ + -F 'repainting_end=60.0' \ + -F 'repaint_strength=0.5' +``` + +Optional form fields (omit to use defaults): + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `caption` | string | — | Style description for the repainted region | +| `lyrics` | string | — | New lyrics for the repainted region | +| `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`, …) | +| `repainting_start` | float | `0.0` | Start of repaint region in seconds | +| `repainting_end` | float | `-1.0` | End of repaint region in seconds; -1 = until end of track | +| `repaint_strength` | float | `0.5` | 0=preserve source closely, 1=full regeneration | + +`repaint_mode` is fixed to `"balanced"`. + +## 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 8ea0fd5..48ffb1d 100644 --- a/main.py +++ b/main.py @@ -283,6 +283,83 @@ async def fullsong_remix( ) +@app.post( + "/v1/fullsong/repaint", + tags=["fullsong"], + summary="Repaint a time region of an existing song (ACE-Step 1.5)", +) +async def fullsong_repaint( + 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), + repainting_start: float = Form(0.0), + repainting_end: float = Form(-1.0), + repaint_strength: float = Form(0.5), +): + """Upload a source audio file and re-generate a time region in a new style. + + - `audio_file`: source audio (MP3, WAV, FLAC, …) + - `caption`: style description for the repainted region + - `repainting_start`: start of the region to repaint in seconds (default 0.0) + - `repainting_end`: end of the region in seconds; -1 = until end of track (default -1.0) + - `repaint_strength`: 0=preserve source closely, 1=full regeneration (default 0.5) + + 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_repaint_", delete=False) as tmp: + shutil.copyfileobj(audio_file.file, tmp) + src_audio_path = tmp.name + + payload: dict = { + "task_type": "repaint", + "src_audio_path": src_audio_path, + "repaint_mode": "balanced", + "repainting_start": repainting_start, + "repainting_end": repainting_end, + "repaint_strength": repaint_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 43f1b5d..a4022d5 100644 --- a/mock-server/main.py +++ b/mock-server/main.py @@ -248,6 +248,44 @@ async def fullsong_remix( } +@app.post( + "/v1/fullsong/repaint", + tags=["fullsong"], + summary="Repaint a time region of an existing song (ACE-Step 1.5)", +) +async def fullsong_repaint( + 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), + repainting_start: float = Form(0.0), + repainting_end: float = Form(-1.0), + repaint_strength: float = Form(0.5), +): + """Mock repaint 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"],