xiaowei-system/scripts/agnes_image.py

368 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Agnes 图片生成工具(多风格)
============================
基于 agnes-image-2.0-flash 模型,一句话出图。
内置 8 种风格预设,覆盖人物/国风/仙宫/现代/Zine。
用法:
python3 agnes_image.py "一只猫在花园" # 默认风格
python3 agnes_image.py "江南水乡" --style guofeng # 国风建筑
python3 agnes_image.py "仙女宫殿" --style xian # 仙宫天宫
python3 agnes_image.py "美术馆" --style modern # 现代建筑
python3 agnes_image.py "清冷美女" --style char # 人物角色
python3 agnes_image.py "禅意庭院" --style wabi # 日式侘寂
python3 agnes_image.py "赛博城市" --style cyber # 赛博朋克
python3 agnes_image.py "水墨山水" --style ink # 水墨风格
python3 agnes_image.py "描述" --style guofeng --size 1792x1024 # 自定义尺寸
python3 agnes_image.py "描述" --output /tmp/my.png # 指定输出
python3 agnes_image.py "描述" --push # 生成后推飞书
python3 agnes_image.py list # 列出缓存
"""
import json, sys, os, time, subprocess
from datetime import datetime
from pathlib import Path
API_BASE = "https://apihub.agnes-ai.com/v1"
MODEL = "agnes-image-2.0-flash"
FALLBACK_MODEL = "agnes-image-2.1-flash"
OUTPUT_DIR = Path.home() / ".hermes" / "image_cache"
OUTPUT_DIR.mkdir(exist_ok=True)
FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/446db983-e392-4d2c-bfb8-f9060e5df3ad"
# === 风格预设 ===
STYLES = {
"default": {
"name": "默认",
"suffix": "",
"negative": "low quality, blurry, watermark, logo, text",
},
"guofeng": {
"name": "国风建筑",
"suffix": (
", Chinese traditional architecture, flying eaves, blue-grey tiles, "
"red lanterns, stone bridge, pavilion, misty mountains, "
"cinematic lighting, environment concept art, game scene design, "
"aerial view, wide angle, dramatic sunset, warm lantern lights, "
"highly detailed, 8k, trending on artstation, epic scale"
),
"negative": (
"low quality, blurry, bad perspective, distorted architecture, "
"modern elements, readable text, watermark, logo, broken buildings"
),
},
"xian": {
"name": "仙宫天宫",
"suffix": (
", xianxia celestial palace, white marble pillars, golden ornaments, "
"sea of clouds, floating palace, ancient pine, full moon, "
"ethereal mist, celestial atmosphere, dragon carvings, "
"cinematic lighting, environment concept art, game scene design, "
"ultra detailed, 8k, epic scale, mysterious"
),
"negative": (
"low quality, blurry, bad perspective, distorted architecture, "
"modern elements, readable text, watermark, logo, "
"floating without support, wrong scale"
),
},
"modern": {
"name": "现代建筑",
"suffix": (
", modern architecture, glass facade, concrete texture, minimalist, "
"clean lines, architectural visualization, "
"golden hour, reflection, dramatic light, "
"trending on archdaily, behance, unreal engine 5 render, "
"highly detailed, 8k"
),
"negative": (
"low quality, blurry, distorted architecture, mirror error, "
"floating mass, readable text, watermark, logo, "
"messy facade, bad scale"
),
},
"char": {
"name": "人物角色",
"suffix": (
", high-end fashion lookbook, character style sheet, "
"professional studio lighting, photorealistic, "
"four views of the same character in one row, "
"close-up portrait, full-length front, three-quarter side, back view, "
"plain light warm-grey seamless studio background, "
"ultra detailed, 8k"
),
"negative": (
"low quality, blurry, extra fingers, bad anatomy, deformed face, "
"asymmetry, wrong proportions, text, watermark, logo, border, "
"cartoon, painting style, duplicate face"
),
},
"wabi": {
"name": "日式侘寂",
"suffix": (
", Japanese wabi-sabi architecture, exposed concrete, wood lattice, "
"zen garden, soft natural light, minimalist, "
"muted earth tones, imperfect beauty, "
"architectural photography, cinematic lighting, "
"highly detailed, 8k"
),
"negative": (
"low quality, blurry, bright colors, cluttered, "
"readable text, watermark, logo, modern technology"
),
},
"cyber": {
"name": "赛博朋克",
"suffix": (
", cyberpunk cityscape, neon lights, glass curtain wall, "
"futuristic, night scene, rain reflections, "
"holographic advertisements, flying vehicles, "
"cinematic lighting, volumetric fog, "
"highly detailed, 8k, trending on artstation"
),
"negative": (
"low quality, blurry, daylight, rural, "
"readable text, watermark, logo"
),
},
"ink": {
"name": "水墨风格",
"suffix": (
", Chinese ink wash painting style, sumi-e, "
"monochrome black and white, brush strokes, "
"traditional Chinese landscape, mountains and rivers, "
"minimalist composition, rice paper texture, "
"artistic, elegant, 8k"
),
"negative": (
"low quality, blurry, colorful, photographic, "
"readable text, watermark, logo, modern elements"
),
},
}
# === 尺寸预设 ===
SIZES = {
"1024x1024": "1:1 方图",
"1024x1792": "9:16 竖图",
"1792x1024": "16:9 横图",
"1344x768": "16:9 宽屏",
"768x1344": "9:16 竖屏",
}
def get_api_key():
key = os.environ.get("AGNES_API_KEY")
if key:
return key
env_file = Path.home() / ".hermes" / ".env"
if env_file.exists():
for line in env_file.read_text().splitlines():
if line.startswith("AGNES_API_KEY="):
return line.split("=", 1)[1].strip()
print("❌ 未找到 AGNES_API_KEY")
sys.exit(1)
def curl_post(url, data, timeout=120):
"""用curl发POST请求避免urllib IPv6问题"""
key = get_api_key()
cmd = ["curl", "-s", "--max-time", str(timeout), "-X", "POST", url,
"-H", f"Authorization: Bearer {key}",
"-H", "Content-Type: application/json",
"-d", json.dumps(data)]
env = dict(os.environ)
for k in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"]:
env.pop(k, None)
r = subprocess.run(cmd, capture_output=True, timeout=timeout+10, env=env)
text = r.stdout.decode("utf-8", errors="ignore")
if not text.strip():
return None
try:
return json.loads(text)
except json.JSONDecodeError:
return {"raw": text}
def curl_download(url, output_path, timeout=60):
"""用curl下载文件"""
cmd = ["curl", "-s", "--max-time", str(timeout), "-o", str(output_path), url]
env = dict(os.environ)
for k in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"]:
env.pop(k, None)
r = subprocess.run(cmd, capture_output=True, timeout=timeout+5, env=env)
return r.returncode == 0 and Path(output_path).exists()
def generate_image(prompt, style="default", size="1024x1024", output=None, push=False):
"""生成图片"""
style_cfg = STYLES.get(style, STYLES["default"])
# 拼接完整 prompt
full_prompt = prompt + style_cfg["suffix"]
print(f"🎨 Agnes 图片生成")
print(f" 模型: {MODEL}")
print(f" 风格: {style_cfg['name']}")
print(f" 尺寸: {size}")
print(f" 提示词: {prompt}")
print()
# 调用 API
data = {
"model": MODEL,
"prompt": full_prompt,
"n": 1,
"size": size,
}
print("⏳ 生成中约30-60秒...")
start = time.time()
result = curl_post(f"{API_BASE}/images/generations", data, timeout=120)
if not result:
# 尝试备用模型
print(f"⚠️ {MODEL} 超时,尝试 {FALLBACK_MODEL}...")
data["model"] = FALLBACK_MODEL
result = curl_post(f"{API_BASE}/images/generations", data, timeout=120)
if not result:
print("❌ 生成失败API 无响应)")
return None
# 解析响应
image_url = None
if "data" in result and result["data"]:
image_url = result["data"][0].get("url") or result["data"][0].get("b64_json")
elif "url" in result:
image_url = result["url"]
elif "error" in result:
print(f"❌ API 错误: {result['error']}")
return None
if not image_url:
print(f"❌ 未找到图片 URL")
print(f" 响应: {json.dumps(result, indent=2)[:500]}")
return None
# 如果是 base64保存为文件
if image_url.startswith("data:"):
import base64
header, b64data = image_url.split(",", 1)
ext = "png" if "png" in header else "jpg"
if not output:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output = str(OUTPUT_DIR / f"agnes_{style}_{timestamp}.{ext}")
Path(output).write_bytes(base64.b64decode(b64data))
else:
# 下载图片
if not output:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output = str(OUTPUT_DIR / f"agnes_{style}_{timestamp}.png")
if not curl_download(image_url, output):
print("❌ 下载失败")
return None
elapsed = time.time() - start
size_kb = Path(output).stat().st_size / 1024
print(f"\n✅ 生成完成!({elapsed:.0f}秒)")
print(f"📁 已保存: {output} ({size_kb:.0f}KB)")
print(f" 风格: {style_cfg['name']}")
# 推飞书
if push:
send_feishu(output, prompt, style_cfg["name"])
return output
def send_feishu(image_path, prompt, style_name):
"""推送到飞书"""
msg = f"""🎨 Agnes 图片生成完成
风格: {style_name}
提示词: {prompt}
文件: {image_path}
MEDIA:{image_path}"""
payload = json.dumps({"msg_type": "text", "content": {"text": msg}}).encode()
cmd = ["curl", "-s", "--max-time", "10", "-X", "POST", FEISHU_WEBHOOK,
"-H", "Content-Type: application/json", "-d", payload.decode()]
env = dict(os.environ)
for k in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"]:
env.pop(k, None)
subprocess.run(cmd, capture_output=True, timeout=12, env=env)
print("📤 已推送飞书")
def list_cache():
"""列出本地缓存"""
images = sorted(OUTPUT_DIR.glob("agnes_*.png"), reverse=True)
images += sorted(OUTPUT_DIR.glob("agnes_*.jpg"), reverse=True)
if not images:
print("📭 暂无缓存图片")
return
print(f"📁 图片缓存 ({len(images)}张):")
for img in images[:20]:
size_kb = img.stat().st_size / 1024
mtime = datetime.fromtimestamp(img.stat().st_mtime).strftime("%m-%d %H:%M")
print(f" {mtime} {size_kb:.0f}KB {img.name}")
def list_styles():
"""列出所有风格"""
print("🎨 可用风格:")
for key, cfg in STYLES.items():
print(f" --style {key:10s} {cfg['name']}")
print()
print("📐 可用尺寸:")
for size, desc in SIZES.items():
print(f" --size {size:12s} {desc}")
def main():
if len(sys.argv) < 2:
print("用法:")
print(' python3 agnes_image.py "提示词" [--style 风格] [--size 尺寸] [--output 路径] [--push]')
print(" python3 agnes_image.py list # 列出缓存")
print(" python3 agnes_image.py styles # 列出风格")
return
if sys.argv[1] == "list":
list_cache()
return
if sys.argv[1] == "styles":
list_styles()
return
# 解析参数
prompt = sys.argv[1]
style = "default"
size = "1024x1024"
output = None
push = False
args = sys.argv[2:]
i = 0
while i < len(args):
if args[i] == "--style" and i + 1 < len(args):
style = args[i + 1]; i += 2
elif args[i] == "--size" and i + 1 < len(args):
size = args[i + 1]; i += 2
elif args[i] == "--output" and i + 1 < len(args):
output = args[i + 1]; i += 2
elif args[i] == "--push":
push = True; i += 1
else:
i += 1
generate_image(prompt, style, size, output, push)
if __name__ == "__main__":
main()