AI漫剧制 | 第 18 天:AI漫画的叙事结构——如何让AI讲好故事

第 18/60 天

引言

叙事结构是漫画的灵魂。分镜再精美、角色再一致,如果故事讲不好,读者依然会流失。许多 AI 漫剧创作者发现,AI 擅长生成单张精彩画面,但要让 AI 理解”起承转合”、”三幕结构”、”节奏张弛”却异常困难。你扔给 AI 一段小说文本,它可能生成 10 张构图雷同的场景,或者情绪曲线完全扁平。本文从叙事理论出发,系统讲解如何设计 AI 漫画的叙事结构,让 AI 真正学会”讲故事”。

核心概念

什么是叙事结构?

叙事结构是故事的组织框架,决定事件如何展开、信息如何释放、情绪如何起伏。漫画作为一种视觉叙事媒介,其叙事结构包含三个层面:

层面 定义 AI 可控制点
宏观结构 故事的整体骨架(三幕/四幕/起承转合) 脚本分幕、页面分配
中观结构 跨页/章节的节奏安排 分镜密度、页数分配
微观结构 单页内的叙事单元(格数、视角) 提示词中的镜头指令

漫画经典叙事模型

三幕结构(好莱坞经典):
– 第一幕(Setup,25%):建立世界观、引入角色、设定冲突
– 第二幕(Confrontation,50%):冲突升级、角色成长、转折点
– 第三幕(Resolution,25%):高潮对决、剧情收束

起承转合(东方叙事):
– 起(Ki,15%):开场引入
– 承(Sho,35%):展开发展
– 转(Ten,35%):转折高潮
– 合(Ketsu,15%):收束结局

漫画 4 页短篇节奏公式:
– 第 1 页:引子 + 冲突信号(3-4 格)
– 第 2 页:展开 + 升级(4-5 格)
– 第 3 页:转折 + 高潮(5-6 格,大格突出)
– 第 4 页:收束 + 余韵(2-3 格,留白)

AI 叙事的关键限定

AI 大语言模型在生成故事时,存在几个天然局限:
1. 局部最优:AI 倾向于让每句话都”合理”,但缺乏全局叙事弧线
2. 情绪平坦:没有显式指令时,AI 不会主动制造情绪起伏
3. 重复模式:AI 容易重复使用相似的句式、场景类型和情节转折

实战步骤

步骤 1:用结构化提示词设计三幕叙事大纲

首先,用 LLM 生成分幕故事大纲。关键是要给 AI 明确的”叙事约束”而非”自由创作”。

{
  "narrative_structure": {
    "type": "three_act",
    "total_pages": 16,
    "acts": [
      {
        "name": "Act 1: Setup",
        "pages": 4,
        "tone": "intriguing, slightly mysterious",
        "key_events": [
          "主角在日常场景中展现特点",
          "收到神秘信号/邀请",
          "决定踏上旅程/接受挑战"
        ]
      },
      {
        "name": "Act 2: Confrontation",
        "pages": 8,
        "tone": "tense, escalating conflict",
        "key_events": [
          "遇到第一个障碍(轻松克服)",
          "遇到真正的困难(受挫)",
          "获得关键盟友/道具",
          "最终对决前夕"
        ]
      },
      {
        "name": "Act 3: Resolution",
        "pages": 4,
        "tone": "climactic, emotional release",
        "key_events": [
          "最终对决",
          "牺牲/损失",
          "新常态的建立"
        ]
      }
    ]
  }
}

步骤 2:用 Python 脚本生成叙事节奏分配

编写一个脚本,根据页面数和叙事模型自动分配每一页的情绪强度和格数。

#!/usr/bin/env python3
"""AI漫画叙事节奏分配器"""
import json
import math

def generate_rhythm(total_pages, model="three_act"):
    """生成每页的情绪强度和格数"""
    beats = []

    if model == "three_act":
        # 三幕:山形情绪曲线
        act_boundaries = [
            (0, int(total_pages * 0.25)),        # Act 1
            (int(total_pages * 0.25), int(total_pages * 0.75)),  # Act 2
            (int(total_pages * 0.75), total_pages)  # Act 3
        ]

        for page in range(total_pages):
            # 情绪强度在 Act 2 末到 Act 3 初达到峰值
            progress = page / total_pages
            if progress < 0.25:
                intensity = 0.3 + (progress * 1.2)  # 开场逐步上升
            elif progress < 0.65:
                intensity = 0.6 + ((progress - 0.25) * 1.5)  # 冲突升级
            elif progress < 0.75:
                intensity = 1.0 - ((progress - 0.65) * 2.0)  # 短暂低谷(反转前)
            else:
                intensity = 0.8 + ((progress - 0.75) * 0.8)  # 高潮至收束

            # 格数:情绪越高格数越少(大格突出)
            panels = max(2, min(6, int(8 - intensity * 4)))

            beats.append({
                "page": page + 1,
                "intensity": round(intensity, 2),
                "panels": panels,
                "tone": "calm" if intensity < 0.4 else "balanced" if intensity < 0.7 else "intense"
            })

    return beats

# 生成 16 页漫画的节奏分配
rhythm = generate_rhythm(16)

# 输出为表格
print(f"{'页数':<6} {'情绪强度':<10} {'格数':<6} {'基调':<10}")
print("-" * 34)
for beat in rhythm:
    print(f"{beat['page']:<6} {beat['intensity']:<10} {beat['panels']:<6} {beat['tone']:<10}")

# 保存为 JSON 供后续工作流使用
with open("/tmp/narrative_rhythm.json", "w") as f:
    json.dump(rhythm, f, ensure_ascii=False, indent=2)
print("n节奏数据已保存到 /tmp/narrative_rhythm.json")

步骤 3:设计 AI 漫画分镜提示词模板

将叙事节奏映射到具体的分镜提示词。每个分镜需要包含:镜头类型、角色状态、场景氛围。

# 分镜提示词模板:/tmp/panel_prompts.yaml
narrative_style: "少年漫画"
panel_prompt_template:
  default: |
    Manga panel, {camera_angle}, {character_state}, {background_setting},
    {atmosphere}, {color_scheme}, line art with screentone texture,
    professional manga style, high detail, cinematic composition

  camera_angles:
    establishing: "wide shot, full scene, small characters in frame"
    medium: "medium shot, character from waist up, clear expression"
    close_up: "close up, face filling frame, intense emotion"
    extreme_close_up: "extreme close up, eyes or hands only, dramatic"
    action: "dynamic angle, motion lines, speed effect"
    bird_eye: "bird's eye view, looking down, sense of scale"
    worm_eye: "worm's eye view, looking up, sense of power"

  atmospheres:
    calm: "soft lighting, warm tones, peaceful atmosphere"
    tense: "harsh shadows, cool blue tones, atmospheric tension"
    action: "dramatic lighting, speed lines, dynamic contrast"
    sad: "soft focus, muted colors, melancholic mood"
    happy: "bright lighting, warm palette, joyful expression"
    mysterious: "fog/dust particles, rim lighting, unknown shadows"

步骤 4:用 Shell 脚本批量生成分镜提示词

结合叙事节奏和分镜模板,批量生成每页的分镜指令。

#!/bin/bash
# 批量生成 AI 漫画分镜提示词
# 用法:./generate_script.sh <page_count> <story_brief>

PAGE_COUNT=${1:-16}
STORY_BRIEF=${2:-"少年踏上寻找失落的魔法之石的旅程"}
OUTPUT_DIR="/tmp/manga_script"
mkdir -p "$OUTPUT_DIR"

# 情绪曲线定义(每页情绪 0.0-1.0)
INTENSITY=(0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 0.85 0.75 0.9 1.0 0.8 0.6 0.4 0.3)
CAMERA=("establishing" "medium" "medium" "close_up" "medium" "action" "close_up" "action" "medium" "close_up" "extreme_close_up" "action" "bird_eye" "medium" "medium" "establishing")

for ((i=0; i<PAGE_COUNT; i++)); do
    PAGE=$((i+1))
    INTENSITY_VAL=${INTENSITY[$i]}

    if (( $(echo "$INTENSITY_VAL < 0.3" | bc -l) )); then
        ATMOSPHERE="calm"
    elif (( $(echo "$INTENSITY_VAL < 0.7" | bc -l) )); then
        ATMOSPHERE="tense"
    else
        ATMOSPHERE="action"
    fi

    CAM=${CAMERA[$i]}

    cat > "$OUTPUT_DIR/page_${PAGE}.json" << EOF
{
  "page": $PAGE,
  "intensity": $INTENSITY_VAL,
  "camera": "$CAM",
  "atmosphere": "$ATMOSPHERE",
  "prompt": "Manga panel, ${CAM}, scene from story: ${STORY_BRIEF// /_}, ${ATMOSPHERE} atmosphere, professional manga style, high detail"
}
EOF
    echo "Generated page $PAGE: $CAM | $ATMOSPHERE"
done

echo "All $PAGE_COUNT pages generated in $OUTPUT_DIR"

步骤 5:用 ControlNet 和分镜脚本实现叙事一致性

在 ComfyUI 或 SD WebUI 中,使用 ControlNet 的 Canny 和 OpenPose 模型确保每页的角色姿态和构图符合叙事需求。

#!/usr/bin/env python3
"""ComfyUI API 调用:按叙事节奏生成漫画页面"""
import requests
import json
import time

COMFYUI_URL = "http://localhost:8188"

def generate_manga_page(page_data, character_ref, style_ref):
    """根据分镜数据生成漫画页面"""
    prompt = {
        "3": {
            "class_type": "KSampler",
            "inputs": {
                "seed": int(time.time()),
                "steps": 30,
                "cfg": 7.0,
                "sampler_name": "euler_a",
                "scheduler": "normal",
                "denoise": 1.0,
                "model": ["4", 0],
                "positive": ["6", 0],
                "negative": ["7", 0],
                "latent_image": ["5", 0]
            }
        },
        "4": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": style_ref}},
        "5": {"class_type": "EmptyLatentImage", "inputs": {"width": 768, "height": 1080, "batch_size": 1}},
        "6": {
            "class_type": "CLIPTextEncode",
            "inputs": {
                "text": f"Manga panel, {page_data['camera']}, {page_data['atmosphere']}, {character_ref}, professional manga style, high detail, screentone",
                "clip": ["4", 1]
            }
        },
        "7": {
            "class_type": "CLIPTextEncode",
            "inputs": {
                "text": "nsfw, lowres, bad anatomy, bad hands, text, error, extra finger, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, blurry",
                "clip": ["4", 1]
            }
        },
        "8": {
            "class_type": "ControlNetLoader",
            "inputs": {"control_net_name": "control_v11p_sd15_canny.pth"}
        },
        "9": {
            "class_type": "ControlNetApply",
            "inputs": {
                "control_net": ["8", 0],
                "image": ["10", 0],
                "strength": 0.8,
                "conditioning": ["6", 0]
            }
        }
    }

    # 提交工作流
    resp = requests.post(f"{COMFYUI_URL}/prompt", json={"prompt": prompt})
    result = resp.json()
    print(f"Page {page_data['page']}: submitted, prompt_id={result.get('prompt_id', 'unknown')}")
    return result

# 加载叙事节奏
with open("/tmp/narrative_rhythm.json") as f:
    rhythm = json.load(f)

# 逐页生成
for beat in rhythm:
    print(f"Generating page {beat['page']}...")
    generate_manga_page(beat, "character_001", "manga_style_v2")
    time.sleep(2)  # 避免 API 过载

print("所有页面已提交生成!")

常见问题

Q1: AI 总把所有页面画成同样构图怎么办?

在提示词中显式指定每页的镜头类型(wide/medium/close-up/action),并建立镜头切换规则:连续两页不能使用相同镜头,每 4 页必须包含一个 extreme close-up 或 bird’s eye view 作为视觉锚点。

Q2: 如何让 AI 理解”情绪起伏”?

使用情绪曲线图(0-1 标度)提前规划每页的情绪强度,并映射到具体的提示词参数:色彩方案(冷色=低情绪,暖色=高情绪)、光影对比(柔和=低情绪,强烈=高情绪)、格数(多格=叙事进展,少格=情绪凝聚)。

Q3: 4 页短篇和 16 页长篇在叙事上有什么不同?

4 页短篇必须”一页一事件”:第 1 页建置,第 2 页冲突,第 3 页反转,第 4 页收束。16 页长篇则允许更细腻的节奏,可以在第 6-8 页设置”假胜利”作为情绪陷阱,让最终高潮更有冲击力。AI 适合短篇,长篇需要更频繁的人工干预。

Q4: 有没有现成的叙事模板可以用?

可以构建”叙事模板库”:冒险模板(出发→考验→归来)、悬疑模板(线索→误导→反转)、情感模板(日常→裂痕→和解)。每种模板预定义情绪曲线、镜头分布和格数分配,每次创作时选模板 + 填充内容,节省大量设计时间。

Q5: 多线叙事如何让 AI 处理?

多线叙事需要”分线标记”:在线索 A 的提示词里加 [storyline: A],线索 B 加 [storyline: B],然后用 Python 脚本按交叉节奏(A-B-A-B-A-B-A)排列页面顺序。AI 本身不擅长多线交织,但通过结构化分页可以模拟出多线叙事的效果。

总结

  1. 叙事结构先行:AI 漫画的质量上限取决于叙事设计的质量,而非单张画面的精美程度。先画曲线,再画画面。
  2. 显式控制节奏:情绪曲线、镜头分布、格数分配——这些指标必须量化到每个页面,才能让 AI 产出有起伏的故事。
  3. 模板化降低耦合:建立叙事模板、分镜提示词模板和节奏分配脚本,将”讲故事”从每次创作中解放出来,变成可复用的基础设施。
  4. AI 擅长短篇:4-8 页的短篇故事是 AI 漫画的甜蜜点,长篇连载需要更多人工校准和叙事设计师介入。
  5. 工具链整合:将叙事节奏分配器、分镜提示词生成器和 ComfyUI API 调用串联成一条流水线,从”写故事”到”出画面”全自动化。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

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

    暂无评论内容