一、为什么需要后端接入?

前三篇博客中,我们构建的图像生成模块是“自给自足”的——直接运行 test.py 就能生成一本绘本。但这只是一个脚本,离一个可用的产品还有距离。

真正的绘本生成系统需要:

  • 接收用户的创作请求,调用 LLM 生成分页剧本,然后自动配图;

  • 多个用户同时使用时,互不干扰(不同故事的参考图、输出目录要隔离);

  • 后端能判断图像生成能力是否可用(例如 API Key 有没有配置);

  • 即使某页生成失败,也不影响其他页,系统要能优雅降级。

这一篇,我将讲解如何把前几篇的图像生成模块集成到队友的后端服务中,让它成为一个可以被 API 调用的“黑盒”。


二、整体架构:三层各司其职

接入后端的核心目标是:让上层的剧本生成服务(LLM)能够无感知地调用图像生成能力

我设计了三层架构:

┌─────────────────────────────────────────────────────
│  上层:故事生成服务(LLM)                          
│  输出:story_id + 分页剧本(文本)                    
└─────────────────────────────────────────────────────
                           ↓
┌─────────────────────────────────────────────────────
│  中间层:图像生成适配器(story_images.py)             
│  职责:将剧本转换为图像模块的标准输入格式               
└─────────────────────────────────────────────────────
                           ↓
┌─────────────────────────────────────────────────────
│  底层:图像生成执行器(image_generation.py)           
│  职责:动态导入图像模块,管理输出目录,处理并发与错误    
└─────────────────────────────────────────────────────
                           ↓
┌─────────────────────────────────────────────────────
│  图像模块核心(generator.py / Image_generator.py)  
│  职责:调用 Qwen API,一致性评分,重试机制             
└─────────────────────────────────────────────────────

每一层都有明确的边界,上层不依赖下层的实现细节。


三、核心一:标准化内部接口

在前一篇博客中,我们将图像模块封装成了 ImageGeneratorService(image_generator文件中),它有一个 generate_page 方法,接收结构化的 payload,返回一个字典。这个字典是图像模块内部与后端执行层之间的契约,而不是最终对前端暴露的 API 响应。

返回格式json示例(成功时):

{
    "success": true,
    "image_path": "/tmp/generated_images/page_01_try2_abc123.png",
    "seed_used": 12047,
    "min_score": 85,
    "details": { "hero": {"score": 85, "passed": true} }
}

失败时:

{
    "success": false,
    "error": "Qwen image call failed: 429 Too Many Requests",
    "image_path": null
}

其中 min_score 和 details 来自 Qwen-VL-Max 的评分结果。图像模块在生成每一页后,会用 Qwen-VL-Max 对比每个角色的参考图与生成图,给出 0-100 的分数。min_score 是所有角色分数的最小值(短板分数),details 记录每个角色的具体得分和判断理由。

为什么需要这个统一格式?
因为图像模块可能会因为多种原因失败(API 错误、评分不通过等),执行层需要根据 success 和 image_path 决定下一步:将临时文件移动到最终目录,或记录错误日志。这个格式让执行层不需要关心图像模块内部的复杂逻辑。


四、核心二:适配层——将 LLM 文本剧本转成标准 payload

上层 LLM 输出的是分页文本的json,例如:

{
    "title": "小红帽的冒险",
    "pages": [
        {"chapter_title": "出发", "text": "小红帽走进蘑菇森林,开心地笑着。"}
    ]
}

我们需要将它转换成图像模块能接受的 payload。这部分逻辑在 story_images.py 的 build_page_payloads_from_story 中完成。

几个关键设计点

4.1 中文提示词优先

前几篇博客使用的是英文提示词,但实际测试发现,Qwen 对中文的理解更准确,生成结果也更贴合儿童绘本风格。因此适配层直接定义了中文风格和负向提示词:

_ILLUSTRATION_STYLE = "儿童绘本插画,柔和水彩质感,色彩温暖明亮,线条干净,造型卡通可爱..."
_NEGATIVE = "模糊,低清晰度,丑陋,变形,肢体畸形,多余手指,照片级写实,恐怖,血腥,水印..."
4.2 稳定的角色 ID 和种子
  • 每个故事的主角用一个唯一的 ID:{story_id}_hero。这样不同故事的参考图不会相互覆盖。

  • 种子基准值从 story_id 派生:int(hashlib.md5(story_id.encode()).hexdigest()[:8], 16)。相同的故事总能得到相同的种子序列,便于调试和复现。

4..3 自动判断是否为动物主角

如果主角包含“狮子”、“兔子”、“鸟”等关键词,则设置 face_enabled=False。这个字段会传给 Qwen-VL-Max 评分器,告诉它“这是一个动物角色,不需要检测人脸”,评分时会基于整体外观(羽毛颜色、体型、眼睛形状等)进行判断。

4.4 动作描述提取

每页的文本可能较长,我会截取前 400 字符作为 action 字段,足够描述角色的姿态和表情。这个 action 会填入 characters 列表中,作为该页该角色的动作描述。

经过适配层后,每个页面得到一个完整的 payload,后续执行层可以直接使用。


五、核心三:执行层——动态导入、目录隔离与并发控制

image_generation.py 是整个接入的核心执行器。它承担了比较重的职责,我逐一说明。

5.1 动态导入图像模块

图像模块放在 ImageGenerator/Image_generation 目录,与后端主代码物理隔离。为了调用它,我需要把这个目录临时加入 sys.path,然后动态导入:

ig_path = str(Path(__file__).parents[3] / "ImageGenerator" / "Image_generation")
if ig_path not in sys.path:
    sys.path.insert(0, ig_path)

import config as ig_config
from generator import QwenImageGenerator
from Image_generator import ImageGeneratorService

这样做的好处是:图像模块可以独立迭代版本,后端代码不用跟着改。

5.2 输出目录隔离

不同故事的图片不能混在一起。我采用以下目录结构:

  • 数据根目录(由 settings.data_dir 决定):data/

  • 故事图片:data/images/{story_id}/page_{page_num}.png

  • 角色参考图:data/character_refs/{story_id}/{character_id}.png

删除故事时只需删除对应的子目录,干净利落。

5.3 临时覆盖图像模块的配置

图像模块的 config.py 中定义了全局输出目录 OUTPUT_DIR 和 CHARACTER_DIR。为了让每个故事写入自己的隔离目录,我需要在调用前临时修改这些配置,调用完再恢复:

prev_out = ig_config.OUTPUT_DIR
prev_char = ig_config.CHARACTER_DIR
try:
    ig_config.OUTPUT_DIR = str(story_image_dir)
    ig_config.CHARACTER_DIR = str(story_char_dir)
    # 调用图像模块...
finally:
    ig_config.OUTPUT_DIR = prev_out
    ig_config.CHARACTER_DIR = prev_char
5.4 线程锁保护

由于修改了全局配置,且动态导入不是完全线程安全的,我使用 threading.Lock 确保同一时刻只有一个故事在执行图像生成。这牺牲了一定的并发度,但换来了稳定性。

5.5 文件移动与错误降级

图像模块返回的图片路径是临时生成的(如 page_01_try2_abc123.png),我需要把它移动到标准位置 {page_num}.png。移动时要注意:

  • 如果目标已存在(例如重试后新文件覆盖旧文件),先删除旧文件;

  • 如果移动失败(比如跨设备),则降级为复制。

dest = story_image_dir / f"{page_num}.png"
if dest.exists():
    dest.unlink()
shutil.move(src, dest)   # 或 shutil.copy2

六、核心四:配置管理

后端使用 pydantic-settings 管理配置,图像相关的配置项有:

  •  dashscope_api_key:DashScope 的 API 密钥(二选一)

  • image_model_name:图像生成模型,默认 qwen-image-2.0-pro

  • image_vl_model_name:一致性评分模型,默认 qwen-vl-max

这些配置通过 .env 文件或环境变量注入

在执行层中,我从 settings 读取密钥,并设置环境变量 DASHSCOPE_API_KEY,因为图像模块内部是从环境变量读取的。


七、核心五:错误处理与降级策略

图像生成涉及远程 API 调用,容易失败(限流、网络超时、参数错误等)。我设计了多层容错:

  1. API 密钥缺失:直接跳过图像生成,日志记录,不抛异常。

  2. 模块导入失败:打印警告,跳过。

  3. 单页生成失败:捕获异常,记录日志,继续处理下一页(不中断整个故事)。

  4. 文件移动失败:降级为复制,仍能保留结果。

  5. 重试机制:图像模块内部已有重试(Qwen-VL 评分不达标自动重画),执行层不再重复。

这样,即使图像生成全面不可用,后端仍然能返回剧本文字,前端可以显示“图像生成暂时不可用”的占位图。


八、与 LLM 剧本生成模块的衔接

在 story.py(故事生成服务)中,LLM 生成分页剧本后,会调用 try_render_story_images。这个函数会:

  • 调用适配层构建 payload 列表;

  • 调用执行层 render_story_images_from_payloads 生成图片;

  • 图片写入 data/images/{story_id}/ 目录;

  • 前端通过 /api/v1/assets/images/{story_id}/{page_num}.png 访问。

关于异步的思考:图像生成耗时较长(每页约 5-8 秒,整本 40-60 秒)。如果同步执行,API 会超时。生产环境中应当将图像生成作为后台任务(如 Celery)异步执行,用户先看到剧本,稍后刷新看到配图。目前我的实现仍为同步,后续会改进。


九、总结

这一篇,我将独立的图像生成模块成功地集成到了后端服务中。核心设计思路:

  • 分层解耦:LLM 输出 → 适配层 → 执行层 → 图像核心,每层只关心自己的契约。

  • 内部接口标准化:图像模块返回统一的 {success, image_path, ...} 字典,执行层根据这个结果进行后续操作,避免了接口歧义。

  • 目录隔离:通过临时覆盖配置,每个故事的图片和参考图都放在独立的目录中。

  • 动态导入:使得图像模块可以独立演进。

  • 优雅降级:即使图像生成失败,故事文字服务仍然可用。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐