做 AI 短剧的同学,大概率经历过这样的阶段:故事创意已经想好,分镜也列得差不多,但真到动手把文字变成画面时,要在各种“AI 绘画工具、文生视频工具、配音工具、字幕工具、剪辑工具”之间来回切换。同样的 Prompt 要复制好几遍,同一个角色在十几张图里长得不一样,音频对不上画面,剪辑软件里手动对齐要把人逼疯。
我这次要分享的,是一套自己沉淀下来的 AI 短剧自动化思路,代码层面可以理解为一个持续迭代到2.5 版本的流水线。它要解决的核心问题只有一个:从故事创意到成片,尽可能一键完成。这里说的“一键”,不是指完全没有人工,而是把大量重复、机械、容易出错的编排动作交给代码,让创作者把精力留给创意和质量把控。
这篇文章会从整体架构开始拆,然后逐个讲清楚故事生成、分镜拆解、画面素材、配音字幕、视频合成这几个模块,最后给出一个可以本地运行的最小示例,以及我在实际调试中遇到的问题清单。无论你是刚接触 AI 短剧,还是已经在做半自动工具,都可以参考这套设计。
1. AI短剧自动化2.5,到底自动化了什么
1.1 传统AI短剧生产流程的痛点
一条普通的 AI 短剧,通常要经历这些阶段:
- 用大模型生成故事梗概,或者从网文/IP 素材里提取适合短剧的核心冲突。
- 把故事展开成具体的分集剧本,保证每集结尾有悬念。
- 将分集剧本拆成分镜脚本,标注场景、人物、动作、对话、镜头景别。
- 根据分镜脚本生成画面素材,早期主要是文生图,现在更多是图生视频。
- 为每个角色生成稳定的参考图,确保跨镜头长相一致。
- 为对白和旁白生成配音,再把音频对齐到对应镜头。
- 输出字幕、添加背景音乐,最后剪辑合成成片。
如果你在一开始就用“手工流”来做,最大问题并不是某个 AI 工具效果不好,而是环节之间没有统一的数据结构。
故事脚本是 Word 或 Markdown,分镜表可能写在 Excel 里,发给文生视频工具的是临时拼的图片和 Prompt,配音文件散落在多个文件夹,字幕又是另一套时间轴。一旦镜头数变多,或者需要改一个角色描述,整套素材可能都要重来。
这一点往往是新手最容易忽略的。以为只要把多个 AI 能力拼在一起就可以一键生成,实际做起来却发现,上下文到处断,人物前后不一致,脚本改了画面没跟着改。其实背后的核心问题,不是“AI 不够聪明”,而是流程缺少标准化的中间产物。
1.2 自动化要做的事不是替代人
把自动化二字理解成“无人值守”是一个误区。尤其是 AI 短剧这种内容创作场景,创意是否成立、剧情是否合理、画面是否安全,都需要人参与决策。
自动化真正做的事是:
- 用脚本和大模型完成重复的文本分拆与信息转换。
- 用配置管理一部剧的风格、角色、分辨率、字幕参数。
- 用统一的数据模型在故事、分镜、素材、音频、字幕之间传递信息。
- 用任务调度把串行流程拆成可重跑、可续跑、可观察的步骤。
- 用 FFmpeg 等工具在最后阶段完成机械剪辑,避免来回手动操作。
人工要做的就是:审核每一层剧本,挑选画面,处理模型生成失败的镜头,以及做最终质量判断。
1.3 2.5版本在工程上意味着什么
如果把这套系统视为一个独立的项目,2.5 版本是我对流水线成熟度的一种阶段划分:
- v1.0:只有文本能力,能生成短剧故事大纲和分集剧本。
- v2.0:加入了画面生成和配音,但还是半自动,需要人工复制粘贴。
- v2.5:引入了“项目化配置”和“流程编排”,实现了一条主命令跑完故事、分镜、素材生成、合成后处理等核心步骤。
这个版本的关键变化,是把短剧生产建模成可以被代码调度的流水线,而不是一堆互相割裂的脚本。下面要讲的整套实现,就是围绕这个工程理念展开的。
2. 整体架构与项目环境准备
2.1 一次“从故事到成片”的总流程
在设计流水线时,我习惯先把整个链路画成下面这样:
故事梗概/视频Brief ↓ LLM 故事模块 → story.json ↓ LLM 分镜模块 → episode_scenes.json ↓ 媒体服务模块 → shot 视频片段/TTS 音频文件 ↓ 字幕模块 → SRT 文件 ↓ FFmpeg 合成模块 → final_episode.mp4从工程角度看,它其实是一个典型的数据管道。上游每个模块产出的文件,都是下游模块的输入。为了保证出现问题时能定位,我把每个中间文件都落到磁盘上,不放在内存里传递。这样哪怕某个步骤失败,也可以基于已有中间文件继续重试。
2.2 环境与依赖
本文示例以常见环境为例,版本可以根据你本机情况调整,重点是理解配置思路。本项目建议使用:
- Python 3.10 或更高版本
- FFmpeg 4.3 或更高版本,用于视频拼接、配音、字幕烧录
- requests 库,用于调用 HTTP API
- PyYAML 库,用于读取 YAML 配置
- 一个支持 OpenAI 协议的大模型 HTTP 接口,可以是云端模型也可以本地模型服务
安装基础依赖:
pip install requests pyyamlFFmpeg 在 Windows、macOS、Linux 下都有对应的安装方式。安装完成后可以在终端执行:
ffmpeg -version如果能看到版本信息,说明环境已经 OK。
2.3 项目目录结构设计
为了让所有环节都“可配置、可跟踪、可扩展”,我建议把一个 AI 短剧自动化项目按下面的结构组织:
ai_short_drama/ ├── config/ │ ├── global.yaml │ └── project_ep01.yaml ├── schema/ │ ├── story.py │ └── scene.py ├── services/ │ ├── llm_client.py │ ├── media_generator.py │ ├── tts_client.py │ └── mock_services.py ├── pipeline/ │ ├── story_module.py │ ├── script_module.py │ ├── visual_module.py │ ├── audio_module.py │ └── compose_module.py ├── workdir/ │ ├── ep01_story.json │ ├── ep01_scenes.json │ ├── video/ │ ├── audio/ │ └── subtitle/ └── main.py目录说明:
config/保存项目配置和模型配置。schema/定义剧本、分镜、镜头的数据结构。services/封装大模型、图片/视频模型、TTS 等外部能力。pipeline/是流水线核心模块,每个模块负责一个生产阶段。workdir/保存所有中间产物,方便断点续跑和排查。
这种分层方式的好处,是外部工具可以替换。今天用某个文生视频服务,明天换成另一个,只需要改services/media_generator.py的实现,不影响上层流水线逻辑。
2.4 用配置管理一部剧
配置在 AI 短剧自动化里非常重要。如果一部剧的画面比例、角色列表、风格关键词全部写死在代码里,后续复用会非常痛苦。
下面是一个简化版的project_ep01.yaml:
project_name: "demo_ep01" format: width: 1080 height: 1920 fps: 24 thumbnail_time: 2.0 video_gen: duration_per_shot: 3 characters: - character_id: "chen" name: "陈默" description: "穿深色风衣的年轻记者,神情疲惫但坚定" - character_id: "lin" name: "林夏" description: "穿白色连衣裙的女孩,眼神很空" style: global_prompt: "电影感、赛博朋克夜景、高质量光影、35mm电影镜头" negative_prompt: "lowres, bad anatomy, extra fingers, watermark, text, logo" subtitle: enabled: true font: "Noto Sans CJK SC" font_size: 16为什么要把这些放到配置里?因为同一套代码只需要更换 YAML 配置,就能生成不同风格、不同角色的分集。配置还方便团队协作,不需要每个成员都去读源码。
3. 模块一:从故事到可计算的结构化剧本
3.1 用大模型生成故事,但要限定输出格式
故事生成模块是整条流水线的入口。输入可以非常简单,例如一句话 Brief:
一个外卖员深夜接到神秘订单,送到后发现收货人信息全是空,却因此卷入一场城市谜案。如果直接让大模型自由发挥,它可能给你一大段散文,后续解析非常困难。更好的做法是要求模型输出结构化 JSON,同时把字段含义写清楚。
这里我给出一段适用的系统提示词:
你是一名擅长短剧编剧的大纲助手。请根据用户提供的Brief,输出一部竖屏短剧第一集的故事梗概,包含人物设定和主要情节。 必须输出 JSON,不要输出其他文字,JSON 结构如下: { "title": "短剧名称", "genre": "题材", "logline": "一句话故事", "characters": [ { "character_id": "小写英文字母id", "name": "人物名", "description": "外观与人物设定", "personality": "性格描述" } ], "story_beat": [ { "beat_id": "b001", "summary": "本小节情节概述", "conflict": "本小节冲突" } ] }系统提示词里指定 JSON 结构,是非常关键的一步。短剧的后续分镜模块并不需要阅读散文,它只需要title、characters、story_beat这些结构化字段。
3.2 大模型客户端封装
真实项目里,不同大模型往往提供不同 SDK,但它们对 OpenAI 协议兼容度普遍较高。为了不绑定具体 SDK,我在示例里使用requests直接访问 HTTP 接口。
# services/llm_client.py import json import os import requests class LLMClient: def __init__(self): self.api_url = os.getenv("LLM_API_URL", "https://api.openai.com/v1/chat/completions") self.api_key = os.getenv("LLM_API_KEY", "") self.model = os.getenv("LLM_MODEL", "gpt-4o-mini") def chat_json(self, system_prompt: str, user_prompt: str, temperature: float = 0.7): """ 调用大模型,返回解析后的 JSON 对象。 注意:实际使用时请按你所接的模型服务调整认证方式。 """ headers = { "Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}", } payload = { "model": self.model, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], "temperature": temperature, "response_format": {"type": "json_object"}, } resp = requests.post(self.api_url, headers=headers, json=payload, timeout=120) resp.raise_for_status() content = resp.json()["choices"][0]["message"]["content"] return self._safe_load_json(content) @staticmethod def _safe_load_json(content: str): # 有些模型会在 JSON 前后加 ```json 标记,需要兜底清理 content = content.strip() if content.startswith("```"): content = content.split("\n", 1)[-1] if content.endswith("```"): content = content[:-3] return json.loads(content)代码中我用了response_format让模型尽量返回结构化 JSON。但需要注意,并非所有模型都支持该字段。如果使用的是私有化部署的模型,可能需要去掉这个参数,并在系统提示词里更严格地限定输出。
3.3 为什么必须做 JSON 清洗
大模型输出 JSON 时经常出现两类问题:
- 输出带有 Markdown 代码块标记。
- 输出中包含逗号缺失、引号转义错误等异常。
因此_safe_load_json这个清洗函数不是可有可无,它是保证流水线稳定运行的前置防御。即便模型已经声明只输出 JSON,兼容层仍然要处理异常情况。
如果为了省事,直接让下游模块拿原始字符串去解析,一旦模型返回了一个多余字符,整个流程就会中断。放在自动化系统里,这种低频但高频次出现的偶发错误,非常消耗耐心。
3.4 模块输出落到磁盘
故事模块执行完成后,应该把结果保存为文件:
# pipeline/story_module.py import json import os class StoryModule: def __init__(self, llm_client, output_dir: str): self.llm_client = llm_client self.output_dir = output_dir os.makedirs(output_dir, exist_ok=True) def generate(self, brief: str): system_prompt = """你是短剧大纲助手,请按指定 JSON 结构输出。""" story_json = self.llm_client.chat_json( system_prompt=system_prompt, user_prompt=f"故事Brief:{brief}", ) path = os.path.join(self.output_dir, "ep01_story.json") with open(path, "w", encoding="utf-8") as f: json.dump(story_json, f, ensure_ascii=False, indent=2) return path保存中间产物的好处是:如果后面分镜模块出了问题,你不需要再调用一次大模型重新生成故事,可以直接基于磁盘上的ep01_story.json重跑。这在做批量生产时尤其重要,因为大模型调用是有成本和时间开销的。
4. 模块二:从故事到分镜拆分
4.1 先用数据模型表达场景
故事生成了,但故事和成片之间还隔着景别、镜头运动、人物调度、对白节奏。分镜模块要做两件事:
- 把故事的叙事节拍拆成若干个可以生成的视频场景。
- 对每个场景写清楚画面内容、人物、对话、镜头类型、时长。
为了让分镜能被下游模块处理,我建议定义简单的 Scene 数据模型:
# schema/scene.py from dataclasses import dataclass, field from typing import List, Optional @dataclass class Dialogue: character: str text: str @dataclass class Scene: scene_id: str location: str action: str camera: str duration_seconds: int dialogues: List[Dialogue] = field(default_factory=list) @dataclass class EpisodeScript: episode_no: int title: str scenes: List[Scene] = field(default_factory=list)字段说明:
scene_id:场景唯一编号,例如 s001、s002。location:地点,方便统一场景关键字。action:画面里发生的动作。camera:镜头运动,比如推进、特写、摇臂、固定镜头。duration_seconds:这个镜头预估时长。dialogues:对白列表。
4.2 把人物和场景映射到统一的视觉 Prompt
只靠 Scene 里的 action 直接生成视频,效果会不可控。因为模型不知道人物长相、环境氛围、画风。
所以在分镜模块中,需要把配置里的characters、style和 Scene 拼成一个完整的画面 Prompt。例如:
陈默穿深色风衣,站在雨夜旧城街道,神情疲惫但坚定, 电影感、赛博朋克夜景、高质量光影,35mm电影镜头,人物特写,镜头缓慢推进拼接逻辑可以放在一个独立的函数里:
# pipeline/script_module.py def build_scene_visual_prompt(scene: Scene, character_map: dict, style: dict) -> str: parts = [] for dialogue in scene.dialogues: character_id = dialogue.character if character_id in character_map: char_desc = character_map[character_id]["description"] parts.append(char_desc) parts.append(scene.action) parts.append(style.get("global_prompt", "")) parts.append(scene.camera) # 拼接后只保留核心内容,避免 Prompt 过长 return ", ".join([part for part in parts if part])这里character_map是从上一阶段生成的 story 里提炼出的人物字典。这样在 Prompt 里出现的就不再是一个抽象的名字,而是人物的具体外观描述。后续画面生成时,模型更容易理解“陈默”应该是什么样子。
4.3 让大模型负责拆场,代码负责校验
大模型可以帮助拆场,但它可能输出少一个逗号、多一个场景,或者场景时长不合理。因此进入素材生成前,分镜模块要做一次规则校验。
校验内容至少包括:
- 每个场景必须有
action或dialogues。 - 场景数量不为空。
duration_seconds处于允许范围内。scene_id没有重复。
下面是一个简化校验函数:
# pipeline/script_module.py def validate_scenes(scenes: List[Scene]) -> List[str]: errors = [] seen_ids = set() for scene in scenes: if scene.scene_id in seen_ids: errors.append(f"duplicated scene_id: {scene.scene_id}") seen_ids.add(scene.scene_id) if not scene.action and not scene.dialogues: errors.append(f"scene {scene.scene_id} has empty action and dialogue") if not 1 <= scene.duration_seconds <= 10: errors.append(f"scene {scene.scene_id} duration out of range: {scene.duration_seconds}") return errors不要把希望完全寄托在大模型输出正确上,代码必须检查并拦截异常数据。这类防御逻辑,是自动化系统能长期稳定运行的关键。
5. 模块三:画面素材生成与适配器模式
5.1 不同的文生图/文生视频服务差异太大
做 AI 短剧时,画面生成服务的选择非常依赖你所在地区、可用的模型、预算和效果。这个领域变化太快,直接写死某一个 API 不太合适。
所以在工程上,我建议使用适配器模式。定义一个MediaGenerator抽象接口,上游流水线只依赖接口,不依赖具体服务。
# services/media_generator.py from abc import ABC, abstractmethod class MediaGenerator(ABC): @abstractmethod def generate_shot_video(self, scene: dict, output_path: str) -> str: """ 根据场景信息生成一个视频镜头。 返回最终视频文件路径。 """ pass接着,可以根据不同工具写子类。
比如一个简化风格的文生视频类:
# services/media_generator.py class ActualAIVideoGenerator(MediaGenerator): def __init__(self, api_key: str, api_url: str): self.api_key = api_key self.api_url = api_url def generate_shot_video(self, scene: dict, output_path: str) -> str: prompt = scene["visual_prompt"] # 这里需要根据你所使用的视频生成服务SDK/HTTP接口实现 # 一般流程是:提交任务 -> 轮询任务状态 -> 下载结果 # 示例代码省略具体 HTTP 请求体 return output_path为什么我不写一个可以直接运行的实现?因为市面上的文生视频服务接口差异很大,甚至同一个平台每隔几个月就会调整参数。固化成文章里的代码反而容易过时。你只需要保留接口,然后根据当前服务商文档补全请求即可。
5.2 本地 Mock 服务可以让管道先跑起来
在没有付费 API 的情况下,可以先写一个 Mock 实现。它不真正生成画面,而是用 FFmpeg 生成一段纯色视频,用来验证整条流水线的调度逻辑是否通畅。
# services/mock_services.py import subprocess class MockVideoGenerator: def generate_shot_video(self, scene: dict, output_path: str) -> str: width = scene.get("width", 1080) height = scene.get("height", 1920) duration = scene.get("duration_seconds", 3) color = scene.get("mock_color", "black") # 用 FFmpeg 生成 1080x1920 的纯色测试视频 cmd = [ "ffmpeg", "-y", "-f", "lavfi", "-i", f"color=c={color}:s={width}x{height}:d={duration}:r=24", "-c:v", "libx264", "-pix_fmt", "yuv420p", output_path, ] subprocess.run(cmd, check=True, capture_output=True) return output_path这段代码的作用是让流水线在没有真实模型时也能跑通文件流转。如果某天你想测试一个新的文生视频服务,只需要把 Mock 实现替换成真实服务的实现。
5.3 角色一致性
角色一致性是 AI 短剧制作里最让人头疼的问题。技术层面常见做法有这几种:
- 在画面生成 Prompt 里固定描述人物外观,例如发型、服装、颜色。
- 为每个角色准备一张参考图,传给图生视频或参考模型。
- 使用支持角色参考能力的模型,或者用 LoRA 微调固定角色风格。
- 对同一角色的不同镜头使用相近的随机种子,增加稳定性。
从工程角度看,不管使用哪种方案,都需要把“参考图路径”或“人物种子”放到配置中。流水线在拼每个镜头 Prompt 时,必须从配置中读取该角色对应的一致性参数。
我建议在项目中维护一份角色配置:
characters: - character_id: "chen" name: "陈默" ref_image: "./assets/chen_ref.png" seed: 10135.4 素材缓存与断点续跑
画面生成通常比较耗时,失败的镜头也经常需要重试。所以素材生成模块一定要做缓存。
具体策略是按场景 ID 判断:
- 如果
output/shot_s001.mp4已经存在,并且文件大小大于 0,就跳过生成。 - 如果文件不存在,才调用模型接口生成。
- 生成完成后写一个元数据文件,记录 Prompt、seed、模型、耗时。
伪代码逻辑如下:
def ensure_shot_video(scene, work_dir): output_path = f"{work_dir}/video/{scene['scene_id']}.mp4" if exists_and_valid(output_path): return output_path generator = get_generator() generator.generate_shot_video(scene, output_path) return output_path缓存的意义不只是省一次重复调用,更重要的是在长流程里,如果第 15 个镜头生成失败,前面 14 个镜头不需要重新生成。
6. 模块四:配音、字幕与最终合成
6.1 配音模块
当前大多数文本转语音服务都提供 HTTP API。和画面生成一样,我将配音也抽象为接口:
# services/tts_client.py class TTSClient: def synthesize(self, text: str, output_path: str, voice: str = "default") -> str: # 按实际 TTS 服务文档实现 # 本方法只作为示例骨架 return output_path在工程实践中,配音需要做到以下几点:
- 每个对白单独生成一段音频,而不是整集生成一大段。
- 音频文件名与场景 ID、对话序号对应,便于对齐。
- 旁白和对白可以用不同音色,增强剧情感。
- 最后用 FFmpeg 根据时间轴把这些音频片段放到正确的时间点。
音频文件命名示例:
s001_d0.mp3 s001_d1.mp3 s002_d0.mp3第一个字段是场景 ID,第二个字段是场景内对话序号。这样在合成时,直接从目录扫描文件就能知道每个音频归属于哪个镜头。
6.2 字幕模块
短视频的字幕可以直接从 Dialogue 文本生成。因为对白是按场景拆分的,理论上我们可以估算时间轴:每个场景的时长已知,每个对话根据文本长度分配时间。
一个相对简单的做法是:先用配音生成对白音频并测量每个音频文件的实际时长,然后基于实际时长生成 SRT。
SRT 字幕文件内容样例:
1 00:00:00,000 --> 00:00:02,500 这个订单真的能送到吗? 2 00:00:02,500 --> 00:00:05,200 地址是空的,但系统显示已送达。如果后续需要把字幕做得更准确,可以用 ASR 模型识别配音音频生成字幕,或者直接用配音文本按时间轴映射。前者更准,但实现成本更高。
6.3 用 FFmpeg 合成完整视频
当所有镜头视频、配音、字幕都准备好以后,就可以进入最终合成阶段。
先将所有镜头视频按场景顺序拼接。假设所有 MP4 片段都统一编码、分辨率、帧率,使用 FFmpeg concat 协议最简单:
# workdir/filelist.txt file './video/s001.mp4' file './video/s002.mp4' file './video/s003.mp4'执行拼接:
ffmpeg -y -f concat -safe 0 -i filelist.txt -c copy concatenated.mp4如果各镜头编码参数不一致,执行时可能失败。此时需要重新统一编码,不使用-c copy:
ffmpeg -y -f concat -safe 0 -i filelist.txt \ -vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2" \ -c:v libx264 -pix_fmt yuv420p concatenated.mp4加入配音可以使用如下命令:
ffmpeg -y -i concatenated.mp4 -i dialogue_full.m4a \ -c:v copy -c:a aac -shortest temp_episode.mp4最后烧录字幕:
ffmpeg -y -i temp_episode.mp4 \ -vf "subtitles=ep01.srt:force_style='FontName=Noto Sans CJK SC,FontSize=16,Alignment=2'" \ -c:v libx264 -c:a copy final_ep01.mp4这里有一个非常常见的坑:subtitles滤镜里的中文字体名如果包含空格或冒号,需要转义。不同 FFmpeg 版本和 libass 版本的处理方式也有差异。如果执行失败,优先查看 FFmpeg 的报错信息,把那一段字幕滤镜单独拆出来测试。
7. 把模块串成一条可回放的主流水线
7.1 主入口设计
各部分模块就绪后,需要一个主入口统一调度。
# main.py import argparse from services.llm_client import LLMClient from services.mock_services import MockVideoGenerator from pipeline.story_module import StoryModule from pipeline.script_module import ScriptModule def main(): parser = argparse.ArgumentParser(description="AI短剧自动化流水线") parser.add_argument("--brief", type=str, default="一个外卖员的深夜奇遇") parser.add_argument("--mode", type=str, default="mock", choices=["mock", "real"]) args = parser.parse_args() llm_client = LLMClient() story_dir = "./workdir" story_module = StoryModule(llm_client=llm_client, output_dir=story_dir) # 如果已经有 story.json,可以跳过 LLM 调用 story_module.generate(args.brief) # 这里只展示链路入口,完整实现需要继续调用 ScriptModule、VideoModule、AudioModule 等 print("story generated:", story_dir) if __name__ == "__main__": main()真正生产级别的主流程应该是模块化的,而不是把所有函数堆在 main.py 里。我通常会让每个 Pipeline 模块只负责一件事,并且在阶段之间输出一个状态文件。
7.2 阶段状态与断点重跑
设想一下这个场景:你在批量生成第 8 集短视频时,画面服务突然报错,你修复问题后,不需要从第 1 集的 LLM 故事生成开始重跑。这就是断点重跑的价值。
实现思路很朴素:每个模块开始前检查输出文件是否存在,如果存在且状态有效,就跳过;如果不存在或状态无效,才执行。
def is_valid_cache(path: str) -> bool: return os.path.exists(path) and os.path.getsize(path) > 07.3 任务日志
自动化流水线中,日志要包含以下内容:
- 当前执行到哪个阶段。
- 调用了哪个外部服务。
- Prompt 的摘要信息。
- 请求耗时。
- 成功或失败的返回信息。
- 生成的中间产物路径。
简洁的日志输出可以这样设计:
[14:32:01] [SUCCESS] story_module generated ./workdir/ep01_story.json [14:32:03] [SUCCESS] script_module generated 12 scenes [14:32:05] [RUNNING] visual_module generating shot s001 [14:33:10] [SUCCESS] visual_module generated ./workdir/video/s001.mp48. 常见问题与排查清单
下面我把实现过程中容易踩到的问题整理成表格,方便后续排查。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| LLM 返回 JSON 解析失败 | 模型输出了 Markdown 代码块或文本说明 | 增加 JSON 清洗函数,并在系统提示词里严格要求 JSON |
| 生成的角色前后不一致 | Prompt 没有固定人物外观,或缺乏参考图 | 在人物配置中加入完整外观描述和参考图,使用稳定的 seed |
| FFmpeg 拼接失败 | 视频分辨率、帧率、编码不一致 | 先统一缩放和编码,再拼接 |
| 中文字幕乱码或字体无效 | 系统没有安装对应字体,或滤镜字体名不正确 | 安装中文字体,更换FontName,先单独测试字幕滤镜 |
| 音频与画面不同步 | 视频片段时长与实际对白时长不匹配 | 基于音频时长生成字幕时间轴,而不是直接按文本估算 |
| 生成视频阶段频繁失败 | 服务接口当前不可用或并发过高 | 添加重试与限流,队列化执行,而不是并行拉满 |
| 磁盘上中间文件越来越多 | 缺少清理或归档策略 | 按项目目录区分,定期清理旧的 workdir |
如果你遇到了与上面不同的报错,一个比较通用的排查方法是:先把报错信息复制到搜索框里,同时关注报错行的上下文。不要只看最后一行,特别是 FFmpeg 的报错,真正原因往往在中间几行里。
9. 最佳实践与安全边界
9.1 内容合规是动线的底线
AI 短剧自动化虽然能提高产量,但自动化也会放大内容风险。如果脚本本身不安全,批量生产只会批量扩散问题。
在接故事生成模块之前,应该有一套内容过滤机制:
- 不输入违规、暴力、色情等违法或不良内容。
- 不生成涉及真实人物肖像、特定机构或敏感事件的剧情。
- 不编造新闻、冒充官方消息、引导错误认知。
自动化系统里可以加一层“剧本审核 checkpoint”。例如由 LLM 输出后,先经过一份敏感词列表或内容审核 API,再由人工确认,最后才进入画面生成环节。不要试图用技术绕过平台内容安全机制。
9.2 版权与素材授权
短剧如果来自网文、小说、剧本改编,需要确认版权。即使使用 AI 生成,如果参考了特定知名 IP 的设定、角色名、视觉元素,也可能构成侵权。
在工程实践上,我会建议:
- 优先使用原创剧本或已明确授权的素材。
- 不直接复制知名小说剧情结构、人物关系、核心桥段。
- 不使用未经授权的艺人照片或图片作为角色参考图。
- 生成素材保留生成时间、模型版本、Prompt 等元信息,方便追溯。
9.3 模型服务与密钥管理
千万不要把 API Key 直接写死在代码里。建议使用环境变量或机密管理平台。
例如在.env文件里维护:
LLM_API_KEY=your_key_here LLM_API_URL=https://your-llm-service.example.com/v1/chat/completions在 Python 中通过os.getenv读取。同时不要把.env文件提交到 Git 仓库。如果项目是多人协作,还需要做好密钥的权限隔离。
9.4 性能与成本控制
批量生产 AI 短剧,最需要关注的其实是成本。单集短剧如果包含几十个镜头,每个镜头都要走一次文生视频服务,费用会成倍上涨。
建议从这几个方面控制成本:
- 优先用轻量模型完成故事和分镜,再用高质量模型生成关键镜头。
- 构图变化不大的镜头,可以先生成图片,再通过图生视频生成短镜头。
- 能复用的场景,尽量不重复生成。
- 生成前先跑小规模试片,确认效果后再批量。
9.5 人工审核检查点
自动化并不意味着失去控制。一条成熟的 AI 短剧流水线,至少要设置四个检查点:
- 故事大纲确认:剧情方向是否成立。
- 分镜表确认:镜头是否合理,有没有高风险内容。
- 画面效果抽检:角色一致性是否达标。
- 成片终审:配音、字幕、画面节奏是否合格。
这些检查点没必要打断所有自动化流程,但至少要保留“出现问题后可暂停”的能力。
10. 二次开发与后续学习方向
如果你读到这里,说明你并不是只想找一个“复制粘贴就能用”的脚本,而是希望真正掌握 AI 短剧自动化背后的工程方法。这套系统的再往深走,还有几个方向:
10.1 从“一键单集”到“多集批量”
单集跑通后,下一阶段就是如何批量生成多集。
这一步会有新的挑战:
- 剧情连续性:上一集的伏笔如何在下一集回扣。
- 角色状态变化:随着剧情推进,角色的服装、表情状态可能变化。
- 项目管理:几十集的中间文件如何组织。
因此我建议把项目结构从“单集目录”扩展为“整季目录”,例如season_01/ep01/、season_01/ep02/。每集独立执行,但由上一季度的总配置统一管理。
10.2 引入队列与异步任务
当镜头数量足够大时,串行调用文生视频服务会非常慢。可以引入任务队列,例如 Redis 队列或本地进程池,实现多个镜头并行生成。但要特别注意下游服务 QPS 限制。
10.3 接入自动评估
AI 生成结果是否可用,主观性很强,但仍可以设计一些自动检查:
- 检查角色参考图和生成画面的相似度。
- 检查字幕是否有错别字。
- 检查视频时长是否符合配置。
- 检查画面中是否出现明显的水印或文字。
这些检查并不复杂,却能大幅降低人工抽检的成本。
10.4 沉淀自己的短剧模板库
同一套代码,只要配置不同,可以产出悬疑、甜宠、古装、都市等不同品类短剧。每一种品类的分镜节奏、镜头风格、音效要求都不相同。最终要想提升效率,就需要把优秀案例沉淀成标准模板。
模板里可以包含:
- 通用的分镜提示词。
- 固定的画面风格描述。
- 不同角色的参考资源路径。
- 字幕样式与转场预设。
模板越多,复用的杠杆越大。
从故事到成片一键搞定,并不是一句口号,它背后是故事结构化、分镜解析、素材缓存、媒体服务适配、FFmpeg 合成、状态管理等一系列工程问题的组合。如果你能把上面这些模块拆清楚,哪怕第一版实现很粗糙,后续迭代也会越来越顺。希望这篇教程能给你一个可以落地的起点。