第 22/60 天
引言
学了这么多理论知识——提示词工程、角色一致性、ControlNet、ComfyUI 工作流——但真正动手创作一篇完整的 AI 漫画短篇时,很多人还是会卡在「从哪开始」这个坎上。理论是一回事,把 4 页故事按照「起承转合」的节奏连贯地画出来,需要的不仅是工具熟练度,更是一套可复用的实战流程。
本文将以一个完整的 4 页短篇故事为例,从脚本构思、分镜设计、批量生成到后期排版,手把手带你完成第一篇 AI 漫画短篇创作。完成后你将拥有一套可直接复用的标准化工作流,下次创作只需替换故事内容即可。
核心概念
4页故事的结构设计
短篇漫画的「4页」不是随意划分的,它对应经典的叙事节奏:
| 页码 | 叙事功能 | 节奏要点 | 建议格数 |
|---|---|---|---|
| 第 1 页 | 起(引子) | 建立场景、介绍主角、抛出钩子 | 3-5 格 |
| 第 2 页 | 承(发展) | 冲突升级、情感铺垫 | 4-6 格 |
| 第 3 页 | 转(高潮) | 核心冲突爆发、最精彩画面 | 2-4 格(大特写) |
| 第 4 页 | 合(收尾) | 冲突解决、余韵、彩蛋 | 3-5 格 |
实战故事脚本
为了演示完整流程,我们创作一个简单的故事:
《星空下的约定》 —— 一个关于友谊与再会的短篇
- 第 1 页(起):城市天台,少年仰望星空,回忆与好友的约定
- 第 2 页(承):闪回到过去,两人在星空下许下承诺
- 第 3 页(转):现实中的少年收到远方来信,核心冲突爆发
- 第 4 页(合):在同一个天台,两人重逢,故事温馨收尾
所需工具清单
- Stable Diffusion WebUI(或 ComfyUI)—— 图像生成引擎
- ControlNet(Canny + OpenPose)—— 姿态与构图控制
- LoRA / IP-Adapter —— 角色一致性保障
- Photoshop / GIMP / Canva —— 后期排版与对话框
- Python + Pillow —— 批量处理与自动化排版
实战步骤
步骤 1:编写分镜脚本
首先,将故事拆解为具体的分镜描述。每个分镜需要包含:角色描述、场景、动作、视角、构图提示。
{
"story": "星空下的约定",
"pages": [
{
"page": 1,
"panels": [
{
"id": "1-1",
"description": "夜晚城市天台,广角镜头,少年独自站立",
"prompt": "anime style, night city rooftop, wide angle, teenage boy standing alone, looking up at starry sky, city lights in background, cinematic lighting, detailed starry sky, emotional atmosphere",
"negative": "low quality, blurry, watermark, text, deformed hands, extra fingers",
"width": 768,
"height": 512
},
{
"id": "1-2",
"description": "少年仰望星空,特写面部表情,思念的眼神",
"prompt": "anime style, close-up portrait of teenage boy, looking up at sky, longing expression, eyes reflecting starlight, night atmosphere, soft lighting, bishounen style",
"negative": "low quality, blurry, deformed face, extra fingers",
"width": 512,
"height": 512
},
{
"id": "1-3",
"description": "星空特写,流星划过,暗示约定的回忆",
"prompt": "anime style, starry sky, shooting star crossing the sky, deep space, glowing stars, Milky Way visible, magical atmosphere, wide angle, night sky",
"negative": "low quality, blurry, watermark, text",
"width": 768,
"height": 512
}
]
}
]
}
步骤 2:批量生成脚本(Python + Stable Diffusion API)
使用 Python 脚本调用 SD WebUI 的 API 进行批量生成:
#!/usr/bin/env python3
"""
batch_generate.py - 批量生成漫画分镜图片
"""
import requests
import json
import time
import os
from pathlib import Path
# SD WebUI API 地址
SD_URL = "http://localhost:7860"
OUTPUT_DIR = Path("./comic_output")
OUTPUT_DIR.mkdir(exist_ok=True)
# 加载分镜脚本
with open("storyboard.json", "r", encoding="utf-8") as f:
storyboard = json.load(f)
def generate_panel(panel, seed=-1):
"""调用 SD API 生成单个分镜"""
payload = {
"prompt": f"(masterpiece, best quality:1.2), {panel['prompt']}",
"negative_prompt": panel.get('negative', ''),
"width": panel.get('width', 768),
"height": panel.get('height', 512),
"steps": 30,
"cfg_scale": 7.0,
"sampler_name": "DPM++ 2M Karras",
"seed": seed,
"batch_size": 1,
"save_images": True,
"enable_hr": False,
}
resp = requests.post(f"{SD_URL}/sdapi/v1/txt2img", json=payload)
if resp.status_code != 200:
print(f"Error: {resp.text}")
return None
result = resp.json()
images = result.get('images', [])
info = json.loads(result.get('info', '{}'))
print(f" ✓ Generated: {panel['id']} (seed={info.get('seed', 'N/A')})")
return {"images": images, "info": info}
# 逐页逐格生成
for page in storyboard['pages']:
page_num = page['page']
for panel in page['panels']:
panel_id = panel['id']
output_path = OUTPUT_DIR / f"{panel_id}.png"
if output_path.exists():
print(f" SKIP: {panel_id} already exists")
continue
print(f"Generating {panel_id}: {panel['description'][:40]}...")
result = generate_panel(panel)
if result:
# 保存图片
import base64
img_data = base64.b64decode(result['images'][0])
with open(output_path, "wb") as f:
f.write(img_data)
print(f" → Saved: {output_path}")
time.sleep(1) # 避免 API 过载
print("✅ Batch generation complete!")
步骤 3:使用 ControlNet 确保角色一致性
对于需要保持角色一致性的分镜,在分镜配置中添加 ControlNet 参数:
def setup_controlnet(panel, reference_image_path=None):
"""配置 ControlNet 参数"""
controlnet_args = []
# 1. IP-Adapter 用于面部一致性
if reference_image_path and os.path.exists(reference_image_path):
import base64
with open(reference_image_path, "rb") as f:
ref_img = base64.b64encode(f.read()).decode("utf-8")
controlnet_args.append({
"input_image": ref_img,
"module": "ip-adapter_face_id_plus",
"model": "ip-adapter_face_id_plus_sd15 [some_hash]",
"weight": 0.8,
"control_mode": "Balanced"
})
# 2. OpenPose 控制姿态
if panel.get('pose_image'):
with open(panel['pose_image'], "rb") as f:
pose_img = base64.b64encode(f.read()).decode("utf-8")
controlnet_args.append({
"input_image": pose_img,
"module": "openpose_full",
"model": "control_v11p_sd15_openpose [some_hash]",
"weight": 1.0,
"control_mode": "ControlNet is more important"
})
return controlnet_args
步骤 4:自动排版(Python + Pillow)
生成所有分镜图片后,用 Python 进行漫画排版:
#!/usr/bin/env python3
"""
comic_layout.py - 将分镜图片排版为漫画页面
"""
from PIL import Image, ImageDraw, ImageFont
import json
from pathlib import Path
# 页面配置
PAGE_WIDTH = 1800
PAGE_HEIGHT = 2400
MARGIN = 30
GUTTER = 15 # 格间距
BG_COLOR = (255, 255, 255)
def layout_page(page_num, panels, output_dir):
"""将一页的多个分镜排版为漫画页面"""
canvas = Image.new('RGB', (PAGE_WIDTH, PAGE_HEIGHT), BG_COLOR)
# 根据面板数量自动计算布局
panel_count = len(panels)
layout = calculate_layout(panel_count)
for idx, panel_info in enumerate(layout):
panel_path = output_dir / f"{page_num}-{idx+1}.png"
if not panel_path.exists():
continue
img = Image.open(panel_path)
# 按布局计算位置
x = MARGIN + panel_info['x'] * (PAGE_WIDTH - 2 * MARGIN)
y = MARGIN + panel_info['y'] * (PAGE_HEIGHT - 2 * MARGIN)
w = panel_info['w'] * (PAGE_WIDTH - 2 * MARGIN) - GUTTER
h = panel_info['h'] * (PAGE_HEIGHT - 2 * MARGIN) - GUTTER
# 缩放图片适应格位
img_resized = img.resize((int(w), int(h)), Image.LANCZOS)
canvas.paste(img_resized, (int(x), int(y)))
# 绘制边框
draw = ImageDraw.Draw(canvas)
draw.rectangle(
[int(x), int(y), int(x + w), int(y + h)],
outline=(0, 0, 0),
width=3
)
output_path = output_dir / f"page_{page_num}.png"
canvas.save(output_path)
print(f" ✓ Page {page_num} saved: {output_path}")
return output_path
def calculate_layout(panel_count):
"""
根据面板数量返回自动布局方案
返回格式: [{x, y, w, h}, ...] 坐标归一化到 [0,1]
"""
layouts = {
1: [{'x': 0, 'y': 0, 'w': 1, 'h': 1}],
2: [{'x': 0, 'y': 0, 'w': 1, 'h': 0.5},
{'x': 0, 'y': 0.5, 'w': 1, 'h': 0.5}],
3: [{'x': 0, 'y': 0, 'w': 1, 'h': 0.35},
{'x': 0, 'y': 0.35, 'w': 0.5, 'h': 0.65},
{'x': 0.5, 'y': 0.35, 'w': 0.5, 'h': 0.65}],
4: [{'x': 0, 'y': 0, 'w': 0.5, 'h': 0.5},
{'x': 0.5, 'y': 0, 'w': 0.5, 'h': 0.5},
{'x': 0, 'y': 0.5, 'w': 0.5, 'h': 0.5},
{'x': 0.5, 'y': 0.5, 'w': 0.5, 'h': 0.5}],
5: [{'x': 0, 'y': 0, 'w': 1, 'h': 0.33},
{'x': 0, 'y': 0.33, 'w': 0.33, 'h': 0.67},
{'x': 0.33, 'y': 0.33, 'w': 0.33, 'h': 0.67},
{'x': 0.66, 'y': 0.33, 'w': 0.33, 'h': 0.34},
{'x': 0.66, 'y': 0.67, 'w': 0.33, 'h': 0.33}],
6: [{'x': 0, 'y': 0, 'w': 0.5, 'h': 0.33},
{'x': 0.5, 'y': 0, 'w': 0.5, 'h': 0.33},
{'x': 0, 'y': 0.33, 'w': 0.5, 'h': 0.33},
{'x': 0.5, 'y': 0.33, 'w': 0.5, 'h': 0.33},
{'x': 0, 'y': 0.66, 'w': 0.5, 'h': 0.34},
{'x': 0.5, 'y': 0.66, 'w': 0.5, 'h': 0.34}],
}
return layouts.get(panel_count, layouts[4])
# 主流程
def main():
output_dir = Path("./comic_output")
storyboard_path = Path("./storyboard.json")
with open(storyboard_path, "r", encoding="utf-8") as f:
storyboard = json.load(f)
for page in storyboard['pages']:
page_num = page['page']
panels = page['panels']
print(f"Layout page {page_num} with {len(panels)} panels...")
layout_page(page_num, panels, output_dir)
print("✅ All pages laid out!")
if __name__ == "__main__":
main()
步骤 5:添加对话框与文字(Shell 辅助脚本)
使用 ImageMagick 为漫画页面添加对话框:
#!/bin/bash
# add_dialogue.sh - 为漫画页面添加对话框
# 依赖: ImageMagick (convert, mogrify)
PAGE="page_1.png"
DIALOGUE_1="那天晚上,我们许下了约定..."
DIALOGUE_2="无论相隔多远,都要一起看星星..."
# 在页面底部添加对话框
convert "$PAGE"
-fill white -draw "roundrectangle 50,1950,1700,2150 30,30"
-fill black -draw "rectangle 50,1950,1700,2150"
-fill white -stroke none -draw "text 100,2050 '$DIALOGUE_1'"
"dialog_${PAGE}"
echo "Dialog added to $PAGE"
# 批量处理所有页面
for page in page_*.png; do
if [[ "$page" != dialog_* ]]; then
convert "$page"
-font WenQuanYi-Micro-Hei
-pointsize 48
-fill black
-annotate +100+2100 "对话文本占位"
"annotated_${page}"
echo "Processed: ${page}"
fi
done
步骤 6:完整工作流一键执行
将所有步骤串联为一个可复用的 Makefile:
# Makefile - AI漫画短篇创作工作流
.PHONY: all clean storyboard generate layout dialogue publish
# 配置
STORY := 星空下的约定
OUTPUT_DIR := ./comic_output
all: storyboard generate layout dialogue final
# 步骤 1:生成分镜脚本
storyboard:
@echo "📝 [1/5] 生成分镜脚本..."
@python3 generate_storyboard.py --story "$(STORY)" --output storyboard.json
# 步骤 2:批量生成图片
generate:
@echo "🎨 [2/5] 批量生成图片..."
@python3 batch_generate.py
# 步骤 3:自动排版
layout:
@echo "📐 [3/5] 自动排版..."
@python3 comic_layout.py
# 步骤 4:添加对话框
dialogue:
@echo "💬 [4/5] 添加对话框..."
@bash add_dialogue.sh
# 步骤 5:导出为 PDF / 图片
final:
@echo "📦 [5/5] 导出最终成品..."
@python3 export_pdf.py --input $(OUTPUT_DIR) --output "$(STORY).pdf"
# 清理临时文件
clean:
@rm -rf $(OUTPUT_DIR) storyboard.json
@echo "🧹 Cleaned up!"
执行方式:
# 一键完成整个创作流程
make all
# 或安装依赖后执行
pip install requests Pillow
make storyboard
make generate
make layout
make dialogue
make final
常见问题
Q1:4页短篇漫画需要多少张参考图才能保证角色一致性?
至少需要 2-3 张不同角度的角色正脸和侧脸图。推荐使用 IP-Adapter Face ID 或 InstantID 进行面部一致性控制。如果使用 LoRA,建议在训练时提供 10-15 张角色多角度图片。
Q2:分镜格数太多会导致图片质量下降吗?
不会直接导致质量下降,但每格图片尺寸变小后细节会减少。建议:单页不超过 6 格,每格最小分辨率不低于 512×512。对于高潮页(第 3 页),建议使用大格构图(2-3 格),留出空间给全幅或跨页画面。
Q3:批量生成时如何保持故事连贯性?
关键技巧:在提示词中统一以下要素:
1. 角色描述:每个分镜的角色描述必须一致(发色、服装、体型)
2. 场景描述:同一场景的光照、色调、背景保持一致
3. 种子锁定:对同一角色的不同分镜使用相近种子(seed 值相差 1-5)
Q4:AI生成的图片尺寸不统一怎么办?
使用上面的 comic_layout.py 脚本自动处理。脚本会自动缩放所有图片到指定格位大小,并保持比例。如果原图比例与格位不匹配,建议使用 Image.LANCZOS 重采样算法获得最佳缩放效果。
Q5:4页漫画的创作周期大概需要多久?
经过流程优化后:
– 脚本构思:30-60 分钟
– 分镜设计:30 分钟
– AI 批量生成:30-60 分钟(含筛选和重新生成)
– 排版与对话框:30 分钟
– 总计:2-3 小时(远低于手绘漫画的 2-3 天)
总结
- 4页短篇是AI漫画的最佳入门练习——结构清晰、节奏可控、完成后有成就感,是检验工具和工作流掌握程度的最好方式
- 标准化流程胜过临时发挥——将分镜脚本、批量生成、自动排版各步骤抽象为可复用的脚本和配置文件,下次创作只需替换故事内容
- 角色一致性是成败的关键——使用 IP-Adapter / LoRA / 种子锁定 的组合策略,确保同一角色在不同分镜中面貌一致
- 节奏控制决定故事体验——第 1 页建立代入感,第 2 页铺垫情绪,第 3 页释放高潮,第 4 页留下余韵,每页格数按叙事功能分配
- 工具链集成是效率倍增器——将 SD WebUI API、ControlNet、Python 排版脚本串联为 Makefile 一键流程,把创作时间从天级压缩到小时级














暂无评论内容