Skip to main content

Documentation Index

Fetch the complete documentation index at: https://tech.illasoft.com/llms.txt

Use this file to discover all available pages before exploring further.

概述

kira-music-worker 是音乐生成 Inngest function consumer。从 kira-be 接收事件,调用 KIE AI (Suno unofficial) 生成音乐,做后处理(封面生成、歌词对齐、ID3 嵌入),上传到 Supabase Storage,推送 Centrifugo 通知。 架构重要变化:2026-04-02 从 NATS 消费者重构为 Inngest HTTP function(commit 0f2da43)。MiniMax Music 2.5 和 Mureka API 已移除(commit afc6058),当前仅 KIE AI 三条线路。

部署形态

平台Fly.io
对外端口无(仅 kira-music-worker.internal
进程HTTP server,接收 Inngest push
入口src/index.tsserve({ client: inngest, functions })

Inngest Functions

4 个 function,分两类:

生成类(有并发限制)

Function IDEvent并发Provider
music-generation-kie-sunomusic/generation-kie-suno.requestedper-user-per-tool=1,per-provider=100SunoUnofficialProvider
music-instrumental-kie-sunomusic/instrumental-kie-suno.requested同上SunoInstrumentalProvider
music-add-vocals-kie-sunomusic/add-vocals-kie-suno.requested同上SunoAddVocalsProvider

非生成类(无限并发)

Function IDEvent用途
music-upload-kiramusic/upload-kira.requested用户直传音频
music-trim-kiramusic/trim-kira.requested音频修剪
(还有一个 music-cancelled-cleanup 监听 inngest/function.cancelled 做退款/通知。)

标准处理 Pipeline(music generation)

源:src/processor.ts:81-154
step 1: check
  ├─ checkThreadExists(threadId)          # thread 被删则跳过
  ├─ getMusicCreditsCost(toolName) = 25   # 固定 25 credits
  ├─ checkMusicEligibility(userId, 25)    # 余额 / plan
  ├─ deductCredits(userId, 25, taskId)    # 原子扣减(行锁)
  └─ { skip, startTime }

step 2: notify-processing
  ├─ updateMusicInJsonb(taskId, { status: "processing" })
  ├─ updateMessageToolOutput(...)
  └─ notifyUser(..., { type: "audio_status", audio: { status: "processing" } })

step 3: submit-task
  ├─ provider = getProviderByToolName(toolName)
  └─ providerTaskId = await provider.submitTask(task)

step 4: poll-result
  └─ pollWithInngestSteps(step, () => provider.pollTask(providerTaskId))
     # 用 step.sleep + step.waitForEvent 做 durable polling
     # 返回 GenerationResult { success, audioUrl, ... }

step 5: post-process
  └─ processAudio(generationResult, task)  # 见下方详解

step 6: finalize
  ├─ trackToolUsage({ userId, threadId, taskId, tool_name, success, duration_ms, credits_consumed })
  ├─ trackMediaUsage({ taskId, toolName, provider, model, duration_ms, output_duration_ms })  # 仅时长,无金额
  └─ handleSuccess(taskId, userId, threadId, audioResult, task)
     ├─ checkAudioCsam({ id3CoverBuffer, lyrics, audioFileId, coverId, ... })
     ├─ updateMusicInJsonb(taskId, { status: "completed", audioId, coverId, ... })
     └─ notifyUser(..., { type: "audio_status", audio: { status: "completed", url, coverUrl, ... } })

Post-Processing 五步(src/post-processor.ts

1. download          下载 Suno 原始音频 URL
   ├─ fetch(audioUrl)
   ├─ 按 Content-Type 推断格式(mp3/wav/ogg/flac)
   └─ Buffer

2. generate cover    生成封面图
   ├─ 优先:从 Suno generation result 提取 cover_url
   └─ fallback:
      ├─ BytePlus SeedReam `seedream-5-0-260128` 模型
      ├─ prompt: "Album cover art for a song titled '{title}'. {prompt}. Professional..."
      ├─ sharp resize to 2048×2048 JPEG quality 90%
      └─ generateBlurhash (4×4 components, 32×32 resize)

3. STT + forced alignment    歌词时间戳对齐
   ├─ KIE AI STT          # 音频 → 文本
   ├─ ElevenLabs Scribe v2 # 字级时间戳
   └─ Word[] = [{ word, start, end }, ...]

4. embed ID3         嵌入元数据
   ├─ 用 mp3tag.js (Bun shim)
   └─ tags:
      ├─ TIT2  (title)
      ├─ TPE1  (artist)
      ├─ APIC  (cover JPEG)
      ├─ SYLT  (synced lyrics, word-level timestamps)
      └─ USLT  (unsynced lyrics, full text)

5. upload            上传到 Supabase Storage
   ├─ audio: {userId}/{threadId}/music_{audioId}.mp3  cacheControl:3600
   └─ cover: {userId}/{threadId}/mc_{coverId}.jpg     metadata: blurhash, size

Providers(当前)

仅 KIE AI 三路(MiniMax / Mureka 已删除):
toolNameProvider 类Suno 模式
generateMusicSunoUnofficialProvider常规生成(lyrics + style prompt)
generateInstrumentalSunoInstrumentalProvider无人声纯乐器
addVocalsSunoAddVocalsProvider对已有音频添加/替换人声
addVocals 特有参数:vocalStyle (required)、styleWeightweirdnessConstraintaudioWeight

CSAM 审核(src/csam.ts)

finalize step 后触发 handleSuccess → checkAudioCsam
并发:
  ├─ detectCsamFromBuffer(id3CoverBuffer)  # 封面图
  └─ detectCsamFromText(lyrics)            # 歌词全文

命中任一 → block:
  ├─ 删除 audioFileId + coverId(若存在)
  ├─ 写 failed_uploads 表
  ├─ notify { type: "audio_status", audio: { status: "failed", errorCode: "csam_blocked" } }
  └─ credits 不退款(合规处罚)

Centrifugo 推送

  • Channel 格式{userId}/{threadId}#{userId}
  • 端点{CENTRIFUGO_URL}/api/publishX-API-Key 认证
{
  "type": "audio_status",
  "taskId": "...",
  "audio": {
    "taskId": "...",
    "status": "processing" | "completed" | "failed" | "insufficient_credits" | "insufficient_plan",
    "url": "https://...",      // completed 时
    "coverUrl": "https://...", // completed 时
    "duration": 30.5,
    "blurhash": "U6PVzY...",
    "errorCode": "csam_blocked" | ...,
    "errorMessage": "..."
  },
  "timestamp": "2026-04-14T12:34:56Z"
}

Cleanup 机制

music-cancelled-cleanup function 监听 inngest/function.cancelled
  • 非 upload/trim 类:退款 credits + 通知用户 failed / cancelled
  • upload/trim 类:credits 本就为 0,只通知
手动取消路径:
  1. kira-be 发 music/task.cancelled event
  2. kira-be 设 Redis cancel:{taskId} flag (TTL 3600s)
  3. Worker 每个 step 开头检查 flag,命中即抛 NonRetriableError

Billing

工具Credits备注
generateMusic25固定
generateInstrumental25固定
addVocals25固定
upload0免费
trimAudio0免费
CSAM 阻断 不退款;insufficient_credits / insufficient_plan 不扣费。

环境变量

SUPABASE_URL
SUPABASE_KEY
REDIS_URL                  # Dragonfly,cancel flag
INNGEST_EVENT_KEY
INNGEST_SIGNING_KEY
KIE_AI_API_KEY             # Suno unofficial
BYTEPLUS_API_KEY           # SeedReam 封面
ELEVENLABS_API_KEY         # Scribe v2 forced alignment
CENTRIFUGO_URL
CENTRIFUGO_API_KEY
POSTHOG_API_KEY
SENTRY_DSN

相关文档