From d7b9cccb7bbe26e4660d0810cf77f0a00e645fce Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:39:58 +0000 Subject: [PATCH 1/3] feat: add MiniMax image_generation provider Add MiniMax as an image generation provider alongside the existing Grok Imagine (fal.ai) provider. The provider is selected with the PROVIDER environment variable ("grok" default, or "minimax"). MiniMax integration covers the image_generation operation: - Regional endpoints: global_en (api.minimax.io) and cn_zh (api.minimaxi.com), selected via MINIMAX_REGION. - Models: image-01 (default) and image-01-live. - Bearer authorization with MINIMAX_API_KEY. - Request fields: model, prompt, subject_reference, aspect_ratio, width, height, response_format, seed, n, prompt_optimizer. - Response parsing of data.image_urls, metadata.success_count, metadata.failed_count, and base_resp.status_code. Mirrors the changes across scripts/ and skill/scripts/ (TypeScript and bash) and documents the provider in SKILL.md. --- SKILL.md | 37 ++++- scripts/clawra-selfie.sh | 199 ++++++++++++++++++------- scripts/clawra-selfie.ts | 260 +++++++++++++++++++++++++++++---- skill/SKILL.md | 37 ++++- skill/scripts/clawra-selfie.sh | 199 ++++++++++++++++++------- skill/scripts/clawra-selfie.ts | 260 +++++++++++++++++++++++++++++---- 6 files changed, 826 insertions(+), 166 deletions(-) diff --git a/SKILL.md b/SKILL.md index c09ce1db..44725168 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,12 +1,31 @@ --- 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 Clawra's reference image with a configurable image generation provider 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 a fixed reference image using a configurable image generation provider and distribute it across messaging platforms (WhatsApp, Telegram, Discord, Slack, etc.) via OpenClaw. + +## Providers + +The skill supports two image generation providers, selected with the `PROVIDER` environment variable: + +| Provider | `PROVIDER` | API key env | Models | Endpoint | +|----------|------------|-------------|--------|----------| +| Grok Imagine (xAI) via fal.ai | `grok` (default) | `FAL_KEY` | `xai/grok-imagine-image` | `https://fal.run/xai/grok-imagine-image` | +| MiniMax image_generation | `minimax` | `MINIMAX_API_KEY` | `image-01`, `image-01-live` | regional `image_generation` endpoint | + +### MiniMax image_generation + +The MiniMax provider calls the regional `image_generation` endpoint with Bearer authorization and parses the `data.image_urls` response field. + +- **Regions:** `global_en` (default) -> `https://api.minimax.io/v1/image_generation`; `cn_zh` -> `https://api.minimaxi.com/v1/image_generation` +- **Models:** `image-01` (default), `image-01-live` +- **Authorization:** `Bearer $MINIMAX_API_KEY` +- **Request fields:** `model`, `prompt`, `subject_reference`, `aspect_ratio`, `width`, `height`, `response_format`, `seed`, `n`, `prompt_optimizer` +- **Response fields:** `data.image_urls`, `metadata.success_count`, `metadata.failed_count`, `base_resp.status_code` (success = `0`) ## Reference Image @@ -29,15 +48,25 @@ https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png ### Required Environment Variables ```bash +# Provider selection (optional): "grok" (default) or "minimax" +PROVIDER=grok + +# Grok Imagine provider (fal.ai) FAL_KEY=your_fal_api_key # Get from https://fal.ai/dashboard/keys + +# MiniMax provider (image_generation) +MINIMAX_API_KEY=your_minimax_key # Get from https://platform.minimax.io +MINIMAX_REGION=global_en # global_en (default) or cn_zh +MINIMAX_MODEL=image-01 # image-01 (default) or image-01-live + 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 +2. **Generate/edit image** via the configured provider (Grok Imagine via fal.ai, or MiniMax image_generation) +3. **Extract image URL** from the response (`images[0].url` for Grok, `data.image_urls[0]` for MiniMax) 4. **Send to OpenClaw** with target channel(s) ## Step-by-Step Instructions diff --git a/scripts/clawra-selfie.sh b/scripts/clawra-selfie.sh index 72d176d5..74a15b0c 100755 --- a/scripts/clawra-selfie.sh +++ b/scripts/clawra-selfie.sh @@ -1,14 +1,23 @@ #!/bin/bash -# grok-imagine-send.sh -# Generate an image with Grok Imagine and send it via OpenClaw +# clawra-selfie.sh +# Generate an image with a configurable provider and send it via OpenClaw. # -# Usage: ./grok-imagine-send.sh "" "" [""] +# Supported providers: +# grok - xAI Grok Imagine via fal.ai (default) +# minimax - MiniMax image-01 / image-01-live via regional image_generation endpoints # -# Environment variables required: -# FAL_KEY - Your fal.ai API key +# Usage: ./clawra-selfie.sh "" "" [""] [aspect_ratio] [output_format] +# +# Environment variables: +# PROVIDER - "grok" (default) or "minimax" +# FAL_KEY - Your fal.ai API key (required for the grok provider) +# MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider) +# MINIMAX_REGION - "global_en" (default) or "cn_zh" +# MINIMAX_MODEL - "image-01" (default) or "image-01-live" # # 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!" +# PROVIDER=minimax MINIMAX_API_KEY=your_key ./clawra-selfie.sh "A sunset over mountains" "#art" "Check this out!" set -euo pipefail @@ -30,12 +39,17 @@ 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 +# Provider selection +PROVIDER="${PROVIDER:-grok}" +case "$PROVIDER" in + grok|minimax) ;; + *) + log_error "Unknown PROVIDER '$PROVIDER'. Supported: grok, minimax" + exit 1 + ;; +esac + +log_info "Provider: $PROVIDER" # Check for jq if ! command -v jq &> /dev/null; then @@ -55,7 +69,7 @@ fi # Parse arguments PROMPT="${1:-}" CHANNEL="${2:-}" -CAPTION="${3:-Generated with Grok Imagine}" +CAPTION="${3:-Generated with Clawra Selfie}" ASPECT_RATIO="${4:-1:1}" OUTPUT_FORMAT="${5:-jpeg}" @@ -65,55 +79,137 @@ 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 Selfie')" 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 " output_format - Image format (default: jpeg) Options: jpeg, png, webp (grok provider)" echo "" - echo "Example:" - echo " $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\"" + echo "Environment:" + echo " PROVIDER - 'grok' (default) or 'minimax'" + echo " FAL_KEY - Your fal.ai API key (required for the grok provider)" + echo " MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider)" + echo " MINIMAX_REGION - 'global_en' (default) or 'cn_zh'" + echo " MINIMAX_MODEL - 'image-01' (default) or 'image-01-live'" + echo "" + echo "Example (Grok):" + echo " FAL_KEY=your_key $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\"" + echo "Example (MiniMax):" + echo " PROVIDER=minimax MINIMAX_API_KEY=your_key $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\"" exit 1 fi -log_info "Generating image with Grok Imagine..." 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 +IMAGE_URL="" + +if [ "$PROVIDER" = "minimax" ]; then + # MiniMax image_generation provider + if [ -z "${MINIMAX_API_KEY:-}" ]; then + log_error "MINIMAX_API_KEY environment variable not set" + echo "Get your API key from: https://platform.minimax.io" + exit 1 + fi -# Extract image URL -IMAGE_URL=$(echo "$RESPONSE" | jq -r '.images[0].url // empty') + MINIMAX_REGION="${MINIMAX_REGION:-global_en}" + MINIMAX_MODEL="${MINIMAX_MODEL:-image-01}" + + case "$MINIMAX_REGION" in + global_en) ENDPOINT="https://api.minimax.io/v1/image_generation" ;; + cn_zh) ENDPOINT="https://api.minimaxi.com/v1/image_generation" ;; + *) + log_error "Unknown MINIMAX_REGION '$MINIMAX_REGION'. Supported: global_en, cn_zh" + exit 1 + ;; + esac + + case "$MINIMAX_MODEL" in + image-01|image-01-live) ;; + *) + log_error "Unknown MINIMAX_MODEL '$MINIMAX_MODEL'. Supported: image-01, image-01-live" + exit 1 + ;; + esac + + log_info "MiniMax region: $MINIMAX_REGION" + log_info "MiniMax model: $MINIMAX_MODEL" + log_info "Endpoint: $ENDPOINT" + + # Build the request body with the documented image_generation request fields. + JSON_PAYLOAD=$(jq -n \ + --arg model "$MINIMAX_MODEL" \ + --arg prompt "$PROMPT" \ + --arg aspect_ratio "$ASPECT_RATIO" \ + '{model: $model, prompt: $prompt, aspect_ratio: $aspect_ratio}') + + RESPONSE=$(curl -s -X POST "$ENDPOINT" \ + -H "Authorization: Bearer $MINIMAX_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$JSON_PAYLOAD") + + # Parse the documented response fields: base_resp.status_code, metadata, data.image_urls. + STATUS_CODE=$(echo "$RESPONSE" | jq -r '.base_resp.status_code // empty') + if [ -n "$STATUS_CODE" ] && [ "$STATUS_CODE" != "0" ]; then + STATUS_MSG=$(echo "$RESPONSE" | jq -r '.base_resp.status_msg // "unknown error"') + log_error "MiniMax image generation failed (status_code=$STATUS_CODE): $STATUS_MSG" + exit 1 + fi -if [ -z "$IMAGE_URL" ]; then - log_error "Failed to extract image URL from response" - echo "Response: $RESPONSE" - exit 1 + SUCCESS_COUNT=$(echo "$RESPONSE" | jq -r '.metadata.success_count // empty') + FAILED_COUNT=$(echo "$RESPONSE" | jq -r '.metadata.failed_count // empty') + if [ -n "$SUCCESS_COUNT" ] || [ -n "$FAILED_COUNT" ]; then + log_info "MiniMax metadata: success_count=${SUCCESS_COUNT:-n/a} failed_count=${FAILED_COUNT:-n/a}" + fi + + IMAGE_URL=$(echo "$RESPONSE" | jq -r '.data.image_urls[0] // empty') + + if [ -z "$IMAGE_URL" ]; then + log_error "Failed to extract image URL from MiniMax response (data.image_urls)" + echo "Response: $RESPONSE" + exit 1 + fi +else + # Grok Imagine provider via fal.ai + 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 + + log_info "Generating image with Grok Imagine..." + + 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\" + }") + + 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 + + IMAGE_URL=$(echo "$RESPONSE" | jq -r '.images[0].url // empty') + + if [ -z "$IMAGE_URL" ]; then + log_error "Failed to extract image URL from response" + echo "Response: $RESPONSE" + exit 1 + fi + + REVISED_PROMPT=$(echo "$RESPONSE" | jq -r '.revised_prompt // empty') + if [ -n "$REVISED_PROMPT" ]; then + log_info "Revised prompt: $REVISED_PROMPT" + fi 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 - # Send via OpenClaw log_info "Sending to channel: $CHANNEL" @@ -129,11 +225,6 @@ else 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 +245,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..73d8711d 100644 --- a/scripts/clawra-selfie.ts +++ b/scripts/clawra-selfie.ts @@ -1,14 +1,23 @@ /** - * Grok Imagine to OpenClaw Integration + * Clawra Selfie - Image Generation to OpenClaw Integration * - * Generates images using xAI's Grok Imagine model via fal.ai - * and sends them to messaging channels via OpenClaw. + * Generates images using a configurable image generation provider and + * sends them to messaging channels via OpenClaw. + * + * Supported providers: + * - grok: xAI Grok Imagine via fal.ai (default) + * - minimax: MiniMax image-01 / image-01-live via the regional + * image_generation endpoints * * Usage: - * npx ts-node grok-imagine-send.ts "" "" [""] + * npx ts-node clawra-selfie.ts "" "" [""] * * Environment variables: - * FAL_KEY - Your fal.ai API key + * PROVIDER - "grok" (default) or "minimax" + * FAL_KEY - Your fal.ai API key (required for the grok provider) + * MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider) + * MINIMAX_REGION - "global_en" (default) or "cn_zh" + * MINIMAX_MODEL - "image-01" (default) or "image-01-live" * OPENCLAW_GATEWAY_URL - OpenClaw gateway URL (default: http://localhost:18789) * OPENCLAW_GATEWAY_TOKEN - Gateway auth token (optional) */ @@ -63,6 +72,48 @@ type AspectRatio = type OutputFormat = "jpeg" | "png" | "webp"; +type Provider = "grok" | "minimax"; + +// MiniMax configuration (derived from the MiniMax image_generation reference). +// Regional endpoints for the image_generation operation. +const MINIMAX_ENDPOINTS: Record = { + global_en: "https://api.minimax.io/v1/image_generation", + cn_zh: "https://api.minimaxi.com/v1/image_generation", +}; + +// Supported MiniMax image models. The first entry is the default. +const MINIMAX_MODELS: string[] = ["image-01", "image-01-live"]; +const MINIMAX_DEFAULT_MODEL: string = MINIMAX_MODELS[0]; +const MINIMAX_DEFAULT_REGION: string = "global_en"; + +// MiniMax image_generation request fields (per the image reference). +interface MiniMaxImageRequest { + model: string; + prompt: string; + subject_reference?: string; + aspect_ratio?: string; + width?: number; + height?: number; + response_format?: "url" | "base64"; + seed?: number; + n?: number; + prompt_optimizer?: boolean; +} + +interface MiniMaxResponse { + data?: { + image_urls?: string[]; + }; + metadata?: { + success_count?: number; + failed_count?: number; + }; + base_resp?: { + status_code?: number; + status_msg?: string; + }; +} + interface GenerateAndSendOptions { prompt: string; channel: string; @@ -70,6 +121,16 @@ interface GenerateAndSendOptions { aspectRatio?: AspectRatio; outputFormat?: OutputFormat; useClaudeCodeCLI?: boolean; + provider?: Provider; + // MiniMax-only options + minimaxRegion?: string; + minimaxModel?: string; + subjectReference?: string; + width?: number; + height?: number; + seed?: number; + n?: number; + promptOptimizer?: boolean; } interface Result { @@ -78,6 +139,7 @@ interface Result { channel: string; prompt: string; revisedPrompt?: string; + provider: Provider; } // Check for fal.ai client @@ -93,7 +155,7 @@ try { /** * Generate image using Grok Imagine via fal.ai */ -async function generateImage( +async function generateImageGrok( input: GrokImagineInput ): Promise { const falKey = process.env.FAL_KEY; @@ -143,6 +205,117 @@ async function generateImage( return response.json(); } +/** + * Generate image using MiniMax image_generation. + * + * Calls the regional image_generation endpoint with Bearer authorization, + * sends the documented request fields, and parses `data.image_urls`. + */ +async function generateImageMiniMax( + options: GenerateAndSendOptions +): Promise { + const apiKey = process.env.MINIMAX_API_KEY; + if (!apiKey) { + throw new Error( + "MINIMAX_API_KEY environment variable not set. Get your key from https://platform.minimax.io" + ); + } + + const region = options.minimaxRegion || MINIMAX_DEFAULT_REGION; + const endpoint = MINIMAX_ENDPOINTS[region]; + if (!endpoint) { + throw new Error( + `Unknown MINIMAX_REGION "${region}". Supported regions: ${Object.keys( + MINIMAX_ENDPOINTS + ).join(", ")}` + ); + } + + const model = options.minimaxModel || MINIMAX_DEFAULT_MODEL; + if (!MINIMAX_MODELS.includes(model)) { + throw new Error( + `Unknown MiniMax model "${model}". Supported models: ${MINIMAX_MODELS.join( + ", " + )}` + ); + } + + // Build the request body from the documented request fields, only including + // optional fields when they are provided. + const body: MiniMaxImageRequest = { + model, + prompt: options.prompt, + }; + + if (options.subjectReference) { + body.subject_reference = options.subjectReference; + } + if (options.aspectRatio) { + body.aspect_ratio = options.aspectRatio; + } + if (options.width !== undefined) { + body.width = options.width; + } + if (options.height !== undefined) { + body.height = options.height; + } + if (options.seed !== undefined) { + body.seed = options.seed; + } + if (options.n !== undefined) { + body.n = options.n; + } + if (options.promptOptimizer !== undefined) { + body.prompt_optimizer = options.promptOptimizer; + } + + console.log(`[INFO] MiniMax region: ${region}`); + console.log(`[INFO] MiniMax model: ${model}`); + console.log(`[INFO] Endpoint: ${endpoint}`); + + const response = await fetch(endpoint, { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`MiniMax image generation failed: ${error}`); + } + + const result = (await response.json()) as MiniMaxResponse; + + // Parse the documented response fields. + const statusCode = result.base_resp?.status_code; + if (statusCode !== undefined && statusCode !== 0) { + const statusMsg = result.base_resp?.status_msg || "unknown error"; + throw new Error( + `MiniMax image generation failed (status_code=${statusCode}): ${statusMsg}` + ); + } + + const imageUrls = result.data?.image_urls; + if (!imageUrls || imageUrls.length === 0) { + throw new Error( + "MiniMax image generation returned no image URLs in data.image_urls" + ); + } + + const successCount = result.metadata?.success_count; + const failedCount = result.metadata?.failed_count; + if (successCount !== undefined || failedCount !== undefined) { + console.log( + `[INFO] MiniMax metadata: success_count=${successCount ?? "n/a"} failed_count=${failedCount ?? "n/a"}` + ); + } + + return imageUrls[0]; +} + /** * Send image via OpenClaw */ @@ -182,6 +355,20 @@ async function sendViaOpenClaw( } } +/** + * Resolve the image generation provider from the environment. + */ +function resolveProvider(optional?: Provider): Provider { + const fromEnv = (process.env.PROVIDER || "").toLowerCase(); + if (optional) { + return optional; + } + if (fromEnv === "minimax") { + return "minimax"; + } + return "grok"; +} + /** * Main function: Generate image and send to channel */ @@ -189,29 +376,39 @@ async function generateAndSend(options: GenerateAndSendOptions): Promise const { prompt, channel, - caption = "Generated with Grok Imagine", + caption = "Generated with Clawra Selfie", aspectRatio = "1:1", outputFormat = "jpeg", useClaudeCodeCLI = true, } = options; - console.log(`[INFO] Generating image with Grok Imagine...`); + const provider = resolveProvider(options.provider); + + console.log(`[INFO] Provider: ${provider}`); + console.log(`[INFO] Generating image...`); console.log(`[INFO] Prompt: ${prompt}`); console.log(`[INFO] Aspect ratio: ${aspectRatio}`); - // Generate image - const imageResult = await generateImage({ - prompt, - num_images: 1, - aspect_ratio: aspectRatio, - output_format: outputFormat, - }); + let imageUrl: string; + let revisedPrompt: string | undefined; + + if (provider === "minimax") { + imageUrl = await generateImageMiniMax(options); + } else { + const imageResult = await generateImageGrok({ + prompt, + num_images: 1, + aspect_ratio: aspectRatio, + output_format: outputFormat, + }); + imageUrl = imageResult.images[0].url; + revisedPrompt = imageResult.revised_prompt; + } - 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 (revisedPrompt) { + console.log(`[INFO] Revised prompt: ${revisedPrompt}`); } // Send via OpenClaw @@ -234,7 +431,8 @@ async function generateAndSend(options: GenerateAndSendOptions): Promise imageUrl, channel, prompt, - revisedPrompt: imageResult.revised_prompt, + revisedPrompt, + provider, }; } @@ -244,20 +442,27 @@ async function main() { 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] 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 Selfie') 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 + output_format - Image format (default: jpeg) Options: jpeg, png, webp (grok provider) Environment: - FAL_KEY - Your fal.ai API key (required) + PROVIDER - "grok" (default) or "minimax" + FAL_KEY - Your fal.ai API key (required for the grok provider) + MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider) + MINIMAX_REGION - "global_en" (default) or "cn_zh" + MINIMAX_MODEL - "image-01" (default) or "image-01-live" + +Example (Grok): + FAL_KEY=your_key npx ts-node clawra-selfie.ts "A cyberpunk city" "#art" "Check this out!" -Example: - FAL_KEY=your_key npx ts-node grok-imagine-send.ts "A cyberpunk city" "#art" "Check this out!" +Example (MiniMax): + PROVIDER=minimax MINIMAX_API_KEY=your_key npx ts-node clawra-selfie.ts "A cyberpunk city" "#art" "Check this out!" `); process.exit(1); } @@ -283,11 +488,14 @@ Example: // Export for module use export { - generateImage, + generateImageGrok, + generateImageMiniMax, sendViaOpenClaw, generateAndSend, GrokImagineInput, GrokImagineResponse, + MiniMaxImageRequest, + MiniMaxResponse, OpenClawMessage, GenerateAndSendOptions, Result, diff --git a/skill/SKILL.md b/skill/SKILL.md index c09ce1db..44725168 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -1,12 +1,31 @@ --- 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 Clawra's reference image with a configurable image generation provider 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 a fixed reference image using a configurable image generation provider and distribute it across messaging platforms (WhatsApp, Telegram, Discord, Slack, etc.) via OpenClaw. + +## Providers + +The skill supports two image generation providers, selected with the `PROVIDER` environment variable: + +| Provider | `PROVIDER` | API key env | Models | Endpoint | +|----------|------------|-------------|--------|----------| +| Grok Imagine (xAI) via fal.ai | `grok` (default) | `FAL_KEY` | `xai/grok-imagine-image` | `https://fal.run/xai/grok-imagine-image` | +| MiniMax image_generation | `minimax` | `MINIMAX_API_KEY` | `image-01`, `image-01-live` | regional `image_generation` endpoint | + +### MiniMax image_generation + +The MiniMax provider calls the regional `image_generation` endpoint with Bearer authorization and parses the `data.image_urls` response field. + +- **Regions:** `global_en` (default) -> `https://api.minimax.io/v1/image_generation`; `cn_zh` -> `https://api.minimaxi.com/v1/image_generation` +- **Models:** `image-01` (default), `image-01-live` +- **Authorization:** `Bearer $MINIMAX_API_KEY` +- **Request fields:** `model`, `prompt`, `subject_reference`, `aspect_ratio`, `width`, `height`, `response_format`, `seed`, `n`, `prompt_optimizer` +- **Response fields:** `data.image_urls`, `metadata.success_count`, `metadata.failed_count`, `base_resp.status_code` (success = `0`) ## Reference Image @@ -29,15 +48,25 @@ https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png ### Required Environment Variables ```bash +# Provider selection (optional): "grok" (default) or "minimax" +PROVIDER=grok + +# Grok Imagine provider (fal.ai) FAL_KEY=your_fal_api_key # Get from https://fal.ai/dashboard/keys + +# MiniMax provider (image_generation) +MINIMAX_API_KEY=your_minimax_key # Get from https://platform.minimax.io +MINIMAX_REGION=global_en # global_en (default) or cn_zh +MINIMAX_MODEL=image-01 # image-01 (default) or image-01-live + 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 +2. **Generate/edit image** via the configured provider (Grok Imagine via fal.ai, or MiniMax image_generation) +3. **Extract image URL** from the response (`images[0].url` for Grok, `data.image_urls[0]` for MiniMax) 4. **Send to OpenClaw** with target channel(s) ## Step-by-Step Instructions diff --git a/skill/scripts/clawra-selfie.sh b/skill/scripts/clawra-selfie.sh index 72d176d5..74a15b0c 100755 --- a/skill/scripts/clawra-selfie.sh +++ b/skill/scripts/clawra-selfie.sh @@ -1,14 +1,23 @@ #!/bin/bash -# grok-imagine-send.sh -# Generate an image with Grok Imagine and send it via OpenClaw +# clawra-selfie.sh +# Generate an image with a configurable provider and send it via OpenClaw. # -# Usage: ./grok-imagine-send.sh "" "" [""] +# Supported providers: +# grok - xAI Grok Imagine via fal.ai (default) +# minimax - MiniMax image-01 / image-01-live via regional image_generation endpoints # -# Environment variables required: -# FAL_KEY - Your fal.ai API key +# Usage: ./clawra-selfie.sh "" "" [""] [aspect_ratio] [output_format] +# +# Environment variables: +# PROVIDER - "grok" (default) or "minimax" +# FAL_KEY - Your fal.ai API key (required for the grok provider) +# MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider) +# MINIMAX_REGION - "global_en" (default) or "cn_zh" +# MINIMAX_MODEL - "image-01" (default) or "image-01-live" # # 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!" +# PROVIDER=minimax MINIMAX_API_KEY=your_key ./clawra-selfie.sh "A sunset over mountains" "#art" "Check this out!" set -euo pipefail @@ -30,12 +39,17 @@ 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 +# Provider selection +PROVIDER="${PROVIDER:-grok}" +case "$PROVIDER" in + grok|minimax) ;; + *) + log_error "Unknown PROVIDER '$PROVIDER'. Supported: grok, minimax" + exit 1 + ;; +esac + +log_info "Provider: $PROVIDER" # Check for jq if ! command -v jq &> /dev/null; then @@ -55,7 +69,7 @@ fi # Parse arguments PROMPT="${1:-}" CHANNEL="${2:-}" -CAPTION="${3:-Generated with Grok Imagine}" +CAPTION="${3:-Generated with Clawra Selfie}" ASPECT_RATIO="${4:-1:1}" OUTPUT_FORMAT="${5:-jpeg}" @@ -65,55 +79,137 @@ 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 Selfie')" 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 " output_format - Image format (default: jpeg) Options: jpeg, png, webp (grok provider)" echo "" - echo "Example:" - echo " $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\"" + echo "Environment:" + echo " PROVIDER - 'grok' (default) or 'minimax'" + echo " FAL_KEY - Your fal.ai API key (required for the grok provider)" + echo " MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider)" + echo " MINIMAX_REGION - 'global_en' (default) or 'cn_zh'" + echo " MINIMAX_MODEL - 'image-01' (default) or 'image-01-live'" + echo "" + echo "Example (Grok):" + echo " FAL_KEY=your_key $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\"" + echo "Example (MiniMax):" + echo " PROVIDER=minimax MINIMAX_API_KEY=your_key $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\"" exit 1 fi -log_info "Generating image with Grok Imagine..." 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 +IMAGE_URL="" + +if [ "$PROVIDER" = "minimax" ]; then + # MiniMax image_generation provider + if [ -z "${MINIMAX_API_KEY:-}" ]; then + log_error "MINIMAX_API_KEY environment variable not set" + echo "Get your API key from: https://platform.minimax.io" + exit 1 + fi -# Extract image URL -IMAGE_URL=$(echo "$RESPONSE" | jq -r '.images[0].url // empty') + MINIMAX_REGION="${MINIMAX_REGION:-global_en}" + MINIMAX_MODEL="${MINIMAX_MODEL:-image-01}" + + case "$MINIMAX_REGION" in + global_en) ENDPOINT="https://api.minimax.io/v1/image_generation" ;; + cn_zh) ENDPOINT="https://api.minimaxi.com/v1/image_generation" ;; + *) + log_error "Unknown MINIMAX_REGION '$MINIMAX_REGION'. Supported: global_en, cn_zh" + exit 1 + ;; + esac + + case "$MINIMAX_MODEL" in + image-01|image-01-live) ;; + *) + log_error "Unknown MINIMAX_MODEL '$MINIMAX_MODEL'. Supported: image-01, image-01-live" + exit 1 + ;; + esac + + log_info "MiniMax region: $MINIMAX_REGION" + log_info "MiniMax model: $MINIMAX_MODEL" + log_info "Endpoint: $ENDPOINT" + + # Build the request body with the documented image_generation request fields. + JSON_PAYLOAD=$(jq -n \ + --arg model "$MINIMAX_MODEL" \ + --arg prompt "$PROMPT" \ + --arg aspect_ratio "$ASPECT_RATIO" \ + '{model: $model, prompt: $prompt, aspect_ratio: $aspect_ratio}') + + RESPONSE=$(curl -s -X POST "$ENDPOINT" \ + -H "Authorization: Bearer $MINIMAX_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$JSON_PAYLOAD") + + # Parse the documented response fields: base_resp.status_code, metadata, data.image_urls. + STATUS_CODE=$(echo "$RESPONSE" | jq -r '.base_resp.status_code // empty') + if [ -n "$STATUS_CODE" ] && [ "$STATUS_CODE" != "0" ]; then + STATUS_MSG=$(echo "$RESPONSE" | jq -r '.base_resp.status_msg // "unknown error"') + log_error "MiniMax image generation failed (status_code=$STATUS_CODE): $STATUS_MSG" + exit 1 + fi -if [ -z "$IMAGE_URL" ]; then - log_error "Failed to extract image URL from response" - echo "Response: $RESPONSE" - exit 1 + SUCCESS_COUNT=$(echo "$RESPONSE" | jq -r '.metadata.success_count // empty') + FAILED_COUNT=$(echo "$RESPONSE" | jq -r '.metadata.failed_count // empty') + if [ -n "$SUCCESS_COUNT" ] || [ -n "$FAILED_COUNT" ]; then + log_info "MiniMax metadata: success_count=${SUCCESS_COUNT:-n/a} failed_count=${FAILED_COUNT:-n/a}" + fi + + IMAGE_URL=$(echo "$RESPONSE" | jq -r '.data.image_urls[0] // empty') + + if [ -z "$IMAGE_URL" ]; then + log_error "Failed to extract image URL from MiniMax response (data.image_urls)" + echo "Response: $RESPONSE" + exit 1 + fi +else + # Grok Imagine provider via fal.ai + 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 + + log_info "Generating image with Grok Imagine..." + + 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\" + }") + + 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 + + IMAGE_URL=$(echo "$RESPONSE" | jq -r '.images[0].url // empty') + + if [ -z "$IMAGE_URL" ]; then + log_error "Failed to extract image URL from response" + echo "Response: $RESPONSE" + exit 1 + fi + + REVISED_PROMPT=$(echo "$RESPONSE" | jq -r '.revised_prompt // empty') + if [ -n "$REVISED_PROMPT" ]; then + log_info "Revised prompt: $REVISED_PROMPT" + fi 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 - # Send via OpenClaw log_info "Sending to channel: $CHANNEL" @@ -129,11 +225,6 @@ else 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 +245,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..73d8711d 100644 --- a/skill/scripts/clawra-selfie.ts +++ b/skill/scripts/clawra-selfie.ts @@ -1,14 +1,23 @@ /** - * Grok Imagine to OpenClaw Integration + * Clawra Selfie - Image Generation to OpenClaw Integration * - * Generates images using xAI's Grok Imagine model via fal.ai - * and sends them to messaging channels via OpenClaw. + * Generates images using a configurable image generation provider and + * sends them to messaging channels via OpenClaw. + * + * Supported providers: + * - grok: xAI Grok Imagine via fal.ai (default) + * - minimax: MiniMax image-01 / image-01-live via the regional + * image_generation endpoints * * Usage: - * npx ts-node grok-imagine-send.ts "" "" [""] + * npx ts-node clawra-selfie.ts "" "" [""] * * Environment variables: - * FAL_KEY - Your fal.ai API key + * PROVIDER - "grok" (default) or "minimax" + * FAL_KEY - Your fal.ai API key (required for the grok provider) + * MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider) + * MINIMAX_REGION - "global_en" (default) or "cn_zh" + * MINIMAX_MODEL - "image-01" (default) or "image-01-live" * OPENCLAW_GATEWAY_URL - OpenClaw gateway URL (default: http://localhost:18789) * OPENCLAW_GATEWAY_TOKEN - Gateway auth token (optional) */ @@ -63,6 +72,48 @@ type AspectRatio = type OutputFormat = "jpeg" | "png" | "webp"; +type Provider = "grok" | "minimax"; + +// MiniMax configuration (derived from the MiniMax image_generation reference). +// Regional endpoints for the image_generation operation. +const MINIMAX_ENDPOINTS: Record = { + global_en: "https://api.minimax.io/v1/image_generation", + cn_zh: "https://api.minimaxi.com/v1/image_generation", +}; + +// Supported MiniMax image models. The first entry is the default. +const MINIMAX_MODELS: string[] = ["image-01", "image-01-live"]; +const MINIMAX_DEFAULT_MODEL: string = MINIMAX_MODELS[0]; +const MINIMAX_DEFAULT_REGION: string = "global_en"; + +// MiniMax image_generation request fields (per the image reference). +interface MiniMaxImageRequest { + model: string; + prompt: string; + subject_reference?: string; + aspect_ratio?: string; + width?: number; + height?: number; + response_format?: "url" | "base64"; + seed?: number; + n?: number; + prompt_optimizer?: boolean; +} + +interface MiniMaxResponse { + data?: { + image_urls?: string[]; + }; + metadata?: { + success_count?: number; + failed_count?: number; + }; + base_resp?: { + status_code?: number; + status_msg?: string; + }; +} + interface GenerateAndSendOptions { prompt: string; channel: string; @@ -70,6 +121,16 @@ interface GenerateAndSendOptions { aspectRatio?: AspectRatio; outputFormat?: OutputFormat; useClaudeCodeCLI?: boolean; + provider?: Provider; + // MiniMax-only options + minimaxRegion?: string; + minimaxModel?: string; + subjectReference?: string; + width?: number; + height?: number; + seed?: number; + n?: number; + promptOptimizer?: boolean; } interface Result { @@ -78,6 +139,7 @@ interface Result { channel: string; prompt: string; revisedPrompt?: string; + provider: Provider; } // Check for fal.ai client @@ -93,7 +155,7 @@ try { /** * Generate image using Grok Imagine via fal.ai */ -async function generateImage( +async function generateImageGrok( input: GrokImagineInput ): Promise { const falKey = process.env.FAL_KEY; @@ -143,6 +205,117 @@ async function generateImage( return response.json(); } +/** + * Generate image using MiniMax image_generation. + * + * Calls the regional image_generation endpoint with Bearer authorization, + * sends the documented request fields, and parses `data.image_urls`. + */ +async function generateImageMiniMax( + options: GenerateAndSendOptions +): Promise { + const apiKey = process.env.MINIMAX_API_KEY; + if (!apiKey) { + throw new Error( + "MINIMAX_API_KEY environment variable not set. Get your key from https://platform.minimax.io" + ); + } + + const region = options.minimaxRegion || MINIMAX_DEFAULT_REGION; + const endpoint = MINIMAX_ENDPOINTS[region]; + if (!endpoint) { + throw new Error( + `Unknown MINIMAX_REGION "${region}". Supported regions: ${Object.keys( + MINIMAX_ENDPOINTS + ).join(", ")}` + ); + } + + const model = options.minimaxModel || MINIMAX_DEFAULT_MODEL; + if (!MINIMAX_MODELS.includes(model)) { + throw new Error( + `Unknown MiniMax model "${model}". Supported models: ${MINIMAX_MODELS.join( + ", " + )}` + ); + } + + // Build the request body from the documented request fields, only including + // optional fields when they are provided. + const body: MiniMaxImageRequest = { + model, + prompt: options.prompt, + }; + + if (options.subjectReference) { + body.subject_reference = options.subjectReference; + } + if (options.aspectRatio) { + body.aspect_ratio = options.aspectRatio; + } + if (options.width !== undefined) { + body.width = options.width; + } + if (options.height !== undefined) { + body.height = options.height; + } + if (options.seed !== undefined) { + body.seed = options.seed; + } + if (options.n !== undefined) { + body.n = options.n; + } + if (options.promptOptimizer !== undefined) { + body.prompt_optimizer = options.promptOptimizer; + } + + console.log(`[INFO] MiniMax region: ${region}`); + console.log(`[INFO] MiniMax model: ${model}`); + console.log(`[INFO] Endpoint: ${endpoint}`); + + const response = await fetch(endpoint, { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`MiniMax image generation failed: ${error}`); + } + + const result = (await response.json()) as MiniMaxResponse; + + // Parse the documented response fields. + const statusCode = result.base_resp?.status_code; + if (statusCode !== undefined && statusCode !== 0) { + const statusMsg = result.base_resp?.status_msg || "unknown error"; + throw new Error( + `MiniMax image generation failed (status_code=${statusCode}): ${statusMsg}` + ); + } + + const imageUrls = result.data?.image_urls; + if (!imageUrls || imageUrls.length === 0) { + throw new Error( + "MiniMax image generation returned no image URLs in data.image_urls" + ); + } + + const successCount = result.metadata?.success_count; + const failedCount = result.metadata?.failed_count; + if (successCount !== undefined || failedCount !== undefined) { + console.log( + `[INFO] MiniMax metadata: success_count=${successCount ?? "n/a"} failed_count=${failedCount ?? "n/a"}` + ); + } + + return imageUrls[0]; +} + /** * Send image via OpenClaw */ @@ -182,6 +355,20 @@ async function sendViaOpenClaw( } } +/** + * Resolve the image generation provider from the environment. + */ +function resolveProvider(optional?: Provider): Provider { + const fromEnv = (process.env.PROVIDER || "").toLowerCase(); + if (optional) { + return optional; + } + if (fromEnv === "minimax") { + return "minimax"; + } + return "grok"; +} + /** * Main function: Generate image and send to channel */ @@ -189,29 +376,39 @@ async function generateAndSend(options: GenerateAndSendOptions): Promise const { prompt, channel, - caption = "Generated with Grok Imagine", + caption = "Generated with Clawra Selfie", aspectRatio = "1:1", outputFormat = "jpeg", useClaudeCodeCLI = true, } = options; - console.log(`[INFO] Generating image with Grok Imagine...`); + const provider = resolveProvider(options.provider); + + console.log(`[INFO] Provider: ${provider}`); + console.log(`[INFO] Generating image...`); console.log(`[INFO] Prompt: ${prompt}`); console.log(`[INFO] Aspect ratio: ${aspectRatio}`); - // Generate image - const imageResult = await generateImage({ - prompt, - num_images: 1, - aspect_ratio: aspectRatio, - output_format: outputFormat, - }); + let imageUrl: string; + let revisedPrompt: string | undefined; + + if (provider === "minimax") { + imageUrl = await generateImageMiniMax(options); + } else { + const imageResult = await generateImageGrok({ + prompt, + num_images: 1, + aspect_ratio: aspectRatio, + output_format: outputFormat, + }); + imageUrl = imageResult.images[0].url; + revisedPrompt = imageResult.revised_prompt; + } - 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 (revisedPrompt) { + console.log(`[INFO] Revised prompt: ${revisedPrompt}`); } // Send via OpenClaw @@ -234,7 +431,8 @@ async function generateAndSend(options: GenerateAndSendOptions): Promise imageUrl, channel, prompt, - revisedPrompt: imageResult.revised_prompt, + revisedPrompt, + provider, }; } @@ -244,20 +442,27 @@ async function main() { 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] 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 Selfie') 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 + output_format - Image format (default: jpeg) Options: jpeg, png, webp (grok provider) Environment: - FAL_KEY - Your fal.ai API key (required) + PROVIDER - "grok" (default) or "minimax" + FAL_KEY - Your fal.ai API key (required for the grok provider) + MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider) + MINIMAX_REGION - "global_en" (default) or "cn_zh" + MINIMAX_MODEL - "image-01" (default) or "image-01-live" + +Example (Grok): + FAL_KEY=your_key npx ts-node clawra-selfie.ts "A cyberpunk city" "#art" "Check this out!" -Example: - FAL_KEY=your_key npx ts-node grok-imagine-send.ts "A cyberpunk city" "#art" "Check this out!" +Example (MiniMax): + PROVIDER=minimax MINIMAX_API_KEY=your_key npx ts-node clawra-selfie.ts "A cyberpunk city" "#art" "Check this out!" `); process.exit(1); } @@ -283,11 +488,14 @@ Example: // Export for module use export { - generateImage, + generateImageGrok, + generateImageMiniMax, sendViaOpenClaw, generateAndSend, GrokImagineInput, GrokImagineResponse, + MiniMaxImageRequest, + MiniMaxResponse, OpenClawMessage, GenerateAndSendOptions, Result, From 5eb3143b0c7918f7d84db64bdeb9eece25067c72 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:02:13 +0000 Subject: [PATCH 2/3] fix: honor MiniMax image options --- SKILL.md | 1 + scripts/clawra-selfie.sh | 14 +++++++++++++- scripts/clawra-selfie.ts | 23 +++++++++++++++++++++-- skill/SKILL.md | 1 + skill/scripts/clawra-selfie.sh | 14 +++++++++++++- skill/scripts/clawra-selfie.ts | 23 +++++++++++++++++++++-- 6 files changed, 70 insertions(+), 6 deletions(-) diff --git a/SKILL.md b/SKILL.md index 44725168..3c837f42 100644 --- a/SKILL.md +++ b/SKILL.md @@ -58,6 +58,7 @@ FAL_KEY=your_fal_api_key # Get from https://fal.ai/dashboard/keys MINIMAX_API_KEY=your_minimax_key # Get from https://platform.minimax.io MINIMAX_REGION=global_en # global_en (default) or cn_zh MINIMAX_MODEL=image-01 # image-01 (default) or image-01-live +MINIMAX_RESPONSE_FORMAT=url # url (default) or base64 OPENCLAW_GATEWAY_TOKEN=your_token # From: openclaw doctor --generate-gateway-token ``` diff --git a/scripts/clawra-selfie.sh b/scripts/clawra-selfie.sh index 74a15b0c..1c79407a 100755 --- a/scripts/clawra-selfie.sh +++ b/scripts/clawra-selfie.sh @@ -14,6 +14,7 @@ # MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider) # MINIMAX_REGION - "global_en" (default) or "cn_zh" # MINIMAX_MODEL - "image-01" (default) or "image-01-live" +# MINIMAX_RESPONSE_FORMAT - "url" (default) or "base64" # # Example: # FAL_KEY=your_key ./clawra-selfie.sh "A sunset over mountains" "#art" "Check this out!" @@ -89,6 +90,7 @@ if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then echo " MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider)" echo " MINIMAX_REGION - 'global_en' (default) or 'cn_zh'" echo " MINIMAX_MODEL - 'image-01' (default) or 'image-01-live'" + echo " MINIMAX_RESPONSE_FORMAT - 'url' (default) or 'base64'" echo "" echo "Example (Grok):" echo " FAL_KEY=your_key $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\"" @@ -112,6 +114,7 @@ if [ "$PROVIDER" = "minimax" ]; then MINIMAX_REGION="${MINIMAX_REGION:-global_en}" MINIMAX_MODEL="${MINIMAX_MODEL:-image-01}" + MINIMAX_RESPONSE_FORMAT="${MINIMAX_RESPONSE_FORMAT:-url}" case "$MINIMAX_REGION" in global_en) ENDPOINT="https://api.minimax.io/v1/image_generation" ;; @@ -130,6 +133,14 @@ if [ "$PROVIDER" = "minimax" ]; then ;; esac + case "$MINIMAX_RESPONSE_FORMAT" in + url|base64) ;; + *) + log_error "Unknown MINIMAX_RESPONSE_FORMAT '$MINIMAX_RESPONSE_FORMAT'. Supported: url, base64" + exit 1 + ;; + esac + log_info "MiniMax region: $MINIMAX_REGION" log_info "MiniMax model: $MINIMAX_MODEL" log_info "Endpoint: $ENDPOINT" @@ -139,7 +150,8 @@ if [ "$PROVIDER" = "minimax" ]; then --arg model "$MINIMAX_MODEL" \ --arg prompt "$PROMPT" \ --arg aspect_ratio "$ASPECT_RATIO" \ - '{model: $model, prompt: $prompt, aspect_ratio: $aspect_ratio}') + --arg response_format "$MINIMAX_RESPONSE_FORMAT" \ + '{model: $model, prompt: $prompt, aspect_ratio: $aspect_ratio, response_format: $response_format}') RESPONSE=$(curl -s -X POST "$ENDPOINT" \ -H "Authorization: Bearer $MINIMAX_API_KEY" \ diff --git a/scripts/clawra-selfie.ts b/scripts/clawra-selfie.ts index 73d8711d..4b6bb88f 100644 --- a/scripts/clawra-selfie.ts +++ b/scripts/clawra-selfie.ts @@ -18,6 +18,7 @@ * MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider) * MINIMAX_REGION - "global_en" (default) or "cn_zh" * MINIMAX_MODEL - "image-01" (default) or "image-01-live" + * MINIMAX_RESPONSE_FORMAT - "url" (default) or "base64" * OPENCLAW_GATEWAY_URL - OpenClaw gateway URL (default: http://localhost:18789) * OPENCLAW_GATEWAY_TOKEN - Gateway auth token (optional) */ @@ -73,6 +74,7 @@ type AspectRatio = type OutputFormat = "jpeg" | "png" | "webp"; type Provider = "grok" | "minimax"; +type MiniMaxResponseFormat = "url" | "base64"; // MiniMax configuration (derived from the MiniMax image_generation reference). // Regional endpoints for the image_generation operation. @@ -125,6 +127,7 @@ interface GenerateAndSendOptions { // MiniMax-only options minimaxRegion?: string; minimaxModel?: string; + minimaxResponseFormat?: MiniMaxResponseFormat; subjectReference?: string; width?: number; height?: number; @@ -221,7 +224,10 @@ async function generateImageMiniMax( ); } - const region = options.minimaxRegion || MINIMAX_DEFAULT_REGION; + const region = + options.minimaxRegion || + process.env.MINIMAX_REGION || + MINIMAX_DEFAULT_REGION; const endpoint = MINIMAX_ENDPOINTS[region]; if (!endpoint) { throw new Error( @@ -231,7 +237,8 @@ async function generateImageMiniMax( ); } - const model = options.minimaxModel || MINIMAX_DEFAULT_MODEL; + const model = + options.minimaxModel || process.env.MINIMAX_MODEL || MINIMAX_DEFAULT_MODEL; if (!MINIMAX_MODELS.includes(model)) { throw new Error( `Unknown MiniMax model "${model}". Supported models: ${MINIMAX_MODELS.join( @@ -247,6 +254,17 @@ async function generateImageMiniMax( prompt: options.prompt, }; + const responseFormat = + options.minimaxResponseFormat || + process.env.MINIMAX_RESPONSE_FORMAT || + "url"; + if (responseFormat !== "url" && responseFormat !== "base64") { + throw new Error( + `Unknown MINIMAX_RESPONSE_FORMAT "${responseFormat}". Supported formats: url, base64` + ); + } + body.response_format = responseFormat; + if (options.subjectReference) { body.subject_reference = options.subjectReference; } @@ -457,6 +475,7 @@ Environment: MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider) MINIMAX_REGION - "global_en" (default) or "cn_zh" MINIMAX_MODEL - "image-01" (default) or "image-01-live" + MINIMAX_RESPONSE_FORMAT - "url" (default) or "base64" Example (Grok): FAL_KEY=your_key npx ts-node clawra-selfie.ts "A cyberpunk city" "#art" "Check this out!" diff --git a/skill/SKILL.md b/skill/SKILL.md index 44725168..3c837f42 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -58,6 +58,7 @@ FAL_KEY=your_fal_api_key # Get from https://fal.ai/dashboard/keys MINIMAX_API_KEY=your_minimax_key # Get from https://platform.minimax.io MINIMAX_REGION=global_en # global_en (default) or cn_zh MINIMAX_MODEL=image-01 # image-01 (default) or image-01-live +MINIMAX_RESPONSE_FORMAT=url # url (default) or base64 OPENCLAW_GATEWAY_TOKEN=your_token # From: openclaw doctor --generate-gateway-token ``` diff --git a/skill/scripts/clawra-selfie.sh b/skill/scripts/clawra-selfie.sh index 74a15b0c..1c79407a 100755 --- a/skill/scripts/clawra-selfie.sh +++ b/skill/scripts/clawra-selfie.sh @@ -14,6 +14,7 @@ # MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider) # MINIMAX_REGION - "global_en" (default) or "cn_zh" # MINIMAX_MODEL - "image-01" (default) or "image-01-live" +# MINIMAX_RESPONSE_FORMAT - "url" (default) or "base64" # # Example: # FAL_KEY=your_key ./clawra-selfie.sh "A sunset over mountains" "#art" "Check this out!" @@ -89,6 +90,7 @@ if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then echo " MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider)" echo " MINIMAX_REGION - 'global_en' (default) or 'cn_zh'" echo " MINIMAX_MODEL - 'image-01' (default) or 'image-01-live'" + echo " MINIMAX_RESPONSE_FORMAT - 'url' (default) or 'base64'" echo "" echo "Example (Grok):" echo " FAL_KEY=your_key $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\"" @@ -112,6 +114,7 @@ if [ "$PROVIDER" = "minimax" ]; then MINIMAX_REGION="${MINIMAX_REGION:-global_en}" MINIMAX_MODEL="${MINIMAX_MODEL:-image-01}" + MINIMAX_RESPONSE_FORMAT="${MINIMAX_RESPONSE_FORMAT:-url}" case "$MINIMAX_REGION" in global_en) ENDPOINT="https://api.minimax.io/v1/image_generation" ;; @@ -130,6 +133,14 @@ if [ "$PROVIDER" = "minimax" ]; then ;; esac + case "$MINIMAX_RESPONSE_FORMAT" in + url|base64) ;; + *) + log_error "Unknown MINIMAX_RESPONSE_FORMAT '$MINIMAX_RESPONSE_FORMAT'. Supported: url, base64" + exit 1 + ;; + esac + log_info "MiniMax region: $MINIMAX_REGION" log_info "MiniMax model: $MINIMAX_MODEL" log_info "Endpoint: $ENDPOINT" @@ -139,7 +150,8 @@ if [ "$PROVIDER" = "minimax" ]; then --arg model "$MINIMAX_MODEL" \ --arg prompt "$PROMPT" \ --arg aspect_ratio "$ASPECT_RATIO" \ - '{model: $model, prompt: $prompt, aspect_ratio: $aspect_ratio}') + --arg response_format "$MINIMAX_RESPONSE_FORMAT" \ + '{model: $model, prompt: $prompt, aspect_ratio: $aspect_ratio, response_format: $response_format}') RESPONSE=$(curl -s -X POST "$ENDPOINT" \ -H "Authorization: Bearer $MINIMAX_API_KEY" \ diff --git a/skill/scripts/clawra-selfie.ts b/skill/scripts/clawra-selfie.ts index 73d8711d..4b6bb88f 100644 --- a/skill/scripts/clawra-selfie.ts +++ b/skill/scripts/clawra-selfie.ts @@ -18,6 +18,7 @@ * MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider) * MINIMAX_REGION - "global_en" (default) or "cn_zh" * MINIMAX_MODEL - "image-01" (default) or "image-01-live" + * MINIMAX_RESPONSE_FORMAT - "url" (default) or "base64" * OPENCLAW_GATEWAY_URL - OpenClaw gateway URL (default: http://localhost:18789) * OPENCLAW_GATEWAY_TOKEN - Gateway auth token (optional) */ @@ -73,6 +74,7 @@ type AspectRatio = type OutputFormat = "jpeg" | "png" | "webp"; type Provider = "grok" | "minimax"; +type MiniMaxResponseFormat = "url" | "base64"; // MiniMax configuration (derived from the MiniMax image_generation reference). // Regional endpoints for the image_generation operation. @@ -125,6 +127,7 @@ interface GenerateAndSendOptions { // MiniMax-only options minimaxRegion?: string; minimaxModel?: string; + minimaxResponseFormat?: MiniMaxResponseFormat; subjectReference?: string; width?: number; height?: number; @@ -221,7 +224,10 @@ async function generateImageMiniMax( ); } - const region = options.minimaxRegion || MINIMAX_DEFAULT_REGION; + const region = + options.minimaxRegion || + process.env.MINIMAX_REGION || + MINIMAX_DEFAULT_REGION; const endpoint = MINIMAX_ENDPOINTS[region]; if (!endpoint) { throw new Error( @@ -231,7 +237,8 @@ async function generateImageMiniMax( ); } - const model = options.minimaxModel || MINIMAX_DEFAULT_MODEL; + const model = + options.minimaxModel || process.env.MINIMAX_MODEL || MINIMAX_DEFAULT_MODEL; if (!MINIMAX_MODELS.includes(model)) { throw new Error( `Unknown MiniMax model "${model}". Supported models: ${MINIMAX_MODELS.join( @@ -247,6 +254,17 @@ async function generateImageMiniMax( prompt: options.prompt, }; + const responseFormat = + options.minimaxResponseFormat || + process.env.MINIMAX_RESPONSE_FORMAT || + "url"; + if (responseFormat !== "url" && responseFormat !== "base64") { + throw new Error( + `Unknown MINIMAX_RESPONSE_FORMAT "${responseFormat}". Supported formats: url, base64` + ); + } + body.response_format = responseFormat; + if (options.subjectReference) { body.subject_reference = options.subjectReference; } @@ -457,6 +475,7 @@ Environment: MINIMAX_API_KEY - Your MiniMax API key (required for the minimax provider) MINIMAX_REGION - "global_en" (default) or "cn_zh" MINIMAX_MODEL - "image-01" (default) or "image-01-live" + MINIMAX_RESPONSE_FORMAT - "url" (default) or "base64" Example (Grok): FAL_KEY=your_key npx ts-node clawra-selfie.ts "A cyberpunk city" "#art" "Check this out!" From 4d74bcbceeb0fb0aaf498b90e19ef41cc043b56b Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:18:59 +0800 Subject: [PATCH 3/3] fix: complete MiniMax image request payload --- SKILL.md | 31 ++++++- scripts/clawra-selfie.sh | 78 +++++++++++++++- scripts/clawra-selfie.ts | 165 ++++++++++++++++++++++++++------- skill/SKILL.md | 31 ++++++- skill/scripts/clawra-selfie.sh | 78 +++++++++++++++- skill/scripts/clawra-selfie.ts | 165 ++++++++++++++++++++++++++------- 6 files changed, 464 insertions(+), 84 deletions(-) diff --git a/SKILL.md b/SKILL.md index 3c837f42..3ae4707e 100644 --- a/SKILL.md +++ b/SKILL.md @@ -59,6 +59,12 @@ MINIMAX_API_KEY=your_minimax_key # Get from https://platform.minimax.io MINIMAX_REGION=global_en # global_en (default) or cn_zh MINIMAX_MODEL=image-01 # image-01 (default) or image-01-live MINIMAX_RESPONSE_FORMAT=url # url (default) or base64 +MINIMAX_SUBJECT_REFERENCE=https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png +MINIMAX_WIDTH=1024 # optional; set together with MINIMAX_HEIGHT +MINIMAX_HEIGHT=1024 # optional; set together with MINIMAX_WIDTH +MINIMAX_SEED=12345 # optional integer +MINIMAX_N=1 # optional, 1-9 +MINIMAX_PROMPT_OPTIMIZER=false # optional, true or false OPENCLAW_GATEWAY_TOKEN=your_token # From: openclaw doctor --generate-gateway-token ``` @@ -115,7 +121,22 @@ 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 or Edit with the Configured Provider + +Use the bundled executable for normal skill operation. It selects the regional endpoint, sends Bearer authorization, includes the Clawra reference image as `subject_reference`, and parses `data.image_urls`. + +```bash +# MiniMax with the default global endpoint, image-01 model, and Clawra reference image +PROVIDER=minimax MINIMAX_API_KEY="$MINIMAX_API_KEY" \ + ./scripts/clawra-selfie.sh "$PROMPT" "$CHANNEL" "$CAPTION" "1:1" + +# China endpoint and image-01-live model +PROVIDER=minimax MINIMAX_REGION=cn_zh MINIMAX_MODEL=image-01-live \ + MINIMAX_API_KEY="$MINIMAX_API_KEY" \ + ./scripts/clawra-selfie.sh "$PROMPT" "$CHANNEL" "$CAPTION" "1:1" +``` + +The direct API example below applies only to the `grok` provider. Use the fal.ai API to edit the reference image: @@ -180,7 +201,7 @@ curl -X POST "http://localhost:18789/message" \ }' ``` -## Complete Script Example +## Grok-only Direct API Example ```bash #!/bin/bash @@ -263,7 +284,7 @@ openclaw message send \ echo "Done!" ``` -## Node.js/TypeScript Implementation +## Grok-only Node.js/TypeScript Example ```typescript import { fal } from "@fal-ai/client"; @@ -395,7 +416,7 @@ OpenClaw supports sending to: ## Setup Requirements -### 1. Install fal.ai client (for Node.js usage) +### 1. Install fal.ai client (for Grok Node.js usage) ```bash npm install @fal-ai/client ``` @@ -419,6 +440,8 @@ openclaw gateway start ## Error Handling - **FAL_KEY missing**: Ensure the API key is set in environment +- **MINIMAX_API_KEY missing**: Set the MiniMax API key when `PROVIDER=minimax` +- **MiniMax request failed**: Check the selected region, model, request options, and API quota - **Image edit 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 diff --git a/scripts/clawra-selfie.sh b/scripts/clawra-selfie.sh index 1c79407a..fe791346 100755 --- a/scripts/clawra-selfie.sh +++ b/scripts/clawra-selfie.sh @@ -15,6 +15,11 @@ # MINIMAX_REGION - "global_en" (default) or "cn_zh" # MINIMAX_MODEL - "image-01" (default) or "image-01-live" # MINIMAX_RESPONSE_FORMAT - "url" (default) or "base64" +# MINIMAX_SUBJECT_REFERENCE - Reference image URL or data URL (defaults to Clawra) +# MINIMAX_WIDTH / MINIMAX_HEIGHT - Optional image dimensions, set together +# MINIMAX_SEED - Optional integer seed +# MINIMAX_N - Optional image count from 1 to 9 +# MINIMAX_PROMPT_OPTIMIZER - Optional "true" or "false" # # Example: # FAL_KEY=your_key ./clawra-selfie.sh "A sunset over mountains" "#art" "Check this out!" @@ -71,7 +76,7 @@ fi PROMPT="${1:-}" CHANNEL="${2:-}" CAPTION="${3:-Generated with Clawra Selfie}" -ASPECT_RATIO="${4:-1:1}" +ASPECT_RATIO="${4:-}" OUTPUT_FORMAT="${5:-jpeg}" if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then @@ -81,7 +86,7 @@ if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then echo " prompt - Image description (required)" echo " channel - Target channel (required) e.g., #general, @user" echo " caption - Message caption (default: 'Generated with Clawra Selfie')" - echo " aspect_ratio - Image ratio (default: 1:1) Options: 2:1, 16:9, 4:3, 1:1, 3:4, 9:16" + echo " aspect_ratio - Image ratio (default: 1:1); MiniMax also supports 3:2, 2:3, 21:9" echo " output_format - Image format (default: jpeg) Options: jpeg, png, webp (grok provider)" echo "" echo "Environment:" @@ -91,6 +96,11 @@ if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then echo " MINIMAX_REGION - 'global_en' (default) or 'cn_zh'" echo " MINIMAX_MODEL - 'image-01' (default) or 'image-01-live'" echo " MINIMAX_RESPONSE_FORMAT - 'url' (default) or 'base64'" + echo " MINIMAX_SUBJECT_REFERENCE - Reference image URL or data URL (defaults to Clawra)" + echo " MINIMAX_WIDTH / MINIMAX_HEIGHT - Optional image dimensions, set together" + echo " MINIMAX_SEED - Optional integer seed" + echo " MINIMAX_N - Optional image count from 1 to 9" + echo " MINIMAX_PROMPT_OPTIMIZER - Optional 'true' or 'false'" echo "" echo "Example (Grok):" echo " FAL_KEY=your_key $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\"" @@ -100,7 +110,7 @@ if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then fi log_info "Prompt: $PROMPT" -log_info "Aspect ratio: $ASPECT_RATIO" +log_info "Aspect ratio: ${ASPECT_RATIO:-provider default}" IMAGE_URL="" @@ -115,6 +125,12 @@ if [ "$PROVIDER" = "minimax" ]; then MINIMAX_REGION="${MINIMAX_REGION:-global_en}" MINIMAX_MODEL="${MINIMAX_MODEL:-image-01}" MINIMAX_RESPONSE_FORMAT="${MINIMAX_RESPONSE_FORMAT:-url}" + MINIMAX_SUBJECT_REFERENCE="${MINIMAX_SUBJECT_REFERENCE:-https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png}" + MINIMAX_WIDTH="${MINIMAX_WIDTH:-}" + MINIMAX_HEIGHT="${MINIMAX_HEIGHT:-}" + MINIMAX_SEED="${MINIMAX_SEED:-}" + MINIMAX_N="${MINIMAX_N:-}" + MINIMAX_PROMPT_OPTIMIZER="${MINIMAX_PROMPT_OPTIMIZER:-}" case "$MINIMAX_REGION" in global_en) ENDPOINT="https://api.minimax.io/v1/image_generation" ;; @@ -141,6 +157,40 @@ if [ "$PROVIDER" = "minimax" ]; then ;; esac + case "$ASPECT_RATIO" in + ""|1:1|16:9|4:3|3:2|2:3|3:4|9:16|21:9) ;; + *) + log_error "Unsupported MiniMax aspect ratio '$ASPECT_RATIO'" + exit 1 + ;; + esac + + if { [ -n "$MINIMAX_WIDTH" ] && [ -z "$MINIMAX_HEIGHT" ]; } || \ + { [ -z "$MINIMAX_WIDTH" ] && [ -n "$MINIMAX_HEIGHT" ]; }; then + log_error "MINIMAX_WIDTH and MINIMAX_HEIGHT must be set together" + exit 1 + fi + + for VALUE in "$MINIMAX_WIDTH" "$MINIMAX_HEIGHT" "$MINIMAX_SEED" "$MINIMAX_N"; do + if [ -n "$VALUE" ] && ! [[ "$VALUE" =~ ^-?[0-9]+$ ]]; then + log_error "MiniMax numeric options must be integers" + exit 1 + fi + done + + if [ -n "$MINIMAX_N" ] && { [ "$MINIMAX_N" -lt 1 ] || [ "$MINIMAX_N" -gt 9 ]; }; then + log_error "MINIMAX_N must be between 1 and 9" + exit 1 + fi + + case "$MINIMAX_PROMPT_OPTIMIZER" in + ""|true|false) ;; + *) + log_error "MINIMAX_PROMPT_OPTIMIZER must be 'true' or 'false'" + exit 1 + ;; + esac + log_info "MiniMax region: $MINIMAX_REGION" log_info "MiniMax model: $MINIMAX_MODEL" log_info "Endpoint: $ENDPOINT" @@ -149,9 +199,26 @@ if [ "$PROVIDER" = "minimax" ]; then JSON_PAYLOAD=$(jq -n \ --arg model "$MINIMAX_MODEL" \ --arg prompt "$PROMPT" \ + --arg subject_reference "$MINIMAX_SUBJECT_REFERENCE" \ --arg aspect_ratio "$ASPECT_RATIO" \ --arg response_format "$MINIMAX_RESPONSE_FORMAT" \ - '{model: $model, prompt: $prompt, aspect_ratio: $aspect_ratio, response_format: $response_format}') + --arg width "$MINIMAX_WIDTH" \ + --arg height "$MINIMAX_HEIGHT" \ + --arg seed "$MINIMAX_SEED" \ + --arg n "$MINIMAX_N" \ + --arg prompt_optimizer "$MINIMAX_PROMPT_OPTIMIZER" \ + '{ + model: $model, + prompt: $prompt, + subject_reference: [{type: "character", image_file: $subject_reference}], + response_format: $response_format + } + + (if $aspect_ratio != "" then {aspect_ratio: $aspect_ratio} else {} end) + + (if $width != "" then {width: ($width | tonumber)} else {} end) + + (if $height != "" then {height: ($height | tonumber)} else {} end) + + (if $seed != "" then {seed: ($seed | tonumber)} else {} end) + + (if $n != "" then {n: ($n | tonumber)} else {} end) + + (if $prompt_optimizer != "" then {prompt_optimizer: ($prompt_optimizer == "true")} else {} end)') RESPONSE=$(curl -s -X POST "$ENDPOINT" \ -H "Authorization: Bearer $MINIMAX_API_KEY" \ @@ -188,6 +255,7 @@ else fi log_info "Generating image with Grok Imagine..." + GROK_ASPECT_RATIO="${ASPECT_RATIO:-1:1}" RESPONSE=$(curl -s -X POST "https://fal.run/xai/grok-imagine-image" \ -H "Authorization: Key $FAL_KEY" \ @@ -195,7 +263,7 @@ else -d "{ \"prompt\": $(echo "$PROMPT" | jq -Rs .), \"num_images\": 1, - \"aspect_ratio\": \"$ASPECT_RATIO\", + \"aspect_ratio\": \"$GROK_ASPECT_RATIO\", \"output_format\": \"$OUTPUT_FORMAT\" }") diff --git a/scripts/clawra-selfie.ts b/scripts/clawra-selfie.ts index 4b6bb88f..7455b7ab 100644 --- a/scripts/clawra-selfie.ts +++ b/scripts/clawra-selfie.ts @@ -19,6 +19,11 @@ * MINIMAX_REGION - "global_en" (default) or "cn_zh" * MINIMAX_MODEL - "image-01" (default) or "image-01-live" * MINIMAX_RESPONSE_FORMAT - "url" (default) or "base64" + * MINIMAX_SUBJECT_REFERENCE - Reference image URL or data URL (defaults to Clawra) + * MINIMAX_WIDTH / MINIMAX_HEIGHT - Optional image dimensions, set together + * MINIMAX_SEED - Optional integer seed + * MINIMAX_N - Optional image count from 1 to 9 + * MINIMAX_PROMPT_OPTIMIZER - Optional "true" or "false" * OPENCLAW_GATEWAY_URL - OpenClaw gateway URL (default: http://localhost:18789) * OPENCLAW_GATEWAY_TOKEN - Gateway auth token (optional) */ @@ -27,6 +32,8 @@ 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"; // Types interface GrokImagineInput { @@ -75,25 +82,41 @@ type OutputFormat = "jpeg" | "png" | "webp"; type Provider = "grok" | "minimax"; type MiniMaxResponseFormat = "url" | "base64"; +type MiniMaxRegion = "global_en" | "cn_zh"; +type MiniMaxModel = "image-01" | "image-01-live"; +type MiniMaxAspectRatio = + | "1:1" + | "16:9" + | "4:3" + | "3:2" + | "2:3" + | "3:4" + | "9:16" + | "21:9"; // MiniMax configuration (derived from the MiniMax image_generation reference). // Regional endpoints for the image_generation operation. -const MINIMAX_ENDPOINTS: Record = { +const MINIMAX_ENDPOINTS: Record = { global_en: "https://api.minimax.io/v1/image_generation", cn_zh: "https://api.minimaxi.com/v1/image_generation", }; // Supported MiniMax image models. The first entry is the default. -const MINIMAX_MODELS: string[] = ["image-01", "image-01-live"]; -const MINIMAX_DEFAULT_MODEL: string = MINIMAX_MODELS[0]; -const MINIMAX_DEFAULT_REGION: string = "global_en"; +const MINIMAX_MODELS: MiniMaxModel[] = ["image-01", "image-01-live"]; +const MINIMAX_DEFAULT_MODEL: MiniMaxModel = MINIMAX_MODELS[0]; +const MINIMAX_DEFAULT_REGION: MiniMaxRegion = "global_en"; + +interface MiniMaxSubjectReference { + type: "character"; + image_file: string; +} // MiniMax image_generation request fields (per the image reference). interface MiniMaxImageRequest { - model: string; + model: MiniMaxModel; prompt: string; - subject_reference?: string; - aspect_ratio?: string; + subject_reference?: MiniMaxSubjectReference[]; + aspect_ratio?: MiniMaxAspectRatio; width?: number; height?: number; response_format?: "url" | "base64"; @@ -125,10 +148,10 @@ interface GenerateAndSendOptions { useClaudeCodeCLI?: boolean; provider?: Provider; // MiniMax-only options - minimaxRegion?: string; - minimaxModel?: string; + minimaxRegion?: MiniMaxRegion; + minimaxModel?: MiniMaxModel; minimaxResponseFormat?: MiniMaxResponseFormat; - subjectReference?: string; + subjectReference?: MiniMaxSubjectReference[]; width?: number; height?: number; seed?: number; @@ -224,28 +247,30 @@ async function generateImageMiniMax( ); } - const region = + const regionValue = options.minimaxRegion || process.env.MINIMAX_REGION || MINIMAX_DEFAULT_REGION; - const endpoint = MINIMAX_ENDPOINTS[region]; - if (!endpoint) { + if (!(regionValue in MINIMAX_ENDPOINTS)) { throw new Error( - `Unknown MINIMAX_REGION "${region}". Supported regions: ${Object.keys( + `Unknown MINIMAX_REGION "${regionValue}". Supported regions: ${Object.keys( MINIMAX_ENDPOINTS ).join(", ")}` ); } + const region = regionValue as MiniMaxRegion; + const endpoint = MINIMAX_ENDPOINTS[region]; - const model = + const modelValue = options.minimaxModel || process.env.MINIMAX_MODEL || MINIMAX_DEFAULT_MODEL; - if (!MINIMAX_MODELS.includes(model)) { + if (!MINIMAX_MODELS.includes(modelValue as MiniMaxModel)) { throw new Error( - `Unknown MiniMax model "${model}". Supported models: ${MINIMAX_MODELS.join( + `Unknown MiniMax model "${modelValue}". Supported models: ${MINIMAX_MODELS.join( ", " )}` ); } + const model = modelValue as MiniMaxModel; // Build the request body from the documented request fields, only including // optional fields when they are provided. @@ -265,26 +290,64 @@ async function generateImageMiniMax( } body.response_format = responseFormat; - if (options.subjectReference) { - body.subject_reference = options.subjectReference; - } + body.subject_reference = options.subjectReference || [ + { + type: "character", + image_file: process.env.MINIMAX_SUBJECT_REFERENCE || REFERENCE_IMAGE, + }, + ]; + if (options.aspectRatio) { - body.aspect_ratio = options.aspectRatio; + const supportedRatios: MiniMaxAspectRatio[] = [ + "1:1", + "16:9", + "4:3", + "3:2", + "2:3", + "3:4", + "9:16", + "21:9", + ]; + if (!supportedRatios.includes(options.aspectRatio as MiniMaxAspectRatio)) { + throw new Error( + `Unsupported MiniMax aspect ratio "${options.aspectRatio}". Supported ratios: ${supportedRatios.join( + ", " + )}` + ); + } + body.aspect_ratio = options.aspectRatio as MiniMaxAspectRatio; } - if (options.width !== undefined) { - body.width = options.width; + + const width = + options.width ?? parseOptionalIntegerEnv("MINIMAX_WIDTH"); + const height = + options.height ?? parseOptionalIntegerEnv("MINIMAX_HEIGHT"); + if ((width === undefined) !== (height === undefined)) { + throw new Error("MINIMAX_WIDTH and MINIMAX_HEIGHT must be set together"); } - if (options.height !== undefined) { - body.height = options.height; + if (width !== undefined && height !== undefined) { + body.width = width; + body.height = height; } - if (options.seed !== undefined) { - body.seed = options.seed; + + const seed = options.seed ?? parseOptionalIntegerEnv("MINIMAX_SEED"); + if (seed !== undefined) { + body.seed = seed; } - if (options.n !== undefined) { - body.n = options.n; + + const imageCount = options.n ?? parseOptionalIntegerEnv("MINIMAX_N"); + if (imageCount !== undefined) { + if (imageCount < 1 || imageCount > 9) { + throw new Error("MINIMAX_N must be between 1 and 9"); + } + body.n = imageCount; } - if (options.promptOptimizer !== undefined) { - body.prompt_optimizer = options.promptOptimizer; + + const promptOptimizer = + options.promptOptimizer ?? + parseOptionalBooleanEnv("MINIMAX_PROMPT_OPTIMIZER"); + if (promptOptimizer !== undefined) { + body.prompt_optimizer = promptOptimizer; } console.log(`[INFO] MiniMax region: ${region}`); @@ -334,6 +397,31 @@ async function generateImageMiniMax( return imageUrls[0]; } +function parseOptionalIntegerEnv(name: string): number | undefined { + const value = process.env[name]; + if (!value) { + return undefined; + } + if (!/^-?\d+$/.test(value)) { + throw new Error(`${name} must be an integer`); + } + return Number(value); +} + +function parseOptionalBooleanEnv(name: string): boolean | undefined { + const value = process.env[name]; + if (!value) { + return undefined; + } + if (value === "true") { + return true; + } + if (value === "false") { + return false; + } + throw new Error(`${name} must be "true" or "false"`); +} + /** * Send image via OpenClaw */ @@ -381,10 +469,15 @@ function resolveProvider(optional?: Provider): Provider { if (optional) { return optional; } + if (!fromEnv || fromEnv === "grok") { + return "grok"; + } if (fromEnv === "minimax") { return "minimax"; } - return "grok"; + throw new Error( + `Unknown PROVIDER "${process.env.PROVIDER}". Supported providers: grok, minimax` + ); } /** @@ -466,7 +559,7 @@ Arguments: prompt - Image description (required) channel - Target channel (required) e.g., #general, @user caption - Message caption (default: 'Generated with Clawra Selfie') - aspect_ratio - Image ratio (default: 1:1) Options: 2:1, 16:9, 4:3, 1:1, 3:4, 9:16 + aspect_ratio - Image ratio (default: 1:1); MiniMax also supports 3:2, 2:3, 21:9 output_format - Image format (default: jpeg) Options: jpeg, png, webp (grok provider) Environment: @@ -476,6 +569,11 @@ Environment: MINIMAX_REGION - "global_en" (default) or "cn_zh" MINIMAX_MODEL - "image-01" (default) or "image-01-live" MINIMAX_RESPONSE_FORMAT - "url" (default) or "base64" + MINIMAX_SUBJECT_REFERENCE - Reference image URL or data URL (defaults to Clawra) + MINIMAX_WIDTH / MINIMAX_HEIGHT - Optional image dimensions, set together + MINIMAX_SEED - Optional integer seed + MINIMAX_N - Optional image count from 1 to 9 + MINIMAX_PROMPT_OPTIMIZER - Optional "true" or "false" Example (Grok): FAL_KEY=your_key npx ts-node clawra-selfie.ts "A cyberpunk city" "#art" "Check this out!" @@ -515,6 +613,7 @@ export { GrokImagineResponse, MiniMaxImageRequest, MiniMaxResponse, + MiniMaxSubjectReference, OpenClawMessage, GenerateAndSendOptions, Result, diff --git a/skill/SKILL.md b/skill/SKILL.md index 3c837f42..3ae4707e 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -59,6 +59,12 @@ MINIMAX_API_KEY=your_minimax_key # Get from https://platform.minimax.io MINIMAX_REGION=global_en # global_en (default) or cn_zh MINIMAX_MODEL=image-01 # image-01 (default) or image-01-live MINIMAX_RESPONSE_FORMAT=url # url (default) or base64 +MINIMAX_SUBJECT_REFERENCE=https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png +MINIMAX_WIDTH=1024 # optional; set together with MINIMAX_HEIGHT +MINIMAX_HEIGHT=1024 # optional; set together with MINIMAX_WIDTH +MINIMAX_SEED=12345 # optional integer +MINIMAX_N=1 # optional, 1-9 +MINIMAX_PROMPT_OPTIMIZER=false # optional, true or false OPENCLAW_GATEWAY_TOKEN=your_token # From: openclaw doctor --generate-gateway-token ``` @@ -115,7 +121,22 @@ 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 or Edit with the Configured Provider + +Use the bundled executable for normal skill operation. It selects the regional endpoint, sends Bearer authorization, includes the Clawra reference image as `subject_reference`, and parses `data.image_urls`. + +```bash +# MiniMax with the default global endpoint, image-01 model, and Clawra reference image +PROVIDER=minimax MINIMAX_API_KEY="$MINIMAX_API_KEY" \ + ./scripts/clawra-selfie.sh "$PROMPT" "$CHANNEL" "$CAPTION" "1:1" + +# China endpoint and image-01-live model +PROVIDER=minimax MINIMAX_REGION=cn_zh MINIMAX_MODEL=image-01-live \ + MINIMAX_API_KEY="$MINIMAX_API_KEY" \ + ./scripts/clawra-selfie.sh "$PROMPT" "$CHANNEL" "$CAPTION" "1:1" +``` + +The direct API example below applies only to the `grok` provider. Use the fal.ai API to edit the reference image: @@ -180,7 +201,7 @@ curl -X POST "http://localhost:18789/message" \ }' ``` -## Complete Script Example +## Grok-only Direct API Example ```bash #!/bin/bash @@ -263,7 +284,7 @@ openclaw message send \ echo "Done!" ``` -## Node.js/TypeScript Implementation +## Grok-only Node.js/TypeScript Example ```typescript import { fal } from "@fal-ai/client"; @@ -395,7 +416,7 @@ OpenClaw supports sending to: ## Setup Requirements -### 1. Install fal.ai client (for Node.js usage) +### 1. Install fal.ai client (for Grok Node.js usage) ```bash npm install @fal-ai/client ``` @@ -419,6 +440,8 @@ openclaw gateway start ## Error Handling - **FAL_KEY missing**: Ensure the API key is set in environment +- **MINIMAX_API_KEY missing**: Set the MiniMax API key when `PROVIDER=minimax` +- **MiniMax request failed**: Check the selected region, model, request options, and API quota - **Image edit 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 diff --git a/skill/scripts/clawra-selfie.sh b/skill/scripts/clawra-selfie.sh index 1c79407a..fe791346 100755 --- a/skill/scripts/clawra-selfie.sh +++ b/skill/scripts/clawra-selfie.sh @@ -15,6 +15,11 @@ # MINIMAX_REGION - "global_en" (default) or "cn_zh" # MINIMAX_MODEL - "image-01" (default) or "image-01-live" # MINIMAX_RESPONSE_FORMAT - "url" (default) or "base64" +# MINIMAX_SUBJECT_REFERENCE - Reference image URL or data URL (defaults to Clawra) +# MINIMAX_WIDTH / MINIMAX_HEIGHT - Optional image dimensions, set together +# MINIMAX_SEED - Optional integer seed +# MINIMAX_N - Optional image count from 1 to 9 +# MINIMAX_PROMPT_OPTIMIZER - Optional "true" or "false" # # Example: # FAL_KEY=your_key ./clawra-selfie.sh "A sunset over mountains" "#art" "Check this out!" @@ -71,7 +76,7 @@ fi PROMPT="${1:-}" CHANNEL="${2:-}" CAPTION="${3:-Generated with Clawra Selfie}" -ASPECT_RATIO="${4:-1:1}" +ASPECT_RATIO="${4:-}" OUTPUT_FORMAT="${5:-jpeg}" if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then @@ -81,7 +86,7 @@ if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then echo " prompt - Image description (required)" echo " channel - Target channel (required) e.g., #general, @user" echo " caption - Message caption (default: 'Generated with Clawra Selfie')" - echo " aspect_ratio - Image ratio (default: 1:1) Options: 2:1, 16:9, 4:3, 1:1, 3:4, 9:16" + echo " aspect_ratio - Image ratio (default: 1:1); MiniMax also supports 3:2, 2:3, 21:9" echo " output_format - Image format (default: jpeg) Options: jpeg, png, webp (grok provider)" echo "" echo "Environment:" @@ -91,6 +96,11 @@ if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then echo " MINIMAX_REGION - 'global_en' (default) or 'cn_zh'" echo " MINIMAX_MODEL - 'image-01' (default) or 'image-01-live'" echo " MINIMAX_RESPONSE_FORMAT - 'url' (default) or 'base64'" + echo " MINIMAX_SUBJECT_REFERENCE - Reference image URL or data URL (defaults to Clawra)" + echo " MINIMAX_WIDTH / MINIMAX_HEIGHT - Optional image dimensions, set together" + echo " MINIMAX_SEED - Optional integer seed" + echo " MINIMAX_N - Optional image count from 1 to 9" + echo " MINIMAX_PROMPT_OPTIMIZER - Optional 'true' or 'false'" echo "" echo "Example (Grok):" echo " FAL_KEY=your_key $0 \"A cyberpunk city at night\" \"#art-gallery\" \"AI Art!\"" @@ -100,7 +110,7 @@ if [ -z "$PROMPT" ] || [ -z "$CHANNEL" ]; then fi log_info "Prompt: $PROMPT" -log_info "Aspect ratio: $ASPECT_RATIO" +log_info "Aspect ratio: ${ASPECT_RATIO:-provider default}" IMAGE_URL="" @@ -115,6 +125,12 @@ if [ "$PROVIDER" = "minimax" ]; then MINIMAX_REGION="${MINIMAX_REGION:-global_en}" MINIMAX_MODEL="${MINIMAX_MODEL:-image-01}" MINIMAX_RESPONSE_FORMAT="${MINIMAX_RESPONSE_FORMAT:-url}" + MINIMAX_SUBJECT_REFERENCE="${MINIMAX_SUBJECT_REFERENCE:-https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png}" + MINIMAX_WIDTH="${MINIMAX_WIDTH:-}" + MINIMAX_HEIGHT="${MINIMAX_HEIGHT:-}" + MINIMAX_SEED="${MINIMAX_SEED:-}" + MINIMAX_N="${MINIMAX_N:-}" + MINIMAX_PROMPT_OPTIMIZER="${MINIMAX_PROMPT_OPTIMIZER:-}" case "$MINIMAX_REGION" in global_en) ENDPOINT="https://api.minimax.io/v1/image_generation" ;; @@ -141,6 +157,40 @@ if [ "$PROVIDER" = "minimax" ]; then ;; esac + case "$ASPECT_RATIO" in + ""|1:1|16:9|4:3|3:2|2:3|3:4|9:16|21:9) ;; + *) + log_error "Unsupported MiniMax aspect ratio '$ASPECT_RATIO'" + exit 1 + ;; + esac + + if { [ -n "$MINIMAX_WIDTH" ] && [ -z "$MINIMAX_HEIGHT" ]; } || \ + { [ -z "$MINIMAX_WIDTH" ] && [ -n "$MINIMAX_HEIGHT" ]; }; then + log_error "MINIMAX_WIDTH and MINIMAX_HEIGHT must be set together" + exit 1 + fi + + for VALUE in "$MINIMAX_WIDTH" "$MINIMAX_HEIGHT" "$MINIMAX_SEED" "$MINIMAX_N"; do + if [ -n "$VALUE" ] && ! [[ "$VALUE" =~ ^-?[0-9]+$ ]]; then + log_error "MiniMax numeric options must be integers" + exit 1 + fi + done + + if [ -n "$MINIMAX_N" ] && { [ "$MINIMAX_N" -lt 1 ] || [ "$MINIMAX_N" -gt 9 ]; }; then + log_error "MINIMAX_N must be between 1 and 9" + exit 1 + fi + + case "$MINIMAX_PROMPT_OPTIMIZER" in + ""|true|false) ;; + *) + log_error "MINIMAX_PROMPT_OPTIMIZER must be 'true' or 'false'" + exit 1 + ;; + esac + log_info "MiniMax region: $MINIMAX_REGION" log_info "MiniMax model: $MINIMAX_MODEL" log_info "Endpoint: $ENDPOINT" @@ -149,9 +199,26 @@ if [ "$PROVIDER" = "minimax" ]; then JSON_PAYLOAD=$(jq -n \ --arg model "$MINIMAX_MODEL" \ --arg prompt "$PROMPT" \ + --arg subject_reference "$MINIMAX_SUBJECT_REFERENCE" \ --arg aspect_ratio "$ASPECT_RATIO" \ --arg response_format "$MINIMAX_RESPONSE_FORMAT" \ - '{model: $model, prompt: $prompt, aspect_ratio: $aspect_ratio, response_format: $response_format}') + --arg width "$MINIMAX_WIDTH" \ + --arg height "$MINIMAX_HEIGHT" \ + --arg seed "$MINIMAX_SEED" \ + --arg n "$MINIMAX_N" \ + --arg prompt_optimizer "$MINIMAX_PROMPT_OPTIMIZER" \ + '{ + model: $model, + prompt: $prompt, + subject_reference: [{type: "character", image_file: $subject_reference}], + response_format: $response_format + } + + (if $aspect_ratio != "" then {aspect_ratio: $aspect_ratio} else {} end) + + (if $width != "" then {width: ($width | tonumber)} else {} end) + + (if $height != "" then {height: ($height | tonumber)} else {} end) + + (if $seed != "" then {seed: ($seed | tonumber)} else {} end) + + (if $n != "" then {n: ($n | tonumber)} else {} end) + + (if $prompt_optimizer != "" then {prompt_optimizer: ($prompt_optimizer == "true")} else {} end)') RESPONSE=$(curl -s -X POST "$ENDPOINT" \ -H "Authorization: Bearer $MINIMAX_API_KEY" \ @@ -188,6 +255,7 @@ else fi log_info "Generating image with Grok Imagine..." + GROK_ASPECT_RATIO="${ASPECT_RATIO:-1:1}" RESPONSE=$(curl -s -X POST "https://fal.run/xai/grok-imagine-image" \ -H "Authorization: Key $FAL_KEY" \ @@ -195,7 +263,7 @@ else -d "{ \"prompt\": $(echo "$PROMPT" | jq -Rs .), \"num_images\": 1, - \"aspect_ratio\": \"$ASPECT_RATIO\", + \"aspect_ratio\": \"$GROK_ASPECT_RATIO\", \"output_format\": \"$OUTPUT_FORMAT\" }") diff --git a/skill/scripts/clawra-selfie.ts b/skill/scripts/clawra-selfie.ts index 4b6bb88f..7455b7ab 100644 --- a/skill/scripts/clawra-selfie.ts +++ b/skill/scripts/clawra-selfie.ts @@ -19,6 +19,11 @@ * MINIMAX_REGION - "global_en" (default) or "cn_zh" * MINIMAX_MODEL - "image-01" (default) or "image-01-live" * MINIMAX_RESPONSE_FORMAT - "url" (default) or "base64" + * MINIMAX_SUBJECT_REFERENCE - Reference image URL or data URL (defaults to Clawra) + * MINIMAX_WIDTH / MINIMAX_HEIGHT - Optional image dimensions, set together + * MINIMAX_SEED - Optional integer seed + * MINIMAX_N - Optional image count from 1 to 9 + * MINIMAX_PROMPT_OPTIMIZER - Optional "true" or "false" * OPENCLAW_GATEWAY_URL - OpenClaw gateway URL (default: http://localhost:18789) * OPENCLAW_GATEWAY_TOKEN - Gateway auth token (optional) */ @@ -27,6 +32,8 @@ 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"; // Types interface GrokImagineInput { @@ -75,25 +82,41 @@ type OutputFormat = "jpeg" | "png" | "webp"; type Provider = "grok" | "minimax"; type MiniMaxResponseFormat = "url" | "base64"; +type MiniMaxRegion = "global_en" | "cn_zh"; +type MiniMaxModel = "image-01" | "image-01-live"; +type MiniMaxAspectRatio = + | "1:1" + | "16:9" + | "4:3" + | "3:2" + | "2:3" + | "3:4" + | "9:16" + | "21:9"; // MiniMax configuration (derived from the MiniMax image_generation reference). // Regional endpoints for the image_generation operation. -const MINIMAX_ENDPOINTS: Record = { +const MINIMAX_ENDPOINTS: Record = { global_en: "https://api.minimax.io/v1/image_generation", cn_zh: "https://api.minimaxi.com/v1/image_generation", }; // Supported MiniMax image models. The first entry is the default. -const MINIMAX_MODELS: string[] = ["image-01", "image-01-live"]; -const MINIMAX_DEFAULT_MODEL: string = MINIMAX_MODELS[0]; -const MINIMAX_DEFAULT_REGION: string = "global_en"; +const MINIMAX_MODELS: MiniMaxModel[] = ["image-01", "image-01-live"]; +const MINIMAX_DEFAULT_MODEL: MiniMaxModel = MINIMAX_MODELS[0]; +const MINIMAX_DEFAULT_REGION: MiniMaxRegion = "global_en"; + +interface MiniMaxSubjectReference { + type: "character"; + image_file: string; +} // MiniMax image_generation request fields (per the image reference). interface MiniMaxImageRequest { - model: string; + model: MiniMaxModel; prompt: string; - subject_reference?: string; - aspect_ratio?: string; + subject_reference?: MiniMaxSubjectReference[]; + aspect_ratio?: MiniMaxAspectRatio; width?: number; height?: number; response_format?: "url" | "base64"; @@ -125,10 +148,10 @@ interface GenerateAndSendOptions { useClaudeCodeCLI?: boolean; provider?: Provider; // MiniMax-only options - minimaxRegion?: string; - minimaxModel?: string; + minimaxRegion?: MiniMaxRegion; + minimaxModel?: MiniMaxModel; minimaxResponseFormat?: MiniMaxResponseFormat; - subjectReference?: string; + subjectReference?: MiniMaxSubjectReference[]; width?: number; height?: number; seed?: number; @@ -224,28 +247,30 @@ async function generateImageMiniMax( ); } - const region = + const regionValue = options.minimaxRegion || process.env.MINIMAX_REGION || MINIMAX_DEFAULT_REGION; - const endpoint = MINIMAX_ENDPOINTS[region]; - if (!endpoint) { + if (!(regionValue in MINIMAX_ENDPOINTS)) { throw new Error( - `Unknown MINIMAX_REGION "${region}". Supported regions: ${Object.keys( + `Unknown MINIMAX_REGION "${regionValue}". Supported regions: ${Object.keys( MINIMAX_ENDPOINTS ).join(", ")}` ); } + const region = regionValue as MiniMaxRegion; + const endpoint = MINIMAX_ENDPOINTS[region]; - const model = + const modelValue = options.minimaxModel || process.env.MINIMAX_MODEL || MINIMAX_DEFAULT_MODEL; - if (!MINIMAX_MODELS.includes(model)) { + if (!MINIMAX_MODELS.includes(modelValue as MiniMaxModel)) { throw new Error( - `Unknown MiniMax model "${model}". Supported models: ${MINIMAX_MODELS.join( + `Unknown MiniMax model "${modelValue}". Supported models: ${MINIMAX_MODELS.join( ", " )}` ); } + const model = modelValue as MiniMaxModel; // Build the request body from the documented request fields, only including // optional fields when they are provided. @@ -265,26 +290,64 @@ async function generateImageMiniMax( } body.response_format = responseFormat; - if (options.subjectReference) { - body.subject_reference = options.subjectReference; - } + body.subject_reference = options.subjectReference || [ + { + type: "character", + image_file: process.env.MINIMAX_SUBJECT_REFERENCE || REFERENCE_IMAGE, + }, + ]; + if (options.aspectRatio) { - body.aspect_ratio = options.aspectRatio; + const supportedRatios: MiniMaxAspectRatio[] = [ + "1:1", + "16:9", + "4:3", + "3:2", + "2:3", + "3:4", + "9:16", + "21:9", + ]; + if (!supportedRatios.includes(options.aspectRatio as MiniMaxAspectRatio)) { + throw new Error( + `Unsupported MiniMax aspect ratio "${options.aspectRatio}". Supported ratios: ${supportedRatios.join( + ", " + )}` + ); + } + body.aspect_ratio = options.aspectRatio as MiniMaxAspectRatio; } - if (options.width !== undefined) { - body.width = options.width; + + const width = + options.width ?? parseOptionalIntegerEnv("MINIMAX_WIDTH"); + const height = + options.height ?? parseOptionalIntegerEnv("MINIMAX_HEIGHT"); + if ((width === undefined) !== (height === undefined)) { + throw new Error("MINIMAX_WIDTH and MINIMAX_HEIGHT must be set together"); } - if (options.height !== undefined) { - body.height = options.height; + if (width !== undefined && height !== undefined) { + body.width = width; + body.height = height; } - if (options.seed !== undefined) { - body.seed = options.seed; + + const seed = options.seed ?? parseOptionalIntegerEnv("MINIMAX_SEED"); + if (seed !== undefined) { + body.seed = seed; } - if (options.n !== undefined) { - body.n = options.n; + + const imageCount = options.n ?? parseOptionalIntegerEnv("MINIMAX_N"); + if (imageCount !== undefined) { + if (imageCount < 1 || imageCount > 9) { + throw new Error("MINIMAX_N must be between 1 and 9"); + } + body.n = imageCount; } - if (options.promptOptimizer !== undefined) { - body.prompt_optimizer = options.promptOptimizer; + + const promptOptimizer = + options.promptOptimizer ?? + parseOptionalBooleanEnv("MINIMAX_PROMPT_OPTIMIZER"); + if (promptOptimizer !== undefined) { + body.prompt_optimizer = promptOptimizer; } console.log(`[INFO] MiniMax region: ${region}`); @@ -334,6 +397,31 @@ async function generateImageMiniMax( return imageUrls[0]; } +function parseOptionalIntegerEnv(name: string): number | undefined { + const value = process.env[name]; + if (!value) { + return undefined; + } + if (!/^-?\d+$/.test(value)) { + throw new Error(`${name} must be an integer`); + } + return Number(value); +} + +function parseOptionalBooleanEnv(name: string): boolean | undefined { + const value = process.env[name]; + if (!value) { + return undefined; + } + if (value === "true") { + return true; + } + if (value === "false") { + return false; + } + throw new Error(`${name} must be "true" or "false"`); +} + /** * Send image via OpenClaw */ @@ -381,10 +469,15 @@ function resolveProvider(optional?: Provider): Provider { if (optional) { return optional; } + if (!fromEnv || fromEnv === "grok") { + return "grok"; + } if (fromEnv === "minimax") { return "minimax"; } - return "grok"; + throw new Error( + `Unknown PROVIDER "${process.env.PROVIDER}". Supported providers: grok, minimax` + ); } /** @@ -466,7 +559,7 @@ Arguments: prompt - Image description (required) channel - Target channel (required) e.g., #general, @user caption - Message caption (default: 'Generated with Clawra Selfie') - aspect_ratio - Image ratio (default: 1:1) Options: 2:1, 16:9, 4:3, 1:1, 3:4, 9:16 + aspect_ratio - Image ratio (default: 1:1); MiniMax also supports 3:2, 2:3, 21:9 output_format - Image format (default: jpeg) Options: jpeg, png, webp (grok provider) Environment: @@ -476,6 +569,11 @@ Environment: MINIMAX_REGION - "global_en" (default) or "cn_zh" MINIMAX_MODEL - "image-01" (default) or "image-01-live" MINIMAX_RESPONSE_FORMAT - "url" (default) or "base64" + MINIMAX_SUBJECT_REFERENCE - Reference image URL or data URL (defaults to Clawra) + MINIMAX_WIDTH / MINIMAX_HEIGHT - Optional image dimensions, set together + MINIMAX_SEED - Optional integer seed + MINIMAX_N - Optional image count from 1 to 9 + MINIMAX_PROMPT_OPTIMIZER - Optional "true" or "false" Example (Grok): FAL_KEY=your_key npx ts-node clawra-selfie.ts "A cyberpunk city" "#art" "Check this out!" @@ -515,6 +613,7 @@ export { GrokImagineResponse, MiniMaxImageRequest, MiniMaxResponse, + MiniMaxSubjectReference, OpenClawMessage, GenerateAndSendOptions, Result,