架构概览
核心组件
| 组件 | 作用 | 说明 |
|---|
| Music Tools | 任务创建 | 验证权限、检查 NSFW、选择 Provider |
| Agent onFinish | 消息发布 | 将 pending 任务发布到 NATS |
| NATS JetStream | 异步队列 | TASKS stream,subject tasks.music.* |
| kira-music-worker | 任务执行 | 扣费、调用 Provider API、后处理、上传 |
| Supabase Storage | 音频存储 | agent_message bucket |
| Centrifugo | 状态推送 | WebSocket 实时通知前端 |
音乐生成工具
generateMusic
生成完整歌曲(含歌词和可选风格提示)。
| 参数 | 类型 | 必填 | 说明 |
|---|
title | string | 否 | 歌曲标题(最长 200 字符),AI 可自动生成 |
lyrics | string | 是 | 歌词(1-3500 字符),支持结构标签 [Verse], [Chorus], [Bridge], [Outro] |
prompt | string | 否 | 风格/情绪描述(最长 2000 字符) |
coverImageId | string | 否 | 封面图片 ID |
- Provider:
suno_unofficial
- Cost: 25 credits
- Plan: Pro+
generateInstrumental
生成纯器乐曲目。
| 参数 | 类型 | 必填 | 说明 |
|---|
title | string | 否 | 曲目标题 |
prompt | string | 是 | 器乐风格描述(如 “cinematic orchestral”, “lo-fi hip hop”) |
coverImageId | string | 否 | 封面图片 ID |
- Provider:
suno_instrumental
- Cost: 25 credits
- Plan: Pro+
addVocals
为已有器乐曲目添加 AI 人声。
| 参数 | 类型 | 必填 | 说明 |
|---|
sourceAudioId | string | 是 | 源器乐曲目 ID |
vocalStyle | string | 是 | 人声描述 |
title | string | 是 | 标题 |
coverImageId | string | 否 | 复用已有封面;如果不传则使用 Seedream 生成封面 |
style | string | 否 | 风格标签(如 “Jazz”, “Pop”, “R&B”) |
negativeTags | string | 否 | 排除元素(如 “autotune, screaming”) |
styleWeight | number | 否 | 风格权重 (0-1) |
weirdnessConstraint | number | 否 | 创意程度 (0-1) |
audioWeight | number | 否 | 与源音频一致性 (0-1) |
- Provider:
suno_add_vocals
- Cost: 25 credits
- Plan: Pro+
Credits 计费
| 工具 | 成本 | 所需方案 |
|---|
| generateMusic | 25 credits | Pro+ |
| generateInstrumental | 25 credits | Pro+ |
| addVocals | 25 credits | Pro+ |
计费流程
Credits 在 kira-music-worker 中扣除(非 kira-be),Tool 仅验证资格。同一 taskId 不会重复扣费。
任务生命周期
异步处理流程
AI Tool 创建任务
Music tool 检测 NSFW → 验证 Credits → 在 thread_version.audios JSONB 中插入记录
发布到 NATS
Agent onFinish 回调中,遍历 pending 状态的音乐任务,逐个发布到 tasks.music.{provider}.{taskId} subject
Worker 处理
kira-music-worker 消费消息,原子扣费,调用对应 Provider API,更新状态为 processing
后处理
封面生成(Suno Cover API,免费)+ 歌词对齐(Suno Timestamped Lyrics,$0.0025)并行 → 嵌入 ID3 标签 → 上传到 Supabase Storage → 更新状态为 completed
实时通知
通过 Centrifugo WebSocket 推送状态变更到前端(processing → completed/failed)
NATS 消息格式
interface MusicTaskMessage {
taskId: string;
userId: string;
threadId: string;
provider: "suno_unofficial" | "suno_instrumental" | "suno_add_vocals";
title?: string;
lyrics: string;
prompt?: string;
instrumental?: boolean; // generateInstrumental 时为 true
coverImageId?: string;
sourceAudioId?: string; // addVocals 时存在
style?: string; // addVocals
vocalStyle?: string; // addVocals
negativeTags?: string; // addVocals
styleWeight?: number; // addVocals
weirdnessConstraint?: number; // addVocals
audioWeight?: number; // addVocals
}
WebSocket 通知格式
// Centrifugo 频道: {userId}/{threadId}#{userId}
{
type: "audio_status";
taskId: string;
audio: {
taskId: string;
status: "processing" | "completed" | "failed";
audioId?: string; // completed 时存在
coverId?: string; // completed 时存在
durationMs?: number;
syncedLyrics?: Array<{ text: string; start: number; end: number }>;
errorMessage?: string; // failed 时存在
url?: string; // completed 时 Signed URL
coverUrl?: string; // completed 时 Signed URL
}
}
后处理管线
音乐生成完成后,kira-music-worker 对音频进行后处理:
显示歌词直接使用原始 task.lyrics(用户提供,已有行/段结构),无需 LLM 重新分组。Suno Timestamped Lyrics 仅提供逐词时间戳用于卡拉OK同步显示。
Suno Cover API (封面生成)
- API:
POST /api/v1/suno/cover/generate → 轮询 GET /api/v1/suno/cover/record-info
- 输入:
taskId (Suno 音乐生成任务 ID)
- 输出: 2 张风格不同的封面图,取第一张
- Cost: 0 credits (免费)
- 处理: 下载 → resize 1024x1024 → JPEG → 生成 blurhash → 上传 Storage
- 限制: 每个 taskId 只能生成一次 cover
Suno Timestamped Lyrics (歌词对齐)
- API:
POST /api/v1/generate/get-timestamped-lyrics
- 输入:
taskId + audioId (Suno 音频 clip ID)
- 输出:
alignedWords[] — 每个 word 带 startS / endS 时间戳
- Cost: 0.5 credits ($0.0025)
- 用途: 逐词时间戳用于卡拉OK同步显示,显示歌词直接使用原始
task.lyrics
- 跳过: instrumental 曲目不做歌词对齐
ID3 标签嵌入
将以下元数据写入 MP3 文件:
- Title (歌曲标题)
- Artist (
{用户名} via Kira Art)
- Cover image (APIC frame)
- Synced lyrics (SYLT frame, 卡拉OK格式)
成本明细 (每首歌)
| 项目 | 成本 | 来源 |
|---|
| 音乐生成 | $0.06 (12 credits) | Suno API |
| 封面生成 | $0 (免费) | Suno Cover API |
| 歌词对齐 | $0.0025 (0.5 credits) | Suno Timestamped Lyrics |
| 合计 | ≈ $0.0625 | |
数据模型
音频状态存储在 thread_version 表的 JSONB audios 数组中,无独立音频表:
interface Audio {
// 创建时确定(不可变)
toolCallId?: string;
toolName?: string;
taskId: string;
provider: "suno_unofficial" | "suno_instrumental" | "suno_add_vocals";
title?: string | null;
lyrics?: string | null;
prompt?: string | null;
instrumental?: boolean | null;
sourceAudioId?: string | null;
hasVocals?: boolean | null;
hasInstrumental?: boolean | null;
createdAt: string;
// Worker 更新(可变)
status: "pending" | "processing" | "completed" | "failed"
| "insufficient_credits";
audioId?: string; // 存储路径
coverId?: string; // 封面图片路径
coverWidth?: number;
coverHeight?: number;
durationMs?: number;
syncedLyrics?: Array<{ text: string; start: number; end: number }>;
errorMessage?: string;
}
文件存储结构
agent_message/
├── {userId}/
│ └── {threadId}/
│ ├── music_{uuid}.mp3 # 原始音频
│ └── mc_{uuid}.jpg # 封面图片
└── feed/
└── {userId}/
├── music_{uuid}.mp3 # Feed 音频副本
└── mc_{uuid}.jpg # Feed 封面副本
Feed 集成
音频作为一等内容类型发布到 Feed:
{
audioId: string; // Feed 存储路径
coverId?: string | null; // 封面路径
lyrics?: string | null;
prompt?: string | null;
durationMs?: number | null;
coverWidth?: number | null;
coverHeight?: number | null;
syncedLyrics?: Array<{ text: string; start: number; end: number }>;
}
- Feed 表
media_type 字段现支持 "image" / "video" / "audio"
- 音频和封面从 message 存储复制到 feed 存储
- 发布前进行内容审核(封面图片 + 歌词/prompt 文本)
- 使用封面图片生成 embedding 进行向量相似度检查
清理与删除
删除消息或回溯时,自动清理关联的音频文件:
- 从
thread_version.audios 提取 taskId 列表
- 获取对应的
audioId、coverId 存储路径
- 从
agent_message bucket 中删除文件
- 清理 NATS 中未消费的任务(
purgeMusicTasks)
相关文档