这套流程的目标很具体:输入一个主题(比如"小狐狸第一次坐火车"),产出一个可以直接打印或上传的绘本文件——12 页左右,每页一张插图配一到两句中文,主角从头到尾长得一样,文字量适合 3~6 岁亲子阅读。
整条链路分成四段:定结构 → 写文字 → 做插图 → 排版成 PDF。四段之间用文件传递,任何一段不满意都可以单独重跑,不必从头再来。下面按顺序走一遍。
前置条件清单
开始前确认这几样东西:
- 一个能调用文本模型的 API 账号(用于写故事、拆页、生成提示词)
- 一个能调用图像生成模型的 API 账号(用于出插图)
- Python 3.9 以上环境,能执行
pip install requests playwright - 一台能装中文字体的机器(排版用)
- 一个空文件夹作为项目目录,后面所有文件都放里面
关于接口地址、模型名、计费方式,各家差异不小,以官方文档当前版本为准。下面代码里用环境变量占位,换成自己的即可。
```bash
mkdir picturebook && cd picturebook
python -m venv .venv
source .venv/bin/activate # Windows 用 .venv\Scripts\activate
pip install requests playwright
python -m playwright install chromium
```
第一步:先定结构,别急着写故事
很多人一上来就让模型"写一个绘本故事",结果拿到 800 字散文,没法分页。正确做法是先定"页数 × 每页字数"这个骨架。
给一个 12 页的骨架:第 1 页是引入,第 2~10 页是情节推进,第 11 页冲突解决,第 12 页收尾。每页文字控制在 15~35 个汉字,这是 3~6 岁孩子一页的注意力容量。
同时要定一份"角色设定卡",这是后面插图一致性的命根子。写成固定字符串,每次调用图像模型都带上它,不改一个字:
```json
{
"character": "一只圆脸小狐狸,橙色毛发,白色肚皮,脖子上系一条蓝色针织围巾,尾巴尖是白色",
"style": "儿童绘本插画风格,柔和水彩质感,暖色调,圆润线条,背景简洁不杂乱",
"negative": "不要出现任何文字、水印、签名、恐怖元素、尖锐牙齿、写实照片风格、知名卡通角色"
}
```
把这段存成 setting.json,后面脚本读它。
第二步:让模型按 JSON 输出分页文案
关键点是强制结构化输出。不要让它自由发挥格式,否则解析一次崩一次。
```python
make_story.py
import json, os, requests
API_BASE = os.environ["LLM_API_BASE"] # 形如 https://<服务商域名>/v1
API_KEY = os.environ["LLM_API_KEY"]
MODEL = os.environ["LLM_MODEL"] # 具体模型名以官方文档当前版本为准
SYSTEM_PROMPT = """你是一位儿童绘本作者,为 3-6 岁孩子写作。
要求:
1. 每页文字 15-35 个汉字,一到两句话,口语化,可朗读。
2. 不出现暴力、惊吓、危险动作示范、品牌名和真实人名。
3. 输出严格的 JSON,不要额外解释。
JSON 结构:
{"title":"书名","pages":[{"page":1,"text":"本页文字","scene":"本页画面描述(中文,40字内)"}]}
"""
def make_story(idea: str, pages: int = 12) -> dict:
payload = {
"model": MODEL,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"主题:{idea}\n总页数:{pages}"},
],
"temperature": 0.85,
"response_format": {"type": "json_object"}, # 字段名以官方文档为准
}
resp = requests.post(
f"{API_BASE}/chat/completions",
headers={"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"},
json=payload, timeout=180,
)
resp.raise_for_status()
content = resp.json()["choices"][0]["message"]["content"]
return json.loads(content)
if __name__ == "__main__":
story = make_story("小狐狸第一次坐火车去海边")
with open("story.json", "w", encoding="utf-8") as f:
json.dump(story, f, ensure_ascii=False, indent=2)
print("已生成", len(story["pages"]), "页")
```
跑完得到 story.json。这一步一定要自己读一遍,改掉拗口的句子、改掉不合适的词。模型写的故事通常结构没问题,但语感偏书面,人工顺手改两句,朗读效果会好很多。
第三步:加一道自动校验
别靠眼睛一页页数。写个检查脚本,把硬性规则交给代码:
```python
check.py
import json, sys
story = json.load(open("story.json", encoding="utf-8"))
problems = []
if len(story["pages"]) < 8:
problems.append("页数太少,不适合做绘本")
for p in story["pages"]:
n = len(p["text"])
if n > 40:
problems.append(f"第{p['page']}页文字 {n} 字,偏长")
if "scene" not in p or len(p["scene"]) < 8:
problems.append(f"第{p['page']}页缺少画面描述")
banned = ["血", "打死", "可怕", "恐怖"]
for p in story["pages"]:
for w in banned:
if w in p["text"]:
problems.append(f"第{p['page']}页出现敏感词:{w}")
print("\n".join(problems) if problems else "校验通过")
sys.exit(1 if problems else 0)
```
第四步:把画面描述拼成插图提示词
这一步做的是"翻译":把中文画面描述 + 角色设定卡 + 风格词,拼成图像模型能吃的提示词。角色卡每次原样带上,只换场景部分。
```python
make_prompts.py
import json
story = json.load(open("story.json", encoding="utf-8"))
cfg = json.load(open("setting.json", encoding="utf-8"))
for p in story["pages"]:
prompt = (
f"{cfg['character']}。场景:{p['scene']}。"
f"画面风格:{cfg['style']}。"
f"禁止:{cfg['negative']}。"
f"同一本书内角色外观保持一致。"
)
p["prompt"] = prompt
json.dump(story, open("story_with_prompts.json", "w", encoding="utf-8"),
ensure_ascii=False, indent=2)
print("提示词已生成")
```
如果所用平台支持固定随机种子(seed)、参考图上传或角色一致性开关,把参数一起写进配置里。这是让 12 张图里狐狸长得像同一只狐狸的主要手段。平台不提供这些能力时,靠的就是角色卡描述足够细——颜色、材质、配饰写死,不要写"可爱的小动物"这种模糊词。
第五步:批量出图
先出 2 页样张,确认风格和角色满意,再跑全量。图片按张计费,批量重跑代价不小。
```python
make_images.py
import base64, json, os, requests, time
API_BASE = os.environ["IMAGE_API_BASE"]
API_KEY = os.environ["IMAGE_API_KEY"]
IMG_MODEL = os.environ["IMAGE_MODEL"] # 以官方文档当前版本为准
os.makedirs("images", exist_ok=True)
story = json.load(open("story_with_prompts.json", encoding="utf-8"))
def gen(prompt: str, out_path: str, size: str = "1024x1024"):
payload = {"model": IMG_MODEL, "prompt": prompt, "size": size, "n": 1}
r = requests.post(
f"{API_BASE}/images/generations",
headers={"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"},
json=payload, timeout=300,
)
r.raise_for_status()
item = r.json()["data"][0]
if item.get("b64_json"):
data = base64.b64decode(item["b64_json"])
open(out_path, "wb").write(data)
else:
url = item["url"] # 返回字段以官方文档为准
img = requests.get(url, timeout=300).content
open(out_path, "wb").write(img)
for p in story["pages"]:
out = f"images/page_{p['page']:02d}.png"
if os.path.exists(out):
continue
gen(p["prompt"], out)
print("完成", out)
time.sleep(1) # 温和限速,避免触发频率限制
```
脚本里做了断点续跑:已存在的文件跳过,中途报错重跑不会浪费已出的图。
第六步:排版成 PDF
用 HTML 排版再用浏览器转 PDF,中文换行和字体控制比直接画图省事。先生成 HTML:
```python
build_html.py
import json, html
story = json.load(open("story_with_prompts.json", encoding="utf-8"))
pages = []
for p in story["pages"]:
pages.append(f"""
<section class="page">
<img src="images/page_{p['page']:02d}.png" alt="">
<p class="text">{html.escape(p['text'])}</p>
<span class="num">{p['page']}</span>
</section>""")
doc = f"""<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8">
<title>{html.escape(story['title'])}</title>
<style>
@page {{ size: 210mm 210mm; margin: 0; }}
html, body {{ margin: 0; padding: 0; }}
body {{ font-family: "Noto Sans SC", "Source Han Sans SC", sans-serif; }}
.page {{ width: 210mm; height: 210mm; page-break-after: always;
position: relative; overflow: hidden; }}
.page img {{ display: block; width: 210mm; height: 150mm; object-fit: cover; }}
.text {{ font-size: 22pt; line-height: 1.7; margin: 0;
padding: 10mm 14mm; text-align: center; }}
.num {{ position: absolute; bottom: 6mm; right: 10mm;
font-size: 11pt; color: #999; }}
</style></head><body>{''.join(pages)}</body></html>"""
open("book.html", "w", encoding="utf-8").write(doc)
```
然后转 PDF:
```python
to_pdf.py
from playwright.sync_api import sync_playwright
import os
url = "file://" + os.path.abspath("book.html")
with sync_playwright() as pw:
browser = pw.chromium.launch()
page = browser.new_page()
page.goto(url, wait_until="networkidle")
page.pdf(path="book.pdf", width="210mm", height="210mm",
print_background=True, prefer_css_page_size=True)
browser.close()
print("已输出 book.pdf")
```
中文能不能正常显示,取决于系统里有没有装中文字体。Linux 服务器上常见情况是没装,装一个开源中文字体(思源系列、Noto Sans CJK 都可以)再跑,字体名称要和 CSS 里写的对应。字体用于商业出版前,先确认授权条款。
常见坑与排错
每页文字一长一短,排版参差。 根因是模型自由发挥。在系统提示里把字数写死成区间,再用第三步的校验脚本兜底,超长的让它重写那一页。
12 张图里主角换了三个样子。 三种解法按优先级:开启平台的参考图或角色一致性功能;把角色卡描述写到"颜色 + 材质 + 配饰 + 体型"四要素齐全;同一本书固定 seed。都做不到时,接受轻微差异,至少围巾颜色和主色调保持一致。
插图里出现莫名其妙的文字。 图像模型经常在画面里塞字母。在负面提示里明确写"不要出现任何文字、字母、水印",并且成书文字一律由排版层叠加,不指望图里生成字。
JSON 解析失败。 多数是模型多说了几句解释。让提示词强制"只输出 JSON",使用平台提供的 JSON 模式;再不稳就加重试:捕获异常后重发一次,或在提示里附上上次报错信息让它修。
接口报 429 或超时。 批量出图时最常见。给每张图之间加 sleep,请求失败做指数退避重试,别一次并发几十个请求。
打印出来颜色发灰、边缘被裁。 家用打印机直接打 A4 缩放即可,边距留足;正式送印需要出血和 CMYK 色彩空间,具体规格找印厂确认,别自己猜。
生成内容能不能商用。 各平台条款不同,以官方页面为准。另外注意别让角色外观贴近已有知名 IP,儿童读物领域这一点被追究的概率不低。
下一步建议
跑通一册之后,把流程产品化:
一,把 setting.json 升级成角色库。同一个主角换场景写第二、第三个故事,系列感立刻出来,也省掉重新调角色一致性的力气。
二,加配音。用文本转语音把每页文字读出来,配上翻页时间轴,就能出一支绘本视频,适合放在短视频渠道做引流。
三,把人工审校做成固定环节。机器出稿、人改文字、人挑图,三步里人至少占两步。儿童内容的容错空间比成人内容小得多。
四,保留全部中间产物。story.json、提示词、图片、参数都留在项目目录里,一年后要改版或出多语言版本时,改文字重跑就能对齐原有画面。
整条链路的价值不在"一次生成多快",而在于它可复用:骨架定了,换主题就是新书。
