简介Toonflow 是一套面向短剧与漫剧创作者的 AI 一站式生成工具核心能力是把小说文本自动转化为剧本再结合 AI 生成的图片与视频素材完成从剧本到成片的自动化流程。它适合个人创作者、小团队以及预算有限的短剧项目用来降低制作门槛、节省时间与人力让创作者把精力集中在故事创意与情感表达上。资源包共 190 个文件以 155 个 ts 源码文件为主体辅以 png、jpg 图片素材、yml 与 json 配置、md 与 txt 说明文档以及 Dockerfile、dockerignore、gitignore 等部署相关文件整体约 9.93MB结构完整便于本地运行与二次开发。目前已有 338 人学习下载。借助这套源码读者可以了解 AI 短剧漫剧从文本解析、剧本生成到视觉素材合成的完整链路参考其工程组织与配置方式快速搭建属于自己的自动化内容生产流程。1. Toonflow 到底解决什么问题从小说文本到短剧漫剧的自动化流水线手里有一本几十万字的小说想把它变成能发出去的短剧或漫剧传统流程是什么样先找人改编剧本再画分镜再逐帧出图再合成视频一套下来周期以月计成本以万计。Toonflow 这类 AI 短剧漫剧工具瞄准的就是这条链路——把小说文本自动转成结构化剧本再用 AI 生成角色图、场景图和视频片段最后拼成可发布的短剧或漫剧。它适合两类人手里有小说版权或原创文本、想低成本试水短剧的内容创作者以及想搭一套自动化内容生产流水线的开发者。核心价值不在于某一个环节的 AI 效果有多惊艳而在于把「小说 → 剧本 → 分镜 → 图片 → 视频」串成一条可重复跑的管线让批量生产成为可能。下面按实际落地顺序拆开讲。2. 小说转剧本分章、抽角色、生成结构化脚本2.1 为什么不能直接把整本小说丢给大模型很多人第一反应是写个 prompt 把小说全文塞进去让模型输出剧本。这条路在实操中基本走不通原因有三个。第一上下文长度限制一本番茄小说动辄几十万字即使模型支持长上下文成本和延迟也不可接受。第二信息密度问题小说里大量环境描写、心理独白对剧本没有直接价值全量输入会稀释关键情节。第三角色一致性问题整本一次性处理时模型容易混淆角色关系尤其是多线叙事的作品。常见做法是先把小说按章节切分再对每章做结构化信息抽取最后汇总成全局角色表和分集剧本。这个思路和做数据管道的逻辑一样先 ETL 再聚合。2.2 分章与文本清洗的代码实现import re def split_chapters(raw_text: str) - list[dict]: 按常见章节标题模式切分小说文本。 支持 第X章、第X节、Chapter X 等格式。 # 匹配中文章节标题允许前后有空白 pattern re.compile( r^\s*(第[一二三四五六七八九十百千\d][章节回])\s*(.*)$, re.MULTILINE ) matches list(pattern.finditer(raw_text)) if not matches: # 没有识别到章节标题时按固定字数切块 chunk_size 3000 return [ {index: i, title: fchunk_{i}, content: raw_text[i:ichunk_size]} for i in range(0, len(raw_text), chunk_size) ] chapters [] for i, m in enumerate(matches): start m.start() end matches[i 1].start() if i 1 len(matches) else len(raw_text) chapters.append({ index: i, title: m.group(0).strip(), content: raw_text[start:end].strip() }) return chapters def clean_chapter(text: str) - str: 去掉广告行、作者的话、重复空行等噪声。 lines text.split(\n) cleaned [] for line in lines: stripped line.strip() # 跳过常见噪声行 if not stripped: continue if re.match(r^(作者|PS|ps|求票|求收藏|感谢), stripped): continue cleaned.append(stripped) return \n.join(cleaned)split_chapters的核心逻辑是用正则匹配章节标题行按匹配位置切分。chunk_size是兜底参数当正则匹配不到任何章节标题时比如某些 txt 格式混乱的小说按固定字数切块一般设 2000 到 4000 字比较合适太短会丢失上下文太长会增加后续抽取的噪声。clean_chapter处理的是从网上下载的 txt 里常见的广告行和作者留言这些内容如果不清掉会被模型当成正文处理污染角色和情节抽取结果。2.3 用大模型抽取角色和分集大纲分章之后对每章做一次结构化抽取。这里的关键是设计好输出格式让模型返回 JSON方便后续程序处理。import json EXTRACT_PROMPT 你是一个剧本改编助手。请阅读以下小说章节内容完成两件事 1. 列出本章出现的所有角色只列有台词或推动情节的角色每个角色给出姓名、身份、本章中的关键行为。 2. 用 3-5 句话概括本章的核心情节标注适合改编为剧本的场景切换点。 输出格式为 JSON { characters: [{name: , role: , actions: }], plot_summary: , scene_breaks: [场景1描述, 场景2描述] } 章节内容 {chapter_text} def extract_chapter_info(chapter_text: str, llm_client) - dict: prompt EXTRACT_PROMPT.format(chapter_textchapter_text[:4000]) resp llm_client.chat(prompt, temperature0.3) try: return json.loads(resp) except json.JSONDecodeError: # 模型偶尔会输出非标准 JSON做一次修复尝试 resp llm_client.chat( f请把以下内容修复为合法 JSON只输出 JSON\n{resp}, temperature0 ) return json.loads(resp)temperature0.3是为了在抽取任务中保持输出稳定不要用太高的随机性。chapter_text[:4000]是截断保护单章超过 4000 字时只取前 4000 字做抽取因为章节的核心情节通常集中在前半部分。如果小说章节普遍很长可以改成滑动窗口分段抽取再合并。2.4 汇总角色表与生成分集剧本逐章抽取完成后把所有章节的角色信息合并去重得到全局角色表。合并时要注意同名不同人的情况——比如两个角色都叫「小雅」需要通过身份描述区分。def merge_characters(all_chapter_infos: list[dict]) - dict: 合并所有章节的角色信息按姓名聚合。 char_map {} for info in all_chapter_infos: for c in info.get(characters, []): name c[name] if name not in char_map: char_map[name] { name: name, roles: set(), actions: [] } char_map[name][roles].add(c.get(role, )) char_map[name][actions].append(c.get(actions, )) # 转成可序列化格式 result {} for name, data in char_map.items(): result[name] { name: name, role: / .join(filter(None, data[roles])), key_actions: data[actions][:10] # 只保留前10条关键行为 } return result def generate_episode_script( chapter_summaries: list[str], character_table: dict, episodes: int 10 ) - list[dict]: 把章节摘要按集数分组生成分集剧本大纲。 per_episode max(1, len(chapter_summaries) // episodes) scripts [] for ep in range(episodes): start ep * per_episode end start per_episode chunk chapter_summaries[start:end] if not chunk: break scripts.append({ episode: ep 1, source_chapters: f{start1}-{end}, summary: .join(chunk), characters_involved: list(character_table.keys())[:8] }) return scriptsper_episode控制每集覆盖多少章这个参数直接影响短剧节奏。短剧一般每集 1-3 分钟对应小说大概 3-5 章的内容量。如果小说章节本身很短比如每章 1000 字可以适当调大。characters_involved这里做了简化处理实际使用时应该根据每集摘要内容做角色匹配而不是直接取前 8 个。3. AI 出图与角色一致性从文字描述到可用素材3.1 角色图生成的核心矛盾剧本有了下一步是出图。AI 短剧漫剧对图片的要求和普通 AI 绘画不一样同一个角色在不同场景、不同表情下必须保持外貌一致。这是整个流水线里最容易翻车的环节。常见做法是先用角色描述生成一张「角色定妆照」然后用图生图或 IP-Adapter 类方案锁定角色特征再生成不同场景下的图片。3.2 角色定妆照的 prompt 构造def build_character_prompt(character: dict, style: str anime) - str: 根据角色信息构造出图 prompt。 style 可选 anime / realistic / comic。 style_map { anime: anime style, clean lines, vibrant colors, realistic: photorealistic, cinematic lighting, 8k, comic: comic book style, bold outlines, halftone shading } base ( fcharacter portrait, {character[name]}, f{character.get(role, )}, f{style_map.get(style, style_map[anime])}, ffront view, upper body, neutral expression, fwhite background, high detail ) return base # 示例 char {name: 林月, role: 剑客冷峻黑色长发} prompt build_character_prompt(char, styleanime) print(prompt) # 输出: character portrait, 林月, 剑客冷峻黑色长发, anime style, ...prompt 里front view, upper body, neutral expression, white background这几个约束很重要。正面、上半身、中性表情、白底是为了给后续的图生图提供最干净的参考图。如果定妆照本身就是侧脸或者复杂背景后续生成其他场景时角色特征会漂移。3.3 用参考图锁定角色一致性生成定妆照后后续每个场景的图片生成都要带上这张参考图。不同工具的具体接口不一样但核心参数就几个参数作用建议值reference_image角色参考图路径定妆照reference_strength参考强度0.6-0.8denoising_strength重绘幅度0.4-0.6seed随机种子固定值reference_strength太低角色不像太高场景变化出不来。0.6-0.8 是实测比较稳的区间。denoising_strength控制新图和参考图的差异程度场景变化大就调高只是换表情就调低。seed固定住可以减少同一角色在不同图片之间的随机波动。3.4 场景图批量生成的工程化处理一个 10 集的短剧大概需要 50-100 张场景图。手动一张张生成不现实需要批量处理。import os import time def batch_generate_scenes( scenes: list[dict], character_refs: dict, output_dir: str, generator ) - list[str]: scenes: [{episode: 1, scene_desc: ..., characters: [林月]}] character_refs: {林月: /path/to/ref.png} os.makedirs(output_dir, exist_okTrue) results [] for i, scene in enumerate(scenes): # 取第一个出场角色的参考图 ref_char scene[characters][0] if scene[characters] else None ref_img character_refs.get(ref_char) out_path os.path.join(output_dir, fep{scene[episode]}_scene{i:03d}.png) try: generator.generate( promptscene[scene_desc], reference_imageref_img, reference_strength0.7, denoising_strength0.5, output_pathout_path ) results.append(out_path) except Exception as e: print(f[FAIL] scene {i}: {e}) results.append(None) time.sleep(1) # 避免请求过密 return resultstime.sleep(1)是给 API 留缓冲如果是本地部署的模型可以去掉。异常处理里把失败的场景记为None而不是直接中断这样一批跑完后可以单独重跑失败项。实际使用中建议把scenes和character_refs持久化到 JSON 文件方便断点续跑。4. 图片转视频与合成让静态素材动起来4.1 图生视频的两种路线静态图转视频目前有两条路。一条是用图生视频模型比如常见的 image-to-video 方案输入一张图输出几秒的动态片段。另一条是用传统的 Ken Burns 效果——推拉摇移把静态图做出镜头运动感。前者效果更自然但成本高、速度慢后者零成本但动感有限。实际做短剧漫剧时常见做法是混合使用关键镜头用图生视频过渡镜头用 Ken Burns。4.2 用 FFmpeg 做镜头运动# 缓慢推近效果从原图中心放大 1.0 到 1.15 ffmpeg -loop 1 -i scene.png -vf zoompanzmin(zoom0.001,1.15):d125:s1080x1920 \ -c:v libx264 -t 5 -pix_fmt yuv420p scene_motion.mp4 # 缓慢平移效果从左到右 ffmpeg -loop 1 -i scene.png -vf cropiw/1.2:ih:iw/6*t:0,scale1080:1920 \ -c:v libx264 -t 5 -pix_fmt yuv420p scene_pan.mp4第一条命令的zoompan滤镜实现推近zmin(zoom0.001,1.15)表示每帧放大 0.001 倍最大到 1.15 倍d125是总帧数5 秒 × 25fpss1080x1920是竖屏短剧常见分辨率。第二条命令用crop实现平移iw/6*t控制水平偏移速度。这两条命令是短剧漫剧里最常用的镜头运动模板改参数就能适配不同节奏。4.3 拼接、字幕与配音import subprocess def concat_clips(clip_paths: list[str], output: str): 用 FFmpeg concat 协议拼接视频片段。 list_file concat_list.txt with open(list_file, w) as f: for p in clip_paths: f.write(ffile {p}\n) subprocess.run([ ffmpeg, -y, -f, concat, -safe, 0, -i, list_file, -c, copy, output ], checkTrue) def add_subtitles(video: str, srt_path: str, output: str): 烧录字幕到视频。 subprocess.run([ ffmpeg, -y, -i, video, -vf, fsubtitles{srt_path}:force_styleFontSize18,PrimaryColourHFFFFFF, -c:a, copy, output ], checkTrue)concat_clips用的是 FFmpeg 的 concat 协议要求所有片段的编码参数一致分辨率、帧率、编码格式否则拼接会出问题。如果片段来源不统一需要先统一转码。add_subtitles里的force_style控制字幕样式FontSize18在 1080x1920 竖屏下大概占画面宽度的 1/20是比较舒服的阅读大小。配音部分一般用 TTS 接口逐句生成音频再按时间轴对齐这里不展开。5. 避坑与排查这条流水线上最容易翻车的五个地方5.1 角色名在抽取结果里对不上现象第 3 章抽出来叫「林月」第 7 章变成「林玥」合并角色表时出现两个角色。原因大模型在抽取时对同音字、形近字没有统一能力尤其是网文里作者自己都可能写混。解决在合并前加一层别名归一化。维护一个alias_map把常见变体映射到标准名。也可以用编辑距离做模糊匹配相似度超过 0.85 的自动合并但需要人工确认一遍。5.2 生成的场景图和剧本对不上现象剧本写的是「夜晚雨中街道」生成的图是白天晴天。原因场景描述在从剧本到 prompt 的转换过程中丢失了关键修饰词或者模型对否定词理解不好。解决在场景描述转 prompt 时把时间、天气、光线作为独立字段强制拼接到 prompt 开头。不要依赖模型从长句里自己提取这些信息。5.3 视频拼接后音画不同步现象拼接多个片段后后面片段的音频比画面快或慢半秒。原因不同片段的帧率和音频采样率不一致concat 时没有统一。解决拼接前统一转码所有片段强制转为相同的帧率短剧一般 25fps 或 30fps和音频采样率44100Hz。这一步多花几分钟但能省掉后面大量排查时间。5.4 API 调用超时导致批量任务中断现象批量生成 50 张图跑到第 30 张时程序崩溃前面 29 张的结果也没保存。原因没有做增量保存和异常恢复。解决每生成一张图就写一次状态文件记录已完成的任务 ID。程序启动时先读状态文件跳过已完成的。这个习惯在跑任何批量 AI 任务时都值得养成。5.5 输出视频在手机上播放黑屏现象电脑上播放正常发到手机上只有声音没有画面。原因编码格式不兼容常见于用了手机不支持的像素格式或编码器。解决输出时统一用-c:v libx264 -pix_fmt yuv420p这是兼容性最好的组合。分辨率用 1080x1920 竖屏码率控制在 4-6 Mbps。6. 把整条流水线串起来一个可复用的调度脚本前面几章拆开讲了每个环节实际跑的时候需要一个调度层把它们串起来。我一般会写一个简单的 pipeline 脚本用配置文件驱动每个阶段独立可重跑。import json import os class ToonflowPipeline: def __init__(self, config_path: str): with open(config_path) as f: self.cfg json.load(f) self.state_file self.cfg.get(state_file, pipeline_state.json) self.state self._load_state() def _load_state(self) - dict: if os.path.exists(self.state_file): with open(self.state_file) as f: return json.load(f) return {chapters_done: False, scripts_done: False, images_done: False, videos_done: False} def _save_state(self): with open(self.state_file, w) as f: json.dump(self.state, f, ensure_asciiFalse, indent2) def run(self): if not self.state[chapters_done]: self._split_and_clean() self.state[chapters_done] True self._save_state() if not self.state[scripts_done]: self._extract_and_generate_scripts() self.state[scripts_done] True self._save_state() if not self.state[images_done]: self._generate_all_images() self.state[images_done] True self._save_state() if not self.state[videos_done]: self._compose_videos() self.state[videos_done] True self._save_state() def _split_and_clean(self): # 调用第 2 章的分章和清洗逻辑 pass def _extract_and_generate_scripts(self): # 调用第 2 章的抽取和剧本生成逻辑 pass def _generate_all_images(self): # 调用第 3 章的批量出图逻辑 pass def _compose_videos(self): # 调用第 4 章的拼接和字幕逻辑 pass这个调度脚本的核心是状态文件。每个阶段完成后写一次状态下次启动时自动跳过已完成的阶段。state_file建议放在项目根目录和输出目录分开避免清理输出时误删。实际使用时可以把每个_xxx方法里的具体逻辑替换成前面章节的代码配置文件里放 API key、模型名称、输出路径这些可变参数。验证整条流水线是否跑通最直接的方法是拿一篇 3-5 章的短篇小说做端到端测试。先确认分章结果正确再检查角色表有没有明显遗漏然后看生成的场景图是否和剧本描述匹配最后播放合成视频检查音画同步。每一步的输出都单独存一份出问题时能快速定位是哪个环节的锅。我自己的习惯是每换一个小说来源比如从番茄小说换成其他平台先跑一遍分章和抽取人工检查前 10 章的结果确认没有系统性问题后再批量跑。这个前置检查花 10 分钟能省掉后面几个小时的返工。希望帮到你。本文还有配套的精品资源点击获取