6.3 KiB
6.3 KiB
Context
仓库当前已经为 Suno 音乐生成铺好了主路径:
runtime-model-discovery会把lyrics、music、suno等标识统归到audioprovider-routing会为 Suno 推断tuzi.suno.music绑定audio-api-service负责提交/suno/submit/music、轮询/suno/fetch/{taskId}并提取音频 URL- 任务队列、自动插入和任务面板会把
TaskType.AUDIO默认当成“可播放音频资产”
这条链路在音乐生成场景下是成立的,但在歌词场景下不成立:
- 歌词任务仍然属于 Suno 的异步任务
- 但 fetch 结果里的核心字段是:
data.data.textdata.data.titledata.data.tags
- 没有
audio_url
因此,当前代码里“路由类型 = 结果类型 = 画布落地方式”的隐含绑定需要被拆开。
Goals / Non-Goals
- Goals:
- 在不新增独立模态的前提下支持 Suno
lyrics动作 - 保持
audio路由、默认预设和主入口不变 - 显式区分 Suno 的提交动作和最终结果类型
- 让歌词任务进入现有任务队列、恢复、重试和画布插入链路
- 最大化复用现有文本插入能力,而不是新造歌词节点系统
- 在不新增独立模态的前提下支持 Suno
- Non-Goals:
- 不把歌词能力改造成独立
lyrics模态 - 不在本次中实现歌词编辑器、版本对比或歌词协作能力
- 不覆盖所有 Suno 高级动作
- 不重构整个任务系统为严格的 discriminated union 体系
- 不把歌词能力改造成独立
Decisions
-
Decision: 歌词能力继续归属
audio路由族- 预设配置仍然只维护
audio路由 - AI 输入栏也仍然通过
音频模式进入 - 通过
sunoAction = 'music' | 'lyrics'区分当前执行动作
- 预设配置仍然只维护
-
Decision: 显式拆分“提交动作”和“结果类型”
music与lyrics决定 submit path、请求字段和结果提取器resultKind决定任务队列展示和画布落地方式TaskType首批保持为AUDIO,避免把歌词任务拆成新的队列大类
-
Decision:
lyrics走独立 submit,但继续共用 fetchmusic -> POST /suno/submit/musiclyrics -> POST /suno/submit/lyrics- 两者都通过
GET /suno/fetch/{task_id}获取状态与结果
-
Decision: 任务结果模型以增量字段扩展为主
- 保持现有图片、视频、音频路径影响最小
- 为任务结果新增:
resultKindlyricsTextlyricsTitlelyricsTags
url对歌词结果不再视为必填
-
Decision: 歌词落画布复用现有文本插入链
- 任务面板点击“插入”时,把歌词结果格式化成 markdown / text 内容
- 自动插入同样走文本插入能力
- 不创建音频节点,也不伪造音频卡片
-
Decision: UI 参数按动作分域
music保留现有参数:mvtitletagscontinueSourcecontinueClipIdcontinueAt
lyrics只需要:prompt- 可选的内部
notifyHook
- 音乐专属参数在歌词动作下不显示、不提交
Proposed Data Shape
type SunoAction = 'music' | 'lyrics';
type SunoResultKind = 'audio' | 'lyrics';
interface SunoGenerationRequest {
prompt: string;
model?: string;
modelRef?: ModelRef | null;
sunoAction?: SunoAction;
notifyHook?: string;
title?: string;
tags?: string;
mv?: string;
continueClipId?: string;
continueAt?: number;
params?: Record<string, unknown>;
}
interface TaskResult {
resultKind?: 'image' | 'video' | 'audio' | 'lyrics' | 'chat';
url?: string;
urls?: string[];
format: string;
size: number;
title?: string;
previewImageUrl?: string;
providerTaskId?: string;
primaryClipId?: string;
clipIds?: string[];
clips?: AudioClipResult[];
lyricsText?: string;
lyricsTitle?: string;
lyricsTags?: string[];
}
Provider Binding Direction
首批不建议为 lyrics 新增独立模态 binding,而是在现有 audio binding metadata 上增加动作表:
interface ProviderAudioBindingMetadata {
defaultAction?: 'music';
supportedActions?: Array<'music' | 'lyrics' | string>;
actions?: Record<
string,
{
submitPath: string;
resultKind: 'audio' | 'lyrics';
requestFields: string[];
}
>;
versionField?: string;
versionOptions?: string[];
defaultVersion?: string;
supportsContinuation?: boolean;
supportsUploadContinuation?: boolean;
supportsTags?: boolean;
supportsTitle?: boolean;
}
对 Suno 的首批映射可收敛为:
{
operation: 'audio',
protocol: 'tuzi.suno.music',
pollPathTemplate: '/suno/fetch/{taskId}',
metadata: {
audio: {
defaultAction: 'music',
supportedActions: ['music', 'lyrics'],
actions: {
music: {
submitPath: '/suno/submit/music',
resultKind: 'audio',
requestFields: ['prompt', 'mv', 'title', 'tags', 'continue_clip_id', 'continue_at']
},
lyrics: {
submitPath: '/suno/submit/lyrics',
resultKind: 'lyrics',
requestFields: ['prompt', 'notify_hook']
}
}
}
}
}
Canvas Formatting Strategy
歌词插入画布时优先生成语义化文本,而不是直接丢一大段纯文本:
# 战斗进行时
标签: EDM, 激烈的
[Chorus]
...
这样可以直接复用现有:
- markdown 解析为卡片
- 纯文本回退为文本元素
两条路径。
Risks / Trade-offs
-
风险:
TaskType.AUDIO继续承载歌词任务会让部分旧代码仍然误判为音频资产- Mitigation: 统一改为优先判断
result.resultKind
- Mitigation: 统一改为优先判断
-
风险: 把
url改为非必填会触发较多类型修正- Mitigation: 先用增量字段方式扩展,聚焦实际消费点改造
-
风险: 任务队列过滤仍归入“音频”类,用户可能会觉得歌词不是音频
- Mitigation: 在 item 级别增加明确的动作标签,如“歌词”
-
风险: 画布文本插入格式不稳定,影响可读性
- Mitigation: 在服务层统一生成 markdown 模板,不把格式拼装分散到多个组件里
Implementation Outline
- 扩展 Suno 请求和 binding metadata,加入
lyrics动作 - 抽出共享 fetch 轮询与 action-specific 结果提取器
- 扩展任务结果模型与 IndexedDB 读写结构
- 让 AI 输入栏按动作切换参数面板
- 让任务队列、恢复与重试按
resultKind分支 - 让手动插入与自动插入把歌词结果落到文本插入链路