AI漫剧制 | 第 22 天:AI漫画短篇创作实战——4页故事从零到成品

第 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 天)

总结

  1. 4页短篇是AI漫画的最佳入门练习——结构清晰、节奏可控、完成后有成就感,是检验工具和工作流掌握程度的最好方式
  2. 标准化流程胜过临时发挥——将分镜脚本、批量生成、自动排版各步骤抽象为可复用的脚本和配置文件,下次创作只需替换故事内容
  3. 角色一致性是成败的关键——使用 IP-Adapter / LoRA / 种子锁定 的组合策略,确保同一角色在不同分镜中面貌一致
  4. 节奏控制决定故事体验——第 1 页建立代入感,第 2 页铺垫情绪,第 3 页释放高潮,第 4 页留下余韵,每页格数按叙事功能分配
  5. 工具链集成是效率倍增器——将 SD WebUI API、ControlNet、Python 排版脚本串联为 Makefile 一键流程,把创作时间从天级压缩到小时级
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容