
简介这是一款面向短剧创作者与AI开发者的可运行短剧生成项目源码支持仅输入一句话即可走通剧本编写、角色与场景提取、分镜拆解、配音及视频合成全流程。包体内含94个文件以TypeScript源码和Vue前端组件为主辅以Markdown说明文档、JSON配置及Docker部署文件整体仅634KB目录清晰拆分为后端与前端两大工程并集成音色分配、分镜拆解、脚本改写等智能模块便于二次开发与本地部署。已有327人学习/下载适合希望快速搭建AI视频生成工具链、研究多模态工作流或构建短视频自动化生产系统的读者。通过该项目可掌握大语言模型解析剧本、AI绘图生成角色场景、文生视频与图生视频合成分镜的技术落地方法并获得可运行的完整工程模板与部署脚本。1. 一句话开工三分钟成片AI短剧生成平台为什么值得搭AI短剧生成平台不是“用大模型写小说”的玩具而是一条把用户输入的一句话变成可发布短剧的工程流水线剧本编写、角色和场景提取、分镜生成、配音、视频合成五个环节串起来才是完整闭环。这个方向最实用的场景是批量做信息流短剧、口播素材和电商剧情号。对从业者来说它最大的价值不是“一键生成”而是每一段中间产物可看、可改、可复跑——这也是我动手前先想清楚的事。下文把整条链路的实现思路、安装部署和踩坑记录都摊开讲新手能照着搭熟手可以直接拿参数去调自己的工程。2. 五段式链路拆解剧本、角色场景、分镜、配音、合成的协作关系在一口气写代码之前先确认一件事这条链路一定要拆成五个独立阶段而不是让大模型一次把视频吐出来。原因很实际上下文窗口再宽也很难在同一轮输出里同时保证剧情连贯、镜头节奏、台词可读和配音时长匹配。更重要的分拆之后每个阶段的产物都能被人工检查或替换哪怕最后不需要人工干预排错时也少很多黑匣子。我的经验是一个能跑通的“一句话成片”系统核心是把LLM的角色限定在“内容策划”上把声音合成交给专门的TTS把画面拼接交给FFmpeg。全链路参数可控模型迭代才不至于牵一发动全身。2.1 为什么拆成五段而不是一条 prompt 出一个成片如果把“一句话生成一条短剧”当成一次LLM调用你会撞上三个问题。第一是输出不稳定模型生成的JSON字段会漏台词和分镜比例会失衡第二是成本不可控一次生成长文本的token消耗远大于分段调用中途失败就得全部重来第三是后续想单独调某个角色的音色、某段镜头的时长分段结构才能只重跑受影响的部分。对比项单次出片五段式链路失败重跑成本全链路重来烧完整token只重跑失败阶段中间产物可检视无纯黑匣子每阶段落盘JSON换模型/TTS引擎牵一发动全身模块级替换人工介入机会几乎为零改完JSON继续跑换句话说分段的本质是把大模型当成一个协作的ai agent团队编剧Agent输出剧本策划Agent提取角色场景导演Agent拆分镜然后才轮到TTS和视频引擎干活。我一般保留一条总控脚本把五个阶段按顺序执行并在每个阶段结束后把结果落盘成JSON。这样无论哪一步翻车都能从上一个断点恢复而不是从头再烧一遍接口费用。这个“阶段间只传JSON、不传中间视频”的约定是整个工程能稳定迭代的底座。2.2 每个环节的选型LLM做剧情、Edge-TTS做声音、FFmpeg合成画面剧本生成、角色与场景提取、分镜拆分都用同一个LLM API只是切换不同的system prompt和temperature。选型上只要是兼容OpenAI Chat Completions协议的接口都行本地部署就用Ollama这些网关改造base_url指过去。这个阶段对“推理能力”要求不高但对“指令跟随”和“JSON输出稳定性”要求很高——我发现不少模型写文案可以让它严格输出JSON结构就变形。配音用Edge-TTS免费、延迟低、中文声音多命令行就可以调用适合批量跑。音色分配按角色性格来温柔的女声用晓晓沉稳男声用云希旁白单独挑一个中性音色让整条片子的听觉层次拉开。画面合成本来考虑过文生视频模型但实测两个问题生成画面可控性差人物嘴型和台词对不上短视频的实际需求里图文分镜加配音反而是成本最低、最不容易翻车的形式。所以画面层用静态背景图加字幕撑住镜头交给FFmpeg做静帧动画和音画封装。这么一选整条链路只有LLM调用是云端依赖其他环节本地就能完成。部署成本低后续也方便把LLM换成自建模型做二次改造。有人问为什么不直接本地跑开源模型我觉得分情况只是验证流程云端API更快要生产长期跑本地模型省的是单次调用成本但显存和推理速度也会限制并发先按API跑通再决定要不要下沉。2.3 贯穿全程的数据契约script JSON、roles、scenes、shots五个阶段之间靠的是一份固定的数据契约我建议在工程里建一个models.py定义数据结构不要每层随意传字典。最核心的四个字段组分别是script包含标题、一句话概念和若干场次每场有地点、时间段和节拍节拍里记录角色名、动作和台词roles角色表包含character_id、姓名、性别、性格、声音提示这是后面分配TTS音色的依据scenes场景表包含场景ID、名称、地点、时间、氛围和画面设定分镜阶段在这里挑背景素材shots分镜表每个镜头带景别、时长、对白、旁白、画面描述和所属角色。只要这四个字段名固定后面换TTS、换视频引擎都不用改动整个流水线。从接需求的经验看这是源码工程里最值得投资的地方。字段名尽量用英文小写加下划线避免不同平台处理中文键名时出编码意外。还有一个容易忽略的点每个阶段保存的JSON都要带一个version字段因为提示词迭代之后新生成的shots结构和旧的可能不一样没有版本标记后面加载旧产物时会一头雾水。3. 安装部署与目录结构源码包里的工程怎么跑起来3.1 环境准备Python 3.10、虚拟环境与依赖安装先把Python版本固定在3.10以上Edge-TTS和OpenAI SDK都依赖较新的asyncio特性用3.8踩出来的坑不值得重复。建虚拟环境把依赖写成requirements.txt一次性装好python3 -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install -r requirements.txtrequirements.txt建议放这几项openai1.40.0 edge-tts6.1.0 python-dotenv1.0.0 requests2.31.0 pillow10.0.0这里没有把ffmpeg写进pip依赖因为FFmpeg是独立二进制要系统层面安装。macOS用brew install ffmpegUbuntu用apt install ffmpegWindows直接下载静态构建配PATH。装完跑一句ffmpeg -version确认能执行这一步出问题后面视频合成就无从谈起。补充一个细节如果不确定当前环境有没有旧包冲突建议先pip list看一眼重点对齐numpy、Pillow这两个容易起冲突的库版本。很多人第一步跑不起来不是源码的问题是环境里已有旧版edge-tts或openai把SDK接口顶掉了。遇到过最典型的是旧版openai没有client.chat.completions这个属性换1.x之后代码才能跑。3.2 工程目录结构五个阶段对应五个模块拿到源码包先花两分钟看目录结构。常见组织方式是模块名和阶段一一对应看到什么就知道去哪改什么ai_short_drama/ ├── main.py ├── pipeline/ │ ├── __init__.py │ ├── models.py │ ├── prompts.py │ ├── script_gen.py │ ├── extract.py │ ├── shot_split.py │ ├── tts_runner.py │ └── video_composer.py ├── assets/ │ ├── scenes/ │ └── fonts/ ├── config/ │ └── .env.example └── outputs/ ├── script/ ├── shots/ ├── audio/ ├── frames/ └── videos/pipeline里每个模块对应链条中的一环script_gen.py生成剧本extract.py提取角色和场景shot_split.py拆镜头tts_runner.py配音video_composer.py合成视频。outputs下按阶段分目录保存中间产物方便随时查看哪一步的结果不对。配置集中在.env文件里源码包一般会给一个.env.example复制成.env填值# config/.env LLM_API_KEYsk-your-key LLM_BASE_URLhttps://api.deepseek.com/v1 LLM_MODELdeepseek-chat TTS_VOICE_NARRATORzh-CN-XiaoxiaoNeural TTS_VOICE_MALEzh-CN-YunxiNeural TTS_VOICE_FEMALEzh-CN-XiaoyiNeural OUTPUT_DIRoutputs MAX_SHOTS12配置项作用建议值LLM_API_KEY大模型接口密钥环境变量注入别硬编码LLM_BASE_URL接口网关地址云厂商或本地Ollama均可LLM_MODEL生成用的模型名按厂商文档填TTS_VOICE_*不同类型角色音色中文配音常用晓晓/云希OUTPUT_DIR中间产物根目录默认outputsMAX_SHOTS单次生成最大镜头数12以内控时长重点说MAX_SHOTS。它限制一次生成分镜的最大数量防止短视频平台时长限制和LLM输出的镜头数对不上生成一个十几分钟的“长片”。三分钟以内的短剧12个镜头是比较稳的上限再多反而节奏拖沓。3.3 首次冒烟测试跑通配置、目录和LLM连通性不建议一上来就跑完整流程先做两步冒烟测试。第一步确认依赖到位源码包里如果有自检脚本就优先用没有就自己写一小段验证检查环境变量和FFmpeg是否可用import os import subprocess from dotenv import load_dotenv load_dotenv(config/.env) assert os.getenv(LLM_API_KEY), LLM_API_KEY 未配置 subprocess.run([ffmpeg, -version], capture_outputTrue, checkTrue) print(依赖检查通过)第二步用一个很短的prompt调一次剧本生成模块验证LLM接口连通和返回格式能解析。这一步过了再上完整链路。我这里踩过一次坑换了新模型之后LLM_BASE_URL没改冒烟测试直接超时倒回去才发现是网关地址配错。冒烟测试的价值就在这里把配置问题和业务问题分开别让第一次翻车背锅给源码。4. 核心实现逐段过剧本JSON、角色场景提取、分镜与TTS合成代码从这一章开始按源码包里的实际分工把五个阶段逐个走通。以一个具体输入为例用户只给一句话“一个失业程序员回到十年前决定开发AI短剧生成平台”。后续所有代码都围绕这句话展开方便对照中间产物是否合理。4.1 一句话变剧本让LLM按JSON Schema输出剧情剧本生成是整个平台的第一道工序输入一句话输出结构化剧本JSON。关键是把JSON结构写死在system prompt里模型才不会自由发挥。提示词单独放在prompts.py# pipeline/prompts.py SCRIPT_PROMPT 你是一名短剧编剧。根据用户给的一句话故事梗概创作一部适合竖屏短视频的短剧。 输出为 JSON不要用 Markdown 包裹不要额外说明。 JSON 结构 { title: 短剧标题, logline: 一句话概念, scenes: [ { scene_id: 1, location: 地点, time: 日/夜, summary: 本场剧情概括, beats: [ {character: 角色名, action: 动作描述, dialogue: 台词} ] } ] } 要求 - 全剧 3~5 场每场 2~5 个节拍 - 台词口语化每句不超过 30 字 - 结尾要有一个反转或钩子 然后是调用代码。重点有两个一是temperature控制在0.7保证有创造性又不至于跑偏二是处理LLM返回内容被json代码块包裹的情况直接json.loads会炸必须先剥标记。# pipeline/script_gen.py import json from openai import OpenAI from .prompts import SCRIPT_PROMPT def generate_script(client: OpenAI, user_prompt: str, output_dir: str) - dict: resp client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: SCRIPT_PROMPT}, {role: user, content: user_prompt}, ], temperature0.7, max_tokens2048, ) raw resp.choices[0].message.content.strip() if raw.startswith(): # 去掉 Markdown 代码块 raw raw.split(\n, 1)[1].rsplit(, 1)[0].strip() data json.loads(raw) # 解析失败就抛出由上层重试 with open(f{output_dir}/script.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) return data把剧本单独写文件是因为剧本是后续所有阶段的上游肉眼检查一遍再往下走能避免连锁错误。ensure_asciiFalse必须写否则中文会变成\u转义序列人没法读也会影响后续提示词拼接。解析失败时不要静默吞掉我一般让上层捕获异常后重试一次并把原始文本一起打印方便定位是模型输出截断还是格式问题。4.2 角色和场景提取把剧本里的隐含信息表单化剧本生成完里面只有“角色名”和“地点”还没有可用于配音和选背景的角色档案。这一步提取角色表、场景表让角色拥有性格和音色提示让场景拥有氛围和画面描述。提取任务不需要创意重点在稳定所以temperature降到0.3并尽量让模型输出固定结构。API支持response_format{type: json_object}就用后端不支持就删掉改用提示词强约束。# pipeline/extract.py def extract_roles_and_scenes(client: OpenAI, script: dict) - tuple[list, list]: prompt f根据下面的剧本 JSON提取角色表和场景表。 角色字段character_id, name, gender, personality, voice_hint, goal 场景字段scene_id, name, location, time, atmosphere, visual_setting 只输出 JSON结构为 {{roles: [], scenes: []}} 剧本 {json.dumps(script, ensure_asciiFalse)} resp client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: prompt}], temperature0.3, response_format{type: json_object}, ) data json.loads(resp.choices[0].message.content) roles, scenes data[roles], data[scenes] # 校验必需字段缺失字段用默认值兜底 for role in roles: role.setdefault(voice_hint, 沉稳) role.setdefault(goal, ) return roles, scenesvoice_hint在这里只是一个文本标签比如“温柔”“热血”“阴险”真正的音色映射放在TTS阶段处理。把这个映射推迟的好处是可以先跑一版听几轨配音再手动调整角色和音色的对应关系不必重新生成剧本。场景表里的visual_setting会被分镜生成引用构造画面描述相当于给模型一个“布景参考”比让它凭空想象场景可靠得多。4.3 分镜生成给每个镜头定景别、时长和画面描述分镜是整个平台里最像“导演工作”的一步。输入是剧本、角色表和场景表输出是一个shots数组每个镜头包含镜头序号、所属场景、景别、预估时长、台词/旁白、画面描述、出镜角色。这一步建议max_tokens4096因为分镜表往往比较长截断会导致缺镜头。# pipeline/shot_split.py def split_shots(client: OpenAI, script: dict, roles: list, scenes: list) - list: payload { script: script, roles: roles, scenes: scenes, shot_rule: 竖屏短视频单个镜头 2~6 秒全片不超过 12 个镜头, } resp client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: SHOT_PROMPT}, {role: user, content: json.dumps(payload, ensure_asciiFalse)}, ], temperature0.4, max_tokens4096, ) shots json.loads(resp.choices[0].message.content)[shots] for i, shot in enumerate(shots, 1): shot[shot_id] fshot_{i:03d} if not shot.get(duration_sec): shot[duration_sec] estimate_duration(shot.get(dialogue, )) return shots def estimate_duration(dialogue: str) - float: # 按中文口语每分钟 220 字估算基础 1.5 秒最低 2 秒 n max(len(dialogue), 4) return max(round(n / 3.7 1.5, 1), 2.0)这里的时长估算只是临时值真实做法是等配音完成之后用ffprobe读音频真实时长回填覆盖避坑章节会细说。分镜的shot_type字段建议用“远景/全景/中景/近景/特写”五档画面描述尽量带上镜头运动和人物动作Pillow渲染静态帧时才有内容可画。如果生成结果里镜头时长明显不合理先检查shot_rule那句约束是否传进去了很多模型对末尾约束的遵循度会下降。4.4 配音落地Edge-TTS并发转MP3并回填时长配音阶段把每个镜头的台词或旁白转成音频记录文件路径。选Edge-TTS因为它免费、输出稳定、异步批量生成方便。角色音色的映射放在配置里比如旁白用晓晓、男角色用云希、女角色用晓伊。批量生成时如果对每个镜头都直接await Communicate.save()很容易触发服务端连接限制。建议用asyncio.Semaphore(3)限制并发并给单镜头加失败重试。# pipeline/tts_runner.py import asyncio import edge_tts from pathlib import Path import subprocess VOICE_MAP { 旁白: zh-CN-XiaoxiaoNeural, 男: zh-CN-YunxiNeural, 女: zh-CN-XiaoyiNeural, 老年男: zh-CN-YunjianNeural, } async def _gen_one(shot: dict, voice: str, audio_dir: Path, sem: asyncio.Semaphore): text shot.get(dialogue) or shot.get(narration) or if not text.strip(): shot[audio_path] return mp3_path audio_dir / f{shot[shot_id]}.mp3 for attempt in range(2): # 失败重试 2 次 try: async with sem: communicate edge_tts.Communicate(text, voice, rate10%) await communicate.save(str(mp3_path)) shot[audio_path] str(mp3_path) shot[duration_sec] probe_duration(mp3_path) # 回填真实时长 return except Exception: if attempt 1: raise await asyncio.sleep(1.5) def probe_duration(mp3_path: Path) - float: result subprocess.run( [ffprobe, -v, error, -show_entries, formatduration, -of, defaultnoprint_wrappers1:nokey1, str(mp3_path)], capture_outputTrue, textTrue, checkTrue) return round(float(result.stdout.strip()), 1) def run_tts(shots: list, output_dir: str): audio_dir Path(output_dir) / audio audio_dir.mkdir(parentsTrue, exist_okTrue) sem asyncio.Semaphore(3) asyncio.run(asyncio.gather( *(_gen_one(s, VOICE_MAP.get(s.get(gender, 女)), audio_dir, sem) for s in shots if s.get(dialogue) or s.get(narration)) ))rate10%是我试了很多条中文配音之后比较顺耳的语速比默认的“念课文感”好很多。想再调pitch5Hz也可以加一点优先级不高。镜头里如果只有动作没有台词audio_path留空后面合成时用短暂静音占位不要让全片出现长时间无声的尴尬。回填真实时长是关键动作直接把后面视频合成的对拍问题解决了一半。4.5 视频合成Pillow出帧、FFmpeg拼片与字幕烧录最后阶段把每个镜头渲染成一帧带字幕的背景图然后调用FFmpeg把静帧音频封装成小片段最后把所有小片段拼成完整MP4。这里是全工程最容易出现比例变形和音画不同步的地方。# pipeline/video_composer.py from PIL import Image, ImageDraw, ImageFont import subprocess from pathlib import Path def render_frame(shot: dict, scene_asset: str, output_png: str, size(720, 1280)): bg Image.open(scene_asset).convert(RGB) # cover 裁剪取图片中心区域等比缩放到目标尺寸 bg_ratio, target_ratio bg.width / bg.height, size[0] / size[1] if bg_ratio target_ratio: new_w int(bg.height * target_ratio) bg bg.crop(((bg.width - new_w) // 2, 0, (bg.width new_w) // 2, bg.height)) else: new_h int(bg.width / target_ratio) bg bg.crop((0, (bg.height - new_h) // 2, bg.width, (bg.height new_h) // 2)) bg bg.resize(size, Image.LANCZOS) draw ImageDraw.Draw(bg) font ImageFont.truetype(assets/fonts/NotoSansCJK-Bold.ttc, 44) text shot.get(dialogue) or if text: # 简单折行最多两行 lines [text[i:i14] for i in range(0, len(text), 14)] y size[1] - 180 for line in lines[:2]: draw.text((40, y), line, fill(255, 255, 255), fontfont) y 60 bg.save(output_png) def compose_videos(shots: list, output_dir: str): video_dir Path(output_dir) / videos video_dir.mkdir(parentsTrue, exist_okTrue) list_file video_dir / concat_list.txt with open(list_file, w) as fp: for shot in shots: frame_png f{output_dir}/frames/{shot[shot_id]}.png audio shot.get(audio_path) seg f{video_dir}/{shot[shot_id]}.mp4 if audio: cmd [ffmpeg, -y, -loop, 1, -framerate, 30, -i, frame_png, -i, audio, -c:v, libx264, -tune, stillimage, -pix_fmt, yuv420p, -c:a, aac, -shortest, seg] else: cmd [ffmpeg, -y, -loop, 1, -framerate, 30, -i, frame_png, -t, 2, -c:v, libx264, -pix_fmt, yuv420p, seg] subprocess.run(cmd, checkTrue, capture_outputTrue) fp.write(ffile {seg}\n) subprocess.run([ffmpeg, -y, -f, concat, -safe, 0, -i, str(list_file), -c, copy, f{video_dir}/final.mp4], checkTrue)两个细节值得注意。第一-shortest参数保证输出时长以音频为准否则FFmpeg默认会一直循环播放静帧直到视频流结束容易出现一个镜头拖十几秒的怪相。第二concat拼接用-c copy前提是所有片段编码参数一致所以前面每个镜头的视频参数必须写死同一套。如果中途想插转场就得改用xfade滤镜属于进阶玩法先跑通再谈。顺带说明字幕直接用Pillow画到帧上而不是走FFmpeg的subtitles滤镜。原因是中文字体在FFmpeg的字幕渲染里坑特别多字体名对不上、编码不对、换行不智能。把字幕当图片的一部分灵活性低一点但稳定得多不会在最后一步翻车。5. 避坑记录LLM输出、音画对齐与批量合成里的 5 个常见翻车点这章是血泪经验汇总每一条都是真实跑流水线时大概率撞上的。建议收藏等跑起来出问题时回来对照。5.1 JSON解析失败的三种形态与重试策略现象json.loads报错程序停在剧本生成这一步日志里只有一长串异常堆栈。原因基本是三种模型把JSON用json代码块包起来了模型在JSON前后加了“好的这是剧本”之类解释或者max_tokens太小JSON被截断成半截。前两种是格式问题后一种是资源问题。处理方式分两层解析前先剥代码块标记解析失败时记录原始文本并自动重试一次重试时把max_tokens上调30%。如果重试还失败就让脚本抛错并保留原始文本因为这时候大概率是prompt写得太复杂需要人工看模型到底回了什么而不是盲目再烧一次接口。5.2 分镜时长和音频实际时长对不上现象成片里有的镜头画面已经切走了配音还在说有的镜头等了很久声音早播完了。原因分镜阶段通过字数估算的时长和真实TTS时长有偏差中文口语读长句和短句差距很大。解决方式是回填TTS阶段完成后用ffprobe读取每个MP3的真实时长覆写shot[duration_sec]再进入合成。画面停留时间跟着声音走是最稳妥的对齐方式。我专门写了一个probe_duration()函数统一处理回填避免合成时临时去算。还有一个反向问题旁白类镜头没有台词字段但TTS会生成音频如果忘了回填成片开头就会黑屏几秒排查时容易被忽略。5.3 中文排版、字体和换行导致的字幕问题现象字幕显示成方块或者长台词溢出画面底部被截断。原因Pillow渲染中文字体时没指定中文字体文件回退到默认字体或者台词没折行直接顶到屏幕外。解决方式是统一用中文字体文件比如Noto Sans CJK在项目assets/fonts放一份代码里用ImageFont.truetype指定完整路径。换行逻辑按字符数切不算句子边界因为竖屏宽度有限一行14个中文字符是比较稳的经验值。还有一个容易被忽略的坑ImageFont.truetype在Windows和Linux上对路径大小写敏感路径写错时Pillow不报错只是渲染成豆腐块排查时看不出异常。5.4 TTS批量并发被限流现象配音跑到第5个镜头开始报超时或连接错误。原因Edge-TTS免费但同一个出口短时间内并发太多服务端会断连。解决方式是控制并发数我试过Semaphore(3)和Semaphore(5)实测3比较稳。重试还失败就把失败镜头记录到清单里等整批跑完后对清单单独跑一轮不要原地死循环。另外文本里如果有特殊符号如#、[]Edge-TTS会读错或跳过跑之前做一次简单文本清洗把这类符号替换成全角或直接删掉配音稳定很多。5.5 背景图 cover 裁剪和分辨率不统一现象成片里画面有的拉宽有的压扁人脸变形严重或者出现黑边。原因不同素材源图片的宽高比不统一直接resize会破坏构图。解决方式是先cover裁剪再缩放即取图片中心区域等比裁剪到目标宽高比再缩放到目标尺寸。render_frame就是按这个逻辑写的。另一个问题是分辨率竖屏短剧统一720x1280不要有的镜头1080x1920、有的720x1280否则concat参数不一致直接失败。如果遇到FFmpeg提示分辨率不一致在合成前统一加一道-vf scale720:1280处理。6. 进阶验证与调优用最小干预把生成质量拉稳平台跑通之后下一个问题是怎么保证每次生成的不是“能跑但很烂”。我的做法是给整条流水线加一个轻量评分器放在所有阶段完成后。评分器不调大模型只做三件事检查shots数量是否在预期范围、每个镜头audio_path对应的文件是否存在且时长大于1秒、最终视频用ffprobe读出时长和分辨率是否符合设定。这串检查看起来很朴素但能拦住80%的翻车片。分数低于阈值时自动把失败阶段的日志打印出来并重新执行一次分镜生成而不是整个流程重跑。更值钱的调优手段是人肉干预中间产物。这个平台最爽的地方不是“一键生成”而是可以在生成剧本或分镜后手动改几处台词、调一下镜头顺序再继续跑TTS和合成。所以代码里一定要把script.json和shots.json独立落盘并支持“跳过LLM提取直接读取已有JSON继续下面流程”的工作模式。这个断点续跑能力在一轮效果微调里能省下大量接口费用。我坚持了很久的调优习惯是固定一组测试prompt作为回归用例。每次改prompt或换模型先跑这组用例对比之前的生成结果记录失败阶段和报错文本。不要同时改多个参数否则出了问题没法判断是谁导致的黑匣子。需要调调度时先从temperature和max_tokens两个最直接的值入手再动提示词最后才考虑换模型。这个过程没有捷径但走几轮之后你会慢慢摸清当前LLM的脾气。希望这篇笔记能帮你在搭短剧生成平台的路上少踩几个坑。本文还有配套的精品资源点击获取