☰
从零搭建AI填词PV自动化管线:Codex+DeepSeek+FFmpeg实战
2026/10/10 6:33:19 网站建设 项目流程

如果你最近也刷到了“用 Codex 生成脚本、再让 DeepSeek 写词、最后自动合成 PV”这类玩法,大概率会在评论区看到两种声音:一种是“这不就是套了个壳”,另一种是“这套流水线真能省下大半天”。两边说的其实都对,区别只在于你站在哪一种使用姿势里。如果只是把 AI 当聊天框,临时问一句、抄一段,那它确实只是个“壳”;但如果你把提示词、模型调度、输出解析、字幕和视频渲染全部固定成一个可复用的自动化管道,那你手里这个东西就值得叫 harness——它不是一个工具,而是一套围绕大模型搭建的生产系统。

这篇文章要讲的,正是怎么从零搭一个“填词 PV 自动化管线”:让 Codex 类模型负责工程脚手架和脚本生成,让 DeepSeek 负责歌词续写、断句、分镜描述和字幕文本,再用 FFmpeg 把结果拼成带字幕的视频。标题里那种“特供版”玩法,本质上就是给这套管线预设好固定的角色设定、语气偏好和内容边界,然后机器按这个配置批量出片。读完你至少能跑通一条最小链路,并且知道常见的坑藏在哪。

1. 为什么做 AI 内容生成需要一套 harness

先说一个很多人踩过的误区:以为大模型能直接输出成品视频。实际上,大模型能稳定输出的只有文本,而一段 P V 需要的是五类内容同时对齐——歌词每句话的起止时间、断句位置、对应画面描述、字幕样式、音频长度。靠人手动把这些粘到一起,哪怕只做一分钟的片段,也要反复调整十几次。

所谓 harness,翻译过来就是“线束”或“控制装置”。在 AI 工程领域,它通常指把模型调用、提示词模板、上下文组装、输出解析、后处理脚本、缓存和日志全部封装起来的那层代码。你可以把它理解成一条组装流水线:原料是角色设定和选题方向,中间经过若干个工位,每个工位调用不同模型或脚本,最终出来的是可以直接发布的 mp4。

过去你写一段歌词,要在聊天窗口里反复调整语气,然后复制到剪映里手动断句,再一句句配字幕。这个过程的问题不是“AI 能力不够”,而是“信息在人和工具之间来回搬运”,每搬运一次就丢失一点结构。采用 harness 之后,歌词、断句、时间码、分镜说明都是结构化数据,模型输出完直接被下一道工序消费,人工介入点从“每一步”缩到“只在最终预览时检查”。

所以它真正降低的并不是“词写得好不好”这件事,而是“从文本到成片”的工程成本。对于做短视频、二创、教育类解说、甚至是企业内部培训视频的人来说,这套思路比单次对话式的 AI 玩法实用得多。

2. Codex 与 DeepSeek 的角色分工

这个项目名字里同时出现了 Codex 和 DeepSeek,核心不是“二选一”,而是让它们各干各擅长的事。

Codex 类模型被训练时重点解决的是“把一个自然语言任务转成可执行代码”。比如你要一个脚本,把 JSON 歌词转成 srt 字幕,把图片目录拼成视频,这种需求让对话式问答去做会非常累,因为追问和纠错会占用大量轮次。但用 Codex 类工具去生成、修改、运行脚本则顺畅得多。在本文的场景里,它主要负责:

  • 搭建目录骨架和模块拆分;
  • 生成 JSON 转 SRT 的转换脚本;
  • 判断 FFmpeg 命令该怎么组合,并解决跑不通的报错;
  • 帮你把重复的“手工操作”固化成函数。

DeepSeek 的优势则在于中文语境理解、续写风格稳定性、长文本处理的性价比。它适合做内容层的工作:给定主题和角色设定,续写歌词、给每一句配画面描述、把字幕断成适合阅读的短句。如果试过用代码模型直接填词,会感觉句子结构太硬,中文音律感明显不行;反过来让写词模型去写 Python 脚本,也容易在工程细节上翻车。两者搭配,是把“过程正确”和“表达自然”分开交给不同模型。

需要说明的是,“Codex”在不同时期的产品形态不太一样,本文不绑定具体版本,只使用它的核心能力定位:面向代码生成与执行的一类模型工具。DeepSeek 也一样,你可以把它替换成任何中文长文本能力强的模型,harness 的框架不会因此改变。

3. 一次 PV 生成包含哪些数据流

在写代码之前,先画清楚数据流。这个项目里,最核心的不是模型,而是“中间产物”。

一次完整的填词 PV 生成,至少要经过下面几层数据转换:

  1. 配置层:输入角色设定、主题词、BGM 时长、字幕风格、敏感词边界。
  2. 填词层:DeepSeek 输出结构化的歌词 JSON,包含句子、断句、情绪标记。
  3. 分镜层:模型根据歌词生成画面描述,按句拆分。
  4. 时间轴层:根据音频时长估算每句字幕的出现时间。
  5. 渲染层:FFmpeg 把背景画面、字幕、音频合成最终视频。
  6. 检查层:人眼确认字幕是否遮脸、节奏是否对。

这里最容易被忽略的是第 4 层。很多新手以为字幕时间码应该由模型生成,但模型并不真的知道音频文件多长。更稳妥的方式是:先读音频总时长,再按歌词句子数量或朗读速度做比例分配,最后人工微调。这样既不会出现模型瞎编时间码,也不会因为音频换了导致整套字幕作废。

数据流设计得好不好,直接决定后面迭代快不快。如果每一步都输出可读的中间文件(lyrics.json、plot.json、timeline.json、subtitle.srt),那么你的每次调整都只需要改输入配置,而不是从头再让模型生成一遍。

4. 环境准备与基础配置

开始动手前,需要准备好运行环境。这个项目并不需要很高的机器配置,核心依赖集中在 Python、FFmpeg 以及两个模型的 API 访问权限。

推荐环境:

  • 操作系统:Windows 10/11、macOS、Linux 均可;
  • Python 3.10 或更高版本;
  • FFmpeg 可执行文件,并且已经加入系统 PATH;
  • 支持 OpenAI 兼容接口的大模型服务,本文示例使用 DeepSeek;
  • 如果你有 Codex 类 CLI 工具或同类代码模型访问入口,也可以用于生成工程脚本。

如果还不确定自己的模型访问方式,建议先用一个最小脚本验证 API 连通性,再往下搭管线。很多问题的根源其实都出在“以为 API 配好了,实际根本没连上”。

下面是一个简单的目录结构建议:

pv-harness/ ├── configs/ │ └── project.yaml ├── prompts/ │ ├── lyric_writer.md │ └── storyboard.md ├── scripts/ │ ├── llm_client.py │ ├── json_to_srt.py │ └── render_video.py ├── outputs/ │ ├── lyrics.json │ ├── storyboard.json │ ├── subtitle.srt │ └── final.mp4 └── requirements.txt

如果你用的是 Codex 类工具,可以让它先帮你构建这个骨架,再逐步填充脚本。如果完全手写,先从下面的 requirements.txt 开始。

openai>=1.0.0 pyyaml>=6.0

依赖只要两个,一个是访问模型的 SDK,一个是读取 yaml 配置的库。FFmpeg 不在 Python 依赖里,因为我们是直接调用命令行。

5. 提示词模板与结构化输出设计

很多大模型生成内容不稳定,不是因为模型变笨了,而是提示词既没有规定输出结构,也没有给出边界约束。在这个项目里,填词环节的提示词模板是整个管线的地基。

先看一个可复用的歌词生成模板,存储路径为prompts/lyric_writer.md:

你是一名歌词创作者。请根据下面的主题和风格,生成一段适合演唱的中文歌词。 要求: 1. 必须输出 JSON,不要输出任何解释性文字。 2. JSON 结构固定为: { "title": "歌曲标题", "sentences": [ {"text": "第一句歌词", "emotion": "轻快", "hint": "画面:阳光下的街道"}, {"text": "第二句歌词", "emotion": "坚定", "hint": "画面:傍晚逆光剪影"} ] } 3. 每句歌词控制在 8 到 18 个字。 4. 总句数根据 BGM 时长决定,按平均每句 4 秒估算。 5. 不得包含违规内容,也不得使用任何真实人物的姓名、肖像描述。 主题:{{theme}} 风格:{{style}} BGM 时长:{{duration}} 秒

模板里用了{{theme}}、{{style}}、{{duration}}这样的占位符,实际调用时由 Python 替换。这样做的好处是,提示词和代码分离,不同的“特供版”只需要修改配置文件,不需要改代码。

分镜模板prompts/storyboard.md可以设计成接收歌词 JSON,并补充画面描述:

请根据歌词内容生成分镜描述。输入是一个句子列表,输出 JSON。 输出格式: { "shots": [ {"sentence_index": 0, "duration": 4.0, "prompt": "阳光下的街道,人物慢走,镜头跟随"}, {"sentence_index": 1, "duration": 4.5, "prompt": "傍晚逆光剪影,镜头缓慢推近"} ] } 注意:每个 shot 的画面必须与歌词情绪对应,不得出现人物特写和面部可辨识描写。

关于“特供版”要补充一点:如果你是为某个固定观众定制内容,一定要在系统提示词里明确“观众偏好”和“内容红线”。比如观众喜欢某种叙事风格,就写进模板;但涉及真实人物姓名、肖像、声音的,都应默认拒绝,因为没有授权风险太高。

6. 核心代码实现:从模型调用到字幕生成

现在进入工程实现。我们分四个模块写,先跑通最小链路,再逐步完善。

6.1 模型调用封装

创建scripts/llm_client.py,统一封装模型请求。这里以 OpenAI 兼容接口调用 DeepSeek 为例:

# 文件路径:scripts/llm_client.py import os import json from openai import OpenAI client = OpenAI( api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL", "https://api.deepseek.com"), ) def chat_json(system_prompt: str, user_prompt: str, model: str = "deepseek-chat") -> dict: resp = client.chat.completions.create( model=model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], temperature=0.8, ) content = resp.choices[0].message.content # 部分模型会输出代码块包裹的 JSON,这里做一次清洗 content = content.strip() if content.startswith("```"): content = content.strip("`") # 去掉开头可能的 json 标记 if content.startswith("json"): content = content[4:] return json.loads(content)

这段代码的关键逻辑有几处:

  • 从环境变量读取密钥,避免把密钥写进代码仓库;
  • base_url只写默认地址,实际以你使用的模型服务商为准;
  • 统一走chat_json,返回的一定是 dict。如果返回的不是 JSON,说明提示词或服务配置有问题,应该提前暴露而不是吞掉。

6.2 主流程脚本

创建scripts/run_pipeline.py,

# 文件路径:scripts/run_pipeline.py import os import json import subprocess from pathlib import Path from llm_client import chat_json BASE_DIR = Path(__file__).resolve().parent.parent OUTPUT_DIR = BASE_DIR / "outputs" PROMPTS_DIR = BASE_DIR / "prompts" OUTPUT_DIR.mkdir(exist_ok=True) # 1. 读取配置 theme = os.getenv("PV_THEME", "夏日傍晚的奔跑") style = os.getenv("PV_STYLE", "热血、青春") duration = float(os.getenv("PV_DURATION", "90")) # 2. 读取并填充提示词模板 writer_prompt = (PROMPTS_DIR / "lyric_writer.md").read_text(encoding="utf-8") writer_prompt = writer_prompt.replace("{{theme}}", theme) writer_prompt = writer_prompt.replace("{{style}}", style) writer_prompt = writer_prompt.replace("{{duration}}", str(int(duration))) # 3. 生成歌词 lyrics = chat_json( system_prompt="你是歌词创作者,只输出指定 JSON 结构。", user_prompt=writer_prompt, ) with open(OUTPUT_DIR / "lyrics.json", "w", encoding="utf-8") as f: json.dump(lyrics, f, ensure_ascii=False, indent=2) # 4. 生成分镜 storyboard_prompt = (PROMPTS_DIR / "storyboard.md").read_text(encoding="utf-8") storyboard = chat_json( system_prompt="你是分镜师,只输出指定 JSON 结构。", user_prompt=storyboard_prompt + "\n歌词数据:" + json.dumps(lyrics, ensure_ascii=False), ) with open(OUTPUT_DIR / "storyboard.json", "w", encoding="utf-8") as f: json.dump(storyboard, f, ensure_ascii=False, indent=2) print("歌词与分镜生成完成,产物已写入 outputs 目录。")

到这里,你已经完成了内容生成部分。所有中间产物都落盘,方便每一次失败时定位问题。

6.3 JSON 转 SRT 字幕

模型输出的歌词 JSON 还不能直接用于 FFmpeg,要先转成字幕文件。创建一个独立的转换脚本scripts/json_to_srt.py:

# 文件路径:scripts/json_to_srt.py import json import sys from pathlib import Path def estimate_duration(lyrics: dict, total_seconds: float) -> list[dict]: """根据音频总时长和句子数量,估算每句字幕的时间轴。""" sentences = lyrics["sentences"] avg_len = total_seconds / len(sentences) timeline = [] cursor = 0.0 for sentence in sentences: text = sentence["text"] # 保守处理:时间轴向右取整到 0.5 秒倍率 start = round(cursor, 2) end = round(cursor + avg_len, 2) timeline.append({"index": len(timeline) + 1, "start": start, "end": end, "text": text}) cursor = end return timeline def format_srt_time(seconds: float) -> str: hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) millis = int((seconds % 1) * 1000) return f"{hours:02}:{minutes:02}:{secs:02},{millis:03}" def to_srt(timeline: list[dict]) -> str: lines = [] for item in timeline: lines.append(str(item["index"])) lines.append(f"{format_srt_time(item['start'])} --> {format_srt_time(item['end'])}") lines.append(item["text"]) lines.append("") return "\n".join(lines) if __name__ == "__main__": lyrics_path = Path(sys.argv[1]) total_seconds = float(sys.argv[2]) output_path = Path(sys.argv[3]) with open(lyrics_path, encoding="utf-8") as f: lyrics_data = json.load(f) timeline = estimate_duration(lyrics_data, total_seconds) with open(output_path, "w", encoding="utf-8") as f: f.write(to_srt(timeline)) print(f"字幕已生成:{output_path}")

关于时间轴,这里用的是一个非常朴素的“总时长除以句数”算法。实际项目中,音乐有前奏、间奏、副歌,节奏密度完全不同,直接平均分配只能做初版。更精确的做法是先做音频的人声检测或手动打点,再把时间码交给模型修正,但最小链路里不必一上来就做这么重。

6.4 FFmpeg 合成最终视频

字幕文件生成后,接下来是把背景画面、字幕和音频合成视频。假设你已经有一段背景画面background.mp4和一段音频bgm.m4a,可以用下面这条 FFmpeg 命令:

ffmpeg -y \ -i background.mp4 \ -i bgm.m4a \ -vf "subtitles=outputs/subtitle.srt:force_style='FontName=Noto Sans CJK SC,FontSize=20,PrimaryColour=&HFFFFFF,OutlineColour=&H000000,Outline=2'" \ -c:v libx264 -c:a aac \ -pix_fmt yuv420p \ -shortest \ outputs/final.mp4

解释几个参数:

  • subtitles=outputs/subtitle.srt:加载字幕文件;
  • FontName=Noto Sans CJK SC:指定中文字体,避免中文显示成方框;
  • -shortest:以较短的音频或视频流为终点,适合背景视频比音频长的场景;
  • -pix_fmt yuv420p:保证视频在大多数播放器里正常显示。

在 Windows 上,字幕路径里的反斜杠可能需要转义。如果命令执行后提示找不到字幕文件,优先检查这一点。

7. 运行流程与效果验证

现在把整个流程串起来。假设项目根目录为pv-harness,按下面顺序执行:

cd pv-harness pip install -r requirements.txt export LLM_API_KEY="sk-你的密钥" export PV_THEME="夏日傍晚的奔跑" export PV_STYLE="热血、青春" export PV_DURATION="90" python scripts/run_pipeline.py python scripts/json_to_srt.py outputs/lyrics.json 90 outputs/subtitle.srt

执行完毕后,你会在 outputs 目录看到以下几个文件:

outputs/ ├── lyrics.json ├── storyboard.json ├── subtitle.srt └── final.mp4

判断是否成功,不能只看“有没有文件”,要看中间结果是否符合预期:

  • lyrics.json应该是一个标准 JSON,里面有title和sentences数组;
  • storyboard.json的shots数组长度和歌词句子数量一致;
  • subtitle.srt里的时间码是递增的,每段时间在 3 到 6 秒之间;
  • final.mp4能正常播放,中文字幕渲染正常,画面和歌词大致对应。

如果某一步失败,优先看模型返回的原始文本。很多问题并不是代码有 bug,而是模型输出的 JSON 里混进了解释性文字,导致json.loads抛异常。我们的llm_client.py里已经做了一层清理,但只覆盖了最基础的代码块包裹场景,遇到更复杂情况可以打开lyrics.json人工检查。

8. 常见问题与排查方法

这一段是实际使用中踩过最多坑的地方,整理成表格,建议收藏。

问题现象可能原因排查方式解决方案
json.loads报错模型返回内容里混入了解释文字或 Markdown 标记打印模型原始输出,查看返回内容增强提示词中的“只输出 JSON”约束,并在代码里做更强的清洗
中文字幕显示为方框系统中没有对应中文字体查看 FFmpeg 输出的字体警告安装 Noto Sans CJK SC 或指定系统已有中文字体路径
音画不同步时间轴是平均分配的,无法适配前奏/间奏播放视频对比实际收听手动调整时间码,或先做人声检测再分配
Codex 生成的脚本运行报错依赖版本不一致、路径写死检查报错堆栈和当前目录把脚本设计成可传入路径参数,避免cd /Users/xxx这类绝对路径
API 返回 401密钥或 base_url 配置错误检查环境变量重新确认模型服务地址和密钥权限
视频合成时字幕不出现字幕文件编码不是 UTF-8,或路径转义错误用文本编辑器检查 srt 文件头部统一使用 UTF-8 写入,并在 FFmpeg 命令里确保路径正确
歌词内容不够“特供”提示词里缺少观众偏好与边界查看生成结果对比配置把风格、语气、禁忌词放进系统提示词

其中“字幕不出现”是一个非常隐蔽的问题。很多情况下 SRT 文件本身没错,但 FFmpeg 对 Windows 路径里的冒号和反斜杠非常敏感,需要写成相对路径或把正斜杠转义。

9. 版权、安全与内容边界

这类 AI 内容生成玩法,最不值得踩的就是合规问题。尤其是“特供版”这种自定义倾向很强的项目,你可能会把真实姓名、真实照片、特定声音或未经授权的二创素材放进提示词,整个系统的风险就会迅速上升。

本文所使用的模板默认做了几个约束:

  • 不要求模型输出任何真实人物的姓名、肖像、声音特征;
  • 分镜描述只写场景和情绪,不写“谁的脸”“哪个人的声音”;
  • 所有输入素材必须是拥有使用权的内容。

如果你的确需要制作面向特定人群的定制内容,建议在 prompt 里写“观众偏好”,而不是写“某位真实人物的特征”。前者是对受众口味的一种描述,后者可能涉及人格权授权。同时,生产环境里建议加一层敏感词过滤,模型输出后先过过滤器再进入渲染,而不是把希望全部寄托在模型自觉上。

10. 最佳实践与工程化建议

当最小链路已经跑通后,下面几个改进方向能显著提升维护效率和生产稳定性。

10.1 提示词与代码分离

不要在一段长长的 Python 字符串里写提示词。把提示词放进独立的 md 文件,用占位符做参数替换。这样懂业务的人可以直接改文案,不需要动代码,也不会因为改一个逗号导致整个脚本语法错误。

10.2 所有中间结果落盘

我在上面的示例里,把每一阶段的输出都写进了 outputs 目录。这样你换了一个主题、改了一个风格,不需要重新生成前面的内容,只要重新渲染即可。更重要的是,当一次生成失败时,你能准确知道断在哪一层,而不是每次都要从头跑。

10.3 对模型输出做结构校验

json.loads只是第一步。更稳妥的做法是再写一层校验函数,检查句子数量是否大于零、每个句子的 text 是否非空、时间码是否递增。结构校验失败时,重新调用模型或直接失败退出,比“带着错误数据继续跑”节省大量时间。

10.4 成本与限流

填词模型和代码模型都是按 token 计费的。在调试阶段,尽量使用小批量测试,不要每次都生成完整 90 秒内容。也可以引入结果缓存:同样的主题、风格和模板,生成结果可以直接复用,不用再次调用模型。

10.5 保持背景与音频分离

不要让视频编辑操作依赖单一文件。背景视频、音频、字幕、画面素材各归各的目录,最终渲染时通过配置参数组合。这样换一个音频或背景图,成本只有重新渲染几十秒,不需要重新生成全部内容。

11. 下一步:把单向流水线升级为可交互的工作台

如果你已经跑通了上面的完整链路,其实还只完成了“量产工具”的一半。真正的 harness 还应该支持“人工修正回流”:字幕时间轴不准、某句歌词情绪不对、分镜描述不符合预期时,你修改中间 JSON 后,管线能只生成下游产物,而不是全部重跑。

一个简单实现方式:给run_pipeline.py增加参数,比如--from-lyrics、--from-storyboard,跳过上一步直接执行后续渲染。这样你就拥有了一个类似“所见即所得”的工作台:模型负责批量生成初稿,你负责在关键节点做判断和修正。

“特供版填词 PV”的本质,是让一套固定偏好以最高效率重复出现在内容里。你用 harness 锁定的不是某一个视频,而是一整套稳定产出的能力。把这套能力打磨好,比任何一次偶然的“AI 生成爆款”都更值得投入。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询