最近AI漫剧(AI Comic Drama)在各大平台火得一塌糊涂。把静态漫画分镜转化为动态视频,配上AI配音和BGM,几分钟就能产出一条“动漫感”十足的短视频。作为技术人,我第一反应不是“这玩意儿有意思”,而是“这背后怎么实现的?”
经过一个周末的折腾,我搭了一套从剧本到成片全自动的AI漫剧生成管线。这篇文章会从技术实现角度,拆解每个环节的核心逻辑、踩坑经验,以及可落地的代码方案。
AI漫剧制作本质上是一条多模态内容生成流水线,我把它分成五个核心层:
层级 | 功能 | 核心技术 |
|---|---|---|
剧本生成层 | 将用户Prompt扩展为结构化分镜脚本 | LLM + JSON Schema约束 |
图像生成层 | 根据分镜描述生成角色+场景图 | Stable Diffusion + ControlNet |
动态化层 | 让静态图产生运动效果 | 图生视频模型 / 仿射变换 |
配音合成层 | 生成角色对话语音 | TTS + 音色克隆 |
合成渲染层 | 音画同步输出成片 | FFmpeg + 时间线编辑 |
整体数据流如下:
User Prompt → 剧本脚本(JSON) → 分镜图序列 → 动态视频片段 → 配音轨 → 合成MP4漫剧的核心是分镜脚本。直接让LLM写一段故事不够结构化,需要强制输出JSON格式的剧本。
SYSTEM_PROMPT = """
你是一个漫剧编剧,请根据用户故事生成分镜脚本。
输出格式为JSON数组,每个分镜包含:
- scene_id: 场景编号
- description: 场景描述(用于绘图)
- dialogue: 角色台词(含情感标签)
- duration: 预计时长(秒)
- camera: 镜头运动(pan/zoom/static)
- bgm_style: 背景音乐风格
- characters: 场景中角色列表(含服饰、表情)
"""
def generate_script(user_prompt: str) -> list[dict]:
response = llm.chat(
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt}
],
response_format={"type": "json_object"} # 强制JSON
)
return json.loads(response.content)["scenes"]剧本输出的字段必须考虑下游消耗。比如 characters 字段要包含服饰描述,因为后续SD生成图像需要这些细节。camera 字段留给动态化模块决定运动参数。
踩坑点:LLM生成的 description 经常太长,超过SD的77 token长度限制。我加了一层摘要过滤,用另一个轻量LLM把描述压缩到50字以内,保留主体、动作、场景三要素。
漫剧的“漫”字决定了画风必须统一。这里最大的技术挑战是角色一致性——同一角色在不同分镜中要长得像。
方案 | 一致性 | 速度 | 成本 |
|---|---|---|---|
LoRA微调 | ⭐⭐⭐⭐⭐ | 慢 | 高 |
IP-Adapter | ⭐⭐⭐⭐ | 快 | 中 |
ControlNet+Reference | ⭐⭐⭐ | 快 | 低 |
纯Prompt控制 | ⭐⭐ | 最快 | 最低 |
我选择了 LoRA微调 方案:先用角色设定图生成30-50张同角色不同姿态的图片,训练一个轻量LoRA,然后在生成分镜时加载该LoRA。
from diffusers import StableDiffusionPipeline
from peft import LoraConfig, get_peft_model
# 1. 准备角色数据集(统一裁剪512x512)
# 2. 配置LoRA
lora_config = LoraConfig(
r=32, # rank
lora_alpha=64,
target_modules=["to_q", "to_v", "to_k", "to_out.0"],
lora_dropout=0.1,
)
# 3. 训练(用diffusers + accelerate)
# 实际代码略,核心是逐张生成角色图并计算CLIP相似度作为Reward实际训练时,我用了 DreamBooth + LoRA 的组合,在A100上跑约400步,15分钟完成。每生成一个新角色就训一个LoRA,存在本地缓存。
def generate_scene_image(prompt: str, character_lora_path: str) -> Image:
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5"
)
pipe.load_lora_weights(character_lora_path)
# 负面提示词
negative_prompt = "bad anatomy, distorted, blurry, low quality, watermark"
image = pipe(
prompt=prompt,
negative_prompt=negative_prompt,
num_inference_steps=30,
guidance_scale=7.5,
height=512,
width=768, # 16:9宽高比
).images[0]
return image踩坑点:不同分镜用同一个LoRA,但Prompt中角色描述不一致时,LoRA权重和Prompt会“打架”。解决方案:在Prompt中固定角色名称(如“角色-小美”),训练时也用同一套名字。
这是让漫剧“动起来”的关键。我尝试了两种方案。
对静态图做 pan/zoom/rotate 伪运动,配合镜头运动字段:
import cv2
import numpy as np
def apply_camera_motion(image: np.ndarray, motion: str, duration: int, fps: int = 30) -> list[np.ndarray]:
"""生成运动帧序列"""
h, w = image.shape[:2]
frames = []
total_frames = duration * fps
if motion == "pan_left":
for i in range(total_frames):
offset = int((i / total_frames) * w * 0.3)
M = np.float32([[1, 0, -offset], [0, 1, 0]])
frame = cv2.warpAffine(image, M, (w, h))
frames.append(frame)
elif motion == "zoom_in":
for i in range(total_frames):
scale = 1 + 0.15 * (i / total_frames)
M = cv2.getRotationMatrix2D((w/2, h/2), 0, scale)
frame = cv2.warpAffine(image, M, (w, h))
frames.append(frame)
# ... 其他运动
return frames优点:零推理成本、速度快、不会崩坏。 缺点:运动幅度有限,没有“剧情感”。
用 Stable Video Diffusion 或 AnimateDiff 生成真正的动态视频:
from diffusers import StableVideoDiffusionPipeline
def generate_video_from_image(image: Image, prompt: str, duration: int = 3):
pipe = StableVideoDiffusionPipeline.from_pretrained(
"stabilityai/stable-video-diffusion-img2vid"
)
pipe.to("cuda")
# SVD默认生成25帧,调整fps控制时长
frames = pipe(
image=image,
decode_chunk_size=8,
generator=torch.manual_seed(42),
).frames[0]
return frames # list of PIL Images问题:
优化策略:我用 ControlNet + AnimateDiff 组合,用深度图控制运动幅度,限制人物形变:
# 用Depth ControlNet约束结构
from diffusers import ControlNetModel
controlnet = ControlNetModel.from_pretrained("lllyasviel/control_v11f1p_sd15_depth")
# 结合AnimateDiff做运动生成实际项目里,我采用 混合方案:静态对话场景用仿射变换(节省成本),关键动作场景用SVD。
漫剧的“剧”字落在配音上。我用 Edge TTS + GPT-SoVITS 两条腿走路。
import edge_tts
async def generate_voice(text: str, voice: str = "zh-CN-XiaoxiaoNeural"):
"""生成语音"""
communicate = edge_tts.Communicate(text, voice)
audio_path = f"audio_{hash(text)}.mp3"
await communicate.save(audio_path)
return audio_path
# 支持情感调节:开心、悲伤、愤怒等
# voice参数可选不同音色如果想做固定角色音色,用GPT-SoVITS训练:
# 1. 准备角色音频样本(3-10分钟干净人声)
# 2. 训练GPT-SoVITS模型(约2小时)
# 3. 推理时加载模型
from GPT_SoVITS.inference import TTS
tts = TTS(
model_path="character_voice.pth",
config_path="config.yaml"
)
audio = tts.generate("台词文本", emotion="happy")关键细节:配音需要精确对齐分镜时长。我的做法是先计算台词朗读速度(中文约4字/秒),再动态调整分镜持续时间。
def estimate_duration(text: str) -> float:
"""估算朗读时长"""
char_count = len(text)
base_duration = char_count / 4.0 # 4字/秒
return max(base_duration, 1.5) # 最短1.5秒最后把所有素材合成MP4。这里最大的坑是音画同步。
import subprocess
import json
from moviepy.editor import *
def compose_video(scenes: list[dict], scene_videos: list, audio_tracks: list):
"""合成最终视频"""
clips = []
for i, (scene, video_frames, audio_path) in enumerate(zip(scenes, scene_videos, audio_tracks)):
duration = scene["duration"]
# 生成视频片段
if isinstance(video_frames, list):
# 帧序列 → VideoClip
clip = ImageSequenceClip(video_frames, fps=24)
else:
clip = VideoFileClip(video_frames)
# 设置时长
clip = clip.subclip(0, duration)
# 加载音频
audio_clip = AudioFileClip(audio_path)
if audio_clip.duration > duration:
audio_clip = audio_clip.subclip(0, duration)
else:
# 音频短则静音补齐
silence = CompositeAudioClip([audio_clip])
clip = clip.set_audio(silence)
clip = clip.set_audio(audio_clip)
clips.append(clip)
# 拼接所有分镜
final_video = concatenate_videoclips(clips)
final_video.write_videofile("output.mp4", fps=24, codec="libx264", audio_codec="aac")漫剧需要背景音乐,我用 pydub 叠加音轨:
from pydub import AudioSegment
def add_bgm(video_path: str, bgm_path: str, volume_reduction: int = 20):
video_audio = AudioSegment.from_file(video_path)
bgm = AudioSegment.from_file(bgm_path)
# 循环BGM到视频长度
bgm = bgm * (len(video_audio) // len(bgm) + 1)
bgm = bgm[:len(video_audio)]
# 降低BGM音量(-20dB)
bgm = bgm - volume_reduction
# 混合
mixed = video_audio.overlay(bgm)
mixed.export("output_with_bgm.mp4", format="mp4")所有模块串联成一条Pipeline:
class AIMangaPipeline:
def __init__(self):
self.llm = init_llm()
self.sd_pipe = init_sd_pipe()
self.tts = init_tts()
def run(self, user_prompt: str) -> str:
# Step 1: 生成分镜脚本
script = self.generate_script(user_prompt)
# Step 2: 逐分镜生成
scene_data = []
for scene in script:
# 2.1 生成图像
image = self.generate_image(scene)
# 2.2 生成动态视频
video_frames = self.animate_scene(image, scene)
# 2.3 生成配音
audio = self.generate_audio(scene["dialogue"])
scene_data.append((scene, video_frames, audio))
# Step 3: 合成输出
output_path = self.compose_video(scene_data)
return output_path性能数据(单卡A100):
现象:同角色在不同分镜里长相、衣服颜色不一致。 根因:SD生成时Prompt描述不固定 + LoRA权重干扰。 解决:
角色-小美(黑长直、白裙)现象:字幕显示和声音对不上。
解决:用 pysrt 生成精确字幕时间轴,根据音频波形做对齐:
import librosa
def align_subtitles(audio_path: str, text: str, duration: float):
"""根据音频能量做词级对齐(简化版)"""
y, sr = librosa.load(audio_path)
energy = librosa.feature.rms(y=y)
# 根据能量峰值切分时间点
# 返回SRT格式字幕SVD需要约15GB显存。优化方案:
accelerate 做CPU offloaddecode_chunk_sizeAI漫剧制作是一个典型的多模态工程问题,涉及LLM、CV、语音、视频处理多个领域。技术难度不高但工程复杂度大,核心在于各模块之间的数据结构对齐和一致性问题。
如果你也想动手尝试,建议从 LLM生成脚本 + SD生成图片 + FFmpeg合成 这个最小链路开始,再逐步加配音、动效、BGM。别一上来就上SVD,先跑通再优化。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。