☰
用图像生成API逐帧绘制动画GIF——零基础也能做的AI表情包生产术
2026/10/15 19:30:13 网站建设 项目流程

我最近把玩一个图像模型的 API 玩出了新花样:用社区昵称叫 Nano Banana 的模型,一帧一帧地生成定格动画,再合成 GIF,专门用来做表情包。这个思路把"AI 生成视频"这件一直很玄学的事,降维成了"AI 逐帧绘制翻页动画"这种更可控的形式。整套代码我已经整理成了开源小工具,依赖少、流程短,适合对绘图 API 有一点基础、想自己做动态素材的人,也可以直接抄去生成两帧眨眼、点头这类循环小动画。

先说清楚一个反直觉的结论:表情包本质上就是微型定格动画。一个在聊天框里无限循环的 GIF,通常只有 5 到 10 帧,每帧之间变化很小,靠循环播放形成"动起来"的错觉。这和几十年前手工定格动画的制作原理一模一样——摆一下,拍一张;再摆一下,再拍一张。区别只是把"给黏土人偶摆姿势"换成了"用文字提示词让模型修改画面"。这个认知决定了整个项目的技术路线:与其指望模型一次吐出一段连贯视频,不如老老实实回到逐帧生成的古老流程里,让现代模型扮演那个不知疲倦的动画师。

1. 为什么我不玩 AI 视频,反而选择"逐帧生成"这种笨办法

1.1 表情包的第一性原理:GIF 就是微型定格动画

如果你想做一个"猫逐渐笑出来"的表情包,你真正需要的画面其实是这样的:第一帧是平静的猫,第二帧嘴角微微上翘,第三帧咧嘴,第四帧眼睛弯成月牙。把四张图按 120 毫秒一帧循环播放,观感上就是一只猫在慢慢露出笑容。

这个结构决定了制作工具必须满足三个条件:能精确控制每一帧的画面内容;能让相邻帧之间保持"角色看起来是同一个人/同一只猫";能轻松输出小尺寸、低帧率的循环图片。我挨个试过几类视频生成工具,发现它们做表情包的体验让人头大。

1.2 用 AI 视频工具做表情包,体验一言难尽

第一个痛点是时长不可控。视频模型默认输出几秒的片段,而一个表情包通常只需要 1 秒左右的循环,我需要从完整视频里截取一段能首尾相接的循环,这个过程又麻烦又看运气,经常剪出来的循环节在接缝处有明显跳变。

第二个痛点是角色一致性不稳定。早期画面里的角色和后几秒的角色很容易出现"换了张脸"的情况,表情包才播到第二遍,脸就已经不是那张脸了,这种素材根本没法用。

第三个痛点是可控性差。我想让猫在第三帧眨眼、第五帧咧嘴,视频模型完全没有"按帧精确编辑"的入口,它能做的是整体生成,不是局部修改。我试过在提示词里描述详细时间线,结果基本等于碰运气。

所以结论很直接:视频生成模型适合"内容创作",不适合"表情包制作"。表情包需要的是对每一帧的绝对控制权,这恰恰是逐帧生成工作流的天然优势。

1.3 Nano Banana 为什么能接下这个活

Nano Banana 能在这个场景里跑通,靠的是它的"基于参考图编辑"能力:用户给一张图加一句修改指令,它输出一张在保留原图主体特征的前提下、按指令改变局部内容的新图。这正好匹配逐帧动画的生产逻辑——把上一帧当成参考图,让模型只做小幅修改。

我实测下来,这种小步编辑的生成方式,角色一致性比"每次从头画一张新图"高一个量级。打个比方:如果你让一个画家每次都凭空画一只猫,十张画里可能只有两张像同一只猫;但如果你给画家看上一张画,告诉它"只把嘴角往上提一点",它照着改出来的下一张,几乎肯定还是那只猫。

另一个附带好处是工程简单。整个生成链路不需要本地显卡,不需要部署扩散模型,一个普通 Python 脚本循环调用接口就够了。这也让我最终可以把它打包成一个轻量开源工具,而不是一个重型的生成平台。

2. 让角色不"漂移"的工程方案:帧间编辑与提示词约束

2.1 核心思路:每一帧都基于上一帧做编辑

具体流程是这样:先用一句描述加一张空白画布生成角色的第一帧,然后把第一帧作为参考图,输入"保持角色和背景不变,只把嘴角上扬"的指令,得到第二帧;再把第二帧作为参考图继续第三步。这个链路本质上是一个自回归的图像序列生成器。

这招为什么有效?因为模型每次只需要回答"画面比刚才变化了多少",而不是回答"画面应该长什么样"。人类的视觉感知也是这个原理,你之所以觉得动画连续,恰恰是因为相邻帧的差异小到可以被大脑自动补全。模型在生成时也一样,给定上一帧做锚点之后,它不会天马行空地重新想象整个场景,而是老老实实在已有画面的基础上做局部修正,角色漂移的概率因此大幅下降。

2.2 提示词模板:把动作变化翻译成模型能听懂的语言

我踩了不少提示词的坑之后,总结出一套固定套路,把提示词拆成四个部分。

第一是角色外观描述,必须具体到毛色、花纹、颜色、眼睛形状、画风,例如"一只圆脸的橘色条纹猫,白色肚皮,黑色圆眼睛,整体是卡通贴纸风格"。第二是背景与画风描述,明确写"背景保持淡蓝色纯色,镜头不动,构图不变"。第三是变化约束,写清"只改变表情,不改变面部轮廓、毛色、位置"。第四是目标动作,例如"嘴角微微上扬,眼睛稍稍眯起"。

每一帧的提示词都由这四部分拼接而成,前三部分逐字重复,只有第四部分变化。我试过偷懒只写"还是这只猫,让它笑一下",结果第 3 帧开始猫的条纹就莫名其妙少了两根,可见外观描述重复得越完整,角色漂移的概率越低。

2.3 动作步长:别让模型一步跨太大

这一步是整个项目里最需要经验的地方。相邻两帧之间的动作差异,我建议控制在"能明显感觉到变化,但不至于让人物姿态彻底改变"的范围内。比如想做一个"大笑"的动作,不要直接写"放声大笑,头后仰",而应该拆成三次:微笑、咧嘴、笑出声。每次只增加一点幅度。

道理和传统动画补中间帧完全一样——中间帧越多,过渡越顺。代价是 API 调用次数变多,成本变高。我的经验值是:一个完整的表情变化动作,拆成 6 到 10 帧比较舒服,既不会太僵硬,也不会贵到肉疼。少于 6 帧会显得跳,多于 10 帧边际收益就开始下降,画面流畅度提升不明显,成本却线性上涨。

这里的边界条件也值得说两句。往上采样、细化提示词和降低帧间隔之所以有效,是因为模型有记忆窗口限制,参考图在请求里经过压缩之后,细节锚点会被削弱。所以不要让模型承担过多的"记忆任务",它只需要关注"这张图怎么变化"就够了。剩下的流畅性交给帧数和播放速度来补。

3. 完整代码:从一段提示词到一张动图

3.1 环境准备

项目依赖非常克制,Python 3.10 以上即可,需要安装两个库:一个用来发 HTTP 请求,一个用来处理图像和合成 GIF。命令如下:

pip install requests pillow

然后是密钥管理。我强烈建议把 API 密钥写进环境变量,不要硬编码在脚本里。在 Linux 或 macOS 下用export NANO_BANANA_API_KEY=你的密钥,Windows 下在系统环境变量里配成同名变量即可。仓库里我会放一个.env.example文件说明变量名,但不会提交任何真实密钥。

3.2 帧生成主循环

核心函数很简单:读入上一帧图片,转成 base64,连同提示词一起以多模态请求体的形式发给接口,再把返回的图片字节解出来。

import base64 import os import time import requests API_KEY = os.environ.get("NANO_BANANA_API_KEY") API_URL = "官方图像编辑接口地址" def edit_image(prompt: str, input_image_path: str) -> bytes: with open(input_image_path, "rb") as f: img_b64 = base64.b64encode(f.read()).decode() payload = { "contents": [ { "parts": [ {"text": prompt}, {"inline_data": {"mime_type": "image/png", "data": img_b64}}, ] } ] } headers = {"Content-Type": "application/json"} resp = requests.post( f"{API_URL}?key={API_KEY}", json=payload, headers=headers, timeout=120, ) resp.raise_for_status() data = resp.json() b64_data = data["candidates"][0]["content"]["parts"][0]["inline_data"]["data"] return base64.b64decode(b64_data)

这个函数里最容易踩的坑是超时。生成一张图有时候会超过 30 秒,requests 默认没有超时,但长时间挂起会让脚本看起来像死掉了,所以我显式设了 120 秒。另外响应解析路径要按实际接口文档调整,不同版本响应的字段结构可能略有差异。

有了这个基础函数,主循环就直白了:把上一帧的路径传给函数,把返回的字节存成新图片,再把新图片路径更新为下一轮的参考图。循环结束,所有帧就位。

from PIL import Image character_desc = "一只圆脸的橘色条纹猫,白色肚皮,黑色圆眼睛,卡通贴纸风格。背景是淡蓝色纯色。镜头不动,构图不变,只改变表情。" actions = [ "表情平静,眼睛正常大小", "嘴角微微上扬,眼睛稍微眯起", "咧嘴笑,眼睛弯成月牙", "大笑,头微微后仰", "恢复平静但嘴角仍带笑意", ] current_img = "assets/char_base.png" frames = [] for i, action in enumerate(actions): prompt = ( character_desc + "不改变面部轮廓、毛色和位置,画面中不要出现任何文字。" + "当前动作:" + action ) out_path = f"frames/frame_{i:02d}.png" img_bytes = edit_image(prompt, current_img) with open(out_path, "wb") as f: f.write(img_bytes) frames.append(out_path) current_img = out_path time.sleep(1) # 温柔一点,避免瞬间打满并发配额

这里 sleep 的 1 秒非常重要。我实测过,连续快速请求会在十几帧之后触发限流,一旦触发,后续请求会随机失败,重试逻辑再怎么写都有浪费。提前主动降速,反而总体耗时更短。

3.3 合成 GIF

帧序列生成完毕,剩下的事情交给 Pillow。核心参数只有三个:save_all=True表示保存为动画图,duration控制每帧停留毫秒,loop=0表示无限循环。

image_list = [Image.open(path).convert("RGB") for path in frames] image_list[0].save( "emote.gif", save_all=True, append_images=image_list[1:], duration=120, loop=0, optimize=True, )

表情包的常用帧率是每秒 8 到 10 帧,也就是单帧停留 100 到 125 毫秒。这个帧率比视频低很多,但恰好符合聊天框里的观看节奏:太快了看不清表情,太慢了显得拖沓。optimize=True会让 Pillow 尝试做帧间优化,对压缩体积有帮助。

如果输出文件还是太大,可以把图像先转成调色板模式再保存,颜色数限制在 128 或 256。卡通风格的表情包本身颜色数量有限,转调色板几乎看不出画质损失,体积却能明显下降。

3.4 表情包专用后处理:压缩、字幕、循环

给 GIF 加字幕有一个特别容易犯的错:只给第一帧画文字。这样字幕会在播放到第二帧时消失,闪一下就没,效果非常奇怪。正确做法是先把所有帧都加上相同的文字,再重新合成 GIF。

from PIL import Image, ImageDraw, ImageFont font_path = "path/to/your/font.ttf" # 换成系统里可用的中文字体路径 def add_caption(frame_paths, caption_text, target_size=(480, 480)): labeled = [] for path in frame_paths: img = Image.open(path).convert("RGB").resize(target_size, Image.LANCZOS) draw = ImageDraw.Draw(img) draw.text( (16, target_size[1] - 52), caption_text, fill=(255, 255, 255), font=ImageFont.truetype(font_path, 28), stroke_width=3, stroke_fill=(0, 0, 0), ) labeled.append(img) return labeled labeled_frames = add_caption(frames, "???") labeled_frames[0].save( "emote_with_caption.gif", save_all=True, append_images=labeled_frames[1:], duration=120, loop=0, optimize=True, )

中文字体会因为系统不同而路径不同,这是脚本里最需要本地调整的一处。如果找不到中文字体,字幕会变成方块,建议在 README 里写明如何安装或指定系统字体路径。描边宽度我控制在 3 到 4 像素,太粗会盖住角色的脸,太细在浅色背景下又看不清。

字幕文字的像素风格、字号大小也直接影响表情包的质感。我的习惯是:正文 28 到 32 像素,用粗体,白字黑描边,放在画面底部偏左的位置,避免盖住角色下巴。实测下来这套参数在多数表情包场景里都成立。

4. 实测效果与翻车现场

4.1 表现最好的三类场景

我跑了二十多组不同动作的生成测试,发现三类场景的成功率明显高于其他。

第一类是整体旋转平移动作,比如点头、摇头、耸肩。这类动作不涉及局部形态的大改,模型只需要把整个头部绕着一个轴轻微转动,成功率最高。第二类是表情变化,高兴、难过、惊讶这类情绪切换,只要每步幅度控制好,出来的效果非常生动。第三类是"头顶冒东西"的魔法效果,比如凭空出现一个问号、叹气时飘出一团白气。这类场景的背景和角色保持不变,变化的只是额外元素,模型也不容易翻车。

它们的共同特点是形状变化不大,主要是姿态、表情或附加元素的改变。也就是说,模型擅长的是"适度修改"而非"彻底重构",顺着这个特性选题材,翻车率自然低。

4.2 高频翻车清单与修复办法

翻车现场比成功案例更有参考价值,我按出现频率整理了一个问题清单。

现象根因分析修复方案
第 4 帧开始颜色整体变淡参考图在请求过程中被压缩,细节锚点被削弱保存帧时统一用 PNG,提示词加上"保持原有颜色饱和度和亮度"
猫的条纹少了一根角色外观描述被简写,模型自由发挥每一帧都重复完整外观描述,把动作步长拆得更细
背景从纯色渐变成渐变天空背景描述太笼统,模型脑补了环境把背景描述写死为"淡蓝色纯色背景",不要给模型发挥空间
画面里突然出现一行文字模型把提示词里的局部文字渲染出来了在提示词末尾固定加"画面中不要出现任何文字"
连续请求到第 10 帧开始报错触发并发或频率限制主循环里加 sleep,重试时用指数退避而不是立刻硬刚

表格里第五个问题在开发时最容易忽略。我一开始以为限流只会在大量并发时出现,实际上单人连续快速请求也会触发。后来我在循环里加了 1 秒的 sleep,并且把请求封装成带三次重试的函数,才稳定跑完完整长序列。

4.3 成本、速度与上限数据

成本是绕不开的话题。这个模型的 API 按 token 计费,文本和图像 token 分别计算,具体价格需要去官方文档核对。我这边跑下来的体感是:一个 480x480 的 10 帧表情包,累计消耗在几美分量级,作为个人娱乐和素材试验完全能接受,但如果要批量生产几千个表情包,成本就得认真算了。

单帧速度方面,从提交到返回短则 5 秒、长则 20 秒,取决于画面复杂度和输出大小。一个 10 帧的表情包包括生成和合成,全程大约 3 到 5 分钟。这速度跟实时渲染没法比,但做表情包本来就是慢工出细活,我通常挂着脚本去干别的事,回来时素材已经躺在 output 目录里了。

还有一点要注意的是配额和并发上限,免费或试用额度的限制通常很苛刻。我的建议是:先在脚本里把每次请求之间的最小间隔设为 1 秒,把最大并发设为 1,跑通全流程之后再考虑提速。不要一上来就开多线程,脚本能稳定跑完比能跑得快重要得多。

5. 开源后的配置细节与扩展方向

5.1 仓库里不该出现的东西

开源项目第一件要做的事,就是把密钥排除在版本管理之外。几十行的小工具里写死密钥很常见,但一旦推到远端仓库,密钥就等于裸奔。我在仓库里放了.gitignore,明确忽略.env文件和任何包含密钥的本地配置,同时写了一个.env.example说明需要配置哪些变量。

另一个容易被忽略的点是输出目录的清理。生成的帧和中间产物体积不小,不适合全部提交到仓库。我只提交了少量示例产物,其余路径都加进.gitignore,保证别人克隆下来之后,仓库体积还控制在几兆以内。

5.2 仓库里值得放的三样东西

如果只是丢一个脚本上去,别人很难复现,这不是一个负责任的开源项目。我整理了三个必放内容。

第一,prompts.md示例文件。把我前面总结的四段式提示词模板完整写进去,再附上三个成功案例的完整提示词和对应动作列表,让使用者能直接照抄修改。第二,demo_frames/目录。放一组已经生成的帧序列和最后合成的 GIF,这样别人不用花一分钱就能先看到效果,决定要不要继续折腾。第三,一个低速模式运行脚本。把请求间隔、重试次数都调到最保守状态,保证新用户第一次运行就能成功,而不是上来就撞进限流。

5.3 这个思路还能继续做什么

逐帧生成动画这条路线一旦跑通,能做的事情比表情包多得多。

两帧眨眼动画是最轻量的变体:给定角色正面头像,让它在两帧之间切换睁眼和闭眼,循环播放就是一个活灵活现的"活"角色。Loading 动画也是个好方向:一个圆环、一张卡片、一段进度条,每帧变一点,生成 8 到 12 帧就是可以贴进网页的轻量动效。还有系列表情包:同一个角色,保持外观描述完全一致,换不同的动作脚本,批量产出一整套风格统一的表情,这是 AI 视频工具很难做到的。

技术上也有两个值得发力的方向。一是中间帧插值,先用 6 帧做出关键动作,再用传统插值算法把每两帧之间补齐,让动画从"能看到帧跳"变成"丝般顺滑",成本却能省下一大截。二是导出格式的扩展,GIF 方便但在体积上吃亏,这套帧序列流程完全可以再对齐成 WebP 动态图或 APNG 输出,体积更小、画质更好,就看目标平台怎么取舍了。

我自己的下一步打算是把这套流程封装成一个带命令行参数的脚本,输入动作列表的 JSON 文件就直接产出成品 GIF,这样以后再做表情包,连 Python 代码都不用打开,改 JSON 就行。如果你也试过用逐帧生成做动图,大概率会遇到角色漂移的问题,回头看看提示词模板是不是把外观描述写得太随意了——我踩过几次坑之后才明白,模型能帮你省下摆姿势的力气,但"角色长什么样"这件事,还是要你替它记清楚的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询