素材、切片工程与投稿
工作流程为 录播 → 素材 → 切片 → 投稿:
- 素材:从录播中截取的一段视频文件,只提供简单预览。生成时不压制弹幕,但会把对应时间段的弹幕截取保存为素材旁的弹幕文件,录播被清理后弹幕仍然可用。
- 切片:一个简易剪辑工程,由一个或多个素材按顺序组成,在工程中完成裁剪、分割、排序、字幕和弹幕压制,可导出为文件。
- 投稿:独立的功能页,把素材、切片或任意视频文件发布到 B 站,并保留草稿和投稿记录。
数据模型
素材(videos 表)
为了兼容 AI 助手、MCP 与 Webhook,素材沿用 videos 表与 VideoRow。迁移 18 新增:
| 字段 | 说明 |
|---|---|
live_id | 来源录播,导入的视频为空 |
source | 来源信息 JSON:{"ranges":[{"start","end"}],"local_offset":0},时间相对录播播放列表起点 |
duration | 精确时长(秒),旧数据为 0,读取时按需用 ffprobe 补齐 |
素材的旁路文件与视频文件同名:
| 文件 | 说明 |
|---|---|
<stem>.jpg | 缩略图 |
<stem>.opus | 波形用音频样本 |
<stem>.srt | AI 识别字幕缓存(素材时间) |
<stem>.danmu.json | 截取的弹幕,ts 为相对素材起点的毫秒 |
<stem>.peaks.json | 时间轴波形峰值缓存 |
从录播生成素材时,每个选区生成一个素材(直接复制视频流,不重新编码),并可选同时创建包含这些素材的切片工程。
切片(projects 表)
| 字段 | 说明 |
|---|---|
title / note | 标题与备注 |
room_id / platform | 所属直播间,用于素材库筛选和投稿配置 |
cover | 切片封面,相对输出目录(projects/<id>/cover.jpg),为空时使用第一个素材的缩略图;也是投稿的默认封面 |
timeline | 时间轴 JSON,见下文 |
profile | 旧数据迁移来的投稿信息 JSON,为该切片新建投稿时作为初始内容 |
status / bvid | 最近一次投稿的状态与 BV 号,由投稿成功时回写 |
工程文件统一放在 <output>/projects/<id>/,删除工程时一并删除。
时间轴
json
{
"version": 1,
"clips": [{ "id": "c1", "material_id": 12, "start": 3.2, "end": 40.0 }],
"subtitles": [{ "id": "s1", "clip_id": "c1", "start": 5.0, "end": 7.5, "text": "..." }],
"subtitle_style": { "fontName": "Arial", "...": "..." },
"danmu": false
}clips为主轨道,按顺序首尾相接播放(磁吸主轨),start/end是素材内的入点与出点。片段在工程中的起点由前面片段的时长累加得到。- 字幕锚定在片段上,时间是素材时间。片段被移动、裁剪时字幕自动跟随画面;超出片段可见范围的部分被隐藏,重新拉长片段后会恢复。分割片段时,跨越分割点的字幕会复制到两侧。
danmu是工程级设置:为true时,导出和投稿生成视频时会把各片段可见范围内的素材弹幕映射到工程时间后压制。
预览与导出
- 预览:两个
<video>交替播放,在切点处无缝切换;对外暴露一个以工程时间为准的虚拟媒体时钟,libass 字幕预览直接绑定该时钟。预览依赖普通的 HTTP 静态文件,桌面端与 Docker 网页端一致。 - 生成视频:后端把时间轴编译为一条 FFmpeg 命令:每个片段用
-ss/-t作为独立输入,统一缩放到 1920×1080、统一帧率与音频格式后concat,再依次叠加字幕(前端生成的 ASS,与预览完全一致)和弹幕(ASS + 表情图片叠加),经过任务队列执行,支持进度与取消。先写入目标旁的<名称>.part.mp4,完成后再替换目标文件,失败不会破坏已有文件。 - 导出:
export_project把时间轴生成到用户在保存对话框中选择的文件;Web 端不传路径,生成到<cache>/exports/<id>/export.mp4后由浏览器下载。工程不记录导出文件,每次导出都按当前时间轴重新生成。 - 只有一个完整素材、没有字幕和弹幕的工程(
needs_render为false)不需要生成:导出时复制素材文件,投稿时直接使用素材文件。
投稿(uploads 表)
迁移 19 新增,每条记录是一次投稿:未发布时是草稿,发布后作为历史保留。
| 字段 | 说明 |
|---|---|
source / source_id | 来源:material(素材 id)、project(切片 id)或 file(source_id 为 0) |
file | file 来源的视频绝对路径,投稿时直接读取,不复制 |
room_id | 来源所属直播间,用于沿用同一直播间上次的投稿信息 |
uid | 投稿账号 |
cover | 自定义封面,相对输出目录(uploads/<id>/ 下,每次保存写入新文件),为空时使用来源的封面;视频文件在新建时截取一帧作为封面 |
profile | 投稿信息 JSON(B 站 Profile),编辑时自动保存 |
danmu | 素材投稿是否压制弹幕(迁移 21);切片的弹幕设置保存在切片自己的时间轴中,投稿页修改的就是它 |
status | draft / exporting / posting / posted / failed;应用重启时进行中的记录标记为失败 |
message / bvid | 失败原因 / 投稿成功的 BV 号 |
post_upload 校验后立即返回,投稿在后台进行(不占用任务队列,以免阻塞导出),任务类型为 upload:
- 按来源确定视频文件:素材使用素材文件,开启弹幕且素材保存了弹幕时,按只含整个素材的时间轴生成压制弹幕的视频(任务类型
render_material_danmu,同样缓存在uploads/<id>/);视频文件使用file;切片按当前时间轴以<event_id>-export为任务 id 加入导出队列,生成到uploads/<id>/render_<hash>.mp4(字幕 ASS 由前端按切片时间轴生成后传入),等待完成后使用。哈希包含时间轴、字幕与弹幕设置,失败重试时时间轴未变就直接复用,投稿成功后删除 - 上传视频与封面并提交稿件,成功后回写记录的
bvid,并在素材或切片上记录 BV 号
投稿记录的文件统一放在 <output>/uploads/<id>/,删除记录时一并删除。AI 助手的 upload_procedure 投稿成功后也会写入一条素材投稿记录。
接口
| 命令 | 说明 |
|---|---|
generate_materials | 从录播选区生成素材(每个选区一个),可选同时创建工程 |
get_material_usage / get_material_peaks / get_material_danmu | 素材被哪些工程使用 / 波形峰值 / 截取的弹幕 |
get_projects / get_project / create_project / delete_project | 工程增删查 |
update_project_info / update_project_timeline | 工程修改:标题与备注可分别保存;保存时间轴时附带编辑器上次见到的已保存时间轴,后端据此合并期间自己的修改(弹幕设置、删除素材移除的片段、识别回填的字幕),编辑窗口收到变更时按同样规则合并 |
update_project_danmu | 切换工程的弹幕设置(编辑窗口与投稿页共用) |
export_project | 把工程导出到指定文件(Web 端导出到缓存供下载),返回导出任务 |
get_uploads / get_upload / create_upload / delete_upload | 投稿记录增删查,列表附带来源名称、是否需要生成视频与封面 |
update_upload / update_upload_cover | 保存草稿的账号与投稿信息 / 封面 |
update_upload_danmu | 切换压制弹幕:素材写入投稿记录,切片直接修改切片的时间轴 |
post_upload | 在后台投稿,切片先生成视频 |
request_upload | 切片编辑窗口请求主窗口为素材或切片新建投稿(upload-requested 事件),并把主窗口带到前台 |
open_project / open_clip | 打开工程编辑窗口 / 素材预览窗口 |
clip_range 仍然可用(AI 助手与 MCP 使用),它把所有选区合并为一个素材,同样不再压制弹幕。
旧数据迁移
迁移 18 为已有的每条 videos 记录标记 needs_project,启动时为它们各创建一个只含该素材的工程:标题、备注、投稿状态与 BV 号迁到工程,素材的 .srt 字幕导入为工程字幕;已投稿的同时生成一条投稿记录。迁移 19 为当时已有 BV 号的工程生成投稿记录;迁移 20 删除工程的 output / output_hash 列,之前导出到 projects/<id>/ 的文件保留,随工程一起删除。