简介:AI短剧创作教程源码包,专为希望借助AI工具快速产出短剧的内容创作者与开发者设计。资源围绕AiPy工具,覆盖从故事主题设定、提示词生成剧本,到旁白与分镜头视频制作、人物一致性维护,直至音视频合成出片的完整流程,配有可直接套用的提示词模板,零基础用户也能一步步做出连续、连贯的短剧。包内共3个文件,以HTML格式的交互式教程为主体,并附带inscode工程配置与gitignore辅助文件,压缩包仅7KB,结构精简、上手成本极低。已有1283人学习。通过源码可直观查看分镜头示例、剧本大纲写法及工程运行配置,既能当作速成教程,也可作为AI短剧生成工具的二次开发参考,帮助理解AI在剧本创作、场景构建与自动剪辑中的实际落地方式。1. AI短剧创作教程源码包:它到底帮你做了什么AI短剧创作这件事,绝大多数人卡住的地方不是“不会写”,而是“写出来之后怎么把它变成一条真正的视频”。我之前接过一个模拟项目X:一天内要出一段60秒短剧样片,写剧本只花了半小时,剩下六个半小时全在解决“AI生成的音频和画面怎么对上、怎么导进剪辑轨道”。这套AI短剧创作教程源码包给我的感觉,就是有人先把这套流水线配好了。它以剧本拆分为起点,覆盖TTS配音、分镜画面生成、时间轴对齐、最终渲染,适合两类人:一类是刚入行的开发者,想看看一条短剧pipeline到底要分几段;另一类是已经在手搓工具但整条链路断断续续的人,拿来对一对自己的接口设计。下面我把骨架拆给你看。2. 先立骨架:拆目录、配角色卡,把“拍戏”变成数据2.1 源码包目录:入口、配置与产物分开拿到源码包第一步不要急着跑,先看目录。短剧和单点AI工具不同,它是一条强流程的流水线:剧本拆分、配音生成、画面生成、视频渲染,四段各自独立又互相依赖。源码带的目录结构是这样:project_x/ ├── config/ │ ├── settings.yaml # 全局参数:分辨率、帧率、输出路径 │ └── characters.json # 角色卡:音色、语速、外观描述 ├── scripts/ │ ├── 01_prepare.py # 剧本md拆分成分幕JSON │ ├── 02_gen_tts.py # 逐条对白生成音频 │ ├── 03_gen_images.py # 逐分镜生成画面 │ └── 04_render.py # 合成视频 ├── data/ │ ├── script.md # 原始剧本 │ └── scenes/ # 拆分后的中间数据 ├── output/ │ ├── audio/ # TTS产出的音频文件 │ ├── images/ # 分镜图片 │ └── final/ # 渲染成品这个结构值得沿用的地方在于:四个阶段通过文件系统通信,而不是通过数据库或服务调用。音频生成完落盘为一个mp3文件,画面生成完落盘为一张png图片,渲染脚本只认目录不认内存里的对象。好处是任意阶段翻车,你都能直接看产物排查,不用在内存里打日志追变量。坏处也很明显:中间文件会占大量磁盘空间,但短剧项目单条时长短,一张1080x1920的图也就几MB,一百张图完全能接受。入口顺序是01到04,靠文件名天然排序。settings.yaml里最值得改的参数是分辨率,默认1080x1920竖屏,如果做横屏版本要同时改文件里的width和height,不然渲染阶段会拿竖屏参数切横屏素材,画面上下一截黑边。2.2 角色卡与剧情分幕:把“拍戏”翻译成结构化数据整套源码的数据源头是config/characters.json,它把“角色”抽象成一组固定键值。我习惯把它理解成给AI演员写档案:同一角色在剧本里出现十次,每次生成配音和画面都要读这张卡,而不是让模型自己回忆上一场长什么样。{ "role_01": { "name": "角色A", "voice_profile": { "engine": "tts_a", "voice": "female_calm", "speed": 0.95, "pitch": 1.02, "lang": "zh-CN" }, "style_hint": "深色短发,灰色外套,室内灯光", "scene_range": ["scene_001", "scene_008"] }, "role_02": { "name": "角色B", "voice_profile": { "engine": "tts_a", "voice": "male_deep", "speed": 1.0, "pitch": 0.95, "lang": "zh-CN" }, "style_hint": "高个子,浅色卫衣,左侧柔光", "scene_range": ["scene_001", "scene_003"] } }voice_profile里的speed是语速倍数,0.95表示比标准语速略慢,短剧对白通常语速偏快,但TTS在1.0以上容易吞字,尤其“了、呢、吗”这类语气词,所以我一般压到0.9到1.0之间。pitch是音调偏移,拉到1.1以上会明显发“电子音”,不是所有角色都适合。style_hint这行是给画面生成用的,描述要具体到什么发型、什么衣服、什么光位,后面生图阶段会逐场拼进提示词模板。01_prepare.py做的事是把原始剧本md拆成结构化的场次数据,核心就是一个正则切分:# scripts/01_prepare.py import json, re from pathlib import Path def split_scene(md_text): # 按二级标题切分,剧本里每场戏用 "## " 标记 blocks = re.split(r'^## ', md_text, flags=re.M) scenes = [] for i in range(1, len(blocks)): content = blocks[i].split('\n') title = content[0].strip() dialogue = [] for line in content[1:]: if ':' in line: role, text = line.split(':', 1) dialogue.append({ "role": role.strip(), "line": text.strip() }) scenes.append({ "scene_id": "scene_%03d" % i, "title": title, "dialogue": dialogue }) return scenes if __name__ == "__main__": md = Path("data/script.md").read_text(encoding="utf-8") scenes = split_scene(md) out = Path("data/scenes/scenes.json") out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(scenes, ensure_ascii=False, indent=2), encoding="utf-8") print(f"拆分完成:{len(scenes)}场")拆分规则只认##和冒号两个约定。如果你的剧本不是这种格式,比如用数字编号或者-列表,改re.split那行就行。拆完的scenes.json是所有后续脚本的输入,配音脚本按场次遍历,画面脚本按场次生成分镜,渲染脚本也用它来确定时长。我建议你在这一步跑完后打开scenes.json确认一条台词都没丢,这一步检查得越仔细,后面重跑的代价越低。2.3 启动检查:依赖、配置与第一条日志源码包里带了一份requirements.txt,使用前先建干净环境,路径和依赖错了会一路错到渲染结束。我的习惯是这样启动:cd project_x python -m venv venv source venv/bin/activate python -m pip install -r requirements.txt python scripts/01_prepare.py用python -m pip而不是直接pip,是为了避免多版本Python环境下装错包。装完依赖后先只跑01_prepare.py,它是最轻量的步骤,能看到拆分日志就算环境通了。如果这一步报ImportError,大概率是依赖没装上,回到上面重装;如果报编码错误,把script.md转成UTF-8编码,而不是去改读取逻辑。全局参数集中在config/settings.yaml,这里放的是不经常改但影响全链路的配置:scene_dir: data/scenes audio_dir: output/audio image_dir: output/images final_dir: output/final fps: 30 resolution: width: 1080 height: 1920fps我固定用30,60会让渲染时间翻倍,对短剧来说观感提升有限。resolution决定了后面所有画面的长宽比,中途改会让已生成的图片白做,所以第一步就定死。3. 让角色开口:TTS选型、脚本驱动与对白时间轴对齐3.1 TTS选型:两个边界条件源码里TTS这块给了两套接法:一套接系统级TTS服务,一套接本地模型。做短剧选哪套,我只看两个条件:一是音色稳定性,二是批处理吞吐。系统级服务接入成本低,几句代码就能出声,但同一句话重跑两次音色会有细微漂移,单条对白听不出问题,串成一部片子就会觉得角色“忽老忽年轻”。本地模型第一次要下模型文件,之后全程离线跑,参数锁死后音色稳定得多,代价是显存占用量和更慢的生成速度。对比项系统级TTS服务本地模型接入成本低,配好密钥即可中,需要处理模型文件与依赖音色稳定性一般,多次调用有漂移高,锁参数后基本不变批处理速度快,并发接口慢,串行为主可控参数少,通常只有语速和音色多,pitch、停顿、重音都能调短剧的台词量不大,一条60秒成片一般就20到30句对白,吞吐量不是瓶颈,音色稳定才是。所以我在这个项目里选了本地模型路线,并且把参数全部写在角色卡里,任何一场生成都读同一套配置。3.2 从剧本JSON到对白音频:驱动脚本02_gen_tts.py的作用很简单:遍历scenes.json里的每一条对白,读取角色卡配置,调TTS接口生成音频文件,落盘到output/audio/下。# scripts/02_gen_tts.py import json, subprocess, time from pathlib import Path def gen_tts(role_profile, line, out_path): vp = role_profile["voice_profile"] cmd = [ "tts_cli", "--model", vp["engine"], "--voice", vp["voice"], "--speed", str(vp["speed"]), "--pitch", str(vp["pitch"]), "--lang", vp["lang"], "--text", line, "--out", str(out_path) ] for attempt in range(3): # 失败重试3次,避免网络抖动直接断掉管线 r = subprocess.run(cmd, capture_output=True, text=True) if r.returncode == 0: return True print(f"[retry {attempt+1}] {line[:20]}") time.sleep(2) print(f"[FATAL] {line} -> {r.stderr[-300:]}") return False def main(): scenes = json.loads(Path("data/scenes/scenes.json").read_text(encoding="utf-8")) roles = json.loads(Path("config/characters.json").read_text(encoding="utf-8")) out_dir = Path("output/audio") out_dir.mkdir(parents=True, exist_ok=True) timeline = [] for scene in scenes: for idx, dlg in enumerate(scene["dialogue"]): role = roles.get(dlg["role"], roles["default"]) filename = f"{scene['scene_id']}_{idx:02d}.mp3" if gen_tts(role, dlg["line"], out_dir / filename): timeline.append({ "scene": scene["scene_id"], "index": idx, "role": dlg["role"], "file": filename, "text": dlg["line"] }) Path("output/audio_map.json").write_text( json.dumps(timeline, ensure_ascii=False, indent=2), encoding="utf-8") print(f"已生成 {len(timeline)} 条对白") if __name__ == "__main__": main()文件名规则scene_001_00.mp3是后面所有步骤的索引依据:场次号、对白序号都编码在里面。渲染阶段拿到时间轴直接按文件名找音频,不依赖任何数据库字段。audio_map.json是附加索引,如果你要把台词打印成字幕文件,直接用这份数据。参数上注意speed字段,它不是越快越好,短剧虽然节奏紧,但语速到1.2以上,TTS合成的口型会跟呼吸感脱节,听起来像赶场。3.3 对白时间轴:为什么字幕要等音频生成后再算整条流水线最玄学的部分就是对白时长估算。一开始我也图省事,按“每120字约等于3秒”去估每句台词长度,然后把画面时长按这个估拉好,对完字幕才发现一堆对不准。后来改成等所有音频生成完,用ffmpeg工具读真实时长回填时间轴:ffprobe -v error -show_entries format=duration -of csv=p=0 output/audio/scene_001_00.mp3输出是一个秒数,精确到小数点后三位。把每条音频的时长读出来,累加得到每一场的总时长,再反推画面需要补多少秒。这个顺序不能反过来:画面先定死了,音频时长偏长就只能快放,偏短就得补空镜头,怎么弄都是凑合。短剧一共就几十句对白,跑一次ffprobe循环不到一分钟,却能省掉后面人工对齐字幕的一两个小时,这笔账值得算。标记时间轴时还有个容易忽略的细节:TTS生成的音频首尾可能有一段静音,如果直接拿全长去拼接,画面会有一个顿挫感。常见做法是生成后裁掉首尾静音,比如用ffmpeg的silenceremove滤镜,或者在做对白拼接时统一叠一个短促淡入淡出。4. 画面进棚:分镜提示词模板、批次生成与一致性设置4.1 分镜提示词模板:固定结构比自由发挥稳定进入画面生成阶段之前,先把分镜拆清楚。分镜拆分的依据是对白音频的时长:一条对白短于4秒,配一张画面就够;超过8秒还只有一张静止图,观众会觉得画面“冻住”了。源码里的03_gen_images.py支持给一条长对白配两到三张图,切换点落在音频时长的中间位置,再用画面转场补过去。提示词这块,源码给的是固定模板思路,不是放开让模型自由发挥:def build_prompt(scene_meta, role_style, seed): # 模板固定:先定画幅和氛围,再定角色外观,最后定镜头参数 return ( "短剧截图,竖屏9:16,电影感布光,浅景深。\n" f"角色:{role_style['style_hint']},\n" f"场景:{scene_meta['title']};情绪:{scene_meta.get('emotion', '中性')}。\n" "景别:中景;镜头调度:轻微仰拍。\n" "负面提示词:模糊、变形、文字、水印、多余肢体。\n" f"seed:{seed}" )固定结构的好处是:AI模型对有序的输入更稳定,你把“角色外观”“场景”“镜头”三个信息放在固定位置,跑一百条也就只有这三个区域在变。如果每次都换一种句式描述同一个角色,画面风格会跟着漂。style_hint这一段要在每一场都出现,之前有人偷懒只在第一场写了角色穿着,到第三场AI给角色换了件衣服,后续一致性直接崩掉,这是典型的人工漏填字段导致的翻车。4.2 批次落盘:文件名就是场景标识生成脚本遍历分镜记录,把每张图写进output/images/,命名规则和音频对齐:scene_001_00.png这个序号对应audio_map里scene_001_00这条对白。# scripts/03_gen_images.py import json, subprocess from pathlib import Path def gen_image(prompt, out_path, size=(1080, 1920), seed=2024): cmd = [ "image_gen_cli", "--prompt", prompt, "--size", f"{size[0]}x{size[1]}", "--steps", "30", "--cfg", "7.0", "--seed", str(seed), "--out", str(out_path) ] r = subprocess.run(cmd, capture_output=True, text=True) return r.returncode == 0 def main(): audio_map = json.loads(Path("output/audio_map.json").read_text(encoding="utf-8")) scenes = json.loads(Path("data/scenes/scenes.json").read_text(encoding="utf-8")) roles = json.loads(Path("config/characters.json").read_text(encoding="utf-8")) out_dir = Path("output/images") out_dir.mkdir(parents=True, exist_ok=True) for entry in audio_map: role = roles.get(entry["role"], roles["default"]) scene = next(s for s in scenes if s["scene_id"] == entry["scene"]) prompt = build_prompt(scene, role, seed=2024) shot = entry["index"] * 2 # 每句对白预留 2 张图位 out_path = out_dir / f"{entry['scene']}_{entry['index']:02d}_A.png" ok = gen_image(prompt, out_path) if not ok: print(f"[image failed] {out_path}") if __name__ == "__main__": main()参数steps是采样步数,30是一个性价比比较高的位置,低于20会出现细节崩塌,高于40改善很小但耗时明显变长。cfg控制在7附近,太高画面会过度锐化,人脸容易出塑料感;太低则主体边缘发糊。seed这里固定是因为同一分镜要出备选图时,只改这个字段能保证构图一致,不是为了保证跨场一致性。4.3 批次一致性:种子、风格词与跨场复用真正决定角色跨场一致性的不是seed,而是角色卡里的style_hint。种子只能保证同一段提示词下的构图稳定,改一个词构图就会变。所以跨场一致的组合打法是这样:外观描述固定、镜头模板固定、负面提示词固定、种子逐场递增但保持同奇偶性。具体操作是给每场分配一个基准seed,比如第1场用2024,第2场用2026,两个seed相差不超过10,画面主体位置和景别最接近;跳着用seed反而容易出现上一场近景、下一场远景的割裂。目标做法不建议统一人物外观style_hint逐场照抄临场发挥统一光照方向模板固定“左侧柔光”每次重写光位统一构图seed递增且步长小随机乱设seed统一画面质感cfg、steps全片锁死中途调整参数另一个人物一致性手段是参考图:把第一场生成的角色图作为后续场次生成时的参考输入,注意这里说的参考图是局部重绘那一类做法,对短剧批量生产来说,把第一张图复制进后续提示词输入即可,但要在每张图生成后检查一遍,因为参考图会把构图偏好也带进去,导致所有分镜都一个视角,看起来非常“模板化”。5. 避坑指南:5条让成片翻车的现场记录5.1 音量忽大忽小,像换了个人配音现象:台词单句听都正常,拼进成片后这一段音量明显偏大,下一段又偏小,连起来听刺耳。原因:TTS接口对长文本和短文本的响度归一化策略不一致,短句偏安静,长句偏响,拼接时没有做统一响度处理。解决:所有对白生成后先跑一遍响度归一化,我习惯用ffmpeg的loudnorm处理到统一目标响度,再进拼接环节。处理时注意不要对整段成片一次性归一化,那样画面和音轨会一起被推高,而是按每条对白单独归一,最后拼接时加入淡入淡出,感官上会更顺滑。5.2 字幕与对白错位,先估时长就吃亏现象:生成的字幕文件里,话音已经结束了,字幕还挂在画面上。原因:按字数估算对白时长,属于经验值,每句话的实际时长波动很大,标点、语气词、多音字都会影响。解决:严格走“先生成音频、再读时长、最后算字幕”的顺序。写个小循环遍历audio_map.json,逐条读真实时长,再累加得到时间轴,任何一条对白换过文案都要重新跑一遍这一步,不手动改字幕时间戳。5.3 角色换了衣服,中途突变现象:前一场还是深色外套,后一场变成了浅色T恤,发型也变了。原因:style_hint没有逐场写入提示词,只有某一场提了外观,模型在后续场次默认自己发挥。解决:把角色外观字段写死在提示词模板里,每次生成都强制带上。更保险的做法是把第一场产出的正面图作为后续场次的参考图传入生图服务,但要注意参考图权重调低一些,只约束长相和衣着,不约束构图和景别。5.4 渲染卡死或内存溢出现象:渲染到一半内存占满,进程被系统杀掉,前面积累的音频和画面全部作废。原因:一次性把全片的分镜图片全部加载到内存里合成,短剧分镜多时内存直接爆掉。解决:改成按场渲染,每场合成一个小片段,最后再用一次拼接命令把各场视频连起来。这样即使某一场崩溃,也只需要重跑那几秒的渲染任务。源码里的04_render.py支持传--scene scene_005之类的参数,字段原来是空着的,目的就是方便单场重跑。5.5 画面偏灰发绿,调参数调不过来现象:所有生成图都蒙着一层灰绿滤镜,改提示词加“色彩鲜艳”也没效果。原因:负面提示词没有排除常见色调偏移,同时cfg设置偏高,模型把部分色彩描述吞掉了。解决:负面提示词里显式加上“偏绿、偏灰、低饱和、色彩断层”这类描述词,并把cfg降到7以下重跑一遍。先跑3张试点图,确认色调正常再全量生成,不要在参数未验证时直接铺开跑一百张,那样发现翻车就要全军覆没。6. 交付前再快一点:渲染缓存、失败续跑与成片一致性校验6.1 增量渲染与失败续跑全流程跑一次后,每次改一条台词就要重跑全链路的代价非常笨重。源码脚本里预留了--skip-existing参数,重跑时主动跳过已存在的音频和图片文件。我重跑的典型场景是改了角色语速,只需要重新生成音频,画面素材不需要动;但如果改了角色的style_hint,音频保留、图片删除后重生成,render阶段再增量拼接。把路径交付文件独立管理,是整套流水线能持续迭代的前提。python scripts/02_gen_tts.py --skip-existing python scripts/03_gen_images.py --skip-existing --scene scene_005 python scripts/04_render.py --scene scene_0056.2 成片一致性校验渲染完不要直接拷走,花三十秒做一道校验:用ffmpeg确认成片时长与音频地图总长误差在正负0.5秒以内,再记录一个文件哈希,方便后续确认交付的文件没有被二次转码。ffprobe -v error -show_entries format=duration -of csv=p=0 output/final/final.mp4 md5sum output/final/final.mp4时长偏差超过0.5秒,说明转场或音画对齐出问题;哈希值每次渲染都会变,但如果别人发回来的文件哈希和你的记录对不上,就说明被人转过码,画面质量可能已经受损。我从那次内存炸掉重跑三遍之后,每次跑短剧流水线都强制先做增量再校验交付,宁可慢一分钟也不赌全链路一次成功。希望帮到你。本文还有配套的精品资源,点击获取
简介:AI短剧创作教程源码包,专为希望借助AI工具快速产出短剧的内容创作者与开发者设计。资源围绕AiPy工具,覆盖从故事主题设定、提示词生成剧本,到旁白与分镜头视频制作、人物一致性维护,直至音视频合成出片的完整流程,配有可直接套用的提示词模板,零基础用户也能一步步做出连续、连贯的短剧。包内共3个文件,以HTML格式的交互式教程为主体,并附带inscode工程配置与gitignore辅助文件,压缩包仅7KB,结构精简、上手成本极低。已有1283人学习。通过源码可直观查看分镜头示例、剧本大纲写法及工程运行配置,既能当作速成教程,也可作为AI短剧生成工具的二次开发参考,帮助理解AI在剧本创作、场景构建与自动剪辑中的实际落地方式。1. AI短剧创作教程源码包:它到底帮你做了什么AI短剧创作这件事,绝大多数人卡住的地方不是“不会写”,而是“写出来之后怎么把它变成一条真正的视频”。我之前接过一个模拟项目X:一天内要出一段60秒短剧样片,写剧本只花了半小时,剩下六个半小时全在解决“AI生成的音频和画面怎么对上、怎么导进剪辑轨道”。这套AI短剧创作教程源码包给我的感觉,就是有人先把这套流水线配好了。它以剧本拆分为起点,覆盖TTS配音、分镜画面生成、时间轴对齐、最终渲染,适合两类人:一类是刚入行的开发者,想看看一条短剧pipeline到底要分几段;另一类是已经在手搓工具但整条链路断断续续的人,拿来对一对自己的接口设计。下面我把骨架拆给你看。2. 先立骨架:拆目录、配角色卡,把“拍戏”变成数据2.1 源码包目录:入口、配置与产物分开拿到源码包第一步不要急着跑,先看目录。短剧和单点AI工具不同,它是一条强流程的流水线:剧本拆分、配音生成、画面生成、视频渲染,四段各自独立又互相依赖。源码带的目录结构是这样:project_x/ ├── config/ │ ├── settings.yaml # 全局参数:分辨率、帧率、输出路径 │ └── characters.json # 角色卡:音色、语速、外观描述 ├── scripts/ │ ├── 01_prepare.py # 剧本md拆分成分幕JSON │ ├── 02_gen_tts.py # 逐条对白生成音频 │ ├── 03_gen_images.py # 逐分镜生成画面 │ └── 04_render.py # 合成视频 ├── data/ │ ├── script.md # 原始剧本 │ └── scenes/ # 拆分后的中间数据 ├── output/ │ ├── audio/ # TTS产出的音频文件 │ ├── images/ # 分镜图片 │ └── final/ # 渲染成品这个结构值得沿用的地方在于:四个阶段通过文件系统通信,而不是通过数据库或服务调用。音频生成完落盘为一个mp3文件,画面生成完落盘为一张png图片,渲染脚本只认目录不认内存里的对象。好处是任意阶段翻车,你都能直接看产物排查,不用在内存里打日志追变量。坏处也很明显:中间文件会占大量磁盘空间,但短剧项目单条时长短,一张1080x1920的图也就几MB,一百张图完全能接受。入口顺序是01到04,靠文件名天然排序。settings.yaml里最值得改的参数是分辨率,默认1080x1920竖屏,如果做横屏版本要同时改文件里的width和height,不然渲染阶段会拿竖屏参数切横屏素材,画面上下一截黑边。2.2 角色卡与剧情分幕:把“拍戏”翻译成结构化数据整套源码的数据源头是config/characters.json,它把“角色”抽象成一组固定键值。我习惯把它理解成给AI演员写档案:同一角色在剧本里出现十次,每次生成配音和画面都要读这张卡,而不是让模型自己回忆上一场长什么样。{ "role_01": { "name": "角色A", "voice_profile": { "engine": "tts_a", "voice": "female_calm", "speed": 0.95, "pitch": 1.02, "lang": "zh-CN" }, "style_hint": "深色短发,灰色外套,室内灯光", "scene_range": ["scene_001", "scene_008"] }, "role_02": { "name": "角色B", "voice_profile": { "engine": "tts_a", "voice": "male_deep", "speed": 1.0, "pitch": 0.95, "lang": "zh-CN" }, "style_hint": "高个子,浅色卫衣,左侧柔光", "scene_range": ["scene_001", "scene_003"] } }voice_profile里的speed是语速倍数,0.95表示比标准语速略慢,短剧对白通常语速偏快,但TTS在1.0以上容易吞字,尤其“了、呢、吗”这类语气词,所以我一般压到0.9到1.0之间。pitch是音调偏移,拉到1.1以上会明显发“电子音”,不是所有角色都适合。style_hint这行是给画面生成用的,描述要具体到什么发型、什么衣服、什么光位,后面生图阶段会逐场拼进提示词模板。01_prepare.py做的事是把原始剧本md拆成结构化的场次数据,核心就是一个正则切分:# scripts/01_prepare.py import json, re from pathlib import Path def split_scene(md_text): # 按二级标题切分,剧本里每场戏用 "## " 标记 blocks = re.split(r'^## ', md_text, flags=re.M) scenes = [] for i in range(1, len(blocks)): content = blocks[i].split('\n') title = content[0].strip() dialogue = [] for line in content[1:]: if ':' in line: role, text = line.split(':', 1) dialogue.append({ "role": role.strip(), "line": text.strip() }) scenes.append({ "scene_id": "scene_%03d" % i, "title": title, "dialogue": dialogue }) return scenes if __name__ == "__main__": md = Path("data/script.md").read_text(encoding="utf-8") scenes = split_scene(md) out = Path("data/scenes/scenes.json") out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(scenes, ensure_ascii=False, indent=2), encoding="utf-8") print(f"拆分完成:{len(scenes)}场")拆分规则只认##和冒号两个约定。如果你的剧本不是这种格式,比如用数字编号或者-列表,改re.split那行就行。拆完的scenes.json是所有后续脚本的输入,配音脚本按场次遍历,画面脚本按场次生成分镜,渲染脚本也用它来确定时长。我建议你在这一步跑完后打开scenes.json确认一条台词都没丢,这一步检查得越仔细,后面重跑的代价越低。2.3 启动检查:依赖、配置与第一条日志源码包里带了一份requirements.txt,使用前先建干净环境,路径和依赖错了会一路错到渲染结束。我的习惯是这样启动:cd project_x python -m venv venv source venv/bin/activate python -m pip install -r requirements.txt python scripts/01_prepare.py用python -m pip而不是直接pip,是为了避免多版本Python环境下装错包。装完依赖后先只跑01_prepare.py,它是最轻量的步骤,能看到拆分日志就算环境通了。如果这一步报ImportError,大概率是依赖没装上,回到上面重装;如果报编码错误,把script.md转成UTF-8编码,而不是去改读取逻辑。全局参数集中在config/settings.yaml,这里放的是不经常改但影响全链路的配置:scene_dir: data/scenes audio_dir: output/audio image_dir: output/images final_dir: output/final fps: 30 resolution: width: 1080 height: 1920fps我固定用30,60会让渲染时间翻倍,对短剧来说观感提升有限。resolution决定了后面所有画面的长宽比,中途改会让已生成的图片白做,所以第一步就定死。3. 让角色开口:TTS选型、脚本驱动与对白时间轴对齐3.1 TTS选型:两个边界条件源码里TTS这块给了两套接法:一套接系统级TTS服务,一套接本地模型。做短剧选哪套,我只看两个条件:一是音色稳定性,二是批处理吞吐。系统级服务接入成本低,几句代码就能出声,但同一句话重跑两次音色会有细微漂移,单条对白听不出问题,串成一部片子就会觉得角色“忽老忽年轻”。本地模型第一次要下模型文件,之后全程离线跑,参数锁死后音色稳定得多,代价是显存占用量和更慢的生成速度。对比项系统级TTS服务本地模型接入成本低,配好密钥即可中,需要处理模型文件与依赖音色稳定性一般,多次调用有漂移高,锁参数后基本不变批处理速度快,并发接口慢,串行为主可控参数少,通常只有语速和音色多,pitch、停顿、重音都能调短剧的台词量不大,一条60秒成片一般就20到30句对白,吞吐量不是瓶颈,音色稳定才是。所以我在这个项目里选了本地模型路线,并且把参数全部写在角色卡里,任何一场生成都读同一套配置。3.2 从剧本JSON到对白音频:驱动脚本02_gen_tts.py的作用很简单:遍历scenes.json里的每一条对白,读取角色卡配置,调TTS接口生成音频文件,落盘到output/audio/下。# scripts/02_gen_tts.py import json, subprocess, time from pathlib import Path def gen_tts(role_profile, line, out_path): vp = role_profile["voice_profile"] cmd = [ "tts_cli", "--model", vp["engine"], "--voice", vp["voice"], "--speed", str(vp["speed"]), "--pitch", str(vp["pitch"]), "--lang", vp["lang"], "--text", line, "--out", str(out_path) ] for attempt in range(3): # 失败重试3次,避免网络抖动直接断掉管线 r = subprocess.run(cmd, capture_output=True, text=True) if r.returncode == 0: return True print(f"[retry {attempt+1}] {line[:20]}") time.sleep(2) print(f"[FATAL] {line} -> {r.stderr[-300:]}") return False def main(): scenes = json.loads(Path("data/scenes/scenes.json").read_text(encoding="utf-8")) roles = json.loads(Path("config/characters.json").read_text(encoding="utf-8")) out_dir = Path("output/audio") out_dir.mkdir(parents=True, exist_ok=True) timeline = [] for scene in scenes: for idx, dlg in enumerate(scene["dialogue"]): role = roles.get(dlg["role"], roles["default"]) filename = f"{scene['scene_id']}_{idx:02d}.mp3" if gen_tts(role, dlg["line"], out_dir / filename): timeline.append({ "scene": scene["scene_id"], "index": idx, "role": dlg["role"], "file": filename, "text": dlg["line"] }) Path("output/audio_map.json").write_text( json.dumps(timeline, ensure_ascii=False, indent=2), encoding="utf-8") print(f"已生成 {len(timeline)} 条对白") if __name__ == "__main__": main()文件名规则scene_001_00.mp3是后面所有步骤的索引依据:场次号、对白序号都编码在里面。渲染阶段拿到时间轴直接按文件名找音频,不依赖任何数据库字段。audio_map.json是附加索引,如果你要把台词打印成字幕文件,直接用这份数据。参数上注意speed字段,它不是越快越好,短剧虽然节奏紧,但语速到1.2以上,TTS合成的口型会跟呼吸感脱节,听起来像赶场。3.3 对白时间轴:为什么字幕要等音频生成后再算整条流水线最玄学的部分就是对白时长估算。一开始我也图省事,按“每120字约等于3秒”去估每句台词长度,然后把画面时长按这个估拉好,对完字幕才发现一堆对不准。后来改成等所有音频生成完,用ffmpeg工具读真实时长回填时间轴:ffprobe -v error -show_entries format=duration -of csv=p=0 output/audio/scene_001_00.mp3输出是一个秒数,精确到小数点后三位。把每条音频的时长读出来,累加得到每一场的总时长,再反推画面需要补多少秒。这个顺序不能反过来:画面先定死了,音频时长偏长就只能快放,偏短就得补空镜头,怎么弄都是凑合。短剧一共就几十句对白,跑一次ffprobe循环不到一分钟,却能省掉后面人工对齐字幕的一两个小时,这笔账值得算。标记时间轴时还有个容易忽略的细节:TTS生成的音频首尾可能有一段静音,如果直接拿全长去拼接,画面会有一个顿挫感。常见做法是生成后裁掉首尾静音,比如用ffmpeg的silenceremove滤镜,或者在做对白拼接时统一叠一个短促淡入淡出。4. 画面进棚:分镜提示词模板、批次生成与一致性设置4.1 分镜提示词模板:固定结构比自由发挥稳定进入画面生成阶段之前,先把分镜拆清楚。分镜拆分的依据是对白音频的时长:一条对白短于4秒,配一张画面就够;超过8秒还只有一张静止图,观众会觉得画面“冻住”了。源码里的03_gen_images.py支持给一条长对白配两到三张图,切换点落在音频时长的中间位置,再用画面转场补过去。提示词这块,源码给的是固定模板思路,不是放开让模型自由发挥:def build_prompt(scene_meta, role_style, seed): # 模板固定:先定画幅和氛围,再定角色外观,最后定镜头参数 return ( "短剧截图,竖屏9:16,电影感布光,浅景深。\n" f"角色:{role_style['style_hint']},\n" f"场景:{scene_meta['title']};情绪:{scene_meta.get('emotion', '中性')}。\n" "景别:中景;镜头调度:轻微仰拍。\n" "负面提示词:模糊、变形、文字、水印、多余肢体。\n" f"seed:{seed}" )固定结构的好处是:AI模型对有序的输入更稳定,你把“角色外观”“场景”“镜头”三个信息放在固定位置,跑一百条也就只有这三个区域在变。如果每次都换一种句式描述同一个角色,画面风格会跟着漂。style_hint这一段要在每一场都出现,之前有人偷懒只在第一场写了角色穿着,到第三场AI给角色换了件衣服,后续一致性直接崩掉,这是典型的人工漏填字段导致的翻车。4.2 批次落盘:文件名就是场景标识生成脚本遍历分镜记录,把每张图写进output/images/,命名规则和音频对齐:scene_001_00.png这个序号对应audio_map里scene_001_00这条对白。# scripts/03_gen_images.py import json, subprocess from pathlib import Path def gen_image(prompt, out_path, size=(1080, 1920), seed=2024): cmd = [ "image_gen_cli", "--prompt", prompt, "--size", f"{size[0]}x{size[1]}", "--steps", "30", "--cfg", "7.0", "--seed", str(seed), "--out", str(out_path) ] r = subprocess.run(cmd, capture_output=True, text=True) return r.returncode == 0 def main(): audio_map = json.loads(Path("output/audio_map.json").read_text(encoding="utf-8")) scenes = json.loads(Path("data/scenes/scenes.json").read_text(encoding="utf-8")) roles = json.loads(Path("config/characters.json").read_text(encoding="utf-8")) out_dir = Path("output/images") out_dir.mkdir(parents=True, exist_ok=True) for entry in audio_map: role = roles.get(entry["role"], roles["default"]) scene = next(s for s in scenes if s["scene_id"] == entry["scene"]) prompt = build_prompt(scene, role, seed=2024) shot = entry["index"] * 2 # 每句对白预留 2 张图位 out_path = out_dir / f"{entry['scene']}_{entry['index']:02d}_A.png" ok = gen_image(prompt, out_path) if not ok: print(f"[image failed] {out_path}") if __name__ == "__main__": main()参数steps是采样步数,30是一个性价比比较高的位置,低于20会出现细节崩塌,高于40改善很小但耗时明显变长。cfg控制在7附近,太高画面会过度锐化,人脸容易出塑料感;太低则主体边缘发糊。seed这里固定是因为同一分镜要出备选图时,只改这个字段能保证构图一致,不是为了保证跨场一致性。4.3 批次一致性:种子、风格词与跨场复用真正决定角色跨场一致性的不是seed,而是角色卡里的style_hint。种子只能保证同一段提示词下的构图稳定,改一个词构图就会变。所以跨场一致的组合打法是这样:外观描述固定、镜头模板固定、负面提示词固定、种子逐场递增但保持同奇偶性。具体操作是给每场分配一个基准seed,比如第1场用2024,第2场用2026,两个seed相差不超过10,画面主体位置和景别最接近;跳着用seed反而容易出现上一场近景、下一场远景的割裂。目标做法不建议统一人物外观style_hint逐场照抄临场发挥统一光照方向模板固定“左侧柔光”每次重写光位统一构图seed递增且步长小随机乱设seed统一画面质感cfg、steps全片锁死中途调整参数另一个人物一致性手段是参考图:把第一场生成的角色图作为后续场次生成时的参考输入,注意这里说的参考图是局部重绘那一类做法,对短剧批量生产来说,把第一张图复制进后续提示词输入即可,但要在每张图生成后检查一遍,因为参考图会把构图偏好也带进去,导致所有分镜都一个视角,看起来非常“模板化”。5. 避坑指南:5条让成片翻车的现场记录5.1 音量忽大忽小,像换了个人配音现象:台词单句听都正常,拼进成片后这一段音量明显偏大,下一段又偏小,连起来听刺耳。原因:TTS接口对长文本和短文本的响度归一化策略不一致,短句偏安静,长句偏响,拼接时没有做统一响度处理。解决:所有对白生成后先跑一遍响度归一化,我习惯用ffmpeg的loudnorm处理到统一目标响度,再进拼接环节。处理时注意不要对整段成片一次性归一化,那样画面和音轨会一起被推高,而是按每条对白单独归一,最后拼接时加入淡入淡出,感官上会更顺滑。5.2 字幕与对白错位,先估时长就吃亏现象:生成的字幕文件里,话音已经结束了,字幕还挂在画面上。原因:按字数估算对白时长,属于经验值,每句话的实际时长波动很大,标点、语气词、多音字都会影响。解决:严格走“先生成音频、再读时长、最后算字幕”的顺序。写个小循环遍历audio_map.json,逐条读真实时长,再累加得到时间轴,任何一条对白换过文案都要重新跑一遍这一步,不手动改字幕时间戳。5.3 角色换了衣服,中途突变现象:前一场还是深色外套,后一场变成了浅色T恤,发型也变了。原因:style_hint没有逐场写入提示词,只有某一场提了外观,模型在后续场次默认自己发挥。解决:把角色外观字段写死在提示词模板里,每次生成都强制带上。更保险的做法是把第一场产出的正面图作为后续场次的参考图传入生图服务,但要注意参考图权重调低一些,只约束长相和衣着,不约束构图和景别。5.4 渲染卡死或内存溢出现象:渲染到一半内存占满,进程被系统杀掉,前面积累的音频和画面全部作废。原因:一次性把全片的分镜图片全部加载到内存里合成,短剧分镜多时内存直接爆掉。解决:改成按场渲染,每场合成一个小片段,最后再用一次拼接命令把各场视频连起来。这样即使某一场崩溃,也只需要重跑那几秒的渲染任务。源码里的04_render.py支持传--scene scene_005之类的参数,字段原来是空着的,目的就是方便单场重跑。5.5 画面偏灰发绿,调参数调不过来现象:所有生成图都蒙着一层灰绿滤镜,改提示词加“色彩鲜艳”也没效果。原因:负面提示词没有排除常见色调偏移,同时cfg设置偏高,模型把部分色彩描述吞掉了。解决:负面提示词里显式加上“偏绿、偏灰、低饱和、色彩断层”这类描述词,并把cfg降到7以下重跑一遍。先跑3张试点图,确认色调正常再全量生成,不要在参数未验证时直接铺开跑一百张,那样发现翻车就要全军覆没。6. 交付前再快一点:渲染缓存、失败续跑与成片一致性校验6.1 增量渲染与失败续跑全流程跑一次后,每次改一条台词就要重跑全链路的代价非常笨重。源码脚本里预留了--skip-existing参数,重跑时主动跳过已存在的音频和图片文件。我重跑的典型场景是改了角色语速,只需要重新生成音频,画面素材不需要动;但如果改了角色的style_hint,音频保留、图片删除后重生成,render阶段再增量拼接。把路径交付文件独立管理,是整套流水线能持续迭代的前提。python scripts/02_gen_tts.py --skip-existing python scripts/03_gen_images.py --skip-existing --scene scene_005 python scripts/04_render.py --scene scene_0056.2 成片一致性校验渲染完不要直接拷走,花三十秒做一道校验:用ffmpeg确认成片时长与音频地图总长误差在正负0.5秒以内,再记录一个文件哈希,方便后续确认交付的文件没有被二次转码。ffprobe -v error -show_entries format=duration -of csv=p=0 output/final/final.mp4 md5sum output/final/final.mp4时长偏差超过0.5秒,说明转场或音画对齐出问题;哈希值每次渲染都会变,但如果别人发回来的文件哈希和你的记录对不上,就说明被人转过码,画面质量可能已经受损。我从那次内存炸掉重跑三遍之后,每次跑短剧流水线都强制先做增量再校验交付,宁可慢一分钟也不赌全链路一次成功。希望帮到你。本文还有配套的精品资源,点击获取