1. 为什么要把汉服造型生成做成自动化
先说一下这个项目是怎么来的。手头有一个汉服造型参考图的项目,需要大量不同配色、不同形制的搭配样图。传统做法是设计师手工收集资料、修图师一张张调,一天下来能出十几张就算效率不错。后来接触到了一个叫“造相Z-Turbo”的图像生成工具,单张出图速度确实快,但问题也来了——手工在界面上一条条敲提示词、一张张保存图片,不仅累,而且很难保证每张图的风格一致、参数可回溯。于是我就想,能不能写一套Python脚本,把“写提示词、调参、调用生成接口、保存结果、判断质量”这条流水线全部串起来,要做就做一个完整可复用的自动化测试方案。
这个项目最终跑起来之后,批量出图速度大概提升了十倍以上,而且每一张图都有完整的参数记录和质量评估日志,后续复盘、复现、调整方向都非常方便。整个方案并不复杂,核心就三件事:批量构造提示词、稳定调用生成接口、用脚本做质量初筛。适合谁参考呢?如果你也在做AI批量出图这类工作,不管是电商模特图、汉服商拍参考、还是角色设定、场景氛围图,这套思路和代码骨架都可以直接用。
这篇文章我就把从思路到落地再到踩坑的整个过程完整拆开讲,代码片段都是实际跑过的,参数也尽量写得具体。你照着改一改,就能搭出自己的批量生成与质量评估流水线。
1.1 项目落地的三个核心痛点
先说清楚为什么要做自动化测试,而不只是“用脚本批量调一下接口”。这个项目真正要解决的痛点有三个。
第一是提示词管理混乱。手工模式下,每个人写提示词的风格不一样,同一套汉服,有的人写“齐胸襦裙+上襦+披帛”,有的人写“Tang-style chest-high skirt”,同一个意思两种写法,生成结果千差万别。脚本方案可以把提示词拆成组成部分——形制、配色、材质、发型、背景、光影、镜头语言——做成可组合的样本库,跑批时按规则随机组合,既保证多样性,又能追溯每一张图用的到底是什么提示词组合。
第二是结果质量控制完全靠人肉。批量跑一百张图,里面总有几张脸崩了、比例不对、服饰穿帮。人肉盯着屏幕一张张筛,不仅眼睛累,而且标准不统一。今天觉得这张能用,明天又觉得不行。虽然最终质量的拍板还是要靠人,但初筛这步完全可以交给脚本来自动完成。我在这套方案里实现了像素级模糊检测、冗余图片去重、以及基于视觉模型的要素打分,把无效图直接扔进垃圾箱,剩下的才流到人工复核环节。
第三个痛点是参数记录缺失。手工操作时,很少有人会认真记录“这一张是用什么种子值、什么steps、什么权重出的”。等到这批图不满意想复盘,根本不知道问题出在哪个参数上。脚本方案天然解决这个问题:每次调用都会把完整的请求参数和结果哈希存进日志文件,每张图都带一份JSON格式的元数据,随时可以回溯。
1.2 自动化测试的边界划分
这里有一个很重要的认知:自动化并不是取代人的判断,而是把人不擅长做的重复劳动接过去。我在设计这个方案的时候,明确划分了哪些环节交给脚本,哪些环节必须留给人。
交给脚本的:提示词组合与随机化、调参生成、图片保存与命名、日志写入、质量初筛(清晰度、完整度、要素覆盖率)、结果汇总报表。
必须留给人做:最终风格是否可用、服饰形制是否符合历史常识、模特面部气质是否符合品牌调性。这些判断涉及审美和文化背景,机器模型目前只能做辅助打分,不能做最终决策。
这条边界想清楚了,脚本才不会变成“自动生产一堆垃圾”的工具,而是变成“自动生产一批候选素材,再由人做最终筛选”的高效流水线。
2. 脚本架构与关键设计思路
这个项目的整体架构我分成四个模块:提示词样本库、生成调度器、结果处理器、质量评估器。四个模块各干各的活,通过一份JSON配置文件串联起来。这样做的最大好处是:想换工具、换风格、换评估标准,只需要改配置和对应模块,不需要动其他部分。
2.1 跑批模块怎么分层设计
提示词样本库是最简单但最容易被人忽视的模块。它不是简单地放一堆字符串,而是要设计成“槽位填词”的结构。比如汉服提示词可以拆成这些槽位:形制、主色、辅色、面料质感、发型、发饰、背景、季节氛围、镜头语言、光照方向。每个槽位放一个列表,脚本生成提示词时从每个槽位里随机或按权重选词,拼成完整prompt。
我实测下来,用“槽位+权重”而不是“整句随机抽取”的最大优势,是可控的多样性。整句随机是抽到什么是什么,槽位组合则是“形制一定准确,但配色和背景可以变化”,既保证了主题不跑偏,又让每张图之间有足够的差异度。这对汉服这种对形制准确性要求高的场景尤其重要——你不可能让脚本生成一套“唐朝襦裙配现代旗袍开衩”的荒谬组合。
生成调度器的职责就更明确了:它负责读取配置文件里的参数(比如每批几张、并发数、尺寸、采样步数),然后循环调用Z-Turbo的生成接口。这中间还要处理限流、错误重试、超时控制。刚开始的时候我没做重试机制,跑到一半网络抖动就直接崩整个脚本,后来加了三轮指数退避重试之后基本稳定。
结果处理模块管两件事:图片落盘和元数据落盘。图片命名我采用的是“日期-批次-序号”格式,元数据则单独放一份同名JSON文件,里面包含完整请求参数、生成耗时、种子值、评估结果。这两个文件放在同一个目录下,后续不管是人工翻看还是程序统计都很方便。
质量评估器是花时间最多的部分,后面单独开一节说。
2.2 为什么选择Python而非其他方案
说实话,写自动化脚本未必只有Python一条路,Node.js也可以,甚至Shell脚本配合命令行工具都能跑。但在这个场景下Python是最合适的选择,我有几个具体的理由。
第一,Z-Turbo这类图像生成工具普遍提供HTTP接口,而Python的requests库调用HTTP接口几乎是零成本的,写起来比Node的fetch还直观,出错信息也更容易看懂。第二,后续做质量评估需要处理图像,Python的Pillow、OpenCV、以及各种视觉模型库的生态是最完整的,我评估环节里用到的东西放在其他语言里可能还得自己造轮子。第三是维护成本,项目后续要加新功能、换模型、调策略,Python的脚本改起来速度快,适合这种快速迭代的实践类项目。
3. 核心代码模块的实现细节
这里把四个模块的关键代码拆开讲。我用了Python 3.10版本,依赖库只用到了requests、Pillow、numpy和json,视觉模型评估部分单独用了一个模型文件,后面会说明。如果你还没装这些依赖,执行一下pip install requests pillow numpy就够了。
3.1 提示词样本库与随机组合实现
核心数据结构是一个字典,每个键是一个槽位,值是待选列表。为了控制权重,我用一个简单的加权随机函数来做选择。这里给你看看完整的实现。
import random import json # 提示词槽位样本库(精简版) PROMPT_SLOTS = { "形制": [ "齐胸襦裙,唐制,两层大袖衫", "明制立领长袄,搭配马面裙", "宋制褙子,百迭裙,窄袖", "魏晋风交领襦裙,广袖飘逸", ], "主色": ["黛青色", "胭脂红", "月白色", "秋香黄", "藕粉色", "竹青色"], "辅色": ["以金色绣花点缀", "以珍珠白镶边", "以同色系暗纹提花", "以银线勾勒轮廓"], "面料": ["真丝缎面质感,带有自然光泽", "棉麻质感,纹理细腻", "薄纱质感,通透轻盈"], "发型": ["高髻,装饰步摇", "双环髻,系发带", "低盘发,别银簪", "垂鬟分肖髻,素雅"], "背景": [ "古建筑庭院,青砖地面,海棠花枝影影绰绰", "竹林中,雾气弥漫,日光线柔和", "长案茶席,陶瓷茶具,暖黄色烛光", "城墙斑驳,黄昏光线拉出长影", ], "镜头": ["半身构图,视线微微偏左", "全身竖幅构图,重心居中", "近景脸部特写,浅景深"], "光照": ["柔光,阴影过渡自然", "侧逆光,轮廓边缘泛光", "阴天弥散光,低反差"], } def weighted_choice(items, weights=None): if weights is None: return random.choice(items) return random.choices(items, weights=weights, k=1)[0] def build_prompt(rng=None): rng = rng or random parts = [] parts.append(rng.choice(PROMPT_SLOTS["形制"])) parts.append("主色为" + rng.choice(PROMPT_SLOTS["主色"])) parts.append("辅以" + rng.choice(PROMPT_SLOTS["辅色"])) parts.append("面料采用" + rng.choice(PROMPT_SLOTS["面料"])) parts.append("发型:" + rng.choice(PROMPT_SLOTS["发型"])) parts.append("背景:" + rng.choice(PROMPT_SLOTS["背景"])) parts.append("镜头:" + rng.choice(PROMPT_SLOTS["镜头"])) parts.append("光线:" + rng.choice(PROMPT_SLOTS["光照"])) return ",".join(parts)要注意一点:槽位里的词尽量用中文描述,因为Z-Turbo对中文提示词的兼容性明显更好,英文反而容易出现风格漂移。另外,槽位顺序也有讲究。我测试发现把“形制”放在最前面,生成出来的结构完成度最高;如果放在中间,偶尔会出现形制被弱化的情况。这算是调参过程中的经验值,不同模型可能不一样,你可以在自己的项目里做个对比实验验证。
3.2 调用Z-Turbo生成接口的并发控制
这一步是整个流水线的核心。Z-Turbo提供一个HTTP接口,传入提示词和参数,返回图片二进制内容。为了不把自己跑进限流惩罚,我加入了并发控制和失败重试。下面这段代码是我实际使用的版本,做了简化但逻辑完全一致。
import requests import time import hashlib import os from concurrent.futures import ThreadPoolExecutor, as_completed API_URL = "https://api.example-z-turbo.example/v1/generate" API_KEY = os.environ["ZTURBO_API_KEY"] OUTPUT_DIR = "./output" def generate_one(prompt, seed=None, steps=30, width=768, height=1024, max_retries=3): seed = seed if seed is not None else random.randint(1, 2**31) params = { "prompt": prompt, "seed": seed, "steps": steps, "width": width, "height": height, } headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } last_err = None for attempt in range(max_retries): try: resp = requests.post(API_URL, json=params, headers=headers, timeout=60) resp.raise_for_status() image_bytes = resp.content # 计算哈希值,用于去重判断 img_hash = hashlib.md5(image_bytes).hexdigest() return {"ok": True, "image_bytes": image_bytes, "params": params, "hash": img_hash} except requests.RequestException as err: last_err = err # 指数退避:0.5s, 1s, 2s time.sleep(0.5 * (2 ** attempt)) return {"ok": False, "error": str(last_err), "params": params} def run_batch(prompt_list, max_workers=4): results = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [executor.submit(generate_one, p) for p in prompt_list] for fut in as_completed(futures): results.append(fut.result()) return results关于并发数,我实际测下来的经验是:如果你用的不是专门的批量套餐,并发数别超过4。超过4之后,接口一旦触发限流,单张生成时间反而会拉长,整体吞吐率不升反降。如果你用的是高优先级套餐,可以适当放宽到8,但10以上基本没必要,因为本地磁盘写入和评估模块反而是新的瓶颈。
3.3 结果落盘与元数据管理
图片生成之后,文件管理要规范。我是这样组织的:
output/ 20250415/ batch001/ 001.png 001.json 002.png 002.json manifest.json每张图的JSON元数据长这样:
{ "image_id": "20250415-batch001-001", "prompt": "齐胸襦裙,唐制...", "params": { "seed": 123456, "steps": 30, "width": 768, "height": 1024 }, "evaluation": { "blur_score": 0.02, "similarity_to_batch": 0.31, "style_match": 0.87 }, "timestamp": "2025-04-15T10:23:05" }把元数据和图片放在一起而不是单独建一张总表,是因为这样最还原手工操作时的“看图找数据”习惯。你想查这张图当时是怎么生成的,直接在文件夹旁边打开JSON就行,不需要去翻数据库。当批量大小上万之后,再考虑上SQLite集中管理。
落盘代码不复杂,但有一个细节值得注意——一定要用二进制写入模式,并且在写入前检查磁盘剩余空间。生成高分辨率图片单张可能十几兆,一批跑下来就是几个G,磁盘满了之后脚本不会报错,但图片文件会损坏,非常隐晦。
import shutil from pathlib import Path def save_result(meta, output_root): date_str = time.strftime("%Y%m%d") batch_dir = Path(output_root) / date_str / "batch001" batch_dir.mkdir(parents=True, exist_ok=True) img_path = batch_dir / f"{meta['image_id']}.png" json_path = batch_dir / f"{meta['image_id']}.json" # 写入图片 with open(img_path, "wb") as f: f.write(meta["image_bytes"]) # 写入元数据 with open(json_path, "w", encoding="utf-8") as f: json.dump(meta, f, ensure_ascii=False, indent=2) # 写入批次清单 manifest_path = batch_dir / "manifest.json" if manifest_path.exists(): with open(manifest_path, "r", encoding="utf-8") as f: manifest = json.load(f) else: manifest = [] manifest.append(meta["image_id"]) with open(manifest_path, "w", encoding="utf-8") as f: json.dump(manifest, f, ensure_ascii=False, indent=2)4. 自动化质量评估的实现
批量生成只是第一步,真正让这个项目产生价值的是质量评估环节。如果不对生成结果做筛选,脚本跑出来的一千张图里可能混着三百张废图,后面人工处理的时间成本照样居高不下。
4.1 机器能查的和机器查不了的
在动笔写评估代码之前,我先梳理了一个问题:一台没有审美能力的计算机,究竟能对图片做什么客观判断?
答案是四件事:清晰度是否达标、是否与同批其他图高度重复、画面中是否包含关键要素(比如人物、服装区域)、以及某些风格特征是否符合目标区间(比如整体色调、对比度)。至于“这款配色高级不高级”、“这个发型搭配对不对”,机器做不了判断,或者说至少目前的模型做得不够可靠。
所以我设计评估策略时采用了三层过滤:第一层硬指标过滤,机器说了算,不合格直接淘汰;第二层视觉语义过滤,用图像模型打参考分,分数过低就淘汰;第三层人工复核,只处理前两层筛选出的少部分图。这个策略的目的不是“用机器取代人”,而是“用机器把人的工作量降到最低”。
4.2 模糊检测与去重实现
先看最基础的模糊检测。生成图偶尔会出现整体虚掉的情况,原因可能是采样步数太低或者模型偶尔抽风。用拉普拉斯算子计算图像方差,数值越低表示越模糊。我设置的阈值是62,低于这个数直接弃用。
import numpy as np from PIL import Image import cv2 def is_blurry(img_path, threshold=62): img = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE) if img is None: return True laplacian_var = cv2.Laplacian(img, cv2.CV_64F).var() return laplacian_var < threshold, laplacian_var def compute_average_hash(img_path, hash_size=16): img = Image.open(img_path).convert("L").resize((hash_size, hash_size)) pixels = np.asarray(img, dtype=np.float32) avg = pixels.mean() diff = pixels > avg return diff.flatten()去重也很重要。批量生成时,有时候同一个种子值不小心重复使用了,或者模型抽风把相似的图片连出好几张。传统的感知哈希算法(pHash)计算简单、速度极快,用在这里正合适。两张图的哈希汉明距离小于某个阈值,就认为它们高度相似,保留其中一张即可。
我在方案里用了一个简单的计算汉明距离的写法:
def hamming_distance(a, b): return np.count_nonzero(a != b) def deduplicate_frames(paths, threshold=12): hashes = {} keep = [] for p in paths: h = compute_average_hash(p) dup = False for kh in hashes.keys(): if hamming_distance(h, kh) < threshold: dup = True break if not dup: hashes[h] = p keep.append(p) return keep4.3 基于视觉模型的要素打分辅助筛选
这部分稍微复杂一点。为了判断“图里有没有汉服人物”“服饰区域是否完整”,我用了CLIP这类图文匹配模型。简单来说,把图片和一段描述文字分别编码成向量,计算它们的余弦相似度,分数越高表示图与描述越匹配。
def load_clip_model(): from transformers import CLIPProcessor, CLIPModel model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32") processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32") return model, processor def evaluate_image(img_path, candidate_texts, model, processor): image = Image.open(img_path).convert("RGB") inputs = processor(text=candidate_texts, images=image, return_tensors="pt", padding=True) outputs = model(**inputs) logits_per_image = outputs.logits_per_image probs = logits_per_image.softmax(dim=1).squeeze(0).detach().numpy() return probs我用一组候选描述文本来给每张图打分,例如:
- “穿着汉服的人物全身照”
- “只有风景没有人物”
- “穿着非汉服风格服饰的人物”
- “人物面部严重变形,画面失真”
计算相似度后,如果“汉服人物”得分明显偏低,或者“无人物”得分过高,就标记为待检查。用这个方式批量跑了三百张图之后,我发现它能把大约八成明显偏题或者崩坏的图先筛掉,剩下两成是那种“模棱两可”的图,交给人工复核刚刚好。
5. 常见问题与排查技巧实录
自动化跑批不是一次就能顺利跑通的,我在这套方案上踩过的坑不少,这里挑几个典型的分享出来,做成排查速查表,你遇到同类问题可以直接对照。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 跑到一半报连接超时 | 单批并发数过高触发限流 | 查看日志中的请求时间戳 | 把并发数降到4以下,加重试退避 |
| 生成结果大量雷同 | 种子值区间太窄或未用随机种子 | 检查元数据里的seed字段分布 | 用最大范围随机种子,避免固定seed池 |
| 图片文件损坏无法打开 | 磁盘空间不足导致写入不完整 | 看看输出目录所在磁盘剩余空间 | 加磁盘空间检查逻辑,预留缓冲 |
| 输出图像整体偏色 | 提示词中色彩槽位冲突 | 查看彩色关键词是否同时出现多个主色 | 加色彩互斥规则,同一提示词只保留一个主色词 |
| 同一提示词两次生成结果差异极大 | 未传入固定种子值 | 检查API调用参数是否带seed | 强制每次调用传seed并记录 |
| 视觉模型评估极慢 | 每张图都重新加载模型权重 | 检查代码中模型是否在循环内初始化 | 模型只加载一次,评估循环复用 |
除了表格里的问题,还有几个经验想单独说。
第一是提示词长度控制。Z-Turbo对于超长提示词(超过80字)的响应质量会明显下降,有些槽位词会互相干扰。后来我把提示词总长限制在60字左右,实测生成质量最稳定。这个限制不是模型决定的,而是我在大量对比中总结出来的最优区间。
第二是检查日志不能只看报错,还要看正常请求的时间分布。我在日志里记录了每次请求的耗时,有一次发现某段时间请求耗时会突然从3秒跳到15秒,但脚本没报错。后来查出来是对端服务在当时时间段负载升高,但限流策略是丝滑降速而不是报错。针对这个情况,我在调度器里加了一个耗时监控逻辑:如果连续十次请求平均耗时超过正常基线的1.8倍,就主动放慢节奏,过几分钟再恢复。
6. 跑批节奏与实际使用心得
跑批的节奏控制也是一门学问。刚开始做自动化时,我的想法是“反正脚本已经自动化了,一次性跑一千张再说”。这种思路从我踩过的坑来看属于反面教材。
第一次直接跑一千张,跑了两个小时,完成后一看结果,全批次受同一个风格漂移问题的污染——原因是Z-Turbo的采样器在长时间连续请求后,生成结果的风格会出现缓慢漂移,比如色彩饱和度慢慢降低,构图逐渐偏移。当时没有在意这个问题,结果一千张图里有相当一部分需要返工。
后来改成“小批次-间歇检查”的模式:每批跑50张,跑完一批自动暂停,脚本调用视觉模型对整批快速扫描,如果这50张的平均分相比上一批出现超过10%的下降,就直接触发告警,不继续跑下一批,把问题排查清楚再恢复。这种模式看起来是“牺牲”了一些自动化程度,实际上总耗时反而降低了,因为避免了成批返工。
最后分享一个小技巧:保留一份“黄金参考集”。我从人工确认过的高质量图中挑了二十张代表不同风格类型的,作为每次跑批的质量基准。代码跑完一批后,自动计算新图与参考集之间的风格距离分布。如果新一批图的分布明显偏离历史基准,脚本会在评估报告里标红提示“风格漂移”。有了这个参考集,视觉模型评估的稳定性高了很多,不再完全依赖每次随机抽样的运气。
这个项目做完之后,我最大的体会是:自动化测试的价值不在于把人工操作全部替代掉,而在于把原本“做完了才知道结果”的流程,变成“每跑完一个小批次就知道结果、并且结果可以追溯”的闭环。批量生成尤其是在生成量大的时候,最怕的不是速度慢,而是跑完全部才发现方向错了。有了这套脚本和评估机制,方向有效性和参数取舍的判断不再靠感觉,而是靠数据说话。
如果你准备跑类似的项目,建议从五十张的小批量开始试,把提示词样本库、评估阈值、并发参数一个个调到位,再放大到正式批次的规模。一下子追求“大而全”,大概率会把时间和精力浪费在调试烂摊子上,得不偿失。