368 lines
13 KiB
Python
368 lines
13 KiB
Python
#!/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()
|