公众号运营的朋友大概率都遇到过这个尴尬:一篇文章写完了,排版也调好了,但视频号那边还空着。重新剪一条短视频,从找素材、配音、加字幕到导出,没两个小时下不来。更别提日更的账号,每天光这一项就能把人拖垮。我自己的号做了大半年,前三个月基本靠手动剪,后来实在扛不住,就开始琢磨能不能把"文章转视频"这件事拆成一条自动化流水线——用智能体把文案提取、语音合成、画面匹配、字幕烧录、视频合成这几个环节串起来,一键出片。这套东西我前后迭代了四版,踩过的坑从ffmpeg参数写错到whisper模型选型翻车,再到CDP抓取页面时被反爬拦下来,基本都经历了一遍。下面就把这套"公众号文章自动生成视频号短视频"的智能体基建方案完整拆开讲,从整体架构到每个模块的落地细节,再到实测中那些文档里不会写的坑,尽量说透。适合有一定Python基础、想给自己账号做内容自动化的运营者,也适合正在搭智能体工作流、想找一个完整实战案例的开发者。
1. 先想清楚:文章转视频到底难在哪
很多人一上来就想找个"一键转换"的工具,结果发现要么模板丑得没法看,要么配音机械得像机器人,要么字幕和语音对不上。问题的根源在于,这件事本质上不是一个动作,而是一条包含多个异构环节的流水线,每个环节的技术栈都不一样。
1.1 拆解流水线:五个环节各自的技术选型
我把整条链路拆成五步,每一步都有明确的输入输出和候选方案:
| 环节 | 输入 | 输出 | 候选方案 | 我的选择 |
|---|---|---|---|---|
| 文案提取 | 公众号文章URL | 纯文本+段落结构 | 手动复制 / CDP抓取 / 开放接口 | CDP抓取 |
| 语音合成 | 文本 | 音频文件+时间戳 | 云TTS / 本地TTS / 录音 | 云TTS |
| 画面生成 | 段落文本 | 背景视频/图片 | 素材库 / AI生图 / 纯色背景 | 素材库+AI生图 |
| 字幕对齐 | 音频+文本 | SRT字幕 | whisper / 强制对齐 / 手动 | whisper |
| 视频合成 | 音频+画面+字幕 | 成片 | ffmpeg / 剪映 / 专业软件 | ffmpeg |
这张表看着简单,但每个选择背后都有取舍。比如文案提取为什么不用手动复制?因为日更账号一天可能转3-5篇,手动复制粘贴加上清理格式,一篇就要5分钟,一个月就是两个多小时纯浪费。为什么不用开放接口?因为不是所有账号都有权限,而且接口返回的HTML结构经常变,维护成本不比CDP低。
1.2 为什么选CDP而不是直接请求接口
CDP(Chrome DevTools Protocol)这个词在热词里和SDP、DCP一起出现,很多人分不清。简单说,CDP是浏览器暴露出来的一套调试协议,你可以通过它控制一个真实的浏览器实例——打开页面、点击元素、读取DOM、执行JS。SDP是会话描述协议,用在WebRTC里协商媒体流;DCP是Digital Cinema Package,数字电影打包格式,跟这个场景没关系。
选CDP的核心原因是:它操作的是真实浏览器,页面渲染出来的DOM就是最终形态,不用去猜接口返回的数据结构。公众号文章的正文在页面里是渲染好的,用CDP直接读document.querySelector拿到的就是干净的内容。而且CDP可以复用登录态,不用处理cookie和token的过期问题。
注意:CDP控制浏览器时,建议用独立的用户数据目录,不要和你日常用的浏览器混在一起,否则登录态冲突会导致抓取失败。
1.3 智能体在这里扮演什么角色
"智能体"这个词现在被用得很泛,我这里说的智能体,是指一个能自主编排上述五个环节、根据中间结果做决策的执行体。比如whisper识别出来的文本和原文对不上时,智能体要决定是重新识别还是手动修正;比如某段文案太长,智能体要决定是拆成两屏还是一屏放完。
用平台搭建的智能体和用Python搭建的智能体有什么不一样?平台智能体(比如扣子、dify这类)优势是可视化编排、上手快,但涉及ffmpeg这种底层命令调用、whisper这种本地模型推理时,平台的能力边界就出来了。我的方案是混合:用Python写核心处理逻辑,用智能体框架做任务调度和异常决策。这样既有灵活性,又能享受智能体的编排能力。
2. 文案提取:用CDP把公众号文章"读"下来
这一步的目标很明确:给一个文章URL,拿到结构化的正文内容——标题、段落、小标题、图片位置。听起来简单,但公众号页面的DOM结构有不少坑。
2.1 CDP环境搭建与浏览器接管
先装依赖。Python这边用websocket-client或者pychrome来跟CDP通信,但我更推荐直接用playwright,它底层就是CDP,但封装了连接管理、等待、选择器等常用能力,省得自己处理WebSocket心跳。
pip install playwright playwright install chromium启动浏览器时关键参数是--remote-debugging-port,指定一个端口让CDP能连上:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch( headless=False, args=['--remote-debugging-port=9222'] ) context = browser.new_context( user_data_dir='./user_data' # 独立数据目录,保留登录态 ) page = context.new_page() page.goto('https://mp.weixin.qq.com/s/xxxxx')headless=False是有意为之。公众号对无头浏览器有一定识别,用有头模式加上真实的用户数据目录,被抓取的概率低很多。实测下来,headless模式大概十次有三四次会触发验证,有头模式基本稳定。
2.2 正文DOM的定位与清洗
公众号文章的正文容器id是js_content,这个相对稳定。但里面的结构不统一:有的段落是<p>,有的是<section>,小标题可能是<h2>也可能是加粗的<span>。我的做法是先拿整个容器的innerText,再按换行切分,然后做规则清洗。
content = page.eval_on_selector('#js_content', 'el => el.innerText') paragraphs = [p.strip() for p in content.split('\n') if p.strip()]清洗规则包括:去掉"点击上方蓝字关注我们"这类固定话术、去掉图片占位符、合并被错误换行拆开的句子。这里有个经验:公众号编辑器里手动换行和自动换行在DOM里表现不一样,手动换行会产生<br>,自动换行只是文本折行。用innerText拿到的结果里,手动换行会变成\n,自动换行不会。所以按\n切分基本能还原作者的段落意图。
2.3 图片提取与后续使用
文章里的图片要单独提取出来,因为视频里可以用作画面素材。图片的src通常在<img>标签的>images = page.eval_on_selector_all( '#js_content img', 'els => els.map(el => el.getAttribute("data-src") || el.src)' )
拿到图片URL后下载到本地,注意公众号图片有防盗链,请求时要带上Referer: https://mp.weixin.qq.com/。这一步的图片后面会用在画面生成环节,作为该段落的配图。
提示:如果文章图片很多,建议只取前5-8张,视频号短视频时长有限,画面切换太频繁反而影响观感。
3. 语音合成与whisper字幕对齐
文案拿到手之后,下一步是把它变成音频,同时拿到每个字的时间戳,这样才能做字幕对齐。这里有两个技术点:TTS选型和whisper的模型选择。
3.1 TTS选型:为什么不用本地模型
TTS方案我试过三种:本地开源模型(如VITS)、云服务TTS、以及录音。录音直接排除,日更不可能天天自己念。本地模型的问题是音质和自然度跟云服务差距明显,尤其是中文的韵律处理,本地模型经常在逗号处停顿不自然。云TTS按量付费,一篇2000字的文章大概几分钱,成本可以忽略。
选云TTS时要注意两点:一是要支持返回时间戳,有些TTS只返回音频不返回每个字的时长,那样就没法做精确字幕;二是要支持SSML标记,可以在文本里插入停顿、重音,让配音更自然。
# 伪代码示意,具体SDK按所选服务调整 result = tts_client.synthesize( text=article_text, voice='zh-CN-female', speed=1.0, return_timestamps=True ) audio_bytes = result.audio word_timestamps = result.timestamps # [(word, start_ms, end_ms), ...]3.2 whisper模型选择:small还是medium
热词里出现了"potplayer whisper small 模型"和"whisper jax",说明大家对whisper的模型规格很关注。whisper有tiny、base、small、medium、large五个规格。我的实测结论是:中文短视频字幕场景,small是性价比最高的选择。
| 模型 | 参数量 | 中文准确率 | 推理速度(CPU) | 显存占用(GPU) |
|---|---|---|---|---|
| tiny | 39M | 较差 | 极快 | 极低 |
| base | 74M | 一般 | 快 | 低 |
| small | 244M | 良好 | 中等 | 约2GB |
| medium | 769M | 优秀 | 慢 | 约5GB |
| large | 1550M | 最佳 | 很慢 | 约10GB |
small在中文上的准确率已经够用,尤其是配合TTS返回的时间戳做交叉验证时,错误率可以压得很低。medium和large虽然更准,但推理时间成倍增加,日更场景下不划算。如果你有GPU,small基本是秒级出结果;纯CPU的话,2000字音频大概需要30-60秒,也能接受。
3.3 时间戳对齐的两种策略
策略一:直接用TTS返回的时间戳。优点是精确,缺点是有些TTS的时间戳粒度是词级别,不是字级别,做逐字字幕时会有偏差。
策略二:用whisper重新识别音频,拿到字级别时间戳。优点是粒度细,缺点是whisper本身有识别错误,可能把"智能体"识别成"智能题"。
我的做法是两者结合:以TTS时间戳为主,用whisper的结果做校验。如果某个词的时间戳和whisper识别的边界差异超过200ms,就以whisper为准,因为whisper是基于实际音频波形分析的,更能反映真实发音位置。
def align_timestamps(tts_ts, whisper_ts, threshold_ms=200): aligned = [] for word, start, end in tts_ts: # 在whisper结果里找对应词 match = find_closest(whisper_ts, word, start) if match and abs(match.start - start) > threshold_ms: aligned.append((word, match.start, match.end)) else: aligned.append((word, start, end)) return aligned4. 画面生成:让视频不只是一张静态图
如果整条视频就是一张背景图加字幕,那跟PPT没区别。视频号短视频要有点"动"的感觉,画面至少要有变化。我的方案是:每个段落配一张图或一段短视频,段落切换时画面跟着切。
4.1 素材来源的优先级
素材来源按优先级排:文章自带图片 > 本地素材库 > AI生图 > 纯色/渐变背景。文章自带图片最相关,直接用;本地素材库是平时积累的,按标签检索;AI生图适合文章没有配图的情况,但生成速度慢,一篇文可能要多等几分钟;纯色背景是兜底方案,至少保证视频能出来。
AI生图这块,我用的是文生图模型,prompt的构造有个技巧:不要直接拿段落原文去生成,那样出来的图很抽象。要把段落提炼成3-5个关键词,再加上风格限定词。比如"智能体开发"这段,prompt写成"futuristic AI agent workflow, blue tone, clean tech style, no text"。
4.2 画面与音频的时长匹配
这是最容易出问题的地方。音频总时长是固定的,比如3分20秒,你有8个段落,每个段落的音频时长不一样,画面要跟着音频走,不能平均分配。
# 根据每个段落的音频时长,计算画面切换点 segments = [] current_time = 0 for para, audio_duration in zip(paragraphs, durations): segments.append({ 'text': para, 'start': current_time, 'end': current_time + audio_duration, 'image': get_image_for(para) }) current_time += audio_duration如果某个段落特别长(比如超过30秒),建议拆成两个画面,中间加一个转场,避免观众视觉疲劳。转场用ffmpeg的xfade滤镜,0.5秒的淡入淡出就够了,太长会显得拖沓。
4.3 竖屏适配:1080x1920的构图要点
视频号是竖屏,分辨率1080x1920。文章配图通常是横屏,直接放进去会有大片黑边。处理方式有两种:一是模糊背景填充,把原图放大模糊后作为背景,原图居中;二是裁剪,但会损失内容。
我推荐模糊背景填充,ffmpeg命令大概是这样:
ffmpeg -i input.jpg -filter_complex " [0:v]scale=1080:1920:force_original_aspect_ratio=increase, crop=1080:1920,boxblur=20:5[bg]; [0:v]scale=1080:-1[fg]; [bg][fg]overlay=(W-w)/2:(H-h)/2 " output.jpg这段命令先把图片放大裁切到1080x1920做背景并模糊,再把原图按宽度缩放后居中叠加。出来的效果就是上下模糊、中间清晰,视觉上比黑边舒服很多。
5. ffmpeg合成:参数、性能与那些坑
所有素材准备好之后,最后一步是用ffmpeg把它们合成一条完整的视频。这一步是整条流水线里最容易出问题的,参数写错一个字符,可能就要重跑。
5.1 基础合成命令拆解
一条典型的合成命令包含:视频输入(图片序列或视频片段)、音频输入、字幕文件、输出参数。
ffmpeg -y \ -f concat -safe 0 -i images.txt \ -i audio.mp3 \ -vf "subtitles=subtitle.srt:force_style='FontSize=18,PrimaryColour=&HFFFFFF&,OutlineColour=&H000000&,Outline=2'" \ -c:v libx264 -preset medium -crf 23 \ -c:a aac -b:a 128k \ -pix_fmt yuv420p \ -r 30 -s 1080x1920 \ -shortest \ output.mp4逐个参数说:-f concat是拼接多个图片/视频片段,images.txt里列出每个片段的路径和时长;-vf subtitles是烧录字幕,force_style控制字体大小、颜色、描边;-c:v libx264是H.264编码,兼容性最好;-crf 23是质量参数,18-28之间,23是平衡点;-pix_fmt yuv420p是像素格式,不指定的话某些播放器会花屏;-shortest是音频结束后自动停止,避免视频比音频长。
5.2 字幕烧录的字体与样式坑
字幕烧录最常见的坑是字体。ffmpeg的subtitles滤镜依赖libass,libass需要系统里有对应字体。如果你在服务器上跑,服务器可能没装中文字体,烧出来的字幕全是方块。
解决办法是在force_style里指定字体名,并确保系统里有这个字体:
force_style='FontName=Noto Sans CJK SC,FontSize=18,...'Linux上装字体:apt install fonts-noto-cjk。Windows上一般有微软雅黑,用FontName=Microsoft YaHei。
另一个坑是字幕位置。竖屏视频字幕默认在底部,但视频号底部有交互按钮,字幕会被挡住。建议把字幕往上移,用MarginV参数控制:
force_style='...,MarginV=200'200的意思是距底部200像素,具体数值根据你的画面构图调。
5.3 性能优化:为什么你的合成那么慢
ffmpeg合成慢,通常是三个原因:编码器没选对、preset太慢、或者用了CPU软编。
编码器优先用硬件编码。NVIDIA显卡用h264_nvenc,Intel核显用h264_qsv,Mac用h264_videotoolbox。硬件编码比软编快3-5倍,画质差距在短视频场景下基本看不出来。
# NVIDIA硬件编码 -c:v h264_nvenc -preset p4 -cq 23preset从快到慢有p1到p7,p4是平衡点。软编的preset从ultrafast到veryslow,medium是平衡点,但如果你赶时间,用fast也能接受。
还有一个容易忽略的点:图片序列合成时,如果每张图都是1080x1920的大图,ffmpeg要反复解码,很慢。建议提前把图片统一缩放到目标分辨率,合成时就不用再缩放。
注意:热词里提到"ffmpeg推流到srs存在延迟",那是直播推流场景的问题,跟本文的本地合成不是一回事。推流延迟主要跟GOP大小、缓冲策略有关,本地合成不涉及。
5.4 GPL和LGPL的区别对项目的影响
热词里有"ffmpeg gpl和lgpl有什么区别",这个问题在商业项目里很关键。ffmpeg默认编译是LGPL协议,但如果你启用了--enable-gpl,就会引入GPL组件(比如x264、x265),整个ffmpeg就变成GPL。GPL要求你的项目也开源,LGPL则允许闭源调用。
如果你只是自己用或者开源项目,GPL没问题。如果是商业闭源产品,要么用LGPL版本(但功能受限,比如没有x264),要么用硬件编码器(nvenc是LGPL兼容的),要么通过命令行调用ffmpeg可执行文件(而不是链接库),这样通常被认为是"聚合"而非"衍生",可以规避GPL传染。
6. 智能体编排:把五个环节串成一条流水线
前面五章讲的是每个环节怎么做,这一章讲怎么把它们串起来,以及智能体在其中的决策逻辑。
6.1 任务状态机设计
整条流水线是一个状态机,每个环节是一个状态,状态之间有条件跳转。
[开始] -> [抓取文章] -> [提取文案] -> [TTS合成] -> [whisper对齐] | v [结束] <- [ffmpeg合成] <- [画面生成] <- [字幕生成]每个状态有成功、失败、重试三种出口。失败时智能体要根据错误类型决定:是重试、跳过、还是终止。比如TTS服务超时,重试两次还失败,就降级到备用TTS;比如whisper识别置信度低于阈值,就标记该段落需要人工复核,但不阻塞整体流程。
6.2 异常决策的几条规则
规则一:任何环节连续失败3次,终止流程并告警。避免无限重试浪费资源。
规则二:非关键环节失败可降级。画面生成失败可以用纯色背景兜底,字幕对齐失败可以用TTS时间戳兜底,但音频合成失败必须终止,因为没有音频就没有视频。
规则三:中间产物全部落盘。每个环节的输出都存到本地,这样失败重跑时不用从头开始。比如TTS已经合成好了,whisper失败了,重跑时直接读音频文件,不用重新TTS。
def run_pipeline(article_url): state = load_state(article_url) # 读取已有进度 if not state.get('article_text'): state['article_text'] = fetch_article(article_url) save_state(state) if not state.get('audio_path'): state['audio_path'] = tts_synthesize(state['article_text']) save_state(state) # ... 后续环节同理6.3 用Python智能体还是平台智能体
回到热词里那个问题:"利用平台构建的智能体与用python构建的智能体有什么不一样?"我的实际体会是:
平台智能体(扣子、dify等)适合做"编排层"——定义流程、配置条件分支、接入API。它的优势是可视化、改起来快、不用写调度代码。但它的劣势是执行环境受限,你没法在平台里直接跑ffmpeg命令,也没法加载whisper模型。
Python智能体适合做"执行层"——实际调用ffmpeg、跑whisper推理、处理文件IO。它的优势是灵活、可控、能调底层。劣势是调度逻辑要自己写,异常处理要自己兜。
我的方案是两者结合:平台智能体负责接收任务、编排流程、记录状态;Python服务负责执行具体环节,通过HTTP接口被平台调用。这样既享受了平台的可视化编排,又保留了底层的执行能力。
7. 实测中的那些坑与经验
这一章不讲理论,只讲我实际踩过的坑,以及怎么解决的。
7.1 whisper识别中文标点丢失
whisper识别中文时,标点符号经常丢失或者错乱。比如"你好,世界"可能识别成"你好世界"。这会导致字幕没有停顿感。
解决办法是在whisper输出后,用原文的标点做校正。因为原文是我们自己提供的,标点是准确的。把whisper识别的文本和原文做对齐,标点以原文为准。
import difflib def restore_punctuation(whisper_text, original_text): # 用原文的标点替换whisper结果中的对应位置 matcher = difflib.SequenceMatcher(None, whisper_text, original_text) result = list(whisper_text) for tag, i1, i2, j1, j2 in matcher.get_opcodes(): if tag == 'equal': for k in range(i2 - i1): if original_text[j1 + k] in ',。!?;:': result[i1 + k] = original_text[j1 + k] return ''.join(result)7.2 ffmpeg concat拼接时的时长精度问题
用concat拼接图片时,每张图的时长是写在images.txt里的,比如file 'img1.jpg'下一行duration 5.2。但ffmpeg对duration的精度处理有问题,5.2可能被当成5.199或者5.201,累积下来会导致音画不同步。
解决办法是用高精度的时间值,并且最后加一个duration为0的收尾项。或者干脆不用concat,用-loop 1 -t 5.2 -i img1.jpg的方式逐个输入,用concat滤镜拼接。后者更精确,但命令更长。
ffmpeg \ -loop 1 -t 5.2 -i img1.jpg \ -loop 1 -t 3.8 -i img2.jpg \ -filter_complex "[0:v][1:v]concat=n=2:v=1:a=0[v]" \ -map "[v]" ...7.3 CDP抓取时的登录态失效
CDP复用浏览器登录态时,如果公众号登录过期了,抓取会拿到登录页而不是文章页。智能体要能识别这种情况:检查页面标题是否包含"登录"或者正文容器是否存在。
if page.query_selector('#js_content') is None: raise LoginExpiredError('公众号登录态已失效,请重新登录')处理方式是弹出一个有头浏览器让用户手动扫码,登录后再继续。这个环节没法完全自动化,因为扫码需要人参与。但可以做成"登录一次,管用很久",因为公众号的登录态有效期比较长。
7.4 视频号上传的格式要求
最后一步是把生成的mp4上传到视频号。视频号对格式有要求:MP4容器、H.264编码、AAC音频、分辨率建议1080x1920、码率建议4-8Mbps、文件大小不超过1GB。ffmpeg的默认输出基本满足,但码率要控制一下。
-b:v 6M -maxrate 8M -bufsize 12M-b:v 6M是目标码率6Mbps,-maxrate 8M是最大码率,-bufsize 12M是缓冲区大小。这样出来的文件大小可控,画质也够。
7.5 整条流水线的时间成本
实测数据:一篇2000字的文章,从抓取到出片,全流程大概需要3-5分钟。其中TTS合成约30秒,whisper对齐约40秒(GPU)或90秒(CPU),画面生成约60秒(含AI生图),ffmpeg合成约40秒。如果跳过AI生图用本地素材,总时间可以压到2分钟以内。
对比手动剪辑:同样一篇文章,手动找素材、配音、加字幕、导出,熟练工也要40-60分钟。自动化之后效率提升20倍以上,而且可以批量跑——晚上挂机跑10篇,第二天直接上传。
8. 后续可以扩展的方向
这套流水线跑通之后,我陆续加了一些扩展功能,这里简单提几个方向,给想继续深挖的朋友参考。
第一个方向是批量处理。把文章URL列表丢进去,智能体排队处理,每篇之间加个间隔避免触发风控。这个改动不大,主要是状态管理要支持多任务。
第二个方向是多平台适配。视频号是竖屏1080x1920,但抖音、快手、B站的规格不一样。可以在ffmpeg合成环节加一个参数,根据目标平台切换分辨率和码率。
第三个方向是封面自动生成。视频号需要封面图,可以从文章里选一张图,加上标题文字,用ffmpeg或者PIL合成。标题文字的字体、颜色、位置可以做成模板。
第四个方向是数据回流。视频发布后,把播放量、完播率、点赞数抓回来,和文章类型、视频时长、画面风格做关联分析,找出什么样的文章适合转视频、什么样的画面风格完播率高。这个需要额外的数据采集和分析模块,但价值很大。
这套东西我从零搭到现在,最大的体会是:不要追求一步到位。先把最核心的"文章->音频->字幕->视频"跑通,哪怕画面就是纯色背景,先让流程能出片。然后再逐步优化每个环节——换更好的TTS、加画面、加转场、加封面。每优化一个环节,视频质量就提升一点,但流程始终是通的。最怕的是一开始就想做完美,结果卡在某个环节上,整条流水线跑不起来。