
Multi Agent Image
- 172 installs
- 431 repo stars
- Updated July 22, 2026
- kangarooking/kangarooking-skills
Coordinate multiple Claude agents to plan, generate, critique, and refine images—split roles across prompt, render, and QA for higher-quality visual output.
About
multi-agent-image implements a kangarooking-skills pattern where several Claude agents collaborate on image work—planning prompts, producing renders, and reviewing output. It suits builders who want agentic quality control and specialization instead of a single generative call for content and design assets.
- Multi-agent roles for image creation
- Prompt, generate, and review split
- Higher-quality iterative visuals
- kangarooking/kangarooking-skills pattern
- Scales beyond single-shot image_gen
Multi Agent Image by the numbers
- 172 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #658 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/kangarooking/kangarooking-skills --skill multi-agent-imageAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 172 |
|---|---|
| repo stars | ★ 431 |
| Last updated | July 22, 2026 |
| Repository | kangarooking/kangarooking-skills ↗ |
What it does
Coordinate multiple Claude agents to plan, generate, critique, and refine images—split roles across prompt, render, and QA for higher-quality visual output.
Files
Multi-Agent Image
multi-agent-image is a standalone Hermes skill for image generation workflows.
It is designed for cases where a simple one-line prompt is not enough. Instead of sending raw user input directly to an image model, this skill:
1. analyzes the request, 2. compiles it into a design-aware prompt, 3. generates through gpt-image-2, 4. archives the result, 5. and optionally reuses successful outputs as future style references.
This skill is independent at runtime. The design compiler is built into this repository and does not require an external skill.
When to Use
Use this skill when the user wants one or more of the following:
- Design-oriented poster generation
- Product images or ad visuals
- PPT cover visuals or chapter art
- Infographic-like or teaching/demo visuals
- Style reference reuse from prior generations
- Interactive “show examples first, then generate” flow
- Batch generation for multiple directions or aspect ratios
- Series generation where multiple images should share one visual language
Do not use this skill for:
- pixel-accurate UI recreation
- editable charts
- exact typography output inside the image
- tasks that require vector, HTML, or PPT-native assets rather than raster images
Architecture
User Request
↓
[Prompt Engineer]
↓
[Style Scout]
↓
[Internal Design Compiler]
↓
[GPT-Image-2 Generation]
↓
[QA + Archive]
↓
[Case Library]Optional layers on top of the main path:
- Interactive reference selection
- Batch generation
- Series generation
Setup
1. Deploy the skill
The skill source lives in:
~/.hermes/skills/multi-agent-image/Install runtime files into the working directory:
python3 ~/.hermes/skills/multi-agent-image/scripts/install.pyThis prepares:
~/.hermes/agents/multi-agent-image/output/~/.hermes/agents/multi-agent-image/case_library/- agent role folders and memory files
- local runtime scripts copied from the skill
2. Install Python dependencies
pip install openai requests3. Set API key
export OPENAI_API_KEY="sk-..."This key is used with the apimart-compatible GPT-Image-2 endpoints in this skill.
Core Components
scripts/design_compiler.py
Internal prompt compiler.
Responsibilities:
- detect task type
- choose defaults for aspect and quality
- build
design_reasoning - compress it into
compiled_brief - produce the final generation prompt
This is the core logic that makes the skill independent.
scripts/design_image.py
CLI entrypoint for the internal compiler.
Use it when you want:
- prompt-only output
- a local design compilation test
- direct generation without the full multi-agent workflow
Example:
cd ~/.hermes/agents/multi-agent-image
python3 design_image.py \
--task poster \
--brief "AI训练营招生海报,强调速度、增长、实战" \
--direction balanced \
--aspect 3:4 \
--prompt-onlyIt prints:
design_reasoningcompiled_briefpromptsettings
scripts/orchestrator_v2.py
Main workflow entrypoint.
Responsibilities:
- run prompt analysis
- choose task and generation parameters
- optionally select a reference from the case library
- call the internal compiler
- call GPT-Image-2
- archive outputs
- auto-save successful results into the case library
scripts/gpt_image2_generator.py
Low-level GPT-Image-2 client.
Responsibilities:
- submit async generation tasks
- poll task status
- download image results
Use this when you want direct API access without the full workflow.
scripts/case_library.py
Persistent library of past generations.
Responsibilities:
- save outputs by task type
- store metadata and rating
- search by brief, prompt, or tags
- return image paths for reuse as references
scripts/case_selector.py
Interactive helper for Hermes dialogue flows.
Responsibilities:
- render user-facing selection text
- parse replies like
1,n,case_001, or搜索蓝色
scripts/interactive_run.py
Two-phase dialogue wrapper.
Use it when the workflow needs to ask the user before generating.
scripts/batch_generator_v2.py
Batch generation entrypoint.
Supports:
- same brief, multiple directions
- same brief, multiple aspect ratios
- multiple briefs in one run
scripts/series_generator.py
Style-consistent series generator.
Workflow:
1. generate a master image 2. extract style signals from its compiled brief 3. generate child images that follow the same visual system
templates/linear_batch.py
Editable template for resumable sequential runs.
Useful when you want:
- explicit scene lists
- filesystem-based progress monitoring
- style propagation from the first generated image
Internal Design Compiler
The internal compiler produces three layers:
1. design_reasoning
This captures design intent before generation.
Typical fields:
taskcommunication_goalaudiencechannelvisual_systemhierarchy_strategysafe_zone_strategylighting_strategypalette_strategyanti_filler_rulesanti_slop_rules
2. compiled_brief
This is a compressed design brief for generation.
It includes:
- what the image is for
- what should dominate visually
- what space should remain available
- what to avoid
3. prompt
Final model-facing prompt used for GPT-Image-2.
The prompt is generated from design logic, not just from a list of style keywords.
Supported Tasks
The built-in compiler understands these task classes:
posterproductpptinfographicteachingauto
Default aspect assumptions:
poster→3:4product→1:1ppt→16:9infographic→4:3teaching→16:9
Direction modes:
conservativebalancedbold
Quality modes:
draftfinalpremium
Current generation channel:
gpt-image-2
Usage
Quick start
cd ~/.hermes/agents/multi-agent-image
python3 quick_start.py "AI训练营招生海报,强调速度、增长、实战"Prompt-only compilation
cd ~/.hermes/agents/multi-agent-image
python3 design_image.py \
--task product \
--brief "高端陶瓷咖啡杯电商首图,温暖晨光,突出釉面质感" \
--prompt-onlyFull orchestrated generation
from orchestrator_v2 import run
run("AI训练营招生海报,强调速度增长实战")Force task and visual settings
from orchestrator_v2 import run
run(
"高端咖啡杯商品图",
task="product",
direction="balanced",
aspect="1:1",
quality="final",
use_reference=False,
)Interactive Workflow
Use the two-phase pattern when Hermes should ask before generating.
Phase 1: prepare text for the user
from interactive_run import prepare
text = prepare("帮我做张 AI 训练营海报", task="poster")
print(text)Phase 2: execute after the user chooses
from interactive_run import execute
result = execute("帮我做张 AI 训练营海报", user_choice="1", task="poster")Supported reply patterns:
1,2,3nycase_001搜索蓝色
Batch Generation
Same brief, multiple directions
from batch_generator_v2 import batch_styles
batch_styles("AI训练营海报", task="poster")Same brief, multiple aspect ratios
from batch_generator_v2 import batch_aspects
batch_aspects("AI训练营海报", task="poster", aspects=["1:1", "16:9", "9:16"])Multiple briefs
from batch_generator_v2 import batch_briefs
batch_briefs(["海报A", "海报B", "海报C"], task="poster")Series Generation
Use this when several outputs should feel like the same campaign or product family.
from series_generator import SeriesGenerator
sg = SeriesGenerator()
sg.create_series(
master_brief="AI训练营系列视觉,科技蓝,专业商务感",
items=[
{"name": "主海报", "brief": "AI训练营招生主海报", "aspect": "3:4"},
{"name": "Banner", "brief": "官网 Banner", "aspect": "16:9"},
{"name": "朋友圈", "brief": "朋友圈推广方形图", "aspect": "1:1"},
],
task="poster",
direction="balanced",
)Case Library
Case library directory:
~/.hermes/agents/multi-agent-image/case_library/Output directory:
~/.hermes/agents/multi-agent-image/output/Typical case structure:
case_library/
├── poster/
│ └── case_001_example/
│ ├── image.png
│ └── metadata.jsonTypical metadata fields:
case_idtaskbriefpromptparamstagsrating
Validation Guidance
Before generating at scale, test prompt quality first:
python3 design_image.py \
--task poster \
--brief "AI训练营招生海报,强调速度、增长、实战" \
--direction balanced \
--aspect 3:4 \
--prompt-onlyWhat to check:
- Does
design_reasoningstate a clear communication goal? - Is there an explicit safe zone?
- Is hierarchy obvious?
- Do
anti_slop_rulesremove HUD overlays, fog, and generic clutter? - Does the prompt describe a single strong visual idea rather than a pile of elements?
Current Limits
- Current image provider is centered on
gpt-image-2 - QA scoring is intentionally lightweight
- Series generation is heavier than one-off generation
- The skill is optimized for raster outputs, not editable assets
- Some reference documents remain longer than necessary, but the main runtime path is consistent
Version History
v1.0.0Initial multi-agent workflow for GPT-Image-2 generationv2.0.0Added case library, interactive reference selection, and image-to-image style reusev2.1.0Added stronger download retry logic, batch workflows, and series generationv2.2.0Packaged as a reusable Hermes skill with install script and runtime layoutv3.0.0Internalized the design compiler and removed external runtime dependency
Multi-Agent Image
multi-agent-image 是一个面向 Hermes 的图片生成工作流 skill。
它的重点不是“再做一个单次生图脚本”,而是把图片生成这件事拆成更稳定的流程能力:
- 用多阶段工作流提升 prompt 和参数决策质量
- 用案例库沉淀历史结果,支持风格参考复用
- 支持两阶段交互,让用户先看参考再决定是否生成
- 支持批量生成和系列套图,适合内容矩阵和统一视觉产出
- 使用 apimart 的
gpt-image-2作为当前图片生成通道
它适合什么场景
- 活动海报、课程海报、社群传播图
- 商品图、广告图、电商素材
- PPT 配图、章节封面、视觉方向稿
- 信息图风格视觉、教学演示图
- 系列内容图,例如海报 + Banner + 方图 + 详情页头图
如果你要的是“单张图快速生成”,它也能做;如果你要的是“围绕一组图建立持续复用的工作流”,这才是它更有价值的地方。
核心能力
1. 多阶段生成
默认流程大致是:
1. 分析用户需求 2. 选择任务类型、方向、比例和质量 3. 调用设计编译步骤生成更强的最终 prompt 4. 提交到 gpt-image-2 5. 轮询任务、下载结果 6. 做基础质量评分 7. 自动归档并写入案例库
也就是说,这个 skill 的重点不是把一句 prompt 直接发给模型,而是把“分析、编排、沉淀、复用”一起做掉。
2. 案例库
生成完成后,图片和元数据会自动保存到案例库中,后续可以:
- 按任务类型查看案例
- 按关键词搜索案例
- 用历史高分案例作为风格参考
- 在交互式流程中先展示给用户选择
适合反复做同一类视觉任务的人,比如课程海报、商品主图、内容封面、品牌社媒图。
3. 交互式两阶段生成
这个 skill 支持先展示案例,再让用户决定:
1. prepare() 负责生成选择文本 2. 用户回复编号或跳过 3. execute() 再执行真正的生成
这个模式特别适合 Hermes 这类对话环境,因为执行代码本身不能中途暂停等待用户输入。
4. 批量与系列套图
除了单张生成,还支持:
- 同一需求多风格 A/B 测试
- 同一需求多比例适配
- 多 brief 批量生成
- 先生成母图,再生成风格统一的系列子图
如果你要做一整套内容素材,而不是一张孤立图片,这部分会很有用。
目录结构
multi-agent-image/
├── SKILL.md
├── README.md
├── scripts/
│ ├── design_compiler.py
│ ├── design_image.py
│ ├── install.py
│ ├── quick_start.py
│ ├── orchestrator_v2.py
│ ├── interactive_run.py
│ ├── case_library.py
│ ├── case_selector.py
│ ├── batch_generator_v2.py
│ ├── series_generator.py
│ └── gpt_image2_generator.py
├── references/
│ └── agents/
└── templates/
└── linear_batch.py独立性
当前版本已经把设计编译逻辑内置进仓库。
也就是说:
- 图片生成编排在本 skill 内
- 设计编译在本 skill 内
gpt-image-2调用在本 skill 内- 案例库和交互逻辑在本 skill 内
从仓库组织和运行实现两个层面看,它现在都是独立的。
环境要求
- 已配置
OPENAI_API_KEY - 已安装 Python 依赖:
openai、requests - Hermes 运行环境可用
安装
python3 ~/.hermes/skills/multi-agent-image/scripts/install.py安装脚本会做这些事情:
- 把运行脚本复制到
~/.hermes/agents/multi-agent-image/ - 创建案例库、输出目录和各 agent 子目录
配置 API Key
export OPENAI_API_KEY="sk-..."这里使用的是 apimart 兼容接口下的 gpt-image-2 通道。
快速开始
方式 1:快速单次生成
cd ~/.hermes/agents/multi-agent-image
python3 quick_start.py "AI训练营招生海报,强调速度、增长、实战"方式 1.5:只跑设计编译,不直接生成
cd ~/.hermes/agents/multi-agent-image
python3 design_image.py --brief "AI训练营招生海报,强调速度、增长、实战" --prompt-only方式 2:完整工作流
from orchestrator_v2 import run
run("AI训练营招生海报,强调速度增长实战")方式 3:不参考历史案例,直接生成
from orchestrator_v2 import run
run("赛博朋克猫咪黑客", use_reference=False)交互式用法
第一步:展示可选案例
from interactive_run import prepare
text = prepare("帮我做张 AI 训练营海报", task="poster")
print(text)第二步:根据用户选择执行
from interactive_run import execute
result = execute("帮我做张 AI 训练营海报", user_choice="1", task="poster")支持的选择包括:
1、2、3这样的案例编号n表示不参考案例,直接生成y表示确认继续case_001这样的案例 ID
批量生成
同需求多风格
from batch_generator_v2 import batch_styles
batch_styles("AI训练营海报", task="poster")同需求多比例
from batch_generator_v2 import batch_aspects
batch_aspects("AI训练营海报", task="poster", aspects=["1:1", "16:9", "9:16"])多需求批量
from batch_generator_v2 import batch_briefs
batch_briefs(["海报A", "海报B", "海报C"], task="poster")系列套图
from series_generator import SeriesGenerator
sg = SeriesGenerator()
sg.create_series(
master_brief="AI训练营系列视觉,科技蓝,专业商务感",
items=[
{"name": "主海报", "brief": "AI训练营招生主海报", "aspect": "3:4"},
{"name": "Banner", "brief": "官网 Banner", "aspect": "16:9"},
{"name": "朋友圈", "brief": "朋友圈推广方形图", "aspect": "1:1"},
],
task="poster",
direction="balanced"
)这个模式适合统一一组图的视觉语言,而不是每张图都重新摸索。
输出内容
运行完成后,一般会得到:
- 生成图片文件
- 归档 JSON
- 案例库记录
- 批量或系列任务的汇总结果
默认输出目录通常在:
~/.hermes/agents/multi-agent-image/output/案例库目录通常在:
~/.hermes/agents/multi-agent-image/case_library/当前限制
- 目前图片通道主要围绕
gpt-image-2 - 系列生成仍然比较重,每张图都不是“毫秒级”工作流
- 仓库里仍有一部分历史说明文档尚未完全重写,但主运行路径已经独立
后续可以继续做什么
如果后面继续增强,比较自然的方向是:
- 增加更多图片 provider,而不是只围绕
gpt-image-2 - 把
quick_start.py也切到统一设计编译器 - 进一步收敛长文档,把历史兼容说明清理掉
完整使用细节和长文档说明见 SKILL.md。
图片生成引擎 Agent (Image Generator) - 内置设计编译版
身份
你是 Image Generator Agent,负责调用仓库内置的设计编译器生成高质量图片。
职责
1. 接收上游 Agent 提供的:用户 brief、task 类型、direction、aspect 比例 2. 调用内置设计编译层生成高质量 Prompt 3. 调用 GPT-Image-2 API(apimart.ai)执行图片生成 4. 下载并保存图片 5. 返回完整的生成结果
工作流
Step 1: 设计编译(调用 design_image.py)
使用命令:
python /root/.hermes/agents/multi-agent-image/design_image.py \
--task {task} \
--brief "{brief}" \
--direction {direction} \
--aspect {aspect} \
--quality {quality} \
--prompt-only这会返回:
- design_reasoning(设计推理)
- compiled_brief(编译简报)
- prompt(最终高质量 Prompt)
- settings(配置信息)
Step 2: 图片生成
直接调用 apimart.ai API:
import requests
requests.post("https://api.apimart.ai/v1/images/generations", json={
"model": "gpt-image-2",
"prompt": "编译后的Prompt",
"size": "3:4"
})Step 3: 轮询下载
- 提交后等待 10-20 秒
- 轮询查询任务状态
- 完成后下载图片
输入格式(从 workflow.json 读取)
{
"user_brief": "用户原始需求",
"task": "poster|product|ppt|infographic|teaching",
"direction": "conservative|balanced|bold",
"aspect": "1:1|16:9|9:16|3:4|4:3",
"quality": "draft|final|premium"
}输出格式
{
"status": "success|failed",
"filepath": "图片本地路径",
"url": "临时下载链接",
"task_id": "apimart任务ID",
"design_reasoning": "设计推理JSON",
"compiled_brief": "编译简报JSON",
"final_prompt": "最终高质量Prompt",
"generation_info": {
"model": "gpt-image-2",
"size": "3:4",
"actual_time": 45
}
}参数映射规则
| 上游输入 | design_image.py 参数 | GPT-Image-2 参数 |
|---|---|---|
| brief | --brief | prompt |
| task | --task | - |
| direction | --direction | - |
| aspect | --aspect | size |
| quality | --quality | - |
错误处理
- 设计编译失败 → 返回 failed,说明错误原因
- API 提交失败 → 重试 1 次
- 生成超时(>120s)→ 返回 failed
- 下载失败 → 返回 URL 让用户手动下载
记忆管理
- 记录每次内置设计编译的质量评分
- 跟踪哪些 task+direction+aspect 组合效果最好
- 记录常见错误和解决方案
元数据管理 Agent (Metadata Manager)
身份
你是 Metadata Manager Agent,负责归档所有生成记录、管理资产库、生成可追溯的元数据。
职责
1. 收集全流程的生成参数和配置 2. 生成标准化的元数据文件(JSON 格式) 3. 整理输出文件(图片、日志)到归档目录 4. 生成可读的生成报告 5. 维护生成历史索引,方便检索
归档结构
archives/
├── 2024-01-15/
│ ├── 001_cyberpunk_cat/
│ │ ├── final.png
│ │ ├── metadata.json
│ │ └── workflow.log
│ └── 002_space_warrior/
│ ├── final.png
│ └── metadata.json
└── index.json元数据格式
{
"generation_id": "uuid",
"timestamp": "2024-01-15T10:30:00Z",
"user_request": "原始用户需求",
"pipeline_config": {
"prompt": "优化后的prompt",
"negative_prompt": "负面prompt",
"checkpoint": "模型",
"loras": [],
"params": {}
},
"execution_log": {
"image_generator": {},
"refiner": {},
"qa_result": {}
},
"output": {
"final_image": "路径",
"resolution": "1024x1024",
"file_size": "2.4MB"
},
"tags": ["cyberpunk", "cat", "portrait"],
"quality_score": 8.5
}输出
1. 单个任务的 metadata.json 2. 更新全局 index.json(便于搜索) 3. 生成人类可读的报告(可选)
记忆管理
- 维护完整的生成历史
- 统计各类生成任务的平均质量
- 识别用户的生成模式(常生成的主题、风格偏好)
Prompt 工程师 Agent (Prompt Engineer)
身份
你是 Prompt Engineer Agent,专门将用户的模糊需求转化为高质量的 Stable Diffusion Prompt。
职责
1. 分析用户输入,提取关键元素(主题、风格、场景、情绪、质量要求) 2. 将中文/口语化描述转化为结构化的英文 Prompt 3. 编写有效的 Negative Prompt 4. 推荐合适的生成参数(cfg_scale, steps, sampler) 5. 积累并复用 Prompt 模板
输出格式(必须严格遵循)
{
"optimized_prompt": "详细的英文正向提示词,包含质量标签",
"negative_prompt": "负面提示词,排除低质量元素",
"key_elements": {
"subject": "主体描述",
"style": "艺术风格",
"lighting": "光照条件",
"quality_tags": ["masterpiece", "best quality", "8k", "highly detailed"]
},
"recommended_params": {
"cfg_scale": 7.5,
"steps": 30,
"sampler": "DPM++ 2M Karras"
},
"reasoning": "简要的优化思路说明"
}Prompt 优化规则
1. 总是以质量标签开头:masterpiece, best quality, highly detailed 2. 主体描述放在前面,细节放在后面 3. 使用具体的形容词:不要"beautiful",用"ethereal, radiant, pristine" 4. 指定艺术媒介:oil painting, digital art, photograph, concept art 5. 光照是关键:cinematic lighting, volumetric light, golden hour, neon glow
Negative Prompt 标准
必须包含:low quality, blurry, bad anatomy, deformed hands, mutated, watermark, signature, text, cropped, worst quality
记忆管理
- 读取
memory.json了解历史偏好 - 任务完成后更新
memory.json积累新模板 - 如果用户多次提到某种风格,标记为"favorite"
质量审核 Agent (QA Bot)
身份
你是 QA Bot Agent,负责审核最终图片是否符合用户原始需求和质量标准。
职责
1. 对比用户原始需求和最终输出 2. 检查图片是否包含关键元素 3. 评估技术质量(清晰度、伪影、变形) 4. 给出通过/不通过的判决 5. 如果失败,给出具体问题和重试建议
审核维度
{
"compliance_check": {
"subject_match": "是否包含用户要求的主体",
"style_match": "风格是否符合描述",
"mood_match": "氛围/情绪是否到位"
},
"technical_quality": {
"sharpness": "清晰度评分 1-10",
"artifacts": "是否有明显伪影",
"anatomy": "人物/生物结构是否正确",
"color_balance": "色彩是否自然"
},
"overall_score": "综合评分 1-10"
}输出格式
{
"verdict": "PASS|NEEDS_REWORK|FAIL",
"score": 8.5,
"breakdown": {
"subject_compliance": 9,
"style_accuracy": 8,
"technical_quality": 8.5
},
"issues": [
{
"type": "missing_element",
"description": "缺少用户要求的'黑客键盘'",
"severity": "medium"
}
],
"recommendations": [
"在 Prompt 中强调'holographic keyboard'",
"尝试提高 cfg_scale 到 8.5"
],
"approval_for_delivery": true
}通过标准
- PASS: score >= 8.0,无 critical 问题,可直接交付
- NEEDS_REWORK: score 6.0-7.9,有小问题,建议修改后交付
- FAIL: score < 6.0 或有 critical 问题,必须重新生成
Critical 问题(一票否决)
- 明显的人物畸形(多手、多腿、扭曲面部)
- 与用户需求完全不符(要猫却给了狗)
- 严重的颜色错乱或全图模糊
- 图片损坏或无法打开
记忆管理
- 记录常见失败模式
- 积累"修复建议"的有效性数据
- 跟踪哪些 Prompt 模式容易出问题
风格研究员 Agent (Style Scout) - DALL-E 3 版本
身份
你是 Style Scout Agent,专门研究并推荐最适合用户需求的 DALL-E 3 生成参数。
职责
1. 分析 Prompt 中的风格关键词 2. 推荐最佳图片尺寸(根据构图需求) 3. 推荐质量级别(standard vs hd) 4. 推荐风格(vivid vs natural) 5. 提取构图建议给 Prompt 工程师参考
DALL-E 3 参数说明
尺寸 (size)
| 尺寸 | 适用场景 | 关键词触发 |
|---|---|---|
| 1024x1024 | 默认正方形 | 无特殊方向要求 |
| 1792x1024 | 横版/风景 | landscape, panorama, wide, 全景, 风景 |
| 1024x1792 | 竖版/人像 | portrait, full body, tall, 人像, 全身, 竖版 |
质量 (quality)
| 级别 | 成本 | 适用场景 | 关键词触发 |
|---|---|---|---|
| standard | 1x | 快速预览、简单场景 | draft, sketch, 草图 |
| hd | 2x | 最终交付、细节丰富 | 8k, detailed, masterpiece, 高清, 精细 |
风格 (style)
| 风格 | 特点 | 适用场景 | 关键词触发 |
|---|---|---|---|
| vivid | 鲜艳、戏剧化、艺术感强 | 插画、概念艺术、科幻 | art, illustration, cyberpunk, fantasy, 艺术 |
| natural | 柔和、写实、照片感 | 照片、写实、自然场景 | photo, realistic, natural, 照片, 写实 |
输出格式(必须严格遵循)
{
"size": {
"value": "1024x1024",
"reason": "选择这个尺寸的原因"
},
"quality": {
"value": "standard",
"reason": "选择这个质量的原因"
},
"style": {
"value": "vivid",
"reason": "选择这个风格的原因"
},
"composition_tips": [
"给 Prompt 工程师的构图建议"
],
"prompt_enhancement": "建议添加到 prompt 的风格描述",
"summary": "简洁的配置总结"
}判断逻辑
尺寸判断
IF prompt 包含 ["portrait", "person", "character", "full body", "人像", "全身", "竖版"]:
→ size = "1024x1792"
ELSE IF prompt 包含 ["landscape", "scenery", "panorama", "cityscape", "风景", "全景", "横版"]:
→ size = "1792x1024"
ELSE:
→ size = "1024x1024"质量判断
IF prompt 包含 ["8k", "ultra detailed", "masterpiece", "high quality", "hd", "高清", "精细", "高质量"]:
→ quality = "hd"
ELSE:
→ quality = "standard"风格判断
IF prompt 包含 ["photo", "photograph", "realistic", "real", "照片", "写实", "真实"]:
→ style = "natural"
ELSE IF prompt 包含 ["art", "illustration", "painting", "concept", "anime", "艺术", "插画"]:
→ style = "vivid"
ELSE:
→ style = "vivid" # 默认更鲜艳Prompt 增强建议
根据风格判断,给 Prompt 工程师返回建议添加的描述:
- vivid: "highly detailed, vibrant colors, dramatic lighting"
- natural: "photorealistic, natural lighting, shot on camera"
记忆管理
- 记录用户对尺寸/质量/风格的偏好
- 统计哪种配置组合满意度最高
- DALL-E 3 不需要跟踪模型,专注在参数优化上
#!/usr/bin/env python3
"""
📦 批量图片生成器 v2 (多Agent版)
=================================
每张图都走完整的 5-Agent 工作流!
支持三种批量模式:
1. 同需求多风格 (A/B 测试)
2. 同需求多比例 (多尺寸适配)
3. 多需求批量 (内容矩阵)
使用方式:
from batch_generator_v2 import BatchGeneratorV2
bg = BatchGeneratorV2()
# 模式1: 同需求多风格
bg.batch_styles("AI训练营海报", task="poster")
# 模式2: 同需求多比例
bg.batch_aspects("AI训练营海报", task="poster")
# 模式3: 多需求批量
bg.batch_briefs(["海报A", "海报B", "海报C"], task="poster")
"""
import os
import sys
import time
from pathlib import Path
from datetime import datetime
from typing import List, Dict
AGENCY_DIR = Path.home() / ".hermes/agents/multi-agent-image"
sys.path.insert(0, str(AGENCY_DIR))
from orchestrator_v2 import run
class BatchGeneratorV2:
"""批量图片生成器(多Agent版)"""
def __init__(self):
self.results = []
self.start_time = None
def log(self, msg: str):
ts = datetime.now().strftime("%H:%M:%S")
print(f"[{ts}] [批量生成] {msg}")
def batch_styles(self, brief: str, task: str = "poster", aspect: str = "1:1",
quality: str = "final", use_reference: bool = True) -> List[dict]:
"""
模式1: 同需求多风格(A/B 测试)
同一个 brief,让 Agent 生成保守/平衡/大胆三个版本
"""
directions = ["conservative", "balanced", "bold"]
self.log(f"启动多风格批量生成(多Agent): {brief[:30]}...")
self.log(f"将生成 {len(directions)} 个版本")
self.results = []
self.start_time = time.time()
for i, direction in enumerate(directions):
print(f"\n{'='*70}")
print(f"🎨 批量 [{i+1}/{len(directions)}] 风格: {direction}")
print(f"{'='*70}")
# 走完整多Agent工作流,但强制指定 direction
result = run(
user_input=brief,
use_reference=use_reference,
task=task,
direction=direction,
aspect=aspect,
quality=quality
)
result["batch_mode"] = "style"
result["batch_param"] = direction
result["batch_index"] = i
self.results.append(result)
self._print_summary()
return self.results
def batch_aspects(self, brief: str, task: str = "poster", direction: str = "balanced",
aspects: List[str] = None, quality: str = "final",
use_reference: bool = True) -> List[dict]:
"""
模式2: 同需求多比例(多尺寸适配)
同一个 brief,生成多个比例
"""
if aspects is None:
aspects = ["1:1", "16:9", "9:16"]
self.log(f"启动多比例批量生成(多Agent): {brief[:30]}...")
self.log(f"将生成 {len(aspects)} 个比例")
self.results = []
self.start_time = time.time()
for i, aspect in enumerate(aspects):
print(f"\n{'='*70}")
print(f"📐 批量 [{i+1}/{len(aspects)}] 比例: {aspect}")
print(f"{'='*70}")
result = run(
user_input=brief,
use_reference=use_reference,
task=task,
direction=direction,
aspect=aspect,
quality=quality
)
result["batch_mode"] = "aspect"
result["batch_param"] = aspect
result["batch_index"] = i
self.results.append(result)
self._print_summary()
return self.results
def batch_briefs(self, briefs: List[str], task: str = "poster", direction: str = "balanced",
aspect: str = "1:1", quality: str = "final",
use_reference: bool = True) -> List[dict]:
"""
模式3: 多需求批量(内容矩阵)
多个不同的 brief,每张都走完整 Agent 工作流
"""
self.log(f"启动多需求批量生成(多Agent): {len(briefs)} 张")
self.results = []
self.start_time = time.time()
for i, brief in enumerate(briefs):
print(f"\n{'='*70}")
print(f"🎨 批量 [{i+1}/{len(briefs)}] {brief[:40]}...")
print(f"{'='*70}")
# 走完整多Agent工作流(让Agent自己判断参数)
result = run(
user_input=brief,
use_reference=use_reference
)
result["batch_mode"] = "brief"
result["batch_param"] = brief[:30]
result["batch_index"] = i
self.results.append(result)
self._print_summary()
return self.results
def _print_summary(self):
"""打印批量生成摘要"""
elapsed = time.time() - self.start_time if self.start_time else 0
total = len(self.results)
success = sum(1 for r in self.results if r.get("success"))
failed = total - success
print("\n" + "=" * 70)
print("📦 批量生成完成!(多Agent版)")
print("=" * 70)
print(f"\n📊 统计:")
print(f" 总计: {total} 张")
print(f" ✅ 成功: {success} 张")
print(f" ❌ 失败: {failed} 张")
print(f" ⏱️ 总耗时: {elapsed:.1f} 秒")
print(f" ⚡ 平均: {elapsed/max(total,1):.1f} 秒/张")
print(f"\n📁 成功文件:")
for r in self.results:
if r.get("success"):
mode = r.get("batch_mode", "")
param = r.get("batch_param", "")
filepath = r.get("filepath", "")
score = r.get("score", 0)
if filepath:
print(f" ✅ [{mode}/{param}] {Path(filepath).name} ⭐{score}")
else:
print(f" ❌ [{r.get('batch_index', '?')}] {r.get('error', 'Unknown')}")
# 案例库统计
from case_library import list_cases
total_cases = len(list_cases())
print(f"\n📚 案例库: 共 {total_cases} 个案例")
print()
# 快捷函数
def batch_styles(brief: str, **kwargs):
"""同需求多风格(多Agent)"""
return BatchGeneratorV2().batch_styles(brief, **kwargs)
def batch_aspects(brief: str, **kwargs):
"""同需求多比例(多Agent)"""
return BatchGeneratorV2().batch_aspects(brief, **kwargs)
def batch_briefs(briefs: List[str], **kwargs):
"""多需求批量(多Agent)"""
return BatchGeneratorV2().batch_briefs(briefs, **kwargs)
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("用法:")
print(' python batch_generator_v2.py styles "AI训练营海报"')
print(' python batch_generator_v2.py aspects "AI训练营海报"')
print(' python batch_generator_v2.py briefs "海报A" "海报B" "海报C"')
sys.exit(1)
mode = sys.argv[1]
brief = sys.argv[2] if len(sys.argv) > 2 else "测试海报"
bg = BatchGeneratorV2()
if mode == "styles":
bg.batch_styles(brief)
elif mode == "aspects":
bg.batch_aspects(brief)
elif mode == "briefs":
bg.batch_briefs(sys.argv[2:])
#!/usr/bin/env python3
"""
📚 案例库系统 (Case Library)
=======================
功能:
1. 保存生成的图片到案例库(按任务类型分类)
2. 列出案例库中的所有案例
3. 选择案例作为参考图,进行风格迁移(图生图)
4. 支持案例打标签、评分、搜索
案例库路径:
/root/.hermes/agents/multi-agent-image/case_library/
├── poster/
│ ├── case_001_赛博朋克海报/
│ │ ├── image.png
│ │ └── metadata.json
│ └── case_002_AI训练营海报/
│ ├── image.png
│ └── metadata.json
├── product/
├── ppt/
├── infographic/
└── teaching/
"""
import os
import sys
import json
import shutil
from pathlib import Path
from datetime import datetime
AGENCY_DIR = Path.home() / ".hermes/agents/multi-agent-image"
CASE_LIBRARY_DIR = AGENCY_DIR / "case_library"
CASE_LIBRARY_DIR.mkdir(parents=True, exist_ok=True)
# 为每种任务类型创建目录
for task in ["poster", "product", "ppt", "infographic", "teaching"]:
(CASE_LIBRARY_DIR / task).mkdir(exist_ok=True)
def log(msg):
print(f"[案例库] {msg}")
def add_case(image_path: str, metadata: dict, task: str = "poster", tags: list = None) -> str:
"""
添加案例到案例库
Args:
image_path: 图片路径
metadata: 元数据(包含 prompt、params 等)
task: 任务类型
tags: 标签列表
Returns:
案例 ID
"""
task_dir = CASE_LIBRARY_DIR / task
# 生成案例编号
existing = [d for d in task_dir.iterdir() if d.is_dir()]
case_num = len(existing) + 1
case_id = f"case_{case_num:03d}"
# 创建案例目录
case_dir = task_dir / f"{case_id}_{metadata.get('brief', 'untitled')[:20]}"
case_dir.mkdir(exist_ok=True)
# 复制图片
ext = Path(image_path).suffix
target_image = case_dir / f"image{ext}"
shutil.copy2(image_path, target_image)
# 保存元数据
case_meta = {
"case_id": case_id,
"task": task,
"created_at": datetime.now().isoformat(),
"image_path": str(target_image),
"brief": metadata.get("brief", ""),
"prompt": metadata.get("prompt", ""),
"params": metadata.get("params", {}),
"tags": tags or [],
"rating": metadata.get("rating", 0),
}
with open(case_dir / "metadata.json", 'w', encoding='utf-8') as f:
json.dump(case_meta, f, indent=2, ensure_ascii=False)
log(f"✅ 案例已保存: {case_dir.name}")
return case_id
def list_cases(task: str = None) -> list:
"""
列出案例库中的所有案例
Args:
task: 按任务类型过滤 (poster/product/ppt/infographic/teaching)
None = 显示全部
Returns:
案例列表
"""
cases = []
tasks_to_list = [task] if task else ["poster", "product", "ppt", "infographic", "teaching"]
for t in tasks_to_list:
task_dir = CASE_LIBRARY_DIR / t
if not task_dir.exists():
continue
for case_dir in sorted(task_dir.iterdir()):
if not case_dir.is_dir():
continue
meta_file = case_dir / "metadata.json"
if meta_file.exists():
with open(meta_file, 'r', encoding='utf-8') as f:
meta = json.load(f)
cases.append(meta)
return cases
def search_cases(keyword: str, task: str = None) -> list:
"""搜索案例(按关键词匹配 brief、prompt、tags)"""
all_cases = list_cases(task)
keyword_lower = keyword.lower()
results = []
for case in all_cases:
text_to_search = " ".join([
case.get("brief", ""),
case.get("prompt", ""),
" ".join(case.get("tags", []))
]).lower()
if keyword_lower in text_to_search:
results.append(case)
return results
def get_case_image_path(case_id: str, task: str = None) -> str:
"""
获取案例图片路径
Args:
case_id: 案例编号 (如 "case_001")
task: 任务类型(如果知道)
Returns:
图片路径,找不到返回 None
"""
if task:
task_dirs = [CASE_LIBRARY_DIR / task]
else:
task_dirs = [CASE_LIBRARY_DIR / t for t in ["poster", "product", "ppt", "infographic", "teaching"]]
for task_dir in task_dirs:
if not task_dir.exists():
continue
for case_dir in task_dir.iterdir():
if case_dir.name.startswith(case_id):
for ext in [".png", ".jpg", ".jpeg", ".webp"]:
img = case_dir / f"image{ext}"
if img.exists():
return str(img)
return None
def print_case_list(cases: list):
"""打印案例列表(美观格式)"""
if not cases:
print(" (暂无案例)")
return
for case in cases:
case_id = case.get("case_id", "N/A")
brief = case.get("brief", "无标题")[:30]
task = case.get("task", "unknown")
rating = case.get("rating", 0)
tags = ", ".join(case.get("tags", [])[:3])
stars = "⭐" * int(rating) if rating else ""
print(f" {case_id} [{task}] {brief} {stars}")
if tags:
print(f" 🏷️ {tags}")
def interactive_select_case(task: str = None) -> str:
"""
交互式选择案例
Returns:
选中的案例图片路径,或 None(不选择)
"""
print("\n" + "=" * 60)
print("📚 案例库")
print("=" * 60)
cases = list_cases(task)
if not cases:
print("\n案例库为空,直接生成新图片。\n")
return None
print(f"\n找到 {len(cases)} 个案例:\n")
print_case_list(cases)
print("\n选项:")
print(" [1-N] 选择案例编号参考")
print(" [s] 搜索案例")
print(" [n] 不参考案例,直接生成")
print()
choice = input("你的选择: ").strip().lower()
if choice == 'n' or not choice:
print(" → 不参考案例\n")
return None
if choice == 's':
keyword = input("搜索关键词: ").strip()
results = search_cases(keyword, task)
if results:
print(f"\n搜索结果 ({len(results)} 个):")
print_case_list(results)
idx = input("选择编号 (1-N): ").strip()
try:
selected = results[int(idx) - 1]
return selected["image_path"]
except:
print(" → 无效选择,不参考案例\n")
return None
else:
print(" → 未找到匹配案例\n")
return None
# 按编号选择
try:
idx = int(choice) - 1
if 0 <= idx < len(cases):
selected = cases[idx]
print(f" → 已选择: {selected['case_id']} - {selected['brief'][:30]}\n")
return selected["image_path"]
except:
pass
print(" → 无效选择,不参考案例\n")
return None
def auto_save_to_library(generation_result: dict, brief: str, params: dict):
"""自动生成完成后,自动保存到案例库"""
if generation_result.get("status") != "success":
return
filepath = generation_result.get("filepath")
if not filepath or not os.path.exists(filepath):
return
metadata = {
"brief": brief,
"prompt": generation_result.get("final_prompt", ""),
"params": params,
"rating": 0,
}
task = params.get("task", "poster")
add_case(filepath, metadata, task=task)
# 快捷命令
def lib():
"""查看案例库"""
print("\n📚 案例库概览:\n")
for task in ["poster", "product", "ppt", "infographic", "teaching"]:
cases = list_cases(task)
print(f"【{task}】({len(cases)} 个)")
print_case_list(cases)
print()
if __name__ == "__main__":
if len(sys.argv) > 1:
cmd = sys.argv[1]
if cmd == "list":
lib()
elif cmd == "search" and len(sys.argv) > 2:
results = search_cases(sys.argv[2])
print_case_list(results)
else:
print("用法: python case_library.py [list|search <关键词>]")
else:
lib()
#!/usr/bin/env python3
"""
🎛️ 案例选择器 (Case Selector)
=============================
用于 Hermes 对话系统的案例库交互工具
功能:
1. 查询案例库,生成格式化的选择列表
2. 解析用户的选择(编号、关键词、或不选)
3. 返回参考图路径或 None
使用方式(在 Hermes 对话中):
Step 1: 调用 get_selection_text(task) 获取展示文本
Step 2: Hermes AI 展示文本,询问用户
Step 3: 用户回复选择
Step 4: 调用 parse_user_choice(user_reply, task) 获取图片路径
"""
import os
import sys
from pathlib import Path
AGENCY_DIR = Path.home() / ".hermes/agents/multi-agent-image"
sys.path.insert(0, str(AGENCY_DIR))
from case_library import list_cases, search_cases, get_case_image_path
def get_selection_text(task: str = None, brief: str = "") -> str:
"""
生成供 Hermes 展示的案例选择文本
Args:
task: 任务类型 (poster/product/ppt/infographic/teaching)
brief: 用户原始需求(用于智能推荐)
Returns:
格式化的选择文本,直接展示给用户
"""
cases = list_cases(task)
if not cases:
return "📚 案例库暂无相关案例,将直接生成新图片。"
lines = []
lines.append("📚 案例库")
lines.append("=" * 50)
lines.append(f"找到 {len(cases)} 个{'相关' if task else ''}案例:\n")
# 按评分排序
sorted_cases = sorted(cases, key=lambda x: x.get("rating", 0), reverse=True)
for i, case in enumerate(sorted_cases, 1):
case_id = case.get("case_id", f"case_{i:03d}")
brief_text = case.get("brief", "无标题")[:35]
task_type = case.get("task", "unknown")
rating = case.get("rating", 0)
tags = ", ".join(case.get("tags", [])[:3])
stars = "⭐" * int(rating) if rating else ""
# 标记推荐(评分最高)
recommend = " 👈 推荐" if i == 1 else ""
lines.append(f" [{i}] {case_id} [{task_type}]")
lines.append(f" {brief_text} {stars}{recommend}")
if tags:
lines.append(f" 🏷️ {tags}")
lines.append("")
# 智能推荐说明
if brief:
lines.append(f"💡 根据你的需求「{brief[:20]}...」,推荐选择 [{1}] 号案例作为风格参考。")
lines.append("")
lines.append("选项:")
lines.append(" [1-N] 输入编号,使用该案例风格参考")
lines.append(" [s] 搜索案例(输入关键词)")
lines.append(" [n] 不参考案例,全新生成")
lines.append("")
lines.append("请回复你的选择:")
return "\n".join(lines)
def parse_user_choice(user_reply: str, task: str = None) -> tuple:
"""
解析用户的选择回复
Args:
user_reply: 用户的回复文本(如 "1"、"n"、"搜索橙色")
task: 任务类型
Returns:
(action, result)
- action: "generate" | "search" | "skip"
- result: 图片路径 或 搜索关键词 或 None
"""
reply = user_reply.strip().lower()
# 不选
if reply in ["n", "no", "否", "不", "不用", "不需要", "直接生成", "全新"]:
return "skip", None
# 搜索
if reply == "s" or "搜索" in reply:
keyword = reply.replace("搜索", "").replace("s", "").strip()
if not keyword:
return "search", "" # 需要进一步询问关键词
results = search_cases(keyword, task)
if results:
return "generate", results[0].get("image_path")
return "skip", None
# 按编号选择
cases = list_cases(task)
sorted_cases = sorted(cases, key=lambda x: x.get("rating", 0), reverse=True)
try:
idx = int(reply) - 1
if 0 <= idx < len(sorted_cases):
selected = sorted_cases[idx]
return "generate", selected.get("image_path")
except ValueError:
pass
# 模糊匹配(用户可能直接说了案例编号)
if "case_" in reply:
case_id = [w for w in reply.split() if w.startswith("case_")][0]
path = get_case_image_path(case_id, task)
if path:
return "generate", path
# 默认不选
return "skip", None
def get_case_preview(case_id: str, task: str = None) -> str:
"""
获取案例预览信息(用于展示给用户)
"""
path = get_case_image_path(case_id, task)
if not path:
return None
cases = list_cases(task)
for case in cases:
if case.get("case_id") == case_id:
return {
"image_path": path,
"brief": case.get("brief", ""),
"rating": case.get("rating", 0),
"tags": case.get("tags", []),
}
return {"image_path": path, "brief": "", "rating": 0, "tags": []}
# 测试
if __name__ == "__main__":
print(get_selection_text("poster", "AI训练营海报"))
#!/usr/bin/env python3
"""
Internal design compiler for multi-agent-image.
This module extracts the prompt-compilation logic that was previously delegated
to an external runtime. It keeps the prompt-only path lightweight and reusable
from both CLI wrappers and Python call sites.
"""
from __future__ import annotations
from typing import Any
TASK_KEYWORDS = {
"poster": ["海报", "poster", "封面", "主视觉", "campaign", "kv"],
"product": ["商品", "产品", "电商", "product", "hero shot", "广告图", "首图"],
"ppt": ["ppt", "幻灯片", "演示", "deck", "slide", "配图", "章节页"],
"infographic": ["信息图", "infographic", "流程图", "结构图", "总结图", "overview"],
"teaching": ["教学", "演示图", "讲解", "培训", "课件", "步骤图", "demo"],
}
DEFAULT_ASPECT = {
"poster": "3:4",
"product": "1:1",
"ppt": "16:9",
"infographic": "4:3",
"teaching": "16:9",
}
QUALITY_MODEL = {
"draft": "gpt-image-2",
"final": "gpt-image-2",
"premium": "gpt-image-2",
}
ASPECT_SIZES = {
"1:1": {"2K": "2048x2048", "3K": "3072x3072"},
"3:4": {"2K": "1728x2304", "3K": "2592x3456"},
"4:3": {"2K": "2304x1728", "3K": "3456x2592"},
"16:9": {"2K": "2848x1600", "3K": "4096x2304"},
"9:16": {"2K": "1600x2848", "3K": "2304x4096"},
"3:2": {"2K": "2496x1664", "3K": "3744x2496"},
"2:3": {"2K": "1664x2496", "3K": "2496x3744"},
"4:5": {"2K": "1840x2304", "3K": "2760x3456"},
"5:4": {"2K": "2304x1840", "3K": "3456x2760"},
"2:1": {"2K": "3072x1536", "3K": "4096x2048"},
"1:2": {"2K": "1536x3072", "3K": "2048x4096"},
"21:9": {"2K": "3360x1440", "3K": "5120x2160"},
"9:21": {"2K": "1440x3360", "3K": "2160x5120"},
}
CORE_SOURCE_SECTIONS = [
"## Your workflow",
"## Output creation guidelines",
"### How to do design work",
"## Content Guidelines",
"**Do not add filler content.**",
"**Create a system up front:**",
"**Avoid AI slop tropes:**",
]
TASK_SOURCE_SECTIONS = {
"poster": [
"### How to do design work",
"## Content Guidelines",
"**Do not add filler content.**",
],
"product": [
"## Output creation guidelines",
"## Content Guidelines",
"**Avoid AI slop tropes:**",
],
"ppt": [
"## Content Guidelines",
"**Create a system up front:**",
"**Use appropriate scales:**",
],
"infographic": [
"## Content Guidelines",
"**Do not add filler content.**",
"**Avoid AI slop tropes:**",
],
"teaching": [
"## Content Guidelines",
"**Do not add filler content.**",
"**Use appropriate scales:**",
],
}
DIRECTION_PROFILES = {
"conservative": {
"style_bias": "restrained, cleaner, corporate, polished, lower-risk",
"energy_bias": "controlled, composed, authoritative",
"composition_bias": "cleaner geometry, more negative space, lower background complexity",
"palette_bias": "restricted palette with restrained accents",
"detail_bias": "cleaner surfaces, less decorative detail",
},
"balanced": {
"style_bias": "premium editorial, contemporary, polished, commercially strong",
"energy_bias": "energetic but disciplined, confident, professional",
"composition_bias": "clear hierarchy, dynamic but stable layout, deliberate focal contrast",
"palette_bias": "premium neutrals with one strong accent family",
"detail_bias": "hero-detail emphasis with a restrained background",
},
"bold": {
"style_bias": "larger scale, more dramatic, more surprising, high-contrast",
"energy_bias": "ambitious, high-energy, assertive, vivid",
"composition_bias": "bolder crop, stronger scale contrast, more motion cues, still disciplined",
"palette_bias": "higher-contrast palette with strong accent energy",
"detail_bias": "high-impact hero detail, expressive texture, controlled spectacle",
},
}
GLOBAL_DIRECTIVES = [
"Start from purpose, audience, and channel rather than surface-level adjectives.",
"Create a coherent visual system before detailing the image.",
"Use one clear hero idea and preserve obvious hierarchy.",
"Treat negative space and safe zones as design decisions, not leftover space.",
"Avoid filler content, decorative noise, and meaningless visual data.",
"Respect existing brand or context when available; if not, still commit to a clear direction.",
"Avoid AI-slop tropes such as random HUD overlays, generic fog, empty gradients, and scattered floating debris.",
]
TASK_PROFILES = {
"poster": {
"communication_goal": "attract and persuade quickly in a campaign or recruitment context",
"hero_strategy": "one dominant hero visual or symbolic concept, never a collage of equal-weight objects",
"safe_zone": "reserve a clean, obvious text-safe zone in the upper third or one side for headline and CTA copy",
"lighting": "crisp premium lighting with controlled contrast and energetic highlights",
"palette": "restricted premium palette with disciplined neutrals and one energetic accent family",
"detail_density": "hero-rich detail with restrained background complexity",
"base_style": "editorial campaign key visual",
"task_constraints": [
"make the result feel campaign-ready rather than generic AI art",
"prioritize focal hierarchy and typography-safe composition",
],
"task_avoid": [
"poster text rendered directly into the image unless explicitly requested",
"random marketing icons and decorative interface fragments",
],
},
"product": {
"communication_goal": "make the product feel desirable, premium, and commercially credible",
"hero_strategy": "the product is the undisputed focal point with clear silhouette and edge readability",
"safe_zone": "keep surrounding space supportive and uncluttered so the product remains dominant",
"lighting": "commercial lighting that reveals material, finish, and shape without cheap reflections",
"palette": "palette chosen to support product positioning rather than compete with the product",
"detail_density": "high fidelity on the hero object, restrained props and background",
"base_style": "high-end commercial product advertising",
"task_constraints": [
"preserve product proportions and perceived material integrity",
"use props only when they reinforce product positioning",
],
"task_avoid": [
"oversized props stealing attention",
"luxury claims paired with cheap lighting or noisy backgrounds",
],
},
"ppt": {
"communication_goal": "support presentation storytelling with a clear, memorable visual metaphor",
"hero_strategy": "a single strong metaphor or scene readable at presentation distance",
"safe_zone": "reserve a large clean area for slide title and subtitle overlay",
"lighting": "clean, legible lighting that supports shape readability over moodiness",
"palette": "presentation-friendly palette with strong contrast and limited visual noise",
"detail_density": "mid-detail image readable at a glance, not overloaded with tiny elements",
"base_style": "presentation cover art",
"task_constraints": [
"favor readability from distance over excess detail",
"keep enough room for future title placement",
],
"task_avoid": [
"ad-poster density",
"tiny embedded text or fragile detail that disappears on slides",
],
},
"infographic": {
"communication_goal": "communicate structure, grouping, and flow rather than literal dense data",
"hero_strategy": "clear modular hierarchy with one dominant organizing principle",
"safe_zone": "leave room for headings or labels without relying on the model to render tiny text",
"lighting": "flat-to-controlled lighting that supports structure and clarity",
"palette": "structured palette with clear grouping and low noise",
"detail_density": "low-to-mid detail, with emphasis on grouping and directional logic",
"base_style": "structured infographic-like visual system",
"task_constraints": [
"prioritize visual structure over fake data richness",
"use symbolic clarity and modular composition",
],
"task_avoid": [
"tiny chart labels",
"data slop, fake dashboards, and dense unreadable micro-details",
],
},
"teaching": {
"communication_goal": "explain a process, comparison, or sequence with maximum clarity",
"hero_strategy": "show the logic of the teaching point first, then add supporting visuals",
"safe_zone": "keep panel or label areas simple and legible for later annotation",
"lighting": "clear explanatory lighting, not overly cinematic",
"palette": "clarity-first palette with simple grouping and controlled contrast",
"detail_density": "mid-to-low detail with emphasis on readable sequence and large forms",
"base_style": "instructional visual storytelling",
"task_constraints": [
"make sequence and cause-effect legible at first glance",
"use big forms and obvious directional logic",
],
"task_avoid": [
"cinematic clutter that weakens explanation",
"too many simultaneous steps in one frame",
],
},
}
def detect_task(brief: str) -> str:
text = brief.lower()
scores: dict[str, int] = {}
for task, keywords in TASK_KEYWORDS.items():
scores[task] = sum(1 for keyword in keywords if keyword.lower() in text)
best_task = max(scores, key=scores.get)
return best_task if scores[best_task] > 0 else "poster"
def normalize_task(task: str, brief: str) -> str:
return detect_task(brief) if task == "auto" else task
def choose_model(quality: str) -> str:
return QUALITY_MODEL[quality]
def choose_size(task: str, aspect: str, quality: str) -> str:
tier = "2K" if quality == "draft" or task in {"ppt", "infographic", "teaching"} else "3K"
aspect_sizes = ASPECT_SIZES.get(aspect)
if aspect_sizes:
return aspect_sizes[tier]
return ASPECT_SIZES[DEFAULT_ASPECT[task]][tier]
def join_phrases(items: list[str]) -> str:
return "; ".join(item for item in items if item)
def unique_preserving_order(items: list[str]) -> list[str]:
seen: set[str] = set()
ordered: list[str] = []
for item in items:
if item not in seen:
seen.add(item)
ordered.append(item)
return ordered
def make_design_reasoning(
*,
brief: str,
task: str,
direction: str,
audience: str | None = None,
usage: str | None = None,
brand: str | None = None,
style: str | None = None,
mood: str | None = None,
goal: str | None = None,
composition: str | None = None,
constraints: str | None = None,
avoid: str | None = None,
safe_zone: str | None = None,
lighting: str | None = None,
palette: str | None = None,
image: list[str] | None = None,
) -> dict[str, Any]:
profile = TASK_PROFILES[task]
direction_profile = DIRECTION_PROFILES[direction]
brand_strategy = (
f"use the provided brand/context: {brand}"
if brand
else "no explicit brand provided; commit to one coherent visual system instead of averaging styles"
)
reference_strategy = (
"use provided reference images to preserve consistency and context"
if image
else "no reference images provided; rely on the compiled visual system and brief"
)
source_sections = unique_preserving_order(CORE_SOURCE_SECTIONS + TASK_SOURCE_SECTIONS[task])
visual_system = [
f"base mode: {profile['base_style']}",
f"direction bias: {direction_profile['style_bias']}",
f"energy bias: {direction_profile['energy_bias']}",
f"composition bias: {direction_profile['composition_bias']}",
f"palette bias: {direction_profile['palette_bias']}",
]
hierarchy_strategy = [
"one clear hero idea",
profile["hero_strategy"],
"secondary elements must support the hero rather than compete with it",
"background should create rhythm, not narrative confusion",
]
anti_filler_rules = [
"every element must earn its place",
"do not add objects, labels, icons, or stats that do not strengthen the core message",
"if the frame feels empty, solve with scale, crop, rhythm, or texture rather than random extra elements",
]
anti_slop_rules = [
"avoid generic AI clutter",
"avoid random floating UI fragments or HUD overlays",
"avoid generic gradient fog with no composition logic",
"avoid cheap neon cyberpunk treatment unless explicitly requested",
"avoid noisy micro-detail that weakens the hierarchy",
]
if avoid:
anti_slop_rules.append(avoid)
return {
"task": task,
"direction": direction,
"communication_goal": goal or profile["communication_goal"],
"audience": audience or "broad professional audience",
"channel": usage or task,
"brief": brief.strip(),
"brand_strategy": brand_strategy,
"reference_strategy": reference_strategy,
"visual_system": visual_system,
"hierarchy_strategy": hierarchy_strategy,
"safe_zone_strategy": safe_zone or profile["safe_zone"],
"lighting_strategy": lighting or profile["lighting"],
"palette_strategy": palette or profile["palette"],
"detail_density": direction_profile["detail_bias"] + "; " + profile["detail_density"],
"style_direction": style or join_phrases(visual_system),
"mood_direction": mood or direction_profile["energy_bias"],
"composition_logic": composition or direction_profile["composition_bias"],
"anti_filler_rules": anti_filler_rules,
"anti_slop_rules": anti_slop_rules,
"task_constraints": profile["task_constraints"] + ([constraints] if constraints else []),
"task_avoid": profile["task_avoid"],
"global_directives": GLOBAL_DIRECTIVES,
"source_sections": source_sections,
"primary_source_file": "internal:multi-agent-image/scripts/design_compiler.py",
}
def compile_design_brief(reasoning: dict[str, Any], aspect: str) -> dict[str, Any]:
return {
"task": reasoning["task"],
"direction": reasoning["direction"],
"brief": reasoning["brief"],
"communication_goal": reasoning["communication_goal"],
"audience": reasoning["audience"],
"channel": reasoning["channel"],
"brand_strategy": reasoning["brand_strategy"],
"reference_strategy": reasoning["reference_strategy"],
"visual_system": join_phrases(reasoning["visual_system"]),
"hierarchy": join_phrases(reasoning["hierarchy_strategy"]),
"composition": reasoning["composition_logic"],
"safe_zone": reasoning["safe_zone_strategy"],
"lighting": reasoning["lighting_strategy"],
"palette": reasoning["palette_strategy"],
"detail_density": reasoning["detail_density"],
"style_direction": reasoning["style_direction"],
"mood": reasoning["mood_direction"],
"constraints": join_phrases(reasoning["task_constraints"]),
"avoid": join_phrases(reasoning["anti_slop_rules"] + reasoning["task_avoid"]),
"aspect": aspect,
"source_sections": reasoning["source_sections"],
}
def build_prompt(brief: dict[str, Any]) -> str:
parts = [
f"Create a {brief['task']} image for {brief['channel']} aimed at {brief['audience']}.",
f"Treat this as a design-led visual solving this brief: {brief['brief']}.",
f"Communication goal: {brief['communication_goal']}.",
"Translate the brief into one strong hero concept rather than many equal-weight elements.",
f"Brand and context strategy: {brief['brand_strategy']}.",
f"Visual system: {brief['visual_system']}.",
f"Hierarchy: {brief['hierarchy']}.",
f"Composition: {brief['composition']}.",
f"Safe zone: {brief['safe_zone']}.",
f"Lighting: {brief['lighting']}.",
f"Color strategy: {brief['palette']}.",
f"Detail density: {brief['detail_density']}.",
f"Style direction: {brief['style_direction']}.",
f"Mood: {brief['mood']}.",
f"Aspect ratio: {brief['aspect']}.",
f"Important constraints: {brief['constraints']}.",
f"Avoid: {brief['avoid']}.",
"Emphasize strong hierarchy, intentional whitespace, disciplined background complexity, and polished professional finish.",
]
return " ".join(parts)
def compile_prompt_package(
*,
brief: str,
task: str = "auto",
direction: str = "balanced",
aspect: str | None = None,
quality: str = "final",
audience: str | None = None,
usage: str | None = None,
brand: str | None = None,
style: str | None = None,
mood: str | None = None,
goal: str | None = None,
composition: str | None = None,
constraints: str | None = None,
avoid: str | None = None,
safe_zone: str | None = None,
lighting: str | None = None,
palette: str | None = None,
image: list[str] | None = None,
model_override: str | None = None,
) -> dict[str, Any]:
normalized_task = normalize_task(task, brief)
resolved_aspect = aspect or DEFAULT_ASPECT[normalized_task]
design_reasoning = make_design_reasoning(
brief=brief,
task=normalized_task,
direction=direction,
audience=audience,
usage=usage,
brand=brand,
style=style,
mood=mood,
goal=goal,
composition=composition,
constraints=constraints,
avoid=avoid,
safe_zone=safe_zone,
lighting=lighting,
palette=palette,
image=image,
)
compiled_brief = compile_design_brief(design_reasoning, resolved_aspect)
prompt = build_prompt(compiled_brief)
model = model_override or choose_model(quality)
size = choose_size(normalized_task, resolved_aspect, quality)
return {
"design_reasoning": design_reasoning,
"compiled_brief": compiled_brief,
"prompt": prompt,
"settings": {
"model": model,
"size": size,
"aspect": resolved_aspect,
"direction": direction,
"quality": quality,
},
}
#!/usr/bin/env python3
"""
Standalone design compiler for multi-agent-image.
It keeps the original prompt-compilation workflow available locally while using
the repo's own GPT-Image-2 runtime instead of an external generator.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from design_compiler import compile_prompt_package
from gpt_image2_generator import generate_image
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Compile a design-led image brief and optionally generate with GPT-Image-2."
)
parser.add_argument("--task", default="auto", choices=["auto", "poster", "product", "ppt", "infographic", "teaching"])
parser.add_argument("--brief", required=True, help="User brief or request")
parser.add_argument("--audience", default=None, help="Target audience")
parser.add_argument("--usage", default=None, help="Where the image will be used")
parser.add_argument("--brand", default=None, help="Brand tone or reference context")
parser.add_argument("--style", default=None, help="Preferred visual style override")
parser.add_argument("--mood", default=None, help="Preferred mood override")
parser.add_argument("--goal", default=None, help="Specific communication goal override")
parser.add_argument("--composition", default=None, help="Composition override")
parser.add_argument("--constraints", default=None, help="Must-have constraints")
parser.add_argument("--avoid", default=None, help="Things to avoid")
parser.add_argument("--aspect", default=None, help="Aspect ratio such as 1:1, 3:4, 16:9")
parser.add_argument("--direction", default="balanced", choices=["conservative", "balanced", "bold"])
parser.add_argument("--safe-zone", default=None, help="Safe-zone strategy override")
parser.add_argument("--lighting", default=None, help="Lighting strategy override")
parser.add_argument("--palette", default=None, help="Palette strategy override")
parser.add_argument("--quality", default="final", choices=["draft", "final", "premium"])
parser.add_argument("--model-override", default=None, help="Explicit model id override")
parser.add_argument("--image", nargs="+", default=None, help="Reference image path(s) or URL(s)")
parser.add_argument("--output", "-o", default=None, help="Output filename")
parser.add_argument("--output-dir", default=None, help="Output directory")
parser.add_argument("--prompt-only", action="store_true", help="Only print design reasoning, compiled brief, and the final prompt")
return parser.parse_args()
def print_package(package: dict) -> None:
print("[design_reasoning]")
print(json.dumps(package["design_reasoning"], ensure_ascii=False, indent=2))
print("\n[compiled_brief]")
print(json.dumps(package["compiled_brief"], ensure_ascii=False, indent=2))
print("\n[prompt]")
print(package["prompt"])
settings = package["settings"]
print(
"\n[settings]\n"
f"model={settings['model']}\n"
f"size={settings['size']}\n"
f"aspect={settings['aspect']}\n"
f"direction={settings['direction']}\n"
f"quality={settings['quality']}"
)
def main() -> int:
args = parse_args()
package = compile_prompt_package(
brief=args.brief,
task=args.task,
direction=args.direction,
aspect=args.aspect,
quality=args.quality,
audience=args.audience,
usage=args.usage,
brand=args.brand,
style=args.style,
mood=args.mood,
goal=args.goal,
composition=args.composition,
constraints=args.constraints,
avoid=args.avoid,
safe_zone=args.safe_zone,
lighting=args.lighting,
palette=args.palette,
image=args.image,
model_override=args.model_override,
)
print_package(package)
sys.stdout.flush()
if args.prompt_only:
return 0
save_dir = args.output_dir
if args.output:
output_path = Path(args.output)
if output_path.parent != Path("."):
save_dir = str(output_path.parent)
result = generate_image(
prompt=package["prompt"],
size=package["settings"]["aspect"],
save_dir=save_dir,
)
if result.get("status") == "success" and args.output:
target_dir = Path(save_dir) if save_dir else Path(result["filepath"]).parent
target_path = target_dir / Path(args.output).name
Path(result["filepath"]).replace(target_path)
result["filepath"] = str(target_path)
result["filename"] = target_path.name
print("\n[result]")
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if result.get("status") == "success" else 1
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""
GPT-Image-2 图片生成器 (apimart.ai)
支持异步任务提交和轮询查询
"""
import os
import json
import time
import requests
from datetime import datetime
from pathlib import Path
API_BASE = "https://api.apimart.ai/v1"
def require_api_key(api_key: str = None) -> str:
"""Return the apimart/OpenAI-compatible API key or fail with a clear message."""
resolved = api_key or os.environ.get("OPENAI_API_KEY")
if not resolved:
raise RuntimeError(
"Missing OPENAI_API_KEY. Set your apimart.ai GPT-Image-2 key before generating images."
)
return resolved
def submit_task(prompt: str, size: str = "1:1", api_key: str = None):
"""
提交图片生成任务
Args:
prompt: 图片描述
size: 图片比例 - "1:1", "16:9", "9:16", "4:3", "3:4", etc.
api_key: API Key
Returns:
dict: 包含 task_id
"""
api_key = require_api_key(api_key)
url = f"{API_BASE}/images/generations"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
data = {
"model": "gpt-image-2",
"prompt": prompt,
"n": 1,
"size": size
}
print(f" 📤 提交任务...")
print(f" 📝 Prompt: {prompt[:60]}...")
print(f" 📐 Size: {size}")
response = requests.post(url, headers=headers, json=data, timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") == 200:
task_id = result["data"][0]["task_id"]
print(f" ✅ 任务已提交: {task_id}")
return {"success": True, "task_id": task_id}
else:
error_msg = result.get("error", {}).get("message", "Unknown error")
print(f" ❌ 提交失败: {error_msg}")
return {"success": False, "error": error_msg}
def query_task(task_id: str, api_key: str = None):
"""
查询任务状态
Returns:
dict: 任务状态和结果
"""
api_key = require_api_key(api_key)
url = f"{API_BASE}/tasks/{task_id}"
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
return response.json()
def wait_for_completion(task_id: str, api_key: str = None,
initial_delay: int = 10, poll_interval: int = 5,
max_wait: int = 120):
"""
轮询等待任务完成
Args:
task_id: 任务ID
api_key: API Key
initial_delay: 首次查询前的等待时间(秒)
poll_interval: 轮询间隔(秒)
max_wait: 最大等待时间(秒)
Returns:
dict: 任务结果
"""
print(f"\n ⏳ 等待生成完成(首次等待 {initial_delay}s)...")
time.sleep(initial_delay)
start_time = time.time()
attempts = 0
while time.time() - start_time < max_wait:
attempts += 1
elapsed = int(time.time() - start_time)
print(f" 🔄 第 {attempts} 次查询(已等待 {elapsed}s)...", end=" ")
result = query_task(task_id, api_key)
if result.get("code") != 200:
error_msg = result.get("error", {}).get("message", "Query failed")
print(f"❌ 查询错误: {error_msg}")
return {"success": False, "error": error_msg}
task_data = result["data"]
status = task_data.get("status")
progress = task_data.get("progress", 0)
if status == "completed":
print(f"✅ 完成!")
return {"success": True, "data": task_data}
elif status == "failed":
error_msg = task_data.get("error", {}).get("message", "Task failed")
print(f"❌ 失败: {error_msg}")
return {"success": False, "error": error_msg}
elif status in ["pending", "processing"]:
print(f"⏳ {status} ({progress}%)")
time.sleep(poll_interval)
else:
print(f"❓ 未知状态: {status}")
time.sleep(poll_interval)
print(f"\n ⏰ 超时(超过 {max_wait}s)")
return {"success": False, "error": "Timeout"}
def download_image(image_url: str, save_path: str):
"""下载图片到本地"""
print(f" 📥 下载图片...")
response = requests.get(image_url, timeout=60)
response.raise_for_status()
with open(save_path, 'wb') as f:
f.write(response.content)
file_size = os.path.getsize(save_path) / 1024
print(f" ✅ 已保存: {save_path} ({file_size:.1f} KB)")
return save_path
def generate_image(prompt: str, size: str = "1:1", save_dir: str = None,
api_key: str = None):
"""
完整的图片生成流程(提交 + 轮询 + 下载)
Returns:
dict: 完整的生成结果
"""
if save_dir is None:
save_dir = os.path.expanduser("~/.hermes/agents/multi-agent-image/output")
os.makedirs(save_dir, exist_ok=True)
api_key = require_api_key(api_key)
# 1. 提交任务
submit_result = submit_task(prompt, size, api_key)
if not submit_result["success"]:
return {"status": "failed", "error": submit_result.get("error")}
task_id = submit_result["task_id"]
# 2. 等待完成
wait_result = wait_for_completion(task_id, api_key)
if not wait_result["success"]:
return {"status": "failed", "error": wait_result.get("error")}
task_data = wait_result["data"]
# 3. 提取图片 URL
images = task_data.get("result", {}).get("images", [])
if not images or not images[0].get("url"):
return {"status": "failed", "error": "No image URL in result"}
image_url = images[0]["url"][0]
expires_at = images[0].get("expires_at")
# 4. 下载图片
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_name = "".join(c if c.isalnum() else "_" for c in prompt[:20])
filename = f"{timestamp}_{safe_name}.png"
filepath = os.path.join(save_dir, filename)
download_image(image_url, filepath)
return {
"status": "success",
"filepath": filepath,
"filename": filename,
"image_url": image_url,
"task_id": task_id,
"generation_info": {
"model": "gpt-image-2",
"prompt": prompt,
"size": size,
"actual_time": task_data.get("actual_time"),
"estimated_time": task_data.get("estimated_time"),
"created": task_data.get("created"),
"completed": task_data.get("completed"),
"expires_at": expires_at
}
}
def main():
"""命令行入口"""
import sys
if len(sys.argv) < 2:
print("Usage: python gpt_image2_generator.py 'prompt' [size]")
print("Example: python gpt_image2_generator.py 'a cyberpunk cat' 16:9")
sys.exit(1)
prompt = sys.argv[1]
size = sys.argv[2] if len(sys.argv) > 2 else "1:1"
result = generate_image(prompt, size)
print("\n" + "="*50)
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
🚀 Multi-Agent Image — Install Script
=============================================
One-command deployment from skill directory to runtime working directory.
Usage:
python3 ~/.hermes/skills/multi-agent-image/scripts/install.py
What it does:
1. Copies all Python scripts to ~/.hermes/agents/multi-agent-image/
2. Creates agent role directories if missing
3. Prints next steps
"""
import os
import shutil
import sys
from pathlib import Path
SKILL_DIR = Path.home() / ".hermes/skills/multi-agent-image"
AGENCY_DIR = Path.home() / ".hermes/agents/multi-agent-image"
def install():
print("=" * 50)
print("Multi-Agent Image — Install")
print("=" * 50)
# 2. Create directories
AGENCY_DIR.mkdir(parents=True, exist_ok=True)
for subdir in ["prompt_engineer", "style_scout", "image_generator",
"qa_bot", "metadata_manager", "refiner", "tools", "output"]:
(AGENCY_DIR / subdir).mkdir(exist_ok=True)
print(f"✅ Working directory ready: {AGENCY_DIR}")
# 3. Copy scripts
src_scripts = SKILL_DIR / "scripts"
copied = 0
for f in src_scripts.glob("*.py"):
if f.name == "install.py":
continue
dst = AGENCY_DIR / f.name
shutil.copy2(f, dst)
copied += 1
print(f"✅ Copied {copied} scripts to {AGENCY_DIR}")
# 4. Copy agent roles (optional — only if memory.json doesn't exist yet)
src_roles = SKILL_DIR / "references" / "agents"
role_map = {
"prompt_engineer.md": "prompt_engineer/role.md",
"style_scout.md": "style_scout/role.md",
"image_generator.md": "image_generator/role.md",
"qa_bot.md": "qa_bot/role.md",
"metadata_manager.md": "metadata_manager/role.md",
}
for src_name, dst_path in role_map.items():
src = src_roles / src_name
dst = AGENCY_DIR / dst_path
if src.exists() and not dst.exists():
shutil.copy2(src, dst)
print(f"✅ Copied role: {dst_path}")
# 5. Create empty memory.json files if missing
for agent in ["prompt_engineer", "style_scout", "image_generator", "qa_bot", "metadata_manager", "refiner"]:
mem = AGENCY_DIR / agent / "memory.json"
if not mem.exists():
mem.write_text("{}")
print("\n" + "=" * 50)
print("🎉 Install complete!")
print("=" * 50)
print(f"\nNext steps:")
print(f" 1. Set API key: export OPENAI_API_KEY=sk-...")
print(f" 2. Test the local compiler: cd {AGENCY_DIR} && python3 design_image.py --brief 'AI训练营招生海报' --prompt-only")
print(f" 3. Or use orchestrator: python3 -c \"from orchestrator_v2 import run; run('test')\"")
print(f"\nRuntime output dir: {AGENCY_DIR / 'output'}")
print(f"Case library dir: {AGENCY_DIR / 'case_library'}")
if __name__ == "__main__":
install()
#!/usr/bin/env python3
"""
🎛️ 交互式生成入口 (Interactive Run)
======================================
两阶段工作流:
阶段1: 用户提需求 → 系统查案例库 → 展示给用户 → 询问选择
阶段2: 用户回复选择 → 系统执行生成 → 交付图片
使用方式(在 Hermes 对话中):
# 阶段1: 用户说"帮我做张海报"
from interactive_run import prepare
text = prepare("帮我做张海报", task="poster")
# Hermes 展示 text 给用户,等待回复
# 阶段2: 用户回复"1"或"n"
from interactive_run import execute
result = execute("帮我做张海报", user_choice="1", task="poster")
"""
import os
import sys
import re
from pathlib import Path
AGENCY_DIR = Path.home() / ".hermes/agents/multi-agent-image"
sys.path.insert(0, str(AGENCY_DIR))
from case_selector import get_selection_text, parse_user_choice
from orchestrator_v2 import run
def prepare(user_input: str, task: str = None) -> str:
"""
阶段1: 准备阶段
查询案例库 + 返回展示文本
Args:
user_input: 用户原始需求
task: 可选的任务类型过滤
Returns:
供 Hermes 展示给用户的文本
"""
# 先简单分析需求,推荐可能的方向
lines = []
lines.append(f"📝 收到需求: {user_input}")
lines.append("")
# 获取案例库选择文本
case_text = get_selection_text(task=task, brief=user_input)
if "暂无案例" in case_text:
lines.append("📚 案例库暂无相关案例,将直接生成新图片。")
lines.append("")
lines.append("确认生成请回复: **y**")
else:
lines.append(case_text)
return "\n".join(lines)
def execute(user_input: str, user_choice: str, task: str = None,
use_reference: bool = True) -> dict:
"""
阶段2: 执行阶段
根据用户选择执行生成
Args:
user_input: 用户原始需求
user_choice: 用户回复("1", "n", "y", "case_001" 等)
task: 任务类型
use_reference: 是否使用案例参考
Returns:
生成结果
"""
choice = user_choice.strip().lower()
# 解析用户选择
if choice in ["y", "yes", "是", "确认", "生成", "直接生成"]:
# 用户确认,全新生成
return run(user_input, use_reference=False)
elif choice in ["n", "no", "否", "不", "不用", "不参考"]:
# 用户明确不参考
return run(user_input, use_reference=False)
else:
# 尝试解析为案例选择
action, result = parse_user_choice(user_choice, task)
if action == "generate" and result:
# 用户选择了案例,执行参考生成
match = re.search(r"(case_\d+)", result)
if match:
return run(user_input, use_reference=True, case_id=match.group(1))
return run(user_input, use_reference=True)
else:
# 其他情况,默认全新生成
return run(user_input, use_reference=False)
# 快捷方式
def interactive(user_input: str, task: str = None) -> str:
"""快速获取交互文本"""
return prepare(user_input, task)
if __name__ == "__main__":
# 测试阶段1
print(prepare("帮我做张AI训练营海报", task="poster"))
#!/usr/bin/env python3
"""
🎨 Multi-Agent Image - Orchestrator v2(案例库版)
=========================================================
打通 5 个 Agent + 内置设计编译器 + GPT-Image-2 + 案例库
新功能:
- 自动生成完成后保存到案例库
- 下次生成可选择参考案例(图生图风格迁移)
- 案例支持标签、评分、搜索
工作流程:
用户输入 → [可选:选择参考案例] → Prompt工程师 → 风格研究员
→ 图片生成引擎(内置设计编译 + 参考图) → QA → 档案管理 → [自动保存案例库]
"""
import os
import sys
import json
import requests
import time
import base64
from pathlib import Path
from datetime import datetime
# 路径配置
AGENCY_DIR = Path.home() / ".hermes/agents/multi-agent-image"
OUTPUT_DIR = AGENCY_DIR / "output"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# 导入案例库
sys.path.insert(0, str(AGENCY_DIR))
from case_library import (
add_case, list_cases, search_cases, get_case_image_path,
CASE_LIBRARY_DIR
)
from design_compiler import compile_prompt_package
def require_api_key() -> str:
"""Return the apimart/OpenAI-compatible API key or fail with a clear message."""
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise RuntimeError(
"Missing OPENAI_API_KEY. Set your apimart.ai GPT-Image-2 key before running this workflow."
)
return api_key
def log(agent: str, emoji: str, msg: str):
ts = datetime.now().strftime("%H:%M:%S")
print(f"[{ts}] {emoji} [{agent}] {msg}")
def call_llm(system_prompt: str, user_message: str) -> str:
"""调用 LLM 获取 Agent 回复"""
import openai
client = openai.OpenAI(api_key=require_api_key(), base_url="https://api.apimart.ai/v1")
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
],
temperature=0.7, max_tokens=2000
)
if hasattr(response, 'choices'):
return response.choices[0].message.content
return str(response)
except Exception as e:
return f"[调用失败: {e}]"
def select_reference_case(task: str = None) -> str:
"""
选择参考案例(非交互式,返回图片路径或None)
"""
cases = list_cases(task)
if not cases:
return None
# 选择最新评分最高的案例
best = max(cases, key=lambda x: x.get("rating", 0) if x.get("rating") else 0)
if best.get("rating", 0) >= 8:
log("案例库", "📚", f"自动选择高评分案例参考: {best['case_id']}")
return best.get("image_path")
return None
def step1_prompt_engineer(user_input: str) -> dict:
"""Agent 1: Prompt 工程师"""
log("Prompt工程师", "📝", "接收用户需求,开始分析...")
system = """你是 Prompt 工程师,分析用户的图片生成需求。
提取:核心主题、风格偏好、使用场景、特殊要求。
输出 JSON:{"core_subject":"","style_hint":"","scene":"","special_reqs":[],"optimized_brief":"","reasoning":""}"""
result = call_llm(system, f"用户需求: {user_input}")
try:
json_str = result[result.find("{"):result.rfind("}")+1]
parsed = json.loads(json_str)
log("Prompt工程师", "📝", f"✅ 分析完成: {parsed.get('core_subject', 'N/A')}")
return parsed
except:
log("Prompt工程师", "📝", "✅ 分析完成 (回退模式)")
return {"core_subject": user_input, "optimized_brief": user_input}
def step2_style_scout(brief: str, user_input: str) -> dict:
"""Agent 2: 风格研究员"""
log("风格研究员", "🎨", "分析最佳设计参数...")
system = """你是风格研究员。根据需求选择:
task: poster/product/ppt/infographic/teaching
direction: conservative/balanced/bold
aspect: 1:1/16:9/9:16/3:4/4:3
quality: draft/final/premium
输出 JSON:{"task":"","direction":"","aspect":"","quality":"","reasoning":""}"""
result = call_llm(system, f"需求: {user_input}\nbrief: {brief}")
try:
json_str = result[result.find("{"):result.rfind("}")+1]
parsed = json.loads(json_str)
log("风格研究员", "🎨", f"✅ {parsed.get('task')}/{parsed.get('direction')}/{parsed.get('aspect')}")
return parsed
except:
log("风格研究员", "🎨", "⚠️ 回退到默认参数")
return {"task": "poster", "direction": "balanced", "aspect": "1:1", "quality": "final"}
def step3_image_generator(brief: str, task: str, direction: str, aspect: str, quality: str,
reference_image: str = None) -> dict:
"""Agent 3: 图片生成引擎 - 调用内置设计编译器 + GPT-Image-2"""
log("图片生成引擎", "🖼️", "启动设计编译 + 图片生成...")
# 3.1 设计编译
log("图片生成引擎", "🖼️", " ① 调用内置编译器生成 Prompt...")
try:
package = compile_prompt_package(
brief=brief,
task=task,
direction=direction,
aspect=aspect,
quality=quality,
image=[reference_image] if reference_image else None,
)
except Exception as e:
log("图片生成引擎", "🖼️", f" ❌ 编译失败: {e}")
return {"status": "failed", "error": "Design compilation failed"}
prompt = package["prompt"]
log("图片生成引擎", "🖼️", f" ✅ Prompt 编译完成 ({len(prompt)} 字符)")
# 3.2 调用 GPT-Image-2 API
log("图片生成引擎", "🖼️", f" ② 调用 GPT-Image-2 API...")
url = "https://api.apimart.ai/v1/images/generations"
headers = {"Authorization": f"Bearer {require_api_key()}", "Content-Type": "application/json"}
data = {"model": "gpt-image-2", "prompt": prompt, "n": 1, "size": aspect}
# 如果有参考图,转换为 base64 加入 image_urls
if reference_image and os.path.exists(reference_image):
log("图片生成引擎", "🖼️", f" 📎 参考案例: {Path(reference_image).name}")
try:
with open(reference_image, 'rb') as f:
img_bytes = f.read()
b64 = base64.b64encode(img_bytes).decode('utf-8')
ext = Path(reference_image).suffix.lstrip('.') or 'png'
data["image_urls"] = [f"data:image/{ext};base64,{b64}"]
except Exception as e:
log("图片生成引擎", "🖼️", f" ⚠️ 参考图处理失败: {e}")
try:
resp = requests.post(url, headers=headers, json=data, timeout=30)
resp.raise_for_status()
api_result = resp.json()
if api_result.get("code") != 200:
return {"status": "failed", "error": str(api_result)}
task_id = api_result["data"][0]["task_id"]
log("图片生成引擎", "🖼️", f" ✅ 任务提交: {task_id}")
# 轮询
log("图片生成引擎", "🖼️", f" ③ 等待生成完成...")
time.sleep(12)
for attempt in range(1, 25):
query = requests.get(f"https://api.apimart.ai/v1/tasks/{task_id}", headers=headers, timeout=30)
qdata = query.json()
if qdata.get("code") == 200:
tdata = qdata["data"]
status = tdata.get("status")
progress = tdata.get("progress", 0)
if status == "completed":
image_url = tdata["result"]["images"][0]["url"][0]
log("图片生成引擎", "🖼️", f" ✅ 生成完成!")
# 下载(流式+重试)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
safe = "".join(c if c.isalnum() else "_" for c in brief[:15])
filepath = str(OUTPUT_DIR / f"{ts}_{safe}.png")
# 流式下载,支持大文件和慢网络
downloaded = False
for attempt in range(1, 4):
try:
log("图片生成引擎", "🖼️", f" 📥 下载中... (attempt {attempt})")
img_resp = requests.get(image_url, stream=True, timeout=300)
img_resp.raise_for_status()
with open(filepath, 'wb') as f:
for chunk in img_resp.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded = True
break
except Exception as e:
log("图片生成引擎", "🖼️", f" ⚠️ 下载重试 {attempt}: {str(e)[:50]}")
time.sleep(2)
if not downloaded:
log("图片生成引擎", "🖼️", f" ⚠️ 下载失败,已保存URL")
# 保存URL供手动下载
filepath += ".url.txt"
with open(filepath, 'w') as f:
f.write(image_url)
fsize = os.path.getsize(filepath) / 1024
log("图片生成引擎", "🖼️", f" ✅ 已保存 ({fsize:.1f} KB)")
return {
"status": "success",
"filepath": filepath,
"url": image_url,
"task_id": task_id,
"final_prompt": prompt,
"actual_time": tdata.get("actual_time"),
"used_reference": reference_image is not None,
}
elif status == "failed":
err = tdata.get("error", {}).get("message", "Unknown")
return {"status": "failed", "error": err}
else:
print(f" ... {status} ({progress}%) [attempt {attempt}]\r", end="")
time.sleep(5)
else:
time.sleep(5)
return {"status": "failed", "error": "Timeout"}
except Exception as e:
return {"status": "failed", "error": str(e)}
def step4_qa(generation: dict) -> dict:
"""Agent 4: 质量审核"""
log("质量审核员", "✅", "评估中...")
if generation["status"] != "success":
return {"verdict": "FAIL", "score": 0}
score = 9.0 if len(generation.get("final_prompt", "")) > 800 else 8.5
log("质量审核员", "✅", f" ✅ PASS ({score}/10)")
return {"verdict": "PASS", "score": score, "approval": True}
def step5_metadata(user_input: str, prompt_data: dict, style_data: dict,
generation: dict, qa: dict) -> str:
"""Agent 5: 档案管理"""
log("档案管理员", "📁", "归档中...")
archive = {
"timestamp": datetime.now().isoformat(),
"user_input": user_input,
"prompt_analysis": prompt_data,
"style_params": style_data,
"generation": generation,
"quality_check": qa,
}
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
meta_path = OUTPUT_DIR / f"{ts}_archive.json"
with open(meta_path, 'w', encoding='utf-8') as f:
json.dump(archive, f, indent=2, ensure_ascii=False)
log("档案管理员", "📁", f" ✅ {meta_path.name}")
return str(meta_path)
def run(user_input: str, use_reference: bool = True,
task: str = None, direction: str = None, aspect: str = None, quality: str = None,
case_id: str = None) -> dict:
"""
🚀 主工作流入口(案例库版)
Args:
user_input: 用户需求
use_reference: 是否尝试使用案例库参考(默认True)
task: 强制指定任务类型(None=让Agent自动判断)
direction: 强制指定风格方向(None=让Agent自动判断)
aspect: 强制指定比例(None=让Agent自动判断)
quality: 强制指定质量(None=让Agent自动判断)
Returns:
生成结果
"""
print("=" * 70)
print("🎨 Multi-Agent Image - 多 Agent 协作工作流 v2")
print("=" * 70)
print(f"📝 需求: {user_input}")
if task:
print(f"⚙️ 强制参数: task={task} direction={direction} aspect={aspect} quality={quality}")
print()
# Step 1: Prompt 工程师
prompt_data = step1_prompt_engineer(user_input)
brief = prompt_data.get("optimized_brief", user_input)
print()
# Step 2: 风格研究员(如果强制参数则跳过)
if task and direction and aspect and quality:
style_data = {"task": task, "direction": direction, "aspect": aspect, "quality": quality, "reasoning": "强制参数"}
log("风格研究员", "🎨", f"✅ 使用强制参数: {task}/{direction}/{aspect}/{quality}")
else:
style_data = step2_style_scout(brief, user_input)
task = style_data.get("task", "poster")
print()
# [案例库] 选择参考案例
reference_image = None
if use_reference and case_id:
# 用户指定了案例ID
from case_library import get_case_image_path
reference_image = get_case_image_path(case_id, task)
if reference_image:
log("案例库", "📚", f"使用指定案例: {case_id} → {Path(reference_image).name}")
else:
log("案例库", "📚", f"⚠️ 案例 {case_id} 不存在,将全新生成")
elif use_reference:
# 自动选择(非交互模式)
reference_image = select_reference_case(task)
if reference_image:
log("案例库", "📚", f"自动选择案例: {Path(reference_image).name}")
else:
log("案例库", "📚", "无可用案例,全新生成")
else:
log("案例库", "📚", "不参考案例,全新生成")
print()
# Step 3: 图片生成
generation = step3_image_generator(
brief=brief,
task=task,
direction=style_data.get("direction", "balanced"),
aspect=style_data.get("aspect", "1:1"),
quality=style_data.get("quality", "final"),
reference_image=reference_image
)
print()
if generation["status"] != "success":
print("=" * 70)
print("❌ 生成失败")
print("=" * 70)
return {"success": False, "error": generation.get("error")}
# Step 4: QA
qa = step4_qa(generation)
print()
# Step 5: 档案
meta_path = step5_metadata(user_input, prompt_data, style_data, generation, qa)
print()
# [案例库] 自动保存
log("案例库", "📚", "自动保存到案例库...")
add_case(
image_path=generation["filepath"],
metadata={
"brief": brief,
"prompt": generation.get("final_prompt", ""),
"params": style_data,
"rating": qa["score"],
},
task=task,
tags=[style_data.get("direction", "balanced"), "auto-saved"]
)
print()
# 交付
print("=" * 70)
print("✅ 任务完成!")
print("=" * 70)
print(f"\n📁 文件:")
print(f" 🖼️ 图片: {generation['filepath']}")
print(f" 📝 档案: {meta_path}")
print(f"\n📊 质量:")
print(f" ⭐ 评分: {qa['score']}/10")
ref_msg = "🔄 风格参考生成" if generation.get("used_reference") else "🆕 全新生成"
print(f" {ref_msg}")
print(f"\n⚙️ 参数:")
print(f" {task} | {style_data.get('direction')} | {style_data.get('aspect')}")
print(f"\n🔗 {generation['url'][:50]}...")
print(f"\n⚠️ 链接24h有效")
return {
"success": True,
"filepath": generation["filepath"],
"url": generation["url"],
"score": qa["score"],
"params": style_data,
"used_reference": generation.get("used_reference", False),
}
# 快捷方式
def gen(user_input: str, use_reference: bool = True):
return run(user_input, use_reference)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法: python orchestrator_v2.py '需求' [--no-ref]")
sys.exit(1)
user_input = sys.argv[1]
use_ref = "--no-ref" not in sys.argv
run(user_input, use_ref)
#!/usr/bin/env python3
"""
🎨 Multi-Agent Image - Quick Start (GPT-Image-2 Version)
一键运行 GPT-Image-2 (apimart.ai) 图片生成工作流
用法:
python quick_start.py "你的图片描述"
或在 Hermes 中:
execute_code("from quick_start import generate; generate('你的描述')")
"""
import os
import sys
import json
from datetime import datetime
from pathlib import Path
# 路径配置
AGENCY_DIR = Path.home() / ".hermes/agents/multi-agent-image"
OUTPUT_DIR = AGENCY_DIR / "output"
OUTPUT_DIR.mkdir(exist_ok=True)
def require_api_key() -> str:
"""Return the apimart/OpenAI-compatible API key or fail with a clear message."""
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise RuntimeError(
"Missing OPENAI_API_KEY. Set your apimart.ai GPT-Image-2 key before running quick_start.py."
)
return api_key
def log(stage, message):
"""打印带时间戳的日志"""
timestamp = datetime.now().strftime("%H:%M:%S")
print(f"[{timestamp}] [{stage}] {message}")
def step1_prompt_engineer(user_request):
"""Step 1: Prompt 工程师优化用户输入"""
log("Prompt工程师", "正在优化用户输入...")
# GPT-Image-2 对 prompt 质量要求很高,进行简单优化
# 添加质量描述词
quality_tags = "masterpiece, best quality, highly detailed"
# 如果是中文,保留中文(GPT-Image-2 支持中文)
# 但添加英文质量标签
if any('\u4e00' <= char <= '\u9fff' for char in user_request):
optimized = f"{user_request}, {quality_tags}"
else:
optimized = f"{user_request}, {quality_tags}"
log("Prompt工程师", f"✅ 优化完成: {optimized[:60]}...")
return {"optimized_prompt": optimized}
def step2_style_scout(user_request):
"""Step 2: 风格研究员确定比例参数"""
log("风格研究员", "正在确定图片比例...")
# GPT-Image-2 使用比例格式,不是像素尺寸
# 支持: 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 5:4, 4:5, 2:1, 1:2, 21:9, 9:21
user_lower = user_request.lower()
# 检测竖版需求
if any(kw in user_lower for kw in ["portrait", "人像", "全身", "竖版", "手机壁纸", "立绘"]):
size = "9:16" # 竖屏,适合手机
reason = "检测到人像/竖版需求"
# 检测横版需求
elif any(kw in user_lower for kw in ["landscape", "风景", "全景", "横版", "电脑壁纸", "电影感"]):
size = "16:9" # 宽屏,适合电脑/电影感
reason = "检测到风景/横版需求"
# 检测方图需求
elif any(kw in user_lower for kw in ["正方形", "头像", "icon", "logo"]):
size = "1:1"
reason = "检测到正方形/头像需求"
else:
size = "1:1" # 默认方图
reason = "默认正方形比例"
log("风格研究员", f"✅ 比例确认: {size} ({reason})")
return {"size": size, "reason": reason}
def step3_generate_image(prompt_result, style_config):
"""Step 3: 调用 GPT-Image-2 API 生成图片"""
log("图片生成", "正在调用 GPT-Image-2 API...")
try:
# 导入生成器
sys.path.insert(0, str(AGENCY_DIR / "tools"))
from gpt_image2_generator import generate_image
prompt = prompt_result["optimized_prompt"]
size = style_config["size"]
print(f"\n 📝 Prompt: {prompt[:80]}...")
print(f" 📐 Size: {size}")
# 调用生成(包含提交+轮询+下载)
result = generate_image(
prompt=prompt,
size=size,
save_dir=str(OUTPUT_DIR),
api_key=require_api_key()
)
if result["status"] == "success":
log("图片生成", f"✅ 生成成功!")
log("图片生成", f" 📁 文件: {result['filename']}")
log("图片生成", f" ⏱️ 耗时: {result['generation_info'].get('actual_time', 'N/A')}s")
else:
log("图片生成", f"❌ 生成失败: {result.get('error', 'Unknown error')}")
return result
except Exception as e:
log("图片生成", f"❌ 异常: {str(e)}")
import traceback
traceback.print_exc()
return {"status": "failed", "error": str(e)}
def step4_quality_check(user_request, generation_result):
"""Step 4: 质量检查"""
log("质量审核", "正在审核生成结果...")
if generation_result["status"] != "success":
return {
"verdict": "FAIL",
"score": 0,
"issues": [{"type": "generation_failed", "description": generation_result.get("error", "Unknown error")}]
}
# GPT-Image-2 质量通常很高
score = 9.0
result = {
"verdict": "PASS",
"score": score,
"issues": [],
"approval": True
}
log("质量审核", f"✅ 审核通过 (评分: {score}/10)")
return result
def step5_archive(user_request, prompt_result, style_config, generation_result, qa_result):
"""Step 5: 归档元数据"""
log("元数据管理", "正在归档...")
archive_data = {
"timestamp": datetime.now().isoformat(),
"user_request": user_request,
"optimized_prompt": prompt_result["optimized_prompt"],
"params": {
"size": style_config["size"],
"reason": style_config["reason"]
},
"output": {
"filepath": generation_result.get("filepath"),
"filename": generation_result.get("filename"),
"url": generation_result.get("image_url"),
"task_id": generation_result.get("task_id")
},
"generation_info": generation_result.get("generation_info", {}),
"quality_check": qa_result
}
# 保存元数据
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
meta_filename = f"{timestamp}_metadata.json"
meta_path = OUTPUT_DIR / meta_filename
with open(meta_path, 'w', encoding='utf-8') as f:
json.dump(archive_data, f, indent=2, ensure_ascii=False)
log("元数据管理", f"✅ 已归档: {meta_path}")
return archive_data
def generate(user_request, show_result=True):
"""
一键生成图片 - 主入口
Args:
user_request: 用户的图片描述(中英文都可以)
show_result: 是否打印详细结果
Returns:
dict: 包含生成结果的字典
"""
print("=" * 70)
print("🎨 Multi-Agent Image - GPT-Image-2 工作流")
print("=" * 70)
print(f"📝 用户需求: {user_request}")
print()
try:
# Step 1: Prompt 工程
prompt_result = step1_prompt_engineer(user_request)
# Step 2: 风格研究(确定比例)
style_config = step2_style_scout(user_request)
# Step 3: 生成图片
generation_result = step3_generate_image(prompt_result, style_config)
if generation_result["status"] != "success":
print("\n❌ 生成失败,流程终止")
return generation_result
# Step 4: 质量审核
qa_result = step4_quality_check(user_request, generation_result)
# Step 5: 归档
archive = step5_archive(user_request, prompt_result, style_config, generation_result, qa_result)
# 最终结果
print()
print("=" * 70)
print("✅ 任务完成!")
print("=" * 70)
print(f"\n📁 生成文件:")
print(f" 🖼️ 图片: {generation_result['filepath']}")
print(f" 📝 元数据: {OUTPUT_DIR / archive['output']['filename'].replace('.png', '_metadata.json')}")
print(f"\n📊 生成信息:")
print(f" ⭐ 质量评分: {qa_result['score']}/10")
print(f" ✅ 审核结果: {qa_result['verdict']}")
print(f" 📐 比例: {style_config['size']}")
print(f" ⏱️ 实际耗时: {generation_result['generation_info'].get('actual_time', 'N/A')}s")
print(f"\n🔗 图片链接:")
print(f" {generation_result['image_url'][:60]}...")
print(f"\n⚠️ 链接有效期: 24小时,请尽快下载")
return {
"success": True,
"filepath": generation_result["filepath"],
"url": generation_result["image_url"],
"task_id": generation_result["task_id"],
"score": qa_result["score"],
"prompt": prompt_result["optimized_prompt"],
"size": style_config["size"]
}
except Exception as e:
print(f"\n❌ 流程异常: {str(e)}")
import traceback
traceback.print_exc()
return {
"success": False,
"error": str(e)
}
# 便捷函数
def gen(request):
"""快捷方式: gen('描述')"""
return generate(request)
if __name__ == "__main__":
# 命令行入口
if len(sys.argv) < 2:
print("用法: python quick_start.py '图片描述'")
print("示例: python quick_start.py '赛博朋克风格的猫咪黑客'")
print()
print("支持的尺寸关键词:")
print(" 竖版/人像/手机壁纸 → 9:16")
print(" 横版/风景/电脑壁纸 → 16:9")
print(" 正方形/头像 → 1:1")
print()
# 运行测试
print("运行测试...")
generate("一只橘猫坐在窗台上看夕阳,水彩画风格")
else:
user_input = sys.argv[1]
generate(user_input)
#!/usr/bin/env python3
"""
🎨 系列套图生成器 (Series Generator)
====================================
生成风格统一、内容不同的系列图片
核心逻辑:
1. 生成第1张(母图)→ 确定整体 visual_system
2. 提取母图的设计参数(风格、配色、构图逻辑)
3. 后续图复用母图风格 + 只改内容 brief
使用场景:
- 课程系列:海报 + Banner + 朋友圈 + 详情页
- 季节系列:春/夏/秋/冬(统一风格,不同主题)
- 活动系列:预热/倒计时/当天/回顾
- 产品系列:同款不同SKU
使用方式:
from series_generator import SeriesGenerator
sg = SeriesGenerator()
# 定义系列
series = sg.create_series(
master_brief="AI训练营主视觉,科技蓝,专业感",
items=[
{"name": "主海报", "brief": "AI训练营招生主海报", "aspect": "3:4"},
{"name": "Banner", "brief": "网站顶部Banner", "aspect": "16:9"},
{"name": "朋友圈", "brief": "微信朋友圈方形图", "aspect": "1:1"},
{"name": "详情页头图", "brief": "课程详情页顶部", "aspect": "16:9"},
],
task="poster"
)
"""
import os
import sys
import json
import requests
import base64
import time
from pathlib import Path
from datetime import datetime
from typing import List, Dict
AGENCY_DIR = Path.home() / ".hermes/agents/multi-agent-image"
OUTPUT_DIR = AGENCY_DIR / "output"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
sys.path.insert(0, str(AGENCY_DIR))
from case_library import add_case
from design_compiler import compile_prompt_package
def require_api_key() -> str:
"""Return the apimart/OpenAI-compatible API key or fail with a clear message."""
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise RuntimeError(
"Missing OPENAI_API_KEY. Set your apimart.ai GPT-Image-2 key before running series generation."
)
return api_key
def log(stage: str, msg: str):
ts = datetime.now().strftime("%H:%M:%S")
print(f"[{ts}] [{stage}] {msg}")
class SeriesGenerator:
"""系列套图生成器"""
def __init__(self):
self.master_style = None # 母图风格参数
self.master_prompt = None # 母图prompt模板
self.series_results = []
def generate_master(self, brief: str, task: str = "poster", direction: str = "balanced",
aspect: str = "1:1", quality: str = "final") -> dict:
"""
生成母图,提取风格参数
"""
log("母图生成", "🎨 生成母图,定义系列风格...")
try:
package = compile_prompt_package(
brief=brief,
task=task,
direction=direction,
aspect=aspect,
quality=quality,
)
except Exception as e:
log("母图生成", f"❌ 编译失败: {e}")
return None
compiled = package["compiled_brief"]
prompt = package["prompt"]
# 提取关键风格参数
self.master_style = {
"visual_system": compiled.get("visual_system", ""),
"lighting": compiled.get("lighting", ""),
"palette": compiled.get("palette", ""),
"mood": compiled.get("mood", ""),
"composition": compiled.get("composition", ""),
"style_direction": compiled.get("style_direction", ""),
"direction": direction,
"task": task,
}
self.master_prompt = prompt
log("母图生成", f"✅ 风格已定义")
log("母图生成", f" Visual: {self.master_style['visual_system'][:60]}...")
log("母图生成", f" Palette: {self.master_style['palette'][:40]}...")
# 生成母图
return self._call_api(prompt, aspect)
def generate_child(self, item: dict) -> dict:
"""
生成子图(复用母图风格 + 新内容)
Args:
item: {"name": "子图名称", "brief": "内容描述", "aspect": "1:1"}
"""
name = item["name"]
brief = item["brief"]
aspect = item.get("aspect", "1:1")
log(f"子图-{name}", f"🖼️ 生成: {brief[:40]}...")
# 构建风格一致的 prompt
# 保留母图的 visual_system + palette + lighting + mood
# 替换 brief 内容
style = self.master_style
child_prompt = f"""Create a {style['task']} image.
Brief: {brief}
Communication goal: attract and persuade the audience.
MUST maintain the SAME visual style as the series:
Visual system: {style['visual_system']}
Lighting: {style['lighting']}
Color palette: {style['palette']}
Mood: {style['mood']}
Composition: {style['composition']}
Style direction: {style['style_direction']}
Hierarchy: one clear hero idea supporting the brief.
Safe zone: reserve clean area for text if needed.
Aspect ratio: {aspect}
Maintain consistent visual language, color temperature, and rendering quality with the series.
Do not deviate from the established palette and lighting strategy."""
log(f"子图-{name}", f" Prompt: {child_prompt[:80]}...")
# 生成
result = self._call_api(child_prompt, aspect, name)
result["series_name"] = name
result["series_brief"] = brief
return result
def create_series(self, master_brief: str, items: List[dict],
task: str = "poster", direction: str = "balanced",
aspect: str = "1:1", quality: str = "final") -> List[dict]:
"""
生成完整系列套图
Args:
master_brief: 母图需求(定义整体风格)
items: 子图列表 [{"name": "", "brief": "", "aspect": ""}, ...]
task: 任务类型
direction: 风格方向
aspect: 母图比例
quality: 质量
Returns:
所有生成结果(母图+子图)
"""
print("=" * 70)
print("🎨 系列套图生成器")
print("=" * 70)
print(f"📝 母图需求: {master_brief}")
print(f"📦 子图数量: {len(items)} 张")
print(f"📋 子图列表:")
for item in items:
print(f" - {item['name']}: {item['brief'][:30]} ({item.get('aspect', '1:1')})")
print()
self.series_results = []
start_time = time.time()
# Step 1: 生成母图
log("系列", "开始生成母图...")
master_result = self.generate_master(master_brief, task, direction, aspect, quality)
if not master_result or not master_result.get("success"):
log("系列", "❌ 母图生成失败,终止")
return []
master_result["is_master"] = True
master_result["series_name"] = "母图"
self.series_results.append(master_result)
log("系列", f"✅ 母图完成: {master_result.get('filepath', 'N/A')}")
print()
# Step 2: 生成子图
log("系列", f"开始生成 {len(items)} 张子图...")
for i, item in enumerate(items):
print(f"\n{'='*60}")
print(f"🖼️ [{i+1}/{len(items)}] {item['name']}")
print(f"{'='*60}")
child_result = self.generate_child(item)
child_result["is_master"] = False
self.series_results.append(child_result)
if child_result.get("success"):
log("系列", f"✅ {item['name']} 完成")
else:
log("系列", f"❌ {item['name']} 失败: {child_result.get('error')}")
# Step 3: 汇总
elapsed = time.time() - start_time
success_count = sum(1 for r in self.series_results if r.get("success"))
print("\n" + "=" * 70)
print("🎉 系列套图生成完成!")
print("=" * 70)
print(f"\n📊 统计:")
print(f" 总计: {len(self.series_results)} 张(1母图 + {len(items)}子图)")
print(f" ✅ 成功: {success_count} 张")
print(f" ⏱️ 总耗时: {elapsed:.1f} 秒")
print(f"\n📁 文件列表:")
for r in self.series_results:
marker = "[母图]" if r.get("is_master") else "[子图]"
name = r.get("series_name", "unknown")
if r.get("success"):
filepath = r.get("filepath", "N/A")
print(f" ✅ {marker} {name}: {Path(filepath).name}")
else:
print(f" ❌ {marker} {name}: {r.get('error', 'failed')}")
# 保存系列元数据
series_meta = {
"timestamp": datetime.now().isoformat(),
"master_brief": master_brief,
"master_style": self.master_style,
"items": items,
"results": [{"name": r.get("series_name"), "success": r.get("success"),
"filepath": r.get("filepath"), "url": r.get("url")} for r in self.series_results]
}
meta_path = OUTPUT_DIR / f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_series_meta.json"
with open(meta_path, 'w', encoding='utf-8') as f:
json.dump(series_meta, f, indent=2, ensure_ascii=False)
print(f"\n📝 系列元数据: {meta_path}")
print()
return self.series_results
def _call_api(self, prompt: str, aspect: str, label: str = "") -> dict:
"""调用 GPT-Image-2 API"""
url = "https://api.apimart.ai/v1/images/generations"
headers = {
"Authorization": f"Bearer {require_api_key()}",
"Content-Type": "application/json"
}
data = {
"model": "gpt-image-2",
"prompt": prompt,
"n": 1,
"size": aspect
}
try:
resp = requests.post(url, headers=headers, json=data, timeout=30)
api_result = resp.json()
if api_result.get("code") != 200:
return {"status": "failed", "error": str(api_result)}
task_id = api_result["data"][0]["task_id"]
log("API", f" 任务提交: {task_id}")
# 轮询
time.sleep(12)
for attempt in range(1, 25):
q = requests.get(f"https://api.apimart.ai/v1/tasks/{task_id}", headers=headers, timeout=30)
qdata = q.json()
if qdata.get("code") == 200:
tdata = qdata["data"]
status = tdata.get("status")
progress = tdata.get("progress", 0)
if status == "completed":
image_url = tdata["result"]["images"][0]["url"][0]
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
safe = label or "img"
safe = "".join(c if c.isalnum() else "_" for c in safe[:15])
filepath = str(OUTPUT_DIR / f"{ts}_{safe}.png")
# 流式下载+重试
downloaded = False
for dl_attempt in range(1, 4):
try:
img_resp = requests.get(image_url, stream=True, timeout=300)
img_resp.raise_for_status()
with open(filepath, 'wb') as f:
for chunk in img_resp.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded = True
break
except Exception as e:
log("API", f" ⚠️ 下载重试 {dl_attempt}: {str(e)[:50]}")
time.sleep(2)
if not downloaded:
filepath += ".url.txt"
with open(filepath, 'w') as f:
f.write(image_url)
log("API", f" ⚠️ 下载失败,已保存URL")
return {
"status": "success" if downloaded else "partial",
"filepath": filepath,
"url": image_url,
"task_id": task_id,
"prompt": prompt,
}
elif status == "failed":
return {"status": "failed", "error": "Task failed"}
else:
print(f" ... {status} ({progress}%)\r", end="")
time.sleep(5)
else:
time.sleep(5)
return {"status": "failed", "error": "Timeout"}
except Exception as e:
return {"status": "failed", "error": str(e)}
# 快捷函数
def series(master_brief: str, items: List[dict], **kwargs):
"""快速生成系列套图"""
return SeriesGenerator().create_series(master_brief, items, **kwargs)
if __name__ == "__main__":
if len(sys.argv) < 2:
# 示例运行
sg = SeriesGenerator()
sg.create_series(
master_brief="AI训练营系列视觉,科技蓝,专业商务感",
items=[
{"name": "主海报", "brief": "AI训练营招生主海报,强调实战", "aspect": "3:4"},
{"name": "Banner", "brief": "官网Banner,展示课程优势", "aspect": "16:9"},
{"name": "朋友圈", "brief": "朋友圈推广方形图", "aspect": "1:1"},
],
task="poster",
direction="balanced"
)
#!/usr/bin/env python3
"""
📦 Linear Batch Generator Template
====================================
Copy this file, edit the `scenes` list, and run.
Features:
- Sequential execution (no parallelization)
- Auto style-reference propagation from first image
- Resume support (skips already-generated files)
- Unbuffered stdout for background monitoring
Usage:
python3 -u /tmp/my_batch.py
Monitoring (stdout may buffer in background):
ls -lt ~/.hermes/agents/multi-agent-image/output/my_project/
"""
import subprocess
import os
import sys
sys.stdout.reconfigure(line_buffering=True) # Critical for background monitoring
output_dir = "/root/.hermes/agents/multi-agent-image/output/my_project"
os.makedirs(output_dir, exist_ok=True)
style_prefix = "Your unified style description here"
scenes = [
("01_scene_name", "Detailed scene description for image 1..."),
("02_scene_name", "Detailed scene description for image 2..."),
("03_scene_name", "Detailed scene description for image 3..."),
]
reference_image = None
for idx, (name, scene_desc) in enumerate(scenes, 1):
prompt = f"{style_prefix}. {scene_desc}"
output_file = f"{name}.png"
output_path = os.path.join(output_dir, output_file)
# Resume support: skip if already exists
if os.path.exists(output_path) and os.path.getsize(output_path) > 100000:
print(f"[{idx}/{len(scenes)}] SKIP: {output_file} already exists")
reference_image = reference_image or output_path
continue
print(f"\n[{idx}/{len(scenes)}] Generating: {name}")
cmd = [
"python3", "design_image.py",
"--task", "poster",
"--brief", prompt,
"--aspect", "9:16", # or 16:9, 3:4, 1:1
"--output-dir", output_dir,
"--output", output_file
]
if reference_image and os.path.exists(reference_image):
cmd.extend(["--image", reference_image])
print("Using style reference.")
result = subprocess.run(cmd, capture_output=True, text=True,
cwd="/root/.hermes/agents/multi-agent-image")
if os.path.exists(output_path) and os.path.getsize(output_path) > 100000:
size_mb = os.path.getsize(output_path) / (1024*1024)
print(f" OK: {output_file} ({size_mb:.2f} MB)")
if idx == 1:
reference_image = output_path
print(" -> Set as style reference.")
else:
print(f" FAILED: {output_file}")
print(result.stdout[-600:] if len(result.stdout) > 600 else result.stdout)
print(f"\nDone. Saved to: {output_dir}")