diff --git a/.gitignore b/.gitignore index bd22734..1210878 100644 --- a/.gitignore +++ b/.gitignore @@ -111,6 +111,11 @@ scripts/.gitea-tokens.json __pycache__/ *.pyc +# ============================== +# AI 生图工具 — API 密钥 +# ============================== +ai-art-research/.env + # ============================== # 其他 # ============================== diff --git a/ai-art-research/.env.example b/ai-art-research/.env.example new file mode 100644 index 0000000..4c3b7f5 --- /dev/null +++ b/ai-art-research/.env.example @@ -0,0 +1,13 @@ +# API易 平台配置 +# 复制此文件为 .env 并填入你的 API Key +# cp .env.example .env + +# API 令牌(必填)— 在 https://api.apiyi.com/token 创建 +APIYI_API_KEY=sk-your-api-key-here + +# API 地址(通常不需要修改) +APIYI_BASE_URL=https://api.apiyi.com/v1 + +# 默认生图模型 +# 可选: gpt-image-2, gemini-3-1-flash-image-preview, dall-e-3, flux-pro-v1.1 +APIYI_MODEL=gpt-image-2 diff --git a/ai-art-research/tools/image_gen.py b/ai-art-research/tools/image_gen.py new file mode 100644 index 0000000..f9ea3d6 --- /dev/null +++ b/ai-art-research/tools/image_gen.py @@ -0,0 +1,454 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +ChildHood AI Art Generation Tool +通过 API易 平台调用 AI 生图模型 + +Usage: + python image_gen.py --test # 测试 API 连通性 + python image_gen.py --generate "千禧年中国城郊的小卖部门口" # 文生图(自动加风格前缀) + python image_gen.py --generate "a red apple" --no-style # 纯文生图(不加风格前缀) + python image_gen.py --calibrate # 风格校准(5 个测试物体) + python image_gen.py --fuse --style style.png --content photo.jpg # 双参考融合 + python image_gen.py --fuse --style style.png --content photo.jpg --prompt "自定义 prompt" + +Requirements: + pip install openai python-dotenv +""" + +import argparse +import base64 +import io +import json +import os +import sys +import urllib.request + +# Windows 控制台 UTF-8 支持 +if sys.platform == "win32": + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") +import os +import sys +import urllib.request +from datetime import datetime +from pathlib import Path + +# --------------------------------------------------------------------------- +# 路径配置 +# --------------------------------------------------------------------------- +SCRIPT_DIR = Path(__file__).resolve().parent # ai-art-research/tools/ +RESEARCH_DIR = SCRIPT_DIR.parent # ai-art-research/ +DOTENV_PATH = RESEARCH_DIR / ".env" +OUTPUT_BASE = RESEARCH_DIR / "生成结果" + +# --------------------------------------------------------------------------- +# 加载 .env +# --------------------------------------------------------------------------- +if DOTENV_PATH.exists(): + from dotenv import load_dotenv + load_dotenv(DOTENV_PATH) + +API_KEY = os.getenv("APIYI_API_KEY", "") +BASE_URL = os.getenv("APIYI_BASE_URL", "https://api.apiyi.com/v1") +DEFAULT_MODEL = os.getenv("APIYI_MODEL", "gpt-image-2") + +# --------------------------------------------------------------------------- +# 风格 Prompt 种子(来自工作流方案 v0.1) +# --------------------------------------------------------------------------- +STYLE_SEED_EN = ( + "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" +) + +# 风格校准测试物体(中文名, 英文 prompt) +CALIBRATION_OBJECTS = [ + ("乌鸦", "a crow standing on the ground"), + ("茶杯", "a teacup on a wooden table"), + ("老人", "an old man standing in a village street"), + ("山湖", "a mountain lake with trees"), + ("苹果", "a red apple on the ground"), +] + +# 双参考融合默认 prompt +DEFAULT_FUSION_PROMPT = """\ +图1 是我的风格参考(一张游戏截图)。图2 是我的内容参考(一张现实照片)。 +请生成一张新图片,用图1的美术风格来呈现图2中的场景内容。 + +风格要求: +- 高度简化的低多边形几何形体,干净利落的边缘 +- 平面色块着色,不用渐变或写实纹理 +- 鲜艳明亮的暖冷平衡色调(暖黄、天蓝、翠绿为主) +- 柔和散射光,阴影淡而柔和 +- 微低角度第三人称视角 +- 温暖怀旧的夏日午后氛围 + +内容要求: +- 保留图2中的建筑类型、布局、空间关系 +- 但把所有建筑元素替换为千禧年中国城郊的风格 +- 色调保持温暖怀旧""" + + +# --------------------------------------------------------------------------- +# 工具函数 +# --------------------------------------------------------------------------- +def get_client() -> "OpenAI": + """创建 OpenAI 客户端""" + if not API_KEY: + print("❌ 未找到 API Key") + print(f" 请在 {DOTENV_PATH} 中设置: APIYI_API_KEY=sk-xxx") + sys.exit(1) + from openai import OpenAI + return OpenAI(api_key=API_KEY, base_url=BASE_URL) + + +def _extract_image_bytes(img_obj) -> bytes: + """从 API 响应中提取图片字节数据""" + # 优先 base64 + b64 = getattr(img_obj, "b64_json", None) + if b64: + return base64.b64decode(b64) + # 其次 URL + url = getattr(img_obj, "url", None) + if url: + print(" ⬇️ 正在下载图片...") + return urllib.request.urlopen(url).read() + raise ValueError("响应中未找到图片数据(无 b64_json 也无 url)") + + +def _sanitize_filename(s: str, max_len: int = 40) -> str: + """清理文件名中的非法字符""" + result = "".join(c if c.isalnum() or c in "._-" else "_" for c in s) + return result[:max_len].rstrip("_") + + +def save_result(image_bytes: bytes, metadata: dict, subdir: str = "") -> Path: + """保存图片和元数据,返回图片路径""" + ts = datetime.now().strftime("%y%m%d-%H%M%S") + model_slug = metadata.get("model", "unknown").replace("/", "-").replace(".", "-") + + 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" + + img_path = out_dir / filename + img_path.write_bytes(image_bytes) + + meta_path = img_path.with_suffix(".meta.json") + meta_path.write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8") + + print(f"✅ 已保存: {img_path.name}") + return img_path + + +# --------------------------------------------------------------------------- +# 功能:测试连通性 +# --------------------------------------------------------------------------- +def cmd_test(client): + """测试 API 易连通性,生成一张简单测试图""" + print("🔗 测试 API易 连通性...") + print(f" Base URL: {BASE_URL}") + print(f" Model: {DEFAULT_MODEL}") + print(f" API Key: {API_KEY[:8]}...{API_KEY[-4:]}") + print() + + prompt = f"{STYLE_SEED_EN}, a simple red cube on a white background, test image" + print(f" Prompt: {prompt[:80]}...") + + response = client.images.generate( + model=DEFAULT_MODEL, + prompt=prompt, + size="1024x1024", + quality="low", + n=1, + ) + + image_bytes = _extract_image_bytes(response.data[0]) + save_result(image_bytes, { + "model": DEFAULT_MODEL, + "prompt": prompt, + "mode": "connection_test", + "size": "1024x1024", + "quality": "low", + "timestamp": datetime.now().isoformat(), + }, subdir="测试") + + print() + print("✅ API 连通性测试通过!") + + +# --------------------------------------------------------------------------- +# 功能:文生图 +# --------------------------------------------------------------------------- +def cmd_generate(client, prompt: str, use_style: bool, size: str, quality: str, + model: str, subdir: str): + """文生图""" + model = model or DEFAULT_MODEL + full_prompt = f"{STYLE_SEED_EN}, {prompt}" if use_style else prompt + + print(f"🎨 文生图") + print(f" 模型: {model}") + print(f" 尺寸: {size} 质量: {quality}") + print(f" 风格前缀: {'是' if use_style else '否'}") + print(f" Prompt: {full_prompt[:100]}{'...' if len(full_prompt) > 100 else ''}") + print() + + response = client.images.generate( + model=model, + prompt=full_prompt, + size=size, + quality=quality, + n=1, + ) + + image_bytes = _extract_image_bytes(response.data[0]) + save_result(image_bytes, { + "model": model, + "prompt": full_prompt, + "mode": "text_to_image", + "style_prefix_used": use_style, + "size": size, + "quality": quality, + "timestamp": datetime.now().isoformat(), + }, subdir=subdir) + + +# --------------------------------------------------------------------------- +# 功能:风格校准 +# --------------------------------------------------------------------------- +def cmd_calibrate(client, model: str): + """风格校准 — 5 个测试物体""" + model = model or DEFAULT_MODEL + subdir = "风格校准" + + print("🎯 开始风格校准测试") + print(f" 模型: {model}") + print(f" 风格种子: {STYLE_SEED_EN[:60]}...") + print(f" 测试物体: {', '.join(zh for zh, _ in CALIBRATION_OBJECTS)}") + print() + + success = 0 + for zh_name, en_prompt in CALIBRATION_OBJECTS: + full_prompt = f"{STYLE_SEED_EN}, {en_prompt}" + print(f" 🎨 生成: {zh_name} ...") + + try: + response = client.images.generate( + model=model, + prompt=full_prompt, + size="1024x1024", + quality="medium", + n=1, + response_format="b64_json", + ) + image_bytes = _extract_image_bytes(response.data[0]) + save_result(image_bytes, { + "model": model, + "prompt": full_prompt, + "mode": "style_calibration", + "object_zh": zh_name, + "object_en": en_prompt, + "style_seed": STYLE_SEED_EN, + "timestamp": datetime.now().isoformat(), + }, subdir=subdir) + success += 1 + except Exception as e: + print(f" ❌ 失败: {e}") + print() + + print(f"🎉 风格校准完成!成功 {success}/{len(CALIBRATION_OBJECTS)} 张") + print(f" 输出目录: {OUTPUT_BASE / subdir}") + print() + print("📋 验证方法:打开上述目录,检查 5 张图是否视觉风格统一。") + print(" 如果风格不一致,需要调整 STYLE_SEED_EN 后重新运行。") + + +# --------------------------------------------------------------------------- +# 功能:双参考融合 +# --------------------------------------------------------------------------- +def cmd_fuse(client, style_path: str, content_path: str, prompt: str, + model: str, subdir: str): + """双参考图融合""" + 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("🔗 双参考融合") + print(f" 模型: {model}") + print(f" 风格参考: {style_file.name}") + print(f" 内容参考: {content_file.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(带多图的编辑模式) + image_bytes = None + errors = [] + + # 方法 1: images.edit + print(" 尝试方式 1: images.edit ...") + 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, + }}, + ], + 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}") + print(f" ⚠️ 方式 1 失败: {e}") + + # 方法 2: images.generate with b64 image in prompt (chat-style) + if image_bytes is None: + print(" 尝试方式 2: images.generate with extra_body ...") + try: + response = client.images.generate( + model=model, + prompt=fusion_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 成功") + except Exception as e: + errors.append(f"images.generate+extra_body: {e}") + print(f" ⚠️ 方式 2 失败: {e}") + + if image_bytes is None: + print() + print("❌ 双参考融合 API 调用失败。") + print(" 这可能是因为 API易 不支持多图输入的 images.edit 端点。") + print() + print("💡 替代方案:") + print(" 1. 使用 API易 网页界面上传多张参考图手动操作") + print(" 2. 或将风格描述写入 prompt,仅上传内容参考图(单图模式)") + print() + print(" 所有错误:") + for err in errors: + print(f" - {err}") + sys.exit(1) + + save_result(image_bytes, { + "model": model, + "prompt": fusion_prompt, + "mode": "dual_reference_fusion", + "style_reference": str(style_file), + "content_reference": str(content_file), + "size": "1024x1024", + "timestamp": datetime.now().isoformat(), + }, subdir=subdir) + + +# --------------------------------------------------------------------------- +# CLI 入口 +# --------------------------------------------------------------------------- +def main(): + parser = argparse.ArgumentParser( + description="ChildHood AI Art Generation Tool — 通过 API易 生图", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""\ +示例: + python image_gen.py --test # 测试连通性 + python image_gen.py -g "千禧年中国城郊的小卖部门口" # 文生图 + python image_gen.py -g "a red apple" --no-style # 不加风格前缀 + python image_gen.py --calibrate # 风格校准 + python image_gen.py --fuse --style ref.png --content photo.jpg # 双参考融合 + python image_gen.py --fuse -s style.png -c photo.jpg -p "自定义" # 自定义 prompt + +环境变量 (ai-art-research/.env): + APIYI_API_KEY API易 令牌 (必填) + APIYI_BASE_URL API 地址 (默认 https://api.apiyi.com/v1) + APIYI_MODEL 默认模型 (默认 gpt-image-2) +""", + ) + + # 互斥的操作模式 + mode = parser.add_mutually_exclusive_group() + mode.add_argument("--test", action="store_true", help="测试 API 连通性") + mode.add_argument("-g", "--generate", type=str, metavar="PROMPT", help="文生图") + mode.add_argument("--calibrate", action="store_true", help="风格校准(5 个测试物体)") + mode.add_argument("--fuse", action="store_true", help="双参考图融合模式") + + # 参数 + 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("-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="图片尺寸") + parser.add_argument("--quality", type=str, default="medium", + choices=["low", "medium", "high"], help="质量") + parser.add_argument("--subdir", type=str, default="", help="输出子目录名") + + args = parser.parse_args() + + # 无参数时显示帮助 + if len(sys.argv) == 1: + parser.print_help() + return + + client = get_client() + + if args.test: + cmd_test(client) + elif args.generate: + cmd_generate( + client, args.generate, + use_style=not args.no_style, + size=args.size, quality=args.quality, + model=args.model, subdir=args.subdir or "文生图", + ) + elif args.calibrate: + cmd_calibrate(client, model=args.model) + elif args.fuse: + if not args.style or not args.content: + parser.error("双参考融合需要 --style (-s) 和 --content (-c) 参数") + cmd_fuse( + client, args.style, args.content, + prompt=args.prompt, model=args.model, subdir=args.subdir, + ) + + +if __name__ == "__main__": + main() diff --git a/ai-art-research/生成结果/测试/260610-003540_gpt-image-2_3D_low-poly_game_art__flat_cel-shaded_wi.meta.json b/ai-art-research/生成结果/测试/260610-003540_gpt-image-2_3D_low-poly_game_art__flat_cel-shaded_wi.meta.json new file mode 100644 index 0000000..c49841a --- /dev/null +++ b/ai-art-research/生成结果/测试/260610-003540_gpt-image-2_3D_low-poly_game_art__flat_cel-shaded_wi.meta.json @@ -0,0 +1,8 @@ +{ + "model": "gpt-image-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, a simple red cube on a white background, test image", + "mode": "connection_test", + "size": "1024x1024", + "quality": "low", + "timestamp": "2026-06-10T00:35:40.485529" +} \ No newline at end of file diff --git a/ai-art-research/生成结果/测试/260610-003540_gpt-image-2_3D_low-poly_game_art__flat_cel-shaded_wi.png b/ai-art-research/生成结果/测试/260610-003540_gpt-image-2_3D_low-poly_game_art__flat_cel-shaded_wi.png new file mode 100644 index 0000000..4b08b8a --- /dev/null +++ b/ai-art-research/生成结果/测试/260610-003540_gpt-image-2_3D_low-poly_game_art__flat_cel-shaded_wi.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:697c18cb37e02d7f3e481e65c7bdb20a15bba83c63f98b8038a89a83911107dd +size 721711