
你有没有遇到过这种情况花了一晚上写出一篇技术博客转发到工作群或朋友圈链接卡片却显示一张从网页里随机截取的烂图文字也被切了一半。在别人扫过那个卡片的零点几秒里他已经替你的内容下了判断这篇不太专业。而当 AI 可以轻松写出结构工整、语气标准的文章时问题变得更尖锐了。你可能确实一个字一个字敲完了全文但读者分不出来。他看到的是整齐的标题、标准的排版、以及一张看起来像模板批量生成的封面图。他凭什么相信屏幕对面是个活人于是就有了这个系列Going out of my way to prove Im not an AI。每一期用一类具体的技术手段把“人类创作”这个信号写进内容的某个角落。第一期的切入点是社交分享时代最容易忽略、又最能体现个人风格的东西——OG 图。这篇文章会讲清楚四件事OG 图为什么值得重新关注生成动态 OG 图的主流技术路线有哪些如何用 Python Pillow、Puppeteer、SVG Sharp 三条路线实现一套可落地的生成脚本以及怎样在视觉细节上让卡片图尽量摆脱“AI 味”。1. 为什么“证明自己不是 AI”会是一个技术问题先直面一个略显尴尬的现状现在的 AI 生成内容已经不再是“一眼假”的水平了。全自动的文章、自动生成的封面、自动排版的代码块流程比很多个人博客还要规范。而越是规范越容易让人产生怀疑——因为真人写作一定有偏好、有偏爱、有偶尔不按套路出牌的痕迹。创作者面对的问题不是“我是不是用了 AI”而是“读者怎么知道我没用 AI”。你可以在文章里写一百遍“本文由人类撰写”但这种声明没有成本也就没有说服力。真正有说服力的是那些不以“声明”为目的、却自然带出人类特征的细节。OG 图就是这类细节里非常高效的一个。想象一个真实场景你写完一篇关于动态 OG 图生成的文章把它分享到技术交流群里。三秒钟后同事回了一句“这又是 AI 写的吧”。他的判断依据可能很不讲理摘要太整齐、语气太标准、连卡片图都像是从某个模板站下载的。文章内容他根本没点开他看到的只有链接卡片上那 1200×630 像素的缩略图。这恰恰说明OG 图不是你写完文章后的“美化步骤”而是内容被阅读之前的“信任入口”。它决定了一个陌生读者是否愿意点进来。AI 生成的内容越来越多一张有辨识度、有个人风格的 OG 图反而成了区分“批量生产”和“手工制作”的直观线索。所以这个系列的目标不是去“骗过某个 AI 检测器”而是用工程手段建立自己的内容指纹。OG 图只是第一个锚点。它简单、见效快、而且能顺带解决博客链接分享时的颜值问题。2. OG 图社交分享时代的“第一印象”OG 是 Open Graph 的缩写这是 Facebook 在 2010 年提出的网页元数据协议。它的初衷很简单当用户在社交平台上分享一个网页链接时平台会读取页面head里的 og 标签把它解析成一张结构化的卡片而不是光秃秃的 URL。如今微信、QQ、LinkedIn、XTwitter、Slack、Discord、Telegram 等平台都会读取类似的元数据。一个标准的 OG 卡片需要这些字段属性作用示例值og:title卡片标题用动态 OG 图证明“我不是 AI”og:description卡片摘要第一期OG 图生成实战og:type内容类型article、website 等og:url原文链接https://example.com/posts/og-imageog:image卡片主图https://example.com/og/ep1.pngog:image:width图片宽度1200og:image:height图片高度630放到 HTML 里是这样head meta propertyog:title content用动态 OG 图证明我不是 AI / meta propertyog:description content第一期OG 图生成实战 / meta propertyog:type contentarticle / meta propertyog:url contenthttps://example.com/posts/og-image / meta propertyog:image contenthttps://example.com/og/ep1.png / meta propertyog:image:width content1200 / meta propertyog:image:height content630 / /head这里有两个关键点。第一个是尺寸。OG 图最通用的比例是 1.91:1也就是 1200×630 像素。这个比例是 Facebook 当年定的后来几乎所有社交平台都沿用了。图片太小会被拉伸太大又会被裁剪1200×630 是最不容易出错的规格。第二个是图片必须能通过公网 URL 直接访问。平台抓取器会像爬虫一样访问你给的图片地址如果图片藏在需要登录的路径里或者加载速度太慢抓取就会失败卡片直接回退到随机截图。很多站点长期使用一张固定的默认 OG 图。文章的标题明明每天都在变分享出去却永远是一张旧图。这种静态方案的问题不在“丑”而在“失真”——读者看到的卡片信息与实际内容割裂信任感自然打折。而动图生成要解决的问题就是让卡片图跟随内容动态变化文章标题、发布日期、系列编号甚至作者签名都直接渲染进图片里。3. 生成动态 OG 图的四种技术路线如果只是偶尔手动做一两张图Figma 或 PS 就够了。但一旦你的博客有一定更新频率或者你希望每次发布文章时都能自动生成对应的卡片就需要一条可编程的图片生成链路。主流的实现方式有四类它们的取舍其实很清楚。技术路线技术栈优点缺点最适合的场景服务端绘制Python Pillow依赖少、像素级可控排版能力弱中文排版较麻烦自动化脚本、服务端批量生成HTML 模板截图Node.js Puppeteer排版灵活可复用 CSS依赖 Chromium内存占用高设计感强、需要复杂布局的卡片SVG 模板转换SVG Sharp轻量、矢量清晰、文件小部分 CSS 特性不支持简单卡片、边缘渲染场景边缘平台渲染Vercel OG / CF Workers按需生成无需常驻服务器绑定平台冷启动有耗时高频变化、个性化分享卡片从实现成本来看Python Pillow 最直接适合已经用 Python 写脚本的开发者Puppeteer 方案最灵活适合前端背景、习惯用 HTML/CSS 控制设计的人SVG Sharp 体积最小适合集成到 Node 服务或边缘函数里而 Vercel OG Image 这类平台方案则适合不想维护生成服务、希望按请求动态出图的情况。这篇文章会展开前三种因为它们不依赖某个特定云厂商放在自己的服务器或 GitHub Actions 里都能跑。第四种思路你可以在理解了前三种之后自行迁移。4. 环境准备与字体选型开始写代码之前先把运行环境准备好。本文示例以 Python 和 Node.js 为主版本要求不必太严只要不是太旧的版本即可。Python 侧需要安装 Pillow 和 numpy前者负责绘图后者用来做像素级的噪点处理pip install pillow numpyNode.js 侧会用到 puppeteer 和 sharpnpm init -y npm install puppeteer sharppuppeteer 在安装时会下载 Chromium国内网络环境下可能较慢。如果你已经有 Chrome 浏览器可以配置环境变量跳过下载改用系统 Chrome具体可以看 puppeteer 官方文档这里不展开。字体是整个方案里最容易踩坑的部分。OG 图上如果出现中文而系统里没有可用的中文字体Pillow 和 Sharp 都会用默认字体绘制最终渲染出来就是一个个方块。建议先确认系统字体目录。# Linux 下常用字体目录 fc-list :langzh | head -20如果没有中文字体以 Ubuntu/Debian 为例可以安装 Noto CJK 字体包sudo apt install fonts-noto-cjkmacOS 通常自带 PingFang SC但在命令行绘图工具里不一定能直接按名字引用最稳妥的做法是在脚本里写上字体的绝对路径。Windows 可以找C:\Windows\Fonts\msyh.ttc微软雅黑或simhei.ttf黑体。如果你希望 OG 图更有“人味”可以考虑额外装一款开源手写字体比如霞鹜文楷。手写体用在签名、注释、日期这些辅助信息上会让卡片看起来更像手工排版。5. 设计策略让卡片图摆脱“AI 味”代码只是工具真正决定 OG 图气质的是设计策略。这一节梳理几条我在生成卡片时总结的“去 AI 味”原则你可以根据自己的品牌风格调整。第一避免高大上的“发布会模板”。很多 AI 生成的图片和模板站里的设计喜欢用大面积渐变 无衬线大字 居中对齐视觉上挑不出错但也记不住。人类手工做出的设计往往带有个人偏好比如强制左对齐、刻意留白、对某种特定颜色格外偏爱。OG 图不一定需要符合所有人的审美但一定要让人一眼认出“这是你的风格”。第二让图片承载“上下文信息”。除了文章标题尽量加入日期、系列编号、作者代号、文章标签等元素。这些信息单独看都不起眼组合在一起会让图片像一本书的扉页有故事感。比如一张卡片上写着2025-02-10 # Ep.1 - OG images读者会直觉地认为这张图背后有人在做系列规划。第三使用有层次的字体组合。中文标题可以用衬线体或楷体英文编号用等宽字体签名用手写体三套字体各司其职比单一字体更有“排版痕迹”。注意AI 生成图片里的文字通常过于整齐划一真实的设计师反而经常混搭字体。第四让随机性成为一种特征。手动设计不会每次像素级相同。装饰性圆点的位置、手写签名的倾斜角度、噪点的强度都允许有微小变化。这样做还有一个实际好处每次生成的图都略有差异平台不会因为图片完全相同而给出重复内容降权。第五加一层克制的噪点或纹理。纯色平面太“干净”加一层胶片颗粒或纸张纹理后画面会有手作的温度。但强度必须克制否则会变成一张“脏图”。这些策略不是为了让图片复杂而是为了让图片表达“有人认真对待这件事”。接下来我们看代码如何落地。6. 路线一Python Pillow 从零绘制 OG 图Pillow 是 Python 生态里最成熟的图像处理库。用它的路线最直接创建一个 1200×630 的画布用代码画渐变背景、写文字、加噪点最后保存成 PNG 或 JPG。这个方案不依赖浏览器适合放进 CI 流水线或后端服务。下面是一个完整的示例脚本。它生成的卡片包含渐变背景、文章标题、强调线、日期编号、一个伪随机签名和轻微胶片噪点。# 文件路径scripts/gen_og_image.py import argparse import random from PIL import Image, ImageDraw, ImageFont import numpy as np WIDTH, HEIGHT 1200, 630 BG_COLORS [ (15, 23, 42), # 深夜蓝 (30, 27, 75), # 深靛蓝 (25, 32, 64), # 暗海军蓝 ] ACCENT (255, 193, 7) # 琥珀黄作为强调色 TEXT_WHITE (240, 240, 245) TEXT_GRAY (170, 170, 185) def lerp(c1, c2, t): return tuple(int(a (b - a) * t) for a, b in zip(c1, c2)) def make_gradient(): img Image.new(RGB, (WIDTH, HEIGHT)) draw ImageDraw.Draw(img) top random.choice(BG_COLORS) bottom lerp(top, (255, 193, 7), 0.15) for y in range(HEIGHT): draw.line([(0, y), (WIDTH, y)], filllerp(top, bottom, y / HEIGHT)) return img def add_film_noise(img, intensity6): arr np.array(img).astype(np.int16) noise np.random.normal(0, intensity, arr.shape) return Image.fromarray(np.clip(arr noise, 0, 255).astype(np.uint8)) def draw_dot_markers(draw): rng random.Random(2025) for _ in range(400): x rng.randint(0, WIDTH) y rng.randint(0, HEIGHT) r rng.randint(1, 4) color (255, 255, 255, rng.randint(20, 70)) draw.ellipse((x - r, y - r, x r, y r), fillcolor) def load_font(path, size): try: return ImageFont.truetype(path, size) except OSError: return ImageFont.load_default() def main(): parser argparse.ArgumentParser(descriptionGenerate a human-feel OG image) parser.add_argument(--title, defaultGoing out of my way to prove Im not an AI) parser.add_argument(--date, default2025-02-10) parser.add_argument(--episode, defaultEp.1 - OG images) parser.add_argument(--output, defaultog.png) args parser.parse_args() img make_gradient() draw ImageDraw.Draw(img, RGBA) draw_dot_markers(draw) font_title load_font(/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc, 56) font_sub load_font(/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf, 28) lines [args.title[:22], args.title[22:44]] y 200 for line in lines: if line: draw.text((90, y), line, fontfont_title, fillTEXT_WHITE) y 86 draw.rectangle((90, y 6, 300, y 12), fillACCENT) draw.text((90, 500), f{args.date} # {args.episode}, fontfont_sub, fillTEXT_GRAY) draw.text((880, 500), human_who_codes, fontfont_sub, fillACCENT) img add_film_noise(img, intensity6) img.save(args.output, quality95) print(fOG image saved: {args.output}) if __name__ __main__: main()这段代码里有几个细节值得解释。make_gradient()用逐行画线的方式生成从上到下的渐变颜色从深色背景向琥珀色方向渐变。用lerp()做颜色插值比直接调用 Pillow 的渐变滤镜更可控。draw_dot_markers()在图片上随机散布半透明圆点模拟手绘标记感。它用random.Random(2025)固定了随机种子好处是每次生成的装饰位置一致便于回归测试如果你想每期都有不同分布可以去掉种子或改成从日期派生。标题切分这里用了粗暴的args.title[:22]和args.title[22:44]按字符数断行。中文字符宽度相对统一这样简单处理一般够用。但如果你的标题混有中英文推荐先用ImageFont.getlength()计算实际像素宽度再断行避免英文单词被截断。运行它的方式python scripts/gen_og_image.py --title 用动态 OG 图证明我不是 AI --episode Ep.1 - OG images --output og.pngPillow 方案的优点是对环境要求低不需要额外启动浏览器几十毫秒就能生成一张图。缺点是排版能力有限复杂的布局、圆角卡片、阴影、渐变文字等效果需要手动计算坐标代码量会迅速膨胀。如果只是做一张干净的文字卡片它是最合适的选择。7. 路线二HTML Puppeteer 截图生成如果你熟悉前端排版更推荐用 HTML CSS 做模板再让 Puppeteer 打开页面并截屏。这条路线把排版交给浏览器渲染引擎可以用 flex 布局、圆角、阴影、背景模糊等现代 CSS 特性设计自由度比 Pillow 高很多。先看 HTML 模板。为了让标题和日期可以从外部注入模板里放了一段读取 URL query 参数的小脚本。!-- 文件路径templates/og.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 style body { width: 1200px; height: 630px; margin: