Compare commits

1 Commits
Author SHA1 Message Date
chenmingxuan 4e572a4377 feat(voice): add ACE-Step Turbo and release 1.1.0 2026-10-15 10:52:04 +08:00
17 changed files with 569 additions and 27 deletions

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
## Project Overview
本仓库集中维护 5 个 Codex 插件及其 skill 源码。日常按插件逐个更新,发布时将所有插件作为一个整体发版。统一版本以根目录 `VERSION` 为准,当前基线为 1.0.0。
本仓库集中维护 5 个 Codex 插件及其 skill 源码。日常按插件逐个更新,发布时将所有插件作为一个整体发版。统一版本以根目录 `VERSION` 为准,当前统一版本为 1.1.0。
## Scope
+7 -6
View File
@@ -4,19 +4,20 @@
Git 远程:<https://git.playones.com/chenmingxuan/ManySkills.git>,初始分支:`main`。
当前统一版本:**1.0.0**,以根目录 [VERSION](VERSION) 为准。日常逐个更新 skill,发布时整体发版,所有插件版本保持一致。仓库协作规则见 [AGENTS.md](AGENTS.md)。
当前统一版本:**1.1.0**,以根目录 [VERSION](VERSION) 为准。日常逐个更新 skill,发布时整体发版,所有插件版本保持一致。仓库协作规则见 [AGENTS.md](AGENTS.md)。
## 发布记录
- **[v1.1.0](https://git.playones.com/chenmingxuan/ManySkills/releases/tag/v1.1.0)**(2026-10-15):eapil-voice 新增 ACE-Step 1.5 Turbo 纯音乐与歌词歌曲调用,五个插件统一升至 1.1.0。见 [发布说明](docs/releases/1.1.0.md)。Release 提供合集、内含五个独立插件 ZIP 的下载包及 SHA256 校验文件。
- **[v1.0.0](https://git.playones.com/chenmingxuan/ManySkills/releases/tag/v1.0.0)**(2026-10-06):首次整体发布,包含全部 5 个插件的更新。变更、兼容性与验证范围见 [发布说明](docs/releases/1.0.0.md)。Release 提供合集、各插件独立 ZIP 和 SHA256 校验文件。
| 插件 | 市场显示名 | 统一版本 | 入口 |
| --- | --- | --- | --- |
| `generate-agents-md` | AGENTS.md生成器 | 1.0.0 | [SKILL.md](plugins/generate-agents-md/skills/generate-agents-md/SKILL.md) |
| `design-md` | DESIGN.md生成器 | 1.0.0 | [SKILL.md](plugins/design-md/skills/design-md/SKILL.md) |
| `eapil-gpt2-image` | eapil-gpt2-image | 1.0.0 | [SKILL.md](plugins/eapil-gpt2-image/skills/eapil-gpt2-image/SKILL.md) |
| `eapil-llmapi-structured` | 易瞳LLM调用指南 | 1.0.0 | [SKILL.md](plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/SKILL.md) |
| `eapil-voice` | 易瞳音频:语音 / 转录 / 降噪 / 音乐 / 音效 | 1.0.0 | [SKILL.md](plugins/eapil-voice/skills/eapil-voice/SKILL.md) |
| `generate-agents-md` | AGENTS.md生成器 | 1.1.0 | [SKILL.md](plugins/generate-agents-md/skills/generate-agents-md/SKILL.md) |
| `design-md` | DESIGN.md生成器 | 1.1.0 | [SKILL.md](plugins/design-md/skills/design-md/SKILL.md) |
| `eapil-gpt2-image` | eapil-gpt2-image | 1.1.0 | [SKILL.md](plugins/eapil-gpt2-image/skills/eapil-gpt2-image/SKILL.md) |
| `eapil-llmapi-structured` | 易瞳LLM调用指南 | 1.1.0 | [SKILL.md](plugins/eapil-llmapi-structured/skills/eapil-llmapi-structured/SKILL.md) |
| `eapil-voice` | 易瞳音频:语音 / 转录 / 降噪 / 音乐 / 音效 | 1.1.0 | [SKILL.md](plugins/eapil-voice/skills/eapil-voice/SKILL.md) |
## 目录结构
+1 -1
View File
@@ -1 +1 @@
1.0.0
1.1.0
+31
View File
@@ -0,0 +1,31 @@
# ManySkills 1.1.0
发布日期:2026-10-15。整体标签:`v1.1.0`。五个插件统一升至 1.1.0,本轮功能更新集中在 eapil-voice。
## eapil-voice:ACE-Step 1.5 Turbo
- 新增 `POST /v1/audio/music/acestep` 和客户端 `acestep` 命令,覆盖纯音乐、指定歌词歌曲及可选 0.6B 语义 LM。
- 新增按需调用说明、UTF-8 歌词文件示例及参数校验:歌词模式冲突、10–600 秒、1–20 步、32 位无符号种子、语言、BPM、调性和拍号;拒绝混入其他端点的参数。
- 保留显式 false/0 和多行中文歌词,读取响应 `X-Audio-Seed` 中的实际种子,校验完整 WAV 后保存。
- 默认客户端超时 900 秒,失败不自动重试;区分 busy、未安装、内部超时和代理超时。成功音频按 WAV 二进制处理,不因生成的 OpenAPI 200 条目写成 JSON 而误用解析方式。
## 兼容性
- 现有插件 ID、鉴权环境变量、Stable Audio 的 `music`/`sfx` 和 Yue2 的 `yue2` 路由保留;通过新命令显式选择 ACE-Step。
- ACE-Step 使用本服务的受限统一协议,不支持上游协议中的参考音频、翻唱、局部重绘、续写、分轨或批量生成。已有服务 key 或已开通的 KeyInfo 成员 key 仍通过环境变量提供,不需要单独的 ACE-Step key。
- generate-agents-md、design-md、eapil-gpt2-image、eapil-llmapi-structured 本轮仅同步 manifest 版本;本次为兼容新功能的整体 minor 发布。
## 验证与范围
- 77 项离线回归通过:AGENTS 19、图像 13、LLM 15、音频 30(含 8 项新增 ACE-Step 测试)。
- 根 AGENTS.md、skill frontmatter、插件 manifest/资源/统一版本、文档链接及修改脚本的 Ruff 正确性与导入检查通过。
- 2026-10-15 公网 `/models` 与 `/openapi.json` 已核对模型、路由和请求 schema;纯音乐 CLI dry-run 与模拟 HTTP 歌词文件调用通过。
- 本轮未提交 ACE-Step 线上推理,也未进行试听或质量评估;维护者文档中的本机基准不作为公网性能或歌曲质量承诺。详情见 [音频验证记录](../../plugins/eapil-voice/skills/eapil-voice/references/verification.md)。
## 下载
- `ManySkills-1.1.0.zip`:完整仓库合集,插件位于 `plugins/`。
- `ManySkills-plugins-1.1.0.zip`:内含五个独立插件 ZIP,每个插件包根目录包含 `.codex-plugin/`、`skills/`、`assets/`。
- `SHA256SUMS.txt`:合集、独立插件集合包及五个插件 ZIP 的 SHA256 校验值。
发布包来自标签对应提交的跟踪文件,不含凭据、运行日志或生成媒体。本次发布至 Git/Gitea,原技能市场的同步另行执行。
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "design-md",
"version": "1.0.0",
"version": "1.1.0",
"description": "创建或完善 DESIGN.md,维护项目设计读取入口,并应用、核对界面设计规则。",
"author": {
"name": "EAPIL",
@@ -1,6 +1,6 @@
{
"name": "eapil-gpt2-image",
"version": "1.0.0",
"version": "1.1.0",
"description": "通过 EAPIL 图像 API 生成、编辑图像或完善提示词,保留精确文字、参考图角色和用户设计意图。",
"author": {
"name": "EAPIL",
@@ -1,6 +1,6 @@
{
"name": "eapil-llmapi-structured",
"version": "1.0.0",
"version": "1.1.0",
"description": "易瞳 LLM API 通用调用指南:选择、构建和实测 Chat、Responses、结构化输出、工具、视觉、流式、Embedding、Rerank 与 Decisions 请求,核对模型和中转渠道的实际兼容性。",
"author": {
"name": "EAPIL",
@@ -1,7 +1,7 @@
{
"name": "eapil-voice",
"version": "1.0.0",
"description": "易瞳音频服务调用指南:语音设计、朗读、克隆、ASR、MossFormer2/RE-USE 降噪、Stable Audio 音乐与音效、Yue2 歌曲及批量导出。包含请求校验、错误处理和实测记录。",
"version": "1.1.0",
"description": "易瞳音频服务调用指南:语音设计、朗读、克隆、ASR、MossFormer2/RE-USE 降噪、Stable Audio 音乐与音效、ACE-Step 1.5 Turbo 纯音乐/歌曲、Yue2 歌曲及批量导出。包含请求校验、错误处理和验证记录。",
"author": {
"name": "EAPIL",
"url": "https://git.playones.com/arechen/EapilSkillMarket"
@@ -18,7 +18,7 @@
"interface": {
"displayName": "易瞳音频:语音 / 转录 / 降噪 / 音乐 / 音效",
"shortDescription": "语音、ASR、降噪、音乐、音效与歌曲的调用及验证指南。",
"longDescription": "按需选择 Qwen/VoxCPM2 语音、转录、两类降噪、Stable Audio 和 Yue2 接口。配套单请求客户端与顺序批处理,校验 WAV/JSON 响应、使用准确参考转写,并明确超时不可盲目重发及部署限制。保留 eapil-voice 插件 ID。",
"longDescription": "按需选择 Qwen/VoxCPM2 语音、转录、两类降噪、Stable Audio、ACE-Step 1.5 Turbo 和 Yue2 接口。ACE-Step 支持显式纯音乐/歌词模式、语义 LM、调性拍号及实际种子读取。配套单请求客户端与顺序语音批处理,校验 WAV/JSON 响应,并明确超时不可盲目重发及部署限制。保留 eapil-voice 插件 ID。",
"developerName": "EAPIL",
"category": "Creativity",
"capabilities": [
@@ -1,6 +1,6 @@
---
name: eapil-voice
description: Build, test or use EAPIL audio API calls for speech design and cloning, transcription, denoising, music, sound effects and lyric-conditioned songs; includes sequential batch speech export.
description: Build, test or use EAPIL audio API calls for speech, transcription, denoising, music, sound effects and ACE-Step/Yue2 songs; includes sequential batch speech export.
---
# EAPIL audio service
@@ -16,6 +16,7 @@ Use the hosted service at `https://ades.playones.com`, or the deployment chosen
| MossFormer2 enhancement / alias | `/v1/audio/enhance`, `/v1/audio/denoise` | [ASR and enhancement](references/recognition-enhancement.md) |
| RE-USE denoising | `/v1/audio/reuse` | [ASR and enhancement](references/recognition-enhancement.md) |
| Instrumental music or sound effects | `/v1/audio/music`, `/v1/audio/sfx` | [Stable Audio](references/music.md) |
| ACE-Step 1.5 Turbo instrumentals or lyric songs | `/v1/audio/music/acestep` | [ACE-Step](references/acestep.md) |
| Songs from lyrics or ABC arrangement | `/v1/audio/music/yue2` | [Yue2](references/yue2.md) |
| Directory-scale ASR/clone/export | Sequential calls | [Batch audio](references/batch-audio.md) |
@@ -25,9 +26,9 @@ Read [transport and errors](references/api.md) for client integration or trouble
Use `AUDIO_API_BASE` and `AUDIO_API_TOKEN`; old `QWEN3_VOICE_API_BASE_URL` and `OPENAI_API_KEY` remain fallback aliases. Explicit flags take precedence. An available legacy key is not proof of authorization on this deployment. Inference requires Bearer authentication; health/models are public. Obtain missing credentials from the provider without putting them in chat or tracked files.
Speech design/synthesis, music/SFX and Yue2 take JSON. Clone, ASR and enhancement use multipart with file field `file`; let the client create the boundary. Audio success is complete WAV bytes, not JSON/Base64/a URL or inference streaming. ASR returns JSON. Preserve explicit false/0; keep provider and endpoint controls separate.
Speech design/synthesis, music/SFX, ACE-Step and Yue2 take JSON. Clone, ASR and enhancement use multipart with file field `file`; let the client create the boundary. Audio success is complete WAV bytes, not JSON/Base64/a URL or inference streaming. ASR returns JSON. Preserve explicit false/0; keep provider and endpoint controls separate. ACE-Step uses this service's bounded protocol, not the upstream API; choose `acestep` explicitly without rerouting existing `music` or `yue2` calls.
[call_voice_api.py](scripts/call_voice_api.py) makes one validated request. Existing command names remain; new commands are reuse, music, sfx, yue2. Dry run prints the payload without a key/network call, so share only synthetic inputs. Resolve the actual skill directory:
[call_voice_api.py](scripts/call_voice_api.py) makes one validated request; `acestep` adds Turbo music/song generation to the existing commands. Dry run prints the payload without a key/network call, so share only synthetic inputs. Resolve the actual skill directory:
~~~powershell
python '<skill-dir>/scripts/call_voice_api.py' design --text '你好。' --instruct '温暖自然、清晰的女声。' --language Chinese --max-tokens 512 --out voice.wav
@@ -0,0 +1,77 @@
# ACE-Step 1.5 Turbo music and songs
Source: maintainer's **2026-10-15** unified API guide; public [models](https://ades.playones.com/models) and [OpenAPI](https://ades.playones.com/openapi.json) inspected the same day. This is the hosted service's protocol, **not upstream ACE-Step API**.
`POST /v1/audio/music/acestep`, JSON. Model `acestep-v15-turbo`, optional semantic LM `acestep-5Hz-lm-0.6B`. Reuse the existing service Bearer token or an enabled KeyInfo member key via `AUDIO_API_TOKEN`; no separate ACE-Step key. Never embed a real key in the skill or a public frontend.
Choose ACE-Step explicitly for Turbo instrumentals or supplied-lyric songs. Existing `music`/`sfx` still call Stable Audio; `yue2` still supports its own lyric/ABC workflow. This ACE-Step route has no reference audio, covers, inpainting, continuation, stems or multi-output batch mode.
## Fields and mode checks
Only `prompt` is required. Unknown fields return 422. Character limits count Unicode characters, not UTF-8 bytes.
| Field | Default | Allowed / meaning |
|---|---|---|
| prompt | required | Nonblank 1–512 characters; describe 2–5 core traits such as genre, instruments, mood and arrangement. Put sung words in lyrics |
| lyrics | `""` | String <=4096 characters; omitted/empty is valid for instrumentals, **null is invalid** |
| instrumental | true | Boolean; set **false explicitly** for a lyric song |
| seconds | 30 | Number **10–600**; deployment may lower the upper bound, see `models.acestep.max_seconds` |
| steps | 8 | Integer **1–20**; start at 8, try 4 for drafts. Do not copy non-Turbo 30/50-step settings |
| seed | null/random | Integer **0–4294967295** or null; actual value in response `X-Audio-Seed` |
| use_lm | true | Boolean; optional 0.6B planning. False usually lowers latency but changes results; the text encoder still runs |
| vocal_language | unknown | Exact code below; zh Mandarin, yue Cantonese, en English. Not auto/zh-CN; does not translate lyrics |
| bpm | null | Integer **30–300** or null; rhythmic condition, not a guarantee of measured beat accuracy |
| keyscale | `""` | <=16 characters; empty or A–G + optional #/b/♯/♭ + space + Major/Minor, e.g. `C Major`, `F# minor`, `Bb Major`; no `Am` shorthand |
| timesignature | `""` | String `""`, `"2"`, `"3"`, `"4"`, `"6"`; correspond to unspecified, 2/4, 3/4, 4/4, 6/8. Not `"4/4"` |
`instrumental=true` allows only omitted/empty/whitespace lyrics or `[Instrumental]`. `instrumental=false` requires nonblank lyrics other than that marker. Conflicts return 422 instead of silently discarding lyrics. Instrumental conditioning does not guarantee the absence of voice-like sounds.
Language codes:
```text
ar az bg bn ca cs da de el en es fa fi fr he hi hr ht hu id
is it ja ko la lt ms ne nl no pa pl pt ro ru sa sk sr sv sw
ta te th tl tr uk ur vi yue zh unknown
```
Turbo guidance is fixed at 1.0: **do not send cfg, cfg_scale or guidance_scale**. Internal shift=3.0, batch_size=1 and ODE are not request controls. LM does not write/rewrite lyrics or prompt, and does not fill unspecified BPM/key/meter via CoT. More steps do not guarantee better quality.
## CLI and payload examples
Use existing environment credentials. In this repository prefix external commands with `rtk proxy`; replace `<skill-dir>` with the actual skill path.
~~~powershell
python '<skill-dir>/scripts/call_voice_api.py' acestep --prompt 'Warm piano, bright acoustic guitar, steady drums, upbeat indie pop. Instrumental, no vocals.' --instrumental true --seconds 30 --steps 8 --seed 7 --out music.wav
python '<skill-dir>/scripts/call_voice_api.py' acestep --prompt 'Mandarin acoustic pop, clear female vocal, warm piano, gentle drums.' --instrumental false --lyrics-file lyrics.txt --vocal-language zh --seconds 30 --steps 8 --use-lm true --bpm 120 --keyscale 'C Major' --timesignature 4 --seed 7 --out song.wav
~~~
`--lyrics` and UTF-8/BOM `--lyrics-file` are alternatives; add `--dry-run` to inspect a request without credentials or inference. The helper defaults to a 900-second timeout and prints the actual response seed when a valid `X-Audio-Seed` header is available; it does not substitute the requested seed for a missing header.
Equivalent song JSON:
```json
{
"prompt": "Mandarin acoustic pop, clear female vocal, warm piano, gentle drums.",
"instrumental": false,
"lyrics": "[Verse]\n风吹过安静的街道\n你的笑还留在转角\n[Chorus]\n我们向着晨光奔跑\n让每个明天都闪耀",
"vocal_language": "zh",
"seconds": 30,
"steps": 8,
"use_lm": true,
"seed": 7
}
```
Use a JSON serializer for real multiline strings; do not double-escape `\n`. `[Verse]`/`[Chorus]` guide structure but are not strict score instructions. 4096 input characters do not fit automatically in 30 seconds: start with a short verse/chorus, match duration to lyrics, then listen for omissions, truncation, repetition and word accuracy. Neither lyrics nor seed ensure exact performance across hardware/versions.
## Output, availability and failures
Success is complete **48 kHz stereo PCM16 WAV** with `Content-Disposition: attachment; filename=acestep.wav` and `X-Audio-Seed`. Use bytes/arrayBuffer/Blob after checking HTTP status/media; no JSON/Base64/download URL. The generated OpenAPI 200 entry currently says application/json; follow the supplied binary contract and check the actual response. Approximate PCM data: 30 s = 5.76 MB, 600 s = 115.2 MB; account for client/proxy response size and memory. CORS/proxies must expose `X-Audio-Seed` for browser access.
One active ACE-Step task per API process, including first load; busy rejects immediately with **503 + Retry-After: 2**, without queueing. Missing installation, disabled autostart and worker connection failures can also be 503; retry only explicit busy rejection with a finite bound. The helper never retries.
This is synchronous: output starts after generation, with no streaming playback, job ID, progress or cancellation. First use includes weight verification/loading/compilation; first LM use loads the LM too. Client disconnect/timeout or proxy HTML 504 may leave inference running. The guide distinguishes internal JSON 504, which terminates the worker, from proxy timeout; diagnose before manually resubmitting either.
401 needs credential correction; 422 needs schema/mode/deployment-limit correction; 500 indicates failed/invalid generation and worker cleanup. Startup and inference have separate timeouts (currently 120 + 600 seconds); a client timeout of 900 does not extend a gateway timeout.
Public `/models` reports `models.acestep` installed/state/loaded/lm_loaded/max_seconds; `/health` and `/models` do not load or prewarm models. `installed=true` is not proof that weight validation or inference succeeds; stopped/loaded=false can be normal lazy startup. Hosted callers need no local runtime/extra port. Service deployment/worker changes belong to the operator, not this skill's request workflow.
@@ -1,6 +1,6 @@
# Transport and deployed contract
Source: maintainer's API usage guide dated 2026-10-06 and live [OpenAPI](https://ades.playones.com/openapi.json). Schema limits, deployment caps and model recommendations differ; deployment settings can change.
Source: maintainer's API usage guide dated 2026-10-06, ACE-Step addition dated 2026-10-15 and live [OpenAPI](https://ades.playones.com/openapi.json). Schema limits, deployment caps and model recommendations differ; deployment settings can change.
## Requests and credentials
@@ -14,18 +14,20 @@ Size limit: 26,214,400 bytes (25 MiB). Qwen clone, ASR, MossFormer2 and RE-USE e
Audio success is complete binary audio/wav. The helper checks media type and complete RIFF format/data chunks before saving. It handles PCM/IEEE float WAV; RF64/compressed WAV needs another validated decoder. ASR must have string text; an empty transcription can represent silence, but cannot drive a batch clone.
Read actual rate/channels. Music/SFX currently use 44.1 kHz stereo PCM16; Yue2 48 kHz stereo PCM16. Provider speech rates vary. RE-USE encodes the input rate; enhancement/RE-USE mix channels to mono and are not stereo mastering tools.
Read actual rate/channels. Stable Audio music/SFX currently use 44.1 kHz stereo PCM16; Yue2 and ACE-Step 48 kHz stereo PCM16. ACE-Step returns the actual seed in X-Audio-Seed; the CLI includes it in success JSON when available. Provider speech rates vary. RE-USE encodes the input rate; enhancement/RE-USE mix channels to mono and are not stereo mastering tools.
Server-side JavaScript reads response.arrayBuffer() for audio and response.json() for ASR, once per response body. Current deployment lacks general browser CORS; use a backend proxy or configured CORS. Keep service tokens on that backend.
## Timeouts and concurrency
Speech/ASR/enhancement timeout starts at 600 seconds; music/SFX/Yue2 at 1800. Client timeouts cannot extend a gateway timeout. HTTP chunked transport is not progressive generation.
Speech/ASR/enhancement client timeout starts at 600 seconds; music/SFX/Yue2 at 1800; ACE-Step at 900 to cover its separate startup/generation budgets plus transport. Client timeouts cannot extend a gateway timeout. HTTP chunked transport is not progressive generation.
There is no async job, task query, cancellation endpoint or idempotency key. Timeout/504 does not establish that inference stopped; investigate before repeating. Do not invent a status endpoint.
Qwen design can reject busy with 503 and Retry-After. Stable Audio music/SFX share a queue. Qwen synthesis/clone share serialized execution; ASR, VoxCPM2 and Yue2 are separately serialized; MossFormer2/RE-USE share execution. Loading/connection failures can also be 503: status alone is insufficient.
ACE-Step allows one active task per API process, including first load; busy rejects immediately with 503/Retry-After: 2 instead of queueing. Read [ACE-Step](acestep.md) for its mode constraints, response-size budget and JSON-versus-HTML 504 distinction.
The single-call helper never retries. Batch --retries defaults to 1 total attempt, capped at 5. Additional attempts only follow explicit busy/running 503 detail without load/error/timeout indicators. It honors numeric Retry-After up to 60 seconds; longer backoff is left to the application, and fallback delays are bounded. Upload bodies are rebuilt in full. Ambiguous errors are failures, not retry signals.
## Errors and state
@@ -34,4 +36,4 @@ Failure detail can be a string/array or gateway HTML. Check status/media before
Library callers can inspect APIError.status/detail/busy. CLI logs omit raw provider detail because it can echo private inputs; inspect locally and redact before sharing.
Health ok means API responsiveness. Models keys include voice_design_tts, base_tts, asr, speech_enhancement, reuse, stable_audio_3, yue2, voxcpm2. Loaded=false or Yue2 stopped can be normal lazy loading. Hosted API use does not require local model startup.
Health ok means API responsiveness. Models keys include voice_design_tts, base_tts, asr, speech_enhancement, reuse, stable_audio_3, yue2, voxcpm2, acestep. Loaded=false or a stopped lazy worker can be normal. Hosted API use does not require local model startup.
@@ -1,6 +1,6 @@
# Stable Audio music and sound effects
JSON music uses Medium + SAME-L; sfx uses Small-SFX + SAME-S. Text-to-audio only: no dedicated lyrics, reference audio, inpainting or continuation. Use Yue2 for explicit lyrics. MiniMax was removed.
JSON music uses Medium + SAME-L; sfx uses Small-SFX + SAME-S. Text-to-audio only: no dedicated lyrics, reference audio, inpainting or continuation. Use [ACE-Step](acestep.md) or [Yue2](yue2.md) for explicit lyrics. MiniMax was removed. This route remains Stable Audio; ACE-Step has a separate command and schema.
| Field | Default | Schema / effective meaning |
|---|---|---|
@@ -48,3 +48,15 @@ Skill frontmatter/UI metadata, local documentation links, root AGENTS.md and all
- Denoise alias was checked through the client/offline routing; live enhancement used the primary enhance endpoint.
- Deployment caps, maximum-duration behavior, denied denoise=true, invalid-token behavior, real busy/503 handling, raw-binary uploads and browser CORS were not load-tested. Their guidance comes from the supplied contract/public schema; retry/error behavior was simulated offline.
- No broad concurrency tests, timeout resubmission, API/service reconfiguration, commit, push, tag or market publication occurred.
## ACE-Step addition — 2026-10-15
Basis: maintainer's **ACE-Step 1.5 Turbo 调用指南**, dated 2026-10-15, plus public GET inspection of hosted `/models` and `/openapi.json`. These reads did not authenticate, load models or submit inference.
- Hosted OpenAPI lists `POST /v1/audio/music/acestep` and `AceStepGenerationRequest`: 11 allowed fields, required prompt, 512/4096-character prompt/lyrics limits, 10–600 seconds, 1–20 steps, unsigned 32-bit seed, language/meter enums and no additional properties. Lyric-mode/keyscale checks also follow the supplied guide; they are not fully expressed in OpenAPI.
- `/models` reported `acestep-v15-turbo`, optional `acestep-5Hz-lm-0.6B`, installed=true, stopped, loaded/lm_loaded=false, autostart=true, max_seconds=600, max_steps=20, startup timeout=120 and generation timeout=600. Stopped is consistent with lazy startup, not a successful-inference result.
- The generated OpenAPI 200 response lists application/json; the supplied guide specifies PCM16 stereo 48 kHz WAV and X-Audio-Seed. The client checks actual WAV media/structure and reads the actual seed header rather than interpreting successful audio as JSON or echoing the requested seed.
**30 offline regression tests passed** (22 existing + 8 ACE-Step tests). Coverage added: independent endpoint/defaults without changing Stable Audio/Yue2, instrumental/lyric conflicts, Unicode character bounds, numeric types/ranges/nullability, unknown-field rejection, musical controls, UTF-8/BOM multiline lyric files, false/zero wire values, full uint32 seeds, response seed handling, valid WAV saving, and 401/422/503/500/JSON-or-HTML-504 failures without retry or overwriting existing output. HTTP was mocked; no real service token is in the fixtures. Frontmatter, changed-script Ruff correctness/import checks, local reference links and all five plugin manifests/resources/versions were also checked.
The documented instrumental request was executed through the actual CLI in dry-run mode; the song/lyric-file path was exercised by the regression suite. **No ACE-Step inference or listening test was run in this update.** The guide's four local HTTP benchmarks on Apple M3 Ultra are maintainer-reported evidence, not a reproduced hosted-service test, latency SLA or quality guarantee. No deployment configuration, installed plugin cache or 1.0.0 release/tag was changed.
@@ -23,4 +23,4 @@ python '<skill-dir>/scripts/call_voice_api.py' yue2 --style 'Mandopop ballad, fe
python '<skill-dir>/scripts/call_voice_api.py' yue2 --style 'Acoustic folk, intimate vocal' --lyrics-file lyrics.txt --mode melody --abc-file melody.abc --max-duration-seconds 30 --out arrangement.wav
~~~
Lyrics must not be blank/absent. [Instrumental] is experimental, not a guarantee; prefer Stable Audio for pure instrumentals. Current output is 48 kHz stereo PCM16. Lazy startup is handled by the service. Verify lyrics/music by listening; one direct probe does not establish melody/full/ABC.
Lyrics must not be blank/absent. [Instrumental] is experimental, not a guarantee; use Stable Audio or [ACE-Step's explicit instrumental mode](acestep.md) for pure instrumentals. ACE-Step also supports lyric songs, with a separate bounded schema and no ABC input. Current Yue2 output is 48 kHz stereo PCM16. Lazy startup is handled by the service. Verify lyrics/music by listening; one direct probe does not establish melody/full/ABC.
@@ -2,11 +2,13 @@
"""Dependency-free EAPIL audio client. One request, no inference retries."""
from __future__ import annotations
import argparse
import json
import math
import mimetypes
import os
import re
import struct
import sys
import uuid
@@ -31,9 +33,15 @@ PATHS = {
"music": "/v1/audio/music",
"sfx": "/v1/audio/sfx",
"yue2": "/v1/audio/music/yue2",
"acestep": "/v1/audio/music/acestep",
}
QWEN = ("language", "temperature", "max_tokens", "top_k", "top_p", "repetition_penalty")
VOX = ("cfg_value", "inference_timesteps", "normalize", "denoise", "retry_badcase")
ACESTEP_LANGUAGES = (
"ar az bg bn ca cs da de el en es fa fi fr he hi hr ht hu id "
"is it ja ko la lt ms ne nl no pa pl pt ro ru sa sk sr sv sw "
"ta te th tl tr uk ur vi yue zh unknown"
).split()
class APIError(RuntimeError):
@@ -210,6 +218,7 @@ def http_request(
headers=None,
timeout=600,
expected="json",
response_metadata=None,
):
if not math.isfinite(timeout) or timeout <= 0:
raise ValueError("timeout must be positive and finite")
@@ -227,6 +236,15 @@ def http_request(
.strip()
)
data = response.read()
if response_metadata is not None and path == PATHS["acestep"]:
seed = response.headers.get("X-Audio-Seed", "").strip()
if (
seed.isascii()
and seed.isdecimal()
and len(seed) <= 10
and int(seed) <= 4294967295
):
response_metadata["seed"] = int(seed)
except HTTPError as exc:
raw = exc.read().decode("utf-8", "replace")
try:
@@ -258,7 +276,7 @@ def http_request(
return data
def post_json(args, path, payload):
def post_json(args, path, payload, *, response_metadata=None):
return http_request(
method="POST",
base_url=args.base_url,
@@ -268,6 +286,7 @@ def post_json(args, path, payload):
headers={"Content-Type": "application/json"},
timeout=args.timeout,
expected="wav",
response_metadata=response_metadata,
)
@@ -294,7 +313,74 @@ def write_output(path, data):
target.write_bytes(data)
def validate_acestep_payload(payload):
allowed = {
"prompt",
"lyrics",
"instrumental",
"seconds",
"steps",
"seed",
"use_lm",
"vocal_language",
"bpm",
"keyscale",
"timesignature",
}
if payload.keys() - allowed:
raise ValueError("unknown ACE-Step fields; use its dedicated request schema")
if not isinstance(payload.get("prompt"), str) or not payload["prompt"].strip():
raise ValueError("prompt must be nonblank text")
for key, limit in (("prompt", 512), ("lyrics", 4096), ("keyscale", 16)):
value = payload.get(key, "")
if not isinstance(value, str) or len(value) > limit:
raise ValueError(f"{key} must be text of at most {limit} characters")
for key in ("instrumental", "use_lm"):
if not isinstance(payload.get(key, True), bool):
raise ValueError(f"{key} must be a boolean")
for key, low, high, integer, nullable in (
("seconds", 10, 600, False, False),
("steps", 1, 20, True, False),
("seed", 0, 4294967295, True, True),
("bpm", 30, 300, True, True),
):
if key not in payload or payload[key] is None and nullable:
continue
value = payload[key]
if (
isinstance(value, bool)
or not isinstance(value, int if integer else (int, float))
or not low <= value <= high
or not math.isfinite(value)
):
raise ValueError(
f"{key} must be {'an integer' if integer else 'a number'} between {low} and {high}"
)
language = payload.get("vocal_language", "unknown")
if not isinstance(language, str) or language not in ACESTEP_LANGUAGES:
raise ValueError("vocal_language must be an ACE-Step language code")
signature = payload.get("timesignature", "")
if not isinstance(signature, str) or signature not in {"", "2", "3", "4", "6"}:
raise ValueError('timesignature must be "", "2", "3", "4" or "6"')
keyscale = payload.get("keyscale", "")
if keyscale and not re.fullmatch(r"[A-G][#b♯♭]? (?i:major|minor)", keyscale):
raise ValueError(
'keyscale must be empty or e.g. "C Major", "F# minor", "Bb Major"'
)
lyrics = payload.get("lyrics", "").strip()
if payload.get("instrumental", True):
if lyrics and lyrics != "[Instrumental]":
raise ValueError(
"instrumental=true allows only empty lyrics or [Instrumental]"
)
elif not lyrics or lyrics == "[Instrumental]":
raise ValueError("instrumental=false requires nonblank song lyrics")
def validate_payload(command, payload):
if command == "acestep":
validate_acestep_payload(payload)
return
required = {
"design": ("text", "instruct"),
"synthesize": ("text",),
@@ -388,7 +474,11 @@ def build_parser():
sub.add_argument(
"--timeout",
type=float,
default=1800 if command in {"music", "sfx", "yue2"} else 600,
default=900
if command == "acestep"
else 1800
if command in {"music", "sfx", "yue2"}
else 600,
)
sub.add_argument("--dry-run", action="store_true")
if command in {"health", "models"}:
@@ -460,6 +550,24 @@ def build_parser():
sub.add_argument("--cfg-scale", type=float, default=1.0)
sub.add_argument("--min-duration-seconds", type=int, default=8)
sub.add_argument("--max-duration-seconds", type=int, default=120)
if command == "acestep":
sub.add_argument("--prompt", required=True)
lyrics = sub.add_mutually_exclusive_group()
lyrics.add_argument("--lyrics")
lyrics.add_argument("--lyrics-file")
sub.add_argument("--instrumental", type=bool_value, default=True)
sub.add_argument("--seconds", type=float, default=30)
sub.add_argument("--steps", type=int, default=8)
sub.add_argument("--seed", type=int)
sub.add_argument("--use-lm", type=bool_value, default=True)
sub.add_argument(
"--vocal-language", choices=ACESTEP_LANGUAGES, default="unknown"
)
sub.add_argument("--bpm", type=int)
sub.add_argument("--keyscale", default="")
sub.add_argument(
"--timesignature", choices=["", "2", "3", "4", "6"], default=""
)
return parser
@@ -484,6 +592,8 @@ def payload_from_args(args):
if key in payload:
raise ValueError(f"supply {key} text or file, not both")
payload[key] = Path(path).read_text(encoding="utf-8-sig")
if args.command == "acestep":
payload.setdefault("lyrics", "")
validate_payload(args.command, payload)
return payload
@@ -519,6 +629,7 @@ def main(argv=None):
args.token = args.token or (
os.getenv(args.api_key_env) if args.api_key_env else env_token()
)
response_metadata = {}
if args.command in {"health", "models"}:
data = http_request(
method="GET",
@@ -529,7 +640,9 @@ def main(argv=None):
elif file:
data = post_multipart(args, PATHS[args.command], payload, file)
else:
data = post_json(args, PATHS[args.command], payload)
data = post_json(
args, PATHS[args.command], payload, response_metadata=response_metadata
)
if args.command in {"asr", "health", "models"}:
print(json.dumps(json.loads(data), ensure_ascii=False, indent=2))
else:
@@ -540,6 +653,7 @@ def main(argv=None):
"ok": True,
"output": str(Path(args.out).resolve()),
**wav_info(data),
**response_metadata,
},
ensure_ascii=False,
)
@@ -0,0 +1,304 @@
from __future__ import annotations
import contextlib
import io
import json
import os
import sys
import tempfile
import unittest
import wave
from pathlib import Path
from unittest import mock
from urllib.error import HTTPError
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
import call_voice_api as api
def stereo_wav():
data = io.BytesIO()
with wave.open(data, "wb") as stream:
stream.setnchannels(2)
stream.setsampwidth(2)
stream.setframerate(48000)
stream.writeframes(b"\x01\x00\x02\x00" * 480)
return data.getvalue()
class AceStepTests(unittest.TestCase):
def setUp(self):
patch = mock.patch.object(
api, "urlopen", side_effect=AssertionError("unexpected network")
)
self.net = patch.start()
self.addCleanup(patch.stop)
env = mock.patch.dict(os.environ, {"AUDIO_API_TOKEN": "test-key"}, clear=True)
env.start()
self.addCleanup(env.stop)
def test_defaults_and_existing_routes_stay_separate(self):
args = api.build_parser().parse_args(["acestep", "--prompt", "piano"])
payload = api.payload_from_args(args)
self.assertEqual(args.timeout, 900)
self.assertEqual(payload["seconds"], 30)
self.assertEqual(payload["steps"], 8)
self.assertIs(payload["instrumental"], True)
self.assertIs(payload["use_lm"], True)
self.assertEqual(payload["lyrics"], "")
self.assertNotIn("seed", payload)
self.assertNotIn("bpm", payload)
self.assertEqual(api.PATHS["acestep"], "/v1/audio/music/acestep")
music = api.build_parser().parse_args(["music", "--prompt", "piano"])
self.assertEqual(api.PATHS[music.command], "/v1/audio/music")
self.assertEqual(music.timeout, 1800)
self.assertEqual(api.payload_from_args(music)["cfg"], 1)
self.assertNotIn("instrumental", api.payload_from_args(music))
api.validate_payload("music", {"prompt": "p" * 513, "seconds": 5})
api.validate_payload(
"yue2", {"style": "pop", "lyrics": "字" * 4097, "seed": 2**63 - 1}
)
with self.assertRaises(ValueError):
api.validate_payload("music", {"prompt": "piano", "seed": 2**32 - 1})
def test_instrumental_and_lyric_mode_conflicts(self):
for lyrics in (None, "", " \n", "[Instrumental]"):
payload = {"prompt": "piano"}
if lyrics is not None:
payload["lyrics"] = lyrics
api.validate_payload("acestep", payload)
for lyrics, instrumental in (
(None, True),
("歌声", True),
("", False),
(" \n", False),
("[Instrumental]", False),
):
with (
self.subTest(lyrics=lyrics, instrumental=instrumental),
self.assertRaises(ValueError),
):
api.validate_payload(
"acestep",
{"prompt": "pop", "lyrics": lyrics, "instrumental": instrumental},
)
api.validate_payload(
"acestep",
{"prompt": "pop", "lyrics": "[Verse]\n你好", "instrumental": False},
)
def test_string_and_numeric_boundaries(self):
api.validate_payload(
"acestep",
{
"prompt": "音" * 512,
"lyrics": "词" * 4096,
"instrumental": False,
"seconds": 600,
"steps": 20,
"seed": 2**32 - 1,
"bpm": 300,
},
)
api.validate_payload(
"acestep",
{"prompt": "piano", "seconds": 10, "steps": 1, "seed": 0, "bpm": 30},
)
api.validate_payload(
"acestep", {"prompt": "piano", "seconds": 10.5, "seed": None, "bpm": None}
)
invalid = {
"prompt": ("", " \n", "音" * 513, None),
"lyrics": (None, "词" * 4097),
"seconds": (9.99, 600.01, float("nan"), float("inf"), True, "30", None),
"steps": (0, 21, 8.5, True, "8", None),
"seed": (-1, 2**32, 10**1000, 7.5, True, "7"),
"bpm": (29, 301, 120.5, False, "120"),
"instrumental": (None, "false", 0),
"use_lm": (None, "false", 0),
}
for field, values in invalid.items():
for value in values:
with (
self.subTest(field=field, value=value),
self.assertRaises(ValueError),
):
api.validate_payload("acestep", {"prompt": "pop", field: value})
def test_music_controls_and_unknown_fields(self):
for key in ("", "C Major", "F# minor", "Bb Major", "G♯ Minor", "D♭ major"):
api.validate_payload("acestep", {"prompt": "pop", "keyscale": key})
for signature in ("", "2", "3", "4", "6"):
api.validate_payload(
"acestep", {"prompt": "pop", "timesignature": signature}
)
for lang in ("unknown", "zh", "yue", "en"):
api.validate_payload("acestep", {"prompt": "pop", "vocal_language": lang})
for field, value in (
("keyscale", "Am"),
("keyscale", "H Major"),
("keyscale", None),
("timesignature", "4/4"),
("timesignature", 4),
("vocal_language", "auto"),
("vocal_language", "zh-CN"),
("cfg", 1),
("cfg_scale", 1),
("guidance_scale", 1),
("negative_prompt", "noise"),
("abc", "X:1"),
("model", "other"),
("batch_size", 2),
):
with self.subTest(field=field), self.assertRaises(ValueError):
api.validate_payload("acestep", {"prompt": "pop", field: value})
def test_lyric_file_wire_preserves_unicode_false_and_seed_zero(self):
with tempfile.TemporaryDirectory() as directory:
lyrics = Path(directory) / "歌词.txt"
lyrics.write_text("[Verse]\n清晨的风\n[Chorus]\n你好", encoding="utf-8-sig")
args = api.build_parser().parse_args(
[
"acestep",
"--prompt",
"中文流行",
"--lyrics-file",
str(lyrics),
"--instrumental",
"false",
"--use-lm",
"false",
"--seed",
"0",
"--vocal-language",
"zh",
"--bpm",
"120",
"--keyscale",
"C Major",
"--timesignature",
"4",
]
)
args.token = "test-key"
payload = api.payload_from_args(args)
result = io.BytesIO(stereo_wav())
result.headers = {"Content-Type": "audio/wav", "X-Audio-Seed": "0"}
self.net.side_effect = None
self.net.return_value = result
metadata = {}
api.post_json(
args, api.PATHS[args.command], payload, response_metadata=metadata
)
request = self.net.call_args.args[0]
self.assertEqual(
request.full_url, "https://ades.playones.com/v1/audio/music/acestep"
)
self.assertEqual(request.get_header("Authorization"), "Bearer test-key")
self.assertEqual(request.get_header("Accept"), "audio/wav")
self.assertEqual(json.loads(request.data), payload)
self.assertEqual(payload["lyrics"], "[Verse]\n清晨的风\n[Chorus]\n你好")
self.assertIs(payload["instrumental"], False)
self.assertIs(payload["use_lm"], False)
self.assertEqual(payload["seed"], 0)
self.assertEqual(payload["timesignature"], "4")
self.assertEqual(metadata, {"seed": 0})
self.assertEqual(self.net.call_args.kwargs["timeout"], 900)
def test_dry_run_and_conflict_never_call_network(self):
output = io.StringIO()
with (
mock.patch.dict(os.environ, {}, clear=True),
contextlib.redirect_stdout(output),
):
self.assertEqual(api.main(["acestep", "--prompt", "piano", "--dry-run"]), 0)
data = json.loads(output.getvalue())
self.assertNotIn("token", data)
self.assertEqual(data["payload"]["lyrics"], "")
with contextlib.redirect_stderr(io.StringIO()):
self.assertEqual(
api.main(["acestep", "--prompt", "pop", "--lyrics", "歌词"]), 1
)
self.net.assert_not_called()
def test_cli_saves_binary_wav_and_reports_actual_response_seed(self):
with tempfile.TemporaryDirectory() as directory:
for seed in ("4294967295", None, "not-a-seed", "4294967296"):
with self.subTest(seed=seed):
result = io.BytesIO(stereo_wav())
result.headers = {"Content-Type": "audio/wav"}
if seed is not None:
result.headers["X-Audio-Seed"] = seed
self.net.side_effect = None
self.net.return_value = result
output = io.StringIO()
target = Path(directory) / "music.wav"
with contextlib.redirect_stdout(output):
self.assertEqual(
api.main(
[
"acestep",
"--prompt",
"piano",
"--seed",
"7",
"--out",
str(target),
]
),
0,
)
report = json.loads(output.getvalue())
self.assertEqual(target.read_bytes(), stereo_wav())
self.assertEqual(
(report["sample_rate"], report["channels"], report["bits"]),
(48000, 2, 16),
)
if seed == "4294967295":
self.assertEqual(report["seed"], 2**32 - 1)
else:
self.assertNotIn("seed", report)
self.assertNotIn("test-key", output.getvalue())
def test_failed_inference_never_retries_or_overwrites_output(self):
with tempfile.TemporaryDirectory() as directory:
target = Path(directory) / "music.wav"
target.write_bytes(b"existing user output")
for status, raw in (
(401, b'{"detail":"invalid key"}'),
(422, b'{"detail":"deployment duration limit"}'),
(503, b'{"detail":"ACE-Step busy"}'),
(503, b'{"detail":"ACE-Step not installed"}'),
(500, b'{"detail":"invalid output"}'),
(504, b'{"detail":"worker timeout"}'),
(504, b"<html>Gateway Time-out</html>"),
):
with self.subTest(status=status, raw=raw):
self.net.reset_mock()
self.net.side_effect = HTTPError(
"https://example.test",
status,
"error",
{"Retry-After": "2"},
io.BytesIO(raw),
)
error = io.StringIO()
with contextlib.redirect_stderr(error):
self.assertEqual(
api.main(
["acestep", "--prompt", "piano", "--out", str(target)]
),
1,
)
self.net.assert_called_once()
self.assertEqual(target.read_bytes(), b"existing user output")
report = json.loads(error.getvalue())
self.assertEqual(report["http_status"], status)
self.assertEqual(
report["confirmed_busy"], raw == b'{"detail":"ACE-Step busy"}'
)
self.assertNotIn("test-key", error.getvalue())
if __name__ == "__main__":
unittest.main()
@@ -1,6 +1,6 @@
{
"name": "generate-agents-md",
"version": "1.0.0",
"version": "1.1.0",
"description": "创建、更新或审查 AGENTS.md;基于仓库事实生成精炼、准确的 Agent 指南。",
"author": {
"name": "EAPIL",