From 1826c23826695b1e736f3414117c484c52345ccc Mon Sep 17 00:00:00 2001 From: Oleg Proskurin Date: Tue, 24 Feb 2026 22:53:32 +0700 Subject: [PATCH] feat: add skill --- .claude/settings.local.json | 3 +- .claude/skills/gen-image/SKILL.md | 44 ++ .../skills/gen-image/banatie-gen.mjs | 4 +- .../docs/image-generation-advanced.md | 448 ++++++++++++++++++ .../skills/gen-image/docs/image-generation.md | 348 ++++++++++++++ .../skills/gen-image/docs/images-upload.md | 383 +++++++++++++++ CLAUDE.md | 35 +- 7 files changed, 1230 insertions(+), 35 deletions(-) create mode 100644 .claude/skills/gen-image/SKILL.md rename src/scripts/banatie.mjs => .claude/skills/gen-image/banatie-gen.mjs (95%) create mode 100644 .claude/skills/gen-image/docs/image-generation-advanced.md create mode 100644 .claude/skills/gen-image/docs/image-generation.md create mode 100644 .claude/skills/gen-image/docs/images-upload.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index dbeb74c..3b4b911 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -36,7 +36,8 @@ "mcp__chrome-devtools__take_snapshot", "mcp__chrome-devtools__upload_file", "mcp__chrome-devtools__wait_for", - "WebFetch(domain:banatie.app)" + "WebFetch(domain:banatie.app)", + "Bash(npx skills:*)" ] } } diff --git a/.claude/skills/gen-image/SKILL.md b/.claude/skills/gen-image/SKILL.md new file mode 100644 index 0000000..0303c42 --- /dev/null +++ b/.claude/skills/gen-image/SKILL.md @@ -0,0 +1,44 @@ +--- +name: gen-image +description: Generate images via Banatie API — text-to-image with optional reference images, aspect ratios, and enhancement templates +--- + +# Image Generation Skill + +Generate images using the Banatie API. Parses user arguments, validates inputs, and runs the generation script. + +## Arguments + +Parse these from the user's message. Use `AskUserQuestion` for any missing required arguments. + +| Argument | Required | Default | Description | +|----------|----------|---------|-------------| +| **Prompt** | Yes | — | Image description | +| **Output path** | Yes | — | Where to save the file (e.g. `assets/icons/star.png`) | +| **Aspect ratio** | No | `1:1` | `1:1`, `16:9`, `9:16`, `3:2`, `4:3`, `3:4`, `21:9` | +| **Reference images** | No | — | Local file paths or `@alias` names (max 3) | +| **Enhancement template** | No | `general` | `general`, `photorealistic`, `illustration`, `minimalist`, `sticker`, `product`, `comic` | + +## Workflow + +1. **Parse arguments** from the user's message. Extract prompt, output path, aspect ratio, references, and template inline where provided. + +2. **Fill missing required arguments** using `AskUserQuestion`. Suggest an output path based on context (e.g. `assets/backgrounds/` for backgrounds, `assets/icons/` for icons). + +3. **Validate** that any referenced local files exist before proceeding. + +4. **Read API docs** from `docs/` subfolder when the user needs advanced features (references, flows, aliases). The docs are: + - `docs/image-generation.md` — basic generation, aspect ratios, prompt enhancement, templates + - `docs/image-generation-advanced.md` — reference images, aliases, flows, regeneration + - `docs/images-upload.md` — image upload, alias management + +5. **Run generation**: + ```bash + node .claude/skills/gen-image/banatie-gen.mjs --prompt "" --output [--aspect-ratio ] [--ref ]... + ``` + +6. **Report results**: output file path, image dimensions, and the full command for reproducibility. + +## Environment + +The script reads `BANATIE_KEY` from `.env` in the project root. Rate limit: 100 requests/hour. diff --git a/src/scripts/banatie.mjs b/.claude/skills/gen-image/banatie-gen.mjs similarity index 95% rename from src/scripts/banatie.mjs rename to .claude/skills/gen-image/banatie-gen.mjs index 3ac0b6c..2b37593 100644 --- a/src/scripts/banatie.mjs +++ b/.claude/skills/gen-image/banatie-gen.mjs @@ -1,7 +1,7 @@ import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs'; import { resolve, dirname, basename } from 'path'; -const envPath = resolve(dirname(new URL(import.meta.url).pathname), '../../.env'); +const envPath = resolve(process.cwd(), '.env'); try { const envContent = readFileSync(envPath, 'utf-8'); for (const line of envContent.split('\n')) { @@ -155,6 +155,6 @@ const args = parseArgs(process.argv.slice(2)); if (args.prompt && args.output) { generateImage(args); } else if (process.argv.length > 2) { - console.error('Usage: node banatie.mjs --prompt "" --output [--aspect-ratio ] [--ref ]...'); + console.error('Usage: node banatie-gen.mjs --prompt "" --output [--aspect-ratio ] [--ref ]...'); process.exit(1); } diff --git a/.claude/skills/gen-image/docs/image-generation-advanced.md b/.claude/skills/gen-image/docs/image-generation-advanced.md new file mode 100644 index 0000000..0d2df89 --- /dev/null +++ b/.claude/skills/gen-image/docs/image-generation-advanced.md @@ -0,0 +1,448 @@ +# Advanced Image Generation + +Advanced generation features: reference images, aliases, flows, and regeneration. For basic generation, see [image-generation.md](image-generation.md). + +All endpoints require Project Key authentication via `X-API-Key` header. + +--- + +## Reference Images + +Use existing images as style or content references for generation. + +### Using References + +Add `referenceImages` array to your generation request: + +```json +{ + "prompt": "A product photo with the logo in the corner", + "referenceImages": ["@brand-logo", "@product-style"] +} +``` + +References can be: +- **Project aliases**: `@logo`, `@brand-style` +- **Flow aliases**: `@hero` (with flowId context) +- **Technical aliases**: `@last`, `@first`, `@upload` +- **Image UUIDs**: `550e8400-e29b-41d4-a716-446655440000` + +### Auto-Detection from Prompt + +Aliases in the prompt are automatically detected and used as references: + +```json +{ + "prompt": "Create a banner using @brand-logo with blue background" +} +// @brand-logo is auto-detected and added to referenceImages +``` + +### Reference Limits + +| Constraint | Limit | +|------------|-------| +| Max references | 3 images | +| Max file size | 5MB per image | +| Supported formats | PNG, JPEG, WebP | + +### Response with References + +```json +{ + "data": { + "id": "550e8400-...", + "prompt": "Create a banner using @brand-logo", + "referencedImages": [ + { "imageId": "7c4ccf47-...", "alias": "@brand-logo" } + ], + "referenceImages": [ + { + "id": "7c4ccf47-...", + "storageUrl": "http://...", + "alias": "@brand-logo" + } + ] + } +} +``` + +--- + +## Alias Assignment + +Assign aliases to generated images for easy referencing. + +### Project-Scoped Alias + +Use `alias` parameter to assign a project-wide alias: + +```json +{ + "prompt": "A hero banner image", + "alias": "@hero-banner" +} +``` + +The output image will be accessible via `@hero-banner` anywhere in the project. + +### Flow-Scoped Alias + +Use `flowAlias` parameter to assign a flow-specific alias: + +```json +{ + "prompt": "A hero image variation", + "flowId": "550e8400-...", + "flowAlias": "@best" +} +``` + +The alias `@best` is only accessible within this flow's context. + +### Alias Format + +| Rule | Description | +|------|-------------| +| Prefix | Must start with `@` | +| Characters | Alphanumeric, underscore, hyphen | +| Pattern | `@[a-zA-Z0-9_-]+` | +| Max length | 50 characters | +| Examples | `@logo`, `@hero-bg`, `@image_1` | + +### Reserved Aliases + +These aliases are computed automatically and cannot be assigned: + +| Alias | Description | +|-------|-------------| +| `@last` | Most recently generated image in flow | +| `@first` | First generated image in flow | +| `@upload` | Most recently uploaded image in flow | + +### Override Behavior + +When assigning an alias that already exists: +- The **new image gets the alias** +- The **old image loses the alias** (alias set to null) +- The old image is **not deleted**, just unlinked + +--- + +## 3-Tier Alias Resolution + +Aliases are resolved in this order of precedence: + +### 1. Technical Aliases (Highest Priority) + +Computed on-the-fly, require flow context: + +``` +GET /api/v1/images/@last?flowId=550e8400-... +``` + +| Alias | Returns | +|-------|---------| +| `@last` | Last generated image in flow | +| `@first` | First generated image in flow | +| `@upload` | Last uploaded image in flow | + +### 2. Flow Aliases + +Stored in flow's `aliases` JSONB field: + +``` +GET /api/v1/images/@hero?flowId=550e8400-... +``` + +Different flows can have the same alias pointing to different images. + +### 3. Project Aliases (Lowest Priority) + +Stored in image's `alias` column: + +``` +GET /api/v1/images/@logo +``` + +Global across the project, unique per project. + +### Resolution Example + +``` +// Request with flowId +GET /api/v1/images/@hero?flowId=abc-123 + +// Resolution order: +// 1. Is "@hero" a technical alias? No +// 2. Does flow abc-123 have "@hero" in aliases? Check flows.aliases JSONB +// 3. Does any image have alias = "@hero"? Check images.alias column +``` + +--- + +## Flow Integration + +Flows organize related generations into chains. + +### Lazy Flow Creation + +When `flowId` is not provided, a pending flow ID is generated: + +```json +// Request +{ + "prompt": "A red car" + // No flowId +} + +// Response +{ + "data": { + "id": "gen-123", + "flowId": "flow-456" // Auto-generated, flow record not created yet + } +} +``` + +The flow record is created when: +- A second generation uses the same `flowId` +- A `flowAlias` is assigned to any generation in the flow + +### Eager Flow Creation + +When `flowAlias` is provided, the flow is created immediately: + +```json +{ + "prompt": "A hero banner", + "flowAlias": "@hero-flow" +} +``` + +### No Flow Association + +To explicitly create without flow association: + +```json +{ + "prompt": "A standalone image", + "flowId": null +} +``` + +### flowId Behavior Summary + +| Value | Behavior | +|-------|----------| +| `undefined` (not provided) | Auto-generate pendingFlowId, lazy creation | +| `null` (explicitly null) | No flow association | +| `"uuid-string"` | Use provided ID, create flow if doesn't exist | + +--- + +## Regeneration + +### Regenerate Generation + +Recreate an image using the exact same parameters: + +``` +POST /api/v1/generations/:id/regenerate +``` + +**Behavior:** +- Uses exact same prompt, aspect ratio, references +- **Preserves** output image ID and URL +- Works regardless of current status +- No request body needed + +**Response:** Same as original generation with new image + +### Update and Regenerate + +Use PUT to modify parameters with smart regeneration: + +``` +PUT /api/v1/generations/:id +``` + +```json +{ + "prompt": "A blue car instead", + "aspectRatio": "1:1" +} +``` + +**Smart Behavior:** + +| Changed Field | Triggers Regeneration | +|---------------|----------------------| +| `prompt` | Yes | +| `aspectRatio` | Yes | +| `flowId` | No (metadata only) | +| `meta` | No (metadata only) | + +### Flow Regenerate + +Regenerate the most recent generation in a flow: + +``` +POST /api/v1/flows/:id/regenerate +``` + +**Behavior:** +- Finds the most recent generation in flow +- Regenerates with exact same parameters +- Returns error if flow has no generations + +--- + +## Flow Management + +### List Flows + +``` +GET /api/v1/flows +``` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `limit` | number | `20` | Results per page (max: 100) | +| `offset` | number | `0` | Pagination offset | + +**Response:** + +```json +{ + "data": [ + { + "id": "flow-456", + "projectId": "project-123", + "aliases": { "@hero": "img-789", "@best": "img-abc" }, + "generationCount": 5, + "imageCount": 7, + "createdAt": "2025-11-28T10:00:00.000Z" + } + ], + "pagination": { "limit": 20, "offset": 0, "total": 3, "hasMore": false } +} +``` + +### Get Flow + +``` +GET /api/v1/flows/:id +``` + +Returns flow with computed counts and aliases. + +### List Flow Generations + +``` +GET /api/v1/flows/:id/generations +``` + +Returns all generations in the flow, ordered by creation date (newest first). + +### List Flow Images + +``` +GET /api/v1/flows/:id/images +``` + +Returns all images in the flow (generated and uploaded). + +### Update Flow Aliases + +``` +PUT /api/v1/flows/:id/aliases +``` + +```json +{ + "aliases": { + "@hero": "image-id-123", + "@best": "image-id-456" + } +} +``` + +**Behavior:** Merges with existing aliases (does not replace all). + +### Remove Flow Alias + +``` +DELETE /api/v1/flows/:id/aliases/:alias +``` + +Example: `DELETE /api/v1/flows/flow-456/aliases/@hero` + +### Delete Flow + +``` +DELETE /api/v1/flows/:id +``` + +**Cascade Behavior:** +- Flow record is **hard deleted** +- All generations in flow are **hard deleted** +- Images **without** project alias: **hard deleted** with MinIO cleanup +- Images **with** project alias: **kept**, but `flowId` set to null + +--- + +## Full Request Example + +```json +// POST /api/v1/generations +{ + "prompt": "A professional product photo using @brand-style and @product-template", + "aspectRatio": "1:1", + "autoEnhance": true, + "enhancementOptions": { "template": "product" }, + "flowId": "campaign-flow-123", + "alias": "@latest-product", + "flowAlias": "@hero", + "meta": { "campaign": "summer-2025" } +} +``` + +**What happens:** +1. `@brand-style` and `@product-template` resolved and used as references +2. Prompt enhanced using "product" template +3. Generation created in flow `campaign-flow-123` +4. Output image assigned project alias `@latest-product` +5. Output image assigned flow alias `@hero` in the flow +6. Custom metadata stored + +--- + +## Response Fields (Additional) + +| Field | Type | Description | +|-------|------|-------------| +| `flowId` | string | Associated flow UUID | +| `alias` | string | Project-scoped alias (on outputImage) | +| `referencedImages` | array | Resolved references: `[{ imageId, alias }]` | +| `referenceImages` | array | Full image details of references | + +--- + +## Error Codes + +| HTTP Status | Code | Description | +|-------------|------|-------------| +| 400 | `ALIAS_FORMAT_CHECK` | Alias must start with @ | +| 400 | `RESERVED_ALIAS` | Cannot use technical alias | +| 404 | `ALIAS_NOT_FOUND` | Referenced alias doesn't exist | +| 404 | `FLOW_NOT_FOUND` | Flow does not exist | + +--- + +## See Also + +- [Basic Generation](image-generation.md) - Simple generation +- [Image Upload](images-upload.md) - Upload with aliases diff --git a/.claude/skills/gen-image/docs/image-generation.md b/.claude/skills/gen-image/docs/image-generation.md new file mode 100644 index 0000000..296e1d7 --- /dev/null +++ b/.claude/skills/gen-image/docs/image-generation.md @@ -0,0 +1,348 @@ +# Image Generation API + +Basic image generation using AI. For advanced features like references, aliases, and flows, see [image-generation-advanced.md](image-generation-advanced.md). + +All endpoints require Project Key authentication via `X-API-Key` header. + +--- + +## Create Generation + +``` +POST /api/v1/generations +``` + +Generate an AI image from a text prompt. + +**Request Body:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `prompt` | string | Yes | - | Text description of the image to generate | +| `aspectRatio` | string | No | `"1:1"` | Image aspect ratio | +| `autoEnhance` | boolean | No | `true` | Enable AI prompt enhancement | +| `enhancementOptions` | object | No | - | Enhancement configuration | +| `enhancementOptions.template` | string | No | `"general"` | Enhancement template | +| `meta` | object | No | `{}` | Custom metadata | + +**Example Request:** + +```json +{ + "prompt": "a red sports car on a mountain road", + "aspectRatio": "16:9", + "autoEnhance": true, + "enhancementOptions": { + "template": "photorealistic" + } +} +``` + +**Response:** `201 Created` + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "projectId": "57c7f7f4-47de-4d70-9ebd-3807a0b63746", + "prompt": "A photorealistic establishing shot of a sleek red sports car...", + "originalPrompt": "a red sports car on a mountain road", + "autoEnhance": true, + "aspectRatio": "16:9", + "status": "pending", + "outputImageId": null, + "processingTimeMs": null, + "createdAt": "2025-11-28T10:00:00.000Z", + "updatedAt": "2025-11-28T10:00:00.000Z" + } +} +``` + +--- + +## Aspect Ratios + +Supported aspect ratios for image generation: + +| Aspect Ratio | Use Case | +|--------------|----------| +| `1:1` | Square images, social media posts, profile pictures | +| `16:9` | Landscape, hero banners, video thumbnails | +| `9:16` | Portrait, mobile screens, stories | +| `3:2` | Photography standard, print | +| `21:9` | Ultra-wide banners, cinematic | + +--- + +## Prompt Enhancement + +By default, prompts are automatically enhanced by AI to produce better results. + +### How It Works + +When `autoEnhance: true` (default): +- Your original prompt is preserved in `originalPrompt` +- AI enhances it with style details, lighting, composition +- The enhanced version is stored in `prompt` and used for generation + +When `autoEnhance: false`: +- Both `prompt` and `originalPrompt` contain your original text +- No AI enhancement is applied + +### Enhancement Templates + +> **Roadmap**: Template selection is planned for a future release. Currently all enhanced prompts use the default `general` style regardless of the `template` parameter. + +Use `enhancementOptions.template` to guide the enhancement style: + +| Template | Description | Best For | +|----------|-------------|----------| +| `general` | Balanced enhancement (default) | Most use cases | +| `photorealistic` | Photography terms, lighting, camera details | Realistic photos | +| `illustration` | Art style, composition, color palette | Artwork, drawings | +| `minimalist` | Clean, simple, essential elements | Logos, icons | +| `sticker` | Bold outlines, limited colors, vector style | Stickers, emojis | +| `product` | Studio lighting, materials, lifestyle context | E-commerce | +| `comic` | Action lines, expressions, panel composition | Comics, manga | + +### Example: With Enhancement + +```json +// Request +{ + "prompt": "a cat", + "autoEnhance": true, + "enhancementOptions": { "template": "photorealistic" } +} + +// Response +{ + "prompt": "A photorealistic close-up portrait of a domestic cat with soft fur, captured with an 85mm lens at f/1.8, natural window lighting creating soft shadows, detailed whiskers and expressive eyes, shallow depth of field with creamy bokeh background", + "originalPrompt": "a cat", + "autoEnhance": true +} +``` + +### Example: Without Enhancement + +```json +// Request +{ + "prompt": "a cat sitting on a windowsill", + "autoEnhance": false +} + +// Response +{ + "prompt": "a cat sitting on a windowsill", + "originalPrompt": "a cat sitting on a windowsill", + "autoEnhance": false +} +``` + +--- + +## Generation Status + +Generations go through these status stages: + +| Status | Description | +|--------|-------------| +| `pending` | Generation created, waiting to start | +| `processing` | AI is generating the image | +| `success` | Image generated successfully | +| `failed` | Generation failed (see `errorMessage`) | + +Poll the generation endpoint to check status: + +``` +GET /api/v1/generations/:id +``` + +When `status: "success"`, the `outputImageId` field contains the generated image ID. + +--- + +## List Generations + +``` +GET /api/v1/generations +``` + +List all generations with optional filters. + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `status` | string | - | Filter by status: `pending`, `processing`, `success`, `failed` | +| `limit` | number | `20` | Results per page (max: 100) | +| `offset` | number | `0` | Pagination offset | +| `includeDeleted` | boolean | `false` | Include soft-deleted records | + +**Example:** + +``` +GET /api/v1/generations?status=success&limit=10 +``` + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "prompt": "A photorealistic establishing shot...", + "originalPrompt": "a red sports car", + "autoEnhance": true, + "aspectRatio": "16:9", + "status": "success", + "outputImageId": "7c4ccf47-41ce-4718-afbc-8c553b2c631a", + "processingTimeMs": 8500, + "createdAt": "2025-11-28T10:00:00.000Z" + } + ], + "pagination": { + "limit": 10, + "offset": 0, + "total": 42, + "hasMore": true + } +} +``` + +--- + +## Get Generation + +``` +GET /api/v1/generations/:id +``` + +Get a single generation with full details. + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "projectId": "57c7f7f4-47de-4d70-9ebd-3807a0b63746", + "prompt": "A photorealistic establishing shot of a sleek red sports car...", + "originalPrompt": "a red sports car on a mountain road", + "autoEnhance": true, + "aspectRatio": "16:9", + "status": "success", + "outputImageId": "7c4ccf47-41ce-4718-afbc-8c553b2c631a", + "outputImage": { + "id": "7c4ccf47-41ce-4718-afbc-8c553b2c631a", + "storageKey": "default/my-project/img/7c4ccf47-41ce-4718-afbc-8c553b2c631a", + "storageUrl": "https://cdn.banatie.app/default/my-project/img/7c4ccf47-41ce-4718-afbc-8c553b2c631a", + "mimeType": "image/png", + "width": 1792, + "height": 1024, + "fileSize": 1909246 + }, + "processingTimeMs": 8500, + "retryCount": 0, + "errorMessage": null, + "meta": {}, + "createdAt": "2025-11-28T10:00:00.000Z", + "updatedAt": "2025-11-28T10:00:08.500Z" + } +} +``` + +--- + +## Delete Generation + +``` +DELETE /api/v1/generations/:id +``` + +Delete a generation and its output image. + +**Response:** `200 OK` + +```json +{ + "success": true, + "message": "Generation deleted" +} +``` + +**Behavior:** +- Generation record is hard deleted +- Output image is hard deleted (unless it has a project alias) + +--- + +## Response Fields + +### Generation Response + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Generation UUID | +| `projectId` | string | Project UUID | +| `prompt` | string | Prompt used for generation (enhanced if applicable) | +| `originalPrompt` | string | Original user input | +| `autoEnhance` | boolean | Whether enhancement was applied | +| `aspectRatio` | string | Image aspect ratio | +| `status` | string | Generation status | +| `outputImageId` | string | Output image UUID (when successful) | +| `outputImage` | object | Output image details (when successful) | +| `processingTimeMs` | number | Generation time in milliseconds | +| `retryCount` | number | Number of retry attempts | +| `errorMessage` | string | Error details (when failed) | +| `meta` | object | Custom metadata | +| `createdAt` | string | ISO timestamp | +| `updatedAt` | string | ISO timestamp | + +### Output Image + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Image UUID (same as filename in storage) | +| `storageKey` | string | Storage path: `{org}/{project}/img/{imageId}` | +| `storageUrl` | string | CDN URL: `https://cdn.banatie.app/{org}/{project}/img/{imageId}` | +| `mimeType` | string | Image MIME type | +| `width` | number | Image width in pixels | +| `height` | number | Image height in pixels | +| `fileSize` | number | File size in bytes | + +> **Note:** The image filename in storage equals `image.id` (UUID). No file extension is used - Content-Type is stored in object metadata. + +--- + +## Error Codes + +| HTTP Status | Code | Description | +|-------------|------|-------------| +| 400 | `VALIDATION_ERROR` | Invalid parameters | +| 401 | `UNAUTHORIZED` | Missing or invalid API key | +| 404 | `GENERATION_NOT_FOUND` | Generation does not exist | +| 429 | `RATE_LIMIT_EXCEEDED` | Too many requests | +| 500 | `GENERATION_FAILED` | AI generation failed | + +--- + +## Rate Limits + +- **100 requests per hour** per API key +- Rate limit headers included in response: + - `X-RateLimit-Limit`: Maximum requests + - `X-RateLimit-Remaining`: Remaining requests + - `X-RateLimit-Reset`: Seconds until reset + +--- + +## See Also + +- [Advanced Generation](image-generation-advanced.md) - References, aliases, flows +- [Image Upload](images-upload.md) - Upload and manage images diff --git a/.claude/skills/gen-image/docs/images-upload.md b/.claude/skills/gen-image/docs/images-upload.md new file mode 100644 index 0000000..3b04976 --- /dev/null +++ b/.claude/skills/gen-image/docs/images-upload.md @@ -0,0 +1,383 @@ +# Image Upload & Management API + +Upload images and manage your image library. For generation, see [image-generation.md](image-generation.md). + +All endpoints require Project Key authentication via `X-API-Key` header. + +--- + +## Upload Image + +``` +POST /api/v1/images/upload +``` + +Upload an image file with optional alias and flow association. + +**Content-Type:** `multipart/form-data` + +**Form Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `file` | file | Yes | Image file (PNG, JPEG, WebP) | +| `alias` | string | No | Project-scoped alias (e.g., `@logo`) | +| `flowId` | string | No | Flow UUID to associate with | +| `flowAlias` | string | No | Flow-scoped alias (requires flowId) | +| `meta` | string | No | JSON string with custom metadata | + +**File Constraints:** + +| Constraint | Limit | +|------------|-------| +| Max file size | 5MB | +| Supported formats | PNG, JPEG, JPG, WebP | +| MIME types | `image/png`, `image/jpeg`, `image/webp` | + +**Example Request (curl):** + +```bash +curl -X POST http://localhost:3000/api/v1/images/upload \ + -H "X-API-Key: YOUR_PROJECT_KEY" \ + -F "file=@logo.png" \ + -F "alias=@brand-logo" \ + -F 'meta={"tags": ["logo", "brand"]}' +``` + +**Response:** `201 Created` + +```json +{ + "success": true, + "data": { + "id": "7c4ccf47-41ce-4718-afbc-8c553b2c631a", + "projectId": "57c7f7f4-47de-4d70-9ebd-3807a0b63746", + "flowId": null, + "storageKey": "default/my-project/img/7c4ccf47-41ce-4718-afbc-8c553b2c631a", + "storageUrl": "https://cdn.banatie.app/default/my-project/img/7c4ccf47-41ce-4718-afbc-8c553b2c631a", + "mimeType": "image/png", + "fileSize": 45678, + "width": 512, + "height": 512, + "source": "uploaded", + "alias": "@brand-logo", + "focalPoint": null, + "meta": { "tags": ["logo", "brand"] }, + "createdAt": "2025-11-28T10:00:00.000Z" + } +} +``` + +> **Note:** The `id` field equals the filename in storage (UUID). Original filename is preserved in object metadata. + +### flowId Behavior + +| Value | Behavior | +|-------|----------| +| Not provided | Auto-generate `pendingFlowId`, lazy flow creation | +| `null` | No flow association | +| `"uuid"` | Associate with specified flow | + +### Upload with Flow + +```bash +# Associate with existing flow +curl -X POST .../images/upload \ + -F "file=@reference.png" \ + -F "flowId=flow-123" \ + -F "flowAlias=@reference" +``` + +--- + +## List Images + +``` +GET /api/v1/images +``` + +List all images with filtering and pagination. + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `flowId` | string | - | Filter by flow UUID | +| `source` | string | - | Filter by source: `generated`, `uploaded` | +| `alias` | string | - | Filter by exact alias match | +| `limit` | number | `20` | Results per page (max: 100) | +| `offset` | number | `0` | Pagination offset | +| `includeDeleted` | boolean | `false` | Include soft-deleted records | + +**Example:** + +``` +GET /api/v1/images?source=uploaded&limit=10 +``` + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "7c4ccf47-...", + "storageUrl": "http://...", + "source": "uploaded", + "alias": "@brand-logo", + "width": 512, + "height": 512, + "createdAt": "2025-11-28T10:00:00.000Z" + } + ], + "pagination": { + "limit": 10, + "offset": 0, + "total": 25, + "hasMore": true + } +} +``` + +--- + +## Get Image + +``` +GET /api/v1/images/:id_or_alias +``` + +Get a single image by UUID or alias. + +**Path Parameter:** +- `id_or_alias` - Image UUID or `@alias` + +**Query Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `flowId` | string | Flow context for alias resolution | + +**Examples:** + +``` +# By UUID +GET /api/v1/images/7c4ccf47-41ce-4718-afbc-8c553b2c631a + +# By project alias +GET /api/v1/images/@brand-logo + +# By technical alias (requires flowId) +GET /api/v1/images/@last?flowId=flow-123 + +# By flow alias +GET /api/v1/images/@hero?flowId=flow-123 +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "7c4ccf47-41ce-4718-afbc-8c553b2c631a", + "projectId": "57c7f7f4-47de-4d70-9ebd-3807a0b63746", + "flowId": null, + "storageKey": "default/my-project/img/7c4ccf47-41ce-4718-afbc-8c553b2c631a", + "storageUrl": "https://cdn.banatie.app/default/my-project/img/7c4ccf47-41ce-4718-afbc-8c553b2c631a", + "mimeType": "image/png", + "fileSize": 45678, + "width": 512, + "height": 512, + "source": "uploaded", + "alias": "@brand-logo", + "focalPoint": null, + "fileHash": null, + "generationId": null, + "meta": { "tags": ["logo", "brand"] }, + "createdAt": "2025-11-28T10:00:00.000Z", + "updatedAt": "2025-11-28T10:00:00.000Z", + "deletedAt": null + } +} +``` + +--- + +## Update Image Metadata + +``` +PUT /api/v1/images/:id_or_alias +``` + +Update image metadata (focal point, custom metadata). + +**Request Body:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `focalPoint` | object | Focal point: `{ x: 0.0-1.0, y: 0.0-1.0 }` | +| `meta` | object | Custom metadata | + +**Example:** + +```json +// PUT /api/v1/images/@brand-logo +{ + "focalPoint": { "x": 0.5, "y": 0.3 }, + "meta": { + "description": "Updated brand logo", + "tags": ["logo", "brand", "2025"] + } +} +``` + +**Response:** Updated image object. + +> **Note:** Alias assignment has its own dedicated endpoint. + +--- + +## Assign Alias + +``` +PUT /api/v1/images/:id_or_alias/alias +``` + +Assign or remove a project-scoped alias. + +**Request Body:** + +```json +// Assign alias +{ "alias": "@new-logo" } + +// Remove alias +{ "alias": null } +``` + +**Override Behavior:** +- If another image has this alias, it loses the alias +- The new image gets the alias +- Old image is preserved, just unlinked + +**Example:** + +```bash +curl -X PUT http://localhost:3000/api/v1/images/7c4ccf47-.../alias \ + -H "X-API-Key: YOUR_KEY" \ + -H "Content-Type: application/json" \ + -d '{"alias": "@primary-logo"}' +``` + +--- + +## Delete Image + +``` +DELETE /api/v1/images/:id_or_alias +``` + +Permanently delete an image and its storage file. + +**Behavior:** +- **Hard delete** - image record permanently removed +- Storage file deleted from MinIO +- Cascading updates: + - Related generations: `outputImageId` set to null + - Flow aliases: image removed from flow's aliases + - Referenced images: removed from generation's referencedImages + +**Response:** `200 OK` + +```json +{ + "success": true, + "message": "Image deleted" +} +``` + +> **Warning:** This cannot be undone. The image file is permanently removed. + +--- + +## Image Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Image UUID (same as filename in storage) | +| `projectId` | string | Project UUID | +| `flowId` | string | Associated flow UUID (null if none) | +| `storageKey` | string | Storage path: `{org}/{project}/img/{imageId}` | +| `storageUrl` | string | CDN URL: `https://cdn.banatie.app/{org}/{project}/img/{imageId}` | +| `mimeType` | string | Image MIME type | +| `fileSize` | number | File size in bytes | +| `width` | number | Image width in pixels | +| `height` | number | Image height in pixels | +| `source` | string | `"generated"` or `"uploaded"` | +| `alias` | string | Project-scoped alias (null if none) | +| `focalPoint` | object | `{ x, y }` coordinates (0.0-1.0) | +| `fileHash` | string | SHA-256 hash for deduplication | +| `generationId` | string | Source generation UUID (if generated) | +| `meta` | object | Custom metadata | +| `createdAt` | string | ISO timestamp | +| `updatedAt` | string | ISO timestamp | +| `deletedAt` | string | Soft delete timestamp (null if active) | + +### Accessing Images + +Use `storageUrl` for direct CDN access: + +```html + + + + + +``` + +> **Note:** UUID URLs are served directly from MinIO (fast, cacheable). Alias URLs require API resolution. + +--- + +## Storage Organization + +Images are stored in MinIO with a simplified path structure: + +``` +bucket/ + {orgSlug}/ + {projectSlug}/ + img/ + {imageId} # UUID, no file extension + {imageId} # Content-Type in object metadata + ... +``` + +**Key points:** +- **Filename = Image ID (UUID)** - No file extensions +- **Content-Type** stored in MinIO object metadata +- **Original filename** preserved in metadata for reference +- **Single `img/` directory** for all images (generated + uploaded) + +--- + +## Error Codes + +| HTTP Status | Code | Description | +|-------------|------|-------------| +| 400 | `VALIDATION_ERROR` | Invalid parameters | +| 400 | `FILE_TOO_LARGE` | File exceeds 5MB limit | +| 400 | `UNSUPPORTED_FILE_TYPE` | Not PNG, JPEG, or WebP | +| 400 | `ALIAS_FORMAT_CHECK` | Alias must start with @ | +| 401 | `UNAUTHORIZED` | Missing or invalid API key | +| 404 | `IMAGE_NOT_FOUND` | Image or alias doesn't exist | +| 404 | `ALIAS_NOT_FOUND` | Alias doesn't resolve to any image | + +--- + +## See Also + +- [Basic Generation](image-generation.md) - Generate images +- [Advanced Generation](image-generation-advanced.md) - References, aliases, flows diff --git a/CLAUDE.md b/CLAUDE.md index 6e64073..4bc87a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,12 +15,7 @@ pnpm pdf -- # Convert HTML file to PDF pnpm remove-bg -- # Remove white background from PNG icons ``` -Generate images via Banatie API: -```bash -node src/scripts/banatie.mjs --prompt "forest theme" --output assets/backgrounds/forest.png --aspect-ratio 9:16 -node src/scripts/banatie.mjs --prompt "golden star" --output assets/icons/star.png -node src/scripts/banatie.mjs --prompt "similar gem" --output assets/icons/gem.png --ref assets/icons/sample.png -``` +Generate images via `/gen-image` skill (uses Banatie API, reads `BANATIE_KEY` from `.env`). ## Directory Structure @@ -31,7 +26,6 @@ src/ examples/space-worksheet2.html — Finished 3-page example (output reference) scripts/ generate-pdf.mjs — HTML → PDF via Puppeteer - banatie.mjs — Banatie API client for image generation remove-bg.mjs — Remove white background from PNGs (flood fill) tasks/ — JSON task definition files assets/ @@ -149,32 +143,9 @@ When generating HTML worksheets: - **Images in PDF:** Use local file paths (not URLs). Puppeteer resolves `file://` protocol - **Embed images** as base64 data URIs when possible for reliable PDF rendering -## Banatie API +## Image Generation -REST API at `https://api.banatie.app` for generating images. Auth via `X-API-Key` header (reads `BANATIE_KEY` from `.env`). - -**POST `/api/v1/generations`** — create generation: -- `prompt` (string, required) — image description -- `aspectRatio` — `1:1`, `16:9`, `9:16`, `3:2`, `4:3`, `3:4`, `21:9` (default: `1:1`) -- `referenceImages` (string[]) — `@alias` names of reference images -- `flowId` (string) — associate with a flow (for flow-scoped alias resolution) -- `autoEnhance` (boolean) — prompt enhancement (default: true) - -**POST `/api/v1/images/upload`** — upload image (for use as reference): -- Multipart form data: `file` (up to 5MB, JPEG/PNG/WebP), `alias` (@name), `flowId` -- Auto-creates a flow, returns `flowId` in response - -Response returns JSON with `data.outputImage.storageUrl` (CDN URL to download). - -Script `src/scripts/banatie.mjs` CLI: -```bash -node src/scripts/banatie.mjs --prompt "description" --output path.png [--aspect-ratio 1:1] [--ref file.png]... -``` -- `--ref` accepts local file paths or `@alias`. Can be repeated -- Local files are uploaded with auto-generated `@alias` into a shared flow -- The flow's `flowId` is passed to generation for alias resolution - -Rate limit: 100 requests/hour per API key. +Use the `/gen-image` skill to generate images via the Banatie API. The skill has its own script and full API documentation in `.claude/skills/gen-image/`. Auth via `BANATIE_KEY` in `.env`. Rate limit: 100 requests/hour. ## Background Removal