256 lines
7.0 KiB
Markdown
256 lines
7.0 KiB
Markdown
## Context
|
||
|
||
`opentu` 当前的生成体系围绕三类模态构建:
|
||
|
||
- `text`
|
||
- `image`
|
||
- `video`
|
||
|
||
这三个模态已经贯穿以下层级:
|
||
|
||
- `model-config` 中的静态模型类型
|
||
- `settings-manager` 中的默认路由 preset
|
||
- `provider-routing` 中的 operation 与 binding
|
||
- `model-adapters` 中的适配器接口
|
||
- `task-queue` 与历史记录中的任务类型与结果结构
|
||
- `AIInputBar` 与设置页中的模型/模式切换
|
||
|
||
因此“增加音频层”并不是简单地再加一个 UI 选项,而是需要把音频模态接入整条调用链。
|
||
|
||
Suno 的特殊点在于,它暴露的是一组能力动作,而不是一组可以直接照抄提交的模型:
|
||
|
||
- 发现能力示例:
|
||
- `suno_music`
|
||
- `suno-continue`
|
||
- `suno-continue-uploaded`
|
||
- `suno-remix`
|
||
- `suno-remix-uploaded`
|
||
- `suno-infill`
|
||
- `suno-lyrics`
|
||
- `suno-tags`
|
||
- `suno_act_tags`
|
||
- `suno_act_timing`
|
||
- 真实音乐生成提交接口:
|
||
- `POST /suno/submit/music`
|
||
- 真实版本切换字段:
|
||
- `mv`
|
||
- 真实状态查询接口:
|
||
- `GET /suno/fetch/{task_id}`
|
||
|
||
因此,如果把 `suno_music` 这类标识直接当成“可调用模型 ID”,最终会出现两个问题:
|
||
|
||
1. UI 上看到的能力动作和请求里真正需要的 `mv`、续写参数混在一起
|
||
2. 运行时发现层与执行层之间失去稳定映射
|
||
|
||
## Goals / Non-Goals
|
||
|
||
- Goals:
|
||
- 为 `opentu` 增加一等音频生成模态
|
||
- 为 Suno 建立“能力动作 -> 执行规则 -> 版本参数”的明确映射
|
||
- 复用现有多供应商路由和任务轮询思路,而不是单独再造一套音频系统
|
||
- 先跑通音乐生成和单任务查询闭环
|
||
- Non-Goals:
|
||
- 本次不一次性覆盖 Suno 全量能力
|
||
- 本次不实现完整的画布音频后处理编辑器
|
||
- 本次不让发现到的所有 Suno 能力都自动出现在 UI 主路径中
|
||
|
||
## Decisions
|
||
|
||
- Decision: 引入 `audio` 作为一等模态
|
||
|
||
- `ModelType`、`GenerationType`、`TaskType`、`ProviderOperation`、默认 preset 都增加 `audio`
|
||
- 音频不再借用文本或视频通道透传
|
||
|
||
- Decision: 区分“能力动作”和“执行版本”
|
||
|
||
- `suno_music`、`suno-continue` 等发现项表示能力或操作入口
|
||
- `mv` 表示真实执行使用的 Suno 版本,例如 `chirp-v3-0`、`chirp-v3-5`
|
||
- 续写上传场景通过 `mv + '-upload'` 表达
|
||
|
||
- Decision: Suno 初始切片只做 `music submit + fetch`
|
||
|
||
- 首批只支持 `POST /suno/submit/music`
|
||
- 首批只支持 `GET /suno/fetch/{task_id}`
|
||
- 其余 Suno 动作先作为已发现但未激活的能力保留在元数据层
|
||
|
||
- Decision: UI 不直接展示所有发现能力
|
||
|
||
- 用户主路径先暴露统一的 `音频` 模式
|
||
- 进入音频模式后,再根据绑定能力展示当前支持的生成方式和字段
|
||
- 避免让用户直接面对一串 `suno-*` 能力代号
|
||
|
||
- Decision: 任务轮询采用 provider-specific 标准化
|
||
|
||
- `/suno/fetch/{task_id}` 返回的 `status/progress/data` 需要统一转换成内部任务状态
|
||
- 不能强制复用视频任务的原始响应结构
|
||
|
||
## Proposed Data Model
|
||
|
||
```ts
|
||
type ModelType = 'text' | 'image' | 'video' | 'audio';
|
||
|
||
type SunoAction =
|
||
| 'music'
|
||
| 'continue'
|
||
| 'continue-uploaded'
|
||
| 'lyrics'
|
||
| 'tags'
|
||
| 'remix'
|
||
| 'remix-uploaded'
|
||
| 'infill'
|
||
| 'infill-uploaded';
|
||
|
||
interface ProviderAudioBindingMetadata {
|
||
action: SunoAction | string;
|
||
versionField?: string;
|
||
versionOptions?: string[];
|
||
defaultVersion?: string;
|
||
supportsContinuation?: boolean;
|
||
supportsUploadContinuation?: boolean;
|
||
supportsLyricsPrompt?: boolean;
|
||
supportsTags?: boolean;
|
||
supportsTitle?: boolean;
|
||
taskIdField?: string;
|
||
resultAudioUrlPath?: string;
|
||
}
|
||
|
||
interface AudioGenerationRequest {
|
||
prompt: string;
|
||
model?: string;
|
||
modelRef?: ModelRef | null;
|
||
title?: string;
|
||
tags?: string;
|
||
mv?: string;
|
||
continueClipId?: string;
|
||
continueAt?: number;
|
||
params?: Record<string, unknown>;
|
||
}
|
||
|
||
interface AudioGenerationResult {
|
||
url: string;
|
||
title?: string;
|
||
duration?: number | null;
|
||
waveformUrl?: string;
|
||
raw?: unknown;
|
||
}
|
||
```
|
||
|
||
## Suno Binding Strategy
|
||
|
||
### Capability Layer
|
||
|
||
发现层保留原始能力标识,例如:
|
||
|
||
- `suno_music`
|
||
- `suno-continue`
|
||
- `suno-continue-uploaded`
|
||
- `suno-lyrics`
|
||
- `suno-tags`
|
||
|
||
这些标识用于:
|
||
|
||
- 推断该 profile 是否支持音频
|
||
- 推断该 profile 支持哪些音频动作
|
||
- 决定 UI 允许展示哪些高级入口
|
||
|
||
### Execution Layer
|
||
|
||
执行层不直接按发现能力 ID 发请求,而是映射成:
|
||
|
||
- `submitPath`
|
||
- `pollPathTemplate`
|
||
- `action`
|
||
- `mv` 规则
|
||
- 字段可用性
|
||
|
||
例如首批音乐生成统一映射为:
|
||
|
||
```ts
|
||
{
|
||
operation: 'audio',
|
||
protocol: 'tuzi.suno.music',
|
||
submitPath: '/suno/submit/music',
|
||
pollPathTemplate: '/suno/fetch/{task_id}',
|
||
metadata: {
|
||
audio: {
|
||
action: 'music',
|
||
versionField: 'mv',
|
||
versionOptions: ['chirp-v3-0', 'chirp-v3-5'],
|
||
defaultVersion: 'chirp-v3-5',
|
||
supportsContinuation: true,
|
||
supportsUploadContinuation: true,
|
||
supportsTags: true,
|
||
supportsTitle: true,
|
||
supportsLyricsPrompt: true
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
当用户选择“续写已上传音频”时,不改 submitPath,而是通过 metadata 规则把:
|
||
|
||
- `mv=chirp-v3-5`
|
||
|
||
转换为:
|
||
|
||
- `mv=chirp-v3-5-upload`
|
||
|
||
## UI Strategy
|
||
|
||
### Settings
|
||
|
||
- 在默认路由 preset 中增加 `audio`
|
||
- 音频默认路由保存 `ModelRef`
|
||
- 如果当前 provider profile 未发现音频能力,则不允许被选为音频默认路由
|
||
|
||
### AI Input Bar
|
||
|
||
- 增加 `音频` 模式
|
||
- 进入音频模式后,参数区支持:
|
||
- `mv`
|
||
- `title`
|
||
- `tags`
|
||
- `continue_clip_id`
|
||
- `continue_at`
|
||
- 默认交互仍保持与图片/视频一致:
|
||
- 选择模型
|
||
- 输入提示词
|
||
- 提交任务
|
||
|
||
### Task And History
|
||
|
||
- 任务类型增加 `audio`
|
||
- 历史记录支持展示音频结果
|
||
- 结果结构至少包含:
|
||
- 音频 URL
|
||
- 标题
|
||
- 创建时间
|
||
- 执行状态
|
||
|
||
## Migration Plan
|
||
|
||
1. 先补核心类型层和 preset 路由层的 `audio`
|
||
2. 再补 provider routing / adapter / service 的 Suno 音频闭环
|
||
3. 再补 AI 输入栏和设置页入口
|
||
4. 最后补任务列表、历史记录和结果回显
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- 风险: 音频能力切入口过大,影响现有图片/视频链路
|
||
- Mitigation: 首批只做 `music submit + fetch`,不扩张到所有 Suno 动作
|
||
|
||
- 风险: 发现能力与执行规则混在一起,导致后续高级动作难扩展
|
||
- Mitigation: 明确拆分 capability layer 和 execution layer
|
||
|
||
- 风险: UI 直接暴露 `suno-*` 原始能力名,增加理解成本
|
||
- Mitigation: 主路径只暴露 `音频` 模式和人类可理解的操作标签
|
||
|
||
- 风险: 音频结果结构与视频结果结构不同,历史记录复用成本高
|
||
- Mitigation: 在 `TaskResult` 中增加音频最小字段集,而不是强行套用视频字段
|
||
|
||
## Open Questions
|
||
|
||
- 首批是否需要同时支持“仅生成纯音乐”和“包含人声/歌词”的 UI 开关
|
||
- `suno_lyrics`、`suno_tags` 是否应该作为后续独立工具,还是继续作为音乐生成前的辅助步骤
|
||
- 音频结果进入画布后,第一版是否只作为可播放资源卡片,而不引入完整音频节点编辑器
|