Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions skills/atlas-cloud-image-generation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Atlas Cloud Image Generation

A dependency-free agent skill for generating images with the Atlas Cloud
asynchronous media API. The included Node.js CLI submits a Seedream 5 Lite
text-to-image task, polls it to completion, and prints the output URLs as JSON.

## Requirements

- Node.js 18 or newer
- An Atlas Cloud API key in `ATLASCLOUD_API_KEY`

```bash
export ATLASCLOUD_API_KEY="your-api-key-here"
```

## Usage

```bash
node scripts/generate-image.mjs \
--prompt "A minimal red cube on a white studio background" \
--size "2048*2048" \
--output-format jpeg
```

Successful output has this shape:

```json
{
"id": "prediction-id",
"status": "completed",
"model": "bytedance/seedream-v5.0-lite",
"outputs": ["https://example.invalid/generated-image.jpeg"]
}
```

## Options

| Option | Default | Description |
| --- | --- | --- |
| `--prompt` | required | Text description of the image |
| `--size` | `2048*2048` | Image size accepted by the current model schema |
| `--output-format` | `jpeg` | `jpeg` or `png` |
| `--poll-interval-ms` | `3000` | Delay between status requests |
| `--max-attempts` | `60` | Maximum number of status requests |

## Validation

Run the protocol tests without an API key or network access:

```bash
node --test tests/generate-image.test.mjs
```

Before changing the model or parameters, fetch the current model catalogue and
follow the selected model's live `schema` URL. Do not infer model IDs, request
fields, or allowed values.
76 changes: 76 additions & 0 deletions skills/atlas-cloud-image-generation/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
name: atlas-cloud-image-generation
description: >
Generate an image with the Atlas Cloud asynchronous media API. Use when: a user asks for Atlas Cloud image generation or a Seedream image. NOT for: video generation, image editing, or models whose live schema has not been checked.
---

# Atlas Cloud Image Generation

Generate a text-to-image result with Atlas Cloud and poll the asynchronous task
until it completes.

## Use when

- The user explicitly asks to generate an image with Atlas Cloud.
- The user wants a Seedream 5 Lite text-to-image result.
- An agent needs a dependency-free CLI that returns generated image URLs.

NOT for video generation, image editing, or switching to another model without
first checking that model's live Atlas Cloud schema.

## Setup

Set the API key in the environment. Never pass it as a command-line argument or
write it to a project file.

```bash
export ATLASCLOUD_API_KEY="your-api-key-here"
```

## Generate

```bash
node {baseDir}/scripts/generate-image.mjs \
--prompt "A red paper lantern floating above a quiet lake" \
--size "2048*2048" \
--output-format png
```

The command writes JSON to stdout. Return the first URL in `outputs` to the
user, or preserve the full array when multiple outputs are present.

## Workflow

1. Confirm `ATLASCLOUD_API_KEY` is set.
2. Improve the user's prompt without changing the requested subject.
3. Run the CLI and wait for a completed task.
4. Return the generated URL from `outputs`.
5. If the task fails, report the CLI error instead of retrying with guessed
parameters.

## Examples

**User:** Generate a square product photo of a ceramic mug with Atlas Cloud.

```bash
node {baseDir}/scripts/generate-image.mjs \
--prompt "Studio product photo of a white ceramic mug, soft side light, clean gray background" \
--size "2048*2048"
```

**User:** Make a wide cinematic landscape with Seedream.

```bash
node {baseDir}/scripts/generate-image.mjs \
--prompt "Wide cinematic landscape, mountain observatory at sunrise, detailed clouds" \
--size "2848*1600" \
--output-format jpeg
```

## Model safety

The bundled CLI targets `bytedance/seedream-v5.0-lite`, using parameters and
endpoints verified from its live Atlas Cloud schema. Model IDs and schemas can
change. Fetch `https://api.atlascloud.ai/api/v1/models`, require
`display_console: true`, and inspect the model's `schema` URL before changing
the model or request fields.
224 changes: 224 additions & 0 deletions skills/atlas-cloud-image-generation/scripts/generate-image.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
#!/usr/bin/env node

import { pathToFileURL } from "node:url";

export const DEFAULT_MODEL = "bytedance/seedream-v5.0-lite";
export const DEFAULT_BASE_URL = "https://api.atlascloud.ai";
export const DEFAULT_SIZE = "2048*2048";
export const DEFAULT_OUTPUT_FORMAT = "jpeg";

const COMPLETED_STATUSES = new Set(["completed", "succeeded"]);
const FAILED_STATUSES = new Set(["failed", "canceled", "cancelled"]);

function readValue(argv, index, option) {
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`${option} requires a value`);
}
return value;
}

function readPositiveInteger(value, option) {
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new Error(`${option} must be a positive integer`);
}
return parsed;
}

export function parseArgs(argv) {
const options = {
prompt: "",
size: DEFAULT_SIZE,
outputFormat: DEFAULT_OUTPUT_FORMAT,
pollIntervalMs: 3000,
maxAttempts: 60,
help: false,
};

for (let index = 0; index < argv.length; index += 1) {
const option = argv[index];
switch (option) {
case "--prompt":
options.prompt = readValue(argv, index, option);
index += 1;
break;
case "--size":
options.size = readValue(argv, index, option);
index += 1;
break;
case "--output-format":
options.outputFormat = readValue(argv, index, option);
index += 1;
break;
case "--poll-interval-ms":
options.pollIntervalMs = readPositiveInteger(
readValue(argv, index, option),
option,
);
index += 1;
break;
case "--max-attempts":
options.maxAttempts = readPositiveInteger(
readValue(argv, index, option),
option,
);
index += 1;
break;
case "--help":
case "-h":
options.help = true;
break;
default:
throw new Error(`Unknown option: ${option}`);
}
}

if (!options.help && !options.prompt.trim()) {
throw new Error("--prompt is required");
}
if (!new Set(["jpeg", "png"]).has(options.outputFormat)) {
throw new Error("--output-format must be jpeg or png");
}

return options;
}

function unwrapPayload(payload) {
return payload?.data ?? payload;
}

async function requestJson(url, init, fetchImpl) {
const response = await fetchImpl(url, init);
const body = await response.text();
let payload;

try {
payload = body ? JSON.parse(body) : {};
} catch {
throw new Error(`Atlas Cloud returned invalid JSON (HTTP ${response.status})`);
}

const apiCode = payload?.code;
if (!response.ok || (apiCode !== undefined && apiCode !== 200)) {
const message =
payload?.message ?? payload?.error?.message ?? `HTTP ${response.status}`;
throw new Error(`Atlas Cloud request failed: ${message}`);
}

return payload;
}

export async function generateImage(options, dependencies = {}) {
const {
apiKey,
baseUrl = DEFAULT_BASE_URL,
prompt,
size = DEFAULT_SIZE,
outputFormat = DEFAULT_OUTPUT_FORMAT,
pollIntervalMs = 3000,
maxAttempts = 60,
} = options;
const fetchImpl = dependencies.fetchImpl ?? globalThis.fetch;
const sleepImpl =
dependencies.sleepImpl ??
((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));

if (!apiKey) {
throw new Error("ATLASCLOUD_API_KEY is required");
}
if (typeof fetchImpl !== "function") {
throw new Error("A Fetch API implementation is required");
}

const origin = baseUrl.replace(/\/+$/, "");
const headers = {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
};
const submission = await requestJson(
`${origin}/api/v1/model/generateVideo`,
{
method: "POST",
headers,
body: JSON.stringify({
model: DEFAULT_MODEL,
prompt,
size,
output_format: outputFormat,
}),
},
fetchImpl,
);
const task = unwrapPayload(submission);
const predictionId = task?.id ?? task?.request_id;

if (!predictionId) {
throw new Error("Atlas Cloud response did not include a prediction ID");
}

for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const prediction = await requestJson(
`${origin}/api/v1/model/result/${encodeURIComponent(predictionId)}`,
{ method: "GET", headers },
fetchImpl,
);
const result = unwrapPayload(prediction);
const status = String(result?.status ?? "unknown").toLowerCase();

if (COMPLETED_STATUSES.has(status)) {
const outputs = Array.isArray(result?.outputs) ? result.outputs : [];
if (outputs.length === 0) {
throw new Error("Atlas Cloud generation completed without an output URL");
}
return {
id: predictionId,
status,
model: result?.model ?? DEFAULT_MODEL,
outputs,
};
}
if (FAILED_STATUSES.has(status)) {
throw new Error(
`Atlas Cloud generation failed: ${result?.error ?? result?.message ?? status}`,
);
}
if (attempt < maxAttempts) {
await sleepImpl(pollIntervalMs);
}
}

throw new Error(
`Atlas Cloud generation did not finish after ${maxAttempts} status requests`,
);
}

function printHelp() {
process.stdout.write(`Usage: node generate-image.mjs --prompt <text> [options]\n\n`);
process.stdout.write(`Options:\n`);
process.stdout.write(` --size <width*height> Default: ${DEFAULT_SIZE}\n`);
process.stdout.write(` --output-format <jpeg|png> Default: ${DEFAULT_OUTPUT_FORMAT}\n`);
process.stdout.write(` --poll-interval-ms <ms> Default: 3000\n`);
process.stdout.write(` --max-attempts <count> Default: 60\n`);
}

async function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
printHelp();
return;
}

const result = await generateImage({
...options,
apiKey: process.env.ATLASCLOUD_API_KEY,
});
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error) => {
process.stderr.write(`${error.message}\n`);
process.exitCode = 1;
});
}
Loading