diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..71491fe2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# TypeScript cache +*.tsbuildinfo + +# OS files +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Environment / secrets +.env +.env.local +.env.*.local + +# npm +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Temporary files +/tmp/ +*.tmp diff --git a/README.md b/README.md index 18a96fca..a85635c3 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ npx clawra@latest This will: 1. Check OpenClaw is installed -2. Guide you to get a fal.ai API key +2. Guide you to select image provider(s) and enter API key(s) 3. Install the skill to `~/.openclaw/skills/clawra-selfie/` 4. Configure OpenClaw to use the skill 5. Add selfie capabilities to your agent's SOUL.md @@ -29,10 +29,19 @@ Clawra Selfie enables your OpenClaw agent to: | **Mirror** | Full-body shots, outfits | wearing, outfit, fashion | | **Direct** | Close-ups, locations | cafe, beach, portrait, smile | +## Supported Providers + +| Provider | Model | Env Variable | API Key URL | +|----------|-------|-------------|-------------| +| **fal** (default) | Grok Imagine via fal.ai | `FAL_KEY` | [fal.ai/dashboard/keys](https://fal.ai/dashboard/keys) | +| **openai** | gpt-image-1 | `OPENAI_API_KEY` | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) | +| **google** | gemini-2.5-flash-image | `GEMINI_API_KEY` | [aistudio.google.com/apikey](https://aistudio.google.com/apikey) | +| **xai** | grok-imagine-image (direct) | `XAI_API_KEY` | [console.x.ai](https://console.x.ai) | + ## Prerequisites - [OpenClaw](https://github.com/openclaw/openclaw) installed and configured -- [fal.ai](https://fal.ai) account (free tier available) +- At least one provider API key (see table above) ## Manual Installation @@ -40,7 +49,7 @@ If you prefer manual setup: ### 1. Get API Key -Visit [fal.ai/dashboard/keys](https://fal.ai/dashboard/keys) and create an API key. +Choose a provider and get its API key from the table above. ### 2. Clone the Skill @@ -59,6 +68,7 @@ Add to `~/.openclaw/openclaw.json`: "clawra-selfie": { "enabled": true, "env": { + "IMAGE_PROVIDER": "fal", "FAL_KEY": "your_fal_key_here" } } @@ -67,6 +77,36 @@ Add to `~/.openclaw/openclaw.json`: } ``` +Set `IMAGE_PROVIDER` to your chosen provider (`fal`, `openai`, `google`, or `xai`) and include the corresponding API key: + +| IMAGE_PROVIDER | Required Env Key | +|---------------|-----------------| +| `fal` | `FAL_KEY` | +| `openai` | `OPENAI_API_KEY` | +| `google` | `GEMINI_API_KEY` | +| `xai` | `XAI_API_KEY` | + +You can configure multiple providers at once: + +```json +{ + "skills": { + "entries": { + "clawra-selfie": { + "enabled": true, + "env": { + "IMAGE_PROVIDER": "openai", + "FAL_KEY": "your_fal_key", + "OPENAI_API_KEY": "your_openai_key", + "GEMINI_API_KEY": "your_gemini_key", + "XAI_API_KEY": "your_xai_key" + } + } + } + } +} +``` + ### 4. Update SOUL.md Add the selfie persona to `~/.openclaw/workspace/SOUL.md`: @@ -101,7 +141,8 @@ This ensures consistent appearance across all generated images. ## Technical Details -- **Image Generation**: xAI Grok Imagine via fal.ai +- **Image Generation**: Multiple providers (fal.ai, OpenAI, Google Gemini, x.ai) +- **Provider Selection**: Via `IMAGE_PROVIDER` env variable - **Messaging**: OpenClaw Gateway API - **Supported Platforms**: Discord, Telegram, WhatsApp, Slack, Signal, MS Teams @@ -110,13 +151,24 @@ This ensures consistent appearance across all generated images. ``` clawra/ ├── bin/ -│ └── cli.js # npx installer +│ └── cli.js # npx installer (multi-provider) +├── scripts/ +│ ├── providers/ # Provider abstraction layer +│ │ ├── types.ts # Shared interface +│ │ ├── fal.ts # fal.ai provider +│ │ ├── openai.ts # OpenAI provider +│ │ ├── google.ts # Google Gemini provider +│ │ ├── xai.ts # x.ai direct provider +│ │ └── index.ts # Factory function +│ ├── clawra-selfie.ts # TypeScript implementation +│ └── clawra-selfie.sh # Bash implementation ├── skill/ -│ ├── SKILL.md # Skill definition -│ ├── scripts/ # Generation scripts -│ └── assets/ # Reference image +│ ├── SKILL.md # Skill definition +│ ├── scripts/ # Generation scripts (copy) +│ └── assets/ # Reference image ├── templates/ -│ └── soul-injection.md # Persona template +│ └── soul-injection.md # Persona template +├── tsconfig.json └── package.json ``` diff --git a/SKILL.md b/SKILL.md index c09ce1db..d85beb3c 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,12 +1,23 @@ --- name: clawra-selfie -description: Edit Clawra's reference image with Grok Imagine (xAI Aurora) and send selfies to messaging channels via OpenClaw +description: Generate and edit images using multiple AI providers (fal.ai, OpenAI, Google Gemini, x.ai) and send selfies to messaging channels via OpenClaw allowed-tools: Bash(npm:*) Bash(npx:*) Bash(openclaw:*) Bash(curl:*) Read Write WebFetch --- # Clawra Selfie -Edit a fixed reference image using xAI's Grok Imagine model and distribute it across messaging platforms (WhatsApp, Telegram, Discord, Slack, etc.) via OpenClaw. +Generate and edit images using multiple AI providers and distribute them across messaging platforms (WhatsApp, Telegram, Discord, Slack, etc.) via OpenClaw. + +## Supported Providers + +| Provider | Model | Env Variable | API Key URL | +|----------|-------|-------------|-------------| +| **fal** (default) | Grok Imagine (via fal.ai) | `FAL_KEY` | https://fal.ai/dashboard/keys | +| **openai** | gpt-image-1 | `OPENAI_API_KEY` | https://platform.openai.com/api-keys | +| **google** | gemini-2.5-flash-image | `GEMINI_API_KEY` | https://aistudio.google.com/apikey | +| **xai** | grok-imagine-image (direct) | `XAI_API_KEY` | https://console.x.ai | + +Set the provider via the `IMAGE_PROVIDER` environment variable (default: `fal`). ## Reference Image @@ -29,16 +40,24 @@ https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png ### Required Environment Variables ```bash -FAL_KEY=your_fal_api_key # Get from https://fal.ai/dashboard/keys +# Pick ONE provider and set its API key: +IMAGE_PROVIDER=fal # Options: fal, openai, google, xai (default: fal) + +FAL_KEY=your_fal_api_key # For provider: fal +OPENAI_API_KEY=your_openai_key # For provider: openai +GEMINI_API_KEY=your_gemini_key # For provider: google +XAI_API_KEY=your_xai_key # For provider: xai + OPENCLAW_GATEWAY_TOKEN=your_token # From: openclaw doctor --generate-gateway-token ``` ### Workflow 1. **Get user prompt** for how to edit the image -2. **Edit image** via fal.ai Grok Imagine Edit API with fixed reference -3. **Extract image URL** from response -4. **Send to OpenClaw** with target channel(s) +2. **Select provider** (from env var or default) +3. **Generate/edit image** via selected provider +4. **Extract image URL** from response +5. **Send to OpenClaw** with target channel(s) ## Step-by-Step Instructions @@ -49,6 +68,7 @@ Ask the user for: - **Mode** (optional): `mirror` or `direct` selfie style - **Target channel(s)**: Where should it be sent? (e.g., `#general`, `@username`, channel ID) - **Platform** (optional): Which platform? (discord, telegram, whatsapp, slack) +- **Provider** (optional): Which image provider to use? ## Prompt Modes @@ -85,20 +105,13 @@ a close-up selfie taken by herself at a cozy cafe with warm lighting, direct eye | close-up, portrait, face, eyes, smile | `direct` | | full-body, mirror, reflection | `mirror` | -### Step 2: Edit Image with Grok Imagine +### Step 2: Generate/Edit Image -Use the fal.ai API to edit the reference image: +#### Provider: fal.ai (default) ```bash REFERENCE_IMAGE="https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png" -# Mode 1: Mirror Selfie -PROMPT="make a pic of this person, but . the person is taking a mirror selfie" - -# Mode 2: Direct Selfie -PROMPT="a close-up selfie taken by herself at , direct eye contact with the camera, looking straight into the lens, eyes centered and clearly visible, not a mirror selfie, phone held at arm's length, face fully visible" - -# Build JSON payload with jq (handles escaping properly) JSON_PAYLOAD=$(jq -n \ --arg image_url "$REFERENCE_IMAGE" \ --arg prompt "$PROMPT" \ @@ -110,24 +123,114 @@ curl -X POST "https://fal.run/xai/grok-imagine-image/edit" \ -d "$JSON_PAYLOAD" ``` -**Response Format:** +**Response:** ```json { - "images": [ - { - "url": "https://v3b.fal.media/files/...", - "content_type": "image/jpeg", - "width": 1024, - "height": 1024 - } - ], + "images": [{ "url": "https://v3b.fal.media/files/...", "width": 1024, "height": 1024 }], "revised_prompt": "Enhanced prompt text..." } ``` +#### Provider: OpenAI + +```bash +curl -X POST "https://api.openai.com/v1/images/generations" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-image-1", + "prompt": "A cute cat wearing a hat", + "n": 1, + "size": "1024x1024" + }' +``` + +**Response:** +```json +{ + "data": [{ "b64_json": "..." }] +} +``` + +> Note: OpenAI gpt-image-1 returns base64-encoded images. Save the `b64_json` field to a file. + +#### Provider: Google Gemini + +```bash +curl -s -X POST \ + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=$GEMINI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "contents": [{"parts": [{"text": "A cute cat wearing a hat"}]}], + "generationConfig": {"responseModalities": ["IMAGE"]} + }' +``` + +**Response:** +```json +{ + "candidates": [{ + "content": { + "parts": [{ "inlineData": { "mimeType": "image/png", "data": "" } }] + } + }] +} +``` + +> Note: Google Gemini returns base64 inline data. Decode the `data` field and save to a file. + +For image editing with Gemini, include the reference image as an `inlineData` part: +```bash +curl -s -X POST \ + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=$GEMINI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"contents\": [{ + \"parts\": [ + {\"text\": \"Edit this person to be wearing a santa hat\"}, + {\"inlineData\": {\"mimeType\": \"image/jpeg\", \"data\": \"\"}} + ] + }], + \"generationConfig\": {\"responseModalities\": [\"IMAGE\"]} + }" +``` + +#### Provider: x.ai (direct) + +```bash +curl -X POST "https://api.x.ai/v1/images/generations" \ + -H "Authorization: Bearer $XAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "grok-imagine-image", + "prompt": "A cute cat wearing a hat", + "n": 1 + }' +``` + +For image editing, add `image_url`: +```bash +curl -X POST "https://api.x.ai/v1/images/generations" \ + -H "Authorization: Bearer $XAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "grok-imagine-image", + "prompt": "Edit this to wear a santa hat", + "n": 1, + "image_url": "https://example.com/photo.jpg" + }' +``` + +**Response:** +```json +{ + "data": [{ "url": "https://..." }] +} +``` + ### Step 3: Send Image via OpenClaw -Use the OpenClaw messaging API to send the edited image: +Use the OpenClaw messaging API to send the generated image: ```bash openclaw message send \ @@ -154,191 +257,69 @@ curl -X POST "http://localhost:18789/message" \ ```bash #!/bin/bash -# grok-imagine-edit-send.sh - -# Check required environment variables -if [ -z "$FAL_KEY" ]; then - echo "Error: FAL_KEY environment variable not set" - exit 1 -fi - -# Fixed reference image -REFERENCE_IMAGE="https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png" +# Multi-provider image generation and send +PROVIDER="${IMAGE_PROVIDER:-fal}" USER_CONTEXT="$1" CHANNEL="$2" -MODE="${3:-auto}" # mirror, direct, or auto -CAPTION="${4:-Edited with Grok Imagine}" - -if [ -z "$USER_CONTEXT" ] || [ -z "$CHANNEL" ]; then - echo "Usage: $0 [mode] [caption]" - echo "Modes: mirror, direct, auto (default)" - echo "Example: $0 'wearing a cowboy hat' '#general' mirror" - echo "Example: $0 'a cozy cafe' '#general' direct" - exit 1 -fi - -# Auto-detect mode based on keywords -if [ "$MODE" == "auto" ]; then - if echo "$USER_CONTEXT" | grep -qiE "outfit|wearing|clothes|dress|suit|fashion|full-body|mirror"; then - MODE="mirror" - elif echo "$USER_CONTEXT" | grep -qiE "cafe|restaurant|beach|park|city|close-up|portrait|face|eyes|smile"; then - MODE="direct" - else - MODE="mirror" # default - fi - echo "Auto-detected mode: $MODE" -fi - -# Construct the prompt based on mode -if [ "$MODE" == "direct" ]; then - EDIT_PROMPT="a close-up selfie taken by herself at $USER_CONTEXT, direct eye contact with the camera, looking straight into the lens, eyes centered and clearly visible, not a mirror selfie, phone held at arm's length, face fully visible" -else - EDIT_PROMPT="make a pic of this person, but $USER_CONTEXT. the person is taking a mirror selfie" -fi - -echo "Mode: $MODE" -echo "Editing reference image with prompt: $EDIT_PROMPT" - -# Edit image (using jq for proper JSON escaping) -JSON_PAYLOAD=$(jq -n \ - --arg image_url "$REFERENCE_IMAGE" \ - --arg prompt "$EDIT_PROMPT" \ - '{image_url: $image_url, prompt: $prompt, num_images: 1, output_format: "jpeg"}') - -RESPONSE=$(curl -s -X POST "https://fal.run/xai/grok-imagine-image/edit" \ - -H "Authorization: Key $FAL_KEY" \ - -H "Content-Type: application/json" \ - -d "$JSON_PAYLOAD") - -# Extract image URL -IMAGE_URL=$(echo "$RESPONSE" | jq -r '.images[0].url') - -if [ "$IMAGE_URL" == "null" ] || [ -z "$IMAGE_URL" ]; then - echo "Error: Failed to edit image" - echo "Response: $RESPONSE" - exit 1 -fi - -echo "Image edited: $IMAGE_URL" -echo "Sending to channel: $CHANNEL" - -# Send via OpenClaw -openclaw message send \ - --action send \ - --channel "$CHANNEL" \ - --message "$CAPTION" \ - --media "$IMAGE_URL" - -echo "Done!" +CAPTION="${3:-Generated with Clawra}" + +# Route to provider +case "$PROVIDER" in + fal) + curl -s -X POST "https://fal.run/xai/grok-imagine-image" \ + -H "Authorization: Key $FAL_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"prompt\": \"$USER_CONTEXT\", \"num_images\": 1}" + ;; + openai) + curl -s -X POST "https://api.openai.com/v1/images/generations" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"model\": \"gpt-image-1\", \"prompt\": \"$USER_CONTEXT\", \"n\": 1, \"size\": \"1024x1024\"}" + ;; + google) + curl -s -X POST \ + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=$GEMINI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"contents\": [{\"parts\": [{\"text\": \"$USER_CONTEXT\"}]}], \"generationConfig\": {\"responseModalities\": [\"IMAGE\"]}}" + ;; + xai) + curl -s -X POST "https://api.x.ai/v1/images/generations" \ + -H "Authorization: Bearer $XAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"model\": \"grok-imagine-image\", \"prompt\": \"$USER_CONTEXT\", \"n\": 1}" + ;; +esac ``` +For the full implementation with error handling, mode detection, and OpenClaw integration, see the scripts in `scripts/clawra-selfie.sh` or `scripts/clawra-selfie.ts`. + ## Node.js/TypeScript Implementation ```typescript -import { fal } from "@fal-ai/client"; -import { exec } from "child_process"; -import { promisify } from "util"; - -const execAsync = promisify(exec); - -const REFERENCE_IMAGE = "https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png"; - -interface GrokImagineResult { - images: Array<{ - url: string; - content_type: string; - width: number; - height: number; - }>; - revised_prompt?: string; -} - -type SelfieMode = "mirror" | "direct" | "auto"; - -function detectMode(userContext: string): "mirror" | "direct" { - const mirrorKeywords = /outfit|wearing|clothes|dress|suit|fashion|full-body|mirror/i; - const directKeywords = /cafe|restaurant|beach|park|city|close-up|portrait|face|eyes|smile/i; - - if (directKeywords.test(userContext)) return "direct"; - if (mirrorKeywords.test(userContext)) return "mirror"; - return "mirror"; // default -} - -function buildPrompt(userContext: string, mode: "mirror" | "direct"): string { - if (mode === "direct") { - return `a close-up selfie taken by herself at ${userContext}, direct eye contact with the camera, looking straight into the lens, eyes centered and clearly visible, not a mirror selfie, phone held at arm's length, face fully visible`; - } - return `make a pic of this person, but ${userContext}. the person is taking a mirror selfie`; -} - -async function editAndSend( - userContext: string, - channel: string, - mode: SelfieMode = "auto", - caption?: string -): Promise { - // Configure fal.ai client - fal.config({ - credentials: process.env.FAL_KEY! +import { getProvider } from "./providers"; + +// Generate image with any provider +const provider = getProvider("openai"); // or "fal", "google", "xai" +const result = await provider.generateImage({ + prompt: "A sunset over the ocean", + numImages: 1, + aspectRatio: "16:9", + outputFormat: "jpeg", +}); + +console.log(result.images[0].url); + +// Edit image (fal, google, xai support editing) +if (provider.editImage) { + const edited = await provider.editImage({ + imageUrl: "https://example.com/photo.jpg", + prompt: "Add a santa hat", + numImages: 1, }); - - // Determine mode - const actualMode = mode === "auto" ? detectMode(userContext) : mode; - console.log(`Mode: ${actualMode}`); - - // Construct the prompt - const editPrompt = buildPrompt(userContext, actualMode); - - // Edit reference image with Grok Imagine - console.log(`Editing image: "${editPrompt}"`); - - const result = await fal.subscribe("xai/grok-imagine-image/edit", { - input: { - image_url: REFERENCE_IMAGE, - prompt: editPrompt, - num_images: 1, - output_format: "jpeg" - } - }) as { data: GrokImagineResult }; - - const imageUrl = result.data.images[0].url; - console.log(`Edited image URL: ${imageUrl}`); - - // Send via OpenClaw - const messageCaption = caption || `Edited with Grok Imagine`; - - await execAsync( - `openclaw message send --action send --channel "${channel}" --message "${messageCaption}" --media "${imageUrl}"` - ); - - console.log(`Sent to ${channel}`); - return imageUrl; + console.log(edited.images[0].url); } - -// Usage Examples - -// Mirror mode (auto-detected from "wearing") -editAndSend( - "wearing a cyberpunk outfit with neon lights", - "#art-gallery", - "auto", - "Check out this AI-edited art!" -); -// → Mode: mirror -// → Prompt: "make a pic of this person, but wearing a cyberpunk outfit with neon lights. the person is taking a mirror selfie" - -// Direct mode (auto-detected from "cafe") -editAndSend( - "a cozy cafe with warm lighting", - "#photography", - "auto" -); -// → Mode: direct -// → Prompt: "a close-up selfie taken by herself at a cozy cafe with warm lighting, direct eye contact..." - -// Explicit mode override -editAndSend("casual street style", "#fashion", "direct"); ``` ## Supported Platforms @@ -354,20 +335,51 @@ OpenClaw supports sending to: | Signal | Phone number | `+1234567890` | | MS Teams | Channel reference | (varies) | -## Grok Imagine Edit Parameters +## Provider-Specific Parameters + +### fal.ai (Grok Imagine) | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `image_url` | string | required | URL of image to edit (fixed in this skill) | -| `prompt` | string | required | Edit instruction | +| `image_url` | string | - | URL of image to edit | +| `prompt` | string | required | Generation/edit instruction | | `num_images` | 1-4 | 1 | Number of images to generate | +| `aspect_ratio` | enum | "1:1" | 2:1, 16:9, 4:3, 1:1, 3:4, 9:16 | | `output_format` | enum | "jpeg" | jpeg, png, webp | +### OpenAI (gpt-image-1) + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `model` | string | "gpt-image-1" | Model to use | +| `prompt` | string | required | Image description | +| `n` | 1-10 | 1 | Number of images | +| `size` | enum | "1024x1024" | 1024x1024, 1536x1024, 1024x1536 | +| `output_format` | enum | "png" | png, jpeg, webp | + +### Google Gemini + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `contents` | array | required | Prompt parts (text + optional image) | +| `responseModalities` | array | ["IMAGE"] | Must include "IMAGE" | +| `imageMimeType` | string | - | Output mime type | + +### x.ai (Grok Imagine Direct) + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `model` | string | "grok-imagine-image" | Model to use | +| `prompt` | string | required | Image description | +| `n` | number | 1 | Number of images | +| `image_url` | string | - | Source image for editing | +| `aspect_ratio` | enum | - | 1:1, 16:9, 9:16, etc. | + ## Setup Requirements -### 1. Install fal.ai client (for Node.js usage) +### 1. Install dependencies (for Node.js usage) ```bash -npm install @fal-ai/client +npm install @fal-ai/client # only needed for fal provider ``` ### 2. Install OpenClaw CLI @@ -388,10 +400,11 @@ openclaw gateway start ## Error Handling -- **FAL_KEY missing**: Ensure the API key is set in environment -- **Image edit failed**: Check prompt content and API quota +- **API key missing**: Ensure the correct key is set for your chosen provider +- **Image generation failed**: Check prompt content and API quota - **OpenClaw send failed**: Verify gateway is running and channel exists -- **Rate limits**: fal.ai has rate limits; implement retry logic if needed +- **Base64 responses**: OpenAI and Google return base64 data; scripts auto-save to temp files +- **Rate limits**: Each provider has its own rate limits; implement retry logic if needed ## Tips @@ -399,14 +412,18 @@ openclaw gateway start - "wearing a santa hat" - "in a business suit" - "wearing a summer dress" - - "in streetwear fashion" 2. **Direct mode context examples** (location/portrait focus): - "a cozy cafe with warm lighting" - "a sunny beach at sunset" - "a busy city street at night" - - "a peaceful park in autumn" -3. **Mode selection**: Let auto-detect work, or explicitly specify for control -4. **Batch sending**: Edit once, send to multiple channels -5. **Scheduling**: Combine with OpenClaw scheduler for automated posts +3. **Provider selection**: + - Use `fal` for quick Grok Imagine access via fal.ai proxy + - Use `openai` for GPT-image-1's strong prompt following + - Use `google` for Gemini's integrated image generation + - Use `xai` for direct x.ai access without fal.ai proxy + +4. **Mode selection**: Let auto-detect work, or explicitly specify for control +5. **Batch sending**: Generate once, send to multiple channels +6. **Scheduling**: Combine with OpenClaw scheduler for automated posts diff --git a/bin/cli.js b/bin/cli.js index 86f6d6c4..ed40f2e0 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -162,15 +162,23 @@ function copyDir(src, dest) { } } +// Provider definitions +const PROVIDERS = { + fal: { name: "fal.ai (Grok Imagine)", envKey: "FAL_KEY", url: "https://fal.ai/dashboard/keys" }, + openai: { name: "OpenAI (gpt-image-1)", envKey: "OPENAI_API_KEY", url: "https://platform.openai.com/api-keys" }, + google: { name: "Google Gemini", envKey: "GEMINI_API_KEY", url: "https://aistudio.google.com/apikey" }, + xai: { name: "x.ai (Grok Imagine)", envKey: "XAI_API_KEY", url: "https://console.x.ai" }, +}; + // Print banner function printBanner() { console.log(` -${c("magenta", "┌─────────────────────────────────────────┐")} -${c("magenta", "│")} ${c("bright", "Clawra Selfie")} - OpenClaw Skill Installer ${c("magenta", "│")} -${c("magenta", "└─────────────────────────────────────────┘")} +${c("magenta", "┌──────────────────────────────────────────────────────┐")} +${c("magenta", "│")} ${c("bright", "Clawra Selfie")} - Multi-Provider Skill Installer ${c("magenta", "│")} +${c("magenta", "└──────────────────────────────────────────────────────┘")} Add selfie generation superpowers to your OpenClaw agent! -Uses ${c("cyan", "xAI Grok Imagine")} via ${c("cyan", "fal.ai")} for image editing. +Supported providers: ${c("cyan", "fal.ai")}, ${c("cyan", "OpenAI")}, ${c("cyan", "Google Gemini")}, ${c("cyan", "x.ai")} `); } @@ -207,40 +215,75 @@ async function checkPrerequisites() { return true; } -// Get FAL API key -async function getFalApiKey(rl) { - logStep("2/7", "Setting up fal.ai API key..."); +// Get API keys for selected providers +async function getApiKeys(rl) { + logStep("2/7", "Setting up image provider API keys..."); - const FAL_URL = "https://fal.ai/dashboard/keys"; + log(`\nWhich image providers would you like to configure?`); + log(`You need at least one provider. You can add more later.\n`); - log(`\nTo use Grok Imagine, you need a fal.ai API key.`); - log(`${c("cyan", "→")} Get your key from: ${c("bright", FAL_URL)}\n`); + const providerIds = Object.keys(PROVIDERS); + for (let i = 0; i < providerIds.length; i++) { + const id = providerIds[i]; + const p = PROVIDERS[id]; + log(` ${c("cyan", `${i + 1})`)} ${p.name} (${c("dim", p.envKey)})`); + } + log(""); - const openIt = await ask(rl, "Open fal.ai in browser? (Y/n): "); + const selection = await ask( + rl, + `Enter provider numbers (comma-separated, default: 1): ` + ); - if (openIt.toLowerCase() !== "n") { - logInfo("Opening browser..."); - if (!openBrowser(FAL_URL)) { - logWarn("Could not open browser automatically"); - logInfo(`Please visit: ${FAL_URL}`); - } + const selectedNums = (selection || "1") + .split(",") + .map((s) => parseInt(s.trim(), 10) - 1) + .filter((n) => n >= 0 && n < providerIds.length); + + if (selectedNums.length === 0) { + selectedNums.push(0); // default to fal } - log(""); - const falKey = await ask(rl, "Enter your FAL_KEY: "); + const apiKeys = {}; - if (!falKey) { - logError("FAL_KEY is required!"); - return null; + for (const idx of selectedNums) { + const id = providerIds[idx]; + const p = PROVIDERS[id]; + + log(`\n${c("bright", `Setting up ${p.name}...`)}`); + log(`${c("cyan", "→")} Get your key from: ${c("bright", p.url)}\n`); + + const openIt = await ask(rl, `Open ${p.name} in browser? (Y/n): `); + if (openIt.toLowerCase() !== "n") { + logInfo("Opening browser..."); + if (!openBrowser(p.url)) { + logWarn("Could not open browser automatically"); + logInfo(`Please visit: ${p.url}`); + } + } + + log(""); + const key = await ask(rl, `Enter your ${p.envKey}: `); + + if (!key) { + logWarn(`Skipping ${p.name} (no key provided)`); + continue; + } + + if (key.length < 10) { + logWarn("That key looks too short. Make sure you copied the full key."); + } + + apiKeys[id] = key; + logSuccess(`${p.name} API key received`); } - // Basic validation - if (falKey.length < 10) { - logWarn("That key looks too short. Make sure you copied the full key."); + if (Object.keys(apiKeys).length === 0) { + logError("At least one API key is required!"); + return null; } - logSuccess("API key received"); - return falKey; + return apiKeys; } // Install skill files @@ -287,21 +330,31 @@ async function installSkill() { } // Update OpenClaw config -async function updateOpenClawConfig(falKey) { +async function updateOpenClawConfig(apiKeys) { logStep("4/7", "Updating OpenClaw configuration..."); let config = readJsonFile(OPENCLAW_CONFIG) || {}; + // Build env vars from all configured providers + const env = {}; + for (const [providerId, key] of Object.entries(apiKeys)) { + const p = PROVIDERS[providerId]; + if (p) { + env[p.envKey] = key; + } + } + + // Set default provider to the first configured one + const defaultProvider = Object.keys(apiKeys)[0]; + env.IMAGE_PROVIDER = defaultProvider; + // Merge skill configuration const skillConfig = { skills: { entries: { [SKILL_NAME]: { enabled: true, - apiKey: falKey, - env: { - FAL_KEY: falKey, - }, + env, }, }, }, @@ -322,6 +375,11 @@ async function updateOpenClawConfig(falKey) { writeJsonFile(OPENCLAW_CONFIG, config); logSuccess(`Updated: ${OPENCLAW_CONFIG}`); + logInfo(`Default provider: ${PROVIDERS[defaultProvider].name}`); + + for (const [providerId] of Object.entries(apiKeys)) { + logInfo(`Configured: ${PROVIDERS[providerId].name}`); + } return true; } @@ -437,12 +495,15 @@ ${c("cyan", "Identity set:")} ${c("cyan", "Persona updated:")} ${SOUL_MD} +${c("cyan", "Switch provider:")} + Set IMAGE_PROVIDER to: fal, openai, google, or xai + ${c("yellow", "Try saying to your agent:")} "Send me a selfie" "Send a pic wearing a cowboy hat" "What are you doing right now?" -${c("dim", "Your agent now has selfie superpowers!")} +${c("dim", "Your agent now has multi-provider selfie superpowers!")} `); } @@ -485,9 +546,9 @@ async function main() { } } - // Step 2: Get FAL API key - const falKey = await getFalApiKey(rl); - if (!falKey) { + // Step 2: Get API keys (multi-provider) + const apiKeys = await getApiKeys(rl); + if (!apiKeys) { rl.close(); process.exit(1); } @@ -496,7 +557,7 @@ async function main() { await installSkill(); // Step 4: Update OpenClaw config - await updateOpenClawConfig(falKey); + await updateOpenClawConfig(apiKeys); // Step 5: Write IDENTITY.md await writeIdentity(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..c91ce29a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,285 @@ +{ + "name": "clawra", + "version": "1.1.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "clawra", + "version": "1.1.1", + "license": "MIT", + "bin": { + "clawra": "bin/cli.js" + }, + "devDependencies": { + "@fal-ai/client": "^1.2.0", + "@types/node": "^22.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@fal-ai/client": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@fal-ai/client/-/client-1.9.0.tgz", + "integrity": "sha512-okIXfgrjiRlens0CGbg50LFR0cZYfIG+DehC5ObTQUwSziOzqlytkFDi98dqbgvtfu4Ecbtjz90rj5/EVSEZog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@msgpack/msgpack": "^3.0.0-beta2", + "eventsource-parser": "^1.1.2", + "robot3": "^0.4.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eventsource-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", + "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/robot3": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/robot3/-/robot3-0.4.1.tgz", + "integrity": "sha512-hzjy826lrxzx8eRgv80idkf8ua1JAepRc9Efdtj03N3KNJuznQCPlyCJ7gnUmDFwZCLQjxy567mQVKmdv2BsXQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json index aff645bf..4d50b1fb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "clawra", "version": "1.1.1", - "description": "Add selfie superpowers to your OpenClaw agent using xAI Grok Imagine", + "description": "Add selfie superpowers to your OpenClaw agent with multi-provider image generation (fal.ai, OpenAI, Google Gemini, x.ai)", "bin": { "clawra": "./bin/cli.js" }, @@ -20,6 +20,10 @@ "xai", "aurora", "fal.ai", + "openai", + "gemini", + "google", + "imagen", "selfie", "ai-agent", "image-generation", @@ -42,4 +46,4 @@ "ts-node": "^10.9.2", "typescript": "^5.7.0" } -} +} \ No newline at end of file diff --git a/scripts/clawra-selfie.sh b/scripts/clawra-selfie.sh index 72d176d5..e7caafa1 100755 --- a/scripts/clawra-selfie.sh +++ b/scripts/clawra-selfie.sh @@ -1,14 +1,18 @@ #!/bin/bash -# grok-imagine-send.sh -# Generate an image with Grok Imagine and send it via OpenClaw +# clawra-selfie.sh +# Generate an image with multiple AI providers and send it via OpenClaw # -# Usage: ./grok-imagine-send.sh "" "" [""] +# Supported providers (set via IMAGE_PROVIDER env var): +# fal - fal.ai / Grok Imagine (requires FAL_KEY) +# openai - OpenAI gpt-image-1 (requires OPENAI_API_KEY) +# google - Google Gemini (requires GEMINI_API_KEY) +# xai - x.ai direct (requires XAI_API_KEY) # -# Environment variables required: -# FAL_KEY - Your fal.ai API key +# Usage: ./clawra-selfie.sh "" "" [""] [aspect_ratio] [output_format] # # Example: -# FAL_KEY=your_key ./grok-imagine-send.sh "A sunset over mountains" "#art" "Check this out!" +# FAL_KEY=your_key ./clawra-selfie.sh "A sunset over mountains" "#art" "Check this out!" +# IMAGE_PROVIDER=openai OPENAI_API_KEY=xxx ./clawra-selfie.sh "A sunset" "#art" set -euo pipefail @@ -30,13 +34,6 @@ log_error() { echo -e "${RED}[ERROR]${NC} $1" } -# Check required environment variables -if [ -z "${FAL_KEY:-}" ]; then - log_error "FAL_KEY environment variable not set" - echo "Get your API key from: https://fal.ai/dashboard/keys" - exit 1 -fi - # Check for jq if ! command -v jq &> /dev/null; then log_error "jq is required but not installed" @@ -55,9 +52,10 @@ fi # Parse arguments PROMPT="${1:-}" CHANNEL="${2:-}" -CAPTION="${3:-Generated with Grok Imagine}" +CAPTION="${3:-Generated with Clawra}" ASPECT_RATIO="${4:-1:1}" OUTPUT_FORMAT="${5:-jpeg}" +PROVIDER="${IMAGE_PROVIDER:-fal}" if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then echo "Usage: $0 [caption] [aspect_ratio] [output_format]" @@ -65,51 +63,222 @@ if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then echo "Arguments:" echo " prompt - Image description (required)" echo " channel - Target channel (required) e.g., #general, @user" - echo " caption - Message caption (default: 'Generated with Grok Imagine')" + echo " caption - Message caption (default: 'Generated with Clawra')" echo " aspect_ratio - Image ratio (default: 1:1) Options: 2:1, 16:9, 4:3, 1:1, 3:4, 9:16" echo " output_format - Image format (default: jpeg) Options: jpeg, png, webp" echo "" + echo "Environment:" + echo " IMAGE_PROVIDER - Provider to use (default: fal)" + echo " Options: fal, openai, google, xai" + echo " FAL_KEY - Required for provider: fal" + echo " OPENAI_API_KEY - Required for provider: openai" + echo " GEMINI_API_KEY - Required for provider: google" + echo " XAI_API_KEY - Required for provider: xai" + echo "" echo "Example:" echo " $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\"" + echo " IMAGE_PROVIDER=openai $0 \"A sunset\" \"#photos\"" exit 1 fi -log_info "Generating image with Grok Imagine..." +log_info "Provider: $PROVIDER" +log_info "Generating image..." log_info "Prompt: $PROMPT" log_info "Aspect ratio: $ASPECT_RATIO" -# Generate image via fal.ai -RESPONSE=$(curl -s -X POST "https://fal.run/xai/grok-imagine-image" \ - -H "Authorization: Key $FAL_KEY" \ - -H "Content-Type: application/json" \ - -d "{ - \"prompt\": $(echo "$PROMPT" | jq -Rs .), - \"num_images\": 1, - \"aspect_ratio\": \"$ASPECT_RATIO\", - \"output_format\": \"$OUTPUT_FORMAT\" - }") - -# Check for errors in response -if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then - ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error // .detail // "Unknown error"') - log_error "Image generation failed: $ERROR_MSG" - exit 1 -fi +# ============================================================================ +# Provider: fal.ai (Grok Imagine) +# ============================================================================ +generate_fal() { + if [ -z "${FAL_KEY:-}" ]; then + log_error "FAL_KEY environment variable not set" + echo "Get your API key from: https://fal.ai/dashboard/keys" + exit 1 + fi + + RESPONSE=$(curl -s -X POST "https://fal.run/xai/grok-imagine-image" \ + -H "Authorization: Key $FAL_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"prompt\": $(echo "$PROMPT" | jq -Rs .), + \"num_images\": 1, + \"aspect_ratio\": \"$ASPECT_RATIO\", + \"output_format\": \"$OUTPUT_FORMAT\" + }") + + # Check for error (ensure it's not null/empty) + ERROR_VAL=$(echo "$RESPONSE" | jq -r '.error // .detail // empty') + if [ -n "$ERROR_VAL" ]; then + log_error "fal.ai image generation failed: $ERROR_VAL" + exit 1 + fi + + # Try multiple response paths: images[0].url, data[0].url + IMAGE_URL=$(echo "$RESPONSE" | jq -r '.images[0].url // .data[0].url // empty') + REVISED_PROMPT=$(echo "$RESPONSE" | jq -r '.revised_prompt // empty') + + if [ -z "$IMAGE_URL" ]; then + log_warn "Could not extract image URL from fal.ai response" + log_warn "Response: $(echo "$RESPONSE" | head -c 500)" + fi +} + +# ============================================================================ +# Provider: OpenAI (gpt-image-1) +# ============================================================================ +generate_openai() { + if [ -z "${OPENAI_API_KEY:-}" ]; then + log_error "OPENAI_API_KEY environment variable not set" + echo "Get your API key from: https://platform.openai.com/api-keys" + exit 1 + fi + + # Map aspect ratio to OpenAI size + case "$ASPECT_RATIO" in + 16:9|2:1|20:9|19.5:9|3:2) SIZE="1536x1024" ;; + 9:16|1:2|9:19.5|9:20|2:3|3:4) SIZE="1024x1536" ;; + *) SIZE="1024x1024" ;; + esac + + RESPONSE=$(curl -s -X POST "https://api.openai.com/v1/images/generations" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"model\": \"gpt-image-1\", + \"prompt\": $(echo "$PROMPT" | jq -Rs .), + \"n\": 1, + \"size\": \"$SIZE\" + }") + + if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then + ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // .error // "Unknown error"') + log_error "OpenAI image generation failed: $ERROR_MSG" + exit 1 + fi + + # OpenAI gpt-image-1 returns base64 – save to temp file + B64_DATA=$(echo "$RESPONSE" | jq -r '.data[0].b64_json // empty') + URL_DATA=$(echo "$RESPONSE" | jq -r '.data[0].url // empty') -# Extract image URL -IMAGE_URL=$(echo "$RESPONSE" | jq -r '.images[0].url // empty') + if [ -n "$URL_DATA" ]; then + IMAGE_URL="$URL_DATA" + elif [ -n "$B64_DATA" ]; then + TMPFILE=$(mktemp /tmp/clawra-openai-XXXXXX."$OUTPUT_FORMAT") + echo "$B64_DATA" | base64 -d > "$TMPFILE" + IMAGE_URL="$TMPFILE" + log_info "Saved to temp file: $TMPFILE" + else + log_error "OpenAI response contained neither url nor b64_json" + echo "Response: $RESPONSE" + exit 1 + fi + + REVISED_PROMPT=$(echo "$RESPONSE" | jq -r '.data[0].revised_prompt // empty') +} +# ============================================================================ +# Provider: Google Gemini (gemini-2.5-flash-image) +# ============================================================================ +generate_google() { + if [ -z "${GEMINI_API_KEY:-}" ]; then + log_error "GEMINI_API_KEY environment variable not set" + echo "Get your API key from: https://aistudio.google.com/apikey" + exit 1 + fi + + local MODEL="gemini-2.5-flash-image" + + RESPONSE=$(curl -s -X POST \ + "https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${GEMINI_API_KEY}" \ + -H "Content-Type: application/json" \ + -d "{ + \"contents\": [{ + \"parts\": [{\"text\": $(echo "$PROMPT" | jq -Rs .)}] + }], + \"generationConfig\": { + \"responseModalities\": [\"IMAGE\"] + } + }") + + if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then + ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // .error // "Unknown error"') + log_error "Google Gemini image generation failed: $ERROR_MSG" + exit 1 + fi + + # Extract base64 image data from Gemini response + B64_DATA=$(echo "$RESPONSE" | jq -r '.candidates[0].content.parts[] | select(.inlineData) | .inlineData.data // empty' | head -1) + + if [ -z "$B64_DATA" ]; then + log_error "Google Gemini returned no image data" + echo "Response: $RESPONSE" + exit 1 + fi + + TMPFILE=$(mktemp /tmp/clawra-gemini-XXXXXX."$OUTPUT_FORMAT") + echo "$B64_DATA" | base64 -d > "$TMPFILE" + IMAGE_URL="$TMPFILE" + log_info "Saved to temp file: $TMPFILE" + + REVISED_PROMPT="" +} + +# ============================================================================ +# Provider: x.ai direct (grok-imagine-image) +# ============================================================================ +generate_xai() { + if [ -z "${XAI_API_KEY:-}" ]; then + log_error "XAI_API_KEY environment variable not set" + echo "Get your API key from: https://console.x.ai" + exit 1 + fi + + RESPONSE=$(curl -s -X POST "https://api.x.ai/v1/images/generations" \ + -H "Authorization: Bearer $XAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"model\": \"grok-imagine-image\", + \"prompt\": $(echo "$PROMPT" | jq -Rs .), + \"n\": 1 + }") + + if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then + ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // .error // "Unknown error"') + log_error "x.ai image generation failed: $ERROR_MSG" + exit 1 + fi + + IMAGE_URL=$(echo "$RESPONSE" | jq -r '.data[0].url // empty') + REVISED_PROMPT="" +} + +# ============================================================================ +# Route to the correct provider +# ============================================================================ +IMAGE_URL="" +REVISED_PROMPT="" + +case "$PROVIDER" in + fal) generate_fal ;; + openai) generate_openai ;; + google) generate_google ;; + xai) generate_xai ;; + *) + log_error "Unknown provider: $PROVIDER" + echo "Supported providers: fal, openai, google, xai" + exit 1 + ;; +esac + +# Validate result if [ -z "$IMAGE_URL" ]; then log_error "Failed to extract image URL from response" - echo "Response: $RESPONSE" exit 1 fi log_info "Image generated successfully!" log_info "URL: $IMAGE_URL" -# Get revised prompt if available -REVISED_PROMPT=$(echo "$RESPONSE" | jq -r '.revised_prompt // empty') if [ -n "$REVISED_PROMPT" ]; then log_info "Revised prompt: $REVISED_PROMPT" fi @@ -118,22 +287,15 @@ fi log_info "Sending to channel: $CHANNEL" if [ "$USE_CLI" = true ]; then - # Use OpenClaw CLI openclaw message send \ --action send \ --channel "$CHANNEL" \ --message "$CAPTION" \ --media "$IMAGE_URL" else - # Direct API call to local gateway GATEWAY_URL="${OPENCLAW_GATEWAY_URL:-http://localhost:18789}" GATEWAY_TOKEN="${OPENCLAW_GATEWAY_TOKEN:-}" - HEADERS="-H \"Content-Type: application/json\"" - if [ -n "$GATEWAY_TOKEN" ]; then - HEADERS="$HEADERS -H \"Authorization: Bearer $GATEWAY_TOKEN\"" - fi - curl -s -X POST "$GATEWAY_URL/message" \ -H "Content-Type: application/json" \ ${GATEWAY_TOKEN:+-H "Authorization: Bearer $GATEWAY_TOKEN"} \ @@ -154,9 +316,11 @@ jq -n \ --arg url "$IMAGE_URL" \ --arg channel "$CHANNEL" \ --arg prompt "$PROMPT" \ + --arg provider "$PROVIDER" \ '{ success: true, image_url: $url, channel: $channel, - prompt: $prompt + prompt: $prompt, + provider: $provider }' diff --git a/scripts/clawra-selfie.ts b/scripts/clawra-selfie.ts index e15f644a..4b336910 100644 --- a/scripts/clawra-selfie.ts +++ b/scripts/clawra-selfie.ts @@ -1,43 +1,45 @@ /** - * Grok Imagine to OpenClaw Integration + * Clawra Selfie – Multi-Provider Image Generation & Editing * - * Generates images using xAI's Grok Imagine model via fal.ai - * and sends them to messaging channels via OpenClaw. + * Generates or edits images using multiple AI providers and sends them + * to messaging channels via OpenClaw. + * + * Supported providers (set via IMAGE_PROVIDER env var): + * - fal (fal.ai / Grok Imagine) – requires FAL_KEY + * - openai (OpenAI gpt-image-1) – requires OPENAI_API_KEY + * - google (Google Gemini) – requires GEMINI_API_KEY + * - xai (x.ai direct) – requires XAI_API_KEY * * Usage: - * npx ts-node grok-imagine-send.ts "" "" [""] + * npx ts-node clawra-selfie.ts [caption] [aspect_ratio] [output_format] [provider] * * Environment variables: - * FAL_KEY - Your fal.ai API key - * OPENCLAW_GATEWAY_URL - OpenClaw gateway URL (default: http://localhost:18789) - * OPENCLAW_GATEWAY_TOKEN - Gateway auth token (optional) + * IMAGE_PROVIDER - Provider to use (default: fal) + * FAL_KEY / OPENAI_API_KEY / GEMINI_API_KEY / XAI_API_KEY + * OPENCLAW_GATEWAY_URL - OpenClaw gateway URL (default: http://localhost:18789) + * OPENCLAW_GATEWAY_TOKEN - Gateway auth token (optional) */ import { exec } from "child_process"; import { promisify } from "util"; +import { + getProvider, + PROVIDER_ENV_KEYS, + AVAILABLE_PROVIDERS, +} from "./providers"; +import type { + AspectRatio, + OutputFormat, + ProviderName, + GenerateImageResult, +} from "./providers"; + const execAsync = promisify(exec); +// --------------------------------------------------------------------------- // Types -interface GrokImagineInput { - prompt: string; - num_images?: number; - aspect_ratio?: AspectRatio; - output_format?: OutputFormat; -} - -interface GrokImagineImage { - url: string; - content_type: string; - file_name?: string; - width: number; - height: number; -} - -interface GrokImagineResponse { - images: GrokImagineImage[]; - revised_prompt?: string; -} +// --------------------------------------------------------------------------- interface OpenClawMessage { action: "send"; @@ -46,29 +48,13 @@ interface OpenClawMessage { media?: string; } -type AspectRatio = - | "2:1" - | "20:9" - | "19.5:9" - | "16:9" - | "4:3" - | "3:2" - | "1:1" - | "2:3" - | "3:4" - | "9:16" - | "9:19.5" - | "9:20" - | "1:2"; - -type OutputFormat = "jpeg" | "png" | "webp"; - interface GenerateAndSendOptions { prompt: string; channel: string; caption?: string; aspectRatio?: AspectRatio; outputFormat?: OutputFormat; + provider?: ProviderName; useClaudeCodeCLI?: boolean; } @@ -77,87 +63,24 @@ interface Result { imageUrl: string; channel: string; prompt: string; + provider: string; revisedPrompt?: string; } -// Check for fal.ai client -let falClient: any; -try { - const { fal } = require("@fal-ai/client"); - falClient = fal; -} catch { - // Will use fetch instead - falClient = null; -} - -/** - * Generate image using Grok Imagine via fal.ai - */ -async function generateImage( - input: GrokImagineInput -): Promise { - const falKey = process.env.FAL_KEY; - - if (!falKey) { - throw new Error( - "FAL_KEY environment variable not set. Get your key from https://fal.ai/dashboard/keys" - ); - } - - // Use fal client if available - if (falClient) { - falClient.config({ credentials: falKey }); - - const result = await falClient.subscribe("xai/grok-imagine-image", { - input: { - prompt: input.prompt, - num_images: input.num_images || 1, - aspect_ratio: input.aspect_ratio || "1:1", - output_format: input.output_format || "jpeg", - }, - }); - - return result.data as GrokImagineResponse; - } - - // Fallback to fetch - const response = await fetch("https://fal.run/xai/grok-imagine-image", { - method: "POST", - headers: { - Authorization: `Key ${falKey}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - prompt: input.prompt, - num_images: input.num_images || 1, - aspect_ratio: input.aspect_ratio || "1:1", - output_format: input.output_format || "jpeg", - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Image generation failed: ${error}`); - } +// --------------------------------------------------------------------------- +// OpenClaw integration +// --------------------------------------------------------------------------- - return response.json(); -} - -/** - * Send image via OpenClaw - */ async function sendViaOpenClaw( message: OpenClawMessage, useCLI: boolean = true ): Promise { if (useCLI) { - // Use OpenClaw CLI const cmd = `openclaw message send --action send --channel "${message.channel}" --message "${message.message}" --media "${message.media}"`; await execAsync(cmd); return; } - // Direct API call const gatewayUrl = process.env.OPENCLAW_GATEWAY_URL || "http://localhost:18789"; const gatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN; @@ -182,36 +105,43 @@ async function sendViaOpenClaw( } } -/** - * Main function: Generate image and send to channel - */ -async function generateAndSend(options: GenerateAndSendOptions): Promise { +// --------------------------------------------------------------------------- +// Main logic +// --------------------------------------------------------------------------- + +async function generateAndSend( + options: GenerateAndSendOptions +): Promise { const { prompt, channel, - caption = "Generated with Grok Imagine", + caption = "Generated with Clawra", aspectRatio = "1:1", outputFormat = "jpeg", + provider: providerName, useClaudeCodeCLI = true, } = options; - console.log(`[INFO] Generating image with Grok Imagine...`); + // Resolve provider + const provider = getProvider(providerName); + console.log(`[INFO] Provider: ${provider.name}`); + console.log(`[INFO] Generating image...`); console.log(`[INFO] Prompt: ${prompt}`); console.log(`[INFO] Aspect ratio: ${aspectRatio}`); // Generate image - const imageResult = await generateImage({ + const imageResult: GenerateImageResult = await provider.generateImage({ prompt, - num_images: 1, - aspect_ratio: aspectRatio, - output_format: outputFormat, + numImages: 1, + aspectRatio, + outputFormat, }); const imageUrl = imageResult.images[0].url; console.log(`[INFO] Image generated: ${imageUrl}`); - if (imageResult.revised_prompt) { - console.log(`[INFO] Revised prompt: ${imageResult.revised_prompt}`); + if (imageResult.revisedPrompt) { + console.log(`[INFO] Revised prompt: ${imageResult.revisedPrompt}`); } // Send via OpenClaw @@ -234,35 +164,42 @@ async function generateAndSend(options: GenerateAndSendOptions): Promise imageUrl, channel, prompt, - revisedPrompt: imageResult.revised_prompt, + provider: provider.name, + revisedPrompt: imageResult.revisedPrompt, }; } +// --------------------------------------------------------------------------- // CLI entry point +// --------------------------------------------------------------------------- + async function main() { const args = process.argv.slice(2); if (args.length < 2) { console.log(` -Usage: npx ts-node grok-imagine-send.ts [caption] [aspect_ratio] [output_format] +Usage: npx ts-node clawra-selfie.ts [caption] [aspect_ratio] [output_format] [provider] Arguments: prompt - Image description (required) channel - Target channel (required) e.g., #general, @user - caption - Message caption (default: 'Generated with Grok Imagine') + caption - Message caption (default: 'Generated with Clawra') aspect_ratio - Image ratio (default: 1:1) Options: 2:1, 16:9, 4:3, 1:1, 3:4, 9:16 output_format - Image format (default: jpeg) Options: jpeg, png, webp + provider - Image provider (default: fal) Options: ${AVAILABLE_PROVIDERS.join(", ")} Environment: - FAL_KEY - Your fal.ai API key (required) + IMAGE_PROVIDER - Default provider (overridden by provider argument) +${AVAILABLE_PROVIDERS.map((p) => ` ${PROVIDER_ENV_KEYS[p].padEnd(17)}- Required for provider: ${p}`).join("\n")} Example: - FAL_KEY=your_key npx ts-node grok-imagine-send.ts "A cyberpunk city" "#art" "Check this out!" + FAL_KEY=xxx npx ts-node clawra-selfie.ts "A cyberpunk city" "#art" "Check this!" + IMAGE_PROVIDER=openai OPENAI_API_KEY=xxx npx ts-node clawra-selfie.ts "A sunset" "#photos" `); process.exit(1); } - const [prompt, channel, caption, aspectRatio, outputFormat] = args; + const [prompt, channel, caption, aspectRatio, outputFormat, provider] = args; try { const result = await generateAndSend({ @@ -271,6 +208,7 @@ Example: caption, aspectRatio: aspectRatio as AspectRatio, outputFormat: outputFormat as OutputFormat, + provider: provider as ProviderName, }); console.log("\n--- Result ---"); @@ -283,11 +221,8 @@ Example: // Export for module use export { - generateImage, sendViaOpenClaw, generateAndSend, - GrokImagineInput, - GrokImagineResponse, OpenClawMessage, GenerateAndSendOptions, Result, diff --git a/scripts/providers/fal.ts b/scripts/providers/fal.ts new file mode 100644 index 00000000..14f8db32 --- /dev/null +++ b/scripts/providers/fal.ts @@ -0,0 +1,103 @@ +/** + * fal.ai provider – proxies xAI Grok Imagine via fal.ai. + * + * Env: FAL_KEY + */ + +import type { + ImageProvider, + GenerateImageInput, + EditImageInput, + GenerateImageResult, +} from "./types"; + +const FAL_BASE = "https://fal.run/xai/grok-imagine-image"; + +export class FalProvider implements ImageProvider { + readonly name = "fal.ai (Grok Imagine)"; + readonly id = "fal" as const; + + private getKey(): string { + const key = process.env.FAL_KEY; + if (!key) { + throw new Error( + "FAL_KEY environment variable not set. Get your key from https://fal.ai/dashboard/keys" + ); + } + return key; + } + + async generateImage(input: GenerateImageInput): Promise { + const key = this.getKey(); + + const response = await fetch(FAL_BASE, { + method: "POST", + headers: { + Authorization: `Key ${key}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + prompt: input.prompt, + num_images: input.numImages ?? 1, + aspect_ratio: input.aspectRatio ?? "1:1", + output_format: input.outputFormat ?? "jpeg", + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`fal.ai image generation failed: ${error}`); + } + + const data = (await response.json()) as { + images: Array<{ url: string; width: number; height: number }>; + revised_prompt?: string; + }; + + return { + images: data.images.map((img) => ({ + url: img.url, + width: img.width, + height: img.height, + })), + revisedPrompt: data.revised_prompt, + }; + } + + async editImage(input: EditImageInput): Promise { + const key = this.getKey(); + + const response = await fetch(`${FAL_BASE}/edit`, { + method: "POST", + headers: { + Authorization: `Key ${key}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + image_url: input.imageUrl, + prompt: input.prompt, + num_images: input.numImages ?? 1, + output_format: input.outputFormat ?? "jpeg", + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`fal.ai image editing failed: ${error}`); + } + + const data = (await response.json()) as { + images: Array<{ url: string; width: number; height: number }>; + revised_prompt?: string; + }; + + return { + images: data.images.map((img) => ({ + url: img.url, + width: img.width, + height: img.height, + })), + revisedPrompt: data.revised_prompt, + }; + } +} diff --git a/scripts/providers/google.ts b/scripts/providers/google.ts new file mode 100644 index 00000000..ff91a75b --- /dev/null +++ b/scripts/providers/google.ts @@ -0,0 +1,194 @@ +/** + * Google Gemini provider – uses Gemini's native image generation. + * + * Env: GEMINI_API_KEY + * + * Reference: https://ai.google.dev/gemini-api/docs/image-generation + * + * Uses the generateContent endpoint with responseModalities: ["IMAGE"] + * Response returns base64-encoded image data in inlineData parts. + */ + +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import type { + ImageProvider, + GenerateImageInput, + EditImageInput, + GenerateImageResult, +} from "./types"; + +const GEMINI_API_BASE = + "https://generativelanguage.googleapis.com/v1beta/models"; + +export class GoogleProvider implements ImageProvider { + readonly name = "Google Gemini (gemini-2.5-flash-image)"; + readonly id = "google" as const; + + private model = "gemini-2.5-flash-image"; + + private getKey(): string { + const key = process.env.GEMINI_API_KEY; + if (!key) { + throw new Error( + "GEMINI_API_KEY environment variable not set. Get your key from https://aistudio.google.com/apikey" + ); + } + return key; + } + + private buildUrl(): string { + return `${GEMINI_API_BASE}/${this.model}:generateContent`; + } + + /** + * Extract the first image from Gemini's generateContent response. + * The response may contain both text and image parts. + */ + private extractImages( + responseData: GeminiResponse, + outputFormat: string + ): { url: string }[] { + const images: { url: string }[] = []; + + for (const candidate of responseData.candidates ?? []) { + for (const part of candidate.content?.parts ?? []) { + if (part.inlineData?.data) { + const ext = + outputFormat === "png" + ? "png" + : outputFormat === "webp" + ? "webp" + : "jpeg"; + const tmpFile = path.join( + os.tmpdir(), + `clawra-gemini-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}` + ); + fs.writeFileSync( + tmpFile, + Buffer.from(part.inlineData.data, "base64") + ); + images.push({ url: tmpFile }); + } + } + } + + return images; + } + + async generateImage( + input: GenerateImageInput + ): Promise { + const key = this.getKey(); + + const body = { + contents: [ + { + parts: [{ text: input.prompt }], + }, + ], + generationConfig: { + responseModalities: ["IMAGE"], + }, + }; + + const response = await fetch(`${this.buildUrl()}?key=${key}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Google Gemini image generation failed: ${error}`); + } + + const data = (await response.json()) as GeminiResponse; + const images = this.extractImages(data, input.outputFormat ?? "jpeg"); + + if (images.length === 0) { + throw new Error( + "Google Gemini returned no image data in the response" + ); + } + + return { images }; + } + + async editImage(input: EditImageInput): Promise { + const key = this.getKey(); + + // Fetch the reference image and encode as base64 + const imageResponse = await fetch(input.imageUrl); + if (!imageResponse.ok) { + throw new Error(`Failed to fetch reference image: ${input.imageUrl}`); + } + const imageBuffer = Buffer.from(await imageResponse.arrayBuffer()); + const imageBase64 = imageBuffer.toString("base64"); + + // Determine mime type from URL or default to jpeg + const mimeType = input.imageUrl.endsWith(".png") + ? "image/png" + : "image/jpeg"; + + const body = { + contents: [ + { + parts: [ + { text: input.prompt }, + { + inlineData: { + mimeType, + data: imageBase64, + }, + }, + ], + }, + ], + generationConfig: { + responseModalities: ["IMAGE"], + }, + }; + + const response = await fetch(`${this.buildUrl()}?key=${key}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Google Gemini image editing failed: ${error}`); + } + + const data = (await response.json()) as GeminiResponse; + const images = this.extractImages(data, input.outputFormat ?? "jpeg"); + + if (images.length === 0) { + throw new Error( + "Google Gemini returned no image data in the editing response" + ); + } + + return { images }; + } +} + +// --------------------------------------------------------------------------- +// Gemini response types (subset) +// --------------------------------------------------------------------------- + +interface GeminiResponse { + candidates?: Array<{ + content?: { + parts?: Array<{ + text?: string; + inlineData?: { + mimeType?: string; + data?: string; + }; + }>; + }; + }>; +} diff --git a/scripts/providers/index.ts b/scripts/providers/index.ts new file mode 100644 index 00000000..2c52b4e8 --- /dev/null +++ b/scripts/providers/index.ts @@ -0,0 +1,63 @@ +/** + * Provider factory – returns the appropriate ImageProvider based on + * the IMAGE_PROVIDER environment variable or an explicit name. + * + * Supported values: fal | openai | google | xai + * Default: fal (backward-compatible) + */ + +export type { ProviderName, ImageProvider, GenerateImageInput, EditImageInput, GenerateImageResult, GeneratedImage, AspectRatio, OutputFormat } from "./types"; +import type { ImageProvider, ProviderName } from "./types"; + +import { FalProvider } from "./fal"; +import { OpenAIProvider } from "./openai"; +import { GoogleProvider } from "./google"; +import { XAIProvider } from "./xai"; + +/** + * Create an ImageProvider instance. + * + * @param name Provider identifier. If omitted, falls back to + * `process.env.IMAGE_PROVIDER`, then defaults to `"fal"`. + */ +export function getProvider(name?: string): ImageProvider { + const providerName = (name || process.env.IMAGE_PROVIDER || "fal") as ProviderName; + + switch (providerName) { + case "fal": + return new FalProvider(); + case "openai": + return new OpenAIProvider(); + case "google": + return new GoogleProvider(); + case "xai": + return new XAIProvider(); + default: + throw new Error( + `Unknown image provider: "${providerName}". ` + + `Supported providers: fal, openai, google, xai` + ); + } +} + +/** List all available provider identifiers */ +export const AVAILABLE_PROVIDERS: ProviderName[] = [ + "fal", + "openai", + "google", + "xai", +]; + +/** Map of provider → required environment variable */ +export const PROVIDER_ENV_KEYS: Record = { + fal: "FAL_KEY", + openai: "OPENAI_API_KEY", + google: "GEMINI_API_KEY", + xai: "XAI_API_KEY", +}; + +// Re-export provider classes for direct use +export { FalProvider } from "./fal"; +export { OpenAIProvider } from "./openai"; +export { GoogleProvider } from "./google"; +export { XAIProvider } from "./xai"; diff --git a/scripts/providers/openai.ts b/scripts/providers/openai.ts new file mode 100644 index 00000000..e471aed5 --- /dev/null +++ b/scripts/providers/openai.ts @@ -0,0 +1,125 @@ +/** + * OpenAI provider – uses the Images API (gpt-image-1). + * + * Env: OPENAI_API_KEY + * + * Reference: https://developers.openai.com/api/reference/resources/images/methods/generate + * + * Note: gpt-image-1 returns base64-encoded images. We convert them to data URIs + * so they can be passed downstream (e.g. to OpenClaw) the same way as URLs. + * If the caller needs a file, they can decode the base64 from the data URI. + */ + +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import type { + ImageProvider, + GenerateImageInput, + GenerateImageResult, + AspectRatio, +} from "./types"; + +const OPENAI_API_URL = "https://api.openai.com/v1/images/generations"; + +/** Map our aspect ratios to the closest OpenAI size preset */ +function aspectRatioToSize(ar?: AspectRatio): string { + switch (ar) { + case "16:9": + case "2:1": + case "20:9": + case "19.5:9": + case "3:2": + return "1536x1024"; // landscape + case "9:16": + case "1:2": + case "9:19.5": + case "9:20": + case "2:3": + case "3:4": + return "1024x1536"; // portrait + case "4:3": + case "1:1": + default: + return "1024x1024"; // square + } +} + +export class OpenAIProvider implements ImageProvider { + readonly name = "OpenAI (gpt-image-1)"; + readonly id = "openai" as const; + + private getKey(): string { + const key = process.env.OPENAI_API_KEY; + if (!key) { + throw new Error( + "OPENAI_API_KEY environment variable not set. Get your key from https://platform.openai.com/api-keys" + ); + } + return key; + } + + async generateImage(input: GenerateImageInput): Promise { + const key = this.getKey(); + const size = aspectRatioToSize(input.aspectRatio); + + const body: Record = { + model: "gpt-image-1", + prompt: input.prompt, + n: input.numImages ?? 1, + size, + }; + + if (input.outputFormat) { + body.output_format = input.outputFormat; + } + + const response = await fetch(OPENAI_API_URL, { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`OpenAI image generation failed: ${error}`); + } + + const data = (await response.json()) as { + data: Array<{ b64_json?: string; url?: string; revised_prompt?: string }>; + }; + + // Convert base64 images to temporary files so we get usable URLs + const images = await Promise.all( + data.data.map(async (item) => { + if (item.url) { + return { url: item.url }; + } + if (item.b64_json) { + const ext = input.outputFormat ?? "png"; + const tmpFile = path.join( + os.tmpdir(), + `clawra-openai-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}` + ); + fs.writeFileSync(tmpFile, Buffer.from(item.b64_json, "base64")); + return { url: tmpFile }; + } + throw new Error("OpenAI response contained neither url nor b64_json"); + }) + ); + + const revisedPrompt = data.data[0]?.revised_prompt; + + return { + images, + revisedPrompt: revisedPrompt ?? undefined, + }; + } + + // OpenAI Images API does support image editing via /v1/images/edits, + // but it uses multipart/form-data which is more complex. + // For now we only expose text-to-image generation. +} diff --git a/scripts/providers/types.ts b/scripts/providers/types.ts new file mode 100644 index 00000000..2a7c834e --- /dev/null +++ b/scripts/providers/types.ts @@ -0,0 +1,88 @@ +/** + * Multi-provider image generation types and interfaces. + * + * Supported providers: + * - fal (fal.ai / Grok Imagine) + * - openai (OpenAI gpt-image-1) + * - google (Google Gemini gemini-2.5-flash-image) + * - xai (x.ai direct / grok-imagine-image) + */ + +// --------------------------------------------------------------------------- +// Input types +// --------------------------------------------------------------------------- + +export type AspectRatio = + | "2:1" + | "20:9" + | "19.5:9" + | "16:9" + | "4:3" + | "3:2" + | "1:1" + | "2:3" + | "3:4" + | "9:16" + | "9:19.5" + | "9:20" + | "1:2"; + +export type OutputFormat = "jpeg" | "png" | "webp"; + +export interface GenerateImageInput { + /** Text prompt describing the image to generate */ + prompt: string; + /** Number of images to generate (default: 1) */ + numImages?: number; + /** Aspect ratio of the output image */ + aspectRatio?: AspectRatio; + /** Output image format */ + outputFormat?: OutputFormat; +} + +export interface EditImageInput extends GenerateImageInput { + /** URL of the reference / source image to edit */ + imageUrl: string; +} + +// --------------------------------------------------------------------------- +// Output types +// --------------------------------------------------------------------------- + +export interface GeneratedImage { + /** URL of the generated image (may be a temporary URL or data URI) */ + url: string; + /** Image width in pixels (if known) */ + width?: number; + /** Image height in pixels (if known) */ + height?: number; +} + +export interface GenerateImageResult { + /** Array of generated images */ + images: GeneratedImage[]; + /** Revised / enhanced prompt returned by the model (if any) */ + revisedPrompt?: string; +} + +// --------------------------------------------------------------------------- +// Provider interface +// --------------------------------------------------------------------------- + +export type ProviderName = "fal" | "openai" | "google" | "xai"; + +export interface ImageProvider { + /** Human-readable provider name */ + readonly name: string; + /** Provider identifier */ + readonly id: ProviderName; + + /** Generate an image from a text prompt */ + generateImage(input: GenerateImageInput): Promise; + + /** + * Edit / transform an existing image with a text prompt. + * Not all providers support this; callers should check before calling. + */ + editImage?(input: EditImageInput): Promise; +} diff --git a/scripts/providers/xai.ts b/scripts/providers/xai.ts new file mode 100644 index 00000000..917fc7c5 --- /dev/null +++ b/scripts/providers/xai.ts @@ -0,0 +1,113 @@ +/** + * x.ai direct provider – calls x.ai API directly (no fal.ai proxy). + * + * Env: XAI_API_KEY + * + * Reference: https://docs.x.ai/developers/model-capabilities/images/generation + * + * Uses the OpenAI-compatible endpoint at api.x.ai with model grok-imagine-image. + * For image editing, sends image_url in the request body (application/json). + */ + +import type { + ImageProvider, + GenerateImageInput, + EditImageInput, + GenerateImageResult, +} from "./types"; + +const XAI_API_URL = "https://api.x.ai/v1/images/generations"; + +export class XAIProvider implements ImageProvider { + readonly name = "x.ai (Grok Imagine Direct)"; + readonly id = "xai" as const; + + private getKey(): string { + const key = process.env.XAI_API_KEY; + if (!key) { + throw new Error( + "XAI_API_KEY environment variable not set. Get your key from https://console.x.ai" + ); + } + return key; + } + + async generateImage(input: GenerateImageInput): Promise { + const key = this.getKey(); + + const body: Record = { + model: "grok-imagine-image", + prompt: input.prompt, + n: input.numImages ?? 1, + }; + + if (input.aspectRatio) { + body.aspect_ratio = input.aspectRatio; + } + + const response = await fetch(XAI_API_URL, { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`x.ai image generation failed: ${error}`); + } + + const data = (await response.json()) as XAIImageResponse; + + return { + images: data.data.map((item) => ({ url: item.url })), + }; + } + + async editImage(input: EditImageInput): Promise { + const key = this.getKey(); + + const body: Record = { + model: "grok-imagine-image", + prompt: input.prompt, + n: input.numImages ?? 1, + image_url: input.imageUrl, + }; + + if (input.aspectRatio) { + body.aspect_ratio = input.aspectRatio; + } + + const response = await fetch(XAI_API_URL, { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`x.ai image editing failed: ${error}`); + } + + const data = (await response.json()) as XAIImageResponse; + + return { + images: data.data.map((item) => ({ url: item.url })), + }; + } +} + +// --------------------------------------------------------------------------- +// x.ai response types +// --------------------------------------------------------------------------- + +interface XAIImageResponse { + data: Array<{ + url: string; + }>; +} diff --git a/skill/SKILL.md b/skill/SKILL.md index c09ce1db..d85beb3c 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -1,12 +1,23 @@ --- name: clawra-selfie -description: Edit Clawra's reference image with Grok Imagine (xAI Aurora) and send selfies to messaging channels via OpenClaw +description: Generate and edit images using multiple AI providers (fal.ai, OpenAI, Google Gemini, x.ai) and send selfies to messaging channels via OpenClaw allowed-tools: Bash(npm:*) Bash(npx:*) Bash(openclaw:*) Bash(curl:*) Read Write WebFetch --- # Clawra Selfie -Edit a fixed reference image using xAI's Grok Imagine model and distribute it across messaging platforms (WhatsApp, Telegram, Discord, Slack, etc.) via OpenClaw. +Generate and edit images using multiple AI providers and distribute them across messaging platforms (WhatsApp, Telegram, Discord, Slack, etc.) via OpenClaw. + +## Supported Providers + +| Provider | Model | Env Variable | API Key URL | +|----------|-------|-------------|-------------| +| **fal** (default) | Grok Imagine (via fal.ai) | `FAL_KEY` | https://fal.ai/dashboard/keys | +| **openai** | gpt-image-1 | `OPENAI_API_KEY` | https://platform.openai.com/api-keys | +| **google** | gemini-2.5-flash-image | `GEMINI_API_KEY` | https://aistudio.google.com/apikey | +| **xai** | grok-imagine-image (direct) | `XAI_API_KEY` | https://console.x.ai | + +Set the provider via the `IMAGE_PROVIDER` environment variable (default: `fal`). ## Reference Image @@ -29,16 +40,24 @@ https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png ### Required Environment Variables ```bash -FAL_KEY=your_fal_api_key # Get from https://fal.ai/dashboard/keys +# Pick ONE provider and set its API key: +IMAGE_PROVIDER=fal # Options: fal, openai, google, xai (default: fal) + +FAL_KEY=your_fal_api_key # For provider: fal +OPENAI_API_KEY=your_openai_key # For provider: openai +GEMINI_API_KEY=your_gemini_key # For provider: google +XAI_API_KEY=your_xai_key # For provider: xai + OPENCLAW_GATEWAY_TOKEN=your_token # From: openclaw doctor --generate-gateway-token ``` ### Workflow 1. **Get user prompt** for how to edit the image -2. **Edit image** via fal.ai Grok Imagine Edit API with fixed reference -3. **Extract image URL** from response -4. **Send to OpenClaw** with target channel(s) +2. **Select provider** (from env var or default) +3. **Generate/edit image** via selected provider +4. **Extract image URL** from response +5. **Send to OpenClaw** with target channel(s) ## Step-by-Step Instructions @@ -49,6 +68,7 @@ Ask the user for: - **Mode** (optional): `mirror` or `direct` selfie style - **Target channel(s)**: Where should it be sent? (e.g., `#general`, `@username`, channel ID) - **Platform** (optional): Which platform? (discord, telegram, whatsapp, slack) +- **Provider** (optional): Which image provider to use? ## Prompt Modes @@ -85,20 +105,13 @@ a close-up selfie taken by herself at a cozy cafe with warm lighting, direct eye | close-up, portrait, face, eyes, smile | `direct` | | full-body, mirror, reflection | `mirror` | -### Step 2: Edit Image with Grok Imagine +### Step 2: Generate/Edit Image -Use the fal.ai API to edit the reference image: +#### Provider: fal.ai (default) ```bash REFERENCE_IMAGE="https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png" -# Mode 1: Mirror Selfie -PROMPT="make a pic of this person, but . the person is taking a mirror selfie" - -# Mode 2: Direct Selfie -PROMPT="a close-up selfie taken by herself at , direct eye contact with the camera, looking straight into the lens, eyes centered and clearly visible, not a mirror selfie, phone held at arm's length, face fully visible" - -# Build JSON payload with jq (handles escaping properly) JSON_PAYLOAD=$(jq -n \ --arg image_url "$REFERENCE_IMAGE" \ --arg prompt "$PROMPT" \ @@ -110,24 +123,114 @@ curl -X POST "https://fal.run/xai/grok-imagine-image/edit" \ -d "$JSON_PAYLOAD" ``` -**Response Format:** +**Response:** ```json { - "images": [ - { - "url": "https://v3b.fal.media/files/...", - "content_type": "image/jpeg", - "width": 1024, - "height": 1024 - } - ], + "images": [{ "url": "https://v3b.fal.media/files/...", "width": 1024, "height": 1024 }], "revised_prompt": "Enhanced prompt text..." } ``` +#### Provider: OpenAI + +```bash +curl -X POST "https://api.openai.com/v1/images/generations" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-image-1", + "prompt": "A cute cat wearing a hat", + "n": 1, + "size": "1024x1024" + }' +``` + +**Response:** +```json +{ + "data": [{ "b64_json": "..." }] +} +``` + +> Note: OpenAI gpt-image-1 returns base64-encoded images. Save the `b64_json` field to a file. + +#### Provider: Google Gemini + +```bash +curl -s -X POST \ + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=$GEMINI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "contents": [{"parts": [{"text": "A cute cat wearing a hat"}]}], + "generationConfig": {"responseModalities": ["IMAGE"]} + }' +``` + +**Response:** +```json +{ + "candidates": [{ + "content": { + "parts": [{ "inlineData": { "mimeType": "image/png", "data": "" } }] + } + }] +} +``` + +> Note: Google Gemini returns base64 inline data. Decode the `data` field and save to a file. + +For image editing with Gemini, include the reference image as an `inlineData` part: +```bash +curl -s -X POST \ + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=$GEMINI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"contents\": [{ + \"parts\": [ + {\"text\": \"Edit this person to be wearing a santa hat\"}, + {\"inlineData\": {\"mimeType\": \"image/jpeg\", \"data\": \"\"}} + ] + }], + \"generationConfig\": {\"responseModalities\": [\"IMAGE\"]} + }" +``` + +#### Provider: x.ai (direct) + +```bash +curl -X POST "https://api.x.ai/v1/images/generations" \ + -H "Authorization: Bearer $XAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "grok-imagine-image", + "prompt": "A cute cat wearing a hat", + "n": 1 + }' +``` + +For image editing, add `image_url`: +```bash +curl -X POST "https://api.x.ai/v1/images/generations" \ + -H "Authorization: Bearer $XAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "grok-imagine-image", + "prompt": "Edit this to wear a santa hat", + "n": 1, + "image_url": "https://example.com/photo.jpg" + }' +``` + +**Response:** +```json +{ + "data": [{ "url": "https://..." }] +} +``` + ### Step 3: Send Image via OpenClaw -Use the OpenClaw messaging API to send the edited image: +Use the OpenClaw messaging API to send the generated image: ```bash openclaw message send \ @@ -154,191 +257,69 @@ curl -X POST "http://localhost:18789/message" \ ```bash #!/bin/bash -# grok-imagine-edit-send.sh - -# Check required environment variables -if [ -z "$FAL_KEY" ]; then - echo "Error: FAL_KEY environment variable not set" - exit 1 -fi - -# Fixed reference image -REFERENCE_IMAGE="https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png" +# Multi-provider image generation and send +PROVIDER="${IMAGE_PROVIDER:-fal}" USER_CONTEXT="$1" CHANNEL="$2" -MODE="${3:-auto}" # mirror, direct, or auto -CAPTION="${4:-Edited with Grok Imagine}" - -if [ -z "$USER_CONTEXT" ] || [ -z "$CHANNEL" ]; then - echo "Usage: $0 [mode] [caption]" - echo "Modes: mirror, direct, auto (default)" - echo "Example: $0 'wearing a cowboy hat' '#general' mirror" - echo "Example: $0 'a cozy cafe' '#general' direct" - exit 1 -fi - -# Auto-detect mode based on keywords -if [ "$MODE" == "auto" ]; then - if echo "$USER_CONTEXT" | grep -qiE "outfit|wearing|clothes|dress|suit|fashion|full-body|mirror"; then - MODE="mirror" - elif echo "$USER_CONTEXT" | grep -qiE "cafe|restaurant|beach|park|city|close-up|portrait|face|eyes|smile"; then - MODE="direct" - else - MODE="mirror" # default - fi - echo "Auto-detected mode: $MODE" -fi - -# Construct the prompt based on mode -if [ "$MODE" == "direct" ]; then - EDIT_PROMPT="a close-up selfie taken by herself at $USER_CONTEXT, direct eye contact with the camera, looking straight into the lens, eyes centered and clearly visible, not a mirror selfie, phone held at arm's length, face fully visible" -else - EDIT_PROMPT="make a pic of this person, but $USER_CONTEXT. the person is taking a mirror selfie" -fi - -echo "Mode: $MODE" -echo "Editing reference image with prompt: $EDIT_PROMPT" - -# Edit image (using jq for proper JSON escaping) -JSON_PAYLOAD=$(jq -n \ - --arg image_url "$REFERENCE_IMAGE" \ - --arg prompt "$EDIT_PROMPT" \ - '{image_url: $image_url, prompt: $prompt, num_images: 1, output_format: "jpeg"}') - -RESPONSE=$(curl -s -X POST "https://fal.run/xai/grok-imagine-image/edit" \ - -H "Authorization: Key $FAL_KEY" \ - -H "Content-Type: application/json" \ - -d "$JSON_PAYLOAD") - -# Extract image URL -IMAGE_URL=$(echo "$RESPONSE" | jq -r '.images[0].url') - -if [ "$IMAGE_URL" == "null" ] || [ -z "$IMAGE_URL" ]; then - echo "Error: Failed to edit image" - echo "Response: $RESPONSE" - exit 1 -fi - -echo "Image edited: $IMAGE_URL" -echo "Sending to channel: $CHANNEL" - -# Send via OpenClaw -openclaw message send \ - --action send \ - --channel "$CHANNEL" \ - --message "$CAPTION" \ - --media "$IMAGE_URL" - -echo "Done!" +CAPTION="${3:-Generated with Clawra}" + +# Route to provider +case "$PROVIDER" in + fal) + curl -s -X POST "https://fal.run/xai/grok-imagine-image" \ + -H "Authorization: Key $FAL_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"prompt\": \"$USER_CONTEXT\", \"num_images\": 1}" + ;; + openai) + curl -s -X POST "https://api.openai.com/v1/images/generations" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"model\": \"gpt-image-1\", \"prompt\": \"$USER_CONTEXT\", \"n\": 1, \"size\": \"1024x1024\"}" + ;; + google) + curl -s -X POST \ + "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent?key=$GEMINI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"contents\": [{\"parts\": [{\"text\": \"$USER_CONTEXT\"}]}], \"generationConfig\": {\"responseModalities\": [\"IMAGE\"]}}" + ;; + xai) + curl -s -X POST "https://api.x.ai/v1/images/generations" \ + -H "Authorization: Bearer $XAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"model\": \"grok-imagine-image\", \"prompt\": \"$USER_CONTEXT\", \"n\": 1}" + ;; +esac ``` +For the full implementation with error handling, mode detection, and OpenClaw integration, see the scripts in `scripts/clawra-selfie.sh` or `scripts/clawra-selfie.ts`. + ## Node.js/TypeScript Implementation ```typescript -import { fal } from "@fal-ai/client"; -import { exec } from "child_process"; -import { promisify } from "util"; - -const execAsync = promisify(exec); - -const REFERENCE_IMAGE = "https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png"; - -interface GrokImagineResult { - images: Array<{ - url: string; - content_type: string; - width: number; - height: number; - }>; - revised_prompt?: string; -} - -type SelfieMode = "mirror" | "direct" | "auto"; - -function detectMode(userContext: string): "mirror" | "direct" { - const mirrorKeywords = /outfit|wearing|clothes|dress|suit|fashion|full-body|mirror/i; - const directKeywords = /cafe|restaurant|beach|park|city|close-up|portrait|face|eyes|smile/i; - - if (directKeywords.test(userContext)) return "direct"; - if (mirrorKeywords.test(userContext)) return "mirror"; - return "mirror"; // default -} - -function buildPrompt(userContext: string, mode: "mirror" | "direct"): string { - if (mode === "direct") { - return `a close-up selfie taken by herself at ${userContext}, direct eye contact with the camera, looking straight into the lens, eyes centered and clearly visible, not a mirror selfie, phone held at arm's length, face fully visible`; - } - return `make a pic of this person, but ${userContext}. the person is taking a mirror selfie`; -} - -async function editAndSend( - userContext: string, - channel: string, - mode: SelfieMode = "auto", - caption?: string -): Promise { - // Configure fal.ai client - fal.config({ - credentials: process.env.FAL_KEY! +import { getProvider } from "./providers"; + +// Generate image with any provider +const provider = getProvider("openai"); // or "fal", "google", "xai" +const result = await provider.generateImage({ + prompt: "A sunset over the ocean", + numImages: 1, + aspectRatio: "16:9", + outputFormat: "jpeg", +}); + +console.log(result.images[0].url); + +// Edit image (fal, google, xai support editing) +if (provider.editImage) { + const edited = await provider.editImage({ + imageUrl: "https://example.com/photo.jpg", + prompt: "Add a santa hat", + numImages: 1, }); - - // Determine mode - const actualMode = mode === "auto" ? detectMode(userContext) : mode; - console.log(`Mode: ${actualMode}`); - - // Construct the prompt - const editPrompt = buildPrompt(userContext, actualMode); - - // Edit reference image with Grok Imagine - console.log(`Editing image: "${editPrompt}"`); - - const result = await fal.subscribe("xai/grok-imagine-image/edit", { - input: { - image_url: REFERENCE_IMAGE, - prompt: editPrompt, - num_images: 1, - output_format: "jpeg" - } - }) as { data: GrokImagineResult }; - - const imageUrl = result.data.images[0].url; - console.log(`Edited image URL: ${imageUrl}`); - - // Send via OpenClaw - const messageCaption = caption || `Edited with Grok Imagine`; - - await execAsync( - `openclaw message send --action send --channel "${channel}" --message "${messageCaption}" --media "${imageUrl}"` - ); - - console.log(`Sent to ${channel}`); - return imageUrl; + console.log(edited.images[0].url); } - -// Usage Examples - -// Mirror mode (auto-detected from "wearing") -editAndSend( - "wearing a cyberpunk outfit with neon lights", - "#art-gallery", - "auto", - "Check out this AI-edited art!" -); -// → Mode: mirror -// → Prompt: "make a pic of this person, but wearing a cyberpunk outfit with neon lights. the person is taking a mirror selfie" - -// Direct mode (auto-detected from "cafe") -editAndSend( - "a cozy cafe with warm lighting", - "#photography", - "auto" -); -// → Mode: direct -// → Prompt: "a close-up selfie taken by herself at a cozy cafe with warm lighting, direct eye contact..." - -// Explicit mode override -editAndSend("casual street style", "#fashion", "direct"); ``` ## Supported Platforms @@ -354,20 +335,51 @@ OpenClaw supports sending to: | Signal | Phone number | `+1234567890` | | MS Teams | Channel reference | (varies) | -## Grok Imagine Edit Parameters +## Provider-Specific Parameters + +### fal.ai (Grok Imagine) | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `image_url` | string | required | URL of image to edit (fixed in this skill) | -| `prompt` | string | required | Edit instruction | +| `image_url` | string | - | URL of image to edit | +| `prompt` | string | required | Generation/edit instruction | | `num_images` | 1-4 | 1 | Number of images to generate | +| `aspect_ratio` | enum | "1:1" | 2:1, 16:9, 4:3, 1:1, 3:4, 9:16 | | `output_format` | enum | "jpeg" | jpeg, png, webp | +### OpenAI (gpt-image-1) + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `model` | string | "gpt-image-1" | Model to use | +| `prompt` | string | required | Image description | +| `n` | 1-10 | 1 | Number of images | +| `size` | enum | "1024x1024" | 1024x1024, 1536x1024, 1024x1536 | +| `output_format` | enum | "png" | png, jpeg, webp | + +### Google Gemini + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `contents` | array | required | Prompt parts (text + optional image) | +| `responseModalities` | array | ["IMAGE"] | Must include "IMAGE" | +| `imageMimeType` | string | - | Output mime type | + +### x.ai (Grok Imagine Direct) + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `model` | string | "grok-imagine-image" | Model to use | +| `prompt` | string | required | Image description | +| `n` | number | 1 | Number of images | +| `image_url` | string | - | Source image for editing | +| `aspect_ratio` | enum | - | 1:1, 16:9, 9:16, etc. | + ## Setup Requirements -### 1. Install fal.ai client (for Node.js usage) +### 1. Install dependencies (for Node.js usage) ```bash -npm install @fal-ai/client +npm install @fal-ai/client # only needed for fal provider ``` ### 2. Install OpenClaw CLI @@ -388,10 +400,11 @@ openclaw gateway start ## Error Handling -- **FAL_KEY missing**: Ensure the API key is set in environment -- **Image edit failed**: Check prompt content and API quota +- **API key missing**: Ensure the correct key is set for your chosen provider +- **Image generation failed**: Check prompt content and API quota - **OpenClaw send failed**: Verify gateway is running and channel exists -- **Rate limits**: fal.ai has rate limits; implement retry logic if needed +- **Base64 responses**: OpenAI and Google return base64 data; scripts auto-save to temp files +- **Rate limits**: Each provider has its own rate limits; implement retry logic if needed ## Tips @@ -399,14 +412,18 @@ openclaw gateway start - "wearing a santa hat" - "in a business suit" - "wearing a summer dress" - - "in streetwear fashion" 2. **Direct mode context examples** (location/portrait focus): - "a cozy cafe with warm lighting" - "a sunny beach at sunset" - "a busy city street at night" - - "a peaceful park in autumn" -3. **Mode selection**: Let auto-detect work, or explicitly specify for control -4. **Batch sending**: Edit once, send to multiple channels -5. **Scheduling**: Combine with OpenClaw scheduler for automated posts +3. **Provider selection**: + - Use `fal` for quick Grok Imagine access via fal.ai proxy + - Use `openai` for GPT-image-1's strong prompt following + - Use `google` for Gemini's integrated image generation + - Use `xai` for direct x.ai access without fal.ai proxy + +4. **Mode selection**: Let auto-detect work, or explicitly specify for control +5. **Batch sending**: Generate once, send to multiple channels +6. **Scheduling**: Combine with OpenClaw scheduler for automated posts diff --git a/skill/scripts/clawra-selfie.sh b/skill/scripts/clawra-selfie.sh index 72d176d5..e7caafa1 100755 --- a/skill/scripts/clawra-selfie.sh +++ b/skill/scripts/clawra-selfie.sh @@ -1,14 +1,18 @@ #!/bin/bash -# grok-imagine-send.sh -# Generate an image with Grok Imagine and send it via OpenClaw +# clawra-selfie.sh +# Generate an image with multiple AI providers and send it via OpenClaw # -# Usage: ./grok-imagine-send.sh "" "" [""] +# Supported providers (set via IMAGE_PROVIDER env var): +# fal - fal.ai / Grok Imagine (requires FAL_KEY) +# openai - OpenAI gpt-image-1 (requires OPENAI_API_KEY) +# google - Google Gemini (requires GEMINI_API_KEY) +# xai - x.ai direct (requires XAI_API_KEY) # -# Environment variables required: -# FAL_KEY - Your fal.ai API key +# Usage: ./clawra-selfie.sh "" "" [""] [aspect_ratio] [output_format] # # Example: -# FAL_KEY=your_key ./grok-imagine-send.sh "A sunset over mountains" "#art" "Check this out!" +# FAL_KEY=your_key ./clawra-selfie.sh "A sunset over mountains" "#art" "Check this out!" +# IMAGE_PROVIDER=openai OPENAI_API_KEY=xxx ./clawra-selfie.sh "A sunset" "#art" set -euo pipefail @@ -30,13 +34,6 @@ log_error() { echo -e "${RED}[ERROR]${NC} $1" } -# Check required environment variables -if [ -z "${FAL_KEY:-}" ]; then - log_error "FAL_KEY environment variable not set" - echo "Get your API key from: https://fal.ai/dashboard/keys" - exit 1 -fi - # Check for jq if ! command -v jq &> /dev/null; then log_error "jq is required but not installed" @@ -55,9 +52,10 @@ fi # Parse arguments PROMPT="${1:-}" CHANNEL="${2:-}" -CAPTION="${3:-Generated with Grok Imagine}" +CAPTION="${3:-Generated with Clawra}" ASPECT_RATIO="${4:-1:1}" OUTPUT_FORMAT="${5:-jpeg}" +PROVIDER="${IMAGE_PROVIDER:-fal}" if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then echo "Usage: $0 [caption] [aspect_ratio] [output_format]" @@ -65,51 +63,222 @@ if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then echo "Arguments:" echo " prompt - Image description (required)" echo " channel - Target channel (required) e.g., #general, @user" - echo " caption - Message caption (default: 'Generated with Grok Imagine')" + echo " caption - Message caption (default: 'Generated with Clawra')" echo " aspect_ratio - Image ratio (default: 1:1) Options: 2:1, 16:9, 4:3, 1:1, 3:4, 9:16" echo " output_format - Image format (default: jpeg) Options: jpeg, png, webp" echo "" + echo "Environment:" + echo " IMAGE_PROVIDER - Provider to use (default: fal)" + echo " Options: fal, openai, google, xai" + echo " FAL_KEY - Required for provider: fal" + echo " OPENAI_API_KEY - Required for provider: openai" + echo " GEMINI_API_KEY - Required for provider: google" + echo " XAI_API_KEY - Required for provider: xai" + echo "" echo "Example:" echo " $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\"" + echo " IMAGE_PROVIDER=openai $0 \"A sunset\" \"#photos\"" exit 1 fi -log_info "Generating image with Grok Imagine..." +log_info "Provider: $PROVIDER" +log_info "Generating image..." log_info "Prompt: $PROMPT" log_info "Aspect ratio: $ASPECT_RATIO" -# Generate image via fal.ai -RESPONSE=$(curl -s -X POST "https://fal.run/xai/grok-imagine-image" \ - -H "Authorization: Key $FAL_KEY" \ - -H "Content-Type: application/json" \ - -d "{ - \"prompt\": $(echo "$PROMPT" | jq -Rs .), - \"num_images\": 1, - \"aspect_ratio\": \"$ASPECT_RATIO\", - \"output_format\": \"$OUTPUT_FORMAT\" - }") - -# Check for errors in response -if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then - ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error // .detail // "Unknown error"') - log_error "Image generation failed: $ERROR_MSG" - exit 1 -fi +# ============================================================================ +# Provider: fal.ai (Grok Imagine) +# ============================================================================ +generate_fal() { + if [ -z "${FAL_KEY:-}" ]; then + log_error "FAL_KEY environment variable not set" + echo "Get your API key from: https://fal.ai/dashboard/keys" + exit 1 + fi + + RESPONSE=$(curl -s -X POST "https://fal.run/xai/grok-imagine-image" \ + -H "Authorization: Key $FAL_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"prompt\": $(echo "$PROMPT" | jq -Rs .), + \"num_images\": 1, + \"aspect_ratio\": \"$ASPECT_RATIO\", + \"output_format\": \"$OUTPUT_FORMAT\" + }") + + # Check for error (ensure it's not null/empty) + ERROR_VAL=$(echo "$RESPONSE" | jq -r '.error // .detail // empty') + if [ -n "$ERROR_VAL" ]; then + log_error "fal.ai image generation failed: $ERROR_VAL" + exit 1 + fi + + # Try multiple response paths: images[0].url, data[0].url + IMAGE_URL=$(echo "$RESPONSE" | jq -r '.images[0].url // .data[0].url // empty') + REVISED_PROMPT=$(echo "$RESPONSE" | jq -r '.revised_prompt // empty') + + if [ -z "$IMAGE_URL" ]; then + log_warn "Could not extract image URL from fal.ai response" + log_warn "Response: $(echo "$RESPONSE" | head -c 500)" + fi +} + +# ============================================================================ +# Provider: OpenAI (gpt-image-1) +# ============================================================================ +generate_openai() { + if [ -z "${OPENAI_API_KEY:-}" ]; then + log_error "OPENAI_API_KEY environment variable not set" + echo "Get your API key from: https://platform.openai.com/api-keys" + exit 1 + fi + + # Map aspect ratio to OpenAI size + case "$ASPECT_RATIO" in + 16:9|2:1|20:9|19.5:9|3:2) SIZE="1536x1024" ;; + 9:16|1:2|9:19.5|9:20|2:3|3:4) SIZE="1024x1536" ;; + *) SIZE="1024x1024" ;; + esac + + RESPONSE=$(curl -s -X POST "https://api.openai.com/v1/images/generations" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"model\": \"gpt-image-1\", + \"prompt\": $(echo "$PROMPT" | jq -Rs .), + \"n\": 1, + \"size\": \"$SIZE\" + }") + + if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then + ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // .error // "Unknown error"') + log_error "OpenAI image generation failed: $ERROR_MSG" + exit 1 + fi + + # OpenAI gpt-image-1 returns base64 – save to temp file + B64_DATA=$(echo "$RESPONSE" | jq -r '.data[0].b64_json // empty') + URL_DATA=$(echo "$RESPONSE" | jq -r '.data[0].url // empty') -# Extract image URL -IMAGE_URL=$(echo "$RESPONSE" | jq -r '.images[0].url // empty') + if [ -n "$URL_DATA" ]; then + IMAGE_URL="$URL_DATA" + elif [ -n "$B64_DATA" ]; then + TMPFILE=$(mktemp /tmp/clawra-openai-XXXXXX."$OUTPUT_FORMAT") + echo "$B64_DATA" | base64 -d > "$TMPFILE" + IMAGE_URL="$TMPFILE" + log_info "Saved to temp file: $TMPFILE" + else + log_error "OpenAI response contained neither url nor b64_json" + echo "Response: $RESPONSE" + exit 1 + fi + + REVISED_PROMPT=$(echo "$RESPONSE" | jq -r '.data[0].revised_prompt // empty') +} +# ============================================================================ +# Provider: Google Gemini (gemini-2.5-flash-image) +# ============================================================================ +generate_google() { + if [ -z "${GEMINI_API_KEY:-}" ]; then + log_error "GEMINI_API_KEY environment variable not set" + echo "Get your API key from: https://aistudio.google.com/apikey" + exit 1 + fi + + local MODEL="gemini-2.5-flash-image" + + RESPONSE=$(curl -s -X POST \ + "https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${GEMINI_API_KEY}" \ + -H "Content-Type: application/json" \ + -d "{ + \"contents\": [{ + \"parts\": [{\"text\": $(echo "$PROMPT" | jq -Rs .)}] + }], + \"generationConfig\": { + \"responseModalities\": [\"IMAGE\"] + } + }") + + if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then + ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // .error // "Unknown error"') + log_error "Google Gemini image generation failed: $ERROR_MSG" + exit 1 + fi + + # Extract base64 image data from Gemini response + B64_DATA=$(echo "$RESPONSE" | jq -r '.candidates[0].content.parts[] | select(.inlineData) | .inlineData.data // empty' | head -1) + + if [ -z "$B64_DATA" ]; then + log_error "Google Gemini returned no image data" + echo "Response: $RESPONSE" + exit 1 + fi + + TMPFILE=$(mktemp /tmp/clawra-gemini-XXXXXX."$OUTPUT_FORMAT") + echo "$B64_DATA" | base64 -d > "$TMPFILE" + IMAGE_URL="$TMPFILE" + log_info "Saved to temp file: $TMPFILE" + + REVISED_PROMPT="" +} + +# ============================================================================ +# Provider: x.ai direct (grok-imagine-image) +# ============================================================================ +generate_xai() { + if [ -z "${XAI_API_KEY:-}" ]; then + log_error "XAI_API_KEY environment variable not set" + echo "Get your API key from: https://console.x.ai" + exit 1 + fi + + RESPONSE=$(curl -s -X POST "https://api.x.ai/v1/images/generations" \ + -H "Authorization: Bearer $XAI_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"model\": \"grok-imagine-image\", + \"prompt\": $(echo "$PROMPT" | jq -Rs .), + \"n\": 1 + }") + + if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then + ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // .error // "Unknown error"') + log_error "x.ai image generation failed: $ERROR_MSG" + exit 1 + fi + + IMAGE_URL=$(echo "$RESPONSE" | jq -r '.data[0].url // empty') + REVISED_PROMPT="" +} + +# ============================================================================ +# Route to the correct provider +# ============================================================================ +IMAGE_URL="" +REVISED_PROMPT="" + +case "$PROVIDER" in + fal) generate_fal ;; + openai) generate_openai ;; + google) generate_google ;; + xai) generate_xai ;; + *) + log_error "Unknown provider: $PROVIDER" + echo "Supported providers: fal, openai, google, xai" + exit 1 + ;; +esac + +# Validate result if [ -z "$IMAGE_URL" ]; then log_error "Failed to extract image URL from response" - echo "Response: $RESPONSE" exit 1 fi log_info "Image generated successfully!" log_info "URL: $IMAGE_URL" -# Get revised prompt if available -REVISED_PROMPT=$(echo "$RESPONSE" | jq -r '.revised_prompt // empty') if [ -n "$REVISED_PROMPT" ]; then log_info "Revised prompt: $REVISED_PROMPT" fi @@ -118,22 +287,15 @@ fi log_info "Sending to channel: $CHANNEL" if [ "$USE_CLI" = true ]; then - # Use OpenClaw CLI openclaw message send \ --action send \ --channel "$CHANNEL" \ --message "$CAPTION" \ --media "$IMAGE_URL" else - # Direct API call to local gateway GATEWAY_URL="${OPENCLAW_GATEWAY_URL:-http://localhost:18789}" GATEWAY_TOKEN="${OPENCLAW_GATEWAY_TOKEN:-}" - HEADERS="-H \"Content-Type: application/json\"" - if [ -n "$GATEWAY_TOKEN" ]; then - HEADERS="$HEADERS -H \"Authorization: Bearer $GATEWAY_TOKEN\"" - fi - curl -s -X POST "$GATEWAY_URL/message" \ -H "Content-Type: application/json" \ ${GATEWAY_TOKEN:+-H "Authorization: Bearer $GATEWAY_TOKEN"} \ @@ -154,9 +316,11 @@ jq -n \ --arg url "$IMAGE_URL" \ --arg channel "$CHANNEL" \ --arg prompt "$PROMPT" \ + --arg provider "$PROVIDER" \ '{ success: true, image_url: $url, channel: $channel, - prompt: $prompt + prompt: $prompt, + provider: $provider }' diff --git a/skill/scripts/clawra-selfie.ts b/skill/scripts/clawra-selfie.ts index e15f644a..4b336910 100644 --- a/skill/scripts/clawra-selfie.ts +++ b/skill/scripts/clawra-selfie.ts @@ -1,43 +1,45 @@ /** - * Grok Imagine to OpenClaw Integration + * Clawra Selfie – Multi-Provider Image Generation & Editing * - * Generates images using xAI's Grok Imagine model via fal.ai - * and sends them to messaging channels via OpenClaw. + * Generates or edits images using multiple AI providers and sends them + * to messaging channels via OpenClaw. + * + * Supported providers (set via IMAGE_PROVIDER env var): + * - fal (fal.ai / Grok Imagine) – requires FAL_KEY + * - openai (OpenAI gpt-image-1) – requires OPENAI_API_KEY + * - google (Google Gemini) – requires GEMINI_API_KEY + * - xai (x.ai direct) – requires XAI_API_KEY * * Usage: - * npx ts-node grok-imagine-send.ts "" "" [""] + * npx ts-node clawra-selfie.ts [caption] [aspect_ratio] [output_format] [provider] * * Environment variables: - * FAL_KEY - Your fal.ai API key - * OPENCLAW_GATEWAY_URL - OpenClaw gateway URL (default: http://localhost:18789) - * OPENCLAW_GATEWAY_TOKEN - Gateway auth token (optional) + * IMAGE_PROVIDER - Provider to use (default: fal) + * FAL_KEY / OPENAI_API_KEY / GEMINI_API_KEY / XAI_API_KEY + * OPENCLAW_GATEWAY_URL - OpenClaw gateway URL (default: http://localhost:18789) + * OPENCLAW_GATEWAY_TOKEN - Gateway auth token (optional) */ import { exec } from "child_process"; import { promisify } from "util"; +import { + getProvider, + PROVIDER_ENV_KEYS, + AVAILABLE_PROVIDERS, +} from "./providers"; +import type { + AspectRatio, + OutputFormat, + ProviderName, + GenerateImageResult, +} from "./providers"; + const execAsync = promisify(exec); +// --------------------------------------------------------------------------- // Types -interface GrokImagineInput { - prompt: string; - num_images?: number; - aspect_ratio?: AspectRatio; - output_format?: OutputFormat; -} - -interface GrokImagineImage { - url: string; - content_type: string; - file_name?: string; - width: number; - height: number; -} - -interface GrokImagineResponse { - images: GrokImagineImage[]; - revised_prompt?: string; -} +// --------------------------------------------------------------------------- interface OpenClawMessage { action: "send"; @@ -46,29 +48,13 @@ interface OpenClawMessage { media?: string; } -type AspectRatio = - | "2:1" - | "20:9" - | "19.5:9" - | "16:9" - | "4:3" - | "3:2" - | "1:1" - | "2:3" - | "3:4" - | "9:16" - | "9:19.5" - | "9:20" - | "1:2"; - -type OutputFormat = "jpeg" | "png" | "webp"; - interface GenerateAndSendOptions { prompt: string; channel: string; caption?: string; aspectRatio?: AspectRatio; outputFormat?: OutputFormat; + provider?: ProviderName; useClaudeCodeCLI?: boolean; } @@ -77,87 +63,24 @@ interface Result { imageUrl: string; channel: string; prompt: string; + provider: string; revisedPrompt?: string; } -// Check for fal.ai client -let falClient: any; -try { - const { fal } = require("@fal-ai/client"); - falClient = fal; -} catch { - // Will use fetch instead - falClient = null; -} - -/** - * Generate image using Grok Imagine via fal.ai - */ -async function generateImage( - input: GrokImagineInput -): Promise { - const falKey = process.env.FAL_KEY; - - if (!falKey) { - throw new Error( - "FAL_KEY environment variable not set. Get your key from https://fal.ai/dashboard/keys" - ); - } - - // Use fal client if available - if (falClient) { - falClient.config({ credentials: falKey }); - - const result = await falClient.subscribe("xai/grok-imagine-image", { - input: { - prompt: input.prompt, - num_images: input.num_images || 1, - aspect_ratio: input.aspect_ratio || "1:1", - output_format: input.output_format || "jpeg", - }, - }); - - return result.data as GrokImagineResponse; - } - - // Fallback to fetch - const response = await fetch("https://fal.run/xai/grok-imagine-image", { - method: "POST", - headers: { - Authorization: `Key ${falKey}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - prompt: input.prompt, - num_images: input.num_images || 1, - aspect_ratio: input.aspect_ratio || "1:1", - output_format: input.output_format || "jpeg", - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Image generation failed: ${error}`); - } +// --------------------------------------------------------------------------- +// OpenClaw integration +// --------------------------------------------------------------------------- - return response.json(); -} - -/** - * Send image via OpenClaw - */ async function sendViaOpenClaw( message: OpenClawMessage, useCLI: boolean = true ): Promise { if (useCLI) { - // Use OpenClaw CLI const cmd = `openclaw message send --action send --channel "${message.channel}" --message "${message.message}" --media "${message.media}"`; await execAsync(cmd); return; } - // Direct API call const gatewayUrl = process.env.OPENCLAW_GATEWAY_URL || "http://localhost:18789"; const gatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN; @@ -182,36 +105,43 @@ async function sendViaOpenClaw( } } -/** - * Main function: Generate image and send to channel - */ -async function generateAndSend(options: GenerateAndSendOptions): Promise { +// --------------------------------------------------------------------------- +// Main logic +// --------------------------------------------------------------------------- + +async function generateAndSend( + options: GenerateAndSendOptions +): Promise { const { prompt, channel, - caption = "Generated with Grok Imagine", + caption = "Generated with Clawra", aspectRatio = "1:1", outputFormat = "jpeg", + provider: providerName, useClaudeCodeCLI = true, } = options; - console.log(`[INFO] Generating image with Grok Imagine...`); + // Resolve provider + const provider = getProvider(providerName); + console.log(`[INFO] Provider: ${provider.name}`); + console.log(`[INFO] Generating image...`); console.log(`[INFO] Prompt: ${prompt}`); console.log(`[INFO] Aspect ratio: ${aspectRatio}`); // Generate image - const imageResult = await generateImage({ + const imageResult: GenerateImageResult = await provider.generateImage({ prompt, - num_images: 1, - aspect_ratio: aspectRatio, - output_format: outputFormat, + numImages: 1, + aspectRatio, + outputFormat, }); const imageUrl = imageResult.images[0].url; console.log(`[INFO] Image generated: ${imageUrl}`); - if (imageResult.revised_prompt) { - console.log(`[INFO] Revised prompt: ${imageResult.revised_prompt}`); + if (imageResult.revisedPrompt) { + console.log(`[INFO] Revised prompt: ${imageResult.revisedPrompt}`); } // Send via OpenClaw @@ -234,35 +164,42 @@ async function generateAndSend(options: GenerateAndSendOptions): Promise imageUrl, channel, prompt, - revisedPrompt: imageResult.revised_prompt, + provider: provider.name, + revisedPrompt: imageResult.revisedPrompt, }; } +// --------------------------------------------------------------------------- // CLI entry point +// --------------------------------------------------------------------------- + async function main() { const args = process.argv.slice(2); if (args.length < 2) { console.log(` -Usage: npx ts-node grok-imagine-send.ts [caption] [aspect_ratio] [output_format] +Usage: npx ts-node clawra-selfie.ts [caption] [aspect_ratio] [output_format] [provider] Arguments: prompt - Image description (required) channel - Target channel (required) e.g., #general, @user - caption - Message caption (default: 'Generated with Grok Imagine') + caption - Message caption (default: 'Generated with Clawra') aspect_ratio - Image ratio (default: 1:1) Options: 2:1, 16:9, 4:3, 1:1, 3:4, 9:16 output_format - Image format (default: jpeg) Options: jpeg, png, webp + provider - Image provider (default: fal) Options: ${AVAILABLE_PROVIDERS.join(", ")} Environment: - FAL_KEY - Your fal.ai API key (required) + IMAGE_PROVIDER - Default provider (overridden by provider argument) +${AVAILABLE_PROVIDERS.map((p) => ` ${PROVIDER_ENV_KEYS[p].padEnd(17)}- Required for provider: ${p}`).join("\n")} Example: - FAL_KEY=your_key npx ts-node grok-imagine-send.ts "A cyberpunk city" "#art" "Check this out!" + FAL_KEY=xxx npx ts-node clawra-selfie.ts "A cyberpunk city" "#art" "Check this!" + IMAGE_PROVIDER=openai OPENAI_API_KEY=xxx npx ts-node clawra-selfie.ts "A sunset" "#photos" `); process.exit(1); } - const [prompt, channel, caption, aspectRatio, outputFormat] = args; + const [prompt, channel, caption, aspectRatio, outputFormat, provider] = args; try { const result = await generateAndSend({ @@ -271,6 +208,7 @@ Example: caption, aspectRatio: aspectRatio as AspectRatio, outputFormat: outputFormat as OutputFormat, + provider: provider as ProviderName, }); console.log("\n--- Result ---"); @@ -283,11 +221,8 @@ Example: // Export for module use export { - generateImage, sendViaOpenClaw, generateAndSend, - GrokImagineInput, - GrokImagineResponse, OpenClawMessage, GenerateAndSendOptions, Result, diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..ff0b6b08 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "ES2020" + ], + "types": [ + "node" + ], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "outDir": "./dist", + "rootDir": "./scripts", + "declaration": true, + "noEmit": true + }, + "include": [ + "scripts/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file