diff --git a/CAPABILITIES.md b/CAPABILITIES.md new file mode 100644 index 0000000..bd1896b --- /dev/null +++ b/CAPABILITIES.md @@ -0,0 +1,62 @@ +# Suno capability mapping + +Compared with [MCPs at f0eed10abf31](https://github.com/AceDataCloud/MCPs/tree/f0eed10abf310824cb4c33d4944c63d3654ac95b/suno) and the public API contract at PlatformBackend `fa94598267a82545fb1afed6ee26bafd6cbb9ca7`. + +Includes music actions, lyrics, media formats, voices, personas, custom models and projects. Voice/custom-model operations require authorized source recordings. Model creation has its own API price; review current pricing before use. + +| MCP function | Dify equivalent | Notes | +|---|---|---| +| `suno_list_models` | | Model/action selectors and the API reference; informational guidance does not submit a request. | +| `suno_list_actions` | | Model/action selectors and the API reference; informational guidance does not submit a request. | +| `suno_get_lyric_format_guide` | | Model/action selectors and the API reference; informational guidance does not submit a request. | +| `suno_generate_music` | `suno_generate_audios` | Set action=generate | +| `suno_generate_custom_music` | `suno_generate_audios` | Set action=generate | +| `suno_extend_music` | `suno_generate_audios` | Set action=extend | +| `suno_cover_music` | `suno_generate_audios` | Set action=cover | +| `suno_concat_music` | `suno_generate_audios` | Set action=concat | +| `suno_generate_with_persona` | `suno_generate_audios` | Set action=artist_consistency | +| `suno_remaster_music` | `suno_generate_audios` | Set action=remaster | +| `suno_stems_music` | `suno_generate_audios` | Set action=stems | +| `suno_replace_section` | `suno_generate_audios` | Set action=replace_section | +| `suno_upload_extend` | `suno_generate_audios` | Set action=upload_extend | +| `suno_upload_cover` | `suno_generate_audios` | Set action=upload_cover | +| `suno_mashup_music` | `suno_generate_audios` | Set action=mashup | +| `suno_all_stems_music` | `suno_generate_audios` | Set action=all_stems | +| `suno_generate_with_persona_vox` | `suno_generate_audios` | Set action=artist_consistency_vox | +| `suno_underpainting` | `suno_generate_audios` | Set action=underpainting | +| `suno_overpainting` | `suno_generate_audios` | Set action=overpainting | +| `suno_samples_music` | `suno_generate_audios` | Set action=samples | +| `suno_generate_inspo` | `suno_generate_audios` | Set action=inspo | +| `suno_generate_lyrics` | `suno_generate_lyrics` | | +| `suno_get_task` | `suno_task_retrieve` | Set action=retrieve | +| `suno_get_tasks_batch` | `suno_tasks_retrieve_batch` | Set action=retrieve_batch | +| `suno_get_mp4` | `suno_get_mp4` | | +| `suno_get_timing` | `suno_get_timing` | | +| `suno_extract_vocals` | `suno_create_vox_audio` | | +| `suno_get_wav` | `suno_get_wav` | | +| `suno_get_mp3` | `suno_get_mp3` | | +| `suno_get_midi` | `suno_get_midi` | | +| `suno_optimize_style` | `suno_enhance_style` | | +| `suno_mashup_lyrics` | `suno_mashup_lyrics` | | +| `suno_upload_audio` | `suno_upload_reference_audio` | | +| `suno_create_voice` | `suno_create_voice` | | +| `suno_create_persona` | `suno_create_persona` | | +| `suno_list_personas` | `suno_list_personas` | | +| `suno_delete_persona` | `suno_delete_persona` | | +| `suno_create_custom_model` | `suno_custom_models` | Set action=create | +| `suno_get_custom_model` | `suno_custom_models` | Set action=retrieve | +| `suno_list_custom_models` | `suno_custom_models` | Set action=retrieve_batch | +| `suno_generate_with_custom_model` | `suno_custom_models` | Set action=generate | +| `suno_archive_custom_model` | `suno_custom_models` | Set action=delete | + +## Parameter equivalents + +- `suno_replace_section`: `result_mode` → replace_section_result_mode. +- `suno_get_tasks_batch`: `task_ids` → ids. +- `suno_get_custom_model`: `model_id` → id. +- `suno_generate_with_custom_model`: `model_id` → id. +- `suno_archive_custom_model`: `model_id` → id. + +## Verification boundary + +Contract examples and regression tests cover request validation, transport and task handling. Actual Dify browser cases are recorded separately in `tests/e2e-results.json` and `tests/e2e-audit.json` when available. A schema test is not a successful paid generation. Unsupported service availability and untested advanced combinations must not be described as passed. diff --git a/PRIVACY.md b/PRIVACY.md index ad9f833..a6cf1fd 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -2,8 +2,10 @@ Effective date: 2026-10-07. Maintainer/contact: Ace Data Cloud, dev@acedata.cloud. -This plugin sends your API key in an HTTPS Authorization header to `api.acedata.cloud`. It sends the inputs you choose for music generation from descriptions or custom lyrics, including prompts, text or lyrics, settings, reference media URLs where supported, and task IDs. Such inputs may contain personal information if you include it. The API also receives ordinary connection metadata, including the Dify server's IP address and usage required to operate the service. +This plugin sends your API key in an HTTPS Authorization header to `api.acedata.cloud`. It sends the inputs you choose for the selected Suno operation, including prompts, text, settings, reference media URLs and identifiers as applicable. Such inputs may contain personal information if you include it. The API receives ordinary connection metadata, including the Dify server's IP address, and usage information required to operate and bill the service. Voice, persona and custom-model operations, where exposed, send authorized recordings, transcripts and the selected metadata. They may create persistent API-side assets or incur separate training/creation charges. Remove those assets using the applicable API or account controls. -The plugin's only direct API recipient is Ace Data Cloud. API processing, retention and service providers follow the [Ace Data Cloud privacy policy](https://platform.acedata.cloud/privacy). Reference URLs are sent to the API; the plugin does not fetch arbitrary URLs or download media. It returns selected result fields and media links to Dify. It does not intentionally log keys, raw API errors, or saved request bodies, and has no database, telemetry or additional persistent storage. Dify handles credential storage, workflow history and downstream media rendering under your deployment's policies. API-side zero retention is not promised by this plugin. +The plugin's only direct API recipient is Ace Data Cloud. API processing, retention and service providers follow the [Ace Data Cloud privacy policy](https://platform.acedata.cloud/privacy). The plugin passes reference URLs to the API without fetching those references itself. It returns public result fields and output links to Dify. Dify may fetch or render the links under your deployment's policies. -Only submit text and media you are authorized to use. Remove the credential from Dify or revoke it in the Ace Data Cloud console to stop use. Contact dev@acedata.cloud for API data questions and your Dify administrator for local history. This release does not train custom models, clone voices, execute generated code/tools, or perform payments or asset transfers. +The plugin excludes credentials, private stored request bodies, account/routing metadata and raw service-error details from normalized outputs. It has no database, telemetry or additional persistent storage. Dify handles credential storage, workflow histories and downstream rendering. The plugin does not promise API-side zero retention. + +Only submit content and recordings that you are authorized to process. Delete/archive operations exposed by the plugin require an explicit confirmation input. Remove the credential from Dify or revoke it in the Ace Data Cloud console to stop use. Contact dev@acedata.cloud for API data questions and your Dify administrator for local history. diff --git a/README.md b/README.md index 367db3c..b438739 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,74 @@ -# Suno +# Suno for Dify -**Author:** acedatacloud +Use the Ace Data Cloud Suno APIs in Dify workflows. Maintained by Ace Data Cloud. The plugin is free; API calls require your own authorized account and use the current service pricing. -**Type:** tool provider plugin +## Setup -**API:** `https://api.acedata.cloud/suno/audios` +1. Activate the service at [Ace Data Cloud](https://platform.acedata.cloud/console/applications), check [current pricing](https://platform.acedata.cloud/models), and create an API token with the required service access. +2. Install from the official Dify Marketplace once the submission is approved and published. During review, use Dify's documented package/debug installation in a test workspace. A GitHub PR does not establish Marketplace availability or default installation. +3. In Dify's **Plugins / Tools** page, authorize this provider with **Bearer Token** (`acedata_bearer_token`). Do not include credentials in prompts or exported workflows. Credential validation never generates media. Authorization performs a read-only task query. ## Tools -| Tool ID | Purpose | Inputs | -|---|---|---| -| `suno_generate_audios` | Suno Generate Audios | `prompt`, `model`, `custom`, `lyric`, `title`, `style`, `instrumental`, `duration` | -| `suno_task_retrieve` | Suno Retrieve Task | `task_id`, `wait_seconds` | +| Tool | API | +|---|---| +| `suno_generate_audios` | `POST /suno/audios` | +| `suno_generate_lyrics` | `POST /suno/lyrics` | +| `suno_upload_reference_audio` | `POST /suno/upload` | +| `suno_get_mp4` | `POST /suno/mp4` | +| `suno_get_mp3` | `POST /suno/mp3` | +| `suno_get_wav` | `POST /suno/wav` | +| `suno_get_midi` | `POST /suno/midi` | +| `suno_get_timing` | `POST /suno/timing` | +| `suno_create_vox_audio` | `POST /suno/vox` | +| `suno_enhance_style` | `POST /suno/style` | +| `suno_mashup_lyrics` | `POST /suno/mashup-lyrics` | +| `suno_create_voice` | `POST /suno/voices` | +| `suno_create_persona` | `POST /suno/persona` | +| `suno_list_personas` | `GET /suno/persona` | +| `suno_delete_persona` | `DELETE /suno/persona` | +| `suno_custom_models` | `POST /suno/custom-models` | +| `suno_projects` | `POST /suno/projects` | +| `suno_task_retrieve` | `POST /suno/tasks` | +| `suno_tasks_retrieve_batch` | `POST /suno/tasks` | -Outputs include `status`, `task_id`, `media_urls`, `result`, and the earlier plugin conventions `success`, `trace_id`, `data`. `success` is true only when results are complete; failures raise tool errors. +See [CAPABILITIES.md](CAPABILITIES.md) for the current MCP comparison and parameter equivalents. All exposed inputs follow the current published API; model combinations and availability still depend on the service. -The credential field is `acedata_bearer_token`; paste the token without the `Bearer ` prefix. +## Run a workflow -Generate Suno music from descriptions or custom lyrics with Ace Data Cloud. Maintained by Ace Data Cloud. This is a free Dify tool plugin; API use requires your own Ace Data Cloud account and may incur usage charges. +For generation, use **Start → generation tool → task retrieval → Output**. Fill the prompt/text and model, and enter arrays/objects as JSON. Optional values can be left empty. The example requests in [tests/contract-examples.json](https://github.com/AceDataCloud/SunoDify/blob/main/tests/contract-examples.json) show valid shapes; example.org URLs are placeholders that must be replaced with your own accessible media. -## Setup +A submission can return `status=pending` with `task_id`. Save that ID, then use the retrieval tool with `wait_seconds=0` to read once, or 1–240 for a bounded wait. If still pending, query the same task again. Disable automatic retries on generation nodes. No paid request is automatically retried and no substitute model is selected. -1. Sign in at [Ace Data Cloud](https://platform.acedata.cloud/console/applications), activate the service and check [current pricing](https://platform.acedata.cloud/models). -2. Create an API key authorized for this service. A service key is scoped; use a global key only for services your account can access. -3. Install this plugin from Dify Marketplace **after it is published**. During review, source/package tests do not imply official listing. A development package can use Dify's documented remote-debug workflow in an isolated workspace. -4. In Dify **Integrations → Tools** (or **Plugins** in older versions), authorize the plugin with the key. Credential validation makes only a read-only task query; it never generates media. +`status`, `success`, `task_id`, `trace_id`, `media_urls`, `data`, and `result` are available as Dify variables. Only a terminal successful result has `success=true`; intermediate previews remain pending. Batch queries preserve the state of each item. Terminal task failures raise a tool error. Synchronous search, text and management results are returned directly in `data`/`result`. The plugin does not execute model-generated tools. -## Workflow +Includes music actions, lyrics, media formats, voices, personas, custom models and projects. Voice/custom-model operations require authorized source recordings. Model creation has its own API price; review current pricing before use. -1. Create **Start → Generate → Output** and select this plugin's generation tool. Fill the required prompt/text, model and options. Disable automatic retries for generation. -2. Save `task_id` from the result. Status `pending` means accepted, not finished. -3. Pass the same ID to **Retrieve or wait for task**. Set **Wait up to seconds** to 120–240 for a bounded wait, or 0 for one read. If it still returns `pending`, wait and query the same ID again. Do not resubmit generation. -4. When `status` is `succeeded`, read `media_urls` for the image/audio/video links. Use **Output** (or Chatflow **Answer**) to return the URLs. Task failures raise a Dify tool error, rather than returning a false success. +Task/query calls retry transport failures at most twice. Generation has one attempt and a 10-second connect / 60-second read timeout. After a timeout, inspect [request history](https://platform.acedata.cloud/console/usages) before resubmitting; the accepted task may still be running. Delete/archive operations require `confirm=true`. -For custom mode, supply style and lyrics (lyrics can be omitted for instrumental music). Duration is a target, not a guaranteed track length. Intermediate audio previews are never reported as completed media. +## Branding and privacy -This release covers music generation from descriptions or custom lyrics. Task queries return output fields only, excluding stored requests, account IDs and routing metadata. Generation uses asynchronous requests. Each HTTPS request has a 10-second connect and 60-second read timeout; a wait call polls for at most 240 seconds. No automatic paid retries or model fallback is implemented. On a submission timeout, check request history before attempting another submission. +The plugin uses the exact existing system asset recorded in [branding provenance](https://github.com/AceDataCloud/SunoDify/blob/main/tests/branding-source.json), for both light and dark icons. No logo was generated or redrawn. Asset SHA256: `d2ccb1c0039519a68913ad447114f8f9ecca161ffc2105cd61c414e06f8747d9`. -## Billing, network and privacy +Requests go directly to `https://api.acedata.cloud`. The plugin passes reference URLs to that API and returns media links; it does not fetch arbitrary reference URLs. Dify may fetch/render output links under its own policies. Never submit media you lack permission to process. See [PRIVACY.md](PRIVACY.md). -The plugin connects only to `api.acedata.cloud:443`; it does not download reference URLs or media itself. URLs are passed to the API as inputs or returned for downstream use. Only submit content you are authorized to process. +API charges are recorded in Credits in your Ace Data Cloud account. Check the actual usage ledger; Dify execution counts are not a billing ledger. USD = Credits × your current package price / amount. -Check [Usage](https://platform.acedata.cloud/console/usages) by key and time after a real call. Charges are in Credits; USD = Credits × your current package price / amount. Dify tool execution counts are not the billing ledger. A completed task must also have usable media. See [PRIVACY.md](PRIVACY.md). +## Development and evidence -Python 3.12 and `dify-plugin==0.9.1` are required. Default models and listed options may change with the service; check the catalog before using a different model. Contact support for access errors; reduce concurrency on HTTP 429. Do not put keys into prompts or exported workflows. +Python 3.12 is required. Install `requirements.txt`, then run: -## Source and support +```sh +python -m pytest tests -q +ruff check . +ruff format --check . +dify plugin package . +``` -- Source repository: https://github.com/AceDataCloud/SunoDify -- Contact: dev@acedata.cloud +Source contracts, MCP mappings, brand provenance and offline cases are in `tests/`. Recorded real Dify results state their exact coverage; they do not establish all models/options or Dify Cloud/Marketplace installation. See [tests/README.md](https://github.com/AceDataCloud/SunoDify/blob/main/tests/README.md). + +- Source: https://github.com/AceDataCloud/SunoDify - Issues: https://github.com/AceDataCloud/SunoDify/issues -- [Simplified Chinese](readme/README_zh_Hans.md) +- Contact: dev@acedata.cloud - License: MIT - -Development: install `requirements.txt`, run `python -m pytest tests -q`, `ruff check .`, and `dify plugin package .`. Test results and limits are recorded in the submission PR; Marketplace publication is separate from local validation. - -## Earlier Dify plugin conventions - -This repository follows the earlier service-specific icon, tool naming, Bearer Token credential, bilingual metadata and code layout. The tool table above defines this release's scope; it does not restore all historical tools or the private fork's release workflows. +- [Simplified Chinese](readme/README_zh_Hans.md) diff --git a/_assets/icon.jpg b/_assets/icon.jpg index ddaac9d..91f594a 100644 Binary files a/_assets/icon.jpg and b/_assets/icon.jpg differ diff --git a/provider/suno.yaml b/provider/suno.yaml index 3c3cfd2..88dd026 100644 --- a/provider/suno.yaml +++ b/provider/suno.yaml @@ -31,7 +31,24 @@ credentials_for_provider: url: https://platform.acedata.cloud/console/credentials tools: - tools/suno_generate_audios.yaml +- tools/suno_generate_lyrics.yaml +- tools/suno_upload_reference_audio.yaml +- tools/suno_get_mp4.yaml +- tools/suno_get_mp3.yaml +- tools/suno_get_wav.yaml +- tools/suno_get_midi.yaml +- tools/suno_get_timing.yaml +- tools/suno_create_vox_audio.yaml +- tools/suno_enhance_style.yaml +- tools/suno_mashup_lyrics.yaml +- tools/suno_create_voice.yaml +- tools/suno_create_persona.yaml +- tools/suno_list_personas.yaml +- tools/suno_delete_persona.yaml +- tools/suno_custom_models.yaml +- tools/suno_projects.yaml - tools/suno_task_retrieve.yaml +- tools/suno_tasks_retrieve_batch.yaml extra: python: source: provider/suno.py diff --git a/readme/README_zh_Hans.md b/readme/README_zh_Hans.md index b2b4186..d9da302 100644 --- a/readme/README_zh_Hans.md +++ b/readme/README_zh_Hans.md @@ -1,49 +1,57 @@ -# Suno +# Suno Dify 插件 -**Author:** acedatacloud +在 Dify 中使用 Ace Data Cloud 的 Suno API。插件免费,API 需要自己的账号和服务权限,并按当前价格扣费。 -**Type:** tool provider plugin +## 安装与授权 -**API:** `https://api.acedata.cloud/suno/audios` +1. 在 [Ace Data Cloud](https://platform.acedata.cloud/console/applications) 开通服务并创建 API Token;使用前核对[当前模型与价格](https://platform.acedata.cloud/models)。 +2. 官方审核并上架后,可从 Dify Marketplace 安装。审核期间可以在测试环境使用 Dify 官方的包安装或远程调试流程。PR 已提交不表示市场已发布,也不表示默认预装。 +3. 在 Dify 的插件或工具页填写 Bearer Token(`acedata_bearer_token`)。凭据检查不会生成内容。授权使用只读任务查询。 -## 工具 +## 工具与工作流 -| 工具 ID | 功能 | 参数 | -|---|---|---| -| `suno_generate_audios` | Suno 生成歌曲 | `prompt`, `model`, `custom`, `lyric`, `title`, `style`, `instrumental`, `duration` | -| `suno_task_retrieve` | Suno 查询任务 | `task_id`, `wait_seconds` | +| Tool | API | +|---|---| +| `suno_generate_audios` | `POST /suno/audios` | +| `suno_generate_lyrics` | `POST /suno/lyrics` | +| `suno_upload_reference_audio` | `POST /suno/upload` | +| `suno_get_mp4` | `POST /suno/mp4` | +| `suno_get_mp3` | `POST /suno/mp3` | +| `suno_get_wav` | `POST /suno/wav` | +| `suno_get_midi` | `POST /suno/midi` | +| `suno_get_timing` | `POST /suno/timing` | +| `suno_create_vox_audio` | `POST /suno/vox` | +| `suno_enhance_style` | `POST /suno/style` | +| `suno_mashup_lyrics` | `POST /suno/mashup-lyrics` | +| `suno_create_voice` | `POST /suno/voices` | +| `suno_create_persona` | `POST /suno/persona` | +| `suno_list_personas` | `GET /suno/persona` | +| `suno_delete_persona` | `DELETE /suno/persona` | +| `suno_custom_models` | `POST /suno/custom-models` | +| `suno_projects` | `POST /suno/projects` | +| `suno_task_retrieve` | `POST /suno/tasks` | +| `suno_tasks_retrieve_batch` | `POST /suno/tasks` | -输出包含 `status`、`task_id`、`media_urls`、`result`,以及旧插件约定的 `success`、`trace_id`、`data`。`success` 仅在结果完成时为 true;失败会抛出工具错误。 +完整 MCP 对照、参数别名和适用范围见 [CAPABILITIES.md](../CAPABILITIES.md)。复杂参数填写 JSON,示例见 [contract-examples.json](https://github.com/AceDataCloud/SunoDify/blob/main/tests/contract-examples.json);其中 example.org 链接须替换成自己的可访问媒体。 -凭据字段为 `acedata_bearer_token`,粘贴 Token 时不需要 `Bearer ` 前缀。 +生成工作流连接 **开始 → 生成工具 → 查询任务 → 输出**。提交返回 `pending` 时,保存 `task_id`,并使用同一 ID 查询。`wait_seconds=0` 查询一次,1–240 表示最多等待相应秒数。仍在运行时继续查询原任务,不重新提交生成。请关闭生成节点的自动重试。 -通过 Ace Data Cloud 使用描述或自定义歌词生成音乐。插件免费安装,API 使用需要自己的账户与额度。 +输出包含 `status`、`success`、`task_id`、`trace_id`、`media_urls`、`data` 和 `result`。只有任务最终完成才返回成功;预览链接不算完成。批量查询分别保留每个任务的状态。同步操作直接返回结果。删除或归档需要设置 `confirm=true`。插件不会执行模型返回的工具调用。 -## 配置 +本版补充了当前服务接口的参数和操作;不同 MCP 函数可能合并为操作选项或结构化输入。图片接口固定返回 URL,流式接口使用 Dify 可处理的完整响应及异步任务查询。每个高级操作和模型是否已真实验证,以测试证据记录为准。 -1. 登录 [控制台](https://platform.acedata.cloud/console/applications),开通服务并检查[当前价格](https://platform.acedata.cloud/models)。创建有该服务权限的 API Key。 -2. 官方上架后,从 Dify Marketplace 安装;审核中的源码或插件包不等于官方已收录。开发验证可以在隔离工作区使用官方远程调试。 -3. 在 **Integrations → Tools(集成 → 工具)** 或旧版插件页面填写 Key。验证凭据只查询任务,不生成付费媒体。 -4. 工作流连接**开始 → 生成工具 → 输出**。填写提示词或文本、模型和选项,关闭生成步骤自动重试。 -5. 保存返回的 `task_id`,传给 **Retrieve or wait for task**。等待秒数为 0 时只查询一次,120–240 时进行有限等待。`pending` 表示未完成;继续查询同一 ID,不要重发生成。 -6. `status=succeeded` 后,通过 `media_urls` 获取媒体链接。用输出节点或 Chatflow 回答节点返回链接;任务失败会产生工具错误。 +## Logo、隐私与费用 -## 范围与计费 +浅色和深色图标复用系统中已有的官方服务资产,未经重绘;来源和 SHA256 见 [branding-source.json](https://github.com/AceDataCloud/SunoDify/blob/main/tests/branding-source.json)。 -本版支持描述或自定义歌词生成音乐及任务查询。编辑图片时填写公开 HTTPS 图片地址;Seedance 本版只支持文本或首帧输入;Fish 本版不含声音克隆;Suno 的时长是目标值,中间预览不视作完成结果。 +插件仅直接连接 `api.acedata.cloud`,把用户选择的文本、参数、参考媒体 URL 和任务 ID 发给 API,并向 Dify 返回结果。只提交已获授权的媒体。音色和自定义模型创建涉及对应的训练或克隆处理,须先核对素材权限和价格。详见 [隐私说明](../PRIVACY.md)。 -到[用量页](https://platform.acedata.cloud/console/usages)按 Key 和时间核对实际扣减。Credits 的 USD 换算使用当前套餐价格与额度。异步受理、任务成功、文件可读取及扣费应分别验证。 +使用记录以 Ace Data Cloud 的账单为准,单位为 Credits;USD = Credits × 当前套餐价格 / 额度。生成超时后先检查请求历史,避免重复扣费。插件没有自动付费重试或模型替换。 -连接超时 10 秒,读取超时 60 秒,单次任务等待不超过 240 秒。网络不确定时先查请求历史,不盲目重发。本插件不自动重试付费请求或更换模型。 +## 源码与验证 -## 隐私与支持 +- 源码:https://github.com/AceDataCloud/SunoDify +- 问题反馈:https://github.com/AceDataCloud/SunoDify/issues +- 联系:dev@acedata.cloud -Key、提示词、歌词/文本及所选参考媒体 URL 通过 HTTPS 发给 `api.acedata.cloud`。插件不下载任意 URL、不执行代码或额外持久化内容,Dify 管理凭据与运行历史。只使用有权处理的素材,详见 [隐私政策](../PRIVACY.md)。 - -源码:https://github.com/AceDataCloud/SunoDify - -支持:dev@acedata.cloud - -## 与旧 Dify 插件的对应 - -本仓库沿用旧插件的服务图标、工具命名、Bearer Token 配置、中英文说明和代码组织方式。运行范围以上方工具表为准;这是独立的官方市场投稿,不是旧 19 插件发布链的恢复。 +需要 Python 3.12。运行 `python -m pytest tests -q`、`ruff check .`、`ruff format --check .` 和 `dify plugin package .`。真实 Dify 截图与结果、未覆盖项见 tests 目录;单元测试通过不等于所有模型真实调用通过,市场上架和安装另行验收。 diff --git a/requirements.txt b/requirements.txt index e9fc1b2..f93fcb3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ dify-plugin==0.9.1 +requests==2.34.2 +jsonschema==4.25.1 diff --git a/tests/README.md b/tests/README.md index fea02c4..71d18ab 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,17 +1,10 @@ -# Live E2E validation +# Verification evidence -`e2e-results.json` records real Dify CE workflows, not mocked API calls. -Use `e2e-cases.json` as the tool inputs for an isolated Dify 1.17.1 workspace. +- `test_plugin.py` and `contract-examples.json`: SDK registration, Dify defaults, request shapes, read-only credential validation, task/batch states, error redaction and no paid retries. +- `branding-source.json`: exact source asset and SHA256. +- `parity-audit.json` / `mcp-parity.json`: current backend/MCP revisions and operation/parameter mapping. +- `e2e-results.json`: previous actual Dify primary-operation results, when available. +- `e2e-audit.json`: second-audit actual Dify workflows, when available. +- `e2e-blocked.json`: service failures, when present; these are not passing cases. -1. Enable the official remote-debug connection in your workspace and run `python -m main` with the workspace debug host/key. Keep signature verification enabled. -2. Authorize this provider with your service API key. Credential validation only queries task metadata. -3. Build Start → this plugin tool → Output. Put form parameters in the tool configuration and prompt/text parameters in tool inputs. Return status, task_id, media_urls and result. Disable generation retries. -4. Run each generation/edit case once, preserving its task ID. Poll via this plugin's task tool until succeeded. Do not regenerate to recover a timeout. -5. Check each output URL, decode the media, and match the task/trace and nonzero deduction in Ace Data Cloud Usage. -6. Remove the test credential and stop the isolated test environment. - -These cases consume real API usage. They are never part of CI. Results do not assert Dify Cloud, browser interaction, or Marketplace publication. - -## Service convention alignment - -`e2e-alignment.json` records the subsequent read-only verification of restored plugin/tool IDs, service icons, bilingual metadata, `acedata_bearer_token` authorization and actual task workflows. All nine earlier paid input cases map to identical API request payloads after the refactor. Existing task IDs were retrieved again; no new generation was submitted. Image tasks now also return native Dify image files, and `success`, `trace_id`, `data` follow the earlier tool conventions. +Paid real calls are never run by the unit suite or public CI. A passing schema test is not proof of service availability. Each browser case must include terminal workflow and service status; media outputs require decoding, and new paid calls require usage reconciliation. Marketplace installation is a separate gate after official publication. diff --git a/tests/asset-and-convention-sources.json b/tests/asset-and-convention-sources.json index a3700cf..7f75604 100644 --- a/tests/asset-and-convention-sources.json +++ b/tests/asset-and-convention-sources.json @@ -22,5 +22,6 @@ "id": "suno_task_retrieve", "class": "SunoTaskRetrieveTool" } - ] + ], + "historical_note": "This records the earlier implementation baseline. Current icon bytes and provenance are in tests/branding-source.json, updated after the current-system brand audit." } diff --git a/tests/branding-source.json b/tests/branding-source.json new file mode 100644 index 0000000..7d5215c --- /dev/null +++ b/tests/branding-source.json @@ -0,0 +1,5 @@ +{ + "url": "https://cdn.acedata.cloud/l3ffw7.jpg", + "source": "live service icon_url", + "sha256": "d2ccb1c0039519a68913ad447114f8f9ecca161ffc2105cd61c414e06f8747d9" +} diff --git a/tests/contract-examples.json b/tests/contract-examples.json new file mode 100644 index 0000000..71d3315 --- /dev/null +++ b/tests/contract-examples.json @@ -0,0 +1,305 @@ +[ + { + "name": "suno_generate_audios-1", + "tool": "suno_generate_audios", + "inputs": { + "prompt": "Test" + }, + "method": "POST", + "path": "/suno/audios", + "body": { + "model": "chirp-v6", + "action": "generate" + } + }, + { + "name": "suno_generate_audios-2", + "tool": "suno_generate_audios", + "inputs": { + "action": "adjust_speed", + "audio_id": "owned-audio", + "speed_multiplier": 1.1, + "keep_pitch": true + }, + "method": "POST", + "path": "/suno/audios", + "body": { + "action": "adjust_speed", + "speed_multiplier": 1.1 + } + }, + { + "name": "suno_generate_lyrics-3", + "tool": "suno_generate_lyrics", + "inputs": { + "prompt": "Test" + }, + "method": "POST", + "path": "/suno/lyrics", + "body": { + "prompt": "Test", + "model": "default" + } + }, + { + "name": "suno_upload_reference_audio-4", + "tool": "suno_upload_reference_audio", + "inputs": { + "audio_url": "https://example.org/reference.mp3" + }, + "method": "POST", + "path": "/suno/upload", + "body": { + "audio_url": "https://example.org/reference.mp3" + } + }, + { + "name": "suno_enhance_style-5", + "tool": "suno_enhance_style", + "inputs": { + "prompt": "Test" + }, + "method": "POST", + "path": "/suno/style", + "body": { + "prompt": "Test" + } + }, + { + "name": "suno_mashup_lyrics-6", + "tool": "suno_mashup_lyrics", + "inputs": { + "lyrics_a": "One", + "lyrics_b": "Two" + }, + "method": "POST", + "path": "/suno/mashup-lyrics", + "body": { + "lyrics_a": "One" + } + }, + { + "name": "suno_create_voice-7", + "tool": "suno_create_voice", + "inputs": { + "audio_url": "https://example.org/reference.mp3", + "name": "Owned voice" + }, + "method": "POST", + "path": "/suno/voices", + "body": { + "audio_url": "https://example.org/reference.mp3" + } + }, + { + "name": "suno_create_persona-8", + "tool": "suno_create_persona", + "inputs": { + "audio_id": "owned-audio", + "name": "Owned persona" + }, + "method": "POST", + "path": "/suno/persona", + "body": { + "audio_id": "owned-audio" + } + }, + { + "name": "suno_create_vox_audio-9", + "tool": "suno_create_vox_audio", + "inputs": { + "audio_id": "owned-audio", + "vocal_start": 0, + "vocal_end": 3 + }, + "method": "POST", + "path": "/suno/vox", + "body": { + "vocal_end": 3 + } + }, + { + "name": "suno_get_mp4-10", + "tool": "suno_get_mp4", + "inputs": { + "audio_id": "owned-audio" + }, + "method": "POST", + "path": "/suno/mp4", + "body": { + "audio_id": "owned-audio" + } + }, + { + "name": "suno_get_mp3-11", + "tool": "suno_get_mp3", + "inputs": { + "audio_id": "owned-audio" + }, + "method": "POST", + "path": "/suno/mp3", + "body": { + "audio_id": "owned-audio" + } + }, + { + "name": "suno_get_wav-12", + "tool": "suno_get_wav", + "inputs": { + "audio_id": "owned-audio" + }, + "method": "POST", + "path": "/suno/wav", + "body": { + "audio_id": "owned-audio" + } + }, + { + "name": "suno_get_midi-13", + "tool": "suno_get_midi", + "inputs": { + "audio_id": "owned-audio" + }, + "method": "POST", + "path": "/suno/midi", + "body": { + "audio_id": "owned-audio" + } + }, + { + "name": "suno_get_timing-14", + "tool": "suno_get_timing", + "inputs": { + "audio_id": "owned-audio" + }, + "method": "POST", + "path": "/suno/timing", + "body": { + "audio_id": "owned-audio" + } + }, + { + "name": "suno_list_personas-15", + "tool": "suno_list_personas", + "inputs": { + "user_id": "owned-user" + }, + "method": "GET", + "path": "/suno/persona", + "body": {}, + "query": { + "user_id": "owned-user" + }, + "read_only": true + }, + { + "name": "suno_delete_persona-16", + "tool": "suno_delete_persona", + "inputs": { + "persona_id": "owned-persona", + "confirm": true + }, + "method": "DELETE", + "path": "/suno/persona", + "body": {}, + "query": { + "persona_id": "owned-persona" + } + }, + { + "name": "suno_delete_persona-17", + "tool": "suno_delete_persona", + "inputs": { + "persona_id": "owned-persona" + }, + "method": "DELETE", + "path": "/suno/persona", + "body": {}, + "error": true + }, + { + "name": "suno_custom_models-18", + "tool": "suno_custom_models", + "inputs": { + "action": "retrieve_batch", + "limit": 2 + }, + "method": "POST", + "path": "/suno/custom-models", + "body": { + "action": "retrieve_batch", + "limit": 2 + }, + "read_only": true + }, + { + "name": "suno_custom_models-19", + "tool": "suno_custom_models", + "inputs": { + "action": "create", + "name": "Owned music", + "audio_urls": "[\"https://example.org/authorized-0.mp3\", \"https://example.org/authorized-1.mp3\", \"https://example.org/authorized-2.mp3\", \"https://example.org/authorized-3.mp3\", \"https://example.org/authorized-4.mp3\", \"https://example.org/authorized-5.mp3\"]" + }, + "method": "POST", + "path": "/suno/custom-models", + "body": { + "name": "Owned music", + "audio_urls": [ + "https://example.org/authorized-0.mp3", + "https://example.org/authorized-1.mp3", + "https://example.org/authorized-2.mp3", + "https://example.org/authorized-3.mp3", + "https://example.org/authorized-4.mp3", + "https://example.org/authorized-5.mp3" + ] + } + }, + { + "name": "suno_projects-20", + "tool": "suno_projects", + "inputs": { + "action": "retrieve", + "id": "owned-project" + }, + "method": "POST", + "path": "/suno/projects", + "body": { + "action": "retrieve", + "id": "owned-project" + }, + "read_only": true + }, + { + "name": "suno_projects-21", + "tool": "suno_projects", + "inputs": { + "action": "create", + "title": "Owned project", + "idempotency_key": "owned-idempotency" + }, + "method": "POST", + "path": "/suno/projects", + "body": { + "title": "Owned project" + }, + "headers": { + "Idempotency-Key": "owned-idempotency" + } + }, + { + "name": "suno_tasks_retrieve_batch-22", + "tool": "suno_tasks_retrieve_batch", + "inputs": { + "ids": "[\"owned-task\"]" + }, + "method": "POST", + "path": "/suno/tasks", + "body": { + "action": "retrieve_batch", + "ids": [ + "owned-task" + ] + }, + "read_only": true + } +] diff --git a/tests/e2e-audit.json b/tests/e2e-audit.json new file mode 100644 index 0000000..1afd596 --- /dev/null +++ b/tests/e2e-audit.json @@ -0,0 +1,366 @@ +{ + "tested_at": "2026-10-07T08:53:40.730830+00:00", + "environment": "Dify CE 1.17.1 / plugin daemon 0.6.10-local", + "transport": "official remote-debug; signature verification enabled", + "browser_test_run": true, + "unit_contract_tests": "46 passed, 1 warning in 1.36s", + "marketplace_toolkit_passed": true, + "package_sha256": "1932c4d2e528d2145c05290deb4648ad4763ffaac3f214dd62125a3e9e276680", + "exact_package_serverless_install_passed": true, + "marketplace_install_tested": false, + "cloud_tested": false, + "cases": [ + { + "case": "Suno Primary Audio Result", + "tool": "suno_tasks_retrieve_batch", + "workflow_run_id": "c338ce1b-e7c1-4ec6-b5b7-a484343798ff", + "workflow_status": "succeeded", + "service_status": "succeeded", + "task_id": "", + "trace_id": "", + "media_urls": [ + "https://cdn.acedata2.cloud/suno/3f79ee9e-e37b-46cf-804e-10833b608f8a.mp3", + "https://cdn.acedata2.cloud/suno/1f9f24b6-a533-4dc3-add5-7f4035dd85a4.mp3" + ], + "error": null, + "new_submission": false, + "media_verification": [ + { + "case": "Suno Primary Audio Result", + "size": 234078, + "sha256": "351623d613d3a21a65c7d6d8299b5091f9665f4bbf92f70a694a508b0078541f", + "duration": "10.560000", + "streams": [ + { + "codec_name": "mp3", + "codec_type": "audio", + "width": null, + "height": null + } + ], + "full_decode": true + }, + { + "case": "Suno Primary Audio Result", + "size": 410742, + "sha256": "93da4a8a021e4a5d7b784ad197e5f8a6724e00e0f43f28786fe751d049defb34", + "duration": "18.264000", + "streams": [ + { + "codec_name": "mp3", + "codec_type": "audio", + "width": null, + "height": null + } + ], + "full_decode": true + } + ], + "result_fields": [ + "items" + ], + "screenshot": "evidence/suno-primary-audio-result.png", + "workflow": "evidence/suno-primary-audio-result-workflow.json" + }, + { + "case": "Suno Project Create Audit", + "tool": "suno_projects", + "workflow_run_id": "4e3947ae-1993-450b-9c05-31e74b81a74b", + "workflow_status": "succeeded", + "service_status": "succeeded", + "task_id": "", + "trace_id": "039604b8-b3fe-4973-934d-14057e732bfd", + "media_urls": [], + "error": null, + "new_submission": true, + "billing": [ + { + "id": "4699af49-546e-43bf-8c26-c035c5edcb7f", + "trace_id": "039604b8-b3fe-4973-934d-14057e732bfd", + "status_code": 200, + "deducted_amount": 0.0, + "created_at": "2026-10-07T08:25:16.969269Z" + } + ], + "billing_match": "owned credential, API and browser run window", + "media_verification": [], + "result_fields": [ + "data", + "elapsed", + "finished_at", + "started_at", + "success", + "trace_id" + ], + "screenshot": "evidence/suno-project-create-audit.png", + "workflow": "evidence/suno-project-create-audit-workflow.json" + }, + { + "case": "Suno Project Readback Audit", + "tool": "suno_projects", + "workflow_run_id": "1d60c8cc-3ffb-4842-9e42-fbc706eb88b5", + "workflow_status": "succeeded", + "service_status": "succeeded", + "task_id": "", + "trace_id": "ea911173-8c10-4008-bca8-7eae3b53f88b", + "media_urls": [], + "error": null, + "new_submission": true, + "billing": [ + { + "id": "04ebbaf3-1e87-47fc-8627-c0944ef38803", + "trace_id": "ea911173-8c10-4008-bca8-7eae3b53f88b", + "status_code": 200, + "deducted_amount": 0.0, + "created_at": "2026-10-07T08:37:54.017443Z" + } + ], + "billing_match": "owned credential, API and browser run window", + "media_verification": [], + "result_fields": [ + "data", + "elapsed", + "finished_at", + "started_at", + "success", + "trace_id" + ], + "screenshot": "evidence/suno-project-readback-audit.png", + "workflow": "evidence/suno-project-readback-audit-workflow.json" + }, + { + "case": "Suno Custom Models List Audit", + "tool": "suno_custom_models", + "workflow_run_id": "3607d49e-a468-4bfa-be3d-cc4bdcbb239b", + "workflow_status": "succeeded", + "service_status": "succeeded", + "task_id": "", + "trace_id": "", + "media_urls": [], + "error": null, + "new_submission": true, + "billing": [ + { + "id": "a1f4b385-5753-41ba-ac2b-4283c1bf2c76", + "trace_id": "6e98b523-9feb-4025-936a-dc8a25d69f0b", + "status_code": 200, + "deducted_amount": 0.0, + "created_at": "2026-10-07T08:32:55.693582Z" + } + ], + "billing_match": "owned credential, API and browser run window", + "media_verification": [], + "result_fields": [ + "data", + "elapsed", + "finished_at", + "started_at", + "success" + ], + "workflow": "evidence/suno-custom-models-list-audit-workflow.json" + }, + { + "case": "Suno Lyrics Audit", + "tool": "suno_generate_lyrics", + "workflow_run_id": "4281b6f7-20a7-42a1-a819-16122e94c705", + "workflow_status": "succeeded", + "service_status": "succeeded", + "task_id": "e78be773-66af-4286-8941-62068ce4a6cb", + "trace_id": "", + "media_urls": [], + "error": null, + "new_submission": true, + "billing": [ + { + "id": "5b967863-764a-4496-80a9-42760937d1a1", + "trace_id": "e0091ac6-e8ca-4765-a84e-15767aad515f", + "status_code": 200, + "deducted_amount": 0.018, + "created_at": "2026-10-07T08:33:26.018668Z" + } + ], + "billing_match": "owned credential, API and browser run window", + "media_verification": [], + "result_fields": [ + "cost", + "data", + "elapsed", + "finished_at", + "started_at", + "success", + "task_id" + ], + "screenshot": "evidence/suno-lyrics-audit.png", + "workflow": "evidence/suno-lyrics-audit-workflow.json" + }, + { + "case": "Suno Style Audit", + "tool": "suno_enhance_style", + "workflow_run_id": "1ca7aa76-dfd9-4216-bd1b-ae1935f69289", + "workflow_status": "succeeded", + "service_status": "pending", + "task_id": "806a849f-483b-446b-ab1e-3dde4e315c17", + "trace_id": "bb4279ce-6b59-4469-b7f7-36ab0ac1b962", + "media_urls": [], + "error": null, + "new_submission": true, + "resolved_by": "suno-style-audit-readback", + "final_service_status": "succeeded", + "billing": [ + { + "id": "f1f043e0-680b-4a5f-b8c4-296043cba1ae", + "trace_id": "bb4279ce-6b59-4469-b7f7-36ab0ac1b962", + "status_code": 200, + "deducted_amount": 0.036, + "created_at": "2026-10-07T08:33:30.773178Z" + } + ], + "billing_match": "owned credential, API and browser run window", + "media_verification": [], + "result_fields": [ + "cost", + "data", + "elapsed", + "finished_at", + "started_at", + "success", + "task_id", + "trace_id" + ], + "screenshot": "evidence/suno-style-audit.png", + "workflow": "evidence/suno-style-audit-workflow.json" + }, + { + "case": "Suno Audit Batch", + "tool": "suno_tasks_retrieve_batch", + "workflow_run_id": "b65ddef6-438c-4126-a6c9-f9ef08d31e0c", + "workflow_status": "succeeded", + "service_status": "succeeded", + "task_id": "", + "trace_id": "", + "media_urls": [ + "https://cdn.acedata2.cloud/suno/0481c1ad7f216c4de39bd5c0e978c806e81d07c6a9e36c44ac39e58b87207563.jpg", + "https://cdn.acedata2.cloud/suno/3f79ee9e-e37b-46cf-804e-10833b608f8a.mp3", + "https://cdn1.suno.ai/3f79ee9e-e37b-46cf-804e-10833b608f8a.mp4", + "https://cdn.acedata2.cloud/suno/0adce61c6d46e10a7d96f62cdd1db23d63f6b1d73764cacb89e2d6a59c4fcafa.jpg", + "https://cdn.acedata2.cloud/suno/1f9f24b6-a533-4dc3-add5-7f4035dd85a4.mp3", + "https://cdn1.suno.ai/1f9f24b6-a533-4dc3-add5-7f4035dd85a4.mp4" + ], + "error": null, + "new_submission": false, + "resolved_by": "suno-primary-audio-result", + "final_service_status": "succeeded", + "media_verification": [ + { + "case": "Suno Audit Batch", + "size": 11728, + "sha256": "a65245384ad98058de69230484430015de002b74f80849e626217b5fef2ee493", + "width": 360, + "height": 360, + "format": "JPEG" + }, + { + "case": "Suno Audit Batch", + "size": 234078, + "sha256": "351623d613d3a21a65c7d6d8299b5091f9665f4bbf92f70a694a508b0078541f", + "duration": "10.560000", + "streams": [ + { + "codec_name": "mp3", + "codec_type": "audio", + "width": null, + "height": null + } + ], + "full_decode": true + }, + { + "case": "Suno Audit Batch", + "size": 11723, + "sha256": "d1cfb58ed675839dff90b9dc69ac26b66bce253c93068a6b6acea9a77a19fdbb", + "width": 360, + "height": 360, + "format": "JPEG" + }, + { + "case": "Suno Audit Batch", + "size": 410742, + "sha256": "93da4a8a021e4a5d7b784ad197e5f8a6724e00e0f43f28786fe751d049defb34", + "duration": "18.264000", + "streams": [ + { + "codec_name": "mp3", + "codec_type": "audio", + "width": null, + "height": null + } + ], + "full_decode": true + } + ], + "result_fields": [ + "items" + ], + "screenshot": "evidence/suno-audit-batch.png", + "workflow": "evidence/suno-audit-batch-workflow.json" + }, + { + "case": "Suno Style Audit Readback", + "tool": "suno_task_retrieve", + "workflow_run_id": "96312636-4e7a-4b8c-a679-839d0418a511", + "workflow_status": "succeeded", + "service_status": "succeeded", + "task_id": "806a849f-483b-446b-ab1e-3dde4e315c17", + "trace_id": "bb4279ce-6b59-4469-b7f7-36ab0ac1b962", + "media_urls": [], + "error": null, + "new_submission": false, + "media_verification": [], + "result_fields": [ + "cost", + "data", + "success", + "task_id", + "trace_id" + ], + "screenshot": "evidence/suno-style-audit-readback.png", + "workflow": "evidence/suno-style-audit-readback-workflow.json" + }, + { + "case": "Suno Lyrics Mashup Audit", + "tool": "suno_mashup_lyrics", + "workflow_run_id": "8720841c-53de-4a1f-a32f-9a97baee479e", + "workflow_status": "succeeded", + "service_status": "succeeded", + "task_id": "01c69031-b6fe-4a64-9b7d-13897fcc84b3", + "trace_id": "", + "media_urls": [], + "error": null, + "new_submission": true, + "billing": [ + { + "id": "79d1dae2-ad43-4f62-b655-04d1742d6a2f", + "trace_id": "7886a769-c602-4f8a-b41d-e8efedd9cd40", + "status_code": 200, + "deducted_amount": 0.018, + "created_at": "2026-10-07T08:34:03.040525Z" + } + ], + "billing_match": "owned credential, API and browser run window", + "media_verification": [], + "result_fields": [ + "cost", + "data", + "elapsed", + "finished_at", + "started_at", + "success", + "task_id" + ], + "screenshot": "evidence/suno-lyrics-mashup-audit.png", + "workflow": "evidence/suno-lyrics-mashup-audit-workflow.json" + } + ], + "primary_blocked": false, + "scope_note": "Recorded cases only. Batch regressions reuse earlier paid successful tasks; those media generations are not billed or claimed as new. New paid calls have separate usage reconciliation. Optional untested models/actions are not claimed as passed; original failures and their corrections are preserved." +} diff --git a/tests/evidence/suno-audit-batch-workflow.json b/tests/evidence/suno-audit-batch-workflow.json new file mode 100644 index 0000000..7a1049d --- /dev/null +++ b/tests/evidence/suno-audit-batch-workflow.json @@ -0,0 +1,188 @@ +{ + "nodes": [ + { + "id": "start", + "type": "custom", + "data": { + "type": "start", + "title": "Start", + "desc": "", + "variables": [], + "selected": false + }, + "position": { + "x": 0, + "y": 100 + }, + "width": 242, + "height": 73, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 0, + "y": 100 + } + }, + { + "id": "generate", + "type": "custom", + "data": { + "type": "tool", + "title": "Suno", + "desc": "", + "provider_id": "acedatacloud/suno/suno", + "provider_name": "acedatacloud/suno/suno", + "provider_type": "builtin", + "tool_name": "suno_tasks_retrieve_batch", + "tool_label": "Suno Retrieve Tasks", + "tool_configurations": { + "ids": { + "type": "constant", + "value": "[\"95a81d9a-2b30-4b2e-ab9c-2b60474703f6\"]" + } + }, + "tool_parameters": {}, + "tool_node_version": "2", + "plugin_unique_identifier": "acedatacloud/suno:0.0.1@825b68152a404fa7a7ddeeb2fa63413be03e39f1d1c30849f3ef618a85412cb3", + "retry_config": { + "retry_enabled": false, + "max_retries": 0, + "retry_interval": 1000 + }, + "selected": false + }, + "position": { + "x": 300, + "y": 100 + }, + "width": 242, + "height": 88, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 300, + "y": 100 + } + }, + { + "id": "end", + "type": "custom", + "data": { + "type": "end", + "title": "Output", + "desc": "", + "outputs": [ + { + "variable": "status", + "value_selector": [ + "generate", + "status" + ] + }, + { + "variable": "success", + "value_selector": [ + "generate", + "success" + ] + }, + { + "variable": "task_id", + "value_selector": [ + "generate", + "task_id" + ] + }, + { + "variable": "trace_id", + "value_selector": [ + "generate", + "trace_id" + ] + }, + { + "variable": "media_urls", + "value_selector": [ + "generate", + "media_urls" + ] + }, + { + "variable": "data", + "value_selector": [ + "generate", + "data" + ] + }, + { + "variable": "result", + "value_selector": [ + "generate", + "result" + ] + }, + { + "variable": "files", + "value_selector": [ + "generate", + "files" + ] + } + ], + "selected": false + }, + "position": { + "x": 600, + "y": 100 + }, + "width": 242, + "height": 270, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 600, + "y": 100 + } + } + ], + "edges": [ + { + "id": "start-generate", + "source": "start", + "target": "generate", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "start", + "targetType": "tool", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + }, + { + "id": "generate-end", + "source": "generate", + "target": "end", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "tool", + "targetType": "end", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + } + ], + "viewport": { + "x": 60, + "y": 160, + "zoom": 0.8 + } +} diff --git a/tests/evidence/suno-audit-batch.png b/tests/evidence/suno-audit-batch.png new file mode 100644 index 0000000..520f9b2 Binary files /dev/null and b/tests/evidence/suno-audit-batch.png differ diff --git a/tests/evidence/suno-custom-models-list-audit-workflow.json b/tests/evidence/suno-custom-models-list-audit-workflow.json new file mode 100644 index 0000000..adc248b --- /dev/null +++ b/tests/evidence/suno-custom-models-list-audit-workflow.json @@ -0,0 +1,192 @@ +{ + "nodes": [ + { + "id": "start", + "type": "custom", + "data": { + "type": "start", + "title": "Start", + "desc": "", + "variables": [], + "selected": false + }, + "position": { + "x": 0, + "y": 100 + }, + "width": 242, + "height": 73, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 0, + "y": 100 + } + }, + { + "id": "generate", + "type": "custom", + "data": { + "type": "tool", + "title": "Suno", + "desc": "", + "provider_id": "acedatacloud/suno/suno", + "provider_name": "acedatacloud/suno/suno", + "provider_type": "builtin", + "tool_name": "suno_custom_models", + "tool_label": "Suno Custom Models", + "tool_configurations": { + "action": { + "type": "constant", + "value": "retrieve_batch" + }, + "limit": { + "type": "constant", + "value": 2 + } + }, + "tool_parameters": {}, + "tool_node_version": "2", + "plugin_unique_identifier": "acedatacloud/suno:0.0.1@825b68152a404fa7a7ddeeb2fa63413be03e39f1d1c30849f3ef618a85412cb3", + "retry_config": { + "retry_enabled": false, + "max_retries": 0, + "retry_interval": 1000 + }, + "selected": false + }, + "position": { + "x": 300, + "y": 100 + }, + "width": 242, + "height": 114, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 300, + "y": 100 + } + }, + { + "id": "end", + "type": "custom", + "data": { + "type": "end", + "title": "Output", + "desc": "", + "outputs": [ + { + "variable": "status", + "value_selector": [ + "generate", + "status" + ] + }, + { + "variable": "success", + "value_selector": [ + "generate", + "success" + ] + }, + { + "variable": "task_id", + "value_selector": [ + "generate", + "task_id" + ] + }, + { + "variable": "trace_id", + "value_selector": [ + "generate", + "trace_id" + ] + }, + { + "variable": "media_urls", + "value_selector": [ + "generate", + "media_urls" + ] + }, + { + "variable": "data", + "value_selector": [ + "generate", + "data" + ] + }, + { + "variable": "result", + "value_selector": [ + "generate", + "result" + ] + }, + { + "variable": "files", + "value_selector": [ + "generate", + "files" + ] + } + ], + "selected": false + }, + "position": { + "x": 600, + "y": 100 + }, + "width": 242, + "height": 270, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 600, + "y": 100 + } + } + ], + "edges": [ + { + "id": "start-generate", + "source": "start", + "target": "generate", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "start", + "targetType": "tool", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + }, + { + "id": "generate-end", + "source": "generate", + "target": "end", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "tool", + "targetType": "end", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + } + ], + "viewport": { + "x": 60, + "y": 160, + "zoom": 0.8 + } +} diff --git a/tests/evidence/suno-lyrics-audit-workflow.json b/tests/evidence/suno-lyrics-audit-workflow.json new file mode 100644 index 0000000..4254ea2 --- /dev/null +++ b/tests/evidence/suno-lyrics-audit-workflow.json @@ -0,0 +1,193 @@ +{ + "nodes": [ + { + "id": "start", + "type": "custom", + "data": { + "type": "start", + "title": "Start", + "desc": "", + "variables": [], + "selected": false + }, + "position": { + "x": 0, + "y": 100 + }, + "width": 242, + "height": 73, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 0, + "y": 100 + } + }, + { + "id": "generate", + "type": "custom", + "data": { + "type": "tool", + "title": "Suno", + "desc": "", + "provider_id": "acedatacloud/suno/suno", + "provider_name": "acedatacloud/suno/suno", + "provider_type": "builtin", + "tool_name": "suno_generate_lyrics", + "tool_label": "Suno Generate Lyrics", + "tool_configurations": { + "model": { + "type": "constant", + "value": "default" + } + }, + "tool_parameters": { + "prompt": { + "type": "constant", + "value": "A four-line song about a quiet morning at the seaside." + } + }, + "tool_node_version": "2", + "plugin_unique_identifier": "acedatacloud/suno:0.0.1@825b68152a404fa7a7ddeeb2fa63413be03e39f1d1c30849f3ef618a85412cb3", + "retry_config": { + "retry_enabled": false, + "max_retries": 0, + "retry_interval": 1000 + }, + "selected": false + }, + "position": { + "x": 300, + "y": 100 + }, + "width": 242, + "height": 88, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 300, + "y": 100 + } + }, + { + "id": "end", + "type": "custom", + "data": { + "type": "end", + "title": "Output", + "desc": "", + "outputs": [ + { + "variable": "status", + "value_selector": [ + "generate", + "status" + ] + }, + { + "variable": "success", + "value_selector": [ + "generate", + "success" + ] + }, + { + "variable": "task_id", + "value_selector": [ + "generate", + "task_id" + ] + }, + { + "variable": "trace_id", + "value_selector": [ + "generate", + "trace_id" + ] + }, + { + "variable": "media_urls", + "value_selector": [ + "generate", + "media_urls" + ] + }, + { + "variable": "data", + "value_selector": [ + "generate", + "data" + ] + }, + { + "variable": "result", + "value_selector": [ + "generate", + "result" + ] + }, + { + "variable": "files", + "value_selector": [ + "generate", + "files" + ] + } + ], + "selected": false + }, + "position": { + "x": 600, + "y": 100 + }, + "width": 242, + "height": 270, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 600, + "y": 100 + } + } + ], + "edges": [ + { + "id": "start-generate", + "source": "start", + "target": "generate", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "start", + "targetType": "tool", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + }, + { + "id": "generate-end", + "source": "generate", + "target": "end", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "tool", + "targetType": "end", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + } + ], + "viewport": { + "x": 60, + "y": 160, + "zoom": 0.8 + } +} diff --git a/tests/evidence/suno-lyrics-audit.png b/tests/evidence/suno-lyrics-audit.png new file mode 100644 index 0000000..4e6b6e6 Binary files /dev/null and b/tests/evidence/suno-lyrics-audit.png differ diff --git a/tests/evidence/suno-lyrics-mashup-audit-workflow.json b/tests/evidence/suno-lyrics-mashup-audit-workflow.json new file mode 100644 index 0000000..1f576ac --- /dev/null +++ b/tests/evidence/suno-lyrics-mashup-audit-workflow.json @@ -0,0 +1,192 @@ +{ + "nodes": [ + { + "id": "start", + "type": "custom", + "data": { + "type": "start", + "title": "Start", + "desc": "", + "variables": [], + "selected": false + }, + "position": { + "x": 0, + "y": 100 + }, + "width": 242, + "height": 73, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 0, + "y": 100 + } + }, + { + "id": "generate", + "type": "custom", + "data": { + "type": "tool", + "title": "Suno", + "desc": "", + "provider_id": "acedatacloud/suno/suno", + "provider_name": "acedatacloud/suno/suno", + "provider_type": "builtin", + "tool_name": "suno_mashup_lyrics", + "tool_label": "Suno Mashup Lyrics", + "tool_configurations": { + "lyrics_a": { + "type": "constant", + "value": "Morning light across the sea" + }, + "lyrics_b": { + "type": "constant", + "value": "A quiet sky above the trees" + } + }, + "tool_parameters": {}, + "tool_node_version": "2", + "plugin_unique_identifier": "acedatacloud/suno:0.0.1@825b68152a404fa7a7ddeeb2fa63413be03e39f1d1c30849f3ef618a85412cb3", + "retry_config": { + "retry_enabled": false, + "max_retries": 0, + "retry_interval": 1000 + }, + "selected": false + }, + "position": { + "x": 300, + "y": 100 + }, + "width": 242, + "height": 114, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 300, + "y": 100 + } + }, + { + "id": "end", + "type": "custom", + "data": { + "type": "end", + "title": "Output", + "desc": "", + "outputs": [ + { + "variable": "status", + "value_selector": [ + "generate", + "status" + ] + }, + { + "variable": "success", + "value_selector": [ + "generate", + "success" + ] + }, + { + "variable": "task_id", + "value_selector": [ + "generate", + "task_id" + ] + }, + { + "variable": "trace_id", + "value_selector": [ + "generate", + "trace_id" + ] + }, + { + "variable": "media_urls", + "value_selector": [ + "generate", + "media_urls" + ] + }, + { + "variable": "data", + "value_selector": [ + "generate", + "data" + ] + }, + { + "variable": "result", + "value_selector": [ + "generate", + "result" + ] + }, + { + "variable": "files", + "value_selector": [ + "generate", + "files" + ] + } + ], + "selected": false + }, + "position": { + "x": 600, + "y": 100 + }, + "width": 242, + "height": 270, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 600, + "y": 100 + } + } + ], + "edges": [ + { + "id": "start-generate", + "source": "start", + "target": "generate", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "start", + "targetType": "tool", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + }, + { + "id": "generate-end", + "source": "generate", + "target": "end", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "tool", + "targetType": "end", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + } + ], + "viewport": { + "x": 60, + "y": 160, + "zoom": 0.8 + } +} diff --git a/tests/evidence/suno-lyrics-mashup-audit.png b/tests/evidence/suno-lyrics-mashup-audit.png new file mode 100644 index 0000000..51816b8 Binary files /dev/null and b/tests/evidence/suno-lyrics-mashup-audit.png differ diff --git a/tests/evidence/suno-primary-audio-result-workflow.json b/tests/evidence/suno-primary-audio-result-workflow.json new file mode 100644 index 0000000..183f924 --- /dev/null +++ b/tests/evidence/suno-primary-audio-result-workflow.json @@ -0,0 +1,188 @@ +{ + "nodes": [ + { + "id": "start", + "type": "custom", + "data": { + "type": "start", + "title": "Start", + "desc": "", + "variables": [], + "selected": false + }, + "position": { + "x": 0, + "y": 100 + }, + "width": 242, + "height": 73, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 0, + "y": 100 + } + }, + { + "id": "generate", + "type": "custom", + "data": { + "type": "tool", + "title": "Suno", + "desc": "", + "provider_id": "acedatacloud/suno/suno", + "provider_name": "acedatacloud/suno/suno", + "provider_type": "builtin", + "tool_name": "suno_tasks_retrieve_batch", + "tool_label": "Suno Retrieve Tasks", + "tool_configurations": { + "ids": { + "type": "constant", + "value": "[\"95a81d9a-2b30-4b2e-ab9c-2b60474703f6\"]" + } + }, + "tool_parameters": {}, + "tool_node_version": "2", + "plugin_unique_identifier": "acedatacloud/suno:0.0.1@3e661cc51ccd109130baf99830ff6662be07341aefeeac894eb4dd1acb78e86f", + "retry_config": { + "retry_enabled": false, + "max_retries": 0, + "retry_interval": 1000 + }, + "selected": false + }, + "position": { + "x": 300, + "y": 100 + }, + "width": 242, + "height": 88, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 300, + "y": 100 + } + }, + { + "id": "end", + "type": "custom", + "data": { + "type": "end", + "title": "Output", + "desc": "", + "outputs": [ + { + "variable": "status", + "value_selector": [ + "generate", + "status" + ] + }, + { + "variable": "success", + "value_selector": [ + "generate", + "success" + ] + }, + { + "variable": "task_id", + "value_selector": [ + "generate", + "task_id" + ] + }, + { + "variable": "trace_id", + "value_selector": [ + "generate", + "trace_id" + ] + }, + { + "variable": "media_urls", + "value_selector": [ + "generate", + "media_urls" + ] + }, + { + "variable": "data", + "value_selector": [ + "generate", + "data" + ] + }, + { + "variable": "result", + "value_selector": [ + "generate", + "result" + ] + }, + { + "variable": "files", + "value_selector": [ + "generate", + "files" + ] + } + ], + "selected": false + }, + "position": { + "x": 600, + "y": 100 + }, + "width": 242, + "height": 270, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 600, + "y": 100 + } + } + ], + "edges": [ + { + "id": "start-generate", + "source": "start", + "target": "generate", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "start", + "targetType": "tool", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + }, + { + "id": "generate-end", + "source": "generate", + "target": "end", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "tool", + "targetType": "end", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + } + ], + "viewport": { + "x": 60, + "y": 160, + "zoom": 0.8 + } +} diff --git a/tests/evidence/suno-primary-audio-result.png b/tests/evidence/suno-primary-audio-result.png new file mode 100644 index 0000000..960618a Binary files /dev/null and b/tests/evidence/suno-primary-audio-result.png differ diff --git a/tests/evidence/suno-project-create-audit-workflow.json b/tests/evidence/suno-project-create-audit-workflow.json new file mode 100644 index 0000000..add017c --- /dev/null +++ b/tests/evidence/suno-project-create-audit-workflow.json @@ -0,0 +1,196 @@ +{ + "nodes": [ + { + "id": "start", + "type": "custom", + "data": { + "type": "start", + "title": "Start", + "desc": "", + "variables": [], + "selected": false + }, + "position": { + "x": 0, + "y": 100 + }, + "width": 242, + "height": 73, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 0, + "y": 100 + } + }, + { + "id": "generate", + "type": "custom", + "data": { + "type": "tool", + "title": "Suno", + "desc": "", + "provider_id": "acedatacloud/suno/suno", + "provider_name": "acedatacloud/suno/suno", + "provider_type": "builtin", + "tool_name": "suno_projects", + "tool_label": "Suno Projects", + "tool_configurations": { + "action": { + "type": "constant", + "value": "create" + }, + "title": { + "type": "constant", + "value": "Dify QA 2026-10-07" + }, + "idempotency_key": { + "type": "constant", + "value": "dify-qa-project-20261007" + } + }, + "tool_parameters": {}, + "tool_node_version": "2", + "plugin_unique_identifier": "acedatacloud/suno:0.0.1@825b68152a404fa7a7ddeeb2fa63413be03e39f1d1c30849f3ef618a85412cb3", + "retry_config": { + "retry_enabled": false, + "max_retries": 0, + "retry_interval": 1000 + }, + "selected": false + }, + "position": { + "x": 300, + "y": 100 + }, + "width": 242, + "height": 140, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 300, + "y": 100 + } + }, + { + "id": "end", + "type": "custom", + "data": { + "type": "end", + "title": "Output", + "desc": "", + "outputs": [ + { + "variable": "status", + "value_selector": [ + "generate", + "status" + ] + }, + { + "variable": "success", + "value_selector": [ + "generate", + "success" + ] + }, + { + "variable": "task_id", + "value_selector": [ + "generate", + "task_id" + ] + }, + { + "variable": "trace_id", + "value_selector": [ + "generate", + "trace_id" + ] + }, + { + "variable": "media_urls", + "value_selector": [ + "generate", + "media_urls" + ] + }, + { + "variable": "data", + "value_selector": [ + "generate", + "data" + ] + }, + { + "variable": "result", + "value_selector": [ + "generate", + "result" + ] + }, + { + "variable": "files", + "value_selector": [ + "generate", + "files" + ] + } + ], + "selected": false + }, + "position": { + "x": 600, + "y": 100 + }, + "width": 242, + "height": 270, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 600, + "y": 100 + } + } + ], + "edges": [ + { + "id": "start-generate", + "source": "start", + "target": "generate", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "start", + "targetType": "tool", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + }, + { + "id": "generate-end", + "source": "generate", + "target": "end", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "tool", + "targetType": "end", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + } + ], + "viewport": { + "x": 60, + "y": 160, + "zoom": 0.8 + } +} diff --git a/tests/evidence/suno-project-create-audit.png b/tests/evidence/suno-project-create-audit.png new file mode 100644 index 0000000..bcfad00 Binary files /dev/null and b/tests/evidence/suno-project-create-audit.png differ diff --git a/tests/evidence/suno-project-readback-audit-workflow.json b/tests/evidence/suno-project-readback-audit-workflow.json new file mode 100644 index 0000000..11f5f7c --- /dev/null +++ b/tests/evidence/suno-project-readback-audit-workflow.json @@ -0,0 +1,192 @@ +{ + "nodes": [ + { + "id": "start", + "type": "custom", + "data": { + "type": "start", + "title": "Start", + "desc": "", + "variables": [], + "selected": false + }, + "position": { + "x": 0, + "y": 100 + }, + "width": 242, + "height": 73, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 0, + "y": 100 + } + }, + { + "id": "generate", + "type": "custom", + "data": { + "type": "tool", + "title": "Suno", + "desc": "", + "provider_id": "acedatacloud/suno/suno", + "provider_name": "acedatacloud/suno/suno", + "provider_type": "builtin", + "tool_name": "suno_projects", + "tool_label": "Suno Projects", + "tool_configurations": { + "action": { + "type": "constant", + "value": "retrieve" + }, + "id": { + "type": "constant", + "value": "6fbf5323-9cc5-4cc5-9a81-f9781490ce75" + } + }, + "tool_parameters": {}, + "tool_node_version": "2", + "plugin_unique_identifier": "acedatacloud/suno:0.0.1@95e67a36bc3a90f06ac2513b120f33db659ebad5358789b54cc0ba1736611a7c", + "retry_config": { + "retry_enabled": false, + "max_retries": 0, + "retry_interval": 1000 + }, + "selected": false + }, + "position": { + "x": 300, + "y": 100 + }, + "width": 242, + "height": 114, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 300, + "y": 100 + } + }, + { + "id": "end", + "type": "custom", + "data": { + "type": "end", + "title": "Output", + "desc": "", + "outputs": [ + { + "variable": "status", + "value_selector": [ + "generate", + "status" + ] + }, + { + "variable": "success", + "value_selector": [ + "generate", + "success" + ] + }, + { + "variable": "task_id", + "value_selector": [ + "generate", + "task_id" + ] + }, + { + "variable": "trace_id", + "value_selector": [ + "generate", + "trace_id" + ] + }, + { + "variable": "media_urls", + "value_selector": [ + "generate", + "media_urls" + ] + }, + { + "variable": "data", + "value_selector": [ + "generate", + "data" + ] + }, + { + "variable": "result", + "value_selector": [ + "generate", + "result" + ] + }, + { + "variable": "files", + "value_selector": [ + "generate", + "files" + ] + } + ], + "selected": false + }, + "position": { + "x": 600, + "y": 100 + }, + "width": 242, + "height": 270, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 600, + "y": 100 + } + } + ], + "edges": [ + { + "id": "start-generate", + "source": "start", + "target": "generate", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "start", + "targetType": "tool", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + }, + { + "id": "generate-end", + "source": "generate", + "target": "end", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "tool", + "targetType": "end", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + } + ], + "viewport": { + "x": 60, + "y": 160, + "zoom": 0.8 + } +} diff --git a/tests/evidence/suno-project-readback-audit.png b/tests/evidence/suno-project-readback-audit.png new file mode 100644 index 0000000..f6b84ee Binary files /dev/null and b/tests/evidence/suno-project-readback-audit.png differ diff --git a/tests/evidence/suno-style-audit-readback-workflow.json b/tests/evidence/suno-style-audit-readback-workflow.json new file mode 100644 index 0000000..f506179 --- /dev/null +++ b/tests/evidence/suno-style-audit-readback-workflow.json @@ -0,0 +1,192 @@ +{ + "nodes": [ + { + "id": "start", + "type": "custom", + "data": { + "type": "start", + "title": "Start", + "desc": "", + "variables": [], + "selected": false + }, + "position": { + "x": 0, + "y": 100 + }, + "width": 242, + "height": 73, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 0, + "y": 100 + } + }, + { + "id": "generate", + "type": "custom", + "data": { + "type": "tool", + "title": "Suno", + "desc": "", + "provider_id": "acedatacloud/suno/suno", + "provider_name": "acedatacloud/suno/suno", + "provider_type": "builtin", + "tool_name": "suno_task_retrieve", + "tool_label": "Suno Retrieve Task", + "tool_configurations": { + "task_id": { + "type": "constant", + "value": "806a849f-483b-446b-ab1e-3dde4e315c17" + }, + "wait_seconds": { + "type": "constant", + "value": 0 + } + }, + "tool_parameters": {}, + "tool_node_version": "2", + "plugin_unique_identifier": "acedatacloud/suno:0.0.1@95e67a36bc3a90f06ac2513b120f33db659ebad5358789b54cc0ba1736611a7c", + "retry_config": { + "retry_enabled": false, + "max_retries": 0, + "retry_interval": 1000 + }, + "selected": false + }, + "position": { + "x": 300, + "y": 100 + }, + "width": 242, + "height": 114, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 300, + "y": 100 + } + }, + { + "id": "end", + "type": "custom", + "data": { + "type": "end", + "title": "Output", + "desc": "", + "outputs": [ + { + "variable": "status", + "value_selector": [ + "generate", + "status" + ] + }, + { + "variable": "success", + "value_selector": [ + "generate", + "success" + ] + }, + { + "variable": "task_id", + "value_selector": [ + "generate", + "task_id" + ] + }, + { + "variable": "trace_id", + "value_selector": [ + "generate", + "trace_id" + ] + }, + { + "variable": "media_urls", + "value_selector": [ + "generate", + "media_urls" + ] + }, + { + "variable": "data", + "value_selector": [ + "generate", + "data" + ] + }, + { + "variable": "result", + "value_selector": [ + "generate", + "result" + ] + }, + { + "variable": "files", + "value_selector": [ + "generate", + "files" + ] + } + ], + "selected": false + }, + "position": { + "x": 600, + "y": 100 + }, + "width": 242, + "height": 270, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 600, + "y": 100 + } + } + ], + "edges": [ + { + "id": "start-generate", + "source": "start", + "target": "generate", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "start", + "targetType": "tool", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + }, + { + "id": "generate-end", + "source": "generate", + "target": "end", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "tool", + "targetType": "end", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + } + ], + "viewport": { + "x": 60, + "y": 160, + "zoom": 0.8 + } +} diff --git a/tests/evidence/suno-style-audit-readback.png b/tests/evidence/suno-style-audit-readback.png new file mode 100644 index 0000000..3592a0f Binary files /dev/null and b/tests/evidence/suno-style-audit-readback.png differ diff --git a/tests/evidence/suno-style-audit-workflow.json b/tests/evidence/suno-style-audit-workflow.json new file mode 100644 index 0000000..bd45edc --- /dev/null +++ b/tests/evidence/suno-style-audit-workflow.json @@ -0,0 +1,188 @@ +{ + "nodes": [ + { + "id": "start", + "type": "custom", + "data": { + "type": "start", + "title": "Start", + "desc": "", + "variables": [], + "selected": false + }, + "position": { + "x": 0, + "y": 100 + }, + "width": 242, + "height": 73, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 0, + "y": 100 + } + }, + { + "id": "generate", + "type": "custom", + "data": { + "type": "tool", + "title": "Suno", + "desc": "", + "provider_id": "acedatacloud/suno/suno", + "provider_name": "acedatacloud/suno/suno", + "provider_type": "builtin", + "tool_name": "suno_enhance_style", + "tool_label": "Suno Enhance Style", + "tool_configurations": {}, + "tool_parameters": { + "prompt": { + "type": "constant", + "value": "Gentle acoustic instrumental with soft piano." + } + }, + "tool_node_version": "2", + "plugin_unique_identifier": "acedatacloud/suno:0.0.1@825b68152a404fa7a7ddeeb2fa63413be03e39f1d1c30849f3ef618a85412cb3", + "retry_config": { + "retry_enabled": false, + "max_retries": 0, + "retry_interval": 1000 + }, + "selected": false + }, + "position": { + "x": 300, + "y": 100 + }, + "width": 242, + "height": 52, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 300, + "y": 100 + } + }, + { + "id": "end", + "type": "custom", + "data": { + "type": "end", + "title": "Output", + "desc": "", + "outputs": [ + { + "variable": "status", + "value_selector": [ + "generate", + "status" + ] + }, + { + "variable": "success", + "value_selector": [ + "generate", + "success" + ] + }, + { + "variable": "task_id", + "value_selector": [ + "generate", + "task_id" + ] + }, + { + "variable": "trace_id", + "value_selector": [ + "generate", + "trace_id" + ] + }, + { + "variable": "media_urls", + "value_selector": [ + "generate", + "media_urls" + ] + }, + { + "variable": "data", + "value_selector": [ + "generate", + "data" + ] + }, + { + "variable": "result", + "value_selector": [ + "generate", + "result" + ] + }, + { + "variable": "files", + "value_selector": [ + "generate", + "files" + ] + } + ], + "selected": false + }, + "position": { + "x": 600, + "y": 100 + }, + "width": 242, + "height": 270, + "sourcePosition": "right", + "targetPosition": "left", + "zIndex": 0, + "positionAbsolute": { + "x": 600, + "y": 100 + } + } + ], + "edges": [ + { + "id": "start-generate", + "source": "start", + "target": "generate", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "start", + "targetType": "tool", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + }, + { + "id": "generate-end", + "source": "generate", + "target": "end", + "sourceHandle": "source", + "targetHandle": "target", + "type": "custom", + "data": { + "sourceType": "tool", + "targetType": "end", + "isInIteration": false, + "isInLoop": false + }, + "zIndex": 0 + } + ], + "viewport": { + "x": 60, + "y": 160, + "zoom": 0.8 + } +} diff --git a/tests/evidence/suno-style-audit.png b/tests/evidence/suno-style-audit.png new file mode 100644 index 0000000..7f07825 Binary files /dev/null and b/tests/evidence/suno-style-audit.png differ diff --git a/tests/mcp-parity.json b/tests/mcp-parity.json new file mode 100644 index 0000000..13e9c35 --- /dev/null +++ b/tests/mcp-parity.json @@ -0,0 +1,564 @@ +{ + "mcp_revision": "f0eed10abf310824cb4c33d4944c63d3654ac95b", + "mapping": [ + { + "mcp_tool": "suno_list_models", + "dify_tools": [], + "parameters": {}, + "unresolved": [], + "note": "Model/action selectors and the API reference; informational guidance does not submit a request." + }, + { + "mcp_tool": "suno_list_actions", + "dify_tools": [], + "parameters": {}, + "unresolved": [], + "note": "Model/action selectors and the API reference; informational guidance does not submit a request." + }, + { + "mcp_tool": "suno_get_lyric_format_guide", + "dify_tools": [], + "parameters": {}, + "unresolved": [], + "note": "Model/action selectors and the API reference; informational guidance does not submit a request." + }, + { + "mcp_tool": "suno_generate_music", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "prompt": "prompt", + "model": "model", + "instrumental": "instrumental", + "variation_category": "variation_category", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=generate" + }, + { + "mcp_tool": "suno_generate_custom_music", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "lyric": "lyric", + "title": "title", + "style": "style", + "model": "model", + "instrumental": "instrumental", + "lyric_prompt": "lyric_prompt", + "negative_tags": "negative_tags", + "vocal_gender": "vocal_gender", + "variation_category": "variation_category", + "weirdness": "weirdness", + "style_influence": "style_influence", + "duration": "duration", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=generate" + }, + { + "mcp_tool": "suno_extend_music", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "lyric": "lyric", + "continue_at": "continue_at", + "style": "style", + "model": "model", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=extend" + }, + { + "mcp_tool": "suno_cover_music", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "prompt": "prompt", + "style": "style", + "model": "model", + "audio_weight": "audio_weight", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=cover" + }, + { + "mcp_tool": "suno_concat_music", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=concat" + }, + { + "mcp_tool": "suno_generate_with_persona", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "persona_id": "persona_id", + "prompt": "prompt", + "model": "model", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=artist_consistency" + }, + { + "mcp_tool": "suno_remaster_music", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "variation_category": "variation_category", + "model": "model", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=remaster" + }, + { + "mcp_tool": "suno_stems_music", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=stems" + }, + { + "mcp_tool": "suno_replace_section", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "replace_section_start": "replace_section_start", + "replace_section_end": "replace_section_end", + "lyric": "lyric", + "style": "style", + "model": "model", + "callback_url": "callback_url", + "result_mode": "replace_section_result_mode" + }, + "unresolved": [], + "note": "Set action=replace_section" + }, + { + "mcp_tool": "suno_upload_extend", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "lyric": "lyric", + "continue_at": "continue_at", + "style": "style", + "model": "model", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=upload_extend" + }, + { + "mcp_tool": "suno_upload_cover", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "style": "style", + "model": "model", + "audio_weight": "audio_weight", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=upload_cover" + }, + { + "mcp_tool": "suno_mashup_music", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "mashup_audio_ids": "mashup_audio_ids", + "prompt": "prompt", + "style": "style", + "title": "title", + "instrumental": "instrumental", + "model": "model", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=mashup" + }, + { + "mcp_tool": "suno_all_stems_music", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=all_stems" + }, + { + "mcp_tool": "suno_generate_with_persona_vox", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "persona_id": "persona_id", + "prompt": "prompt", + "model": "model", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=artist_consistency_vox" + }, + { + "mcp_tool": "suno_underpainting", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "underpainting_start": "underpainting_start", + "underpainting_end": "underpainting_end", + "model": "model", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=underpainting" + }, + { + "mcp_tool": "suno_overpainting", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "overpainting_start": "overpainting_start", + "overpainting_end": "overpainting_end", + "model": "model", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=overpainting" + }, + { + "mcp_tool": "suno_samples_music", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_id": "audio_id", + "samples_start": "samples_start", + "samples_end": "samples_end", + "model": "model", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=samples" + }, + { + "mcp_tool": "suno_generate_inspo", + "dify_tools": [ + "suno_generate_audios" + ], + "parameters": { + "audio_urls": "audio_urls", + "prompt": "prompt", + "title": "title", + "style": "style", + "model": "model", + "audio_weight": "audio_weight", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "Set action=inspo" + }, + { + "mcp_tool": "suno_generate_lyrics", + "dify_tools": [ + "suno_generate_lyrics" + ], + "parameters": { + "prompt": "prompt", + "model": "model" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_get_task", + "dify_tools": [ + "suno_task_retrieve" + ], + "parameters": { + "task_id": "task_id" + }, + "unresolved": [], + "note": "Set action=retrieve" + }, + { + "mcp_tool": "suno_get_tasks_batch", + "dify_tools": [ + "suno_tasks_retrieve_batch" + ], + "parameters": { + "task_ids": "ids" + }, + "unresolved": [], + "note": "Set action=retrieve_batch" + }, + { + "mcp_tool": "suno_get_mp4", + "dify_tools": [ + "suno_get_mp4" + ], + "parameters": { + "audio_id": "audio_id" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_get_timing", + "dify_tools": [ + "suno_get_timing" + ], + "parameters": { + "audio_id": "audio_id" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_extract_vocals", + "dify_tools": [ + "suno_create_vox_audio" + ], + "parameters": { + "audio_id": "audio_id", + "vocal_start": "vocal_start", + "vocal_end": "vocal_end", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_get_wav", + "dify_tools": [ + "suno_get_wav" + ], + "parameters": { + "audio_id": "audio_id", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_get_mp3", + "dify_tools": [ + "suno_get_mp3" + ], + "parameters": { + "audio_id": "audio_id", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_get_midi", + "dify_tools": [ + "suno_get_midi" + ], + "parameters": { + "audio_id": "audio_id", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_optimize_style", + "dify_tools": [ + "suno_enhance_style" + ], + "parameters": { + "prompt": "prompt" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_mashup_lyrics", + "dify_tools": [ + "suno_mashup_lyrics" + ], + "parameters": { + "lyrics_a": "lyrics_a", + "lyrics_b": "lyrics_b" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_upload_audio", + "dify_tools": [ + "suno_upload_reference_audio" + ], + "parameters": { + "audio_url": "audio_url", + "mode": "mode", + "name": "name", + "callback_url": "callback_url" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_create_voice", + "dify_tools": [ + "suno_create_voice" + ], + "parameters": { + "audio_url": "audio_url", + "name": "name", + "description": "description" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_create_persona", + "dify_tools": [ + "suno_create_persona" + ], + "parameters": { + "audio_id": "audio_id", + "name": "name", + "vox_audio_id": "vox_audio_id", + "vocal_start": "vocal_start", + "vocal_end": "vocal_end", + "description": "description" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_list_personas", + "dify_tools": [ + "suno_list_personas" + ], + "parameters": { + "user_id": "user_id", + "limit": "limit", + "offset": "offset" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_delete_persona", + "dify_tools": [ + "suno_delete_persona" + ], + "parameters": { + "persona_id": "persona_id", + "user_id": "user_id" + }, + "unresolved": [], + "note": "" + }, + { + "mcp_tool": "suno_create_custom_model", + "dify_tools": [ + "suno_custom_models" + ], + "parameters": { + "name": "name", + "audio_urls": "audio_urls", + "callback_url": "callback_url", + "idempotency_key": "idempotency_key" + }, + "unresolved": [], + "note": "Set action=create" + }, + { + "mcp_tool": "suno_get_custom_model", + "dify_tools": [ + "suno_custom_models" + ], + "parameters": { + "model_id": "id" + }, + "unresolved": [], + "note": "Set action=retrieve" + }, + { + "mcp_tool": "suno_list_custom_models", + "dify_tools": [ + "suno_custom_models" + ], + "parameters": { + "limit": "limit", + "offset": "offset", + "status": "status" + }, + "unresolved": [], + "note": "Set action=retrieve_batch" + }, + { + "mcp_tool": "suno_generate_with_custom_model", + "dify_tools": [ + "suno_custom_models" + ], + "parameters": { + "model_id": "id", + "title": "title", + "lyric": "lyric", + "style": "style" + }, + "unresolved": [], + "note": "Set action=generate" + }, + { + "mcp_tool": "suno_archive_custom_model", + "dify_tools": [ + "suno_custom_models" + ], + "parameters": { + "model_id": "id" + }, + "unresolved": [], + "note": "Set action=delete" + } + ] +} diff --git a/tests/parity-audit.json b/tests/parity-audit.json new file mode 100644 index 0000000..f95bc17 --- /dev/null +++ b/tests/parity-audit.json @@ -0,0 +1,310 @@ +{ + "backend_revision": "fa94598267a82545fb1afed6ee26bafd6cbb9ca7", + "mcp_revision": "f0eed10abf310824cb4c33d4944c63d3654ac95b", + "tools": [ + { + "tool": "suno_generate_audios", + "method": "POST", + "path": "/suno/audios", + "parameters": [ + "lyric", + "model", + "max_mode", + "variety", + "style", + "variation_category", + "title", + "action", + "custom", + "prompt", + "lyric_prompt", + "audio_id", + "stem_type", + "audio_format", + "sound", + "sound_type", + "bpm", + "key", + "speed_multiplier", + "keep_pitch", + "mashup_audio_ids", + "audio_urls", + "weirdness", + "persona_id", + "overpainting_start", + "overpainting_end", + "samples_start", + "samples_end", + "underpainting_start", + "underpainting_end", + "continue_at", + "callback_url", + "instrumental", + "vocal_gender", + "negative_tags", + "style_influence", + "audio_weight", + "duration", + "replace_section_end", + "replace_section_result_mode", + "replace_section_start" + ], + "aliases": {}, + "schema_source": "openapi/09a26295-5972-4392-9318-dcd9b218f90d.json" + }, + { + "tool": "suno_generate_lyrics", + "method": "POST", + "path": "/suno/lyrics", + "parameters": [ + "model", + "prompt" + ], + "aliases": {}, + "schema_source": "openapi/c98fe4e5-73c0-462d-ad24-5080ee1fa01a.json" + }, + { + "tool": "suno_upload_reference_audio", + "method": "POST", + "path": "/suno/upload", + "parameters": [ + "audio_url", + "mode", + "name", + "callback_url" + ], + "aliases": {}, + "schema_source": "openapi/8823cd83-54f6-4ea8-979a-7fa448577186.json" + }, + { + "tool": "suno_get_mp4", + "method": "POST", + "path": "/suno/mp4", + "parameters": [ + "audio_id" + ], + "aliases": {}, + "schema_source": "openapi/2e4c9e30-a793-4f43-9322-d682120ffff2.json" + }, + { + "tool": "suno_get_mp3", + "method": "POST", + "path": "/suno/mp3", + "parameters": [ + "audio_id", + "callback_url" + ], + "aliases": {}, + "schema_source": "openapi/32141b95-043f-5845-b832-77c32ddd6fa6.json" + }, + { + "tool": "suno_get_wav", + "method": "POST", + "path": "/suno/wav", + "parameters": [ + "audio_id", + "callback_url" + ], + "aliases": {}, + "schema_source": "openapi/d4004774-4d0b-4aac-a8ea-be99627880e6.json" + }, + { + "tool": "suno_get_midi", + "method": "POST", + "path": "/suno/midi", + "parameters": [ + "audio_id", + "callback_url" + ], + "aliases": {}, + "schema_source": "openapi/be1de4f0-b5f5-43dc-bc7a-d2f90b2efe9c.json" + }, + { + "tool": "suno_get_timing", + "method": "POST", + "path": "/suno/timing", + "parameters": [ + "audio_id" + ], + "aliases": {}, + "schema_source": "openapi/da474a4e-f32a-41dc-b2fe-59cc5cea5c32.json" + }, + { + "tool": "suno_create_vox_audio", + "method": "POST", + "path": "/suno/vox", + "parameters": [ + "audio_id", + "vocal_start", + "vocal_end", + "callback_url" + ], + "aliases": {}, + "schema_source": "openapi/a8d46e2d-a042-4321-ab97-a5ab1d7fed5e.json" + }, + { + "tool": "suno_enhance_style", + "method": "POST", + "path": "/suno/style", + "parameters": [ + "prompt" + ], + "aliases": {}, + "schema_source": "openapi/e5ff24c0-ae4e-46bb-93db-2d678610a8d3.json" + }, + { + "tool": "suno_mashup_lyrics", + "method": "POST", + "path": "/suno/mashup-lyrics", + "parameters": [ + "lyrics_a", + "lyrics_b" + ], + "aliases": {}, + "schema_source": "openapi/e0ffe8d7-c6dd-4b46-93d9-5fa6c744310c.json" + }, + { + "tool": "suno_create_voice", + "method": "POST", + "path": "/suno/voices", + "parameters": [ + "audio_url", + "name", + "description" + ], + "aliases": {}, + "schema_source": "openapi/1aece23f-07b3-4c21-8b2b-d93ce9bf0e84.json" + }, + { + "tool": "suno_create_persona", + "method": "POST", + "path": "/suno/persona", + "parameters": [ + "name", + "audio_id", + "vox_audio_id", + "vocal_start", + "vocal_end", + "description" + ], + "aliases": {}, + "schema_source": "openapi/c578b8de-f9fe-4220-8d5f-5da7384705fd.json" + }, + { + "tool": "suno_list_personas", + "method": "GET", + "path": "/suno/persona", + "parameters": [ + "user_id", + "limit", + "offset" + ], + "aliases": {}, + "schema_source": "openapi/c578b8de-f9fe-4220-8d5f-5da7384705fd.json" + }, + { + "tool": "suno_delete_persona", + "method": "DELETE", + "path": "/suno/persona", + "parameters": [ + "persona_id", + "user_id", + "confirm" + ], + "aliases": {}, + "schema_source": "openapi/c578b8de-f9fe-4220-8d5f-5da7384705fd.json" + }, + { + "tool": "suno_custom_models", + "method": "POST", + "path": "/suno/custom-models", + "parameters": [ + "action", + "name", + "audio_urls", + "callback_url", + "id", + "lyric", + "lyric_prompt", + "style", + "negative_tags", + "title", + "instrumental", + "vocal_gender", + "weirdness", + "style_influence", + "duration", + "limit", + "offset", + "status", + "idempotency_key", + "confirm" + ], + "aliases": {}, + "schema_source": "openapi/3620cff3-7d18-4e16-aeac-72abafdf8a81.json" + }, + { + "tool": "suno_projects", + "method": "POST", + "path": "/suno/projects", + "parameters": [ + "action", + "title", + "id", + "version_id", + "state", + "audio_url", + "callback_url", + "audio_id", + "name", + "start_beats", + "end_beats", + "gain", + "source_audio_id", + "model", + "tags", + "prompt", + "start_seconds", + "end_seconds", + "render_audio_id", + "stem_control_tags", + "batch_size", + "replacement_lyrics", + "fixed", + "operation_id", + "candidate_id", + "track_id", + "lyrics", + "idempotency_key" + ], + "aliases": {}, + "schema_source": "openapi/ea455d55-7129-44ae-80a3-bf71aae074d8.json" + }, + { + "tool": "suno_task_retrieve", + "method": "POST", + "path": "/suno/tasks", + "parameters": [ + "task_id", + "wait_seconds" + ], + "aliases": {}, + "schema_source": "openapi/f1143f0b-8146-4fc0-b02c-ac8db969392d.json" + }, + { + "tool": "suno_tasks_retrieve_batch", + "method": "POST", + "path": "/suno/tasks", + "parameters": [ + "ids" + ], + "aliases": {}, + "schema_source": "openapi/f1143f0b-8146-4fc0-b02c-ac8db969392d.json" + } + ], + "mcp_scope": "suno", + "notes": [ + "Informational MCP model/action lists are represented by tool dropdowns and README capability tables.", + "GPT Image covers image-generation/editing/task capabilities of the broader OpenAI MCP; chat is supplied by ModelProviderDify." + ] +} diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 5a6d822..27c89a4 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,189 +1,319 @@ +import importlib import json from pathlib import Path import dify_plugin # noqa: F401 import pytest import requests +import yaml from dify_plugin.config.config import DifyPluginEnv from dify_plugin.core.plugin_registration import PluginRegistration +from dify_plugin.entities.tool import ToolRuntime -from tools.acedata_client import BASE, SERVICE, https_urls +import tools.acedata_client as api from tools.acedata_client import AceDataSunoClient as Client from tools.acedata_client import AceDataSunoError as APIError +from tools.api_contracts import ENDPOINTS, TASK_PATH def response(body, status=200): - r = requests.Response() - r.status_code = status - r._content = json.dumps(body).encode() - r.close = lambda: None - return r + result = requests.Response() + result.status_code = status + result._content = json.dumps(body).encode() + result.close = lambda: None + return result -def test_plugin_loads_all_declared_tools(): +def test_sdk_registration_and_branded_assets(): registration = PluginRegistration(DifyPluginEnv()) assert registration.configuration.name - assert Path("_assets/" + registration.configuration.icon).is_file() + import hashlib + provenance = json.loads(Path("tests/branding-source.json").read_text()) + icon = Path("_assets") / registration.configuration.icon + assert icon.is_file() + assert hashlib.sha256(icon.read_bytes()).hexdigest() == provenance["sha256"] -def test_credential_validation_is_read_only(monkeypatch): + +def test_credentials_are_read_only(monkeypatch): + calls = [] + monkeypatch.setattr(requests, "request", lambda *a, **k: calls.append((a, k)) or response(None)) + Client("token").validate() + assert len(calls) == (1 if TASK_PATH else 0) + if calls: + args, kwargs = calls[0] + assert args == ("POST", api.BASE + TASK_PATH) + assert kwargs["json"]["action"] == "retrieve" + assert kwargs["allow_redirects"] is False + + +@pytest.mark.parametrize("status", [302, 400, 401, 403, 429, 500]) +def test_error_bodies_are_never_exposed(monkeypatch, status): + monkeypatch.setattr(requests, "request", lambda *a, **k: response({"error": "PRIVATE"}, status)) + with pytest.raises(APIError) as error: + Client("SECRET")._request("POST", "/example", {}) + assert str(status) in str(error.value) + assert "PRIVATE" not in str(error.value) and "SECRET" not in str(error.value) + + +def test_paid_request_is_never_retried(monkeypatch): calls = [] - def request(method, url, **kwargs): - calls.append((method, url, kwargs)) - return response({}) + def fail(*args, **kwargs): + calls.append(kwargs) + raise requests.Timeout("PRIVATE") - monkeypatch.setattr(requests, "request", request) - Client("sample").validate() - method, url, kwargs = calls[0] - assert method == "POST" and url == BASE + SERVICE["prefix"] + "/tasks" - assert kwargs["json"]["action"] == "retrieve" - assert kwargs["allow_redirects"] is False + monkeypatch.setattr(requests, "request", fail) + with pytest.raises(APIError): + Client("token")._request("POST", "/example", {}) + assert len(calls) == 1 -@pytest.mark.parametrize("status", [400, 401, 403, 429, 500]) -def test_api_errors_redact_bodies(monkeypatch, status): - monkeypatch.setattr( - requests, "request", lambda *a, **k: response({"error": "private-user-data"}, status) - ) - with pytest.raises(APIError) as exc: - Client("secret").validate() - assert str(status) in str(exc.value) - assert "private-user-data" not in str(exc.value) - assert "secret" not in str(exc.value) +def test_read_only_transport_can_recover(monkeypatch): + calls = [] + def send(*args, **kwargs): + calls.append(kwargs) + if len(calls) < 3: + raise requests.ConnectionError("PRIVATE") + return response({}) -def test_network_failure_does_not_retry_submission(monkeypatch): - calls = [] + monkeypatch.setattr(requests, "request", send) + monkeypatch.setattr(api.time, "sleep", lambda _: None) + Client("token")._request("POST", "/example", {}, read_only=True) + assert len(calls) == 3 - def fail(*a, **kw): - calls.append(1) - raise requests.Timeout("private-user-data") - monkeypatch.setattr(requests, "request", fail) - with pytest.raises(APIError) as exc: - Client("sample").request("POST", SERVICE["prefix"] + "/tasks", {}) - assert len(calls) == 1 and "private-user-data" not in str(exc.value) +def test_running_previews_remain_pending(): + result = Client("token").normalize( + {"response": {"status": "running", "data": [{"audio_url": "https://example.org/preview"}]}}, + "owned-task", + retrieved=True, + ) + assert result["status"] == "pending" and not result["success"] and result["media_urls"] == [] -def test_task_strips_request_and_identity(monkeypatch): - key = {"image": "image_url", "video": "video_url", "audio": "audio_url"}[SERVICE["media"]] - body = { - "id": "task", - "finished_at": 10, - "user_id": "private", - "request": {"prompt": "private"}, - "response": { - "success": True, - "state": "complete", - "data": [{key: "https://cdn.example.org/result", "internal": "private"}], +def test_explicit_unfinished_task_stays_pending_even_with_intermediate_error(): + result = Client("token").normalize( + {"finished_at": None, "response": {"success": False, "error": "PRIVATE"}}, + "owned-task", + retrieved=True, + ) + assert result["status"] == "pending" + assert "PRIVATE" not in json.dumps(result) + + +def test_completed_results_keep_public_data_and_remove_private_metadata(): + result = Client("token").normalize( + { + "response": { + "status": "succeeded", + "data": [ + {"image_url": "https://example.org/final.png", "public_result": "retained"} + ], + "request": {"image_url": "https://example.org/private.png"}, + "user_id": "PRIVATE", + "internal_supplier": "PRIVATE", + "actual_model": "PRIVATE", + } }, - } - monkeypatch.setattr(requests, "request", lambda *a, **k: response(body)) - result = Client("sample").execute("task", {"task_id": "task", "wait_seconds": 0}) - assert result["status"] == "succeeded" - assert result["media_urls"] == ["https://cdn.example.org/result"] - assert "private" not in json.dumps(result) + "owned-task", + retrieved=True, + ) + assert result["status"] == "succeeded" and result["success"] + assert result["media_urls"] == ["https://example.org/final.png"] + assert result["data"][0]["public_result"] == "retained" + assert "PRIVATE" not in json.dumps(result) and "private.png" not in json.dumps(result) -def test_pending_media_is_not_reported_complete(monkeypatch): - key = {"image": "image_url", "video": "video_url", "audio": "audio_url"}[SERVICE["media"]] - body = { - "id": "task", - "response": { - "success": True, - "state": "pending", - "data": [{key: "https://cdn.example.org/preview"}], +def test_terminal_failure_raises(): + with pytest.raises(APIError): + Client("token").normalize({"response": {"status": "failed"}}, "owned-task", retrieved=True) + + +def test_native_task_envelope_completes(): + result = Client("token").normalize( + { + "task": { + "id": "owned-task", + "status": "succeeded", + "content": {"url": "https://example.org/final.mp4"}, + } }, - } - monkeypatch.setattr(requests, "request", lambda *a, **k: response(body)) - result = Client("sample").execute("task", {"task_id": "task", "wait_seconds": 0}) - assert result["status"] == "pending" and result["media_urls"] == [] + "owned-task", + retrieved=True, + ) + assert result["status"] == "succeeded" and result["media_urls"] == [ + "https://example.org/final.mp4" + ] -def test_failed_task_is_not_reported_as_success(monkeypatch): - monkeypatch.setattr( - requests, - "request", - lambda *a, **k: response({"response": {"success": False, "error": "private"}}), +def test_sync_acceptance_is_not_completion(): + result = Client("token").normalize( + {"task_id": "owned-task", "status": "pending"}, "owned-task", synchronous=True ) - with pytest.raises(APIError): - Client("sample").execute("task", {"task_id": "task", "wait_seconds": 0}) + assert result["status"] == "pending" -def test_input_validation_before_network(monkeypatch): - monkeypatch.setattr( - requests, "request", lambda *a, **k: pytest.fail("invalid input made a request") +def test_finished_text_result_needs_no_media(): + result = Client("token").normalize( + {"finished_at": 10, "response": {"data": {"text": "lyrics"}}}, "owned-task", retrieved=True ) - with pytest.raises(ValueError): - Client("sample").execute("generate", {}) - with pytest.raises(ValueError): - https_urls(["http://example.org/private"]) + assert result["status"] == "succeeded" and result["data"] == {"text": "lyrics"} -def test_service_specific_payload(): - p = {"prompt": "test", "text": "test", "model": SERVICE["default"]} - data, path, header = Client("sample").payload("generate", p) - assert data["async"] is True - assert path.startswith(SERVICE["prefix"] + "/") - if SERVICE["family"] == "fish": - assert header == SERVICE["default"] and "model" not in data - else: - assert data["model"] == SERVICE["default"] - if SERVICE["family"] == "seedance": - assert data["content"] == [{"type": "text", "text": "test"}] - if SERVICE["family"] in {"nano", "gpt"}: +def test_invalid_values_fail_before_network(monkeypatch): + monkeypatch.setattr(requests, "request", lambda *a, **k: pytest.fail("invalid request escaped")) + for value in [True, float("inf"), "bad"]: with pytest.raises(ValueError): - Client("sample").payload("edit", p) - data, _, _ = Client("sample").payload( - "edit", {**p, "image_urls": "https://example.org/input.png"} - ) - assert data.get("image_urls", data.get("image")) == ["https://example.org/input.png"] + v = api.coerce(value, "duration", {"type": "integer"}) + api.validate(v, {"type": "integer"}, "duration") + with pytest.raises(ValueError): + api.coerce("not-json", "content", {"type": "array", "items": {"type": "object"}}) -@pytest.mark.parametrize("complete", [False, True]) -def test_task_tool_uses_legacy_credentials_and_output_messages(monkeypatch, complete): - from dify_plugin.entities.tool import ToolRuntime +def test_batch_preserves_individual_states(monkeypatch): + name = next((k for k, v in ENDPOINTS.items() if v.get("operation") == "batch"), None) + if not name: + return + body = { + "items": [ + { + "id": "done", + "finished_at": 1, + "response": { + "status": "succeeded", + "data": [{"audio_url": "https://example.org/final"}], + }, + }, + {"id": "pending", "finished_at": None, "response": None}, + {"id": "failed", "finished_at": 1, "response": {"success": False, "error": "PRIVATE"}}, + ] + } + monkeypatch.setattr(requests, "request", lambda *a, **k: response(body)) + result = Client("token").invoke(name, {"ids": '["done","pending","failed"]'}) + assert result["status"] == "pending" + assert [x["status"] for x in result["data"]] == ["succeeded", "pending", "failed"] + assert "PRIVATE" not in json.dumps(result) - from tools.suno_task_retrieve import SunoTaskRetrieveTool - key = {"image": "image_url", "video": "video_url", "audio": "audio_url"}[SERVICE["media"]] - url = "https://cdn.example.org/output" +@pytest.mark.parametrize("complete", [False, True]) +def test_actual_tool_returns_compatible_variables_and_terminal_media(monkeypatch, complete): + name = next((k for k, v in ENDPOINTS.items() if v.get("operation") == "task"), None) + if not name: + return body = { - "id": "existing-task", - "finished_at": 10 if complete else None, + "id": "owned-task", + "finished_at": 1 if complete else None, "response": { - "success": True, - "state": "complete" if complete else "pending", - "trace_id": "test-trace", - "data": [{key: url}], + "state": "complete" if complete else "running", + "trace_id": "trace", + "data": [{"image_url": "https://example.org/final.png"}], }, } calls = [] - def request(method, endpoint, **kwargs): - calls.append((method, endpoint, kwargs)) + def send(*args, **kwargs): + calls.append(kwargs) return response(body) - monkeypatch.setattr(requests, "request", request) - tool = SunoTaskRetrieveTool( + monkeypatch.setattr(requests, "request", send) + definition = next( + yaml.safe_load(p.read_text()) + for p in Path("tools").glob("*.yaml") + if yaml.safe_load(p.read_text())["identity"]["name"] == name + ) + module = importlib.import_module( + definition["extra"]["python"]["source"].replace("/", ".").removesuffix(".py") + ) + cls = next( + x for x in vars(module).values() if isinstance(x, type) and x.__module__ == module.__name__ + ) + tool = cls( runtime=ToolRuntime( - credentials={"acedata_bearer_token": "owned-test-token"}, user_id=None, session_id=None + credentials={"acedata_bearer_token": "owned-token"}, user_id=None, session_id=None ), session=None, ) - messages = list(tool._invoke({"task_id": "existing-task", "wait_seconds": 0})) + messages = list(tool._invoke({"task_id": "owned-task", "wait_seconds": 0})) variables = { m.message.variable_name: m.message.variable_value for m in messages if m.type.value == "variable" } - assert len(calls) == 1 - assert calls[0][2]["headers"]["Authorization"] == "Bearer owned-test-token" - assert calls[0][2]["json"] == {"action": "retrieve", "id": "existing-task"} - assert variables["success"] is complete + assert calls[0]["headers"]["Authorization"] == "Bearer owned-token" assert variables["status"] == ("succeeded" if complete else "pending") - assert variables["trace_id"] == "test-trace" - assert variables["data"] == [{key: url}] - media_type = "image" if SERVICE["media"] == "image" else "link" - assert len([m for m in messages if m.type.value == media_type]) == int(complete) + assert variables["success"] is complete + assert variables["trace_id"] == "trace" + assert len([m for m in messages if m.type.value in ["image", "link"]]) == int(complete) + + +CASES = json.loads(Path("tests/contract-examples.json").read_text()) + + +@pytest.mark.parametrize("case", CASES, ids=[c["name"] for c in CASES]) +def test_public_contract_and_actual_dify_defaults(case): + tool = case["tool"] + definition = next( + yaml.safe_load(p.read_text()) + for p in Path("tools").glob("*.yaml") + if yaml.safe_load(p.read_text())["identity"]["name"] == tool + ) + defaults = {p["name"]: p["default"] for p in definition["parameters"] if "default" in p} + if case.get("error"): + with pytest.raises(ValueError): + Client("token").prepare(tool, {**defaults, **case["inputs"]}) + return + method, path, body, query, headers, readonly, asynchronous = Client("token").prepare( + tool, {**defaults, **case["inputs"]} + ) + assert method == case["method"] and path == case["path"] + for k, v in case.get("body", {}).items(): + assert body[k] == v + for k in case.get("absent", []): + assert k not in body + assert query == case.get("query", {}) + assert headers == case.get("headers", {}) + assert readonly is case.get("read_only", False) + if "async" in case: + assert asynchronous is case["async"] + + +def test_union_boolean_parameter_is_not_a_string(): + assert ( + api.coerce("false", "multi_shot", {"anyOf": [{"type": "boolean"}, {"enum": [True]}]}) + is False + ) + + +def test_partial_output_has_explicit_status(): + body = { + "response": { + "data": [ + {"status": "succeeded", "image_url": "https://example.org/final.png"}, + {"status": "failed", "error": {"message": "PRIVATE"}}, + ] + } + } + if False: + result = Client("token").normalize(body, "owned-task", retrieved=True) + assert result["status"] == "partial" and not result["success"] and result["media_urls"] + else: + with pytest.raises(APIError): + Client("token").normalize(body, "owned-task", retrieved=True) + + +def test_primary_audio_does_not_advertise_optional_unrendered_video(): + value = { + "data": [ + { + "audio_url": "https://example.org/song.mp3", + "video_url": "https://example.org/not-rendered.mp4", + "image_url": "https://example.org/cover.jpg", + } + ] + } + if True: + assert api.media_urls(value) == ["https://example.org/song.mp3"] + else: + assert "https://example.org/song.mp3" in api.media_urls(value) diff --git a/tools/acedata_client.py b/tools/acedata_client.py index 1b56def..e10790e 100644 --- a/tools/acedata_client.py +++ b/tools/acedata_client.py @@ -1,287 +1,584 @@ -"""Fixed Ace Data Cloud APIs; submission is never retried automatically.""" +"""Suno API transport and task handling, using the published API contract.""" from __future__ import annotations import json +import math +import re import time from typing import Any -from urllib.parse import urlsplit +from urllib.parse import quote import requests +from jsonschema import Draft202012Validator + +from tools.api_contracts import ENDPOINTS, TASK_PATH -SERVICE = { - "family": "suno", - "prefix": "/suno", - "media": "audio", - "default": "chirp-v6", - "models": ["chirp-v6", "chirp-v6-wild", "chirp-v6-mini", "chirp-v5-5", "chirp-v5"], -} BASE = "https://api.acedata.cloud" -TERMINAL_FAILURES = {"failed", "error", "cancelled", "canceled", "rejected"} -TERMINAL_SUCCESS = {"complete", "completed", "succeeded", "succeed", "success", "finished"} +FAILED = {"failed", "error", "cancelled", "canceled", "rejected"} +COMPLETE = {"complete", "completed", "succeeded", "succeed", "success", "finished"} +SENSITIVE_KEYS = { + "request", + "request_body", + "user_id", + "actor_user_id", + "credential_id", + "authorization_id", + "application_id", + "api_key", + "access_token", + "refresh_token", + "authorization", + "headers", + "supplier", + "supplier_id", + "provider_route", + "upstream", + "upstream_model", + "actual_model", + "secret", +} +MEDIA_KEYS = {"image_url", "audio_url", "video_url", "file_url", "raw_image_url", "url"} class AceDataSunoError(RuntimeError): - pass - + """A public error which never includes raw service payloads or credentials.""" -def integer(value: Any, field: str, low: int, high: int) -> int: - if isinstance(value, bool): - raise ValueError(f"{field} must be an integer.") - try: - number = int(value) - if float(value) != number or not low <= number <= high: - raise ValueError - return number - except (ValueError, TypeError, OverflowError): - raise ValueError(f"{field} must be an integer from {low} to {high}.") from None - - -def https_urls(value: Any, required: bool = False) -> list[str]: - if not value: - if required: - raise ValueError("At least one reference image URL is required.") - return [] - if isinstance(value, str): - value = json.loads(value) if value.strip().startswith("[") else value.splitlines() - if not isinstance(value, list) or not 1 <= len(value) <= 16: - raise ValueError("Provide 1–16 public HTTPS image URLs.") - result = [] - for url in value: - if not isinstance(url, str): - raise ValueError("Each reference must be a public HTTPS URL.") - url = url.strip() - parsed = urlsplit(url) - if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password: - raise ValueError("Each reference must be a public HTTPS URL without user credentials.") - result.append(url) - return result - - -def required_text(params: dict[str, Any], name: str) -> str: - value = params.get(name) - if not isinstance(value, str) or not value.strip(): - raise ValueError(f"{name} is required.") - return value.strip() - - -def choice(params: dict[str, Any], name: str, allowed: list[str], default: str) -> str: - value = params.get(name) or default - if value not in allowed: - raise ValueError(f"Unsupported {name} value.") - return value - -def safe_result(value: Any) -> Any: - """Return output fields, excluding saved request, identity and routing metadata.""" - fields = { - "data", - "content", - "success", - "id", - "task_id", - "trace_id", - "state", - "status", - "model", - "image_url", - "audio_url", - "video_url", - "last_frame_url", - "url", - "title", - "lyric", - "duration", - "width", - "height", - "created", - "format", - "items", - "_id", - "languages", - "tags", - "prompt_tokens", - "completion_tokens", - "total_tokens", - "usage", - } +def clean_result(value: Any) -> Any: if isinstance(value, dict): - return {k: safe_result(v) for k, v in value.items() if k in fields} + result = {} + for key, item in value.items(): + if key.lower() in SENSITIVE_KEYS or re.search( + r"(^internal(?:_|$)|supplier|upstream|(?:^|_)secret(?:_|$))", key, re.I + ): + continue + if key == "error" and item: + result[key] = { + "code": item.get("code", "api_error") + if isinstance(item, dict) + else "api_error", + "message": "Operation failed; use the task or trace ID to inspect the request.", + } + else: + result[key] = clean_result(item) + return result if isinstance(value, list): - return [safe_result(v) for v in value] + return [clean_result(v) for v in value] return value def media_urls(value: Any) -> list[str]: - urls = [] - keys = {"image": {"url", "image_url"}, "audio": {"audio_url"}, "video": {"video_url"}}[ - SERVICE["media"] - ] + result: list[str] = [] + audio_result: list[str] = [] def walk(node: Any) -> None: if isinstance(node, dict): - for k, v in node.items(): - if k in keys and isinstance(v, str) and v.startswith("https://"): - urls.append(v) - elif k in {"data", "content"}: - walk(v) + for key, item in node.items(): + if ( + key in MEDIA_KEYS + and isinstance(item, str) + and item.startswith(("https://", "http://")) + ): + result.append(item) + if key == "audio_url": + audio_result.append(item) + elif isinstance(item, (dict, list)): + walk(item) elif isinstance(node, list): - for v in node: - walk(v) + for item in node: + walk(item) walk(value) - return list(dict.fromkeys(urls)) + return list(dict.fromkeys(audio_result or result)) + + +def parse_json(value: Any, name: str, *, lines: bool = False) -> Any: + if not isinstance(value, str): + return value + value = value.strip() + try: + return json.loads(value) + except ValueError: + if lines: + return [x.strip() for x in value.splitlines() if x.strip()] + raise ValueError(f"{name} must contain valid JSON.") from None + + +def coerce(value: Any, name: str, schema: dict[str, Any]) -> Any: + typ = schema.get("type") + options = schema.get("oneOf", schema.get("anyOf", [])) + types = {s.get("type") for s in options if isinstance(s.get("type"), str)} + if isinstance(typ, list): + types.update(typ) + if isinstance(typ, str) and typ in {"array", "object"}: + value = parse_json( + value, name, lines=typ == "array" and schema.get("items", {}).get("type") == "string" + ) + elif ( + isinstance(value, str) + and value.strip().startswith(("[", "{")) + and types.intersection({"array", "object"}) + ): + value = parse_json(value, name) + if isinstance(typ, str) and typ in {"number", "integer"} and not isinstance(value, bool): + if isinstance(value, str): + try: + value = float(value) + except ValueError: + raise ValueError(f"{name} must be a number.") from None + if isinstance(value, (int, float)) and not math.isfinite(value): + raise ValueError(f"{name} must be finite.") + if typ == "integer" and isinstance(value, (int, float)) and value == int(value): + value = int(value) + if ( + isinstance(value, str) + and types.intersection({"integer", "number"}) + and value.lstrip("-+").replace(".", "", 1).isdigit() + ): + number = float(value) + value = int(number) if number.is_integer() else number + if ( + (typ == "boolean" or types == {"boolean"}) + and isinstance(value, str) + and value.lower() in {"true", "false"} + ): + value = value.lower() == "true" + return value + + +def validate(value: Any, schema: dict[str, Any], context: str) -> None: + errors = list(Draft202012Validator(schema).iter_errors(value)) + if errors: + first = errors[0] + location = ".".join(str(x) for x in first.absolute_path) or context + raise ValueError( + f"Invalid {location}: does not meet the API's {first.validator} constraint." + ) + + +def allows_property(schema: dict[str, Any], name: str, values: dict[str, Any]) -> bool: + if name in schema.get("properties", {}): + return True + for group in ["oneOf", "anyOf", "allOf"]: + for branch in schema.get(group, []): + discriminators = { + k: v + for k, v in branch.get("properties", {}).items() + if k in {"action", "mode"} and (v.get("enum") or "const" in v) + } + matches = all( + k not in values or values[k] in v.get("enum", [v.get("const")]) + for k, v in discriminators.items() + ) + if matches and allows_property(branch, name, values): + return True + return False class AceDataSunoClient: def __init__(self, bearer_token: str) -> None: - key = bearer_token - if not isinstance(key, str) or not key.strip(): - raise ValueError("An Ace Data Cloud API key is required.") - self.key = key.strip().removeprefix("Bearer ").strip() - if not self.key: - raise ValueError("An Ace Data Cloud API key is required.") - - def request( + if not isinstance(bearer_token, str) or not bearer_token.strip(): + raise ValueError("An Ace Data Cloud token is required.") + token = bearer_token.strip() + self._token = token[7:].strip() if token.lower().startswith("bearer ") else token + if not self._token: + raise ValueError("An Ace Data Cloud token is required.") + + def _request( self, method: str, path: str, payload: dict[str, Any] | None = None, query: dict[str, Any] | None = None, - model_header: str | None = None, - ) -> dict[str, Any]: - headers = {"Authorization": f"Bearer {self.key}", "Accept": "application/json"} - if model_header: - headers["model"] = model_header - try: - with requests.request( - method, - BASE + path, - headers=headers, - json=payload, - params=query, - timeout=(10, 60), - allow_redirects=False, - ) as response: - if response.status_code != 200: - hints = { - 400: "Check inputs and model access.", - 401: "Check the API key.", - 403: "Check permissions and content restrictions.", - 429: "Rate limited; wait before trying again.", - } - raise AceDataSunoError( - f"Ace Data Cloud HTTP {response.status_code}. " - + hints.get( - response.status_code, - "Service unavailable. Check request history before resubmitting.", + headers: dict[str, str] | None = None, + *, + read_only: bool = False, + validation: bool = False, + ) -> dict[str, Any] | list[Any]: + attempts = 3 if read_only else 1 + for attempt in range(attempts): + try: + with requests.request( + method, + BASE + path, + json=payload, + params=query, + headers={ + "Authorization": "Bearer " + self._token, + "Accept": "application/json", + **(headers or {}), + }, + timeout=(10, 60), + allow_redirects=False, + ) as response: + if not 200 <= response.status_code < 300: + raise AceDataSunoError( + f"Ace Data Cloud HTTP {response.status_code}. Check service access and request history before submitting again." ) - ) - body = response.json() - except requests.RequestException: - raise AceDataSunoError( - "Connection failed. Do not resubmit blindly; check request history for an accepted task." - ) from None - except ValueError: - raise AceDataSunoError("The API returned an invalid JSON response.") from None - if not isinstance(body, dict): - raise AceDataSunoError("The API returned an invalid result.") - if body.get("success") is False or body.get("error"): + if response.status_code == 204: + return {} + body = response.json() + trace = response.headers.get("x-trace-id") + if isinstance(body, dict) and trace and not body.get("trace_id"): + body["trace_id"] = trace + break + except requests.RequestException as exc: + if attempt + 1 < attempts: + time.sleep(0.5 * (attempt + 1)) + continue + raise AceDataSunoError( + f"Connection failed ({type(exc).__name__}). Preserve any accepted task ID and inspect request history before resubmitting." + ) from None + except ValueError: + raise AceDataSunoError("The service returned invalid JSON.") from None + if body is None and (validation or path == TASK_PATH): + return {} + if not isinstance(body, (dict, list)): + raise AceDataSunoError("The service returned an invalid result.") + if ( + not validation + and isinstance(body, dict) + and (body.get("success") is False or body.get("error")) + ): raise AceDataSunoError( - "The API reported a failure. Check the request in the Ace Data Cloud console." + "The service reported an error. Inspect the request using its task or trace ID." ) return body def validate(self) -> None: - self.request( - "POST", - SERVICE["prefix"] + "/tasks", - {"action": "retrieve", "id": "00000000-0000-0000-0000-000000000000"}, + if TASK_PATH: + self._request( + "POST", + TASK_PATH, + {"action": "retrieve", "id": "00000000-0000-0000-0000-000000000000"}, + read_only=True, + validation=True, + ) + + def prepare( + self, tool: str, parameters: dict[str, Any] + ) -> tuple[str, str, dict[str, Any], dict[str, Any], dict[str, str], bool, bool]: + endpoint = ENDPOINTS[tool] + params = {k: v for k, v in parameters.items() if v is not None} + if endpoint.get("selector"): + choice = params.get("action", endpoint["selector_default"]) + if choice not in endpoint["selector"]: + raise ValueError("Unsupported operation.") + endpoint = ENDPOINTS[endpoint["selector"][choice]] + params = adapt_parameters(tool, params, endpoint) + values = {**endpoint.get("defaults", {}), **params, **endpoint.get("fixed", {})} + properties = endpoint["properties"] + body: dict[str, Any] = {} + for name, schema in properties.items(): + if name in values and values[name] != "": + body[name] = coerce(values[name], name, schema) + elif name in values and name in endpoint.get("allow_empty", []): + body[name] = values[name] + query_action = values.get("action") in endpoint.get("query_actions", []) + if ( + not query_action + and "async" in properties + and allows_property(endpoint["schema"], "async", values) + ): + body["async"] = True + if "stream" in properties and allows_property(endpoint["schema"], "stream", values): + body["stream"] = False + if "response_format" in properties and endpoint.get("media_response"): + body["response_format"] = "url" + path = endpoint["path"] + query: dict[str, Any] = {} + request_headers: dict[str, str] = {} + for parameter in endpoint.get("parameters", []): + name = parameter["name"] + input_name = "idempotency_key" if name.lower() == "idempotency-key" else name + if input_name not in values or values[input_name] == "": + if parameter.get("required"): + raise ValueError(f"{input_name} is required.") + continue + value = coerce(values[input_name], input_name, parameter.get("schema", {})) + validate(value, parameter.get("schema", {}), input_name) + if parameter["in"] == "path": + path = path.replace("{" + name + "}", quote(str(value), safe="")) + elif parameter["in"] == "query": + query[name] = value + elif parameter["in"] == "header": + request_headers[name] = str(value) + body = finalize_payload(tool, body, values, endpoint) + validate(body, endpoint["schema"], "parameters") + action = body.get("action") + if endpoint.get("destructive") or action in {"delete", "archive"}: + if params.get("confirm") is not True: + raise ValueError( + "Set confirm to true to perform this deletion or archive operation." + ) + read_only = ( + endpoint["method"] == "GET" + or endpoint.get("read_only", False) + or (endpoint.get("query_actions") and action in endpoint["query_actions"]) + ) + asynchronous = endpoint.get("asynchronous", False) or body.get("async") is True + return ( + endpoint["method"], + path, + body, + query, + request_headers, + bool(read_only), + bool(asynchronous), ) - def execute(self, action: str, params: dict[str, Any]) -> dict[str, Any]: - if action == "task": - task_id = required_text(params, "task_id") - wait = integer(params.get("wait_seconds", 0), "wait_seconds", 0, 240) + def invoke(self, tool: str, parameters: dict[str, Any]) -> dict[str, Any]: + endpoint = ENDPOINTS[tool] + if endpoint.get("operation") == "task": + task_id = parameters.get("task_id") or parameters.get("id") + trace_id = parameters.get("trace_id") if "trace_id" in endpoint["properties"] else None + if not (isinstance(task_id, str) and task_id.strip()) and not ( + isinstance(trace_id, str) and trace_id.strip() + ): + raise ValueError("A task ID or supported trace ID is required.") + lookup = {"action": "retrieve"} + if task_id: + lookup["id"] = task_id + if trace_id: + lookup["trace_id"] = trace_id + wait = coerce(parameters.get("wait_seconds", 0), "wait_seconds", {"type": "integer"}) + validate(wait, {"type": "integer", "minimum": 0, "maximum": 240}, "wait_seconds") deadline = time.monotonic() + wait while True: - body = self.request( - "POST", SERVICE["prefix"] + "/tasks", {"action": "retrieve", "id": task_id} + body = self._request("POST", TASK_PATH, lookup, read_only=True) + resolved_id = str( + task_id or (body.get("id") if isinstance(body, dict) else "") or "" ) - result = self.normalise(body, task_id, retrieved=True) + result = self.normalize(body, resolved_id, retrieved=True) if result["status"] != "pending" or time.monotonic() >= deadline: return result time.sleep(min(5, max(0, deadline - time.monotonic()))) - if action not in {"generate", "edit"}: - raise ValueError("Unsupported operation.") - payload, path, model_header = self.payload(action, params) - body = self.request("POST", path, payload, model_header=model_header) - return self.normalise(body, str(body.get("task_id") or body.get("id") or "")) - - def payload(self, action: str, p: dict[str, Any]) -> tuple[dict[str, Any], str, str | None]: - model = choice(p, "model", SERVICE["models"], SERVICE["default"]) - data = {"model": model, "async": True} - header = None - data.update( - action="generate", - custom=bool(p.get("custom", False)), - instrumental=bool(p.get("instrumental", False)), + method, path, body, query, headers, read_only, asynchronous = self.prepare(tool, parameters) + result = self._request( + method, + path, + body if method not in {"GET", "DELETE"} else None, + query, + headers, + read_only=read_only, ) - if data["custom"]: - data["style"] = required_text(p, "style") - if not data["instrumental"]: - data["lyric"] = required_text(p, "lyric") - else: - data["prompt"] = required_text(p, "prompt") - for name in ["title", "style", "lyric"]: - if p.get(name): - data[name] = p[name] - data["duration"] = integer(p.get("duration", 30), "duration", 10, 360) - path = "/suno/audios" - return (data, path, header) - - def normalise( - self, body: dict[str, Any], task_id: str, retrieved: bool = False + if endpoint.get("operation") == "batch": + items = ( + result + if isinstance(result, list) + else result.get("items", result.get("tasks", result.get("data", []))) + ) + if not isinstance(items, list): + items = [] + normalized = [] + for item in items: + if not isinstance(item, dict): + continue + try: + normalized.append( + self.normalize( + item, str(item.get("id") or item.get("task_id") or ""), retrieved=True + ) + ) + except AceDataSunoError: + normalized.append( + { + "status": "failed", + "success": False, + "task_id": str(item.get("id") or item.get("task_id") or ""), + "trace_id": "", + "media_urls": [], + "data": {}, + "result": {}, + } + ) + status = ( + "pending" + if any(x["status"] == "pending" for x in normalized) + else "failed" + if any(x["status"] == "failed" for x in normalized) + else "succeeded" + ) + return { + "status": status, + "success": status == "succeeded", + "task_id": "", + "trace_id": "", + "media_urls": [u for x in normalized for u in x["media_urls"]], + "data": normalized, + "result": {"items": normalized}, + } + task_id = ( + str( + result.get("task_id") + or ( + result.get("task", {}).get("id") if isinstance(result.get("task"), dict) else "" + ) + or "" + ) + if isinstance(result, dict) + else "" + ) + return self.normalize(result, task_id, synchronous=not asynchronous) + + def normalize( + self, + body: dict[str, Any] | list[Any], + task_id: str, + *, + retrieved: bool = False, + synchronous: bool = False, ) -> dict[str, Any]: - if retrieved and (not body): - return {"status": "pending", "task_id": task_id, "media_urls": [], "result": {}} - result = body.get("response") if retrieved else body - if not isinstance(result, dict): - return {"status": "pending", "task_id": task_id, "media_urls": [], "result": {}} - data = result.get("data") - states = [str(result[k]).lower() for k in ["state", "status"] if result.get(k)] - if isinstance(data, dict): - states += [str(data[k]).lower() for k in ["state", "status"] if data.get(k)] - if isinstance(data, list): - states += [ - str(item[k]).lower() - for item in data - if isinstance(item, dict) - for k in ["state", "status"] - if item.get(k) - ] + pending = { + "status": "pending", + "success": False, + "task_id": task_id, + "trace_id": "", + "media_urls": [], + "data": {}, + "result": {}, + } + unfinished = ( + retrieved + and isinstance(body, dict) + and "finished_at" in body + and body["finished_at"] is None + ) + if retrieved and isinstance(body, dict) and "response" in body: + result = body.get("response") + else: + result = body + if isinstance(result, str): + try: + result = json.loads(result) + except ValueError: + result = {} + if result is None or result == {} and retrieved: + return pending + states = [] + errors = [] + + def visit(node: Any) -> None: + if isinstance(node, dict): + if node.get("success") is False or node.get("error"): + errors.append(True) + for key, value in node.items(): + if key in {"state", "status"} and isinstance(value, str): + states.append(value.lower()) + elif key in {"data", "content", "task"}: + visit(value) + elif isinstance(node, list): + for item in node: + visit(item) + + if not (synchronous and not task_id): + visit(result) + urls = media_urls(clean_result(result)) + failures = errors or any(s in FAILED for s in states) + if failures and not unfinished and not (urls and False): + raise AceDataSunoError( + "Task failed. Use its task or trace ID to inspect the service request." + ) + done = ( + (synchronous and not task_id) + or (bool(states) and all(s in COMPLETE for s in states)) + or (bool(urls) and not states) + ) + if failures and urls and False: + done = all(s in COMPLETE | FAILED for s in states) if ( - result.get("success") is False - or result.get("error") - or any((s in TERMINAL_FAILURES for s in states)) + retrieved + and isinstance(body, dict) + and body.get("finished_at") + and not states + and result ): + done = True + if unfinished: + done = False + if not done and not task_id: raise AceDataSunoError( - f"Generation failed for task {task_id}. Check the console for details." + "No task ID or completed result was returned. Check request history before submitting again." ) - urls = media_urls(result) - done = bool(urls) and (not states or all((s in TERMINAL_SUCCESS for s in states))) - done = bool(urls) and ( - all((s in TERMINAL_SUCCESS for s in states)) - if states - else bool(body.get("finished_at")) + status = "partial" if done and failures else "succeeded" if done else "pending" + data = ( + result.get("data", result.get("content", result)) + if isinstance(result, dict) + else result ) - if not task_id and (not done): - raise AceDataSunoError( - "No task ID or completed media returned; check request history before resubmitting." + trace = ( + str( + result.get("trace_id") + or (body.get("trace_id") if isinstance(body, dict) else "") + or "" ) + if isinstance(result, dict) + else "" + ) return { - "status": "succeeded" if done else "pending", + "status": status, + "success": status == "succeeded", "task_id": task_id, + "trace_id": trace, "media_urls": urls if done else [], - "result": safe_result(result), + "data": clean_result(data), + "result": clean_result(result if isinstance(result, dict) else {"items": result}), } + + +def adapt_parameters(tool: str, params: dict[str, Any], endpoint: dict[str, Any]) -> dict[str, Any]: + if endpoint["path"] == "/suno/audios" and isinstance(params.get("audio_urls"), str): + params["audio_urls"] = parse_json(params["audio_urls"], "audio_urls", lines=True) + return params + + +def finalize_payload( + tool: str, body: dict[str, Any], values: dict[str, Any], endpoint: dict[str, Any] +) -> dict[str, Any]: + if endpoint["path"] == "/suno/audios": + action = body.get("action", "generate") + if action == "generate": + if body.get("custom"): + if ( + not body.get("instrumental") + and not body.get("lyric") + and not body.get("lyric_prompt") + ): + raise ValueError("Custom vocal music requires lyrics or a lyric prompt.") + elif not body.get("prompt"): + raise ValueError("A music description is required.") + required = { + "extend": ["audio_id", "continue_at"], + "cover": ["audio_id"], + "concat": ["audio_id"], + "artist_consistency": ["persona_id"], + "artist_consistency_vox": ["persona_id"], + "remaster": ["audio_id"], + "stems": ["audio_id"], + "all_stems": ["audio_id"], + "replace_section": ["audio_id", "replace_section_start", "replace_section_end"], + "upload_extend": ["audio_id", "continue_at"], + "upload_cover": ["audio_id"], + "mashup": ["mashup_audio_ids"], + "underpainting": ["audio_id", "underpainting_start", "underpainting_end"], + "overpainting": ["audio_id", "overpainting_start", "overpainting_end"], + "samples": ["audio_id", "samples_start", "samples_end"], + "adjust_speed": ["audio_id", "speed_multiplier"], + "add_stem": ["audio_id"], + "extract_stem": ["audio_id"], + "sounds": ["sound"], + }.get(action, []) + if any(body.get(k) is None or body.get(k) == "" for k in required): + raise ValueError( + "The selected action is missing required inputs: " + ", ".join(required) + ) + for start, end in [ + ("replace_section_start", "replace_section_end"), + ("underpainting_start", "underpainting_end"), + ("overpainting_start", "overpainting_end"), + ("samples_start", "samples_end"), + ]: + if start in body and end in body and body[end] <= body[start]: + raise ValueError(end + " must exceed " + start + ".") + return body diff --git a/tools/api_contracts.py b/tools/api_contracts.py new file mode 100644 index 0000000..144723a --- /dev/null +++ b/tools/api_contracts.py @@ -0,0 +1,1007 @@ +"""Published API contracts; source revisions are recorded in tests/parity-audit.json.""" + +TASK_PATH = "/suno/tasks" + +ENDPOINTS = { + "suno_generate_audios": { + "method": "POST", + "path": "/suno/audios", + "operation": "generate", + "schema": { + "type": "object", + "properties": { + "lyric": {"type": "string"}, + "model": { + "enum": [ + "chirp-v6", + "chirp-v6-wild", + "chirp-v6-mini", + "chirp-v5-5", + "chirp-v5", + "chirp-v4-5-plus", + "chirp-v4-5", + "chirp-v4", + "chirp-v3-5", + "chirp-v3-0", + ], + "type": "string", + }, + "max_mode": {"type": "boolean"}, + "variety": {"enum": ["off", "normal", "high", "extra", "max"], "type": "string"}, + "style": {"type": "string"}, + "variation_category": {"type": "string", "enum": ["high", "normal", "subtle"]}, + "title": {"type": "string"}, + "action": { + "enum": [ + "generate", + "extend", + "upload_extend", + "upload_cover", + "concat", + "cover", + "artist_consistency", + "artist_consistency_vox", + "stems", + "all_stems", + "add_stem", + "extract_stem", + "sounds", + "adjust_speed", + "replace_section", + "underpainting", + "overpainting", + "remaster", + "mashup", + "samples", + "inspo", + ], + "type": "string", + }, + "custom": {"type": "boolean"}, + "prompt": {"type": "string", "additionalProperties": True}, + "lyric_prompt": {"type": "string"}, + "audio_id": {"type": "string"}, + "stem_type": { + "type": "string", + "enum": [ + "vocals", + "backing_vocals", + "drums", + "bass", + "guitar", + "keyboard", + "percussion", + "strings", + "synth", + "fx", + "brass", + "woodwinds", + ], + }, + "audio_format": {"type": "string", "enum": ["mp3"]}, + "sound": {"type": "string"}, + "sound_type": {"type": "string", "enum": ["one-shot", "loop"]}, + "bpm": {"type": "integer", "minimum": 1, "maximum": 300}, + "key": {"type": "string"}, + "speed_multiplier": {"type": "number", "minimum": 0.25, "maximum": 4}, + "keep_pitch": {"type": "boolean"}, + "mashup_audio_ids": {"type": "array", "items": {"type": "string"}}, + "audio_urls": { + "type": "array", + "items": {"type": "string", "pattern": "^https?://"}, + "minItems": 1, + "maxItems": 4, + }, + "weirdness": {"type": "number", "minimum": 0, "maximum": 1}, + "persona_id": {"type": "string"}, + "overpainting_start": {"type": "number"}, + "overpainting_end": {"type": "number"}, + "samples_start": {"type": "number"}, + "samples_end": {"type": "number"}, + "underpainting_start": {"type": "number"}, + "underpainting_end": {"type": "number"}, + "continue_at": {"type": "number"}, + "callback_url": {"type": "string"}, + "async": {"type": "boolean"}, + "instrumental": {"type": "boolean"}, + "vocal_gender": {"type": "string", "enum": ["f", "m"]}, + "negative_tags": {"type": "string"}, + "style_influence": {"type": "number", "minimum": 0, "maximum": 1}, + "audio_weight": {"type": "number", "minimum": 0, "maximum": 1}, + "duration": {"type": "integer", "minimum": 10, "maximum": 360}, + "replace_section_end": {"type": "number"}, + "replace_section_result_mode": { + "type": "string", + "enum": ["candidates", "full_song"], + }, + "replace_section_start": {"type": "number"}, + }, + }, + "properties": { + "lyric": {"type": "string"}, + "model": { + "enum": [ + "chirp-v6", + "chirp-v6-wild", + "chirp-v6-mini", + "chirp-v5-5", + "chirp-v5", + "chirp-v4-5-plus", + "chirp-v4-5", + "chirp-v4", + "chirp-v3-5", + "chirp-v3-0", + ], + "type": "string", + }, + "max_mode": {"type": "boolean"}, + "variety": {"enum": ["off", "normal", "high", "extra", "max"], "type": "string"}, + "style": {"type": "string"}, + "variation_category": {"type": "string", "enum": ["high", "normal", "subtle"]}, + "title": {"type": "string"}, + "action": { + "enum": [ + "generate", + "extend", + "upload_extend", + "upload_cover", + "concat", + "cover", + "artist_consistency", + "artist_consistency_vox", + "stems", + "all_stems", + "add_stem", + "extract_stem", + "sounds", + "adjust_speed", + "replace_section", + "underpainting", + "overpainting", + "remaster", + "mashup", + "samples", + "inspo", + ], + "type": "string", + }, + "custom": {"type": "boolean"}, + "prompt": {"type": "string", "additionalProperties": True}, + "lyric_prompt": {"type": "string"}, + "audio_id": {"type": "string"}, + "stem_type": { + "type": "string", + "enum": [ + "vocals", + "backing_vocals", + "drums", + "bass", + "guitar", + "keyboard", + "percussion", + "strings", + "synth", + "fx", + "brass", + "woodwinds", + ], + }, + "audio_format": {"type": "string", "enum": ["mp3"]}, + "sound": {"type": "string"}, + "sound_type": {"type": "string", "enum": ["one-shot", "loop"]}, + "bpm": {"type": "integer", "minimum": 1, "maximum": 300}, + "key": {"type": "string"}, + "speed_multiplier": {"type": "number", "minimum": 0.25, "maximum": 4}, + "keep_pitch": {"type": "boolean"}, + "mashup_audio_ids": {"type": "array", "items": {"type": "string"}}, + "audio_urls": { + "type": "array", + "items": {"type": "string", "pattern": "^https?://"}, + "minItems": 1, + "maxItems": 4, + }, + "weirdness": {"type": "number", "minimum": 0, "maximum": 1}, + "persona_id": {"type": "string"}, + "overpainting_start": {"type": "number"}, + "overpainting_end": {"type": "number"}, + "samples_start": {"type": "number"}, + "samples_end": {"type": "number"}, + "underpainting_start": {"type": "number"}, + "underpainting_end": {"type": "number"}, + "continue_at": {"type": "number"}, + "callback_url": {"type": "string"}, + "async": {"type": "boolean"}, + "instrumental": {"type": "boolean"}, + "vocal_gender": {"type": "string", "enum": ["f", "m"]}, + "negative_tags": {"type": "string"}, + "style_influence": {"type": "number", "minimum": 0, "maximum": 1}, + "audio_weight": {"type": "number", "minimum": 0, "maximum": 1}, + "duration": {"type": "integer", "minimum": 10, "maximum": 360}, + "replace_section_end": {"type": "number"}, + "replace_section_result_mode": {"type": "string", "enum": ["candidates", "full_song"]}, + "replace_section_start": {"type": "number"}, + }, + "parameters": [], + "defaults": { + "model": "chirp-v6", + "custom": False, + "instrumental": False, + "action": "generate", + }, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_generate_lyrics": { + "method": "POST", + "path": "/suno/lyrics", + "operation": "request", + "schema": { + "type": "object", + "required": ["prompt", "model"], + "properties": { + "model": {"enum": ["default", "remi-v1"], "type": "string"}, + "prompt": {"type": "string", "additionalProperties": True}, + }, + }, + "properties": { + "model": {"enum": ["default", "remi-v1"], "type": "string"}, + "prompt": {"type": "string", "additionalProperties": True}, + }, + "parameters": [], + "defaults": {"model": "default"}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_upload_reference_audio": { + "method": "POST", + "path": "/suno/upload", + "operation": "request", + "schema": { + "type": "object", + "required": ["audio_url"], + "properties": { + "audio_url": {"type": "string"}, + "mode": {"type": "string", "enum": ["standard", "enhanced"]}, + "name": {"type": "string", "minLength": 1, "maxLength": 100}, + "callback_url": {"type": "string", "format": "uri"}, + }, + }, + "properties": { + "audio_url": {"type": "string"}, + "mode": {"type": "string", "enum": ["standard", "enhanced"]}, + "name": {"type": "string", "minLength": 1, "maxLength": 100}, + "callback_url": {"type": "string", "format": "uri"}, + }, + "parameters": [], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_get_mp4": { + "method": "POST", + "path": "/suno/mp4", + "operation": "request", + "schema": { + "type": "object", + "required": ["audio_id"], + "properties": {"audio_id": {"type": "string"}}, + }, + "properties": {"audio_id": {"type": "string"}}, + "parameters": [], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_get_mp3": { + "method": "POST", + "path": "/suno/mp3", + "operation": "request", + "schema": { + "type": "object", + "required": ["audio_id"], + "properties": { + "audio_id": {"type": "string"}, + "callback_url": {"type": "string"}, + "async": {"type": "boolean"}, + }, + }, + "properties": { + "audio_id": {"type": "string"}, + "callback_url": {"type": "string"}, + "async": {"type": "boolean"}, + }, + "parameters": [], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_get_wav": { + "method": "POST", + "path": "/suno/wav", + "operation": "request", + "schema": { + "type": "object", + "required": ["audio_id"], + "properties": { + "audio_id": {"type": "string"}, + "callback_url": {"type": "string"}, + "async": {"type": "boolean"}, + }, + }, + "properties": { + "audio_id": {"type": "string"}, + "callback_url": {"type": "string"}, + "async": {"type": "boolean"}, + }, + "parameters": [], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_get_midi": { + "method": "POST", + "path": "/suno/midi", + "operation": "request", + "schema": { + "type": "object", + "required": ["audio_id"], + "properties": { + "audio_id": {"type": "string"}, + "callback_url": {"type": "string"}, + "async": {"type": "boolean"}, + }, + }, + "properties": { + "audio_id": {"type": "string"}, + "callback_url": {"type": "string"}, + "async": {"type": "boolean"}, + }, + "parameters": [], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_get_timing": { + "method": "POST", + "path": "/suno/timing", + "operation": "request", + "schema": { + "type": "object", + "required": ["audio_id"], + "properties": {"audio_id": {"type": "string"}}, + }, + "properties": {"audio_id": {"type": "string"}}, + "parameters": [], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_create_vox_audio": { + "method": "POST", + "path": "/suno/vox", + "operation": "request", + "schema": { + "type": "object", + "required": ["audio_id", "vocal_start", "vocal_end"], + "properties": { + "audio_id": {"type": "string"}, + "vocal_start": {"type": "number", "minimum": 0}, + "vocal_end": {"type": "number", "exclusiveMinimum": 0}, + "callback_url": {"type": "string"}, + "async": {"type": "boolean"}, + }, + }, + "properties": { + "audio_id": {"type": "string"}, + "vocal_start": {"type": "number", "minimum": 0}, + "vocal_end": {"type": "number", "exclusiveMinimum": 0}, + "callback_url": {"type": "string"}, + "async": {"type": "boolean"}, + }, + "parameters": [], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_enhance_style": { + "method": "POST", + "path": "/suno/style", + "operation": "request", + "schema": { + "type": "object", + "required": ["prompt"], + "properties": {"prompt": {"type": "string"}}, + }, + "properties": {"prompt": {"type": "string"}}, + "parameters": [], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_mashup_lyrics": { + "method": "POST", + "path": "/suno/mashup-lyrics", + "operation": "request", + "schema": { + "type": "object", + "required": ["lyrics_a", "lyrics_b"], + "properties": {"lyrics_a": {"type": "string"}, "lyrics_b": {"type": "string"}}, + }, + "properties": {"lyrics_a": {"type": "string"}, "lyrics_b": {"type": "string"}}, + "parameters": [], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_create_voice": { + "method": "POST", + "path": "/suno/voices", + "operation": "request", + "schema": { + "type": "object", + "required": ["audio_url"], + "properties": { + "audio_url": {"type": "string"}, + "name": {"type": "string"}, + "description": {"type": "string"}, + }, + }, + "properties": { + "audio_url": {"type": "string"}, + "name": {"type": "string"}, + "description": {"type": "string"}, + }, + "parameters": [], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": ["retrieve", "retrieve_batch", "list", "presets"], + "media_response": False, + }, + "suno_create_persona": { + "method": "POST", + "path": "/suno/persona", + "operation": "request", + "schema": { + "type": "object", + "required": ["audio_id", "name"], + "properties": { + "name": {"type": "string"}, + "audio_id": {"type": "string"}, + "vox_audio_id": {"type": "string"}, + "vocal_start": {"type": "number"}, + "vocal_end": {"type": "number"}, + "description": {"type": "string"}, + }, + }, + "properties": { + "name": {"type": "string"}, + "audio_id": {"type": "string"}, + "vox_audio_id": {"type": "string"}, + "vocal_start": {"type": "number"}, + "vocal_end": {"type": "number"}, + "description": {"type": "string"}, + }, + "parameters": [], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_list_personas": { + "method": "GET", + "path": "/suno/persona", + "operation": "request", + "schema": {"type": "object", "properties": {}}, + "properties": {}, + "parameters": [ + {"in": "query", "name": "user_id", "schema": {"type": "string"}, "required": True}, + {"in": "query", "name": "limit", "schema": {"type": "integer"}, "required": False}, + {"in": "query", "name": "offset", "schema": {"type": "integer"}, "required": False}, + ], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + }, + "suno_delete_persona": { + "method": "DELETE", + "path": "/suno/persona", + "operation": "request", + "schema": {"type": "object", "properties": {}}, + "properties": {}, + "parameters": [ + {"in": "query", "name": "persona_id", "schema": {"type": "string"}, "required": True}, + {"in": "query", "name": "user_id", "schema": {"type": "string"}, "required": False}, + ], + "defaults": {}, + "fixed": {}, + "allow_empty": [], + "query_actions": [], + "media_response": False, + "destructive": True, + }, + "suno_custom_models": { + "method": "POST", + "path": "/suno/custom-models", + "operation": "request", + "schema": { + "oneOf": [ + { + "type": "object", + "additionalProperties": False, + "required": ["action", "name", "audio_urls"], + "properties": { + "action": {"type": "string", "enum": ["create"]}, + "name": {"type": "string", "minLength": 1, "maxLength": 100}, + "audio_urls": { + "type": "array", + "minItems": 6, + "maxItems": 24, + "uniqueItems": True, + "items": {"type": "string", "format": "uri", "pattern": "^https://"}, + }, + "callback_url": {"type": "string", "format": "uri", "pattern": "^https://"}, + }, + }, + { + "type": "object", + "additionalProperties": False, + "required": ["action", "id"], + "properties": { + "action": {"type": "string", "enum": ["generate"]}, + "id": {"type": "string", "format": "uuid"}, + "lyric": {"type": "string"}, + "lyric_prompt": {"type": "string"}, + "style": {"type": "string"}, + "negative_tags": {"type": "string"}, + "title": {"type": "string"}, + "instrumental": {"type": "boolean"}, + "vocal_gender": {"type": "string", "enum": ["f", "m"]}, + "weirdness": {"type": "number", "minimum": 0, "maximum": 1}, + "style_influence": {"type": "number", "minimum": 0, "maximum": 1}, + "duration": {"type": "integer"}, + "async": {"type": "boolean"}, + "callback_url": {"type": "string", "format": "uri", "pattern": "^https://"}, + }, + }, + { + "type": "object", + "additionalProperties": False, + "required": ["action", "id"], + "properties": { + "action": {"type": "string", "enum": ["retrieve"]}, + "id": {"type": "string", "format": "uuid"}, + }, + }, + { + "type": "object", + "additionalProperties": False, + "required": ["action"], + "properties": { + "action": {"type": "string", "enum": ["retrieve_batch"]}, + "limit": {"type": "integer", "minimum": 1, "maximum": 100}, + "offset": {"type": "integer", "minimum": 0}, + "status": { + "type": "string", + "enum": [ + "queued", + "uploading", + "training", + "ready", + "failed", + "archived", + ], + }, + }, + }, + { + "type": "object", + "additionalProperties": False, + "required": ["action", "id"], + "properties": { + "action": {"type": "string", "enum": ["delete"]}, + "id": {"type": "string", "format": "uuid"}, + }, + }, + ], + "discriminator": {"propertyName": "action"}, + }, + "properties": { + "action": { + "type": "string", + "enum": ["create", "generate", "retrieve", "retrieve_batch", "delete"], + }, + "name": {"type": "string", "minLength": 1, "maxLength": 100}, + "audio_urls": { + "type": "array", + "minItems": 6, + "maxItems": 24, + "uniqueItems": True, + "items": {"type": "string", "format": "uri", "pattern": "^https://"}, + }, + "callback_url": {"type": "string", "format": "uri", "pattern": "^https://"}, + "id": {"type": "string", "format": "uuid"}, + "lyric": {"type": "string"}, + "lyric_prompt": {"type": "string"}, + "style": {"type": "string"}, + "negative_tags": {"type": "string"}, + "title": {"type": "string"}, + "instrumental": {"type": "boolean"}, + "vocal_gender": {"type": "string", "enum": ["f", "m"]}, + "weirdness": {"type": "number", "minimum": 0, "maximum": 1}, + "style_influence": {"type": "number", "minimum": 0, "maximum": 1}, + "duration": {"type": "integer"}, + "async": {"type": "boolean"}, + "limit": {"type": "integer", "minimum": 1, "maximum": 100}, + "offset": {"type": "integer", "minimum": 0}, + "status": { + "type": "string", + "enum": ["queued", "uploading", "training", "ready", "failed", "archived"], + }, + }, + "parameters": [ + { + "name": "Idempotency-Key", + "in": "header", + "required": False, + "schema": {"type": "string", "minLength": 1, "maxLength": 128}, + } + ], + "defaults": {"action": "retrieve_batch"}, + "fixed": {}, + "allow_empty": [], + "query_actions": ["retrieve", "retrieve_batch"], + "media_response": False, + }, + "suno_projects": { + "method": "POST", + "path": "/suno/projects", + "operation": "request", + "schema": { + "oneOf": [ + { + "type": "object", + "required": ["action", "title"], + "additionalProperties": False, + "properties": { + "action": {"type": "string", "enum": ["create"]}, + "title": {"type": "string", "minLength": 1, "maxLength": 200}, + }, + }, + { + "type": "object", + "required": ["action", "id"], + "additionalProperties": False, + "properties": { + "action": {"type": "string", "enum": ["retrieve"]}, + "id": {"type": "string"}, + }, + }, + { + "type": "object", + "required": ["action", "id", "state"], + "additionalProperties": False, + "properties": { + "action": {"type": "string", "enum": ["save"]}, + "id": {"type": "string"}, + "version_id": {"type": "string"}, + "title": {"type": "string"}, + "state": {"type": "object", "additionalProperties": True}, + }, + }, + { + "type": "object", + "required": ["action", "id", "version_id", "audio_url"], + "additionalProperties": False, + "properties": { + "action": {"type": "string", "enum": ["upload"]}, + "id": {"type": "string"}, + "version_id": {"type": "string"}, + "audio_url": {"type": "string", "pattern": "^https://"}, + "async": {"type": "boolean"}, + "callback_url": {"type": "string", "format": "uri"}, + }, + }, + { + "type": "object", + "required": ["action", "id", "version_id", "audio_id"], + "additionalProperties": False, + "properties": { + "action": {"type": "string", "enum": ["add_track"]}, + "id": {"type": "string"}, + "version_id": {"type": "string"}, + "audio_id": {"type": "string"}, + "name": {"type": "string"}, + "start_beats": {"type": "number", "minimum": 0}, + "end_beats": {"type": "number", "minimum": 0}, + "gain": {"type": "number"}, + }, + }, + { + "type": "object", + "required": [ + "action", + "id", + "version_id", + "source_audio_id", + "render_audio_id", + "stem_control_tags", + "model", + "start_seconds", + "end_seconds", + ], + "additionalProperties": False, + "properties": { + "action": {"type": "string", "enum": ["generate_track"]}, + "id": {"type": "string"}, + "version_id": {"type": "string"}, + "source_audio_id": {"type": "string"}, + "model": { + "type": "string", + "enum": [ + "chirp-v3-5", + "chirp-v4", + "chirp-v4-5", + "chirp-v4-5-plus", + "chirp-v5", + "chirp-v5-5", + "chirp-v6", + "chirp-v6-wild", + "chirp-v6-mini", + ], + }, + "title": {"type": "string"}, + "tags": {"type": "string"}, + "prompt": {"type": "string"}, + "start_seconds": {"type": "number", "minimum": 0}, + "end_seconds": {"type": "number", "exclusiveMinimum": 0}, + "async": {"type": "boolean"}, + "callback_url": {"type": "string", "format": "uri"}, + "render_audio_id": {"type": "string"}, + "stem_control_tags": {"type": "string"}, + "batch_size": {"type": "integer", "minimum": 1, "maximum": 4}, + }, + }, + { + "type": "object", + "required": [ + "action", + "id", + "version_id", + "source_audio_id", + "model", + "start_seconds", + "end_seconds", + ], + "additionalProperties": False, + "properties": { + "action": {"type": "string", "enum": ["replace_section"]}, + "id": {"type": "string"}, + "version_id": {"type": "string"}, + "source_audio_id": {"type": "string"}, + "model": { + "type": "string", + "enum": [ + "chirp-v3-5", + "chirp-v4", + "chirp-v4-5", + "chirp-v4-5-plus", + "chirp-v5", + "chirp-v5-5", + "chirp-v6", + "chirp-v6-wild", + "chirp-v6-mini", + ], + }, + "title": {"type": "string"}, + "tags": {"type": "string"}, + "prompt": {"type": "string"}, + "start_seconds": {"type": "number", "minimum": 0}, + "end_seconds": {"type": "number", "exclusiveMinimum": 0}, + "async": {"type": "boolean"}, + "callback_url": {"type": "string", "format": "uri"}, + "replacement_lyrics": {"type": "string"}, + "fixed": {"type": "boolean"}, + }, + }, + { + "type": "object", + "required": [ + "action", + "id", + "version_id", + "operation_id", + "candidate_id", + "track_id", + ], + "additionalProperties": False, + "properties": { + "action": {"type": "string", "enum": ["commit_candidate"]}, + "id": {"type": "string"}, + "version_id": {"type": "string"}, + "operation_id": {"type": "string"}, + "candidate_id": {"type": "string"}, + "track_id": {"type": "string"}, + "start_beats": {"type": "number", "minimum": 0}, + "end_beats": {"type": "number", "minimum": 0}, + "async": {"type": "boolean"}, + "callback_url": {"type": "string", "format": "uri"}, + }, + }, + { + "type": "object", + "required": ["action", "id", "version_id", "track_id"], + "additionalProperties": False, + "properties": { + "action": {"type": "string", "enum": ["remove_track"]}, + "id": {"type": "string"}, + "version_id": {"type": "string"}, + "track_id": {"type": "string"}, + }, + }, + { + "type": "object", + "required": ["action", "id", "version_id", "title"], + "additionalProperties": False, + "properties": { + "action": {"type": "string", "enum": ["render"]}, + "id": {"type": "string"}, + "version_id": {"type": "string"}, + "title": {"type": "string"}, + "lyrics": {"type": "string"}, + "tags": {"type": "string"}, + "start_beats": {"type": "number", "minimum": 0}, + "end_beats": {"type": "number", "minimum": 0}, + "async": {"type": "boolean"}, + "callback_url": {"type": "string", "format": "uri"}, + }, + }, + ], + "discriminator": {"propertyName": "action"}, + }, + "properties": { + "action": { + "type": "string", + "enum": [ + "create", + "retrieve", + "save", + "upload", + "add_track", + "generate_track", + "replace_section", + "commit_candidate", + "remove_track", + "render", + ], + }, + "title": {"type": "string", "minLength": 1, "maxLength": 200}, + "id": {"type": "string"}, + "version_id": {"type": "string"}, + "state": {"type": "object", "additionalProperties": True}, + "audio_url": {"type": "string", "pattern": "^https://"}, + "async": {"type": "boolean"}, + "callback_url": {"type": "string", "format": "uri"}, + "audio_id": {"type": "string"}, + "name": {"type": "string"}, + "start_beats": {"type": "number", "minimum": 0}, + "end_beats": {"type": "number", "minimum": 0}, + "gain": {"type": "number"}, + "source_audio_id": {"type": "string"}, + "model": { + "type": "string", + "enum": [ + "chirp-v3-5", + "chirp-v4", + "chirp-v4-5", + "chirp-v4-5-plus", + "chirp-v5", + "chirp-v5-5", + "chirp-v6", + "chirp-v6-wild", + "chirp-v6-mini", + ], + }, + "tags": {"type": "string"}, + "prompt": {"type": "string"}, + "start_seconds": {"type": "number", "minimum": 0}, + "end_seconds": {"type": "number", "exclusiveMinimum": 0}, + "render_audio_id": {"type": "string"}, + "stem_control_tags": {"type": "string"}, + "batch_size": {"type": "integer", "minimum": 1, "maximum": 4}, + "replacement_lyrics": {"type": "string"}, + "fixed": {"type": "boolean"}, + "operation_id": {"type": "string"}, + "candidate_id": {"type": "string"}, + "track_id": {"type": "string"}, + "lyrics": {"type": "string"}, + }, + "parameters": [ + { + "name": "Idempotency-Key", + "in": "header", + "required": False, + "schema": {"type": "string", "minLength": 1, "maxLength": 128}, + } + ], + "defaults": {"action": "retrieve"}, + "fixed": {}, + "allow_empty": [], + "query_actions": ["retrieve", "retrieve_batch", "list", "presets"], + "media_response": False, + }, + "suno_task_retrieve": { + "method": "POST", + "path": "/suno/tasks", + "operation": "task", + "schema": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "ids": {"type": "array", "items": {"type": "string"}}, + "action": {"enum": ["retrieve", "retrieve_batch"], "type": "string"}, + }, + }, + "properties": { + "id": {"type": "string"}, + "ids": {"type": "array", "items": {"type": "string"}}, + "action": {"enum": ["retrieve", "retrieve_batch"], "type": "string"}, + }, + "parameters": [], + "defaults": {"wait_seconds": 0}, + "fixed": {}, + "allow_empty": [], + "query_actions": ["retrieve", "retrieve_batch", "list", "presets"], + "media_response": False, + }, + "suno_tasks_retrieve_batch": { + "method": "POST", + "path": "/suno/tasks", + "operation": "batch", + "schema": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "ids": {"type": "array", "items": {"type": "string"}, "minItems": 1}, + "action": {"enum": ["retrieve", "retrieve_batch"], "type": "string"}, + }, + "required": ["action", "ids"], + }, + "properties": { + "id": {"type": "string"}, + "ids": {"type": "array", "items": {"type": "string"}, "minItems": 1}, + "action": {"enum": ["retrieve", "retrieve_batch"], "type": "string"}, + }, + "parameters": [], + "defaults": {}, + "fixed": {"action": "retrieve_batch"}, + "allow_empty": [], + "query_actions": ["retrieve", "retrieve_batch", "list", "presets"], + "media_response": False, + }, +} diff --git a/tools/suno_create_persona.py b/tools/suno_create_persona.py new file mode 100644 index 0000000..bcb95ee --- /dev/null +++ b/tools/suno_create_persona.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoCreatePersonaTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_create_persona", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_create_persona.yaml b/tools/suno_create_persona.yaml new file mode 100644 index 0000000..5c00fa2 --- /dev/null +++ b/tools/suno_create_persona.yaml @@ -0,0 +1,101 @@ +identity: + name: suno_create_persona + author: acedatacloud + label: + en_US: Suno Create Persona + zh_Hans: Suno 创建 Persona +description: + human: + en_US: Create Persona through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Create Persona。 + llm: Create Persona through the Ace Data Cloud Suno API. For asynchronous results, + preserve task_id and use the task retrieval tool until completion. A successful + submission is not completed output. +parameters: +- name: name + type: string + required: true + form: form + label: + en_US: Name + zh_Hans: 名称 + human_description: + en_US: Set name for this operation. + zh_Hans: 歌手风格的名称。 + llm_description: Set name for this operation. +- name: audio_id + type: string + required: true + form: form + label: + en_US: Audio Id + zh_Hans: 音频 ID + human_description: + en_US: Set audio id for this operation. + zh_Hans: 用于创建歌手风格的已生成歌曲 ID。 + llm_description: Set audio id for this operation. +- name: vox_audio_id + type: string + required: false + form: form + label: + en_US: Vox Audio Id + zh_Hans: Vox 音频 ID + human_description: + en_US: Set vox audio id for this operation. + zh_Hans: 用于生成新歌手风格的音频 ID(人声参考音频)。 + llm_description: Set vox audio id for this operation. +- name: vocal_start + type: number + required: false + form: form + label: + en_US: Vocal Start + zh_Hans: 人声片段开始 + human_description: + en_US: Set vocal start for this operation. + zh_Hans: 音频中人声片段的起始时间(秒)。 + llm_description: Set vocal start for this operation. +- name: vocal_end + type: number + required: false + form: form + label: + en_US: Vocal End + zh_Hans: 人声片段结束 + human_description: + en_US: Set vocal end for this operation. + zh_Hans: 音频中人声片段的结束时间(秒)。 + llm_description: Set vocal end for this operation. +- name: description + type: string + required: false + form: form + label: + en_US: Description + zh_Hans: 描述 + human_description: + en_US: Set description for this operation. + zh_Hans: 对歌手风格的文字描述。 + llm_description: Set description for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_create_persona.py diff --git a/tools/suno_create_voice.py b/tools/suno_create_voice.py new file mode 100644 index 0000000..c9a8989 --- /dev/null +++ b/tools/suno_create_voice.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoCreateVoiceTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_create_voice", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_create_voice.yaml b/tools/suno_create_voice.yaml new file mode 100644 index 0000000..22b3d21 --- /dev/null +++ b/tools/suno_create_voice.yaml @@ -0,0 +1,68 @@ +identity: + name: suno_create_voice + author: acedatacloud + label: + en_US: Suno Create Voice + zh_Hans: Suno 创建音色 +description: + human: + en_US: Create Voice through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Create Voice。 + llm: Create Voice through the Ace Data Cloud Suno API. For asynchronous results, + preserve task_id and use the task retrieval tool until completion. A successful + submission is not completed output. +parameters: +- name: audio_url + type: string + required: true + form: form + label: + en_US: Audio Url + zh_Hans: 音频链接 + human_description: + en_US: Set audio url for this operation. + zh_Hans: 用于创建声音的音频文件的公开可访问 URL。必须为 MP3 或 WAV 格式,时长需在 10 至 240 秒之间(推荐 30 至 60 秒),且须包含单一说话人的清晰人声,不得含有背景噪音或背景音乐。带伴奏的完整歌曲通常无法通过声纹校验。 + llm_description: Set audio url for this operation. +- name: name + type: string + required: false + form: form + label: + en_US: Name + zh_Hans: 名称 + human_description: + en_US: Set name for this operation. + zh_Hans: 自定义声音人格的名称。 + llm_description: Set name for this operation. +- name: description + type: string + required: false + form: form + label: + en_US: Description + zh_Hans: 描述 + human_description: + en_US: Set description for this operation. + zh_Hans: 自定义声音人格的描述信息。 + llm_description: Set description for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_create_voice.py diff --git a/tools/suno_create_vox_audio.py b/tools/suno_create_vox_audio.py new file mode 100644 index 0000000..78590e9 --- /dev/null +++ b/tools/suno_create_vox_audio.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoCreateVoxAudioTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_create_vox_audio", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_create_vox_audio.yaml b/tools/suno_create_vox_audio.yaml new file mode 100644 index 0000000..a1f408c --- /dev/null +++ b/tools/suno_create_vox_audio.yaml @@ -0,0 +1,80 @@ +identity: + name: suno_create_vox_audio + author: acedatacloud + label: + en_US: Suno Create Vox Audio + zh_Hans: Suno 提取人声片段 +description: + human: + en_US: Create Vox Audio through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Create Vox Audio。 + llm: Create Vox Audio through the Ace Data Cloud Suno API. For asynchronous results, + preserve task_id and use the task retrieval tool until completion. A successful + submission is not completed output. +parameters: +- name: audio_id + type: string + required: true + form: form + label: + en_US: Audio Id + zh_Hans: 音频 ID + human_description: + en_US: Set audio id for this operation. + zh_Hans: 用于提取人声的源音频 ID,即待处理的 Suno 音频片段的唯一标识。 + llm_description: Set audio id for this operation. +- name: vocal_start + type: number + required: true + form: form + label: + en_US: Vocal Start + zh_Hans: 人声片段开始 + human_description: + en_US: Set vocal start for this operation. + zh_Hans: 必填。人声提取的起始时间点(单位:秒),必须大于或等于 0。 + llm_description: Set vocal start for this operation. + min: 0 +- name: vocal_end + type: number + required: true + form: form + label: + en_US: Vocal End + zh_Hans: 人声片段结束 + human_description: + en_US: Set vocal end for this operation. + zh_Hans: 必填。人声提取的结束时间点(单位:秒),必须大于起始时间,且提取区间必须短于 30 秒。 + llm_description: Set vocal end for this operation. +- name: callback_url + type: string + required: false + form: form + label: + en_US: Callback Url + zh_Hans: 回调 URL + human_description: + en_US: Set callback url for this operation. + zh_Hans: 用于接收处理结果的回调地址。若未指定,结果将直接在响应体中返回。 + llm_description: Set callback url for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_create_vox_audio.py diff --git a/tools/suno_custom_models.py b/tools/suno_custom_models.py new file mode 100644 index 0000000..2121428 --- /dev/null +++ b/tools/suno_custom_models.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoCustomModelsTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_custom_models", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_custom_models.yaml b/tools/suno_custom_models.yaml new file mode 100644 index 0000000..ce387d2 --- /dev/null +++ b/tools/suno_custom_models.yaml @@ -0,0 +1,319 @@ +identity: + name: suno_custom_models + author: acedatacloud + label: + en_US: Suno Custom Models + zh_Hans: Suno 自定义音乐模型 +description: + human: + en_US: Custom Models through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Custom Models。 + llm: Custom Models through the Ace Data Cloud Suno API. For asynchronous results, + preserve task_id and use the task retrieval tool until completion. A successful + submission is not completed output. +parameters: +- name: action + type: select + required: false + form: form + label: + en_US: Action + zh_Hans: 操作 + human_description: + en_US: Set action for this operation. + zh_Hans: 创建成功消耗 10 Credits;创建失败不扣费。 + llm_description: Set action for this operation. + default: retrieve_batch + options: + - value: create + label: + en_US: create + zh_Hans: create + - value: generate + label: + en_US: generate + zh_Hans: generate + - value: retrieve + label: + en_US: retrieve + zh_Hans: retrieve + - value: retrieve_batch + label: + en_US: retrieve_batch + zh_Hans: retrieve_batch + - value: delete + label: + en_US: delete + zh_Hans: delete +- name: name + type: string + required: false + form: form + label: + en_US: Name + zh_Hans: 名称 + human_description: + en_US: Set name for this operation. + zh_Hans: 模型名称,最长 100 个字符。 + llm_description: Set name for this operation. +- name: audio_urls + type: string + required: false + form: form + label: + en_US: Audio Urls + zh_Hans: 参考音频链接 + human_description: + en_US: Set audio urls for this operation. Enter a JSON array or object where applicable. + zh_Hans: 6 至 24 个互不重复、可公开访问的 HTTPS 音频 URL。音频必须已取得模型创建与生成授权。 数组或对象请使用 JSON 格式。 + llm_description: Set audio urls for this operation. Enter a JSON array or object + where applicable. +- name: callback_url + type: string + required: false + form: form + label: + en_US: Callback Url + zh_Hans: 回调 URL + human_description: + en_US: Set callback url for this operation. + zh_Hans: 可选 HTTPS 回调地址;模型就绪或创建失败后推送最终结果。 + llm_description: Set callback url for this operation. +- name: id + type: string + required: false + form: form + label: + en_US: Id + zh_Hans: ID + human_description: + en_US: Set id for this operation. + zh_Hans: 专属音乐模型的平台 ID。 + llm_description: Set id for this operation. +- name: lyric + type: string + required: false + form: llm + label: + en_US: Lyric + zh_Hans: 歌词 + human_description: + en_US: Set lyric for this operation. + zh_Hans: 自定义歌词。非纯音乐生成时需提供 `lyric` 或 `lyric_prompt`。 + llm_description: Set lyric for this operation. +- name: lyric_prompt + type: string + required: false + form: form + label: + en_US: Lyric Prompt + zh_Hans: 歌词提示词 + human_description: + en_US: Set lyric prompt for this operation. + zh_Hans: 歌词创作提示词。非纯音乐生成时可与 `lyric` 二选一。 + llm_description: Set lyric prompt for this operation. +- name: style + type: string + required: false + form: form + label: + en_US: Style + zh_Hans: 风格 + human_description: + en_US: Set style for this operation. + zh_Hans: 期望的音乐风格。 + llm_description: Set style for this operation. +- name: negative_tags + type: string + required: false + form: form + label: + en_US: Negative Tags + zh_Hans: 排除风格 + human_description: + en_US: Set negative tags for this operation. + zh_Hans: 希望避免的音乐风格。 + llm_description: Set negative tags for this operation. +- name: title + type: string + required: false + form: form + label: + en_US: Title + zh_Hans: 标题 + human_description: + en_US: Set title for this operation. + zh_Hans: 歌曲标题。 + llm_description: Set title for this operation. +- name: instrumental + type: boolean + required: false + form: form + label: + en_US: Instrumental + zh_Hans: 纯音乐 + human_description: + en_US: Set instrumental for this operation. + zh_Hans: 是否生成纯音乐。 + llm_description: Set instrumental for this operation. +- name: vocal_gender + type: select + required: false + form: form + label: + en_US: Vocal Gender + zh_Hans: 人声性别 + human_description: + en_US: Set vocal gender for this operation. + zh_Hans: 设置人声性别。 + llm_description: Set vocal gender for this operation. + options: + - value: f + label: + en_US: f + zh_Hans: f + - value: m + label: + en_US: m + zh_Hans: m +- name: weirdness + type: number + required: false + form: form + label: + en_US: Weirdness + zh_Hans: 创意程度 + human_description: + en_US: Set weirdness for this operation. + zh_Hans: 设置创意程度。 + llm_description: Set weirdness for this operation. + min: 0 + max: 1 +- name: style_influence + type: number + required: false + form: form + label: + en_US: Style Influence + zh_Hans: 风格影响 + human_description: + en_US: Set style influence for this operation. + zh_Hans: 设置风格影响。 + llm_description: Set style influence for this operation. + min: 0 + max: 1 +- name: duration + type: number + required: false + form: form + label: + en_US: Duration + zh_Hans: 时长(秒) + human_description: + en_US: Set duration for this operation. + zh_Hans: 设置时长(秒)。 + llm_description: Set duration for this operation. +- name: limit + type: number + required: false + form: form + label: + en_US: Limit + zh_Hans: 返回数量 + human_description: + en_US: Set limit for this operation. + zh_Hans: 设置返回数量。 + llm_description: Set limit for this operation. + min: 1 + max: 100 +- name: offset + type: number + required: false + form: form + label: + en_US: Offset + zh_Hans: 偏移量 + human_description: + en_US: Set offset for this operation. + zh_Hans: 设置偏移量。 + llm_description: Set offset for this operation. + min: 0 +- name: status + type: select + required: false + form: form + label: + en_US: Status + zh_Hans: 状态 + human_description: + en_US: Set status for this operation. + zh_Hans: 设置状态。 + llm_description: Set status for this operation. + options: + - value: queued + label: + en_US: queued + zh_Hans: queued + - value: uploading + label: + en_US: uploading + zh_Hans: uploading + - value: training + label: + en_US: training + zh_Hans: training + - value: ready + label: + en_US: ready + zh_Hans: ready + - value: failed + label: + en_US: failed + zh_Hans: failed + - value: archived + label: + en_US: archived + zh_Hans: archived +- name: idempotency_key + type: string + required: false + form: form + label: + en_US: Idempotency Key + zh_Hans: 幂等键 + human_description: + en_US: Set idempotency key for this operation. + zh_Hans: 设置幂等键。 + llm_description: Set idempotency key for this operation. +- name: confirm + type: boolean + required: false + form: form + label: + en_US: Confirm + zh_Hans: 确认删除或归档 + human_description: + en_US: Set true only to confirm the selected deletion or archive operation. + zh_Hans: 仅在确认执行删除或归档操作时设为 true。 + llm_description: Set true only to confirm the selected deletion or archive operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_custom_models.py diff --git a/tools/suno_delete_persona.py b/tools/suno_delete_persona.py new file mode 100644 index 0000000..2495b4c --- /dev/null +++ b/tools/suno_delete_persona.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoDeletePersonaTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_delete_persona", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_delete_persona.yaml b/tools/suno_delete_persona.yaml new file mode 100644 index 0000000..4047f42 --- /dev/null +++ b/tools/suno_delete_persona.yaml @@ -0,0 +1,68 @@ +identity: + name: suno_delete_persona + author: acedatacloud + label: + en_US: Suno Delete Persona + zh_Hans: Suno 删除 Persona +description: + human: + en_US: Delete Persona through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Delete Persona。 + llm: Delete Persona through the Ace Data Cloud Suno API. For asynchronous results, + preserve task_id and use the task retrieval tool until completion. A successful + submission is not completed output. +parameters: +- name: persona_id + type: string + required: true + form: form + label: + en_US: Persona Id + zh_Hans: Persona ID + human_description: + en_US: Set persona id for this operation. + zh_Hans: 设置Persona ID。 + llm_description: Set persona id for this operation. +- name: user_id + type: string + required: false + form: form + label: + en_US: User Id + zh_Hans: 用户 ID + human_description: + en_US: Set user id for this operation. + zh_Hans: 设置用户 ID。 + llm_description: Set user id for this operation. +- name: confirm + type: boolean + required: false + form: form + label: + en_US: Confirm + zh_Hans: 确认删除或归档 + human_description: + en_US: Set true only to confirm the selected deletion or archive operation. + zh_Hans: 仅在确认执行删除或归档操作时设为 true。 + llm_description: Set true only to confirm the selected deletion or archive operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_delete_persona.py diff --git a/tools/suno_enhance_style.py b/tools/suno_enhance_style.py new file mode 100644 index 0000000..d93fb71 --- /dev/null +++ b/tools/suno_enhance_style.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoEnhanceStyleTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_enhance_style", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_enhance_style.yaml b/tools/suno_enhance_style.yaml new file mode 100644 index 0000000..e8a32bc --- /dev/null +++ b/tools/suno_enhance_style.yaml @@ -0,0 +1,46 @@ +identity: + name: suno_enhance_style + author: acedatacloud + label: + en_US: Suno Enhance Style + zh_Hans: Suno 优化风格 +description: + human: + en_US: Enhance Style through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Enhance Style。 + llm: Enhance Style through the Ace Data Cloud Suno API. For asynchronous results, + preserve task_id and use the task retrieval tool until completion. A successful + submission is not completed output. +parameters: +- name: prompt + type: string + required: true + form: llm + label: + en_US: Prompt + zh_Hans: 提示词 + human_description: + en_US: Set prompt for this operation. + zh_Hans: 需要进行优化的风格提示词。 + llm_description: Set prompt for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_enhance_style.py diff --git a/tools/suno_generate_audios.py b/tools/suno_generate_audios.py index 36ec1fd..165316b 100644 --- a/tools/suno_generate_audios.py +++ b/tools/suno_generate_audios.py @@ -11,17 +11,21 @@ class SunoGenerateAudiosTool(Tool): def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: - client = AceDataSunoClient( - bearer_token=self.runtime.credentials.get("acedata_bearer_token", "") + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_generate_audios", tool_parameters ) - result = client.execute("generate", tool_parameters) yield self.create_json_message(result) - for field in ["status", "task_id", "media_urls", "result"]: - yield self.create_variable_message(field, result[field]) - yield self.create_variable_message("success", result["status"] == "succeeded") - yield self.create_variable_message("trace_id", result["result"].get("trace_id") or "") - yield self.create_variable_message( - "data", result["result"].get("data", result["result"].get("content", {})) - ) + for name, value in result.items(): + yield self.create_variable_message(name, value) for url in result["media_urls"]: - yield self.create_link_message(url) + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_generate_audios.yaml b/tools/suno_generate_audios.yaml index fa9863d..c916f03 100644 --- a/tools/suno_generate_audios.yaml +++ b/tools/suno_generate_audios.yaml @@ -8,34 +8,35 @@ description: human: en_US: Generate songs with Ace Data Cloud Suno Audios API. zh_Hans: 通过 Ace Data Cloud Suno Audios API 生成歌曲。 - llm: Generate songs via Ace Data Cloud Suno Audios API. Returns a task_id and song - metadata (audio_url/image_url/lyric/etc). Returns a task ID; use the task retrieval - tool to obtain completed media. Never repeat a pending submission. + llm: Generate songs with Ace Data Cloud Suno Audios API. For asynchronous results, + preserve task_id and use the task retrieval tool until completion. A successful + submission is not completed output. parameters: -- name: prompt - label: - en_US: Prompt - zh_Hans: 提示词 +- name: lyric type: string required: false form: llm + label: + en_US: Lyric + zh_Hans: 歌词 human_description: - en_US: Music description - zh_Hans: 描述希望生成的歌曲;启用自定义模式后可留空。 - llm_description: Music description + en_US: Lyrics for custom mode; omit for instrumental music. + zh_Hans: 自定义模式(`custom` 为 `true`)下用于生成音乐的歌词。`chirp-v3-5` 与 `chirp-v4` 最多 3000 + 字符;`chirp-v4-5` 及以上(含 `chirp-v5`、`chirp-v5-5`)最多 5000 字符。 + llm_description: Lyrics for custom mode; omit for instrumental music. - name: model - label: - en_US: Model - zh_Hans: 模型 type: select required: false form: form + label: + en_US: Model + zh_Hans: 模型 human_description: - en_US: Select the model. Available models, access and pricing are shown in the - Ace Data Cloud console. - zh_Hans: 选择要调用的模型。模型权限和价格以 Ace Data Cloud 控制台为准。 - llm_description: Select the model. Available models, access and pricing are shown - in the Ace Data Cloud console. + en_US: Select a model currently supported by this API. Availability and pricing + depend on the chosen model. + zh_Hans: 用于生成音乐的模型。支持 `chirp-v6`、`chirp-v6-wild` 和 `chirp-v6-mini`;推荐使用 `chirp-v6`。旧模型名称继续兼容。 + llm_description: Select a model currently supported by this API. Availability and + pricing depend on the chosen model. default: chirp-v6 options: - value: chirp-v6 @@ -58,95 +59,709 @@ parameters: label: en_US: chirp-v5 zh_Hans: chirp-v5 -- name: custom + - value: chirp-v4-5-plus + label: + en_US: chirp-v4-5-plus + zh_Hans: chirp-v4-5-plus + - value: chirp-v4-5 + label: + en_US: chirp-v4-5 + zh_Hans: chirp-v4-5 + - value: chirp-v4 + label: + en_US: chirp-v4 + zh_Hans: chirp-v4 + - value: chirp-v3-5 + label: + en_US: chirp-v3-5 + zh_Hans: chirp-v3-5 + - value: chirp-v3-0 + label: + en_US: chirp-v3-0 + zh_Hans: chirp-v3-0 +- name: max_mode + type: boolean + required: false + form: form label: - en_US: Custom Mode - zh_Hans: 自定义模式 + en_US: Max Mode + zh_Hans: Max 模式 + human_description: + en_US: Set max mode for this operation. + zh_Hans: 是否启用增强生成模式。仅支持 `generate`(同时要求 `custom` 为 `true`)和 `add_stem`。开启后分别消耗 + 1.12 和 1.344 Credits;关闭或省略时按普通模式计费。其他 action 传入该参数会返回 400。 + llm_description: Set max mode for this operation. +- name: variety + type: select + required: false + form: form + label: + en_US: Variety + zh_Hans: 多样性 + human_description: + en_US: Set variety for this operation. + zh_Hans: 生成结果的多样性强度,可选 `off`、`normal`、`high`、`extra`、`max`。仅支持 `generate` 和 `add_stem`;其他 + action 传入会返回 400。 + llm_description: Set variety for this operation. + options: + - value: 'off' + label: + en_US: 'off' + zh_Hans: 'off' + - value: normal + label: + en_US: normal + zh_Hans: normal + - value: high + label: + en_US: high + zh_Hans: high + - value: extra + label: + en_US: extra + zh_Hans: extra + - value: max + label: + en_US: max + zh_Hans: max +- name: style + type: string + required: false + form: form + label: + en_US: Style + zh_Hans: 风格 + human_description: + en_US: Describe the genre, tempo and instruments. + zh_Hans: 音乐风格描述。`chirp-v3-5` 与 `chirp-v4` 最多 200 字符;`chirp-v4-5` 及以上(含 `chirp-v5`、`chirp-v5-5`)最多 + 1000 字符。 当 `action` 为 `add_stem` 时可选。 + llm_description: Describe the genre, tempo and instruments. +- name: variation_category + type: select + required: false + form: form + label: + en_US: Variation Category + zh_Hans: 变奏强度 + human_description: + en_US: Set variation category for this operation. + zh_Hans: 重制变化幅度,仅用于 `remaster`,可选 `high`、`normal`、`subtle`。 + llm_description: Set variation category for this operation. + options: + - value: high + label: + en_US: high + zh_Hans: high + - value: normal + label: + en_US: normal + zh_Hans: normal + - value: subtle + label: + en_US: subtle + zh_Hans: subtle +- name: title + type: string + required: false + form: form + label: + en_US: Title + zh_Hans: 标题 + human_description: + en_US: Title of the generated song. + zh_Hans: 音乐标题(自定义模式)。`chirp-v3-5`、`chirp-v4` 最多 80 字符;`chirp-v4-5` 及以上(含 `chirp-v5`、`chirp-v5-5`)最多 + 100 字符。 当 `action` 为 `add_stem` 时可选;当 `action` 为 `adjust_speed` 时必填。 + llm_description: Title of the generated song. +- name: action + type: select + required: false + form: form + label: + en_US: Action + zh_Hans: 操作 + human_description: + en_US: Set action for this operation. + zh_Hans: 生成音乐的操作类型。`generate`:根据提示词生成音频;`extend`:在已有音频基础上续生成;`concat`:将已有音频片段拼接为完整曲目;`cover`:复制已有曲目的音乐风格并重新演绎;`upload_cover`:对已上传的音频进行风格翻唱;`upload_extend`:将已上传的音频延伸续生成;`artist_consistency`:以指定歌手风格(Persona)演唱新歌曲;`artist_consistency_vox`:以 + VOX 模式使用指定歌手风格演唱新歌曲;`stems`:将歌曲分离为人声和伴奏两轨;`all_stems`:将歌曲分离为所有独立音轨(人声、鼓、贝斯、其他乐器);`add_stem`:为已有音频添加新音轨,必填 + `audio_id`、`stem_type`,可选 `prompt`、`title`、`style`;`extract_stem`:从已有音频中提取指定音轨,必填 + `audio_id`、`stem_type`,可选 `audio_format`;`sounds`:生成独立音效,必填 `sound`、`sound_type`,可选 + `bpm`、`key`;`adjust_speed`:调整已有音频的播放速度,必填 `audio_id`、`speed_multiplier`、`title`,`keep_pitch` + 可选;`replace_section`:替换指定时间段内的片段;`underpainting`:为已上传的人声轨道生成并添加 AI 伴奏;`overpainting`:为已上传的伴奏轨道生成并添加 + AI 人声;`samples`:从一段已有音频中选择起止区间进行采样创作;`remaster`:对已有音频进行重新母带处理以提升音质;`mashup`:将多首歌曲混合拼接为一首曲目;`inspo`:Inspo + 灵感创作功能,使用 1 到 4 段拖拽或上传的参考音频启发全新音乐创作。 + llm_description: Set action for this operation. + default: generate + options: + - value: generate + label: + en_US: generate + zh_Hans: generate + - value: extend + label: + en_US: extend + zh_Hans: extend + - value: upload_extend + label: + en_US: upload_extend + zh_Hans: upload_extend + - value: upload_cover + label: + en_US: upload_cover + zh_Hans: upload_cover + - value: concat + label: + en_US: concat + zh_Hans: concat + - value: cover + label: + en_US: cover + zh_Hans: cover + - value: artist_consistency + label: + en_US: artist_consistency + zh_Hans: artist_consistency + - value: artist_consistency_vox + label: + en_US: artist_consistency_vox + zh_Hans: artist_consistency_vox + - value: stems + label: + en_US: stems + zh_Hans: stems + - value: all_stems + label: + en_US: all_stems + zh_Hans: all_stems + - value: add_stem + label: + en_US: add_stem + zh_Hans: add_stem + - value: extract_stem + label: + en_US: extract_stem + zh_Hans: extract_stem + - value: sounds + label: + en_US: sounds + zh_Hans: sounds + - value: adjust_speed + label: + en_US: adjust_speed + zh_Hans: adjust_speed + - value: replace_section + label: + en_US: replace_section + zh_Hans: replace_section + - value: underpainting + label: + en_US: underpainting + zh_Hans: underpainting + - value: overpainting + label: + en_US: overpainting + zh_Hans: overpainting + - value: remaster + label: + en_US: remaster + zh_Hans: remaster + - value: mashup + label: + en_US: mashup + zh_Hans: mashup + - value: samples + label: + en_US: samples + zh_Hans: samples + - value: inspo + label: + en_US: inspo + zh_Hans: inspo +- name: custom type: boolean required: false form: form + label: + en_US: Custom Mode + zh_Hans: 自定义模式 human_description: en_US: Use style and custom lyrics when enabled; otherwise describe the song in the prompt. - zh_Hans: 启用后使用风格和自定义歌词;关闭时使用提示词描述歌曲。 + zh_Hans: 是否启用自定义模式的标志。若为 `true`,音频将根据歌词生成;否则将根据提示词生成。 llm_description: Use style and custom lyrics when enabled; otherwise describe the song in the prompt. default: false -- name: lyric - label: - en_US: Lyric - zh_Hans: 歌词 +- name: prompt type: string required: false form: llm + label: + en_US: Prompt + zh_Hans: 提示词 human_description: - en_US: Lyrics for custom mode; omit for instrumental music. - zh_Hans: 自定义模式下的歌词;纯音乐模式可留空。 - llm_description: Lyrics for custom mode; omit for instrumental music. -- name: title + en_US: Music description + zh_Hans: 灵感模式(`custom` 为 `false` 时生效)下用于生成音乐的提示词,长度不得超过 500 个字符。自定义模式请改用 `lyric` + 与 `style`。 当 `action` 为 `add_stem` 时,此字段可选,用于描述要添加的音轨。 + llm_description: Music description +- name: lyric_prompt + type: string + required: false + form: form label: - en_US: Title - zh_Hans: 标题 + en_US: Lyric Prompt + zh_Hans: 歌词提示词 + human_description: + en_US: Set lyric prompt for this operation. + zh_Hans: 用于自动生成歌词的提示词,仅在 `custom` 为 `true` 且 `lyric` 为空时生效 + llm_description: Set lyric prompt for this operation. +- name: audio_id type: string required: false - form: llm + form: form + label: + en_US: Audio Id + zh_Hans: 音频 ID human_description: - en_US: Title of the generated song. - zh_Hans: 生成歌曲的标题。 - llm_description: Title of the generated song. -- name: style + en_US: Set audio id for this operation. + zh_Hans: 已有音频的 ID。当 `action` 为 `extend`、`concat`、`add_stem`、`extract_stem` 或 `adjust_speed` + 时,此字段必填。 + llm_description: Set audio id for this operation. +- name: stem_type + type: select + required: false + form: form label: - en_US: Style - zh_Hans: 风格 + en_US: Stem Type + zh_Hans: 音轨类型 + human_description: + en_US: Set stem type for this operation. + zh_Hans: 音轨类型。当 `action` 为 `add_stem` 或 `extract_stem` 时必填;支持 `vocals`、`backing_vocals`、`drums`、`bass`、`guitar`、`keyboard`、`percussion`、`strings`、`synth`、`fx`、`brass`、`woodwinds`。 + llm_description: Set stem type for this operation. + options: + - value: vocals + label: + en_US: vocals + zh_Hans: vocals + - value: backing_vocals + label: + en_US: backing_vocals + zh_Hans: backing_vocals + - value: drums + label: + en_US: drums + zh_Hans: drums + - value: bass + label: + en_US: bass + zh_Hans: bass + - value: guitar + label: + en_US: guitar + zh_Hans: guitar + - value: keyboard + label: + en_US: keyboard + zh_Hans: keyboard + - value: percussion + label: + en_US: percussion + zh_Hans: percussion + - value: strings + label: + en_US: strings + zh_Hans: strings + - value: synth + label: + en_US: synth + zh_Hans: synth + - value: fx + label: + en_US: fx + zh_Hans: fx + - value: brass + label: + en_US: brass + zh_Hans: brass + - value: woodwinds + label: + en_US: woodwinds + zh_Hans: woodwinds +- name: audio_format + type: select + required: false + form: form + label: + en_US: Audio Format + zh_Hans: 音频格式 + human_description: + en_US: Set audio format for this operation. + zh_Hans: 输出音频格式,仅在 `action` 为 `extract_stem` 或 `sounds` 时可选,当前支持 `mp3`。 + llm_description: Set audio format for this operation. + options: + - value: mp3 + label: + en_US: mp3 + zh_Hans: mp3 +- name: sound type: string required: false - form: llm + form: form + label: + en_US: Sound + zh_Hans: 声音描述 human_description: - en_US: Describe the genre, tempo and instruments. - zh_Hans: 歌曲风格,例如曲风、节奏和乐器。 - llm_description: Describe the genre, tempo and instruments. -- name: instrumental + en_US: Set sound for this operation. + zh_Hans: 要生成的音效描述,当 `action` 为 `sounds` 时必填。 + llm_description: Set sound for this operation. +- name: sound_type + type: select + required: false + form: form label: - en_US: Instrumental - zh_Hans: 纯音乐 + en_US: Sound Type + zh_Hans: 声音类型 + human_description: + en_US: Set sound type for this operation. + zh_Hans: 音效类型,当 `action` 为 `sounds` 时必填。`one-shot` 表示单次音效,`loop` 表示可循环音效。 + llm_description: Set sound type for this operation. + options: + - value: one-shot + label: + en_US: one-shot + zh_Hans: one-shot + - value: loop + label: + en_US: loop + zh_Hans: loop +- name: bpm + type: number + required: false + form: form + label: + en_US: Bpm + zh_Hans: 每分钟节拍 + human_description: + en_US: Set bpm for this operation. + zh_Hans: 音效目标速度(每分钟节拍数),仅在 `action` 为 `sounds` 时可选,需为 1 到 300 的整数。 + llm_description: Set bpm for this operation. + min: 1 + max: 300 +- name: key + type: string + required: false + form: form + label: + en_US: Key + zh_Hans: 调性 + human_description: + en_US: Set key for this operation. + zh_Hans: 音效的目标调性,仅在 `action` 为 `sounds` 时可选。 + llm_description: Set key for this operation. +- name: speed_multiplier + type: number + required: false + form: form + label: + en_US: Speed Multiplier + zh_Hans: 速度倍率 + human_description: + en_US: Set speed multiplier for this operation. + zh_Hans: 播放速度倍数,当 `action` 为 `adjust_speed` 时必填,范围为 0.25 到 4。 + llm_description: Set speed multiplier for this operation. + min: 0.25 + max: 4 +- name: keep_pitch type: boolean required: false form: form + label: + en_US: Keep Pitch + zh_Hans: 保持音高 + human_description: + en_US: Set keep pitch for this operation. + zh_Hans: 调整速度时是否保持原音高,默认为 `false`,仅用于 `adjust_speed`。 + llm_description: Set keep pitch for this operation. +- name: mashup_audio_ids + type: string + required: false + form: form + label: + en_US: Mashup Audio Ids + zh_Hans: 混音音频 ID 列表 + human_description: + en_US: Set mashup audio ids for this operation. Enter a JSON array or object where + applicable. + zh_Hans: 用于混音混搭的音频 ID 列表。当 `action` 为 `mashup` 时,此字段必填。 数组或对象请使用 JSON 格式。 + llm_description: Set mashup audio ids for this operation. Enter a JSON array or + object where applicable. +- name: audio_urls + type: string + required: false + form: form + label: + en_US: Audio Urls + zh_Hans: 参考音频链接 + human_description: + en_US: Set audio urls for this operation. Enter a JSON array or object where applicable. + zh_Hans: 用于 Inspo 灵感创作功能的参考音频 URL 列表,需提供 1 到 4 个公开可访问的 HTTP(S) 音频地址。当 `action` + 为 `inspo` 时,此字段必填。 数组或对象请使用 JSON 格式。 + llm_description: Set audio urls for this operation. Enter a JSON array or object + where applicable. +- name: weirdness + type: number + required: false + form: form + label: + en_US: Weirdness + zh_Hans: 创意程度 + human_description: + en_US: Set weirdness for this operation. + zh_Hans: 怪异度,范围 0 到 1。值越高,结果通常越具实验性;0 会作为有效值传递。具体效果取决于操作类型、模式和模型。 + llm_description: Set weirdness for this operation. + min: 0 + max: 1 +- name: persona_id + type: string + required: false + form: form + label: + en_US: Persona Id + zh_Hans: Persona ID + human_description: + en_US: Set persona id for this operation. + zh_Hans: 根据指定歌手的独特风格特征生成歌曲时使用的歌手 Persona ID。 + llm_description: Set persona id for this operation. +- name: overpainting_start + type: number + required: false + form: form + label: + en_US: Overpainting Start + zh_Hans: 重绘开始时间 + human_description: + en_US: Set overpainting start for this operation. + zh_Hans: 为已上传音频添加 AI 人声的默认起始时间,默认值为 0。 + llm_description: Set overpainting start for this operation. +- name: overpainting_end + type: number + required: false + form: form + label: + en_US: Overpainting End + zh_Hans: 重绘结束时间 + human_description: + en_US: Set overpainting end for this operation. + zh_Hans: 为已上传音频添加 AI 人声的结束时间,必须小于歌曲的总时长。 + llm_description: Set overpainting end for this operation. +- name: samples_start + type: number + required: false + form: form + label: + en_US: Samples Start + zh_Hans: 采样开始时间 + human_description: + en_US: Set samples start for this operation. + zh_Hans: 为已上传音频添加采样的默认起始时间,默认值为 0。 + llm_description: Set samples start for this operation. +- name: samples_end + type: number + required: false + form: form + label: + en_US: Samples End + zh_Hans: 采样结束时间 + human_description: + en_US: Set samples end for this operation. + zh_Hans: 为已上传音频添加采样的结束时间,必须小于歌曲的总时长。 + llm_description: Set samples end for this operation. +- name: underpainting_start + type: number + required: false + form: form + label: + en_US: Underpainting Start + zh_Hans: 补写开始时间 + human_description: + en_US: Set underpainting start for this operation. + zh_Hans: 为已上传音频添加 AI 伴奏的默认起始时间,默认值为 0。 + llm_description: Set underpainting start for this operation. +- name: underpainting_end + type: number + required: false + form: form + label: + en_US: Underpainting End + zh_Hans: 补写结束时间 + human_description: + en_US: Set underpainting end for this operation. + zh_Hans: 为已上传音频添加 AI 伴奏的结束时间,必须小于歌曲的总时长。 + llm_description: Set underpainting end for this operation. +- name: continue_at + type: number + required: false + form: form + label: + en_US: Continue At + zh_Hans: 续写开始时间 + human_description: + en_US: Set continue at for this operation. + zh_Hans: 续写边界,单位为秒。例如,213.5 表示从第 3 分钟 33.5 秒处开始生成后续片段。`lyric` 和 `style` 只引导该边界之后的新内容,不会替换源音频中边界之前的歌词或演唱;若要让整首歌曲使用新歌词,请使用 + `generate` 重新生成,或使用 `cover` 重新演绎。 + llm_description: Set continue at for this operation. +- name: callback_url + type: string + required: false + form: form + label: + en_US: Callback Url + zh_Hans: 回调 URL + human_description: + en_US: Set callback url for this operation. + zh_Hans: 音频生成完成后将被调用的回调 URL + llm_description: Set callback url for this operation. +- name: instrumental + type: boolean + required: false + form: form + label: + en_US: Instrumental + zh_Hans: 纯音乐 human_description: en_US: Generate instrumental music without vocals. - zh_Hans: 仅生成伴奏,不包含人声。 + zh_Hans: 纯伴奏模式(无歌词),默认为 `false`。设为 `true` 时,上方填写的歌词将被忽略。 llm_description: Generate instrumental music without vocals. default: false +- name: vocal_gender + type: select + required: false + form: form + label: + en_US: Vocal Gender + zh_Hans: 人声性别 + human_description: + en_US: Set vocal gender for this operation. + zh_Hans: 人声性别偏好,可选 `m`(男声)或 `f`(女声)。这是生成倾向,不保证严格遵循。 + llm_description: Set vocal gender for this operation. + options: + - value: f + label: + en_US: f + zh_Hans: f + - value: m + label: + en_US: m + zh_Hans: m +- name: negative_tags + type: string + required: false + form: form + label: + en_US: Negative Tags + zh_Hans: 排除风格 + human_description: + en_US: Set negative tags for this operation. + zh_Hans: 不希望在生成音乐中出现的风格或流派描述,仅在自定义模式(`custom` 为 `true`)下生效 + llm_description: Set negative tags for this operation. +- name: style_influence + type: number + required: false + form: form + label: + en_US: Style Influence + zh_Hans: 风格影响 + human_description: + en_US: Set style influence for this operation. + zh_Hans: 风格影响力,范围 0 到 1。值越高,结果通常越贴近 `style`;0 会作为有效值传递。具体效果取决于操作类型、模式和模型。 + llm_description: Set style influence for this operation. + min: 0 + max: 1 +- name: audio_weight + type: number + required: false + form: form + label: + en_US: Audio Weight + zh_Hans: 音频权重 + human_description: + en_US: Set audio weight for this operation. + zh_Hans: 音频或声音特征的影响权重,范围 0 到 1,0 也是有效值。可用于生成、续写、翻唱、添加音轨和声音一致性等操作;没有参考音频或声音特征的场景中,模型可能弱化或忽略该值。 + llm_description: Set audio weight for this operation. + min: 0 + max: 1 - name: duration + type: number + required: false + form: form label: en_US: Duration zh_Hans: 时长 + human_description: + en_US: 10–360 seconds; a target rather than a guaranteed output length. + zh_Hans: 期望歌曲时长(秒),必须是 10 到 360 的整数。平台会传递该值,具体操作、模式或模型可能弱化或忽略它。这是生成目标而非硬性成品长度,实际时长以响应为准。 + llm_description: 10–360 seconds; a target rather than a guaranteed output length. + min: 10 + max: 360 +- name: replace_section_end type: number required: false form: form + label: + en_US: Replace Section End + zh_Hans: 替换片段结束 human_description: - en_US: 10–360 seconds; a target rather than a guaranteed output length. - zh_Hans: 目标时长为 10–360 秒,实际音频长度可能不同。 - llm_description: 10–360 seconds; a target rather than a guaranteed output length. - default: 30 + en_US: Set replace section end for this operation. + zh_Hans: 当 `action` 为 `replace_section` 时,指定待替换片段的结束时间(秒)。 + llm_description: Set replace section end for this operation. +- name: replace_section_result_mode + type: select + required: false + form: form + label: + en_US: Replace Section Result Mode + zh_Hans: 替换片段输出方式 + human_description: + en_US: Set replace section result mode for this operation. + zh_Hans: 当 `action` 为 `replace_section` 时,指定返回形态。`full_song`(默认)会分别拼接 2 个候选并返回 + 2 首完整歌曲;`candidates` 返回 2 个尚未拼接的替换片段候选,需要选取候选后再调用 `concat`。 + llm_description: Set replace section result mode for this operation. + options: + - value: candidates + label: + en_US: candidates + zh_Hans: candidates + - value: full_song + label: + en_US: full_song + zh_Hans: full_song +- name: replace_section_start + type: number + required: false + form: form + label: + en_US: Replace Section Start + zh_Hans: 替换片段开始 + human_description: + en_US: Set replace section start for this operation. + zh_Hans: 当 `action` 为 `replace_section` 时,指定待替换片段的起始时间(秒)。 + llm_description: Set replace section start for this operation. output_schema: type: object properties: status: type: string + success: + type: boolean task_id: type: string + trace_id: + type: string media_urls: type: array items: type: string + data: {} result: type: object - success: - type: boolean - trace_id: - type: string - data: {} extra: python: source: tools/suno_generate_audios.py diff --git a/tools/suno_generate_lyrics.py b/tools/suno_generate_lyrics.py new file mode 100644 index 0000000..4792541 --- /dev/null +++ b/tools/suno_generate_lyrics.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoGenerateLyricsTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_generate_lyrics", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_generate_lyrics.yaml b/tools/suno_generate_lyrics.yaml new file mode 100644 index 0000000..e30fc7a --- /dev/null +++ b/tools/suno_generate_lyrics.yaml @@ -0,0 +1,69 @@ +identity: + name: suno_generate_lyrics + author: acedatacloud + label: + en_US: Suno Generate Lyrics + zh_Hans: Suno 生成歌词 +description: + human: + en_US: Generate Lyrics through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Generate Lyrics。 + llm: Generate Lyrics through the Ace Data Cloud Suno API. For asynchronous results, + preserve task_id and use the task retrieval tool until completion. A successful + submission is not completed output. +parameters: +- name: model + type: select + required: false + form: form + label: + en_US: Model + zh_Hans: 模型 + human_description: + en_US: Select a model currently supported by this API. Availability and pricing + depend on the chosen model. + zh_Hans: 用于生成歌词的模型,默认值为 `default`,可选值包括 `default` 和 `remi-v1`。 + llm_description: Select a model currently supported by this API. Availability and + pricing depend on the chosen model. + default: default + options: + - value: default + label: + en_US: default + zh_Hans: default + - value: remi-v1 + label: + en_US: remi-v1 + zh_Hans: remi-v1 +- name: prompt + type: string + required: true + form: llm + label: + en_US: Prompt + zh_Hans: 提示词 + human_description: + en_US: Set prompt for this operation. + zh_Hans: 用于生成歌词的提示词,描述期望的歌词主题或风格。 + llm_description: Set prompt for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_generate_lyrics.py diff --git a/tools/suno_get_midi.py b/tools/suno_get_midi.py new file mode 100644 index 0000000..5b0ace3 --- /dev/null +++ b/tools/suno_get_midi.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoGetMidiTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_get_midi", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_get_midi.yaml b/tools/suno_get_midi.yaml new file mode 100644 index 0000000..e167e8a --- /dev/null +++ b/tools/suno_get_midi.yaml @@ -0,0 +1,57 @@ +identity: + name: suno_get_midi + author: acedatacloud + label: + en_US: Suno Get Midi + zh_Hans: Suno 获取 MIDI +description: + human: + en_US: Get Midi through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Get Midi。 + llm: Get Midi through the Ace Data Cloud Suno API. For asynchronous results, preserve + task_id and use the task retrieval tool until completion. A successful submission + is not completed output. +parameters: +- name: audio_id + type: string + required: true + form: form + label: + en_US: Audio Id + zh_Hans: 音频 ID + human_description: + en_US: Set audio id for this operation. + zh_Hans: 用于生成 MIDI 的源音频 ID,将基于该已有音频提取 MIDI 内容。 + llm_description: Set audio id for this operation. +- name: callback_url + type: string + required: false + form: form + label: + en_US: Callback Url + zh_Hans: 回调 URL + human_description: + en_US: Set callback url for this operation. + zh_Hans: 用于接收处理结果的回调地址。若未指定,结果将直接在响应体中返回。 + llm_description: Set callback url for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_get_midi.py diff --git a/tools/suno_get_mp3.py b/tools/suno_get_mp3.py new file mode 100644 index 0000000..b87879f --- /dev/null +++ b/tools/suno_get_mp3.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoGetMp3Tool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_get_mp3", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_get_mp3.yaml b/tools/suno_get_mp3.yaml new file mode 100644 index 0000000..8ebe91b --- /dev/null +++ b/tools/suno_get_mp3.yaml @@ -0,0 +1,57 @@ +identity: + name: suno_get_mp3 + author: acedatacloud + label: + en_US: Suno Get Mp3 + zh_Hans: Suno 获取 MP3 +description: + human: + en_US: Get Mp3 through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Get Mp3。 + llm: Get Mp3 through the Ace Data Cloud Suno API. For asynchronous results, preserve + task_id and use the task retrieval tool until completion. A successful submission + is not completed output. +parameters: +- name: audio_id + type: string + required: true + form: form + label: + en_US: Audio Id + zh_Hans: 音频 ID + human_description: + en_US: Set audio id for this operation. + zh_Hans: 用于获取 MP3 URL 的已有音频 ID。 + llm_description: Set audio id for this operation. +- name: callback_url + type: string + required: false + form: form + label: + en_US: Callback Url + zh_Hans: 回调 URL + human_description: + en_US: Set callback url for this operation. + zh_Hans: 用于接收处理结果的回调 URL。若未指定,结果将直接在响应体中返回。 + llm_description: Set callback url for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_get_mp3.py diff --git a/tools/suno_get_mp4.py b/tools/suno_get_mp4.py new file mode 100644 index 0000000..f2df7a2 --- /dev/null +++ b/tools/suno_get_mp4.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoGetMp4Tool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_get_mp4", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_get_mp4.yaml b/tools/suno_get_mp4.yaml new file mode 100644 index 0000000..b42995b --- /dev/null +++ b/tools/suno_get_mp4.yaml @@ -0,0 +1,46 @@ +identity: + name: suno_get_mp4 + author: acedatacloud + label: + en_US: Suno Get Mp4 + zh_Hans: Suno 获取 MP4 +description: + human: + en_US: Get Mp4 through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Get Mp4。 + llm: Get Mp4 through the Ace Data Cloud Suno API. For asynchronous results, preserve + task_id and use the task retrieval tool until completion. A successful submission + is not completed output. +parameters: +- name: audio_id + type: string + required: true + form: form + label: + en_US: Audio Id + zh_Hans: 音频 ID + human_description: + en_US: Set audio id for this operation. + zh_Hans: 用于获取对应歌曲 MP4 的歌曲 ID。 + llm_description: Set audio id for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_get_mp4.py diff --git a/tools/suno_get_timing.py b/tools/suno_get_timing.py new file mode 100644 index 0000000..db269be --- /dev/null +++ b/tools/suno_get_timing.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoGetTimingTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_get_timing", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_get_timing.yaml b/tools/suno_get_timing.yaml new file mode 100644 index 0000000..6f10cc3 --- /dev/null +++ b/tools/suno_get_timing.yaml @@ -0,0 +1,46 @@ +identity: + name: suno_get_timing + author: acedatacloud + label: + en_US: Suno Get Timing + zh_Hans: Suno 获取时间轴 +description: + human: + en_US: Get Timing through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Get Timing。 + llm: Get Timing through the Ace Data Cloud Suno API. For asynchronous results, preserve + task_id and use the task retrieval tool until completion. A successful submission + is not completed output. +parameters: +- name: audio_id + type: string + required: true + form: form + label: + en_US: Audio Id + zh_Hans: 音频 ID + human_description: + en_US: Set audio id for this operation. + zh_Hans: 需要获取计时/字幕数据的音频 ID,即已生成的 Suno 歌曲 ID。 + llm_description: Set audio id for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_get_timing.py diff --git a/tools/suno_get_wav.py b/tools/suno_get_wav.py new file mode 100644 index 0000000..ded0e94 --- /dev/null +++ b/tools/suno_get_wav.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoGetWavTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_get_wav", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_get_wav.yaml b/tools/suno_get_wav.yaml new file mode 100644 index 0000000..aa41d4e --- /dev/null +++ b/tools/suno_get_wav.yaml @@ -0,0 +1,57 @@ +identity: + name: suno_get_wav + author: acedatacloud + label: + en_US: Suno Get Wav + zh_Hans: Suno 获取 WAV +description: + human: + en_US: Get Wav through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Get Wav。 + llm: Get Wav through the Ace Data Cloud Suno API. For asynchronous results, preserve + task_id and use the task retrieval tool until completion. A successful submission + is not completed output. +parameters: +- name: audio_id + type: string + required: true + form: form + label: + en_US: Audio Id + zh_Hans: 音频 ID + human_description: + en_US: Set audio id for this operation. + zh_Hans: 用于获取 WAV 格式音频的已有音频 ID。 + llm_description: Set audio id for this operation. +- name: callback_url + type: string + required: false + form: form + label: + en_US: Callback Url + zh_Hans: 回调 URL + human_description: + en_US: Set callback url for this operation. + zh_Hans: 用于接收处理结果的回调 URL。若未指定,结果将直接在响应体中返回。 + llm_description: Set callback url for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_get_wav.py diff --git a/tools/suno_list_personas.py b/tools/suno_list_personas.py new file mode 100644 index 0000000..7c654ab --- /dev/null +++ b/tools/suno_list_personas.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoListPersonasTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_list_personas", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_list_personas.yaml b/tools/suno_list_personas.yaml new file mode 100644 index 0000000..efc924d --- /dev/null +++ b/tools/suno_list_personas.yaml @@ -0,0 +1,68 @@ +identity: + name: suno_list_personas + author: acedatacloud + label: + en_US: Suno List Personas + zh_Hans: Suno 列出 Persona +description: + human: + en_US: List Personas through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 List Personas。 + llm: List Personas through the Ace Data Cloud Suno API. For asynchronous results, + preserve task_id and use the task retrieval tool until completion. A successful + submission is not completed output. +parameters: +- name: user_id + type: string + required: true + form: form + label: + en_US: User Id + zh_Hans: 用户 ID + human_description: + en_US: Set user id for this operation. + zh_Hans: 设置用户 ID。 + llm_description: Set user id for this operation. +- name: limit + type: number + required: false + form: form + label: + en_US: Limit + zh_Hans: 返回数量 + human_description: + en_US: Set limit for this operation. + zh_Hans: 设置返回数量。 + llm_description: Set limit for this operation. +- name: offset + type: number + required: false + form: form + label: + en_US: Offset + zh_Hans: 偏移量 + human_description: + en_US: Set offset for this operation. + zh_Hans: 设置偏移量。 + llm_description: Set offset for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_list_personas.py diff --git a/tools/suno_mashup_lyrics.py b/tools/suno_mashup_lyrics.py new file mode 100644 index 0000000..18e5a96 --- /dev/null +++ b/tools/suno_mashup_lyrics.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoMashupLyricsTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_mashup_lyrics", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_mashup_lyrics.yaml b/tools/suno_mashup_lyrics.yaml new file mode 100644 index 0000000..2900ae2 --- /dev/null +++ b/tools/suno_mashup_lyrics.yaml @@ -0,0 +1,57 @@ +identity: + name: suno_mashup_lyrics + author: acedatacloud + label: + en_US: Suno Mashup Lyrics + zh_Hans: Suno 混合歌词 +description: + human: + en_US: Mashup Lyrics through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Mashup Lyrics。 + llm: Mashup Lyrics through the Ace Data Cloud Suno API. For asynchronous results, + preserve task_id and use the task retrieval tool until completion. A successful + submission is not completed output. +parameters: +- name: lyrics_a + type: string + required: true + form: form + label: + en_US: Lyrics A + zh_Hans: 第一组歌词 + human_description: + en_US: Set lyrics a for this operation. + zh_Hans: 用于混合生成歌词的第一段歌词内容。 + llm_description: Set lyrics a for this operation. +- name: lyrics_b + type: string + required: true + form: form + label: + en_US: Lyrics B + zh_Hans: 第二组歌词 + human_description: + en_US: Set lyrics b for this operation. + zh_Hans: 用于混合生成歌词的第二段歌词内容。 + llm_description: Set lyrics b for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_mashup_lyrics.py diff --git a/tools/suno_projects.py b/tools/suno_projects.py new file mode 100644 index 0000000..9b7933f --- /dev/null +++ b/tools/suno_projects.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoProjectsTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_projects", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_projects.yaml b/tools/suno_projects.yaml new file mode 100644 index 0000000..7ecc000 --- /dev/null +++ b/tools/suno_projects.yaml @@ -0,0 +1,430 @@ +identity: + name: suno_projects + author: acedatacloud + label: + en_US: Suno Projects + zh_Hans: Suno 音乐项目 +description: + human: + en_US: Projects through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Projects。 + llm: Projects through the Ace Data Cloud Suno API. For asynchronous results, preserve + task_id and use the task retrieval tool until completion. A successful submission + is not completed output. +parameters: +- name: action + type: select + required: false + form: form + label: + en_US: Action + zh_Hans: 操作 + human_description: + en_US: Set action for this operation. + zh_Hans: 设置操作。 + llm_description: Set action for this operation. + default: retrieve + options: + - value: create + label: + en_US: create + zh_Hans: create + - value: retrieve + label: + en_US: retrieve + zh_Hans: retrieve + - value: save + label: + en_US: save + zh_Hans: save + - value: upload + label: + en_US: upload + zh_Hans: upload + - value: add_track + label: + en_US: add_track + zh_Hans: add_track + - value: generate_track + label: + en_US: generate_track + zh_Hans: generate_track + - value: replace_section + label: + en_US: replace_section + zh_Hans: replace_section + - value: commit_candidate + label: + en_US: commit_candidate + zh_Hans: commit_candidate + - value: remove_track + label: + en_US: remove_track + zh_Hans: remove_track + - value: render + label: + en_US: render + zh_Hans: render +- name: title + type: string + required: false + form: form + label: + en_US: Title + zh_Hans: 标题 + human_description: + en_US: Set title for this operation. + zh_Hans: 设置标题。 + llm_description: Set title for this operation. +- name: id + type: string + required: false + form: form + label: + en_US: Id + zh_Hans: ID + human_description: + en_US: Set id for this operation. + zh_Hans: 设置ID。 + llm_description: Set id for this operation. +- name: version_id + type: string + required: false + form: form + label: + en_US: Version Id + zh_Hans: 版本 ID + human_description: + en_US: Set version id for this operation. + zh_Hans: 新创建的空工程尚无 version_id;仅首次保存可省略。工程生成版本后,后续保存必须提交最新 version_id。 + llm_description: Set version id for this operation. +- name: state + type: string + required: false + form: form + label: + en_US: State + zh_Hans: 项目状态 + human_description: + en_US: Set state for this operation. Enter a JSON array or object where applicable. + zh_Hans: 工程 timing.bps 为每秒拍数,默认为 2,必须为正数。片段位置使用工程拍单位。 数组或对象请使用 JSON 格式。 + llm_description: Set state for this operation. Enter a JSON array or object where + applicable. +- name: audio_url + type: string + required: false + form: form + label: + en_US: Audio Url + zh_Hans: 音频链接 + human_description: + en_US: Set audio url for this operation. + zh_Hans: 设置音频链接。 + llm_description: Set audio url for this operation. +- name: callback_url + type: string + required: false + form: form + label: + en_US: Callback Url + zh_Hans: 回调 URL + human_description: + en_US: Set callback url for this operation. + zh_Hans: 设置回调 URL。 + llm_description: Set callback url for this operation. +- name: audio_id + type: string + required: false + form: form + label: + en_US: Audio Id + zh_Hans: 音频 ID + human_description: + en_US: Set audio id for this operation. + zh_Hans: 设置音频 ID。 + llm_description: Set audio id for this operation. +- name: name + type: string + required: false + form: form + label: + en_US: Name + zh_Hans: 名称 + human_description: + en_US: Set name for this operation. + zh_Hans: 设置名称。 + llm_description: Set name for this operation. +- name: start_beats + type: number + required: false + form: form + label: + en_US: Start Beats + zh_Hans: 开始节拍 + human_description: + en_US: Set start beats for this operation. + zh_Hans: 设置开始节拍。 + llm_description: Set start beats for this operation. + min: 0 +- name: end_beats + type: number + required: false + form: form + label: + en_US: End Beats + zh_Hans: 结束节拍 + human_description: + en_US: Set end beats for this operation. + zh_Hans: 设置结束节拍。 + llm_description: Set end beats for this operation. + min: 0 +- name: gain + type: number + required: false + form: form + label: + en_US: Gain + zh_Hans: 增益 + human_description: + en_US: Set gain for this operation. + zh_Hans: 设置增益。 + llm_description: Set gain for this operation. +- name: source_audio_id + type: string + required: false + form: form + label: + en_US: Source Audio Id + zh_Hans: 来源音频 ID + human_description: + en_US: Set source audio id for this operation. + zh_Hans: 设置来源音频 ID。 + llm_description: Set source audio id for this operation. +- name: model + type: select + required: false + form: form + label: + en_US: Model + zh_Hans: 模型 + human_description: + en_US: Select a model currently supported by this API. Availability and pricing + depend on the chosen model. + zh_Hans: 设置模型。 + llm_description: Select a model currently supported by this API. Availability and + pricing depend on the chosen model. + options: + - value: chirp-v3-5 + label: + en_US: chirp-v3-5 + zh_Hans: chirp-v3-5 + - value: chirp-v4 + label: + en_US: chirp-v4 + zh_Hans: chirp-v4 + - value: chirp-v4-5 + label: + en_US: chirp-v4-5 + zh_Hans: chirp-v4-5 + - value: chirp-v4-5-plus + label: + en_US: chirp-v4-5-plus + zh_Hans: chirp-v4-5-plus + - value: chirp-v5 + label: + en_US: chirp-v5 + zh_Hans: chirp-v5 + - value: chirp-v5-5 + label: + en_US: chirp-v5-5 + zh_Hans: chirp-v5-5 + - value: chirp-v6 + label: + en_US: chirp-v6 + zh_Hans: chirp-v6 + - value: chirp-v6-wild + label: + en_US: chirp-v6-wild + zh_Hans: chirp-v6-wild + - value: chirp-v6-mini + label: + en_US: chirp-v6-mini + zh_Hans: chirp-v6-mini +- name: tags + type: string + required: false + form: form + label: + en_US: Tags + zh_Hans: 标签 + human_description: + en_US: Set tags for this operation. + zh_Hans: 设置标签。 + llm_description: Set tags for this operation. +- name: prompt + type: string + required: false + form: llm + label: + en_US: Prompt + zh_Hans: 提示词 + human_description: + en_US: Set prompt for this operation. + zh_Hans: 设置提示词。 + llm_description: Set prompt for this operation. +- name: start_seconds + type: number + required: false + form: form + label: + en_US: Start Seconds + zh_Hans: 开始秒数 + human_description: + en_US: Set start seconds for this operation. + zh_Hans: 设置开始秒数。 + llm_description: Set start seconds for this operation. + min: 0 +- name: end_seconds + type: number + required: false + form: form + label: + en_US: End Seconds + zh_Hans: 结束秒数 + human_description: + en_US: Set end seconds for this operation. + zh_Hans: 设置结束秒数。 + llm_description: Set end seconds for this operation. +- name: render_audio_id + type: string + required: false + form: form + label: + en_US: Render Audio Id + zh_Hans: 渲染音频 ID + human_description: + en_US: Set render audio id for this operation. + zh_Hans: 设置渲染音频 ID。 + llm_description: Set render audio id for this operation. +- name: stem_control_tags + type: string + required: false + form: form + label: + en_US: Stem Control Tags + zh_Hans: 音轨控制标签 + human_description: + en_US: Set stem control tags for this operation. + zh_Hans: 设置音轨控制标签。 + llm_description: Set stem control tags for this operation. +- name: batch_size + type: number + required: false + form: form + label: + en_US: Batch Size + zh_Hans: 批量数量 + human_description: + en_US: Set batch size for this operation. + zh_Hans: 设置批量数量。 + llm_description: Set batch size for this operation. + min: 1 + max: 4 +- name: replacement_lyrics + type: string + required: false + form: form + label: + en_US: Replacement Lyrics + zh_Hans: 替换歌词 + human_description: + en_US: Set replacement lyrics for this operation. + zh_Hans: 设置替换歌词。 + llm_description: Set replacement lyrics for this operation. +- name: fixed + type: boolean + required: false + form: form + label: + en_US: Fixed + zh_Hans: 固定模式 + human_description: + en_US: Set fixed for this operation. + zh_Hans: 设置固定模式。 + llm_description: Set fixed for this operation. +- name: operation_id + type: string + required: false + form: form + label: + en_US: Operation Id + zh_Hans: 操作 ID + human_description: + en_US: Set operation id for this operation. + zh_Hans: 设置操作 ID。 + llm_description: Set operation id for this operation. +- name: candidate_id + type: string + required: false + form: form + label: + en_US: Candidate Id + zh_Hans: 候选 ID + human_description: + en_US: Set candidate id for this operation. + zh_Hans: 设置候选 ID。 + llm_description: Set candidate id for this operation. +- name: track_id + type: string + required: false + form: form + label: + en_US: Track Id + zh_Hans: 音轨 ID + human_description: + en_US: Set track id for this operation. + zh_Hans: 替换候选提交到原轨道并保留源片段位置;新轨候选提交到提前创建的空轨道或无重叠位置。 + llm_description: Set track id for this operation. +- name: lyrics + type: string + required: false + form: form + label: + en_US: Lyrics + zh_Hans: 歌词 + human_description: + en_US: Set lyrics for this operation. + zh_Hans: 设置歌词。 + llm_description: Set lyrics for this operation. +- name: idempotency_key + type: string + required: false + form: form + label: + en_US: Idempotency Key + zh_Hans: 幂等键 + human_description: + en_US: Set idempotency key for this operation. + zh_Hans: 设置幂等键。 + llm_description: Set idempotency key for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_projects.py diff --git a/tools/suno_task_retrieve.py b/tools/suno_task_retrieve.py index c8ba3b7..d712e2f 100644 --- a/tools/suno_task_retrieve.py +++ b/tools/suno_task_retrieve.py @@ -11,17 +11,21 @@ class SunoTaskRetrieveTool(Tool): def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: - client = AceDataSunoClient( - bearer_token=self.runtime.credentials.get("acedata_bearer_token", "") + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_task_retrieve", tool_parameters ) - result = client.execute("task", tool_parameters) yield self.create_json_message(result) - for field in ["status", "task_id", "media_urls", "result"]: - yield self.create_variable_message(field, result[field]) - yield self.create_variable_message("success", result["status"] == "succeeded") - yield self.create_variable_message("trace_id", result["result"].get("trace_id") or "") - yield self.create_variable_message( - "data", result["result"].get("data", result["result"].get("content", {})) - ) + for name, value in result.items(): + yield self.create_variable_message(name, value) for url in result["media_urls"]: - yield self.create_link_message(url) + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_task_retrieve.yaml b/tools/suno_task_retrieve.yaml index c92b796..f87b3cd 100644 --- a/tools/suno_task_retrieve.yaml +++ b/tools/suno_task_retrieve.yaml @@ -6,54 +6,55 @@ identity: zh_Hans: Suno 查询任务 description: human: - en_US: Retrieve a task or wait up to 240 seconds for completion. - zh_Hans: 查询任务,或最多等待 240 秒至完成。 - llm: Retrieve an existing task. Poll the same task ID if pending; never resubmit - generation to check progress. + en_US: Retrieve one task, optionally waiting up to 240 seconds. + zh_Hans: 查询单个任务,可等待最多 240 秒。 + llm: Query the existing task ID; do not repeat generation while it is pending. parameters: - name: task_id + type: string + required: true + form: form label: en_US: Task ID zh_Hans: 任务 ID - type: string - required: true - form: llm human_description: - en_US: The task ID returned by a generation or edit tool. - zh_Hans: 生成或编辑工具返回的任务 ID。 - llm_description: The task ID returned by a generation or edit tool. + en_US: Use the task ID or IDs returned by earlier submissions. + zh_Hans: 使用之前提交返回的任务 ID。 + llm_description: Use the task ID or IDs returned by earlier submissions. - name: wait_seconds - label: - en_US: Wait up to seconds - zh_Hans: 最长等待秒数 type: number required: false form: form + label: + en_US: Wait up to seconds + zh_Hans: 最长等待秒数 human_description: - en_US: 0 returns current status; 1–240 polls until completion. Pending results - keep the same task ID. Do not resubmit generation. - zh_Hans: 0 表示仅查询一次;1–240 表示等待至完成或超时。未完成时继续查询同一任务,请勿重复提交生成。 - llm_description: 0 returns current status; 1–240 polls until completion. Pending - results keep the same task ID. Do not resubmit generation. + en_US: 0 queries once; 1–240 waits for the same task to finish. Pending is not + complete. + zh_Hans: 0 表示查询一次;1–240 表示等待同一任务完成。pending 不表示已完成。 + llm_description: 0 queries once; 1–240 waits for the same task to finish. Pending + is not complete. default: 0 + min: 0 + max: 240 output_schema: type: object properties: status: type: string + success: + type: boolean task_id: type: string + trace_id: + type: string media_urls: type: array items: type: string + data: {} result: type: object - success: - type: boolean - trace_id: - type: string - data: {} extra: python: source: tools/suno_task_retrieve.py diff --git a/tools/suno_tasks_retrieve_batch.py b/tools/suno_tasks_retrieve_batch.py new file mode 100644 index 0000000..7b47e01 --- /dev/null +++ b/tools/suno_tasks_retrieve_batch.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoTasksRetrieveBatchTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_tasks_retrieve_batch", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_tasks_retrieve_batch.yaml b/tools/suno_tasks_retrieve_batch.yaml new file mode 100644 index 0000000..8b66dad --- /dev/null +++ b/tools/suno_tasks_retrieve_batch.yaml @@ -0,0 +1,46 @@ +identity: + name: suno_tasks_retrieve_batch + author: acedatacloud + label: + en_US: Suno Retrieve Tasks + zh_Hans: Suno 批量查询任务 +description: + human: + en_US: Retrieve existing tasks by ID without repeating generation. + zh_Hans: 按 ID 批量查询已有任务,不重复提交生成。 + llm: Query existing task IDs. Each item retains its own terminal or pending status. +parameters: +- name: ids + type: string + required: true + form: form + label: + en_US: Ids + zh_Hans: ID 列表 + human_description: + en_US: Use the task ID or IDs returned by earlier submissions. Enter a JSON array + or object where applicable. + zh_Hans: 使用之前提交返回的任务 ID。 数组或对象请使用 JSON 格式。 + llm_description: Use the task ID or IDs returned by earlier submissions. Enter a + JSON array or object where applicable. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_tasks_retrieve_batch.py diff --git a/tools/suno_upload_reference_audio.py b/tools/suno_upload_reference_audio.py new file mode 100644 index 0000000..4f3bbc4 --- /dev/null +++ b/tools/suno_upload_reference_audio.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + +from tools.acedata_client import AceDataSunoClient + + +class SunoUploadReferenceAudioTool(Tool): + def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]: + result = AceDataSunoClient(self.runtime.credentials.get("acedata_bearer_token", "")).invoke( + "suno_upload_reference_audio", tool_parameters + ) + yield self.create_json_message(result) + for name, value in result.items(): + yield self.create_variable_message(name, value) + for url in result["media_urls"]: + if ( + "audio" == "image" + or "audio" == "mixed" + and any( + suffix in url.lower().split("?")[0] + for suffix in [".png", ".jpg", ".jpeg", ".webp"] + ) + ): + yield self.create_image_message(url) + else: + yield self.create_link_message(url) diff --git a/tools/suno_upload_reference_audio.yaml b/tools/suno_upload_reference_audio.yaml new file mode 100644 index 0000000..682bd1c --- /dev/null +++ b/tools/suno_upload_reference_audio.yaml @@ -0,0 +1,89 @@ +identity: + name: suno_upload_reference_audio + author: acedatacloud + label: + en_US: Suno Upload Reference Audio + zh_Hans: Suno 上传参考音频 +description: + human: + en_US: Upload Reference Audio through the Ace Data Cloud Suno API. + zh_Hans: 通过 Ace Data Cloud Suno API 执行 Upload Reference Audio。 + llm: Upload Reference Audio through the Ace Data Cloud Suno API. For asynchronous + results, preserve task_id and use the task retrieval tool until completion. A + successful submission is not completed output. +parameters: +- name: audio_url + type: string + required: true + form: form + label: + en_US: Audio Url + zh_Hans: 音频链接 + human_description: + en_US: Set audio url for this operation. + zh_Hans: 待处理的自定义音频 URL,必须可公开访问;该输入地址不会由终态媒体持久化流程预先转存。 + llm_description: Set audio url for this operation. +- name: mode + type: select + required: false + form: form + label: + en_US: Mode + zh_Hans: 模式 + human_description: + en_US: Set mode for this operation. + zh_Hans: 上传模式。`standard` 成功处理消耗 0.06 Credits;`enhanced` 用于部分普通上传无法处理的授权音频,固定异步执行,成功处理消耗 + 1.87 Credits。处理失败不扣费。 + llm_description: Set mode for this operation. + options: + - value: standard + label: + en_US: standard + zh_Hans: standard + - value: enhanced + label: + en_US: enhanced + zh_Hans: enhanced +- name: name + type: string + required: false + form: form + label: + en_US: Name + zh_Hans: 名称 + human_description: + en_US: Set name for this operation. + zh_Hans: 音频名称。`mode=enhanced` 时必填,长度为 1 至 100 个字符。 + llm_description: Set name for this operation. +- name: callback_url + type: string + required: false + form: form + label: + en_US: Callback Url + zh_Hans: 回调 URL + human_description: + en_US: Set callback url for this operation. + zh_Hans: 可选的 HTTPS 回调地址。增强上传完成或失败后,结果将发送至该地址。 + llm_description: Set callback url for this operation. +output_schema: + type: object + properties: + status: + type: string + success: + type: boolean + task_id: + type: string + trace_id: + type: string + media_urls: + type: array + items: + type: string + data: {} + result: + type: object +extra: + python: + source: tools/suno_upload_reference_audio.py