
Image Gen
- 1 installs
- 6 repo stars
- Updated April 12, 2026
- yizhiyanhua-ai/agent-skills
Generates publication-quality images from text descriptions via the ZetaTechs API, engineering a structured seven-parameter prompt so the first generation is usable.
About
Turns a brief text description into a precise image prompt (medium, subject, traits, background, lighting, style, mood) and calls the ZetaTechs image API to save a local file. A developer uses it to create illustrations, hero images, and blog covers, especially book-illustration assets.
- Seven-parameter structured prompt methodology with a 40-word floor
- Delivers a local image file with optional public URL
Image Gen by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,200 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yizhiyanhua-ai/agent-skills --skill image-genAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 6 |
| Last updated | April 12, 2026 |
| Repository | yizhiyanhua-ai/agent-skills ↗ |
What it does
Generates publication-quality images from text descriptions via the ZetaTechs API, engineering a structured seven-parameter prompt so the first generation is usable.
Files
<!-- ╔══════════════════════════════════════════════════════╗ ║ 图片生成 Skill — 书稿插图专用 ║ ║ Powered by ZetaTechs (zetatechs.com) ║ ╚══════════════════════════════════════════════════════╝
使用前配置(一次性):
1. 前往 zetatechs.com 注册账号,获取 API Key 2. 在 ~/.zshrc 或 ~/.bashrc 中添加:
export OPENROUTER_API_KEY="你的 ZetaTechs API Key" export OPENAI_BASE_URL="https://api.zetatechs.com/v1"
3. 执行 source ~/.zshrc 使其生效 4. 确保已安装 Bun:curl -fsSL https://bun.sh/install | bash 5. 将本文件夹复制到 ~/.claude/skills/image-gen/
安全提醒:密钥只存环境变量,不进代码、不进 Git、不发聊天。 -->
Turn a text description into a publication-quality image, delivered as a local file with an optional public URL. The definition of success: the user describes what they see in their mind, and a matching image appears on disk — with a prompt engineered well enough that the first generation is usable, not the fifth.
「一句话变一张图,第一次就对」
= Prompt Engineering — The Work That Matters
The API call is trivial; the prompt is everything. A vague prompt produces a generic image. A structured prompt produces what you actually wanted. Your job is to transform the user's brief description into a precise visual specification before it touches the API.
以下是 the structured prompt methodology, adapted from production image generation workflows. Apply this framework to every generation:
== The Seven Parameters
Every image prompt resolves into seven parameters. The user typically provides 1-3 of them; you infer or create the rest. When a parameter is unspecified, make a creative choice that serves the user's intent — do not leave it blank or generic.
<parameters> 1. Medium — What art form? (digital painting, photograph, watercolor, 3D render, ink illustration, oil painting) 2. Subject — The main focus. Be concrete: not "a flower" but "a single violet blooming through cracked concrete" 3. Traits — Subject's visual characteristics: colors, pose, texture, viewing angle, expression, scale 4. Background — The setting. Complements the subject: "morning fog over a lake" or "clean white studio" 5. Lighting — The emotional driver: "soft golden hour sidelight" or "harsh overhead noon sun with deep shadows" 6. Style — Artistic DNA: "in the style of Studio Ghibli" or "editorial photography, Leica M11, f/1.4" 7. Mood — The feeling: "melancholic serenity" or "electric anticipation" </parameters>
以上是 the seven parameters. Not all seven need explicit mention in every prompt — sometimes mood is conveyed through lighting and color choices, sometimes style implies the medium. But all seven should be consciously considered.
== Prompt Construction
Assemble parameters into a single flowing description. Natural language outperforms keyword lists — the model understands "a watercolor painting of a violet flower blooming through cracked pavement, bathed in soft morning light, with mist rising from wet concrete" better than "watercolor, violet, cracked pavement, morning, mist, wet."
Prompt length floor: 40 words minimum — under 40 leaves too much to chance. No hard ceiling: every token that carries non-redundant visual information improves output. The risk with long prompts is not length but contradiction — if two instructions conflict, the model resolves unpredictably. Keep every token signal-bearing; prune only noise, never information.
== Enhancement Examples
<examples> User says: "紫罗兰花在晨光中" Enhanced prompt: "A watercolor painting of a cluster of violet flowers catching the first light of dawn, petals translucent with dew, soft golden rays filtering through morning mist, shallow depth of field with bokeh in the background, gentle and contemplative mood, muted purple and warm amber palette"
User says: "AI consciousness concept art" Enhanced prompt: "Digital art of an abstract neural network forming the silhouette of a contemplative face in profile, glowing connections in deep indigo and violet against a dark void, scattered points of warm light emerging from nodes like distant stars, cinematic lighting from below, ethereal and introspective atmosphere, inspired by the visual language of Blade Runner 2049"
User says: "blog hero image about rules vs understanding" Enhanced prompt: "Editorial illustration in warm earth tones, split composition: left side shows rigid geometric grid lines dissolving into organic flowing curves on the right side, transition zone where structure meets fluidity rendered in watercolor wash technique, warm gray background (#faf9f5), minimalist with generous negative space, contemplative mood" </examples>
== What NOT To Prompt
Avoid text rendering in images — current models handle it poorly. If the user needs text on an image, generate the visual element and suggest overlaying text with a design tool. Avoid requesting specific numbers of objects beyond 3-4 (models lose count). Avoid contradictory instructions ("photorealistic watercolor" — pick one medium).
= API Execution
The generation script handles the API call. It lives at ~/.claude/skills/image-gen/scripts/generate.ts and accepts these parameters:
bun run ~/.claude/skills/image-gen/scripts/generate.ts \
--prompt "your enhanced prompt here" \
--output "/path/to/output.png" \
--size "1024x1024" \
--preset "constitution-blog" \
--quality "high"== Parameters
--prompt (required) The enhanced image description
--output (required) Output file path
--size (optional) Image dimensions (default: 1024x1024, or preset default)
--model (optional) Model name (default: gemini-3.1-flash-image-preview)
--preset (optional) Named style preset from presets.json (see Presets section)
--quality (optional) Generation quality: low | medium | high | auto (default: auto)CLI flags always override preset defaults. For example, --preset constitution-blog --size 1024x1024 uses the preset's style but overrides its default size.
== Presets
Presets live in ~/.claude/skills/image-gen/presets.json. Each preset defines default parameters and style wrapping that gets prepended/appended to the user's prompt. This ensures visual consistency across a series.
To list available presets:
bun run ~/.claude/skills/image-gen/scripts/generate.ts --prompt "" --output "" --preset listCurrent presets:
constitution-blog Contemplative editorial illustrations for the constitution blog series.
Warm earth tones, violet accents, minimalist, generous negative space.
Default: 1536x1024, quality high.
hero-dark Dark-themed hero images for technical content.
Deep blue/purple gradients, geometric elements, soft glow.
Default: 1536x1024, quality high.
social-card Social media cards — bold, eye-catching, minimal.
Flat illustration, limited palette, high contrast.
Default: 1024x1024, quality medium.When using a preset, your --prompt describes the SUBJECT only — the preset handles medium, style, mood, and background. Example:
# Without preset — you specify everything:
bun run generate.ts \
--prompt "Editorial illustration, warm earth tones, a finger pointing at the moon..." \
--output "./moon.png" --size 1536x1024 --quality high
# With preset — you specify subject only:
bun run generate.ts \
--prompt "A finger pointing at the moon, but the finger is dissolving into light" \
--output "./moon.png" --preset constitution-blog== Quality Selection
Choose quality based on the image's intended use:
Final publication → high Best output, slower, higher cost
Draft / iteration → medium Good balance for preview and refinement
Quick test → low Fastest, for prompt validation only
Uncertain → auto Let the model decide (default)== Size Selection
Choose size based on the image's intended use, not by asking:
Blog hero / banner → 1536x1024 (3:2 landscape)
Social card / OG → 1200x630 (≈1.9:1, use 1536x1024 and crop)
Avatar / icon → 1024x1024 (square)
Portrait / character → 1024x1536 (2:3 portrait)
General / uncertain → 1024x1024 (square, safe default)== Model Selection
Two models, chosen by intent:
gemini-3.1-flash-image-preview 默认模型 — 中文渲染效果最佳,适合信息图和结构化内容
gpt-image-1 备用 — 照片级质量更高,但中文渲染较弱
Default to `gemini-3.1-flash-image-preview`(中文渲染最佳)。仅当需要照片级真实感时切换到 `gpt-image-1`。 The API is OpenAI-compatible at `$OPENAI_BASE_URL/images/generations`, authentication via `$OPENROUTER_API_KEY`.
== Execution: Always Background
Image generation takes 10-30 seconds. ALWAYS run the Bash command with `run_in_background: true` so the main thread is not blocked. While waiting, you can compose the FOSS upload question or work on other tasks. Read the result when the background notification arrives.
== Error Handling
The script exits with code 0 on success (image saved), non-zero on failure (prints error to stderr). Common failures: rate limiting (wait and retry), content policy rejection (rephrase the prompt), channel not found (switch model), timeout (retry once). If the model rejects a prompt, simplify it — remove potentially sensitive terms and try again before telling the user it failed.
= Post-Generation
After a successful generation:
1. Confirm the image was saved: report the file path
2. Ask the user whether to upload via FOSS for a public URL — some generations are drafts that do not need hosting
3. If uploading, invoke `$foss` with the generated file
Default output location: the current working directory, named `{descriptive-slug}-{timestamp}.png`. If the user specified an output path, use that instead.
━━━━━━━━━━
The skill is complete when an image file exists at the output path, the file is a valid PNG/JPEG readable by standard tools, and the user has been asked about FOSS upload. If the generation fails after one retry, report the error honestly — do not silently swallow failures or claim success without a file on disk.
This is NOT an image editor (no inpainting, outpainting, or style transfer). NOT a diagram tool (use Mermaid or Excalidraw). NOT a batch generator (one image per invocation). NOT a prompt-only tool — the deliverable is an image file, not a prompt string.
{
"moxi": {
"description": "墨线图 — 黑白印刷专用技术书插图风格",
"size": "2048x2048",
"quality": "high",
"model": "gemini-3.1-flash-image-preview",
"style_prefix": "Black and white technical textbook illustration for print publication. Style name: 墨线图 (Moxi). Pure monochrome — exactly 5 tonal levels: #FFFFFF (white), #E0E0E0 (light gray), #A0A0A0 (medium gray), #404040 (dark gray), #000000 (black). No color whatsoever. Thick 3px black lines for all borders. Clean white background. High contrast, no gradients, no decorative elements, no shadows. Shape encoding: rectangles = tasks/content, rounded rectangles = results/output, diamonds = decisions, circles = people/roles. Line encoding: solid 3px = main flow, dashed 2px = optional/alternative, dotted 1px = dependency/reference. Fill encoding: white = normal, light gray = emphasis, dark gray = header/label, black with white text = critical/warning. Maximum 7 visual elements per diagram. All spacing between elements no less than 1/3 of element width. All Chinese characters bold, crisp, minimum equivalent to 10pt print size.",
"style_suffix": "Technical manual illustration. Print-ready monochrome. 墨线图 style: ink, lines, truth. No artistic flourishes — every stroke carries information. Aspect ratio flexible: use landscape for flow diagrams, square for comparison charts."
}
}#!/usr/bin/env bun
/**
* Image generation via ZETATECHS — dual protocol support with preset system
*
* OpenAI protocol: gpt-image-1, dall-e-3, flux-* etc.
* POST /v1/images/generations
*
* Gemini protocol: gemini-3-pro-image-preview, gemini-3.1-flash-image-preview etc.
* POST /v1beta/models/{model}:generateContent
*
* Usage:
* bun run generate.ts --prompt "..." --output "./output.png" [--size 1024x1024] [--model gemini-3.1-flash-image-preview] [--preset name] [--quality auto]
*/
import { parseArgs } from "util";
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
prompt: { type: "string" },
output: { type: "string" },
size: { type: "string" },
model: { type: "string" },
preset: { type: "string" },
quality: { type: "string" },
},
strict: true,
});
// Handle --preset list before required param validation
if (values.preset === "list") {
const presetsPath = new URL("../presets.json", import.meta.url).pathname;
try {
const presets = (await Bun.file(presetsPath).json()) as Record<string, { description: string; size?: string; quality?: string }>;
console.error("Available presets:");
for (const [name, preset] of Object.entries(presets)) {
console.error(` ${name} — ${preset.description}`);
if (preset.size) console.error(` size: ${preset.size}`);
if (preset.quality) console.error(` quality: ${preset.quality}`);
}
} catch {
console.error("No presets found.");
}
process.exit(0);
}
if (!values.prompt || !values.output) {
console.error(
"Usage: bun run generate.ts --prompt <text> --output <path> [--size WxH] [--model name] [--preset name] [--quality low|medium|high|auto]"
);
process.exit(1);
}
const API_KEY = process.env.OPENROUTER_API_KEY;
// Strip trailing /v1 — protocols build their own paths
const PROXY_BASE = (process.env.OPENAI_BASE_URL || "https://api.zetatechs.com/v1").replace(/\/v1\/?$/, "");
if (!API_KEY) {
console.error("Error: OPENROUTER_API_KEY environment variable is not set");
process.exit(1);
}
// ━━━━━━━━━━ Preset System ━━━━━━━━━━
interface Preset {
description: string;
size?: string;
quality?: string;
model?: string;
style_prefix?: string;
style_suffix?: string;
}
async function loadPreset(name: string): Promise<Preset | null> {
const presetsPath = new URL("../presets.json", import.meta.url).pathname;
try {
const file = Bun.file(presetsPath);
const presets = (await file.json()) as Record<string, Preset>;
return presets[name] ?? null;
} catch {
console.error(` Warning: could not load presets from ${presetsPath}`);
return null;
}
}
// listPresets moved to top-level for --preset list early exit
// ━━━━━━━━━━ Resolve Parameters ━━━━━━━━━━
async function resolveParams() {
let prompt = values.prompt!;
let size = values.size ?? "1024x1024";
let quality = values.quality ?? "auto";
let model = values.model ?? "gemini-3.1-flash-image-preview";
if (values.preset) {
const preset = await loadPreset(values.preset);
if (!preset) {
console.error(`Error: preset "${values.preset}" not found. Run with --preset list to see available presets.`);
process.exit(1);
}
console.error(` Preset: ${values.preset} — ${preset.description}`);
// CLI flags override preset defaults
if (!values.size && preset.size) size = preset.size;
if (!values.quality && preset.quality) quality = preset.quality;
if (!values.model && preset.model) model = preset.model;
// Wrap prompt with style prefix/suffix
const prefix = preset.style_prefix ? `${preset.style_prefix} ` : "";
const suffix = preset.style_suffix ? ` ${preset.style_suffix}` : "";
prompt = `${prefix}${prompt}${suffix}`;
}
return { prompt, size, quality, model };
}
// ━━━━━━━━━━ Protocol Detection ━━━━━━━━━━
function isGeminiModel(model: string): boolean {
return model.startsWith("gemini-");
}
// ━━━━━━━━━━ OpenAI Protocol ━━━━━━━━━━
interface OpenAIImageResponse {
data: Array<{ b64_json?: string; url?: string }>;
}
async function generateOpenAI(prompt: string, size: string, model: string, quality: string): Promise<Buffer> {
const url = `${PROXY_BASE}/v1/images/generations`;
const body: Record<string, unknown> = { model, prompt, size, n: 1 };
if (quality !== "auto") body.quality = quality;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`API error (${response.status}): ${await response.text()}`);
}
const result = (await response.json()) as OpenAIImageResponse;
const imageData = result.data?.[0];
if (!imageData) throw new Error("No image data in OpenAI response");
if (imageData.b64_json) {
return Buffer.from(imageData.b64_json, "base64");
}
if (imageData.url) {
console.error(` Downloading from URL...`);
const imgRes = await fetch(imageData.url);
if (!imgRes.ok) throw new Error(`Download failed: ${imgRes.statusText}`);
return Buffer.from(await imgRes.arrayBuffer());
}
throw new Error("OpenAI response contains neither b64_json nor url");
}
// ━━━━━━━━━━ Gemini Protocol ━━━━━━━━━━
interface GeminiResponse {
candidates?: Array<{
content?: {
parts?: Array<{
text?: string;
inlineData?: { mimeType: string; data: string };
}>;
};
}>;
error?: { message: string; code: number };
}
function sizeToAspectRatio(size: string): string | undefined {
const map: Record<string, string> = {
"1024x1024": "1:1",
"1536x1024": "3:2",
"1024x1536": "2:3",
"1792x1024": "16:9",
"1024x1792": "9:16",
};
return map[size];
}
async function generateGemini(prompt: string, size: string, model: string): Promise<Buffer> {
const url = `${PROXY_BASE}/v1beta/models/${model}:generateContent`;
const body: Record<string, unknown> = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
responseModalities: ["TEXT", "IMAGE"],
},
};
// Inject aspect ratio if the model supports it
const aspectRatio = sizeToAspectRatio(size);
if (aspectRatio) {
(body.generationConfig as Record<string, unknown>).aspectRatio = aspectRatio;
}
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`API error (${response.status}): ${await response.text()}`);
}
const result = (await response.json()) as GeminiResponse;
if (result.error) {
throw new Error(`Gemini error (${result.error.code}): ${result.error.message}`);
}
// Extract first image part from candidates
const parts = result.candidates?.[0]?.content?.parts;
if (!parts) throw new Error("No candidates in Gemini response");
for (const part of parts) {
if (part.inlineData?.data) {
return Buffer.from(part.inlineData.data, "base64");
}
}
throw new Error("No image data found in Gemini response parts");
}
// ━━━━━━━━━━ Main ━━━━━━━━━━
async function main() {
const { prompt, size, quality, model } = await resolveParams();
const protocol = isGeminiModel(model) ? "Gemini" : "OpenAI";
console.error(`Generating image...`);
console.error(` Model: ${model} (${protocol} protocol)`);
console.error(` Size: ${size}`);
console.error(` Quality: ${quality}`);
console.error(` Prompt: ${prompt.substring(0, 120)}${prompt.length > 120 ? "..." : ""}`);
const maxRetries = 1;
let lastError: Error | null = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
if (attempt > 0) {
console.error(` Retry attempt ${attempt}...`);
await new Promise((r) => setTimeout(r, 2000));
}
const imageBuffer = isGeminiModel(model)
? await generateGemini(prompt, size, model)
: await generateOpenAI(prompt, size, model, quality);
await Bun.write(values.output!, imageBuffer);
const fileSize = Bun.file(values.output!).size;
console.error(` Saved: ${values.output} (${((await fileSize) / 1024).toFixed(1)} KB)`);
console.log(values.output);
process.exit(0);
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
console.error(` Error: ${lastError.message}`);
}
}
console.error(`Failed after ${maxRetries + 1} attempts: ${lastError?.message}`);
process.exit(1);
}
main();