Skill: shapes/productai-skill (version 1.0.1)

--- name: productai description: Generate professional AI product photos using ProductAI.photo service. Use when users need to create, enhance, or transform product images for e-commerce, marketing, catalogs, or campaigns. Supports background replacement, product placement, scene generation, adaptive templates, and video ads. Use for tasks involving product photography, lifestyle shots, mockups, or marketing visuals. --- # ProductAI Integration ProductAI.photo is an AI-powered service that generates professional product photos from existing images. It enables e-commerce businesses, marketers, and designers to create studio-quality product photography without hiring photographers. ## Quick Start **1. Get Your API Key** Visit [ProductAI Studio](https://www.productai.photo) → **API Access** → Copy your API key **2. Run Setup** ```bash cd ~/.openclaw/workspace/productai scripts/setup.py # Paste your API key when prompted ``` **3. Generate Images** ```bash # Generate product photo with custom background scripts/generate_photo.py \ --image https://example.com/product.jpg \ --prompt "modern living room with natural lighting" \ --output result.png # Use multiple reference images (nanobanana/seedream support 2 images) scripts/generate_photo.py \ --image https://example.com/product1.jpg https://example.com/product2.jpg \ --prompt "Put the first image on top of the second image" \ --output result.png # High quality with Nano Banana Pro scripts/generate_photo.py \ --image https://example.com/product.jpg \ --prompt "white studio background" \ --model nanobananapro \ --output hq.png # Upscale an image (20 tokens) scripts/upscale_image.py \ --image https://example.com/photo.jpg \ --output upscaled.png ``` ## Core Capabilities **Photo Generation (`/api/generate`)** - Background replacement with AI-generated scenes - Product placement in realistic environments - Multi-image compositing (up to 2 reference images with certain models) - Custom prompts for full creative control **Image Upscaling (`/api/upscale`)** - Professional AI upscaling (Magnific Precision Upscale) - Preserves image details without distortion - 20 tokens per upscale **Models Available** - **gpt-low** — GPT Low Quality (2 tokens) - **gpt-medium** — GPT Medium Quality (3 tokens) - **gpt-high** — GPT High Quality (8 tokens) - **kontext-pro** — Kontext Pro (3 tokens) - **nanobanana** — Nano Banana (3 tokens) — **DEFAULT** - **nanobananapro** — Nano Banana Pro (8 tokens) - **seedream** — Seedream (3 tokens) **Multi-Image Support:** `nanobanana`, `nanobananapro`, and `seedream` support up to 2 reference images for advanced compositing. ## Configuration ### API Setup **Step 1: Get Your API Key** 1. Visit [ProductAI Studio](https://www.productai.photo) 2. Navigate to **API Access** section 3. Click **Generate API Key** or copy existing key **Step 2: Run Setup Script** ```bash cd ~/.openclaw/workspace/productai scripts/setup.py ``` This will interactively create `config.json` with your credentials: ```json { "api_key": "your-api-key-here", "api_endpoint": "https://api.productai.photo/v1", "default_model": "nanobanana", "default_resolution": "1024x1024", "plan": "standard" } ``` The config file is automatically secured with 600 permissions. ### Installation The integration scripts require Python 3.7+ with these dependencies: ```bash pip install requests pillow ``` All dependencies are handled automatically by the scripts. ## Usage Examples ### E-commerce Product Photos Generate clean product shots for online stores: ```bash scripts/generate_photo.py \ --image https://example.com/raw-product.jpg \ --prompt "white studio background with soft shadows" \ --output store-listing.png ``` ### Marketing Campaign Visuals Create lifestyle shots for advertising: ```bash scripts/generate_photo.py \ --image https://example.com/bottle.jpg \ --prompt "outdoor picnic scene with blanket and basket, golden hour lighting" \ --output campaign-hero.png \ --model kontext-pro ``` ### Multi-Image Compositing Combine multiple products into one scene: ```bash scripts/generate_photo.py \ --image https://example.com/product1.jpg https://example.com/product2.jpg \ --prompt "Place the lipstick on top of the cosmetics box" \ --model nanobanana \ --output composite.png ``` ### High-Quality Output Use Nano Banana Pro for premium results: ```bash scripts/generate_photo.py \ --image https://example.com/product.jpg \ --prompt "luxury marble countertop setting with morning light" \ --model nanobananapro \ --output premium.png ``` ### Image Upscaling Upscale images for print or high-res displays: ```bash scripts/upscale_image.py \ --image https://example.com/product.jpg \ --output upscaled.png ``` ### Async Workflow (No Wait) Start generation and check later: ```bash # Start job (prints job ID) scripts/generate_photo.py \ --image https://example.com/product.jpg \ --prompt "studio background" \ --no-wait # Output: Job created: 12345 # Later, resume and download scripts/generate_photo.py \ --job-id 12345 \ --output result.png ``` ## API Reference See [API.md](references/API.md) for complete endpoint documentation, request/response formats, and authentication details. ## Pricing & Token Costs ProductAI uses a token-based pricing system: | Operation | Token Cost | |-----------|------------| | GPT Low Quality Generation | 2 tokens | | GPT Medium Quality Generation | 3 tokens | | GPT High Quality Generation | 8 tokens | | Kontext Pro | 3 tokens | | Nano Banana Pro | 8 tokens | | Nano Banana | 3 tokens | | Seedream | 3 tokens | | Magnific Precision Upscale | 20 tokens | **Subscription Plans:** Visit [ProductAI.photo](https://www.productai.photo) for current plans and token packages. **Rate Limits:** - 15 requests per minute - Daily limits based on subscription plan **Note:** Tokens are deducted when each operation **starts** (not on completion). ## Troubleshooting **API Key Issues** - Verify `config.json` exists: `cat ~/.openclaw/workspace/productai/config.json` - Check API key in ProductAI Studio → API Access - Regenerate key if needed **OUT_OF_TOKENS Error** - Check your token balance in ProductAI Studio - Purchase more tokens or upgrade plan - Remember: tokens are deducted when jobs **start**, not on completion **Rate Limit (429)** - Maximum 15 requests per minute - Wait before retrying - Use `--no-wait` for batch jobs and check status later **Job Failed (ERROR status)** - Check image URL is accessible and under 10MB - Verify image format (PNG, JPG, or WebP only) - Check prompt length and content - Try a different model **Multi-Image Not Working** - Only `nanobanana`, `nanobananapro`, and `seedream` support multiple images - Maximum 2 reference images - Use array format: `--image url1 url2` ## Support - Website: https://www.productai.photo - Documentation: See references/API.md - Contact: [REDACTED_SECRET] (or team contact when available) ## Advanced Topics For detailed API specifications, authentication flows, webhook integration, and batch processing patterns, see [API.md](references/API.md).

path: CHANGELOG.md
content: # Changelog

All notable changes to the ProductAI skill will be documented in this file.

## [1.2.0] - 2026-02-24

### Updated
- **API Reference Documentation** — Complete rewrite of `references/API.md` with official ProductAI API endpoints
- Added detailed documentation for `/api/generate` endpoint with all parameters
- Added `/api/job/:jobId` polling endpoint documentation
- Added `/api/upscale` endpoint details
- Added `/api/generate-custom-model` endpoint for custom model support
- Added `/api/key-check` endpoint for API key validation
- Documented new models: `kontext-max`, updated pricing table
- Added `aspect_ratio` parameter support with extended ratio options for nanobanana/nanobananapro
- Added `resolution` parameter documentation (1K/2K/4K) for nanobanana models
- Improved error handling documentation with specific error codes
- Added best practices section for async workflows and rate limiting

### Technical Details
- Models now include: `gpt-low`, `gpt-medium`, `gpt-high`, `kontext-pro`, `kontext-max`, `nanobanana`, `nanobananapro`, `seedream`
- Rate limit: 15 requests per minute per IP
- Multi-image support: up to 2 images with `nanobanana`, `nanobananapro`, `seedream`
- Output formats: PNG (default), JPG/JPEG
- Aspect ratios: Standard (SQUARE/LANDSCAPE/PORTRAIT) + extended ratios for nano models

## [1.1.0] - 2026-02-23

### Added
- Initial release with core functionality
- Image generation with multiple model support
- Image upscaling support
- Multi-image compositing (up to 2 images)
- Async job polling
- Setup wizard for API key configuration
- Comprehensive documentation and examples

### Features
- `generate_photo.py` — Generate AI product photos
- `upscale_image.py` — Upscale images with Magnific AI
- `batch_generate.py` — Batch processing support
- `setup.py` — Interactive setup wizard


path: QUICKSTART.md
content: # ProductAI Quick Start Guide

Get started with ProductAI in 5 minutes.

## Step 1: Get Your API Key

1. Visit **[ProductAI Studio](https://www.productai.photo)**
2. Click on **API Access** in the navigation
3. Find or generate your API key
4. Copy it to clipboard

> **Where to find it:**
> 
> ![API Access Screenshot - Shows where to find your API key in the ProductAI dashboard]

## Step 2: Run Setup

```bash
cd ~/.openclaw/workspace/productai
./scripts/setup.py
```

When prompted:
- **API Key:** Paste your key from Step 1
- **API Endpoint:** Press Enter (uses default: `https://api.productai.photo/v1`)
- **Default Model:** Press Enter (uses `nanobanana`)
- **Default Resolution:** Press Enter (uses `1024x1024`)
- **Your Plan:** Enter your plan (`basic`, `standard`, or `pro`)

The setup script will:
- Create `config.json` with your settings
- Secure the file (permissions: 600)
- Confirm everything is ready

## Step 3: Generate Your First Image

Try a simple generation:

```bash
./scripts/generate_photo.py \
  --image "https://example.com/your-product.jpg" \
  --prompt "white studio background with soft lighting" \
  --output my-first-result.png
```

**What happens:**
1. API request sent to ProductAI
2. Job created (you'll see the job ID)
3. Script polls for completion (~5-30 seconds)
4. Image downloads to `my-first-result.png`

## Common Use Cases

### Clean Product Backgrounds

```bash
./scripts/generate_photo.py \
  --image "https://example.com/messy-bg.jpg" \
  --prompt "pure white background" \
  --output clean.png
```

### Lifestyle Shots

```bash
./scripts/generate_photo.py \
  --image "https://example.com/watch.jpg" \
  --prompt "wrist wearing the watch, business attire, office desk background" \
  --output lifestyle.png
```

### Combine Multiple Products

```bash
./scripts/generate_photo.py \
  --image "https://example.com/lipstick.jpg" "https://example.com/box.jpg" \
  --prompt "Place the lipstick on top of the cosmetic box" \
  --model nanobanana \
  --output combo.png
```

### High Quality (Nano Banana Pro)

```bash
./scripts/generate_photo.py \
  --image "https://example.com/product.jpg" \
  --prompt "luxury marble countertop, golden hour lighting" \
  --model nanobananapro \
  --output premium.png
```

> **Note:** Nano Banana Pro costs 8 tokens vs 3 for regular models

### Upscale for Print

```bash
./scripts/upscale_image.py \
  --image "https://example.com/product.jpg" \
  --output upscaled.png
```

> **Note:** Upscaling costs 20 tokens

## Available Models

| Model | Token Cost | Best For |
|-------|------------|----------|
| `gpt-low` | 2 | Quick tests, drafts |
| `gpt-medium` | 3 | Standard quality |
| `gpt-high` | 8 | High quality |
| `kontext-pro` | 3 | Product placement |
| `nanobanana` | 3 | **Default** — fast & good |
| `nanobananapro` | 8 | Premium quality |
| `seedream` | 3 | Creative scenes |

**Multi-image support:** `nanobanana`, `nanobananapro`, `seedream` (max 2 images)

## Async Workflow (For Batch Jobs)

Instead of waiting for each job, start them all and check later:

```bash
# Start 3 jobs (collect job IDs)
./scripts/generate_photo.py --image "url1" --prompt "prompt1" --no-wait  # → Job ID: 101
./scripts/generate_photo.py --image "url2" --prompt "prompt2" --no-wait  # → Job ID: 102
./scripts/generate_photo.py --image "url3" --prompt "prompt3" --no-wait  # → Job ID: 103

# Later, download results
./scripts/generate_photo.py --job-id 101 --output result1.png
./scripts/generate_photo.py --job-id 102 --output result2.png
./scripts/generate_photo.py --job-id 103 --output result3.png
```

## Troubleshooting

### "Config file not found"
Run `./scripts/setup.py` first.

### "OUT_OF_TOKENS"
Check your token balance in ProductAI Studio. Purchase more tokens or upgrade plan.

### "Invalid API key" (401)
Regenerate your API key in ProductAI Studio → API Access.

### Job stuck at "RUNNING"
Jobs usually complete in 5-30 seconds. If stuck after 5 minutes, check ProductAI Studio for service status.

### Image not downloaded
Check that the image URL is publicly accessible and under 10MB (PNG, JPG, or WebP).

## Next Steps

- Read [SKILL.md](SKILL.md) for full feature documentation
- Check [API.md](references/API.md) for detailed API reference
- Explore batch processing with `batch_generate.py`
- Set up webhooks for production workflows

## Support

- **Website:** https://www.productai.photo
- **API Docs:** [references/API.md](references/API.md)
- **Issues:** Contact ProductAI support via dashboard

---

**Happy generating! 🚀**


path: SETUP-GUIDE.md
content: # ProductAI Integration Setup Guide

**Goal:** Make getting your ProductAI API key and integrating it with OpenClaw as easy as possible.

## For End Users: Getting Started

### Step 1: Get Your ProductAI API Key

**Option A: If you already have a ProductAI account**

1. Go to **[https://www.productai.photo](https://www.productai.photo)**
2. Log in to your account
3. Navigate to **API Access** (in the dashboard/settings)
4. You'll see your API key displayed — copy it
5. If no key exists, click **Generate API Key**

**Option B: If you're new to ProductAI**

1. Go to **[https://www.productai.photo](https://www.productai.photo)**
2. Sign up for an account
3. Choose a plan (Basic, Standard, or Pro)
4. Once logged in, go to **API Access**
5. Generate your first API key

**⚠️ Important:** Keep your API key secret! Don't share it publicly or commit it to version control.

### Step 2: Run the Setup Script

Open your terminal and run:

```bash
cd ~/.openclaw/workspace/productai
./scripts/setup.py
```

The script will ask you a few questions:

**Q: API Key:**
Paste the key you copied from ProductAI Studio.

**Q: API Endpoint:**
Just press Enter (uses default: `https://api.productai.photo/v1`)

**Q: Default Model:**
Press Enter (uses `nanobanana` — good balance of speed and quality)

**Q: Default Resolution:**
Press Enter (uses `1024x1024`)

**Q: Your Plan:**
Enter `basic`, `standard`, or `pro` depending on your ProductAI subscription.

**That's it!** Your configuration is saved and secured.

### Step 3: Test It

Run a quick test to make sure everything works:

```bash
./scripts/generate_photo.py \
  --image "https://example.com/test-product.jpg" \
  --prompt "white background" \
  --output test.png
```

If you see:
```
✓ Job created: 12345
  Status: RUNNING

Waiting for completion...

✓ Generation complete!
Image URL: https://...
Downloading image to test.png...
✓ Saved to test.png
```

**You're all set!** 🎉

---

## For Skill Creators: Integration Checklist

When building a ProductAI integration for OpenClaw users, here's what to prepare:

### Pre-Integration Questions to Ask

**1. Account Status**
- Do you have a ProductAI account? (Yes / No)
- If yes, which plan? (Basic / Standard / Pro)

**2. API Access**
- Have you generated an API key? (Yes / No)
- If yes, do you have it handy?

**3. Use Case**
- What will you use ProductAI for?
  - E-commerce product photos?
  - Marketing campaigns?
  - Social media content?
  - Other?

### Setup Flow

**Path A: User has API key already**
```
1. Run setup script
2. Paste API key
3. Test with sample generation
4. Done!
```

**Path B: User needs to get API key**
```
1. Direct to productai.photo
2. Guide through signup/login
3. Navigate to API Access
4. Copy key
5. Run setup script
6. Paste key
7. Test
8. Done!
```

### Making It "Super Easy"

**✅ DO:**
- Provide direct links to API Access page
- Show screenshots of where to find the key
- Auto-detect common issues (missing config, invalid key)
- Test API key immediately after setup
- Give clear success/failure messages
- Provide example commands users can copy-paste

**❌ DON'T:**
- Ask for information you can auto-detect (endpoint URL, default model)
- Require manual JSON editing
- Show raw error messages without explanation
- Leave users wondering if setup worked

### Sample Setup Conversation Flow

```
Agent: "Hey! To use ProductAI, I'll need your API key. Do you have one?"

User: "No"

Agent: "No problem! Here's how to get one:

1. Visit https://www.productai.photo
2. Sign up or log in
3. Go to API Access
4. Copy your API key

Let me know when you have it!"

User: "Got it: sk_prod_abc123..."

Agent: "Perfect! Setting that up now..."
[Runs setup script programmatically]

Agent: "✓ API key saved and tested! Want to try generating an image?"

User: "Sure"

Agent: "Great! Send me a product image URL and tell me what background you want."

User: "https://example.com/watch.jpg — modern office desk"

Agent: [Runs generate_photo.py]
"Here's your result! [image]"
```

### Error Handling

**Invalid API Key (401)**
```
Agent: "Hmm, that API key didn't work. Double-check it in ProductAI Studio → API Access. 
Want to try again or regenerate a new key?"
```

**Out of Tokens**
```
Agent: "Looks like you're out of tokens. You can:
- Purchase more tokens at productai.photo
- Upgrade your plan for more monthly credits

Let me know when you're ready to try again!"
```

**Rate Limited (429)**
```
Agent: "ProductAI has a rate limit of 15 requests/minute. Let's wait a bit before trying again."
[Auto-retry after delay]
```

---

## Technical Implementation Notes

### Config File Structure

```json
{
  "api_key": "sk_prod_...",
  "api_endpoint": "https://api.productai.photo/v1",
  "default_model": "nanobanana",
  "default_resolution": "1024x1024",
  "plan": "standard"
}
```

- Stored at: `~/.openclaw/workspace/productai/config.json`
- Permissions: `600` (user read/write only)
- Never log or display the API key

### Testing the API Key

After setup, immediately test with a simple API call:

```python
import requests

response = requests.get(
    "https://api.productai.photo/v1/api/job/1",  # Dummy job ID
    headers={"x-api-key": api_key}
)

if response.status_code == 401:
    print("❌ Invalid API key")
elif response.status_code in [200, 404]:
    print("✓ API key valid!")
else:
    print(f"⚠️ Unexpected response: {response.status_code}")
```

### Programmatic Setup (For Agents)

Agents can run setup programmatically instead of interactively:

```python
import json
from pathlib import Path

config = {
    "api_key": user_provided_key,
    "api_endpoint": "https://api.productai.photo/v1",
    "default_model": "nanobanana",
    "default_resolution": "1024x1024",
    "plan": user_plan or "standard"
}

config_path = Path.home() / '.openclaw' / 'workspace' / 'productai' / 'config.json'
config_path.parent.mkdir(parents=True, exist_ok=True)

with open(config_path, 'w') as f:
    json.dump(config, f, indent=2)

config_path.chmod(0o600)
```

---

## FAQ

**Q: Do I need to install anything?**
A: Python dependencies are auto-installed. Just run the setup script.

**Q: Can I change my API key later?**
A: Yes! Just run `./scripts/setup.py` again and it will overwrite the old config.

**Q: Where is my API key stored?**
A: In `~/.openclaw/workspace/productai/config.json` (secured with 600 permissions).

**Q: Can I use this with multiple ProductAI accounts?**
A: You can only have one API key configured at a time. To switch accounts, run setup again.

**Q: What if I run out of tokens?**
A: Visit ProductAI Studio to purchase more or upgrade your plan.

**Q: How do I know how many tokens I have left?**
A: Check your ProductAI Studio dashboard for current balance.

---

**Questions or issues?** Check [SKILL.md](SKILL.md) or [API.md](references/API.md) for more details.


path: README.md
content: # ProductAI Integration for OpenClaw

Generate professional AI product photos directly from OpenClaw using the ProductAI.photo API.

## What is ProductAI?

ProductAI.photo is an AI-powered service that transforms product images with:
- Background replacement and scene generation
- Multi-image compositing
- Professional upscaling
- Multiple AI models for different quality levels

Perfect for e-commerce, marketing, and content creation.

## Quick Links

- **[ProductAI Website](https://www.productai.photo)** — Sign up & get API key
- **[Quick Start Guide](QUICKSTART.md)** — Get running in 5 minutes
- **[Setup Guide](SETUP-GUIDE.md)** — Detailed setup instructions & FAQ
- **[Skill Documentation](SKILL.md)** — Full feature reference
- **[API Reference](references/API.md)** — Complete API documentation

## Installation

### 1. Get Your API Key

Visit [ProductAI Studio](https://www.productai.photo) → **API Access** → Copy your key

### 2. Run Setup

```bash
cd ~/.openclaw/workspace/productai
./scripts/setup.py
```

Paste your API key when prompted. That's it!

### 3. Generate Images

```bash
./scripts/generate_photo.py \
  --image "https://example.com/product.jpg" \
  --prompt "white studio background" \
  --output result.png
```

## Available Scripts

| Script | Purpose |
|--------|---------|
| `setup.py` | Configure API credentials |
| `generate_photo.py` | Generate product photos |
| `upscale_image.py` | Upscale images (20 tokens) |
| `batch_generate.py` | Batch process multiple images |

## Common Use Cases

### Clean Product Backgrounds
```bash
./scripts/generate_photo.py \
  --image "https://example.com/messy.jpg" \
  --prompt "pure white background" \
  --output clean.png
```

### Lifestyle Photography
```bash
./scripts/generate_photo.py \
  --image "https://example.com/watch.jpg" \
  --prompt "wrist wearing watch, business setting" \
  --output lifestyle.png
```

### Multi-Image Compositing
```bash
./scripts/generate_photo.py \
  --image "url1" "url2" \
  --prompt "Combine both products" \
  --model nanobanana \
  --output combo.png
```

### High Quality (Nano Banana Pro)
```bash
./scripts/generate_photo.py \
  --image "https://example.com/product.jpg" \
  --prompt "luxury marble countertop" \
  --model nanobananapro \
  --output premium.png
```

## Models & Pricing

| Model | Cost | Quality |
|-------|------|---------|
| `gpt-low` | 2 tokens | Low |
| `gpt-medium` | 3 tokens | Medium |
| `nanobanana` | 3 tokens | **Recommended** |
| `kontext-pro` | 3 tokens | Good |
| `seedream` | 3 tokens | Creative |
| `gpt-high` | 8 tokens | High |
| `nanobananapro` | 8 tokens | Premium |
| **Upscale** | 20 tokens | — |

**Multi-image support:** `nanobanana`, `nanobananapro`, `seedream` (max 2 images)

## Documentation

- **New users:** Start with [QUICKSTART.md](QUICKSTART.md)
- **Setup help:** See [SETUP-GUIDE.md](SETUP-GUIDE.md)
- **Feature reference:** Read [SKILL.md](SKILL.md)
- **API details:** Check [API.md](references/API.md)
- **Security:** See [SECURITY.md](SECURITY.md)

## Troubleshooting

**"Config file not found"**
→ Run `./scripts/setup.py` first

**"OUT_OF_TOKENS"**
→ Purchase more tokens at [productai.photo](https://www.productai.photo)

**"Unauthorized" (401)**
→ Check your API key in ProductAI Studio → API Access

**Rate limited (429)**
→ Wait a minute (limit: 15 requests/minute)

More help in [SETUP-GUIDE.md](SETUP-GUIDE.md#troubleshooting)

## Security Features

✅ **HTTPS-only URLs** — Blocks HTTP to prevent insecure requests  
✅ **SSRF Prevention** — Blocks private IPs and localhost  
✅ **Request Timeouts** — All HTTP requests timeout after 30s  
✅ **Rate Limiting** — Batch processing respects 15 req/min limit  
✅ **Path Traversal Protection** — Sanitizes all filenames

See [SECURITY.md](SECURITY.md) for full details.

## Support

- **Website:** https://www.productai.photo
- **Get API Key:** ProductAI Studio → API Access
- **Issues:** Contact ProductAI support via dashboard

---

**Ready to create amazing product photos?** Start with [QUICKSTART.md](QUICKSTART.md)!


path: config.example.json
content: api_key: your-api-key-here
api_endpoint: https://api.productai.photo/v1
default_model: nanobanana
default_resolution: 1024x1024
plan: standard

path: package.json
content: {
  "name": "productai",
  "version": "1.2.0",
  "description": "Generate professional AI product photos using ProductAI.photo service. Supports background replacement, product placement, scene generation, and upscaling for e-commerce and marketing.",
  "author": "Shape Team",
  "license": "MIT",
  "keywords": [
    "image-generation",
    "product-photography",
    "ai",
    "e-commerce",
    "marketing",
    "background-replacement",
    "upscaling"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/shape-team/productai-skill"
  },
  "main": "SKILL.md",
  "scripts": {
    "setup": "python3 scripts/setup.py",
    "generate": "python3 scripts/generate_photo.py",
    "upscale": "python3 scripts/upscale_image.py",
    "batch": "python3 scripts/batch_generate.py"
  },
  "dependencies": {
    "python": ">=3.7",
    "requests": ">=2.25.0"
  },
  "clawhub": {
    "category": "productivity",
    "difficulty": "beginner",
    "setup_required": true,
    "api_key_required": true
  }
}


path: INTEGRATION-QUESTIONS.md
content: # ProductAI Integration - Questions for Users

This document contains the key questions to ask users when setting up ProductAI integration, along with the easiest setup flow.

---

## Pre-Integration Questions

### 1. Do you have a ProductAI account?

**If NO:**
- Direct them to: **[https://www.productai.photo](https://www.productai.photo)**
- They need to sign up and choose a plan (Basic, Standard, or Pro)
- Wait for them to complete signup

**If YES:**
- Proceed to question 2

---

### 2. Do you have a ProductAI API key?

**If NO:**
- Guide them: "Go to ProductAI Studio → **API Access** → Generate API Key"
- Wait for them to copy the key
- **Important:** Remind them to keep it secret!

**If YES:**
- Ask them to have it ready (they'll paste it in setup)

---

### 3. What plan are you on?

Options:
- **Basic** ($8/month) — 70 + 20 free credits
- **Standard** ($16/month) — 250 + 20 free credits
- **Pro** ($49/month) — 950 + 20 free credits

This helps set expectations for token usage.

---

### 4. What will you use ProductAI for?

Common answers:
- E-commerce product photos (clean backgrounds, lifestyle shots)
- Marketing campaigns (hero images, social media)
- Batch processing product catalogs
- Creative compositing (multi-image scenes)
- Image upscaling for print/high-res

This helps recommend the right model and workflow.

---

## Super Easy Setup Flow

### Step 1: Verify They Have an API Key

```
Agent: "To use ProductAI, you'll need an API key. Do you have one?"

User: "No" → Guide to productai.photo, wait
User: "Yes" → Proceed
```

---

### Step 2: Run Setup

**Option A: Interactive (User runs script)**

```bash
cd ~/.openclaw/workspace/productai
./scripts/setup.py
```

The script asks:
1. **API Key:** User pastes key
2. **API Endpoint:** [Press Enter for default]
3. **Default Model:** [Press Enter for nanobanana]
4. **Default Resolution:** [Press Enter for 1024x1024]
5. **Your Plan:** basic / standard / pro

**Option B: Agent-Driven (Programmatic)**

```python
# Agent collects API key in conversation
[REDACTED_SECRET]  # e.g., "sk_prod_abc123..."

# Agent creates config
import json
from pathlib import Path

config = {
    "api_key": api_key,
    "api_endpoint": "https://api.productai.photo/v1",
    "default_model": "nanobanana",
    "default_resolution": "1024x1024",
    "plan": "standard"  # Or ask user
}

config_path = Path.home() / '.openclaw' / 'workspace' / 'productai' / 'config.json'
config_path.parent.mkdir(parents=True, exist_ok=True)

with open(config_path, 'w') as f:
    json.dump(config, f, indent=2)

config_path.chmod(0o600)

# Confirm to user
print("✓ ProductAI configured! Your API key is saved securely.")
```

---

### Step 3: Test It Immediately

**Run a quick test to confirm setup works:**

```bash
./scripts/generate_photo.py \
  --image "https://i.imgur.com/sample.jpg" \
  --prompt "white background" \
  --output test.png
```

**If successful:**
```
✓ Job created: 12345
Waiting for completion...
✓ Generation complete!
Downloading image to test.png...
✓ Saved to test.png
```

**If failed:**
- `401 Unauthorized` → Invalid API key (ask user to regenerate)
- `OUT_OF_TOKENS` → No credits left (ask user to purchase more)
- Other errors → Check error message and guide user

---

## Making API Key Input Super Easy

### Best Practices

**✅ DO:**
1. **Provide direct link:** "Get your key here: [https://www.productai.photo/api-access](https://www.productai.photo)"
2. **Show exactly where to find it:** "Look for **API Access** in your dashboard"
3. **Auto-validate:** Test the API key immediately after setup
4. **Give clear feedback:** "✓ API key valid!" or "❌ Invalid key, please check and try again"
5. **Secure it automatically:** Set file permissions to 600 (user-only read/write)

**❌ DON'T:**
1. Ask for info you can auto-detect (API endpoint, default model)
2. Show raw JSON or complex config formats
3. Leave users wondering if setup worked (always test!)
4. Display the API key in logs or responses (security!)

---

## Sample Conversation Flow

```
Agent: "Ready to set up ProductAI! Do you have an API key?"

User: "No"

Agent: "No problem! Here's what to do:

1. Visit https://www.productai.photo
2. Log in (or sign up if you're new)
3. Go to **API Access**
4. Copy your API key

Let me know when you have it!"

---

User: "Got it: sk_prod_abc123xyz"

Agent: "Perfect! Setting that up now..."
[Creates config.json programmatically]

Agent: "✓ API key saved securely!

Want to test it? Send me a product image URL and I'll generate 
a version with a clean white background."

---

User: "https://example.com/watch.jpg"

Agent: [Runs generate_photo.py]
"Here's your product with a white studio background! 🎨
[Sends generated image]

Cost: 3 tokens (Nano Banana model)

What else would you like to create?"
```

---

## Error Recovery

### Invalid API Key (401)

```
Agent: "Hmm, that API key didn't work. Here's what to check:

1. Make sure you copied the full key (starts with 'sk_prod_')
2. Verify it's active in ProductAI Studio → API Access
3. Try regenerating a new key if needed

Want to try again?"
```

---

### Out of Tokens

```
Agent: "You're out of tokens! Here's how to get more:

- **Purchase tokens:** Visit productai.photo
- **Upgrade plan:** Get more monthly credits with Standard or Pro

Your current plan: [plan]

Let me know when you're ready to continue!"
```

---

### Rate Limited (429)

```
Agent: "ProductAI has a rate limit of 15 requests/minute. 
Let's wait a moment before trying again..."

[Auto-retry after 10 seconds]
```

---

## Key Takeaways

1. **Minimize friction:** Only ask what you absolutely need (API key, plan)
2. **Validate immediately:** Test the API key right after setup
3. **Provide clear guidance:** Direct links, step-by-step, screenshots
4. **Handle errors gracefully:** Clear messages + recovery steps
5. **Make it conversational:** Feel like talking to a helpful human

---

**Goal:** User gets from "I want to use ProductAI" to "Here's my first generated image" in under 2 minutes.


path: SECURITY.md
content: # Security Features & Mitigations

This document outlines the security measures implemented in the ProductAI skill.

## Security Fixes Applied

### ✅ Fixed Issues

#### 1. URL Validation & SSRF Prevention
**Issue:** Image URLs accepted without validation, potential SSRF attacks  
**Fix:** `productai_client.py:validate_image_url()`
- **HTTPS-only:** Rejects HTTP URLs
- **Private network blocking:** Prevents access to:
  - localhost / 127.x.x.x
  - Private IPs (10.x.x.x, 172.16-31.x.x, 192.168.x.x)
  - Link-local addresses (169.254.x.x)
  - IPv6 localhost/link-local (::1, fe80::)
- Applied to all image URL inputs in `generate()` and `upscale()`

#### 2. Request Timeouts
**Issue:** HTTP requests without timeouts could hang indefinitely  
**Fix:** Added 30-second timeout to all requests:
- `productai_client.py`: All API calls (`DEFAULT_TIMEOUT = 30`)
- `generate_photo.py`: Image downloads
- `upscale_image.py`: Image downloads
- `batch_generate.py`: Image downloads

#### 3. Rate Limiting (Batch Processing)
**Issue:** No rate limiting despite 15 req/min API limit  
**Fix:** `batch_generate.py`
- Enforces 4-second minimum between requests (15 req/min = 4s interval)
- Default max workers: 3 (to stay comfortably under limit)
- Sequential request submission with `time.sleep()` between requests

#### 4. Path Traversal Prevention
**Issue:** Unsanitized filenames in batch processing  
**Fix:** `batch_generate.py:sanitize_filename()`
- Removes path separators (`/`, `\`)
- Blocks parent directory references (`..`)
- Allows only alphanumeric, dash, underscore, dot
- Prevents hidden files (leading `.`)

---

## Acceptable Trade-offs

### Plain Text API Key Storage
**Rationale:**
- Industry standard for CLI tools (AWS CLI, gcloud, stripe-cli all do this)
- File permissions set to `600` (user read/write only)
- Alternative (OS keychain) adds complexity and dependencies
- Users can use environment variables if preferred: `PRODUCTAI_API_KEY`

**Mitigation:**
- Config file: `chmod 600` (user-only access)
- API key never logged or displayed in output
- Clear documentation warns users to keep keys secure

---

## Security Best Practices for Users

### 1. Protect Your API Key
- Never commit `config.json` to version control
- Don't share your API key in chat, logs, or screenshots
- Regenerate key if accidentally exposed

### 2. Use HTTPS URLs Only
- The skill enforces HTTPS-only image URLs
- Avoids exposing API keys over unencrypted connections

### 3. Monitor API Usage
- Check ProductAI dashboard regularly for unexpected usage
- Rate limits prevent runaway costs

### 4. Keep Dependencies Updated
- Run `pip install --upgrade requests` periodically
- Monitor security advisories for Python dependencies

---

## Implementation Details

### URL Validation (`productai_client.py`)

```python
@staticmethod
def validate_image_url(url: str) -> None:
    """Validate image URL for security."""
    parsed = urlparse(url)
    
    # Only allow HTTPS
    if parsed.scheme != 'https':
        raise ValueError(f"Only HTTPS URLs are allowed. Got: {parsed.scheme}://")
    
    # Block private/local network addresses (SSRF prevention)
    hostname = parsed.hostname
    if not hostname:
        raise ValueError("Invalid URL: missing hostname")
    
    # Block localhost, private IPs, link-local
    blocked_patterns = [
        r'^localhost$',
        r'^127\.',
        r'^10\.',
        r'^172\.(1[6-9]|2[0-9]|3[01])\.',
        r'^192\.168\.',
        r'^169\.254\.',
        r'^::1$',
        r'^fe80:',
    ]
    
    for pattern in blocked_patterns:
        if re.match(pattern, hostname, re.IGNORECASE):
            raise ValueError(f"Private/local addresses are not allowed: {hostname}")
```

### Request Timeouts

All HTTP requests include explicit timeouts:

```python
# API calls
response = self.session.post(url, json=payload, timeout=30)

# Image downloads
response = requests.get(url, stream=True, timeout=30)
```

### Rate Limiting (`batch_generate.py`)

```python
RATE_LIMIT_SECONDS = 4.0  # 15 req/min = 4s interval

for image_url in image_urls:
    # Rate limiting: ensure minimum time between requests
    elapsed = time.time() - last_request_time
    if elapsed < RATE_LIMIT_SECONDS:
        time.sleep(RATE_LIMIT_SECONDS - elapsed)
    
    # Submit job
    future = executor.submit(process_single_image, ...)
    last_request_time = time.time()
```

### Filename Sanitization (`batch_generate.py`)

```python
def sanitize_filename(filename: str) -> str:
    """Sanitize filename to prevent path traversal."""
    # Remove path separators and parent references
    safe_name = filename.replace('/', '_').replace('\\', '_').replace('..', '_')
    
    # Keep only safe characters
    safe_name = re.sub(r'[^a-zA-Z0-9._-]', '_', safe_name)
    
    # Prevent hidden files
    if safe_name.startswith('.'):
        safe_name = '_' + safe_name[1:]
    
    return safe_name
```

---

## Testing

All security fixes have been tested:

### URL Validation Tests
```bash
✓ Valid HTTPS URL accepted
✓ HTTP blocked: Only HTTPS URLs are allowed. Got: http://
✓ Localhost blocked: Private/local addresses are not allowed: localhost
✓ Private IP blocked: Private/local addresses are not allowed: 192.168.1.1
```

### Filename Sanitization Tests
```bash
✓ "normal.jpg" → "normal.jpg"
✓ "../../../etc/passwd" → "______etc_passwd"
✓ "file/with/slashes.png" → "file_with_slashes.png"
✓ ".hidden" → "_hidden"
✓ "file with spaces.jpg" → "file_with_spaces.jpg"
```

### Integration Test
```bash
✓ Real image generation with HTTPS URL: Success
✓ HTTP URL rejection: Blocked as expected
✓ Request timeout: All requests complete within 30s
```

---

## Reporting Security Issues

If you discover a security vulnerability in this skill:

1. **Do NOT open a public issue**
2. Contact the skill maintainer directly
3. Provide details: affected code, reproduction steps, potential impact
4. Allow reasonable time for a fix before public disclosure

---

## Version History

**v1.1.0** (2026-02-23)
- Added URL validation (HTTPS-only, SSRF prevention)
- Added request timeouts (30s default)
- Added rate limiting for batch processing
- Added filename sanitization

**v1.0.0** (2026-02-22)
- Initial release

---

**Security is a shared responsibility.** Keep your API keys secure, monitor usage, and report issues promptly.


path: scripts/productai_client.py
content: """
ProductAI API Client

Python client for interacting with the ProductAI.photo API.
"""

import json
import time
import re
from pathlib import Path
from typing import Optional, Union, List
from urllib.parse import urlparse
import requests


class ProductAIClient:
    """Client for ProductAI.photo API."""
    
    # Default request timeout in seconds
    DEFAULT_TIMEOUT = 30
    
    def __init__(self, api_key: str, api_endpoint: str = "https://api.productai.photo/v1"):
        """
        Initialize ProductAI client.
        
        Args:
            api_key: Your ProductAI API key
            api_endpoint: API base URL (default: https://api.productai.photo/v1)
        """
        self.[REDACTED_SECRET]
        self.api_endpoint = api_endpoint.rstrip('/')
        self.session = requests.Session()
        self.session.headers.update({
            'x-api-key': api_key,
            'Content-Type': 'application/json'
        })
    
    @staticmethod
    def validate_image_url(url: str) -> None:
        """
        Validate image URL for security.
        
        Args:
            url: URL to validate
        
        Raises:
            ValueError: If URL is invalid or insecure
        """
        parsed = urlparse(url)
        
        # Only allow HTTPS
        if parsed.scheme != 'https':
            raise ValueError(f"Only HTTPS URLs are allowed. Got: {parsed.scheme}://")
        
        # Block private/local network addresses (SSRF prevention)
        hostname = parsed.hostname
        if not hostname:
            raise ValueError("Invalid URL: missing hostname")
        
        # Block localhost, private IPs, link-local
        blocked_patterns = [
            r'^localhost$',
            r'^127\.',
            r'^10\.',
            r'^172\.(1[6-9]|2[0-9]|3[01])\.',
            r'^192\.168\.',
            r'^169\.254\.',
            r'^::1$',
            r'^fe80:',
        ]
        
        for pattern in blocked_patterns:
            if re.match(pattern, hostname, re.IGNORECASE):
                raise ValueError(f"Private/local addresses are not allowed: {hostname}")
    
    def generate(
        self,
        image_url: Union[str, List[str]],
        prompt: str,
        model: str = "nanobanana"
    ) -> dict:
        """
        Generate AI-powered product image.
        
        Args:
            image_url: URL(s) of source image(s). Can be:
                      - Single URL string for one image
                      - List of URLs for multiple reference images (seedream, nanobanana, nanobananapro support 2 images)
            prompt: Text description of desired output
            model: Model to use. Options:
                  - gpt-low (2 tokens)
                  - gpt-medium (3 tokens)
                  - gpt-high (8 tokens)
                  - kontext-pro (3 tokens)
                  - nanobanana (3 tokens)
                  - nanobananapro (8 tokens)
                  - seedream (3 tokens)
        
        Returns:
            dict with job_id and status
        
        Raises:
            requests.HTTPError: If API request fails
            ValueError: If image URL is invalid
        """
        # Validate image URLs
        urls_to_validate = [image_url] if isinstance(image_url, str) else image_url
        for url in urls_to_validate:
            self.validate_image_url(url)
        
        payload = {
            "model": model,
            "image_url": image_url,
            "prompt": prompt
        }
        
        response = self.session.post(
            f"{self.api_endpoint}/api/generate",
            json=payload,
            timeout=self.DEFAULT_TIMEOUT
        )
        response.raise_for_status()
        
        data = response.json()
        
        if data.get('status') != 'OK':
            raise Exception(f"API error: {data}")
        
        return data['data']
    
    def upscale(self, image_url: str) -> dict:
        """
        Upscale an image using AI (20 tokens).
        
        Args:
            image_url: URL of the image to upscale
        
        Returns:
            dict with job_id and status
        
        Raises:
            requests.HTTPError: If API request fails
            ValueError: If image URL is invalid
        """
        # Validate image URL
        self.validate_image_url(image_url)
        
        payload = {
            "image_url": image_url
        }
        
        response = self.session.post(
            f"{self.api_endpoint}/api/upscale",
            json=payload,
            timeout=self.DEFAULT_TIMEOUT
        )
        response.raise_for_status()
        
        data = response.json()
        
        if data.get('status') != 'OK':
            raise Exception(f"API error: {data}")
        
        return data['data']
    
    def get_job_status(self, job_id: int) -> dict:
        """
        Check the status of a generation job.
        
        Args:
            job_id: Job ID returned from generate() or upscale()
        
        Returns:
            dict with status, and image_url if completed
        
        Raises:
            requests.HTTPError: If API request fails
        """
        response = self.session.get(
            f"{self.api_endpoint}/api/job/{job_id}",
            timeout=self.DEFAULT_TIMEOUT
        )
        response.raise_for_status()
        
        data = response.json()
        
        if data.get('status') != 'OK':
            raise Exception(f"API error: {data}")
        
        return data['data']
    
    def wait_for_completion(
        self,
        job_id: int,
        poll_interval: int = 5,
        timeout: int = 300
    ) -> str:
        """
        Poll job status until completion.
        
        Args:
            job_id: Job ID to monitor
            poll_interval: Seconds to wait between checks (default: 5)
            timeout: Maximum seconds to wait (default: 300)
        
        Returns:
            str: URL of generated image
        
        Raises:
            TimeoutError: If job doesn't complete within timeout
            Exception: If job fails
        """
        start_time = time.time()
        
        while True:
            elapsed = time.time() - start_time
            if elapsed > timeout:
                raise TimeoutError(f"Job {job_id} did not complete within {timeout} seconds")
            
            status_data = self.get_job_status(job_id)
            status = status_data.get('status')
            
            if status == 'COMPLETED':
                return status_data['image_url']
            elif status == 'ERROR':
                raise Exception(f"Job {job_id} failed")
            elif status == 'RUNNING':
                time.sleep(poll_interval)
            else:
                raise Exception(f"Unknown status: {status}")
    
    def generate_and_wait(
        self,
        image_url: Union[str, List[str]],
        prompt: str,
        model: str = "nanobanana",
        poll_interval: int = 5,
        timeout: int = 300
    ) -> str:
        """
        Generate image and wait for completion (convenience method).
        
        Args:
            image_url: URL(s) of source image(s)
            prompt: Text description
            model: Model to use
            poll_interval: Seconds between status checks
            timeout: Maximum wait time
        
        Returns:
            str: URL of generated image
        """
        job = self.generate(image_url, prompt, model)
        job_id = job['id']
        
        print(f"Job {job_id} started. Waiting for completion...")
        return self.wait_for_completion(job_id, poll_interval, timeout)
    
    def upscale_and_wait(
        self,
        image_url: str,
        poll_interval: int = 5,
        timeout: int = 300
    ) -> str:
        """
        Upscale image and wait for completion (convenience method).
        
        Args:
            image_url: URL of image to upscale
            poll_interval: Seconds between status checks
            timeout: Maximum wait time
        
        Returns:
            str: URL of upscaled image
        """
        job = self.upscale(image_url)
        job_id = job['id']
        
        print(f"Upscale job {job_id} started. Waiting for completion...")
        return self.wait_for_completion(job_id, poll_interval, timeout)


def load_config(config_path: Optional[Path] = None) -> dict:
    """
    Load ProductAI configuration.
    
    Args:
        config_path: Path to config file (default: ~/.openclaw/workspace/productai/config.json)
    
    Returns:
        dict: Configuration data
    
    Raises:
        FileNotFoundError: If config file doesn't exist
    """
    if config_path is None:
        config_path = Path.home() / '.openclaw' / 'workspace' / 'productai' / 'config.json'
    
    if not config_path.exists():
        raise FileNotFoundError(
            f"Config file not found: {config_path}\n"
            f"Run scripts/setup.py to create it."
        )
    
    with open(config_path) as f:
        return json.load(f)


def create_client_from_config(config_path: Optional[Path] = None) -> ProductAIClient:
    """
    Create ProductAI client from configuration file.
    
    Args:
        config_path: Path to config file
    
    Returns:
        ProductAIClient instance
    """
    config = load_config(config_path)
    return ProductAIClient(
        [REDACTED_SECRET]'api_key'],
        api_endpoint=config.get('api_endpoint', 'https://api.productai.photo/v1')
    )


path: scripts/setup.py
content: #!/usr/bin/env python3
"""
ProductAI Setup Helper

Initialize ProductAI integration with API credentials.
"""

import json
import sys
from pathlib import Path

CONFIG_PATH = Path.home() / '.openclaw' / 'workspace' / 'productai' / 'config.json'

def setup():
    """Interactive setup for ProductAI integration."""
    print("ProductAI Setup")
    print("=" * 60)
    print()
    
    # Check if config already exists
    if CONFIG_PATH.exists():
        print(f"Configuration already exists at {CONFIG_PATH}")
        response = input("Overwrite? [y/N]: ").strip().lower()
        if response != 'y':
            print("Setup cancelled.")
            sys.exit(0)
        print()
    
    # Gather configuration
    print("Enter your ProductAI API credentials:")
    print("(Get your API key from ProductAI dashboard when available)")
    print()
    
    [REDACTED_SECRET]"API Key: ").strip()
    if not [REDACTED_SECRET]"Error: API key is required", file=sys.stderr)
        sys.exit(1)
    
    api_endpoint = input("API Endpoint [https://api.productai.photo/v1]: ").strip()
    if not api_endpoint:
        api_endpoint = "https://api.productai.photo/v1"
    
    default_model = input("Default Model [nano-banana-2]: ").strip()
    if not default_model:
        default_model = "nano-banana-2"
    
    default_resolution = input("Default Resolution [1024x1024]: ").strip()
    if not default_resolution:
        default_resolution = "1024x1024"
    
    plan = input("Your Plan (basic/standard/pro) [standard]: ").strip().lower()
    if not plan:
        plan = "standard"
    
    # Create config
    config = {
        "api_key": api_key,
        "api_endpoint": api_endpoint,
        "default_model": default_model,
        "default_resolution": default_resolution,
        "plan": plan
    }
    
    # Save config
    CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
    with open(CONFIG_PATH, 'w') as f:
        json.dump(config, f, indent=2)
    
    # Secure the config file
    CONFIG_PATH.chmod(0o600)
    
    print()
    print("=" * 60)
    print(f"✓ Configuration saved to {CONFIG_PATH}")
    print()
    print("You're ready to use ProductAI!")
    print()
    print("Next steps:")
    print("  1. Test the integration:")
    print("     scripts/generate_photo.py --image product.jpg --prompt 'white background' --output test.png")
    print()
    print("  2. Read the documentation:")
    print("     cat SKILL.md")
    print("     cat references/API.md")
    print()

def main():
    try:
        setup()
    except KeyboardInterrupt:
        print("\n\nSetup cancelled.")
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == '__main__':
    main()


path: scripts/upscale_image.py
content: #!/usr/bin/env python3
"""
ProductAI Image Upscaler

Upscale images using professional AI upscaling technology.
"""

import argparse
import sys
from pathlib import Path
import requests

from productai_client import create_client_from_config


def download_image(url: str, output_path: Path) -> None:
    """Download image from URL to local file."""
    print(f"Downloading image to {output_path}...")
    response = requests.get(url, stream=True, timeout=30)
    response.raise_for_status()
    
    with open(output_path, 'wb') as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)
    
    print(f"✓ Saved to {output_path}")


def main():
    parser = argparse.ArgumentParser(
        description='Upscale images using ProductAI (20 tokens per upscale)',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # Upscale an image
  %(prog)s --image https://example.com/product.jpg --output upscaled.png
  
  # Just get the job ID (don't wait)
  %(prog)s --image https://example.com/product.jpg --no-wait
  
  # Resume existing job
  %(prog)s --job-id 12345 --output upscaled.png
        '''
    )
    
    parser.add_argument(
        '--image',
        help='Image URL to upscale'
    )
    parser.add_argument(
        '--output',
        type=Path,
        help='Output file path (e.g., upscaled.png). If not specified, prints image URL only.'
    )
    parser.add_argument(
        '--no-wait',
        action='store_true',
        help="Don't wait for completion, just print job ID and exit"
    )
    parser.add_argument(
        '--poll-interval',
        type=int,
        default=5,
        help='Seconds to wait between status checks (default: 5)'
    )
    parser.add_argument(
        '--timeout',
        type=int,
        default=300,
        help='Maximum seconds to wait for completion (default: 300)'
    )
    parser.add_argument(
        '--job-id',
        type=int,
        help='Resume waiting for an existing job ID instead of creating new job'
    )
    parser.add_argument(
        '--config',
        type=Path,
        help='Path to config.json (default: ~/.openclaw/workspace/productai/config.json)'
    )
    
    args = parser.parse_args()
    
    # Validation
    if not args.job_id and not args.image:
        parser.error("Either --image or --job-id is required")
    
    try:
        # Create client
        client = create_client_from_config(args.config)
        
        # If resuming existing job
        if args.job_id:
            print(f"Checking status of upscale job {args.job_id}...")
            image_url = client.wait_for_completion(
                args.job_id,
                poll_interval=args.poll_interval,
                timeout=args.timeout
            )
        else:
            # Start upscale
            print(f"Upscaling image: {args.image}")
            print("Cost: 20 tokens (Magnific Precision Upscale)")
            print()
            
            job = client.upscale(args.image)
            
            job_id = job['id']
            print(f"✓ Upscale job created: {job_id}")
            print(f"  Status: {job['status']}")
            
            if args.no_wait:
                print()
                print("Not waiting for completion (--no-wait specified)")
                print(f"To check status later: {sys.argv[0]} --job-id {job_id} --output <file>")
                sys.exit(0)
            
            print()
            print("Waiting for completion...")
            image_url = client.wait_for_completion(
                job_id,
                poll_interval=args.poll_interval,
                timeout=args.timeout
            )
        
        print(f"\n✓ Upscale complete!")
        print(f"Image URL: {image_url}")
        
        # Download if output specified
        if args.output:
            download_image(image_url, args.output)
        
    except FileNotFoundError as e:
        print(f"Error: {e}", file=sys.stderr)
        print("\nRun scripts/setup.py first to configure API credentials.", file=sys.stderr)
        sys.exit(1)
    except KeyboardInterrupt:
        print("\n\nCancelled by user.", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()


path: scripts/generate_photo.py
content: #!/usr/bin/env python3
"""
ProductAI Photo Generator

Generate professional AI product photos from existing images.
"""

import argparse
import sys
from pathlib import Path
from typing import List
import requests

from productai_client import create_client_from_config, ProductAIClient


AVAILABLE_MODELS = {
    'gpt-low': 'GPT Low Quality (2 tokens)',
    'gpt-medium': 'GPT Medium Quality (3 tokens)',
    'gpt-high': 'GPT High Quality (8 tokens)',
    'kontext-pro': 'Kontext Pro (3 tokens)',
    'nanobanana': 'Nano Banana (3 tokens)',
    'nanobananapro': 'Nano Banana Pro (8 tokens)',
    'seedream': 'Seedream (3 tokens)'
}


def download_image(url: str, output_path: Path) -> None:
    """Download image from URL to local file."""
    print(f"Downloading image to {output_path}...")
    response = requests.get(url, stream=True, timeout=30)
    response.raise_for_status()
    
    with open(output_path, 'wb') as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)
    
    print(f"✓ Saved to {output_path}")


def main():
    parser = argparse.ArgumentParser(
        description='Generate AI-powered product photos',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # Generate with custom background
  %(prog)s --image product.jpg --prompt "modern living room" --output result.png
  
  # Use multiple reference images (nanobanana/seedream support 2 images)
  %(prog)s --image product1.jpg product2.jpg --prompt "Combine both products" --output result.png
  
  # High quality output
  %(prog)s --image product.jpg --prompt "studio background" --model nanobananapro --output hq.png
  
  # Just get the job ID (don't wait)
  %(prog)s --image product.jpg --prompt "white background" --no-wait

Available Models:
  gpt-low         GPT Low Quality (2 tokens)
  gpt-medium      GPT Medium Quality (3 tokens)
  gpt-high        GPT High Quality (8 tokens)
  kontext-pro     Kontext Pro (3 tokens)
  nanobanana      Nano Banana (3 tokens) - DEFAULT
  nanobananapro   Nano Banana Pro (8 tokens)
  seedream        Seedream (3 tokens)
        '''
    )
    
    parser.add_argument(
        '--image',
        required=True,
        nargs='+',
        help='Image URL(s) to process. Can be single URL or multiple URLs for models that support it.'
    )
    parser.add_argument(
        '--prompt',
        required=True,
        help='Text description of desired output (e.g., "modern living room background")'
    )
    parser.add_argument(
        '--model',
        default='nanobanana',
        choices=list(AVAILABLE_MODELS.keys()),
        help='Model to use for generation (default: nanobanana)'
    )
    parser.add_argument(
        '--output',
        type=Path,
        help='Output file path (e.g., result.png). If not specified, prints image URL only.'
    )
    parser.add_argument(
        '--no-wait',
        action='store_true',
        help="Don't wait for completion, just print job ID and exit"
    )
    parser.add_argument(
        '--poll-interval',
        type=int,
        default=5,
        help='Seconds to wait between status checks (default: 5)'
    )
    parser.add_argument(
        '--timeout',
        type=int,
        default=300,
        help='Maximum seconds to wait for completion (default: 300)'
    )
    parser.add_argument(
        '--job-id',
        type=int,
        help='Resume waiting for an existing job ID instead of creating new job'
    )
    parser.add_argument(
        '--config',
        type=Path,
        help='Path to config.json (default: ~/.openclaw/workspace/productai/config.json)'
    )
    
    args = parser.parse_args()
    
    try:
        # Create client
        client = create_client_from_config(args.config)
        
        # If resuming existing job
        if args.job_id:
            print(f"Checking status of job {args.job_id}...")
            image_url = client.wait_for_completion(
                args.job_id,
                poll_interval=args.poll_interval,
                timeout=args.timeout
            )
        else:
            # Prepare image URL(s)
            image_url_input = args.image[0] if len(args.image) == 1 else args.image
            
            # Validate multi-image support
            if isinstance(image_url_input, list) and len(image_url_input) > 1:
                multi_image_models = ['nanobanana', 'nanobananapro', 'seedream']
                if args.model not in multi_image_models:
                    print(f"Error: Model '{args.model}' does not support multiple images.", file=sys.stderr)
                    print(f"Use one of: {', '.join(multi_image_models)}", file=sys.stderr)
                    sys.exit(1)
                if len(image_url_input) > 2:
                    print("Error: Maximum 2 reference images supported", file=sys.stderr)
                    sys.exit(1)
            
            # Start generation
            print(f"Generating with model: {AVAILABLE_MODELS[args.model]}")
            if isinstance(image_url_input, list):
                print(f"Reference images: {len(image_url_input)}")
                for i, url in enumerate(image_url_input, 1):
                    print(f"  {i}. {url}")
            else:
                print(f"Image: {image_url_input}")
            print(f"Prompt: {args.prompt}")
            print()
            
            job = client.generate(
                image_url=image_url_input,
                prompt=args.prompt,
                model=args.model
            )
            
            job_id = job['id']
            print(f"✓ Job created: {job_id}")
            print(f"  Status: {job['status']}")
            
            if args.no_wait:
                print()
                print("Not waiting for completion (--no-wait specified)")
                print(f"To check status later: {sys.argv[0]} --job-id {job_id} --output <file>")
                sys.exit(0)
            
            print()
            print("Waiting for completion...")
            image_url = client.wait_for_completion(
                job_id,
                poll_interval=args.poll_interval,
                timeout=args.timeout
            )
        
        print(f"\n✓ Generation complete!")
        print(f"Image URL: {image_url}")
        
        # Download if output specified
        if args.output:
            download_image(image_url, args.output)
        
    except FileNotFoundError as e:
        print(f"Error: {e}", file=sys.stderr)
        print("\nRun scripts/setup.py first to configure API credentials.", file=sys.stderr)
        sys.exit(1)
    except KeyboardInterrupt:
        print("\n\nCancelled by user.", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()


path: scripts/batch_generate.py
content: #!/usr/bin/env python3
"""
ProductAI Batch Photo Generator

Process multiple product images with consistent styling.
"""

import argparse
import sys
import time
import re
from pathlib import Path
from typing import List
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests

from productai_client import create_client_from_config


# Rate limit: 15 requests per minute = 1 request per 4 seconds
RATE_LIMIT_SECONDS = 4.0


def sanitize_filename(filename: str) -> str:
    """
    Sanitize filename to prevent path traversal.
    
    Args:
        filename: Original filename
    
    Returns:
        Safe filename with only alphanumeric, dash, underscore, dot
    """
    # Remove path separators and parent references
    safe_name = filename.replace('/', '_').replace('\\', '_').replace('..', '_')
    
    # Keep only safe characters
    safe_name = re.sub(r'[^a-zA-Z0-9._-]', '_', safe_name)
    
    # Prevent hidden files
    if safe_name.startswith('.'):
        safe_name = '_' + safe_name[1:]
    
    return safe_name


def download_image(url: str, output_path: Path) -> None:
    """Download image from URL to local file."""
    response = requests.get(url, stream=True, timeout=30)
    response.raise_for_status()
    
    with open(output_path, 'wb') as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)


def process_single_image(
    image_url: str,
    prompt: str,
    output_dir: Path,
    model: str,
    client
) -> dict:
    """
    Process a single image with rate limiting.
    
    Args:
        image_url: URL of source image
        prompt: Generation prompt
        output_dir: Output directory
        model: Model to use
        client: ProductAI client instance
    
    Returns:
        dict with results
    """
    start_time = time.time()
    
    try:
        # Extract filename from URL for output naming
        url_path = image_url.split('/')[-1].split('?')[0]
        safe_name = sanitize_filename(url_path)
        base_name = Path(safe_name).stem
        
        print(f"Processing {safe_name}...")
        
        # Generate
        job = client.generate(image_url, prompt, model)
        job_id = job['id']
        
        # Wait for completion
        image_result_url = client.wait_for_completion(job_id)
        
        # Download result
        output_path = output_dir / f"{base_name}_generated.png"
        download_image(image_result_url, output_path)
        
        elapsed = time.time() - start_time
        print(f"✓ {safe_name} → {output_path.name} ({elapsed:.1f}s)")
        
        return {
            'input': image_url,
            'output': str(output_path),
            'status': 'success',
            'job_id': job_id
        }
        
    except Exception as e:
        print(f"✗ {image_url}: {e}", file=sys.stderr)
        return {
            'input': image_url,
            'output': None,
            'status': 'error',
            'error': str(e)
        }


def rate_limited_process(image_urls: List[str], prompt: str, output_dir: Path, model: str, client, max_workers: int = 3):
    """
    Process images with rate limiting (15 req/min).
    
    Args:
        image_urls: List of image URLs
        prompt: Generation prompt
        output_dir: Output directory
        model: Model to use
        client: ProductAI client
        max_workers: Max concurrent workers (default: 3 to stay under 15 req/min)
    """
    results = []
    last_request_time = 0
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = []
        
        for image_url in image_urls:
            # Rate limiting: ensure minimum time between requests
            elapsed = time.time() - last_request_time
            if elapsed < RATE_LIMIT_SECONDS:
                time.sleep(RATE_LIMIT_SECONDS - elapsed)
            
            future = executor.submit(
                process_single_image,
                image_url,
                prompt,
                output_dir,
                model,
                client
            )
            futures.append(future)
            last_request_time = time.time()
        
        # Collect results
        for future in as_completed(futures):
            results.append(future.result())
    
    return results


def main():
    parser = argparse.ArgumentParser(
        description='Batch process product images with ProductAI',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # Process multiple images with same prompt
  %(prog)s --images url1 url2 url3 --prompt "white background" --output-dir ./results
  
  # Use a different model
  %(prog)s --images url1 url2 --prompt "luxury setting" --model nanobananapro --output-dir ./results
  
  # From a text file (one URL per line)
  %(prog)s --input-file urls.txt --prompt "studio lighting" --output-dir ./results

Note: Rate limited to 15 requests/minute to comply with API limits.
        '''
    )
    
    input_group = parser.add_mutually_exclusive_group(required=True)
    input_group.add_argument(
        '--images',
        nargs='+',
        help='Image URLs to process'
    )
    input_group.add_argument(
        '--input-file',
        type=Path,
        help='Text file with image URLs (one per line)'
    )
    
    parser.add_argument(
        '--prompt',
        required=True,
        help='Generation prompt to apply to all images'
    )
    parser.add_argument(
        '--output-dir',
        type=Path,
        required=True,
        help='Output directory for generated images'
    )
    parser.add_argument(
        '--model',
        default='nanobanana',
        choices=['gpt-low', 'gpt-medium', 'gpt-high', 'kontext-pro', 'nanobanana', 'nanobananapro', 'seedream'],
        help='Model to use (default: nanobanana)'
    )
    parser.add_argument(
        '--max-workers',
        type=int,
        default=3,
        help='Maximum concurrent workers (default: 3, max recommended: 3 to stay under 15 req/min)'
    )
    parser.add_argument(
        '--config',
        type=Path,
        help='Path to config.json'
    )
    
    args = parser.parse_args()
    
    # Load image URLs
    if args.images:
        image_urls = args.images
    else:
        with open(args.input_file) as f:
            image_urls = [line.strip() for line in f if line.strip() and not line.startswith('#')]
    
    if not image_urls:
        print("Error: No image URLs provided", file=sys.stderr)
        sys.exit(1)
    
    # Create output directory
    args.output_dir.mkdir(parents=True, exist_ok=True)
    
    print(f"Batch processing {len(image_urls)} images")
    print(f"Model: {args.model}")
    print(f"Prompt: {args.prompt}")
    print(f"Output: {args.output_dir}")
    print(f"Rate limit: {RATE_LIMIT_SECONDS}s between requests")
    print()
    
    try:
        # Create client
        client = create_client_from_config(args.config)
        
        # Process with rate limiting
        results = rate_limited_process(
            image_urls,
            args.prompt,
            args.output_dir,
            args.model,
            client,
            max_workers=args.max_workers
        )
        
        # Summary
        success_count = sum(1 for r in results if r['status'] == 'success')
        error_count = len(results) - success_count
        
        print()
        print("=" * 60)
        print(f"Batch processing complete!")
        print(f"  ✓ Success: {success_count}")
        print(f"  ✗ Errors: {error_count}")
        print(f"  Output: {args.output_dir}")
        
    except KeyboardInterrupt:
        print("\n\nCancelled by user.", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()


path: references/API.md
content: # ProductAI API Reference

Complete documentation for ProductAI.photo public API endpoints.

## Base URL

```
https://api.productai.photo/v1
```

## Authentication

All API requests require authentication via the `x-api-key` header:

```bash
curl -H "x-[REDACTED_SECRET]" https://api.productai.photo/v1/api/generate
```

**Rate Limiting:** 15 requests per minute per IP address.

---

## `/api/generate` — Generate AI Product Photos

Generate an AI-edited image from one or more input images with a text prompt.

### Endpoint

```
POST /api/generate
```

### Request Body

| Field           | Type                   | Required | Description                                                                                                                                                                              |
| --------------- | ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `image_url`     | `string` or `string[]` | Yes      | URL(s) of input image(s). Maximum 2 images.                                                                                                                                              |
| `prompt`        | `string`               | Yes      | Text prompt describing the desired edit/generation.                                                                                                                                      |
| `model`         | `string`               | Yes      | One of: `gpt-low`, `gpt-medium`, `gpt-high`, `kontext-pro`, `kontext-max`, `nanobanana`, `nanobananapro`, `seedream`                                                                    |
| `output_format` | `string`               | No       | `"png"` (default) or `"jpg"` / `"jpeg"`                                                                                                                                                 |
| `aspect_ratio`  | `string`               | No       | `"SQUARE"`, `"LANDSCAPE"`, `"PORTRAIT"`. For `nanobanana`/`nanobananapro` also supports: `"LANDSCAPE_4_3"`, `"LANDSCAPE_5_4"`, `"SQUARE_1_1"`, `"PORTRAIT_4_5"`, `"PORTRAIT_3_4"`, or direct ratios like `"4:3"`, `"9:16"`, etc. |
| `resolution`    | `string`               | No       | For `nanobanana`/`nanobananapro` only: `"1K"`, `"2K"` (default), or `"4K"`                                                                                                               |

### Models & Pricing

| Model          | Credits | Engine        |
| -------------- | ------- | ------------- |
| `gpt-low`      | 2       | GPT (Lambda)  |
| `gpt-medium`   | 3       | GPT (Lambda)  |
| `gpt-high`     | 8       | GPT (Lambda)  |
| `kontext-pro`  | 2       | FAL           |
| `kontext-max`  | 3       | FAL           |
| `nanobanana`   | 3       | FAL           |
| `nanobananapro`| 8       | FAL           |
| `seedream`     | 3       | FAL           |

### Example Request (Single Image)

```bash
curl -X POST https://api.productai.photo/v1/api/generate \
  -H "x-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": "https://example.com/photo.jpg",
    "prompt": "Make the background a sunset beach",
    "model": "nanobananapro",
    "output_format": "png",
    "aspect_ratio": "LANDSCAPE",
    "resolution": "2K"
  }'
```

### Example Request (Multiple Images)

```bash
curl -X POST https://api.productai.photo/v1/api/generate \
  -H "x-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": ["https://example.com/product1.jpg", "https://example.com/product2.jpg"],
    "prompt": "Place the first product on top of the second product",
    "model": "nanobanana",
    "output_format": "png"
  }'
```

### Response

```json
{
  "status": "OK",
  "data": {
    "id": 12345,
    "status": "RUNNING",
    "prompt": "Make the background a sunset beach"
  }
}
```

**Note:** Generation is **asynchronous** — use the job ID to poll for completion via `/api/job/:jobId`.

### Status Codes

| Code | Meaning |
|------|---------|
| `200` | Job created successfully |
| `400` | Invalid request (check image URLs, prompt, model) |
| `401` | Invalid API key |
| `429` | Rate limit exceeded (max 15 requests/minute) |
| `402` | Insufficient tokens |
| `500` | Server error |

---

## `/api/job/:jobId` — Check Job Status

Check the status of a generation job and retrieve the result.

### Endpoint

```
GET /api/job/:jobId
```

### Authentication

Requires `x-api-key` header. The job must belong to the authenticated user.

### Example Request

```bash
curl -H "x-[REDACTED_SECRET]" \
  https://api.productai.photo/v1/api/job/12345
```

### Response (In Progress)

```json
{
  "status": "OK",
  "data": {
    "id": 12345,
    "status": "RUNNING",
    "prompt": "Make the background a sunset beach"
  }
}
```

### Response (Completed)

```json
{
  "status": "OK",
  "data": {
    "id": 12345,
    "status": "COMPLETED",
    "prompt": "Make the background a sunset beach",
    "image_url": "https://cdn.productai.photo/generated/result.png"
  }
}
```

### Job Status Values

| Status | Description |
|--------|-------------|
| `NOT_STARTED` | Job queued but not processing yet |
| `RUNNING` | Currently generating |
| `COMPLETED` | Finished — `image_url` available |
| `ERROR` | Failed — check logs or contact support |

**Note:** The `image_url` field only appears when status is `"COMPLETED"`.

---

## `/api/upscale` — Upscale Image

Upscale an image using Magnific Precision Upscale AI.

### Endpoint

```
POST /api/upscale
```

### Request Body

| Field       | Type     | Required | Description |
|-------------|----------|----------|-------------|
| `image_url` | `string` | Yes      | URL of the image to upscale |

### Example Request

```bash
curl -X POST https://api.productai.photo/v1/api/upscale \
  -H "x-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": "https://example.com/photo.jpg"
  }'
```

### Response

```json
{
  "status": "OK",
  "data": {
    "id": 67890,
    "status": "RUNNING"
  }
}
```

**Cost:** 20 tokens per upscale operation.

Poll `/api/job/:jobId` to retrieve the upscaled image URL when complete.

---

## `/api/generate-custom-model` — Generate with Custom Model

Generate images using a custom-trained model (e.g., brand-specific product models).

### Endpoint

```
POST /api/generate-custom-model
```

### Request Body

| Field       | Type     | Required | Description |
|-------------|----------|----------|-------------|
| `image_url` | `string` | Yes      | URL of input image |
| `prompt`    | `string` | Yes      | Generation prompt |
| `model`     | `string` | Yes      | Custom model identifier (e.g., `custom-owesa-avatars`) |

### Example Request

```bash
curl -X POST https://api.productai.photo/v1/api/generate-custom-model \
  -H "x-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "image_url": "https://example.com/photo.jpg",
    "prompt": "Transform into brand style",
    "model": "custom-owesa-avatars"
  }'
```

### Response

Same format as `/api/generate` — returns a job ID to poll for completion.

---

## `/api/key-check` — Validate API Key

Verify that your API key is valid and check remaining token balance.

### Endpoint

```
GET /api/key-check
```

### Example Request

```bash
curl -H "x-[REDACTED_SECRET]" \
  https://api.productai.photo/v1/api/key-check
```

### Response

```json
{
  "status": "OK",
  "data": {
    "valid": true,
    "tokens_remaining": 1250,
    "plan": "standard"
  }
}
```

---

## Error Handling

All error responses follow this format:

```json
{
  "status": "ERROR",
  "message": "Descriptive error message",
  "code": "ERROR_CODE"
}
```

### Common Error Codes

| Code | HTTP Status | Meaning |
|------|-------------|---------|
| `INVALID_API_KEY` | 401 | API key missing or invalid |
| `OUT_OF_TOKENS` | 402 | Insufficient tokens in account |
| `RATE_LIMIT_EXCEEDED` | 429 | More than 15 requests/minute |
| `INVALID_IMAGE_URL` | 400 | Image URL inaccessible or invalid format |
| `INVALID_MODEL` | 400 | Model name not recognized |
| `JOB_NOT_FOUND` | 404 | Job ID doesn't exist or belongs to another user |

---

## Best Practices

### Asynchronous Workflow

1. **Submit job** via `/api/generate` or `/api/upscale`
2. **Store job ID** from response
3. **Poll `/api/job/:jobId`** every 3-5 seconds until status is `COMPLETED` or `ERROR`
4. **Download result** from `image_url` field

### Rate Limiting

- Maximum **15 requests per minute** per IP
- Use exponential backoff when hitting 429 errors
- For batch operations, space out requests (4-5 seconds apart)

### Image Requirements

- **Formats:** PNG, JPG, JPEG, WebP
- **Max size:** 10 MB
- **Max images per request:** 2 (only with `nanobanana`, `nanobananapro`, `seedream`)
- **URLs must be publicly accessible** (no authentication required)

### Token Management

- Tokens are deducted when jobs **start** (not on completion)
- Check balance via `/api/key-check` before large batches
- Failed jobs still consume tokens

---

## SDK & Integration Examples

For Python integration examples and helper scripts, see:
- `scripts/generate_photo.py` — Complete generation workflow
- `scripts/upscale_image.py` — Upscaling workflow
- `references/INTEGRATION_GUIDE.md` — Advanced patterns

---

## Support

- **API Issues:** [REDACTED_SECRET]
- **Documentation:** https://www.productai.photo/docs
- **Status Page:** https://status.productai.photo


path: references/INTEGRATION_GUIDE.md
content: # ProductAI Integration Guide

Complete guide for integrating ProductAI into your applications, workflows, and AI agents.

## Quick Start (5 minutes)

### 1. Install the Skill

```bash
# If skill is packaged as .skill file
openclaw skills install productai.skill

# Or clone directly
git clone https://github.com/your-org/productai-skill ~/.openclaw/workspace/productai
```

### 2. Configure API Credentials

```bash
cd ~/.openclaw/workspace/productai
python3 scripts/setup.py
```

Or manually create `config.json`:

```json
{
  "api_key": "your-api-key-here",
  "api_endpoint": "https://api.productai.photo/v1",
  "default_model": "nano-banana-2",
  "default_resolution": "1024x1024",
  "plan": "standard"
}
```

### 3. Test It

```bash
# Generate your first photo
scripts/generate_photo.py \
  --image product.jpg \
  --prompt "white studio background with soft shadows" \
  --output result.png
```

**Done!** You now have ProductAI integrated.

## Integration Patterns

### Pattern 1: Command-Line Scripts

Best for: Manual workflows, batch jobs, cron tasks

```bash
# Single photo generation
scripts/generate_photo.py --image product.jpg --prompt "modern living room" --output result.png

# Background replacement
scripts/generate_photo.py --image product.jpg --background-replace --output clean.png

# Batch processing
scripts/batch_generate.py \
  --input-dir ./products \
  --output-dir ./processed \
  --template "white background with subtle shadows"
```

### Pattern 2: Python Library

Best for: Python applications, Jupyter notebooks, automation scripts

```python
from productai_client import create_client

# Initialize client
client = create_client()

# Generate photo
result = client.generate(
    image='product.jpg',
    prompt='modern living room with natural lighting'
)

# Download result
client.download_result(result['image_url'], 'output.png')

print(f"Credits used: {result['credits_used']}")
print(f"Processing time: {result['processing_time_ms']}ms")
```

### Pattern 3: AI Agent Integration

Best for: OpenClaw agents, LangChain, AutoGPT, other AI systems

**OpenClaw Integration:**

The skill is auto-loaded when OpenClaw detects product photo tasks. Just ask:

> "Generate a professional product photo of this bottle in a modern kitchen setting"

**Custom Agent Integration:**

```python
import subprocess
import json

def generate_product_photo(image_path: str, prompt: str) -> dict:
    """Call ProductAI from any AI agent."""
    result = subprocess.run(
        [
            'python3',
            '/path/to/productai/scripts/generate_photo.py',
            '--image', image_path,
            '--prompt', prompt,
            '--output', 'result.png'
        ],
        capture_output=True,
        text=True
    )
    
    return json.loads(result.stdout)

# Use in your agent
result = generate_product_photo('product.jpg', 'white background')
print(f"Generated: {result['image_url']}")
```

### Pattern 4: REST API Wrapper

Best for: Microservices, web apps, team access

```python
from flask import Flask, request, jsonify
from productai_client import create_client

app = Flask(__name__)
client = create_client()

@app.route('/api/generate', methods=['POST'])
def generate():
    data = request.json
    
    result = client.generate(
        image=data['image'],
        prompt=data['prompt'],
        model=data.get('model', 'nano-banana-2')
    )
    
    return jsonify(result)

@app.route('/api/background-replace', methods=['POST'])
def background_replace():
    data = request.json
    
    result = client.background_replace(
        image=data['image'],
        background_type=data.get('background_type', 'white')
    )
    
    return jsonify(result)

if __name__ == '__main__':
    app.run(port=5000)
```

## Real-World Use Cases

### E-Commerce Product Photos

**Challenge:** Need 1000 product photos with consistent white backgrounds for Shopify store.

**Solution:**

```bash
# 1. Organize products
mkdir -p products/raw products/processed

# 2. Batch process with white background
scripts/batch_generate.py \
  --input-dir products/raw \
  --output-dir products/processed \
  --template "clean white background with soft drop shadow" \
  --max-workers 5

# 3. Upload to Shopify (use Shopify API or bulk upload)
```

**Result:** Professional product photos at 10x lower cost than photographer.

### Marketing Campaign Visuals

**Challenge:** Create lifestyle product shots for Instagram campaign.

**Solution:**

```python
from productai_client import create_client

client = create_client()

scenes = [
    "product on rustic wooden table with morning coffee",
    "product held in hands outdoors with natural lighting",
    "product on modern desk with laptop and plant",
    "product on kitchen counter with fresh ingredients"
]

for i, scene in enumerate(scenes):
    result = client.generate(
        image='product.jpg',
        prompt=scene,
        model='flux-kontext',
        resolution='1024x1024'
    )
    
    client.download_result(
        result['image_url'],
        f'campaign/instagram_{i+1}.png'
    )
    
    print(f"✓ Generated scene {i+1}: {scene}")
```

**Result:** 4 unique lifestyle shots ready for social media in minutes.

### Product Catalog Updates

**Challenge:** Seasonal catalog needs all products re-shot with holiday theme.

**Solution:**

```bash
# Generate holiday-themed versions
scripts/batch_generate.py \
  --input-dir catalog/products \
  --output-dir catalog/holiday \
  --template "festive holiday setting with warm lighting and decorations" \
  --model nano-banana-2-pro

# Or summer theme
scripts/batch_generate.py \
  --input-dir catalog/products \
  --output-dir catalog/summer \
  --template "bright summer outdoor setting with natural sunlight"
```

**Result:** Entire catalog refreshed for new season without re-shooting products.

### Automated Listing Generator

**Challenge:** Automatically create marketplace listings with professional photos.

**Solution:**

```python
from productai_client import create_client
import csv

client = create_client()

# Read product inventory
with open('inventory.csv') as f:
    products = csv.DictReader(f)
    
    for product in products:
        # Generate professional photo
        result = client.generate(
            image=product['raw_photo_url'],
            prompt='clean white background for e-commerce',
            model='nano-banana-2'
        )
        
        # Download and save
        output_path = f"listings/{product['sku']}.png"
        client.download_result(result['image_url'], output_path)
        
        # Create listing (pseudo-code)
        create_marketplace_listing(
            title=product['name'],
            image=output_path,
            price=product['price']
        )
        
        print(f"✓ Listed {product['name']}")
```

**Result:** Fully automated listing creation with professional photos.

## Advanced Topics

### Custom Webhooks

Get notified when batch jobs complete:

```python
from flask import Flask, request
from productai_client import create_client

app = Flask(__name__)
client = create_client()

@app.route('/webhook/productai', methods=['POST'])
def productai_webhook():
    event = request.json
    
    if event['event'] == 'job.completed':
        job_id = event['job_id']
        result_url = event['result']['image_url']
        
        # Process completed job
        client.download_result(result_url, f'results/{job_id}.png')
        
        # Trigger next step in workflow
        process_completed_image(job_id)
    
    return '', 200

# Submit async batch
result = client.batch_generate(
    images=['product1.jpg', 'product2.jpg'],
    template='white background',
    webhook_url='https://your-server.com/webhook/productai'
)

batch_id = result['batch_id']
print(f"Batch submitted: {batch_id}")
```

### Credit Management

Monitor and optimize credit usage:

```python
from productai_client import create_client

client = create_client()

# Track credits
total_credits = 0
results = []

for image in product_images:
    result = client.generate(image=image, prompt='white background')
    
    credits_used = result['credits_used']
    total_credits += credits_used
    results.append(result)
    
    print(f"Processed {image}: {credits_used} credits")

print(f"Total credits used: {total_credits}")
print(f"Average per image: {total_credits / len(product_images):.2f}")
```

**Optimization tips:**
- Use lower resolution for thumbnails
- Batch similar products to save credits
- Cache generated images
- Use Basic plan for simple backgrounds, Pro for high-end

### Quality Control

Implement QA checks on generated images:

```python
from PIL import Image
import requests
from productai_client import create_client

client = create_client()

def check_image_quality(image_url: str) -> dict:
    """Basic quality checks on generated image."""
    response = requests.get(image_url)
    img = Image.open(BytesIO(response.content))
    
    width, height = img.size
    aspect_ratio = width / height
    
    # Check resolution
    if width < 1000 or height < 1000:
        return {'status': 'low_resolution', 'width': width, 'height': height}
    
    # Check aspect ratio (for square products)
    if not (0.9 < aspect_ratio < 1.1):
        return {'status': 'wrong_aspect_ratio', 'ratio': aspect_ratio}
    
    return {'status': 'ok'}

# Generate with QA
result = client.generate(
    image='product.jpg',
    prompt='white background',
    resolution='1024x1024'
)

qa_result = check_image_quality(result['image_url'])
if qa_result['status'] == 'ok':
    client.download_result(result['image_url'], 'approved.png')
else:
    print(f"Quality issue: {qa_result}")
    # Retry or flag for manual review
```

### Template Management

Create reusable templates for consistent brand styling:

```python
# templates.py
TEMPLATES = {
    'ecommerce_white': 'clean white background with soft drop shadow',
    'ecommerce_grey': 'light grey gradient background',
    'lifestyle_home': 'modern home interior with natural lighting',
    'lifestyle_outdoor': 'outdoor setting with natural environment',
    'hands_holding': 'product held in hands with professional lighting',
    'table_flat_lay': 'flat lay on white marble table with props'
}

# Use templates
from productai_client import create_client

client = create_client()

result = client.generate(
    image='product.jpg',
    prompt=TEMPLATES['lifestyle_home'],
    model='flux-kontext'
)
```

### Error Handling & Retries

Robust error handling for production:

```python
import time
from productai_client import create_client, ProductAIClient
import requests

def generate_with_retry(
    client: ProductAIClient,
    image: str,
    prompt: str,
    max_retries: int = 3
) -> dict:
    """Generate with exponential backoff retry."""
    
    for attempt in range(max_retries):
        try:
            result = client.generate(image=image, prompt=prompt)
            return result
            
        except requests.exceptions.HTTPError as e:
            if e.response.status_code == 429:
                # Rate limited
                wait = 2 ** attempt
                print(f"Rate limited. Waiting {wait}s...")
                time.sleep(wait)
                continue
            
            elif e.response.status_code == 402:
                # Insufficient credits
                raise Exception("Out of credits")
            
            elif attempt == max_retries - 1:
                # Last attempt, give up
                raise
            
            else:
                # Other error, retry
                wait = 2 ** attempt
                time.sleep(wait)
                continue
        
        except requests.exceptions.RequestException as e:
            # Network error
            if attempt == max_retries - 1:
                raise
            
            wait = 2 ** attempt
            print(f"Network error. Retrying in {wait}s...")
            time.sleep(wait)
            continue
    
    raise Exception("Max retries exceeded")

# Use it
client = create_client()
result = generate_with_retry(client, 'product.jpg', 'white background')
```

## Performance Optimization

### Parallel Processing

```python
from concurrent.futures import ThreadPoolExecutor, as_completed
from productai_client import create_client

client = create_client()

def process_product(image_path: str, template: str) -> dict:
    result = client.generate(image=image_path, prompt=template)
    return {'input': image_path, 'result': result}

# Process 100 products in parallel (respect rate limits)
with ThreadPoolExecutor(max_workers=5) as executor:
    futures = {
        executor.submit(process_product, img, 'white background'): img
        for img in product_images
    }
    
    for future in as_completed(futures):
        result = future.result()
        print(f"✓ Processed {result['input']}")
```

### Caching Strategy

```python
import hashlib
import json
from pathlib import Path

CACHE_DIR = Path('cache/productai')
CACHE_DIR.mkdir(parents=True, exist_ok=True)

def get_cache_key(image: str, prompt: str, **kwargs) -> str:
    """Generate cache key from parameters."""
    params = {'image': image, 'prompt': prompt, **kwargs}
    key_string = json.dumps(params, sort_keys=True)
    return hashlib.md5(key_string.encode()).hexdigest()

def cached_generate(client, image: str, prompt: str, **kwargs) -> dict:
    """Generate with caching."""
    cache_key = get_cache_key(image, prompt, **kwargs)
    cache_file = CACHE_DIR / f"{cache_key}.json"
    
    # Check cache
    if cache_file.exists():
        print(f"Cache hit: {cache_key}")
        return json.loads(cache_file.read_text())
    
    # Generate
    result = client.generate(image=image, prompt=prompt, **kwargs)
    
    # Cache result
    cache_file.write_text(json.dumps(result, indent=2))
    
    return result
```

## Troubleshooting

### Common Issues

**Issue:** `Configuration file not found`

**Solution:**
```bash
cd ~/.openclaw/workspace/productai
python3 scripts/setup.py
```

---

**Issue:** `API Error 401: Unauthorized`

**Solution:** Check your API key in `config.json` is correct.

---

**Issue:** `API Error 402: Insufficient credits`

**Solution:** Your plan has run out of credits. Upgrade or wait for monthly reset.

---

**Issue:** `API Error 429: Rate limit exceeded`

**Solution:** Reduce `max_workers` in batch jobs or add delays between requests.

---

**Issue:** Generated images don't match prompt

**Solution:** 
- Make prompts more specific
- Try different models (flux-kontext for placement, nano-banana-2-pro for quality)
- Adjust scale parameter

---

**Issue:** Batch processing too slow

**Solution:**
- Increase `max_workers` (respect rate limits)
- Use async batch API with webhooks instead of polling
- Parallelize with multiple API keys if allowed

## Support & Resources

- **Documentation:** `references/API.md`
- **Website:** https://www.productai.photo
- **Email:** [REDACTED_SECRET]
- **Discord:** (when available)

## Contributing

To contribute improvements to this integration:

1. Fork the skill repository
2. Make your changes
3. Test thoroughly
4. Submit pull request with description

**Areas for contribution:**
- Additional language bindings (JS, Ruby, Go)
- Enhanced error handling
- Performance optimizations
- New templates and presets
- Integration examples

## License

This integration skill is provided as-is for use with ProductAI service.
Check ProductAI's terms of service for API usage terms.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)