【Agent Harness】AI 生成图片里的文字全是“鬼画符”?一文讲透错字根源与工业级解决方案

AI 生成图片里的文字全是“鬼画符”?一文讲透错字根源与工业级解决方案

摘要:AI 生成图片中的文字错乱(如“新年快乐”变“新年快东”)是文生图领域的常见痛点。本文深入剖析了错字的五大技术根源:文本编码器不关注字符级信息、图像生成损失不惩罚拼写错误、VAE 压缩丢失笔画细节、训练数据标注粗糙、布局与生成任务耦合过重。并提供了从“无字背景+排版引擎”到“字形条件局部编辑”再到“自研双编码器模型”的工业级解决方案,帮助开发者和设计师从架构上根治文字错乱问题。
关键词:AI 文生图;文字生成;错字;Stable Diffusion;Midjourney;DALL-E;文本编码器;VAE;OCR;Glyph-ByT5;AnyText;工业级方案

就像本文封面图一样,你让 AI 画一张“新年快乐”的海报,它给你输出“新年快东”;让它写 “SALE”,它给你写 “SLAE”。更离谱的是,同样的 Prompt 跑十次,可能错得各不相同。为什么 AI 画图就是写不对字?这事到底能不能根治?

一、现象:越想让 AI 写字,它越容易翻车

几乎所有用过 Midjourney、Stable Diffusion、DALL‑E 的文生图玩家都遇到过:

  • 英文单词字母顺序颠倒(COFFEECOFFE
  • 字母被吃掉(WELCOMEWELCME
  • 字母被替换(SALESAL ESALC
  • 中文缺胳膊少腿(“福”变成“示”+“一口”)
  • 多个文字区域互相串词
  • 不存在的“幻觉文字”自动冒出来
  • 同一 Prompt 跑多次,错误各不相同

最典型翻车现场

生成一个店铺招牌,要求写 “FRESH COFFEE”,结果图里写的是 “FRES H COFFE” 或 “FRE5H COFFEE”。

flowchart LR A[用户期望] --> B["“FRESH COFFEE”"] C[AI 实际输出] --> D["“FRES H COFFE”"] B -.-> C style B fill:#a8e6cf style D fill:#ffb3b3

很多人的第一反应是“换 Prompt”“加权重”“多抽几次卡”——但这只能碰运气。要根治,得先搞清楚底层原理。


二、核心结论:文生图模型不是打字机,是“视觉模仿者”

大多数文生图模型本质上是像素/视觉纹理生成器,不是文字排版引擎。

语言模型生成文字时,输出的是离散 token,错一个字母就会受到明确惩罚(交叉熵损失)。而图像模型生成文字时,输出的是连续的像素或图像 latent。对模型来说,“SALE”和“SLAE”在形状、颜色、位置和整体语义上可能已经足够相似,因此普通图像生成损失不会强烈惩罚这种拼写错误。

可以这样理解:

  • 文字排版软件是在“打印一个确定的字符串”;
  • 文生图模型是在“模仿一块看起来像有文字的招牌”。

因此,仅靠改 Prompt 很难从根本上解决。真正可靠的方案,是把文字内容、文字布局和视觉风格分开处理


三、为什么错字屡禁不止?五大技术原因拆解

1. 文本编码器只看“语义”,不数“字母”

很多文生图模型使用 CLIP、T5 等文本编码器,把 Prompt 转换成向量。文本编码器主要表达的是“画面是什么意思”“有什么对象”“风格是什么”,而不是为每一个字母、汉字建立严格的一一对应关系。

例如:

A coffee shop sign saying "FRESH COFFEE"

模型可能很好地理解:

  • 咖啡店
  • 木质招牌
  • “新鲜咖啡”这一语义
  • 英文招牌的视觉样式

但它未必获得类似这样的硬约束:

字符1必须是 F
字符2必须是 R
字符3必须是 E
……
字符12必须是 E

证据:Glyph-ByT5 研究指出,常规 CLIP 更关注概念级视觉语义,而不是字符级信息及其与字形的对应关系,因此研究者专门引入了“字符感知、字形对齐”的编码器。

flowchart TD subgraph A[常规文本编码器] A1["Prompt: \"FRESH COFFEE\""] --> A2[CLIP/T5 向量] A2 --> A3[语义: 咖啡+新鲜+招牌] end subgraph B[真正的文字约束] B1[每个字符的 code point] B2[每个字符的字形] B3[字符顺序与位置] end A -.-> B style A fill:#fff3cd style B fill:#d4edda

2. 图像生成损失 ≠ 拼写检查

典型扩散模型学习的是:

预测噪声 ≈ 真实噪声,或者让生成图像在视觉特征上接近训练图像。

但是,对文字而言:

  • 少一横,可能从一个汉字变成另一个汉字;
  • E 多一笔,可能变成类似 B 的形状;
  • rn 粘在一起,可能看起来像 m
  • 两个相邻字母交换,整体图案仍然很像。

这些错误在人类语言层面非常严重,但在像素距离、感知距离或图像语义距离上,差异可能很小。损失函数不会因此加大惩罚,模型自然懒得纠结。

3. Latent 空间压缩把笔画“磨没了”

许多文生图系统并不是直接在最终像素上生成,而是在经过 VAE 压缩的 latent 空间中生成。

例如,一张 1024×1024 的图片会先缩成 64×64 的特征图。普通物体能承受这种压缩:

  • 一棵树少几片叶子,仍然是一棵树;
  • 一张脸的纹理略有变化,仍然是一张脸。

文字却不能

  • 一个汉字可能只占几个 latent 单元;
  • 短横、点、撇可能在压缩中消失;
  • 相邻笔画可能合并;
  • 解码时又可能被重新“猜”出来。

TextInVision 等分析明确指出,VAE 本身可能成为视觉文字生成的瓶颈。SceneVTG++ 则使用像素级去噪来保留小文字特征,避开 latent VAE 压缩造成的细节损失。

4. 训练数据中文字标注不够“细”

训练图像中的文字通常来自:

  • Logo、路牌、海报、包装、水印
  • 常见品牌、常见英文短词

但这些图片的 Caption 往往不会准确抄录图片上的全部文字。即使使用 OCR 自动标注,也会受到模糊、遮挡、艺术字体、倾斜和语言识别错误影响。

因此模型可能学会:

“咖啡店招牌应该长得像有英文单词”

而不是:

“无论给我什么字符串,我都能准确逐字符排版”

研究也印证了这点:模型生成常见单词(如 SALE)比生成随机字符串(如 X7KpQ)准确得多。这说明它更多依赖记忆而非真正的文字复制能力

5. 布局与字符生成同时进行,任务太重

普通文生图模型要一次完成:

  • 决定文字放在哪里
  • 决定几行、字体、字号、透视
  • 决定每个字符的准确形状
  • 同时生成背景和其他对象

文字越多、区域越多,注意力越容易混乱,出现:

  • 丢字、多字、字符顺序交换
  • 两行文字互相串字
  • 自动增加不存在的装饰文字

TextDiffuser 因此把任务拆成两个阶段:先规划文字布局和字符区域,再由扩散模型结合布局生成图像。


四、为什么中文、阿拉伯文更“难搞”?

中文的“地狱级”难度

  1. 字符规模大:英文 26 个字母,中文常用汉字几千个。
  2. 笔画密度高:小字号下,相同面积内要容纳更多结构。
  3. 形近字多:少一笔或多一笔就变另一个字(“日”与“曰”,“未”与“末”)。
  4. 字体差异大:宋体、黑体、楷体、书法体形态迥异。
  5. 训练数据不均衡:高质量中文 OCR 标注远少于英文。

FLUX-Text 等研究特别指出,中文、日文、韩文等非拉丁文字的复杂字形更容易发生笔画缺失和字形扭曲,细微错误也很容易被读者发现。

复杂文字体系(阿拉伯文、印度文、韩文)

  • 阿拉伯文:字母根据位置改变形态,涉及连写和从右向左排版。
  • 印度文:辅音+元音符号构成复杂组合字形。
  • 韩文:需要正确组合初声、中声、终声。
  • Emoji/组合字符:一个用户感知字符可能由多个 code point 组成。

Unicode 本身区分 code point、字素簇和用户感知字符。简单地把 Unicode 字符逐个作为普通 token 处理,远不能完成正确排版。


五、最可靠的解决原则:让专业的人做专业的事

不要让图像模型负责“写字”,让它负责“画背景和视觉效果”。

把文字内容、文字布局和视觉风格分开处理,可靠程度远高于“一次生成”。

使用场景 推荐方法 文字准确性
海报、Banner、封面、电商图 生成无字背景,再用 SVG/Canvas 排字 最高,确定性正确
图表、教学材料、长段落 HTML/SVG/PDF 排版引擎 最高
照片中的路牌、包装、店招 字形约束的局部重绘 + OCR 校验 较高
艺术字、霓虹字、金属字 确定字形骨架 + AI 风格化 较高
直接一次性文生图并要求准确长文字 不推荐 不稳定

“先生成无字背景,再渲染文字”已成为视觉文字研究领域的明确共识:先创建 text-free background,再根据字形条件融合文字,控制更准确,实际可用性更强。


六、工业级落地的三套方案

方案 A:背景生成 + 确定性文字排版(最推荐)

适用于海报、广告、电商、社交媒体。

流程图

flowchart TD User[用户需求] --> LLM[LLM 拆分场景描述和文字内容] LLM --> Gen[图像模型生成无字背景] LLM --> Layout[布局引擎确定文本框] Gen --> Merge[融合层] Layout --> Render[SVG/Canvas 排版准确文字] Render --> Merge Merge --> OCR[OCR 校验] OCR -->|通过| Out[输出] OCR -->|错误| Fix[局部修复或重新排版]

关键数据设计

文字内容必须作为独立结构化字段保存,不能混在 Prompt 里。

{
  "scene_prompt": "夜晚的现代咖啡店门面,中央有一块空白木质招牌,不出现任何文字",
  "text_items": [
    {
      "id": "sign_1",
      "content": "城市咖啡",
      "locale": "zh-Hans",
      "position": [0.23, 0.16, 0.54, 0.14],
      "alignment": "center",
      "render_mode": "vector_then_blend"
    }
  ]
}

规则

  • content 不允许被 LLM 改写或翻译;
  • 场景 Prompt 中只描述“空白招牌”,不让模型生成正式文字;
  • 文字在最终输出尺寸上排版,不依赖生成模型超分;
  • 保留 SVG/PSD 图层,便于后续修改。

Python 代码示例:方案 A 核心步骤实现

以下代码演示了使用 LLM 拆分需求、调用图像模型生成无字背景、再用 PIL 渲染精确文字的全流程。实际生产环境中需替换为真实的 LLM API 和图像模型 API。

import json
import requests
from PIL import Image, ImageDraw, ImageFont
from typing import Optional

# ============================================================
# 步骤 1:使用 LLM 拆分场景描述和文字内容
# ============================================================
def parse_user_request_with_llm(user_input: str) -> dict:
    """
    模拟 LLM 将用户需求拆分为场景描述和文字项列表。
    实际使用时请替换为真实的 LLM API 调用(如 OpenAI / 本地模型)。
    """
    # 模拟 LLM 输出(实际应调用 LLM API)
    mock_llm_output = {
        "scene_prompt": "夜晚的现代咖啡店门面,中央有一块空白木质招牌,不出现任何文字",
        "text_items": [
            {
                "id": "sign_1",
                "content": "城市咖啡",
                "locale": "zh-Hans",
                "position": [0.23, 0.16, 0.54, 0.14],  # [x, y, w, h] 归一化坐标
                "alignment": "center",
                "font_size_ratio": 0.08  # 字号相对于图片高度的比例
            }
        ]
    }
    return mock_llm_output


# ============================================================
# 步骤 2:调用图像模型生成无字背景
# ============================================================
def generate_text_free_background(scene_prompt: str,
                                   width: int = 1024,
                                   height: int = 768) -> Optional[Image.Image]:
    """
    调用图像模型生成不含文字的背景图。
    实际使用时请替换为 Stable Diffusion / DALL-E / Midjourney API。
    """
    # 模拟:生成一张纯色背景(实际应调用图像生成 API)
    # 示例:requests.post("https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image", ...)
    print(f"[步骤 2] 生成无字背景,Prompt: {scene_prompt}")
    background = Image.new("RGB", (width, height), color=(30, 40, 60))
    # 模拟一些渐变效果
    draw = ImageDraw.Draw(background)
    for y in range(height):
        r = int(30 + 20 * (y / height))
        g = int(40 + 30 * (y / height))
        b = int(60 + 40 * (y / height))
        draw.line([(0, y), (width, y)], fill=(r, g, b))
    return background


# ============================================================
# 步骤 3:使用 PIL 将精确文字渲染到背景上
# ============================================================
def render_text_on_background(background: Image.Image,
                              text_items: list,
                              font_path: str = "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc") -> Image.Image:
    """
    在背景图上渲染精确文字。文字内容由 LLM 解析得到,不经过图像模型。
    注意:font_path 需替换为系统中实际的中文字体路径。
    """
    img = background.copy()
    draw = ImageDraw.Draw(img)
    img_width, img_height = img.size

    for item in text_items:
        content = item["content"]
        # 关键:content 不允许被 LLM 改写或翻译,直接使用原始字符串
        x_norm, y_norm, w_norm, h_norm = item["position"]
        alignment = item.get("alignment", "center")
        font_size = int(img_height * item.get("font_size_ratio", 0.08))

        # 计算文字区域的实际像素坐标
        box_x = int(x_norm * img_width)
        box_y = int(y_norm * img_height)
        box_w = int(w_norm * img_width)
        box_h = int(h_norm * img_height)

        # 加载字体(生产环境应使用系统字体或自定义字体文件)
        try:
            font = ImageFont.truetype(font_path, font_size)
        except IOError:
            # 回退到默认字体(不推荐,中文可能显示为方块)
            print(f"[警告] 未找到字体 {font_path},使用默认字体")
            font = ImageFont.load_default()

        # 计算文字尺寸,确定对齐位置
        bbox = draw.textbbox((0, 0), content, font=font)
        text_width = bbox[2] - bbox[0]
        text_height = bbox[3] - bbox[1]

        if alignment == "center":
            text_x = box_x + (box_w - text_width) // 2
        elif alignment == "left":
            text_x = box_x
        elif alignment == "right":
            text_x = box_x + box_w - text_width
        else:
            text_x = box_x

        # 垂直居中
        text_y = box_y + (box_h - text_height) // 2

        # 绘制文字(先描边再填充,增强可读性)
        # 描边
        stroke_color = (0, 0, 0)
        for dx in [-1, 0, 1]:
            for dy in [-1, 0, 1]:
                if dx != 0 or dy != 0:
                    draw.text((text_x + dx, text_y + dy), content,
                              font=font, fill=stroke_color)
        # 填充
        draw.text((text_x, text_y), content, font=font, fill=(255, 255, 200))

        print(f"[步骤 3] 渲染文字 '{content}' 到位置 ({box_x}, {box_y}),字号 {font_size}")

    return img


# ============================================================
# 主流程:串联三个步骤
# ============================================================
def main():
    # 用户原始需求(不可被 LLM 改写)
    user_request = "帮我生成一张咖啡店招牌海报,招牌上写'城市咖啡',夜晚风格"

    # 步骤 1:LLM 拆分
    print("=" * 60)
    print("[步骤 1] LLM 拆分用户需求")
    parsed = parse_user_request_with_llm(user_request)
    scene_prompt = parsed["scene_prompt"]
    text_items = parsed["text_items"]
    print(f"场景 Prompt: {scene_prompt}")
    print(f"文字项: {json.dumps(text_items, ensure_ascii=False, indent=2)}")
    print()

    # 步骤 2:生成无字背景
    print("=" * 60)
    background = generate_text_free_background(scene_prompt)
    background.save("step2_background.png")
    print("背景图已保存为 step2_background.png")
    print()

    # 步骤 3:渲染精确文字
    print("=" * 60)
    result = render_text_on_background(background, text_items)
    result.save("step3_final_output.png")
    print("最终输出已保存为 step3_final_output.png")
    print()

    print("=" * 60)
    print("✅ 方案 A 核心流程完成!")
    print("关键原则:文字内容由 LLM 解析后直接传给排版引擎,")
    print("          图像模型只负责生成无字背景,不参与文字生成。")


if __name__ == "__main__":
    main()

代码要点说明

  1. 文字内容不可变content 字段由 LLM 从用户需求中解析,后续直接传给 PIL 渲染,不允许图像模型或 LLM 再次改写。
  2. 场景 Prompt 不含文字scene_prompt 中明确要求“不出现任何文字”,避免图像模型生成错误文字。
  3. 确定性排版:使用 PIL 的 ImageFont.truetype 加载系统字体,确保每个字符精确渲染,不受图像模型影响。
  4. 生产替换:实际使用时,parse_user_request_with_llm 应调用真实的 LLM API(如 GPT-4),generate_text_free_background 应调用 Stable Diffusion / DALL-E 等图像生成 API。
  5. 字体路径:Linux 系统可安装 fonts-noto-cjk 包获取中文字体;Windows/macOS 需替换为系统字体路径。

方案 B:确定字形 + AI 自然场景融合

适用于弯曲包装、皱褶衣服、倾斜路牌、金属蚀刻等。

流程

flowchart LR A[目标文字] --> B[字体引擎渲染精确字形] B --> C[生成字形 Mask / 距离场] D[背景图] --> E[估计透视/深度/网格变形] C --> E E --> F[映射字形到目标表面] F --> G[局部编辑模型生成材质/光照/阴影] G --> H[OCR 校验] H -->|错误| I[仅修复错误区域]

专门视觉文字模型(如 AnyText、CharGen)正是向模型提供字形、位置、Mask、OCR 特征等条件,把任务从“猜字”降为“局部融合”。

方案 C:OCR 验收与闭环修复

任何仍然由生成模型参与文字生成的方案,都应该有机器验收

校验流程

flowchart TD Input[生成图片] --> Det[检测所有文字区域] Det --> OCR[OCR 识别] OCR --> Compare[与目标字符串逐字符比较] Compare -->|全部正确| Pass[通过输出] Compare -->|有错误| Locate[定位错误区域] Locate --> Fix[局部重绘或确定性覆盖] Fix --> OCR

常用指标:字符错误率(CER)

CER = (替换数 + 删除数 + 插入数) / 目标字符总数

生产要求

  • 品牌名、价格、日期、地址、型号:CER = 0
  • 所有目标文字精确匹配;
  • 不得出现额外非预期文字;
  • 中英混排用各自适合的 OCR;
  • 关键内容使用两个 OCR 交叉验证;
  • 高风险内容增加人工终审。

七、给 AI Agent 的设计建议:任务路由 + 不可变文字

一个可靠的图片生成 Agent 不应把所有需求直接拼成一个 Prompt 丢给图像模型,而应先路由,再执行

flowchart TD Req[用户需求] --> Planner[LLM 规划器] Planner --> Scene[scene_spec<br>场景与风格] Planner --> Text["text_spec[]<br>不可变字符串列表"] Scene --> BG[背景图生成器] Text --> Render[排版/字形引擎] BG --> Fusion[表面融合或局部编辑] Render --> Fusion Fusion --> Verify[OCR / 字形校验] Verify -->|正确| Out[输出] Verify -->|错误| Fix[局部重绘或排版兜底]

关键约束

  1. 文字内容不可变
    解析后立即保存原始字符串、Unicode code points、语言标签。后续 LLM 可改字号、颜色、位置,但绝不可改 content

  2. 场景 Prompt 使用占位符
    不要写“招牌上写‘新品上市’”,而写“有一块标记为 <TEXT_REGION_1> 的空白招牌”。
    真正的文字通过独立字段传给排版器。

  3. 按任务类型路由

hard_text = true
    → 确定性排版,不走纯文生图

natural_scene_text = true
    → Glyph 条件局部编辑 + OCR

stylized_text = true
    → 确定字形骨架 + 生成式风格化

long_document = true
    → HTML/SVG/PDF 排版

八、如果需要训练或微调模型,技术路线怎么走?

1. 双编码器:语义通道 + 文字通道

  • 语义通道:常规 T5/CLIP 编码画面内容、风格。
  • 文字通道:使用字符级/字节级编码字形图像编码,并输入字符位置、语言方向等信息。

Glyph-ByT5、EasyText 等近年工作都证明,字符级编码能大幅提升文字准确性。

2. 显式提供空间条件

模型不应自行猜测文字布局,应接收:

  • 每行 bounding box、基线
  • 字符位置 / 分割图 / 字形 mask
  • 字间距、行间距、方向、透视变换
  • 字体和颜色 embedding

这样模型的任务从“既想文字内容,又想文字位置”降为“在这个区域,按照给定字形做视觉融合”。

3. 高分辨率文字分支

使用双分支:背景分支用正常 latent / DiT,文字分支用高分辨率 RGB 或更低压缩率的 latent,避免小字被 VAE 压缩抹掉细节。
UM-Text 等近期方法已采用 latent 与 RGB 区域一致性监督来改善字形。

4. 改造训练数据:不要只喂“常见单词”

合成数据必须包含:

  • 常用词 + 随机字符串 + 生僻字
  • 多种字体、字号、字距、透视、弯曲、遮挡
  • 多语言混排、形近字

真实数据必须包含精确 OCR 标注(字符级位置和转录)。尤其要大量加入从未见过的随机组合,强迫模型学习“复制字符”而非“记忆单词”。

5. 文字专用损失函数

总损失可以设计为:

L_total = L_image 
          + λ1 * L_OCR 
          + λ2 * L_glyph 
          + λ3 * L_layout 
          + λ4 * L_stroke
          + λ5 * L_extra_text
  • L_OCR:OCR 识别序列损失
  • L_glyph:目标字形与生成字形的感知距离
  • L_layout:位置偏差
  • L_stroke:笔画/边缘误差
  • L_extra_text:惩罚额外文字

注意:不能只依赖一个 OCR 模型做奖励,否则生成器可能学会“机器能认、人类看着怪”的字形。


九、最终架构:矢量 + 生成式混合体

长远来看,最可靠的不是让一个纯图像模型“更努力地写字”,而是把创造性和确定性分离

flowchart LR A[LLM 输出场景图 / 设计 DSL] --> B[图像模型生成非文字视觉内容] A --> C[排版引擎生成精确 SVG / 字形] B --> D[几何模块处理透视、弯曲、遮挡] C --> D D --> E[生成模型只负责材质、光照、融合] E --> F[OCR + 字形验证]

让神经网络负责创造性,让传统渲染器负责确定性。


十、落地优先级建议

阶段 行动 覆盖场景
阶段一(无需训练) 无字背景 + SVG/Canvas 文字层 + OCR 校验 + 可编辑图层 海报、封面、Banner、电商图、教学图
阶段二(提升融合度) 增加文字区域检测、透视/深度估计、Glyph mask、局部 Inpainting 路牌、包装、衣服、墙面、招牌
阶段三(自研模型) 字符/字形双编码器 + 空间布局条件 + 高分辨率文字分支 + 联合损失 所有场景,追求极致准确性

十一、总结

只要文字的准确性属于业务要求,就不能把“纯文生图一次生成成功”作为系统保证。

错字的根源在于:

  1. 文本编码器不关心字符级信息
  2. 图像生成损失不惩罚拼写错误
  3. VAE 压缩丢失笔画细节
  4. 训练数据标注粗糙
  5. 布局与生成任务耦合过重

根治的方法

  • 确定性文字(海报/广告)→ 排版引擎,不做文生图
  • 自然场景文字(路牌/包装)→ 字形条件 + 局部编辑 + OCR 闭环
  • 艺术风格文字(霓虹/金属)→ 确定字形骨架 + AI 风格化
  • 训练/微调 → 双编码器 + 空间条件 + 高分辨率分支 + 字符级监督

下次再看到 AI 把“新年快乐”写成“新年快东”,别再盲目抽卡了——按这套方法论,从架构上解决问题,才能一劳永逸。

posted @ 2026-07-11 08:40  doiito  阅读(9)  评论(0)    收藏  举报