diff --git a/ai-art-research/README.md b/ai-art-research/README.md new file mode 100644 index 0000000..0764d59 --- /dev/null +++ b/ai-art-research/README.md @@ -0,0 +1,211 @@ +# ai-art-research — AI 美术参考图生产工具链 + +> 为 ChildHood 项目(千禧年城郊童年模拟 · UE5.7 · 3D Lowpoly)稳定产出**统一风格美术参考图**的全套工具、素材与方法论。 +> 创建时间:2026-06-06 · 风格方向:偏《深夏时光!二十世纪的暑假》现代 3D 卡通渲染 + +--- + +## 一、它是什么 + +这套工具解决一个具体问题:**让一个非美术专业、但"知道自己想要什么感觉"的开发者,能用 AI 稳定地生成符合项目统一风格的美术参考图**(注意:是概念参考图,不是最终游戏资产)。 + +核心难点是**双参考融合**——手里有两类素材: + +| 素材类型 | 作用 | 来源 | +|---------|------|------| +| **风格参考图**("怎么画") | 锁定美术风格 | 《深夏时光》游戏截图 | +| **内容参考图**("画什么") | 提供场景内容 | 千禧年中国城郊真实照片 | + +目标输出 = **用风格参考的画法,呈现内容参考的场景**。 + +工具链由三层组成: + +1. **方法论层** — [`AI风格化生图工作流方案.md`](./AI风格化生图工作流方案.md):四步走流程 + 预置风格 Prompt 模板 +2. **工具层** — [`tools/image_gen.py`](./tools/image_gen.py):封装 API 调用的 Python CLI +3. **调研选型层** — [`调研文档/`](./调研文档/):平台与模型横评 + +--- + +## 二、目录结构 + +``` +ai-art-research/ +├── README.md # 本文件 +├── .env.example # API易 配置模板(复制为 .env 填写) +├── .env # 实际配置(含密钥,已被 .gitignore 排除) +├── AI风格化生图工作流方案.md # ⭐ 核心方法论文档(主入口) +│ +├── tools/ +│ └── image_gen.py # ⭐ Python 生图 CLI +│ +├── 参考图/ # 输入素材库(共 230 张) +│ ├── 参考图素材链接清单.md # 各素材下载链接 +│ ├── 待补充素材.md # 占位(当前为空) +│ ├── 游戏截图/ # 44 张(《深夏时光》《我的暑假》《放学后少年》) +│ ├── 真实场景/ # 125 张(千禧年中国城郊实景) +│ └── 美术风格/ # 61 张(Lowpoly/怀旧风格参考项目) +│ +├── 生成结果/ # 输出库(每张图配套 .meta.json) +│ ├── 测试/ # --test 连通性测试产出 +│ ├── 风格校准/ # --calibrate 5 物体校准产出(待补) +│ ├── 双参考融合/ # --fuse 产出(待补) +│ ├── 国内模型/ # 横评产出(待补) +│ └── 国外模型/ # 横评产出(待补) +│ +└── 调研文档/ + ├── 01-聚合站与中转站调研.md # 平台选型 + └── 02-模型测试计划.md # 模型横评计划与评分表 +``` + +> ⚠️ 本目录被项目根 `.ignore` 屏蔽,**不进入 ripgrep/Glob 搜索**(这是有意为之,避免参考图/产出图污染代码检索)。查找本目录内容需直接读文件或用 `find`。 + +--- + +## 三、核心方法论速览 + +详见 [`AI风格化生图工作流方案.md`](./AI风格化生图工作流方案.md),要点如下: + +### 1. 风格六维度框架 +用"做菜配方"的方式拆解任意一张图的风格,便于非美术人员描述: + +| 维度 | 观察点 | +|------|--------| +| 几何/形体 | 简化程度、边缘锐利还是圆润 | +| 色彩 | 主色、冷暖、饱和度 | +| 光照 | 光源方向、阴影软硬、整体亮度 | +| 纹理 | 表面光滑/粗糙、有无笔触 | +| 构图 | 俯视/平视、近景/远景 | +| 氛围 | 三个词描述感受 | + +### 2. 风格种子(已硬编码进脚本,自动注入) +英文版(作为生图 prompt 前缀): +> *"3D low-poly game art, flat cel-shaded with solid color blocks, bright warm-cool balanced saturated palette, soft diffused lighting with subtle shadows, simplified geometric forms with clean edges, low-angle third-person perspective, nostalgic warm summer atmosphere"* + +### 3. 风格方向(已确认) +- ✅ 中等几何简化、保留圆滑曲线;鲜艳明亮色彩;柔和自然阴影;干净纯色平面着色(cel-shaded) +- ❌ 不追求 PS1 的像素化贴图、vertex jitter 等复古缺陷 + +### 4. 三工具分工策略 + +| 角色 | 工具 | 用途 | +|------|------|------| +| 主力生图 | **GPT-Image-2** | 双参考融合,精确控制风格 + 内容 | +| 风格锁定 | **Nano Banana 2 (Gemini)** | 上传多张(最多 14 张)参考图做风格锚定 | +| 中文探索 | **Seedream 5 (即梦)** | 自然中文描述探索方向 | + +--- + +## 四、工具使用:`image_gen.py` + +通过 **API易 平台**(OpenAI 兼容格式)调用生图模型。每次产出同时保存图片和 `.meta.json`(记录模型、prompt、时间戳等,便于复现)。 + +### 环境准备 + +```bash +cd ai-art-research +cp .env.example .env # 填入 APIYI_API_KEY +pip install openai python-dotenv +``` + +`.env` 关键字段: +| 字段 | 说明 | 默认 | +|------|------|------| +| `APIYI_API_KEY` | API易 令牌(必填,https://api.apiyi.com/token 创建) | — | +| `APIYI_BASE_URL` | API 地址 | `https://api.apiyi.com/v1` | +| `APIYI_MODEL` | 默认模型 | `gpt-image-2` | + +### 四个命令 + +```bash +# 1) 测试连通性(生成一张简单测试图到 生成结果/测试/) +python tools/image_gen.py --test + +# 2) 文生图(自动加风格前缀;--no-style 关闭) +python tools/image_gen.py -g "千禧年中国城郊的小卖部门口" +python tools/image_gen.py -g "a red apple" --no-style + +# 3) 风格校准 —— 用同一风格种子生成 5 个固定物体(乌鸦/茶杯/老人/山湖/苹果),验证风格一致性 +python tools/image_gen.py --calibrate + +# 4) 双参考融合 —— 风格参考 + 内容参考 +python tools/image_gen.py --fuse --style 深夏时光截图.png --content 城郊照片.jpg +python tools/image_gen.py --fuse -s style.png -c photo.jpg -p "自定义 prompt" +``` + +常用可选参数:`-m/--model` 换模型、`--size`(默认 1024x1024)、`--quality`(low/medium/high,迭代用 medium、最终用 high)、`--subdir` 自定义输出子目录。 + +### 双参考融合的实现细节 +脚本对 API 调用做了两级降级: +1. 优先 `images.edit`(多图编辑模式,两张图都上传) +2. 失败则回退 `images.generate + extra_body` +3. 全部失败会提示改用网页端手动操作(API易 可能不支持多图 `images.edit` 端点) + +--- + +## 五、调研与选型结论 + +### 平台选型(详见 [01-聚合站与中转站调研.md](./调研文档/01-聚合站与中转站调研.md)) + +| 类型 | 推荐 | 理由 | +|------|------|------| +| 网页端一站式 | **Monica** | 一个平台覆盖 Claude 分析 + GPT-Image-2 + Nano Banana 2 | +| API 中转(脚本用) | **API易** ⭐ | 性价比最高,GPT-Image-2 ~0.04-0.05 元/张,token 永不过期 | + +> ⚠️ 中转站行业不稳定,**不要大额充值**;注意"注水"问题(低端模型冒充高端)。 + +### 模型横评(详见 [02-模型测试计划.md](./调研文档/02-模型测试计划.md)) +计划对 8+ 模型用**统一 prompt** 横评:国内(可灵 Kling / 即梦 Seedream / 通义万相 / 混元 / CogView)+ 国外(GPT-Image-2 / Nano Banana 2 / Claude)。 +- 四套测试:A 场景概念图、B 角色设计、C 风格一致性(同 prompt 连生 3 次)、D 风格分析 +- 五维评分:画质 / 风格匹配 / Prompt 遵循 / 中国元素 / 一致性 +- 📌 **评分表尚未填写,横评进行中** + +--- + +## 六、当前状态与待办 + +### ✅ 已就绪 +- 方法论文档与风格 Prompt 模板 v0.1 +- `image_gen.py` 四命令 CLI(`--test` 已跑通,`生成结果/测试/` 有 2026-06-10 产出) +- 素材库充实(游戏截图 44 / 真实场景 125 / 美术风格 61) +- 根目录有早期网页端手工测试产出若干(gpt-image-2 / gemini-3 系) + +### 🔲 待补 +- [ ] 完成模型横评并填写评分表 → 产出选型结论 +- [ ] 跑通 `--calibrate` 与 `--fuse`,填充对应产出目录 +- [ ] 工作流方案规划但尚未产出的文档:`风格词汇表.md`、`风格Prompt模板.md`、`选型结论.md` +- [ ] 补充 `参考图/待补充素材.md` + +--- + +## 七、与主项目的关系 + +- 本目录是 **研究/工具性质**,独立于 UE5 工程;产出物(风格结论、参考图)按需回流到主项目。 +- 工作流方案明确要求:**同步更新 [`docs/art/美术风格指南.md`](../docs/art/美术风格指南.md)**,把"类似 PS1 游戏的渲染风格"改为"类似《深夏时光》的现代 3D 卡通渲染风格"。 +- 用户记忆中已记录本工具的风格方向(见 `~/.claude/.../memory/ai-art-style-direction.md`)。 +- Git:目录纳入版本管理,仅 `.env`(密钥)被 `.gitignore` 排除。 + +--- + +## 八、快速开始(5 分钟) + +```bash +# 1. 配置密钥 +cd ai-art-research && cp .env.example .env +# 编辑 .env,填入 APIYI_API_KEY + +# 2. 安装依赖 +pip install openai python-dotenv + +# 3. 验证连通 +python tools/image_gen.py --test + +# 4. 文生图试一张(自动带风格前缀) +python tools/image_gen.py -g "千禧年中国城郊的小卖部门口" + +# 5. 双参考融合(核心玩法) +python tools/image_gen.py --fuse \ + --style "参考图/游戏截图/深夏时光01.png" \ + --content "参考图/真实场景/城郊小卖部.jpg" +``` + +不满意就用自然语言迭代 prompt:"几何再简化一点"、"色彩再温暖一些"、"下午4点的光线"。 diff --git a/ai-art-research/tools/image_gen.py b/ai-art-research/tools/image_gen.py index f9ea3d6..1899f0f 100644 --- a/ai-art-research/tools/image_gen.py +++ b/ai-art-research/tools/image_gen.py @@ -124,16 +124,30 @@ def _sanitize_filename(s: str, max_len: int = 40) -> str: return result[:max_len].rstrip("_") +# 模式 → 简短英文文件名 tag +# 避免把整段中文 prompt 塞进文件名(CDN URL 过长/编码异常会让图像分析工具解析失败) +_MODE_SLUG = { + "text_to_image": "gen", + "style_calibration": "calib", + "multi_reference_fusion": "fusion", + "dual_reference_fusion": "fusion", + "connection_test": "test", +} + + def save_result(image_bytes: bytes, metadata: dict, subdir: str = "") -> Path: - """保存图片和元数据,返回图片路径""" + """保存图片和元数据,返回图片路径。 + 文件名用「时间戳_模型_模式」短英文命名(如 260617-183020_gpt-image-2_fusion.png); + 完整 prompt 与参数存在 .meta.json 里,不丢信息。 + """ ts = datetime.now().strftime("%y%m%d-%H%M%S") model_slug = metadata.get("model", "unknown").replace("/", "-").replace(".", "-") + mode_slug = _MODE_SLUG.get(metadata.get("mode", ""), "img") out_dir = OUTPUT_BASE / subdir if subdir else OUTPUT_BASE out_dir.mkdir(parents=True, exist_ok=True) - prompt_tag = _sanitize_filename(metadata.get("prompt", "gen")) - filename = f"{ts}_{model_slug}_{prompt_tag}.png" + filename = f"{ts}_{model_slug}_{mode_slug}.png" img_path = out_dir / filename img_path.write_bytes(image_bytes) @@ -270,99 +284,100 @@ def cmd_calibrate(client, model: str): # --------------------------------------------------------------------------- # 功能:双参考融合 # --------------------------------------------------------------------------- -def cmd_fuse(client, style_path: str, content_path: str, prompt: str, +def cmd_fuse(client, style_path: str, content_paths, prompt: str, model: str, subdir: str): - """双参考图融合""" + """参考图融合(1 张风格参考 + 1~N 张内容参考)""" model = model or DEFAULT_MODEL fusion_prompt = prompt or DEFAULT_FUSION_PROMPT subdir = subdir or "双参考融合" style_file = Path(style_path).resolve() - content_file = Path(content_path).resolve() - if not style_file.exists(): print(f"❌ 风格参考图不存在: {style_file}") sys.exit(1) - if not content_file.exists(): - print(f"❌ 内容参考图不存在: {content_file}") - sys.exit(1) - print("🔗 双参考融合") + # content_paths 兼容单个 str/Path 或 list + if isinstance(content_paths, (str, Path)): + content_paths = [content_paths] + content_files = [] + for cp in content_paths: + cf = Path(cp).resolve() + if not cf.exists(): + print(f"❌ 内容参考图不存在: {cf}") + sys.exit(1) + content_files.append(cf) + + label = "双参考融合" if len(content_files) == 1 else f"多参考融合(1 风格 + {len(content_files)} 内容)" + print(f"🔗 {label}") print(f" 模型: {model}") print(f" 风格参考: {style_file.name}") - print(f" 内容参考: {content_file.name}") + for cf in content_files: + print(f" 内容参考: {cf.name}") print(f" Prompt: {fusion_prompt[:80]}...") print() - # 编码图片为 base64 - style_b64 = base64.b64encode(style_file.read_bytes()).decode() - content_b64 = base64.b64encode(content_file.read_bytes()).decode() - # 确定图片 MIME 类型 ext_map = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif"} - style_mime = ext_map.get(style_file.suffix.lower(), "image/png") - content_mime = ext_map.get(content_file.suffix.lower(), "image/png") - # 尝试方法 1: images.edit(带多图的编辑模式) + # openai 库文件上传格式:tuple = (filename, bytes, content_type) + style_tuple = (style_file.name, style_file.read_bytes(), + ext_map.get(style_file.suffix.lower(), "image/png")) + content_tuples = [ + (cf.name, cf.read_bytes(), ext_map.get(cf.suffix.lower(), "image/png")) + for cf in content_files + ] + image_list = [style_tuple] + content_tuples + image_bytes = None errors = [] - # 方法 1: images.edit - print(" 尝试方式 1: images.edit ...") + # 方式 1: images.edit 多图(风格参考 + 全部内容参考) + print(f" 尝试方式 1: images.edit ({len(image_list)} 图) 融合 ...") try: response = client.images.edit( model=model, - image=[ - {"type": "image_file", "image_file": { - "file_data": f"data:{style_mime};base64,{style_b64}", - "filename": style_file.name, - }}, - {"type": "image_file", "image_file": { - "file_data": f"data:{content_mime};base64,{content_b64}", - "filename": content_file.name, - }}, - ], + image=image_list, prompt=fusion_prompt, size="1024x1024", ) image_bytes = _extract_image_bytes(response.data[0]) print(" ✅ 方式 1 成功") except Exception as e: - errors.append(f"images.edit: {e}") + errors.append(f"images.edit 多图: {e}") print(f" ⚠️ 方式 1 失败: {e}") - # 方法 2: images.generate with b64 image in prompt (chat-style) + # 方式 2: images.edit 单图(仅第一张内容参考,风格靠文字)—— 降级 if image_bytes is None: - print(" 尝试方式 2: images.generate with extra_body ...") + print(" 尝试方式 2: images.edit 单图(风格写入 prompt)...") + single_prompt = ( + f"{STYLE_SEED_EN}\n\n" + "Re-render the uploaded photo into this 3D low-poly cel-shaded game art style. " + "Keep the photo's scene layout, building structure and objects, but re-render " + "everything in the style described above. Warm nostalgic late-afternoon lighting, " + "clean silhouette, game concept art." + ) try: - response = client.images.generate( + response = client.images.edit( model=model, - prompt=fusion_prompt, + image=content_tuples[0], + prompt=single_prompt, size="1024x1024", - quality="medium", - n=1, - response_format="b64_json", - extra_body={ - "image": [ - f"data:{style_mime};base64,{style_b64[:100]}...", # 截断防止请求过大 - ], - }, ) image_bytes = _extract_image_bytes(response.data[0]) - print(" ✅ 方式 2 成功") + print(" ✅ 方式 2 成功(单图 + 文字风格,效果弱于多图)") except Exception as e: - errors.append(f"images.generate+extra_body: {e}") + errors.append(f"images.edit 单图: {e}") print(f" ⚠️ 方式 2 失败: {e}") if image_bytes is None: print() - print("❌ 双参考融合 API 调用失败。") - print(" 这可能是因为 API易 不支持多图输入的 images.edit 端点。") + print("❌ 双参考融合 API 调用失败(双图 edit 与单图 edit 均失败)。") print() - print("💡 替代方案:") - print(" 1. 使用 API易 网页界面上传多张参考图手动操作") - print(" 2. 或将风格描述写入 prompt,仅上传内容参考图(单图模式)") + print("💡 可能原因与替代方案:") + print(" 1. API易 的该模型可能不支持 images.edit → 改用网页端手动上传两张图") + print(" 2. 换支持参考图的模型重试: -m gemini-3-1-flash-image-preview") + print(" 3. 改走纯文生图: python tools/image_gen.py -g \"<建筑内容描述>\"") print() print(" 所有错误:") for err in errors: @@ -372,9 +387,9 @@ def cmd_fuse(client, style_path: str, content_path: str, prompt: str, save_result(image_bytes, { "model": model, "prompt": fusion_prompt, - "mode": "dual_reference_fusion", + "mode": "multi_reference_fusion", "style_reference": str(style_file), - "content_reference": str(content_file), + "content_references": [str(cf) for cf in content_files], "size": "1024x1024", "timestamp": datetime.now().isoformat(), }, subdir=subdir) @@ -412,8 +427,11 @@ def main(): # 参数 parser.add_argument("-s", "--style", type=str, help="风格参考图路径(配合 --fuse)") - parser.add_argument("-c", "--content", type=str, help="内容参考图路径(配合 --fuse)") - parser.add_argument("-p", "--prompt", type=str, help="自定义 prompt") + parser.add_argument("-c", "--content", action="append", type=str, + help="内容参考图路径(配合 --fuse,可重复传多张:首张为整楼,后续为细节特写如晾衣架)") + parser.add_argument("-p", "--prompt", type=str, help="自定义 prompt(短)") + parser.add_argument("-pf", "--prompt-file", type=str, + help="从 .txt 文件读取 prompt(配合 --fuse,适合长 prompt,避免命令行转义噪音)") parser.add_argument("-m", "--model", type=str, help=f"模型 (默认: {DEFAULT_MODEL})") parser.add_argument("--no-style", action="store_true", help="文生图不加风格前缀") parser.add_argument("--size", type=str, default="1024x1024", help="图片尺寸") @@ -444,9 +462,17 @@ def main(): elif args.fuse: if not args.style or not args.content: parser.error("双参考融合需要 --style (-s) 和 --content (-c) 参数") + if args.prompt_file: + pf = Path(args.prompt_file) + if not pf.exists(): + parser.error(f"prompt 文件不存在: {pf}") + fuse_prompt = pf.read_text(encoding="utf-8") + print(f" 📄 从文件读取 prompt: {pf.name}") + else: + fuse_prompt = args.prompt cmd_fuse( client, args.style, args.content, - prompt=args.prompt, model=args.model, subdir=args.subdir, + prompt=fuse_prompt, model=args.model, subdir=args.subdir, )