目录
[一、背景:让 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 个姿势(指、慌、欢呼、笔记本、盖章、得意),风格终于统一了。

两个细节:
- Seedream 4.0 不认
output_format参数,传了会直接报错,需要按模型版本决定是否携带; - 生成图即使扩展名写成
.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,难怪转场像断片。
解决分两步:
- 重新合成一版轻快的 BGM(music-box 琶音 + 轻鼓点),归一化到峰值 -3.4dB、均值 -25.1dB;
- 把混音音量从
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 内容流水线",可以直接按这份清单验收:
- 运行时版本先对齐,尤其是"无报错静默退出"的情况;
- 交互层优先用现成插件,别一上来就自己写协议;
- 素材本地化:表情包、字体、音效、立绘都落到本地目录;
- 角色一致性用参考图,不要只靠提示词;
- TTS 先确认资源 ID 和音色属于同一套;
- 小内存机器渲染固定
--concurrency=1; - 叠加元素先看 z-index,再看坐标;
- BGM 用
volumedetect+silencedetect验收,不要凭感觉; - 场景时长写进配置并做边界校验;
- 发布前必须人工确认,自动化只负责把草稿和成片送到你面前。
九、下一步
Day 0 只是把"能生产"跑通了。接下来要做的是"持续生产":
- 选题从固定模板过渡到热点 + 评论区投票;
- 把成片、封面、简介、标签一起打包,走 B 站投稿接口;
- 评论区用同一个 Agent 回复,形成"观众点题 → 下期内容"的闭环;
- 给每期视频记录渲染耗时、体积、完播数据,反向优化节奏。
AI 运营账号真正的门槛,不是某一次生成得多惊艳,而是能不能稳定地、低成本地、每天交付一个及格线以上的作品。这套流水线离"完美"还很远,但至少每一次失败都能定位到具体的一行配置,而不是一句"模型今天不太行"。