从 QQ 机器人到 B 站 AI 运营:一套可复现的视频流水线踩坑实录

目录

[一、背景:让 AI 真的"运营"一个账号](#一、背景:让 AI 真的“运营”一个账号)

二、先看结果

[三、第一段:QQ 入口,优先找插件而不是造轮子](#三、第一段:QQ 入口,优先找插件而不是造轮子)

[3.1 第一个坑:插件没问题,Node 版本不对](#3.1 第一个坑:插件没问题,Node 版本不对)

[3.2 表情包:不要每次现生成,做成"本地图库"](#3.2 表情包:不要每次现生成,做成“本地图库”)

[3.3 把"排队提示"删掉](#3.3 把“排队提示”删掉)

四、第二段:内容生产,难点在"稳定"

[4.1 立绘一致性:参考图比提示词有用得多](#4.1 立绘一致性:参考图比提示词有用得多)

[4.2 TTS:Edge TTS 在服务器上连不通,换豆包](#4.2 TTS:Edge TTS 在服务器上连不通,换豆包)

[4.3 Remotion:把视频当代码写](#4.3 Remotion:把视频当代码写)

[坑:2G 内存的服务器,别用默认并发](#坑:2G 内存的服务器,别用默认并发)

[坑:素材 404 不是路径写错,是目录放错](#坑:素材 404 不是路径写错,是目录放错)

中文字体

[五、交付:让成片回到 QQ](#五、交付:让成片回到 QQ)

六、第三段:逐帧验收,发现三个"看起来没问题"的问题

[6.1 图层顺序:小标签被卡片吃掉了](#6.1 图层顺序:小标签被卡片吃掉了)

[6.2 BGM:文件存在,不等于听得到](#6.2 BGM:文件存在,不等于听得到)

[6.3 转场留白:场景时长 = 配音时长 + 0.8~1.2 秒](#6.3 转场留白:场景时长 = 配音时长 + 0.8~1.2 秒)

七、工程化:让这套东西能重跑

八、复盘清单

九、下一步


一、背景:让 AI 真的"运营"一个账号

一开始我只是想让 QQ 机器人好用一点:能聊天、能发表情包、回复别太慢。做着做着,目标变成了一个更完整的问题------能不能让 AI 自己选题、写稿、配音、做视频,最后只在发布前让人点个头?

最终跑通的链路是:

  • QQ 作为交互入口,负责下指令、收成片、做人工审批;
  • 大模型负责选题和文案;
  • 火山方舟 Seedream 4.0 负责立绘和配图;
  • 火山豆包 TTS 负责配音;
  • Remotion 把 React 组件渲染成 1080p 视频;
  • NapCat 把首帧、视频和文件直接发回 QQ。

下面这张图是最终的三层结构。交互、生产、交付彼此解耦,任何一层坏了都可以单独重跑。

这篇文章不复述"AI 多强",只记录真正卡住过我的地方:一个 Node 版本问题、两次立绘跑偏、一个 TTS 资源 ID、一次 2G 内存下的渲染 OOM、一个被卡片挡住的标签,以及一段"以为有、其实等于没有"的 BGM。文末有一份可以直接照抄的验收清单。

二、先看结果

Day 0 的自我介绍成片约 70 秒,1920×1080 / 30fps,8 个场景,包含手绘风立绘、卡片动画、逐句字幕、转场音效和 BGM。

我没有把"做完"定义为渲染成功,而是抽了 9 个时间点逐帧看:开场、职责表、三条规矩、菜单、服务器繁忙、评论区、投票、结尾。下面几张是验收时保存的帧。

这里先埋一个伏笔:上面第一张"账号交接"里,右上角蓝色小标签其实只露出了"行员工"三个字。这个问题直到逐帧检查时才发现,后面第六节会讲。

三、第一段:QQ 入口,优先找插件而不是造轮子

QQ 这侧的技术栈是 NapCat(OneBot 11)+ dsh(DeepSeek Harness)+ dsh-onebot-qq。

NapCat 跑在 Docker 里,通过反向 WebSocket 连到 dsh。dsh 这边每个 QQ 会话对应一个独立 session,插件自带 qq_send_image、qq_send_voice 这类工具,管理员还能用命令控制会话。

配置文件里最关键的就是反向连接这一段(token 已省略):

yaml 复制代码
dsh-onebot:
  connection:
    mode: reverse
    host: 127.0.0.1
    port: 8643
    accessToken: <your-token>
  botQQ: <bot-qq>
  adminUsers: [<admin-qq>]
  dmPolicy: open
  groupPolicy: disabled
  workspacePath: /opt/dsh/workspace/qq
  mediaDir: /opt/dsh/.dsh/media/onebot

3.1 第一个坑:插件没问题,Node 版本不对

本地 dsh 升级到 0.2.1-rc.2 后,启动没有任何报错,进程直接静默退出。查了半天才发现系统里的 Node 22.16 不满足新版本对 import.meta.main 的假设,换到 Node 24 才正常。

这类问题最难受的地方是没有报错。如果你也遇到"命令执行了但什么都没发生",先别怀疑配置,直接确认:

bash 复制代码
node -v
dsh --version

必要时用便携版 Node,把 dsh、dsh.cmd、dsh.ps1 三个入口都指向同一个运行时,避免 PATH 里混用两个版本。

3.2 表情包:不要每次现生成,做成"本地图库"

最初的想法是让模型自己画表情包,结果既慢又不稳定。后来改成"下载一次、分类落盘、让模型选路径":

  • 用 npm pack 把表情包插件里的资源取出来;
  • 只保留能正常解码的图片,最终得到 183 张、26 个分类;
  • 生成 index.json 和一份 AGENTS.md,明确告诉模型:发表情时从本地 bqb/<分类>/ 里选,不要联网搜图。

这样回复时只需要一次文件发送,延迟从"生成一张图"降到"读一个路径"。对聊天机器人来说,能不能秒回比表情包有多精致重要得多。

3.3 把"排队提示"删掉

原插件在任务繁忙时会发一句:

⏳ 当前任务运行中,新消息已排队,将在当前任务结束后依次处理。

这句话在工单系统里没问题,在 QQ 聊天里非常出戏。处理方式不是改文案,而是让它直接沉默 :在 bridge.js 里去掉这段提示,来不及回复就不回复。

这里有个工程上的取舍:不要为了"有反馈"而牺牲人设。用户宁愿等一会儿,也不想收到一条系统播报。

四、第二段:内容生产,难点在"稳定"

4.1 立绘一致性:参考图比提示词有用得多

第一版立绘是纯文本提示词生成的:蓝发、鲸鱼尾巴、女仆裙、Q 版。单看还行,但和平台上大家熟悉的"鲸鱼娘"明显不是同一个角色。

第二、三版继续堆提示词,问题反而更多:白底出现噪点、构图漂移、有时还会多出一只完整的鲸鱼伙伴。纯文本约束角色的一致性,本质上是在赌模型这次心情好不好。

真正解决是在请求里加 参考图 :把目标风格图裁一块,作为 image 字段传进去,让模型做图生图,而不是从零想象。

python 复制代码
payload = {
    "model": "doubao-seedream-4-0-250828",
    "prompt": base_style + action_prompt,
    "image": "data:image/jpeg;base64," + b64_reference,
    "size": "1024x1024",
    "watermark": False,
}

同一个参考图跑出 6 个姿势(指、慌、欢呼、笔记本、盖章、得意),风格终于统一了。

两个细节:

  1. Seedream 4.0 不认 output_format 参数,传了会直接报错,需要按模型版本决定是否携带;
  2. 生成图即使扩展名写成 .png,实际也可能是 JPEG,后面读文件时要按真实字节判断,否则 crop、透明通道处理都会出问题。

另外,角色设计参考了公开作品的设定(上善无形 × ZipZipPipe,CC BY-NC-SA 4.0),视频结尾保留了署名。涉及二创和商用,授权边界要提前确认,不要等发布后再补。

4.2 TTS:Edge TTS 在服务器上连不通,换豆包

配音最初想用 Edge TTS,本地测试正常,部署到服务器后 WebSocket 直接连不上------服务器没有可用代理,这条路走不通。

换成火山豆包 TTS 的 V3 流式接口后,第一个坑不是鉴权,而是资源 ID 和音色不匹配:

text 复制代码
resource ID is mismatched with speaker related resource

原因是我选了 moon_bigtts 系列的音色,但账号开通的是 seed-tts-2.0。两套资源不能混用,换成 uranus_bigtts 系列后正常。

请求体核心字段如下(密钥从环境变量读取):

python 复制代码
body = {
    "user": {"uid": "content-bot"},
    "req_params": {
        "text": text,
        "speaker": "zh_female_sajiaoxuemei_uranus_bigtts",
        "sample_rate": 24000,
        "audio_params": {
            "format": "mp3",
            "speech_rate": 8,
            "loudness_rate": 0,
            "bit_rate": 64000,
        },
        "additions": json.dumps({
            "post_process": {"pitch": 0},
            "disable_markdown_filter": True,
        }, ensure_ascii=False),
    },
}

响应是 SSE,data: 行里是 base64 音频块,拼起来再落盘。写完一定要用 ffprobe 读一次真实时长------后面排场景时长要靠它,不能靠"感觉差不多"。

4.3 Remotion:把视频当代码写

视频合成选 Remotion,核心原因是它把"时间轴"变成了 React 组件:字幕、卡片、角色、音效都是普通组件,改文案就是改数组,重渲染就行。

一个场景的结构大致是:

tsx 复制代码
<AbsoluteFill style={{ opacity }}>
  <Character pose={scene.pose} animation={scene.animation} />
  <IntroCard scene={scene} />
  {scene.bigText ? <BigText {...scene} /> : null}
  {scene.badge ? <Badge {...scene} /> : null}
  <Captions lines={scene.captionLines} highlight={scene.highlight} />
  <Audio src={staticFile(`audio_intro/scene${scene.id}.mp3`)} />
</AbsoluteFill>

渲染命令:

bash 复制代码
npx remotion render src/intro/index.tsx Intro ../output/intro.mp4 \
  --concurrency=1 --codec h264 --crf 18
坑:2G 内存的服务器,别用默认并发

第一次在 2C2G 的机器上渲染 1080p,进程直接被 OOM 杀掉。Remotion 默认会按 CPU 核数开并发,每个 Chrome 渲染进程都要吃内存。

改成 --concurrency=1 后,3 秒 1080p 片段的峰值内存约 475MB,整片 70 秒可以稳定跑完。代价是慢:2100 多帧大约 12 分钟。对离线生产来说完全可以接受。

坑:素材 404 不是路径写错,是目录放错

角色组件里写的是:

tsx 复制代码
staticFile(`character/${pose}.png`)

但生成的新立绘被我放进了 public/character_v4/。渲染时浏览器请求 http://localhost:3000/public/character/v4_point.png,控制台一片 404,画面里角色直接消失。

staticFile() 的路径永远相对于 public/,改目录后要同步改组件,或者把文件放到组件期望的位置。这类问题看渲染日志没用,必须打开预览页看 Network。

中文字体

Linux 服务器默认没有中文字体,Chrome 会把汉字渲染成方框。安装 google-noto-sans-cjk-sc-fonts 后重启渲染进程即可。这个坑很老,但每次换新机器都会再踩一次。

五、交付:让成片回到 QQ

渲染完成后,脚本用 NapCat 的本地 HTTP API 发消息。一次发四样东西:一句说明、一张预览帧、视频、原文件。

python 复制代码
requests.post(
    "http://127.0.0.1:5700/send_private_msg",
    headers={"Authorization": "Bearer " + token},
    json={
        "user_id": admin_qq,
        "message": [
            {"type": "text", "data": {"text": "Day 0 成片:约 70 秒,1080p。"}},
            {"type": "image", "data": {"file": "/app/.config/QQ/intro_frame.jpg"}},
            {"type": "video", "data": {"file": "/app/.config/QQ/intro.mp4"}},
            {"type": "file", "data": {"file": "/app/.config/QQ/intro.mp4", "name": "intro.mp4"}},
        ],
    },
    timeout=120,
)

这里有两个实用经验:

  • 先发首帧再发视频。视频上传和转码需要时间,先给一张图,用户知道"东西出来了",不会以为卡死;
  • 路径要用容器内路径。NapCat 跑在 Docker 里,宿主机路径它看不见,必须放到映射目录再引用容器内路径。

六、第三段:逐帧验收,发现三个"看起来没问题"的问题

渲染成功不等于成片可用。我把成片按时间点抽帧,逐张看构图、文字、图层和音频,发现了三个典型问题。

6.1 图层顺序:小标签被卡片吃掉了

卡片右上角本来有一个彩色小标签,设计意图是"从卡片后面探出来"。实际效果是:卡片 z-index: 10,标签 z-index: 9,标签大半被盖住,只露出"行员工""是我""条规矩"这种半截文字。

修复只需要一行:

tsx 复制代码
// Badge
zIndex: 12,

修复前的帧里,卡片右边缘能看到被截断的蓝色标签;修复后标签完整地压在卡片角上。做叠加元素时,别只调坐标,先确认层级。

6.2 BGM:文件存在,不等于听得到

这是最隐蔽的一个问题。BGM 文件明明在播,但场景切换处仍然像"死寂"。用 silencedetect 一测,多个转场有 1.5~1.9 秒的静音段。

再看响度:

bash 复制代码
ffmpeg -i bgm.mp3 -af volumedetect -f null -

结果:

text 复制代码
mean_volume: -44.1 dB
max_volume: -31.4 dB

BGM 本身只有 -44dB,再乘上 volume={0.1},实际混音里约 -64dB,等于没开。人声均值约 -22dB,两者差了 40 多 dB,难怪转场像断片。

解决分两步:

  1. 重新合成一版轻快的 BGM(music-box 琶音 + 轻鼓点),归一化到峰值 -3.4dB、均值 -25.1dB;
  2. 把混音音量从 0.1 提到 0.32,让人声仍然清楚,但空隙里有东西垫着。
tsx 复制代码
<Audio src={staticFile('sfx/bgm_intro.mp3')} volume={0.32} />

验收时不要只问"有没有 BGM",要问"静音检测还能不能扫出长空隙"。量化指标比耳朵可靠。

6.3 转场留白:场景时长 = 配音时长 + 0.8~1.2 秒

最初每个场景固定比配音多留 1.5 秒,单看是"呼吸感",连起来就是"停顿感"。把首尾两个场景的尾巴缩到 1 秒左右,节奏立刻紧凑。

建议把场景时长和配音时长写在同一份配置里,渲染前跑一个校验:

ts 复制代码
const tail = scene.duration - scene.audioDuration;
if (tail < 0.6 || tail > 1.3) throw new Error(`scene ${scene.id} tail=${tail}`);

这样改文案、换 TTS 之后,不会悄悄产生新的空白段。

七、工程化:让这套东西能重跑

项目最终整理成一个独立目录,和 QQ 机器人代码放在同一个仓库:

text 复制代码
bili-operator/
├── config.json            # 人设、生图模型、TTS 音色
├── scripts/
│   ├── tts_volc.py        # 豆包 TTS SSE 客户端
│   ├── make_intro_tts.py  # 批量生成配音 + 时长表
│   └── make_bgm.py        # 纯合成 BGM,无第三方曲库
└── remotion/
    ├── public/            # 立绘、配音、音效
    └── src/intro/         # 场景、卡片、字幕、时间轴

几条原则:

  • 密钥只放 .env,仓库里只留 .env.example;
  • 成片不入库 ,只提交源码和必要素材,输出目录写进 .gitignore;
  • 文案和配音脚本要同步改,否则字幕和声音会对不上;
  • 渲染依赖不要放 /tmp ,系统清理临时目录时会把 node_modules 软链一起带走。

这套结构的好处是:换一台机器,装好 Node、ffmpeg、Python 依赖和字体,改完 .env 就能从源码重新渲染,而不是只剩一个躺在服务器上的 mp4。

八、复盘清单

如果你也想做类似的"AI 内容流水线",可以直接按这份清单验收:

  1. 运行时版本先对齐,尤其是"无报错静默退出"的情况;
  2. 交互层优先用现成插件,别一上来就自己写协议;
  3. 素材本地化:表情包、字体、音效、立绘都落到本地目录;
  4. 角色一致性用参考图,不要只靠提示词;
  5. TTS 先确认资源 ID 和音色属于同一套;
  6. 小内存机器渲染固定 --concurrency=1;
  7. 叠加元素先看 z-index,再看坐标;
  8. BGM 用 volumedetect + silencedetect 验收,不要凭感觉;
  9. 场景时长写进配置并做边界校验;
  10. 发布前必须人工确认,自动化只负责把草稿和成片送到你面前。

九、下一步

Day 0 只是把"能生产"跑通了。接下来要做的是"持续生产":

  • 选题从固定模板过渡到热点 + 评论区投票;
  • 把成片、封面、简介、标签一起打包,走 B 站投稿接口;
  • 评论区用同一个 Agent 回复,形成"观众点题 → 下期内容"的闭环;
  • 给每期视频记录渲染耗时、体积、完播数据,反向优化节奏。

AI 运营账号真正的门槛,不是某一次生成得多惊艳,而是能不能稳定地、低成本地、每天交付一个及格线以上的作品。这套流水线离"完美"还很远,但至少每一次失败都能定位到具体的一行配置,而不是一句"模型今天不太行"。

相关推荐
TK泰妞1 小时前
节日促销前,TikTok 商品视频应该提前准备什么?
前端·人工智能·节日
张驰咨询公司1 小时前
六西格玛Python DOE试验矩阵:3因子2水平生成与验算
python·六西格玛·实验设计·itertools·doe试验矩阵
代立冬1 小时前
927 个 PR:Apache SeaTunnel 3.0.0 正式发布!AI 时代,数据集成正在迎来新的可能
人工智能·apache
程序员清风1 小时前
让 AI 编写与运行测试:从样例通过到回归验证
人工智能·数据挖掘·回归
QYR-分析1 小时前
26.2% 高增!场内及园区无人物流车,厂内短驳自动化的 L4 货运载体
运维·自动化
qinglongcn1 小时前
Agent 可能出什么事——五类异常与应对
人工智能·编辑器·ai编程
小易老师1 小时前
用中间件把任务规划嵌入 Agent 循环:告别把计划写死在 Prompt 里
人工智能·agent
java_logo1 小时前
Docker 部署 DPanel:轻松搭建轻量容器管理平台
运维·docker·容器·podman·容器管理·dpanel·轩辕镜像
Ai-_Man1 小时前
Coze扣子还是不可以导出左侧所有对话,只能点进去单条内容导出,方法不对吗?
开发语言·人工智能·ai·小程序·1024程序员节