第1章:项目全景与架构总览
本章鸟瞰 live-mixer(直播切片工作台)的整体面貌:它解决什么问题、由哪些部分组成、数据如何流动。后续所有章节都在这张地图上展开,建议先通读本章再进入细节。
流程
live-mixer 是一条面向直播回放的端到端切片流水线。用户从浏览器(React 前端)录入直播素材 URL,后端(Go + Gin)将素材落库并异步触发 ASR 语音转写;转写完成后,用户创建剪辑项目,选择系统提示词;随后走上两条路径之一——「一键成片」自动串联 AI 切片与剪映草稿生成,或「AI 粗选 + 人工精修」先让大模型挑出高价值句段(clips1),人工调整后再单独生成草稿。最终产物是可供下载的剪映草稿(draft_url)、切片 tar 包(clips_tar_url)以及可选的成片视频(video_url)。
整个系统由四类进程/容器协作:
- live-mixer 主服务:单进程同时承载 HTTP API 与后台 Worker(ASR、AI 切片、草稿、一键成片四组)。
- capcut-mate 服务:独立的剪映草稿生成服务,主服务通过 REST 调用它。
- nginx:静态资源与反向代理,统一对外入口。
- PostgreSQL:唯一的持久化存储,Worker 通过数据库乐观锁实现多实例安全调度。
flowchart TD
A[浏览器 React 前端] -->|HTTP/JSON| B[Go API: Gin 路由]
B --> C[(PostgreSQL)]
B --> D[ASR Worker]
B --> E[AI 切片 Worker]
B --> F[草稿/一键成片 Worker]
D --> G[豆包 BigModel ASR]
E --> H[OpenAI 兼容 LLM]
F --> I[capcut-mate]
F --> J[ffmpeg / 对象存储]
I --> K[剪映草稿 / 成片]
一次典型「一键成片」任务的时序是:POST /tasks/ai-slice-draft 创建 Task(pending)→ Worker 乐观锁抢占(processing)→ 读取素材 ASR 段落与项目提示词 → LLM 返回索引数组与标题/描述/话题 → 回写 video_project.clips1 → 下载源视频、ffmpeg 裁剪切片、上传对象存储 → 调用 capcut-mate 建草稿、加视频轨、加字幕 →(可选)触发成片生成并轮询 → 任务标记 completed,客户端轮询 GET /tasks/:id 拿到三个 URL。
实现
代码采用经典的 Go 分层架构,目录即架构:
backend/internal/
├── handler/ # HTTP 层:参数绑定、校验、调 service、写响应
├── service/ # 业务层:编排 repository 与 Worker、跨模块流程
├── repository/ # 数据访问层:GORM 封装、乐观锁、筛选
├── model/ # 实体与 JSONB 嵌套结构
├── draft/ # 剪映草稿流水线(prepare → session → steps)
├── pkg/ # 可复用内聚包:asr / llm / capcutmate / storage / media ...
├── scheduler/ # 独立于业务 Worker 的定时任务
├── migrator/ # 建表(仅 envinit 调用)
└── seeder/ # 种子数据
几个贯穿全书的架构决策值得先记住:
- API 与 Worker 同进程:
cmd/webserver/main.go在启动 HTTP 服务的同时Start()四个 Worker,部署上只需一个容器。并发度与孤儿回收阈值由WorkerConfig控制。 - 数据库即任务队列:没有引入消息队列,Task 表 + 乐观锁(version CAS)+ 定时轮询(3 秒)完成多实例安全的任务分发。这是整个系统最值得学习的简化取舍。
- 快照式冗余:Task 创建时把画布尺寸、live_url、live_name 从关联表复制到本行,列表查询零 JOIN;代价是关联数据变更后任务显示的是创建时快照。
- 外部依赖全部接口化:LLM(
LLMChatClient)、裁剪(VideoSegmentCutter)、上传(ObjectUploader)等在依赖处定义为小接口,配合大量*_test.go实现高覆盖单测。
前端是独立的 React 18 + Vite + Ant Design 5 工程,通过 /openapi/live-mixer/v1/* 与后端通信,详见第 47~49 章。
📌 设计决策
- 选择「DB 任务队列」而非 Redis/MQ:依赖最小化,多实例扩展仍安全;代价是轮询延迟(≤3 秒)与数据库写放大,对本系统任务量级完全可接受。
- 选择「同进程 Worker」而非独立 worker 容器:运维简单;若未来需要独立扩缩容,只需把
main.go中 Worker 启动段拆为另一个 cmd。 - 大量业务字段(clips、asr_paragraphs、topics)用 PostgreSQL JSONB + GORM serializer 存储,避免为嵌套结构建关联表。
代码示例
启动入口清晰展示了「一个进程,两类职责」的装配方式:
// cmd/webserver/main.go(节选)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
asrWorker.Start(ctx) // ASR 转写 Worker
aiSliceWorker.Start(ctx) // AI 切片 Worker
draftWorker.Start(ctx) // 剪映草稿 Worker
aiSliceDraftWorker.Start(ctx) // 一键成片 Worker
sched := scheduler.New(logger)
sched.Register(scheduler.Job{
/* staging 清理 */ })
sched.Start(ctx)
srv := &http.Server{
Addr: cfg.Server.Addr(), Handler: r}
go func() {
_ = srv.ListenAndServe() }()
<-ctx.Done() // 优雅退出:先停 HTTP,再随 ctx 取消停 Worker
任务实体是理解全局的钥匙,其字段注释即业务契约:
// internal/model/task.go(节选)
type Task struct {
ID string `gorm:"primaryKey;size:36"` // UUID,仓储层生成
Type string `gorm:"size:32;index"` // ai_slice / draft / ai_slice_draft
Status string `gorm:"default:pending;index"`
Progress int16 // 0-100,客户端轮询展示
Version int64 // 乐观锁:CAS 抢占时递增
VideoProjectID *uint // 关联剪辑项目(可空)
DraftURL string // 草稿下载地址,成功后回写
VideoURL string // 成片地址,gen_video 完成后回写
ClipsTarURL string // 切片 tar 包地址
LiveURL string // 创建时快照,列表免 JOIN
}
小结
- live-mixer = 素材管理 + ASR + AI 选段 + 剪映草稿 + 成片导出的单进程 Go 服务。
- 核心架构关键词:分层、DB 任务队列(乐观锁)、JSONB、外部依赖接口化。
- 三个产物 URL(draft / video / clips_tar)都挂在 Task 上,客户端只需轮询一个接口。
思考题
- 若任务量从每天数百涨到数十万,DB 任务队列会在哪些点先到达瓶颈?
- Task 冗余快照字段与实时 JOIN 各自的适用边界在哪里?
项目信息
- GitHub仓库:Chyona/live-mixer