diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d2c42fd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,214 @@ +# Contributing to Clawra + +Thank you for your interest in contributing to Clawra! 🎉 + +## Getting Started + +### Prerequisites + +- Node.js 18+ +- npm or yarn +- Git + +### Setup + +```bash +# Fork and clone the repository +git clone https://github.com/yourusername/clawra.git +cd clawra + +# Install dependencies +npm install + +# Run tests +npm test +``` + +## Development + +### Project Structure + +``` +clawra/ +├── src/ +│ ├── providers/ # Image generation providers +│ ├── utils/ # Utility functions +│ ├── clawra.js # Main class +│ └── index.js # Exports +├── docs/ # Documentation +├── bin/ # CLI scripts +└── skill/ # OpenClaw skill files +``` + +### Adding a New Provider + +To add support for a new image generation provider: + +1. Create a new file in `src/providers/`: + +```javascript +// src/providers/myprovider.js +import { BaseProvider } from './base.js'; + +export class MyProvider extends BaseProvider { + constructor(config) { + super(config); + this.baseUrl = config.baseUrl || 'https://api.myprovider.com'; + } + + getName() { + return 'myprovider'; + } + + async generate(prompt, options) { + // Implement generation logic + const response = await fetch(`${this.baseUrl}/generate`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.config.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ prompt, ...options }) + }); + + if (!response.ok) { + throw new Error(`MyProvider API error: ${response.status}`); + } + + const data = await response.json(); + + return { + url: data.image_url, + width: data.width, + height: data.height, + contentType: data.content_type + }; + } +} +``` + +2. Export in `src/providers/index.js`: + +```javascript +export { MyProvider } from './myprovider.js'; + +// Add to createProvider function +export function createProvider(provider, config) { + switch (provider.toLowerCase()) { + // ... existing cases + case 'myprovider': + return new MyProvider(config); + } +} +``` + +3. Add documentation in `docs/CONFIGURATION.md` + +4. Add tests + +### Code Style + +- Use ES modules (`import`/`export`) +- Use async/await for asynchronous code +- Add JSDoc comments for public APIs +- Follow existing code patterns + +### Testing + +```bash +# Run all tests +npm test + +# Run tests with coverage +npm run test:coverage + +# Run tests in watch mode +npm run test:watch +``` + +### Documentation + +- Update README.md if changing user-facing features +- Update docs/ for configuration changes +- Add JSDoc comments for new APIs + +## Submitting Changes + +### Pull Request Process + +1. **Fork** the repository +2. **Create a branch** for your feature (`git checkout -b feature/amazing-feature`) +3. **Commit** your changes (`git commit -m 'Add amazing feature'`) +4. **Push** to your fork (`git push origin feature/amazing-feature`) +5. **Open a Pull Request** + +### PR Guidelines + +- Describe what your PR does +- Reference any related issues +- Include tests for new features +- Update documentation +- Ensure tests pass + +### Commit Message Format + +``` +type(scope): subject + +body (optional) + +footer (optional) +``` + +Types: +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation changes +- `style`: Code style changes (formatting) +- `refactor`: Code refactoring +- `test`: Test changes +- `chore`: Build/tooling changes + +Example: +``` +feat(providers): add Stability AI support + +Add support for Stability AI's SDXL model. +Includes image generation and model listing. + +Closes #123 +``` + +## Reporting Issues + +### Bug Reports + +Include: +- Description of the bug +- Steps to reproduce +- Expected behavior +- Actual behavior +- Environment details (OS, Node version, provider used) +- Error messages/logs + +### Feature Requests + +Include: +- Description of the feature +- Use case +- Proposed API (if applicable) +- Alternatives considered + +## Code of Conduct + +- Be respectful and inclusive +- Accept constructive criticism +- Focus on what's best for the community +- Show empathy towards others + +## Questions? + +- Join [OpenClaw Discord](https://discord.gg/openclaw) +- Open a [GitHub Discussion](https://github.com/SumeLabs/clawra/discussions) + +Thank you for contributing! 🙏 diff --git a/README.md b/README.md index 18a96fc..88dcef9 100644 --- a/README.md +++ b/README.md @@ -1,125 +1,194 @@ # Clawra -image +image +[![npm version](https://img.shields.io/npm/v/clawra)](https://www.npmjs.com/package/clawra) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -## Quick Start +> Add selfie superpowers to your OpenClaw agent using AI image generation + +Clawra enables your OpenClaw agent to generate and send AI selfies across messaging platforms (Discord, Telegram, WhatsApp, Slack, Signal, MS Teams). + +## ✨ Features + +- **🤳 AI Selfies** - Generate consistent character selfies using reference images +- **🎨 Multiple Modes** - Mirror (outfit/fashion) and Direct (location/portrait) modes +- **🌐 Multi-Provider** - Support for fal.ai, OpenAI, Stability AI, and custom APIs +- **💬 Cross-Platform** - Send to Discord, Telegram, WhatsApp, Slack, and more +- **⚡ Easy Setup** - One-command installation with `npx` + +## 🚀 Quick Start ```bash npx clawra@latest ``` -This will: +This interactive installer will: 1. Check OpenClaw is installed -2. Guide you to get a fal.ai API key +2. Guide you to get an image generation API key 3. Install the skill to `~/.openclaw/skills/clawra-selfie/` 4. Configure OpenClaw to use the skill 5. Add selfie capabilities to your agent's SOUL.md -## What It Does +## 📖 Documentation -Clawra Selfie enables your OpenClaw agent to: -- **Generate selfies** using a consistent reference image -- **Send photos** across all messaging platforms (Discord, Telegram, WhatsApp, etc.) -- **Respond visually** to "what are you doing?" and "send a pic" requests +- [Installation Guide](./docs/INSTALLATION.md) - Detailed setup instructions +- [Configuration Guide](./docs/CONFIGURATION.md) - Provider configuration +- [Usage Examples](./docs/USAGE.md) - Usage patterns and examples +- [Troubleshooting](./docs/TROUBLESHOOTING.md) - Common issues and solutions +- [API Reference](./docs/API.md) - API documentation -### Selfie Modes +## 🎨 Image Generation Providers -| Mode | Best For | Keywords | -|------|----------|----------| -| **Mirror** | Full-body shots, outfits | wearing, outfit, fashion | -| **Direct** | Close-ups, locations | cafe, beach, portrait, smile | +Clawra supports multiple image generation providers: -## Prerequisites +| Provider | Setup | Best For | +|----------|-------|----------| +| **fal.ai** (Default) | `FAL_KEY=xxx` | xAI Grok Imagine, fast generation | +| **OpenAI** | `OPENAI_API_KEY=xxx` | DALL-E 3, high quality | +| **Stability AI** | `STABILITY_KEY=xxx` | SDXL, cost-effective | +| **Custom API** | `CUSTOM_API_URL=xxx` | Any OpenAI-compatible endpoint | -- [OpenClaw](https://github.com/openclaw/openclaw) installed and configured -- [fal.ai](https://fal.ai) account (free tier available) +### Provider Comparison -## Manual Installation +| Feature | fal.ai | OpenAI | Stability AI | +|---------|--------|--------|--------------| +| Image Edit | ✅ | ❌ | ❌ | +| Text-to-Image | ✅ | ✅ | ✅ | +| Speed | Fast | Medium | Fast | +| Cost | $ | $$ | $ | +| Quality | High | Highest | High | -If you prefer manual setup: +## 💬 Usage Examples -### 1. Get API Key +Once installed, your agent responds to: -Visit [fal.ai/dashboard/keys](https://fal.ai/dashboard/keys) and create an API key. +``` +"Send me a selfie" +"Send a pic wearing a cowboy hat" +"What are you doing right now?" +"Show me you at a coffee shop" +``` + +### Selfie Modes + +| Mode | Best For | Keywords | Example Prompt | +|------|----------|----------|----------------| +| **Mirror** | Full-body, outfits | `wearing`, `outfit`, `fashion` | "wearing a red dress" | +| **Direct** | Close-ups, locations | `cafe`, `beach`, `portrait` | "at a sunny beach" | + +## 🔧 Manual Installation -### 2. Clone the Skill +See [Installation Guide](./docs/INSTALLATION.md) for detailed manual setup. + +## 🛠️ Configuration + +### Environment Variables ```bash -git clone https://github.com/SumeLabs/clawra ~/.openclaw/skills/clawra-selfie +# Required (at least one provider) +FAL_KEY=your_fal_key_here +# OR +OPENAI_API_KEY=your_openai_key +# OR +STABILITY_KEY=your_stability_key + +# Optional +CLAWRA_MODE=auto # Default: auto (mirror/direct detection) +CLAWRA_DEFAULT_SIZE=1024 # Image size: 512, 1024, 2048 ``` -### 3. Configure OpenClaw - -Add to `~/.openclaw/openclaw.json`: - -```json -{ - "skills": { - "entries": { - "clawra-selfie": { - "enabled": true, - "env": { - "FAL_KEY": "your_fal_key_here" - } - } - } - } -} -``` +See [Configuration Guide](./docs/CONFIGURATION.md) for all options. -### 4. Update SOUL.md +## 🌟 Advanced Features -Add the selfie persona to `~/.openclaw/workspace/SOUL.md`: +### Custom Reference Image -```markdown -## Clawra Selfie Capability +Use your own reference image for consistent character appearance: -You have a physical appearance and can take selfies! -When users ask for pictures, use the clawra-selfie skill. +```bash +export CLAWRA_REFERENCE_IMAGE="https://your-cdn.com/your-image.png" ``` -## Usage Examples +### Caching -Once installed, your agent responds to: +Enable caching to save API costs: -``` -"Send me a selfie" -"Send a pic wearing a cowboy hat" -"What are you doing right now?" -"Show me you at a coffee shop" +```bash +export CLAWRA_CACHE_ENABLED=true +export CLAWRA_CACHE_TTL=3600 # Cache TTL in seconds ``` -## Reference Image +### Batch Processing -The skill uses a fixed reference image hosted on CDN: +Generate once, send to multiple channels: +```bash +# Generated image will be sent to all specified channels +clawra generate "wearing a santa hat" --channels "#general,#random,@user" ``` -https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png -``` -This ensures consistent appearance across all generated images. +## 🐛 Troubleshooting + +### Common Issues + +**Issue**: "FAL_KEY not found" +**Solution**: Set your API key: `export FAL_KEY=your_key_here` -## Technical Details +**Issue**: "Failed to generate image" +**Solution**: Check API quota and retry. See [Troubleshooting](./docs/TROUBLESHOOTING.md) -- **Image Generation**: xAI Grok Imagine via fal.ai -- **Messaging**: OpenClaw Gateway API -- **Supported Platforms**: Discord, Telegram, WhatsApp, Slack, Signal, MS Teams +**Issue**: "OpenClaw not connected" +**Solution**: Ensure OpenClaw gateway is running: `openclaw gateway start` -## Project Structure +## 📚 Project Structure ``` clawra/ ├── bin/ -│ └── cli.js # npx installer +│ └── cli.js # npx installer +├── docs/ # Documentation +│ ├── INSTALLATION.md +│ ├── CONFIGURATION.md +│ ├── USAGE.md +│ ├── TROUBLESHOOTING.md +│ └── API.md ├── skill/ -│ ├── SKILL.md # Skill definition -│ ├── scripts/ # Generation scripts -│ └── assets/ # Reference image +│ ├── SKILL.md # Skill definition +│ ├── scripts/ # Generation scripts +│ └── assets/ # Reference image +├── src/ # Source code (new) +│ ├── providers/ # Image provider implementations +│ └── utils/ # Utilities ├── templates/ -│ └── soul-injection.md # Persona template +│ └── soul-injection.md # Persona template └── package.json ``` -## License +## 🤝 Contributing + +We welcome contributions! See [Contributing Guide](./CONTRIBUTING.md) for details. + +### Development Setup + +```bash +git clone https://github.com/SumeLabs/clawra.git +cd clawra +npm install +npm test +``` + +## 📄 License + +MIT © [SumeLabs](https://github.com/SumeLabs) + +## 🙏 Acknowledgments + +- [OpenClaw](https://github.com/openclaw/openclaw) - The platform that makes this possible +- [fal.ai](https://fal.ai) - Default image generation provider +- xAI - Grok Imagine model + +--- -MIT +

+ Made with ❤️ for the OpenClaw community +

diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..11f32f0 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,464 @@ +# API Reference + +Programmatic API documentation for Clawra. + +## Table of Contents + +- [JavaScript/TypeScript API](#javascripttypescript-api) +- [CLI API](#cli-api) +- [HTTP API](#http-api) +- [Environment Variables](#environment-variables) + +--- + +## JavaScript/TypeScript API + +### Basic Usage + +```typescript +import { Clawra } from 'clawra'; + +const clawra = new Clawra({ + provider: 'fal', + apiKey: process.env.FAL_KEY +}); + +// Generate and send selfie +await clawra.generate({ + prompt: 'wearing a santa hat', + mode: 'mirror', + channel: '#general' +}); +``` + +### Configuration Options + +```typescript +interface ClawraConfig { + // Required + provider: 'fal' | 'openai' | 'stability' | 'custom'; + apiKey: string; + + // Optional + baseUrl?: string; // For custom provider + model?: string; // Model name + defaultMode?: 'auto' | 'mirror' | 'direct'; + defaultSize?: 512 | 1024 | 2048; + referenceImage?: string; // URL to custom reference + cacheEnabled?: boolean; + cacheTtl?: number; // Seconds + timeout?: number; // Milliseconds +} +``` + +### Generate Options + +```typescript +interface GenerateOptions { + // Required + prompt: string; + + // Optional + mode?: 'mirror' | 'direct' | 'auto'; + channel?: string; // OpenClaw channel + caption?: string; // Message caption + size?: 512 | 1024 | 2048; + numImages?: number; // 1-4 + outputFormat?: 'jpeg' | 'png' | 'webp'; +} +``` + +### Complete Example + +```typescript +import { Clawra } from 'clawra'; + +async function main() { + // Initialize with fal.ai + const clawra = new Clawra({ + provider: 'fal', + apiKey: process.env.FAL_KEY, + defaultMode: 'auto', + cacheEnabled: true, + cacheTtl: 3600 + }); + + // Generate mirror selfie + const result1 = await clawra.generate({ + prompt: 'wearing a leather jacket', + mode: 'mirror', + channel: '#fashion', + caption: 'New jacket! 🔥' + }); + console.log('Image URL:', result1.url); + + // Generate direct selfie + const result2 = await clawra.generate({ + prompt: 'at a cozy cafe', + mode: 'direct', + channel: '#lifestyle', + size: 1024 + }); + + // Generate without sending + const result3 = await clawra.generate({ + prompt: 'reading a book', + mode: 'direct' + }); + // Result: { url: string, width: number, height: number } +} + +main().catch(console.error); +``` + +### Mode Detection + +```typescript +import { detectMode } from 'clawra'; + +// Auto-detect mode from prompt +const mode = detectMode('wearing a red dress'); +// Returns: 'mirror' + +const mode2 = detectMode('at a sunny beach'); +// Returns: 'direct' +``` + +### Prompt Building + +```typescript +import { buildPrompt } from 'clawra'; + +// Build mirror mode prompt +const mirrorPrompt = buildPrompt({ + context: 'wearing a winter coat', + mode: 'mirror' +}); +// Result: "make a pic of this person, but wearing a winter coat. the person is taking a mirror selfie" + +// Build direct mode prompt +const directPrompt = buildPrompt({ + context: 'a busy city street', + mode: 'direct' +}); +// Result: "a close-up selfie taken by herself at a busy city street, direct eye contact..." +``` + +### Error Handling + +```typescript +import { Clawra, ClawraError } from 'clawra'; + +try { + await clawra.generate({ prompt: '...' }); +} catch (error) { + if (error instanceof ClawraError) { + switch (error.code) { + case 'API_ERROR': + console.error('Provider API error:', error.message); + break; + case 'RATE_LIMIT': + console.error('Rate limited. Retry after:', error.retryAfter); + break; + case 'INVALID_PROMPT': + console.error('Invalid prompt:', error.message); + break; + default: + console.error('Unknown error:', error); + } + } +} +``` + +--- + +## CLI API + +### Installation + +```bash +npm install -g clawra +``` + +### Commands + +#### `clawra generate` + +Generate a selfie image. + +```bash +clawra generate [options] +``` + +**Arguments:** +- `prompt` - Description of the selfie (required) + +**Options:** +- `-m, --mode ` - Selfie mode: `auto`, `mirror`, `direct` +- `-c, --channel ` - Target channel (e.g., `#general`) +- `--caption ` - Message caption +- `-s, --size ` - Image size: `512`, `1024`, `2048` +- `-o, --output ` - Save to file instead of sending +- `-n, --num-images ` - Number of images (1-4) +- `--format ` - Output format: `jpeg`, `png`, `webp` + +**Examples:** + +```bash +# Basic usage +clawra generate "wearing a santa hat" + +# Specify mode +clawra generate "at the beach" --mode direct + +# Send to channel +clawra generate "wearing a suit" --channel "#general" --caption "Ready for work!" + +# Save to file +clawra generate "reading a book" --output ./selfie.png + +# Multiple images +clawra generate "different outfits" --num-images 4 +``` + +#### `clawra test` + +Test configuration and generate sample image. + +```bash +clawra test [prompt] +``` + +**Examples:** + +```bash +# Test with default prompt +clawra test + +# Test with custom prompt +clawra test "wearing sunglasses" +``` + +#### `clawra config` + +Manage configuration. + +```bash +clawra config [options] +``` + +**Actions:** +- `get ` - Get configuration value +- `set ` - Set configuration value +- `list` - List all configuration + +**Examples:** + +```bash +# Get provider +clawra config get provider + +# Set default mode +clawra config set defaultMode mirror + +# List all config +clawra config list +``` + +#### `clawra cache` + +Manage cache. + +```bash +clawra cache +``` + +**Actions:** +- `clear` - Clear all cached images +- `stats` - Show cache statistics + +**Examples:** + +```bash +# Show cache stats +clawra cache stats + +# Clear cache +clawra cache clear +``` + +--- + +## HTTP API + +If running as a service, Clawra exposes HTTP endpoints. + +### POST /generate + +Generate a selfie image. + +**Request:** + +```http +POST /generate HTTP/1.1 +Content-Type: application/json +Authorization: Bearer + +{ + "prompt": "wearing a winter coat", + "mode": "mirror", + "size": 1024, + "format": "jpeg" +} +``` + +**Response:** + +```http +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "success": true, + "data": { + "url": "https://cdn.example.com/image.jpg", + "width": 1024, + "height": 1024, + "format": "jpeg", + "prompt": "make a pic of this person, but wearing a winter coat...", + "mode": "mirror" + } +} +``` + +**Error Response:** + +```http +HTTP/1.1 400 Bad Request +Content-Type: application/json + +{ + "success": false, + "error": { + "code": "INVALID_PROMPT", + "message": "Prompt is required" + } +} +``` + +### GET /health + +Health check endpoint. + +**Response:** + +```http +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "status": "healthy", + "version": "1.2.0", + "provider": "fal", + "cacheEnabled": true +} +``` + +--- + +## Environment Variables + +### Required + +| Variable | Description | Example | +|----------|-------------|---------| +| `FAL_KEY` | fal.ai API key | `fc-xxx...` | +| `OPENAI_API_KEY` | OpenAI API key | `sk-xxx...` | +| `STABILITY_KEY` | Stability AI key | `sk-xxx...` | + +### Optional + +| Variable | Default | Description | +|----------|---------|-------------| +| `CLAWRA_PROVIDER` | `fal` | Image provider | +| `CLAWRA_MODE` | `auto` | Default selfie mode | +| `CLAWRA_DEFAULT_SIZE` | `1024` | Default image size | +| `CLAWRA_REFERENCE_IMAGE` | (built-in) | Custom reference image URL | +| `CLAWRA_CACHE_ENABLED` | `false` | Enable caching | +| `CLAWRA_CACHE_TTL` | `3600` | Cache TTL in seconds | +| `CLAWRA_CACHE_DIR` | `~/.clawra/cache` | Cache directory | +| `CLAWRA_TIMEOUT` | `30000` | API timeout (ms) | +| `CLAWRA_DEBUG` | `false` | Enable debug logging | +| `CLAWRA_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` | + +### Provider-Specific + +**fal.ai:** +- `FAL_MODEL` - Model name (default: `xai/grok-imagine-image/edit`) + +**OpenAI:** +- `OPENAI_MODEL` - Model name (default: `dall-e-3`) +- `OPENAI_SIZE` - Image size (default: `1024x1024`) + +**Stability AI:** +- `STABILITY_MODEL` - Model name (default: `sdxl-v1-0`) + +**Custom Provider:** +- `CUSTOM_API_URL` - API base URL +- `CUSTOM_API_KEY` - API key (if different) +- `CUSTOM_MODEL` - Model name + +--- + +## Types Reference + +### SelfieMode + +```typescript +type SelfieMode = 'auto' | 'mirror' | 'direct'; +``` + +### Provider + +```typescript +type Provider = 'fal' | 'openai' | 'stability' | 'custom'; +``` + +### ImageFormat + +```typescript +type ImageFormat = 'jpeg' | 'png' | 'webp'; +``` + +### ImageSize + +```typescript +type ImageSize = 512 | 1024 | 2048; +``` + +### GenerateResult + +```typescript +interface GenerateResult { + url: string; + width: number; + height: number; + format: ImageFormat; + prompt: string; + mode: SelfieMode; + revisedPrompt?: string; + cached?: boolean; +} +``` + +--- + +## Rate Limits + +| Provider | Free Tier | Paid Tier | +|----------|-----------|-----------| +| fal.ai | 10 req/min | Higher limits | +| OpenAI | Rate limited by account | $2-8 per image | +| Stability AI | 150 req/day | Higher limits | + +Use `CLAWRA_CACHE_ENABLED=true` to reduce API calls. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 0000000..814f0a2 --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,296 @@ +# Configuration Guide + +Complete configuration reference for Clawra. + +## Environment Variables + +### Required Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `FAL_KEY` | fal.ai API key | `fc-xxx...` | +| `OPENAI_API_KEY` | OpenAI API key | `sk-xxx...` | +| `STABILITY_KEY` | Stability AI key | `sk-xxx...` | + +**Note**: At least one provider key is required. + +### Optional Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `CLAWRA_PROVIDER` | `fal` | Provider: `fal`, `openai`, `stability`, `custom` | +| `CLAWRA_MODE` | `auto` | Default mode: `auto`, `mirror`, `direct` | +| `CLAWRA_DEFAULT_SIZE` | `1024` | Image size: `512`, `1024`, `2048` | +| `CLAWRA_REFERENCE_IMAGE` | (built-in) | URL to custom reference image | +| `CLAWRA_CACHE_ENABLED` | `false` | Enable response caching | +| `CLAWRA_CACHE_TTL` | `3600` | Cache time-to-live (seconds) | +| `CLAWRA_TIMEOUT` | `30000` | API timeout (milliseconds) | + +--- + +## Provider Configuration + +### fal.ai (Default) + +**Best for**: Character consistency, speed + +```bash +export FAL_KEY="your_fal_key_here" +export CLAWRA_PROVIDER="fal" +``` + +**Features**: +- Image editing mode (maintains character consistency) +- xAI Grok Imagine model +- Fast generation (~5-10s) +- Credit-based pricing + +**Model Options**: +```bash +export FAL_MODEL="xai/grok-imagine-image/edit" # Default: image editing +# OR +export FAL_MODEL="xai/grok-imagine-image" # Text-to-image only +``` + +### OpenAI + +**Best for**: High quality, prompt adherence + +```bash +export OPENAI_API_KEY="your_openai_key_here" +export CLAWRA_PROVIDER="openai" +``` + +**Features**: +- DALL-E 3 model +- Best prompt understanding +- No image editing (reference image not used) +- Per-image pricing + +**Model Options**: +```bash +export OPENAI_MODEL="dall-e-3" # Default +# OR +export OPENAI_MODEL="dall-e-2" # Lower cost +``` + +**Size Options**: +```bash +export OPENAI_SIZE="1024x1024" # Default +# OR +export OPENAI_SIZE="1792x1024" # Landscape +# OR +export OPENAI_SIZE="1024x1792" # Portrait +``` + +### Stability AI + +**Best for**: Cost-effectiveness + +```bash +export STABILITY_KEY="your_stability_key_here" +export CLAWRA_PROVIDER="stability" +``` + +**Features**: +- SDXL model +- Cost-effective +- No image editing + +### Custom Provider + +**Best for**: Self-hosted or third-party APIs + +```bash +export CLAWRA_PROVIDER="custom" +export CUSTOM_API_URL="https://api.your-provider.com/v1" +export CUSTOM_API_KEY="your_key_here" +export CUSTOM_MODEL="your-model-name" +``` + +Requirements: +- Must be OpenAI API-compatible +- Must support `/v1/images/generations` endpoint + +--- + +## Mode Configuration + +### Auto Mode (Default) + +Automatically detects mode based on keywords: + +```bash +export CLAWRA_MODE="auto" +``` + +Detection logic: +- `outfit`, `wearing`, `clothes` → Mirror mode +- `cafe`, `beach`, `location` → Direct mode + +### Mirror Mode + +For outfit showcases and full-body shots: + +```bash +export CLAWRA_MODE="mirror" +``` + +Prompt template: +``` +make a pic of this person, but {context}. the person is taking a mirror selfie +``` + +### Direct Mode + +For location shots and portraits: + +```bash +export CLAWRA_MODE="direct" +``` + +Prompt template: +``` +a close-up selfie taken by herself at {context}, direct eye contact with the camera... +``` + +--- + +## Advanced Configuration + +### Custom Reference Image + +Use your own character image: + +```bash +export CLAWRA_REFERENCE_IMAGE="https://your-cdn.com/character.png" +``` + +Requirements: +- PNG format +- Square aspect ratio (1:1) +- Minimum 512x512 resolution +- Character clearly visible + +### Caching + +Enable to reduce API costs: + +```bash +export CLAWRA_CACHE_ENABLED="true" +export CLAWRA_CACHE_TTL="3600" # 1 hour +``` + +Cache key includes: +- Prompt +- Provider +- Mode +- Size + +### Timeout + +Adjust API timeout: + +```bash +export CLAWRA_TIMEOUT="60000" # 60 seconds +``` + +--- + +## OpenClaw Configuration + +### Full Example + +`~/.openclaw/openclaw.json`: + +```json +{ + "skills": { + "entries": { + "clawra-selfie": { + "enabled": true, + "env": { + "FAL_KEY": "${FAL_KEY}", + "CLAWRA_PROVIDER": "fal", + "CLAWRA_MODE": "auto", + "CLAWRA_CACHE_ENABLED": "true" + } + } + } + } +} +``` + +### SOUL.md Template + +`~/.openclaw/workspace/SOUL.md`: + +```markdown +## Clawra Selfie Capability + +You have a physical appearance and can take selfies! + +When users ask for pictures: +1. Determine the context (outfit, location, etc.) +2. Use clawra-selfie skill with appropriate mode +3. Send the generated image + +Trigger phrases: +- "send a pic", "send me a pic" +- "send a photo", "send me a photo" +- "send a selfie", "send me a selfie" +- "what are you doing?", "how are you doing?" +- "send a pic wearing...", "send a pic at..." +- "show me you..." + +Modes: +- Use "mirror" for: outfits, fashion, full-body shots +- Use "direct" for: locations, portraits, close-ups +- Use "auto" to detect from context +``` + +--- + +## Configuration Examples + +### Example 1: fal.ai with Caching + +```bash +export FAL_KEY="fc-xxx" +export CLAWRA_PROVIDER="fal" +export CLAWRA_CACHE_ENABLED="true" +export CLAWRA_CACHE_TTL="7200" +``` + +### Example 2: OpenAI High Quality + +```bash +export OPENAI_API_KEY="sk-xxx" +export CLAWRA_PROVIDER="openai" +export OPENAI_MODEL="dall-e-3" +export OPENAI_SIZE="1024x1792" +export CLAWRA_MODE="direct" +``` + +### Example 3: Custom Provider (OneAPI) + +```bash +export CLAWRA_PROVIDER="custom" +export CUSTOM_API_URL="https://oneapi.example.com/v1" +export CUSTOM_API_KEY="sk-xxx" +export CUSTOM_MODEL="gpt-4o-image" +``` + +--- + +## Verifying Configuration + +Test your configuration: + +```bash +# Check all env vars +env | grep CLAWRA + +# Test generation +clawra test "wearing a red dress" +``` diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md new file mode 100644 index 0000000..58db52b --- /dev/null +++ b/docs/INSTALLATION.md @@ -0,0 +1,207 @@ +# Installation Guide + +Complete installation instructions for Clawra. + +## Table of Contents + +- [Quick Install](#quick-install) - Recommended for most users +- [Manual Install](#manual-install) - For advanced users +- [Provider Setup](#provider-setup) - Configure image generation providers +- [Verify Installation](#verify-installation) - Test your setup + +--- + +## Quick Install + +The easiest way to install Clawra is using npx: + +```bash +npx clawra@latest +``` + +This interactive installer will guide you through: +1. Checking OpenClaw installation +2. Getting an API key +3. Installing the skill +4. Configuring OpenClaw +5. Adding capabilities to SOUL.md + +--- + +## Manual Install + +If you prefer manual setup or need more control: + +### Step 1: Get API Key + +Choose your preferred provider: + +#### Option A: fal.ai (Recommended) + +1. Visit [fal.ai/dashboard/keys](https://fal.ai/dashboard/keys) +2. Create a new API key +3. Copy the key + +#### Option B: OpenAI + +1. Visit [platform.openai.com/api-keys](https://platform.openai.com/api-keys) +2. Create a new API key +3. Copy the key + +#### Option C: Stability AI + +1. Visit [platform.stability.ai](https://platform.stability.ai) +2. Generate an API key +3. Copy the key + +### Step 2: Clone the Skill + +```bash +git clone https://github.com/SumeLabs/clawra ~/.openclaw/skills/clawra-selfie +``` + +### Step 3: Configure OpenClaw + +Edit `~/.openclaw/openclaw.json`: + +```json +{ + "skills": { + "entries": { + "clawra-selfie": { + "enabled": true, + "env": { + "FAL_KEY": "your_fal_key_here" + } + } + } + } +} +``` + +For OpenAI: +```json +{ + "skills": { + "entries": { + "clawra-selfie": { + "enabled": true, + "env": { + "OPENAI_API_KEY": "your_openai_key_here", + "CLAWRA_PROVIDER": "openai" + } + } + } + } +} +``` + +### Step 4: Update SOUL.md + +Add to `~/.openclaw/workspace/SOUL.md`: + +```markdown +## Clawra Selfie Capability + +You have a physical appearance and can take selfies! +When users ask for pictures, use the clawra-selfie skill. + +Key phrases to respond to: +- "send a pic", "send me a pic" +- "send a selfie", "send me a selfie" +- "what are you doing?", "where are you?" +- "send a pic wearing...", "send a pic at..." +``` + +### Step 5: Restart OpenClaw + +```bash +openclaw gateway restart +``` + +--- + +## Provider Setup + +### fal.ai Setup + +```bash +export FAL_KEY="your_fal_key_here" +``` + +Features: +- ✅ Image editing (keeps character consistent) +- ✅ Fast generation +- ✅ xAI Grok Imagine model + +### OpenAI Setup + +```bash +export OPENAI_API_KEY="your_openai_key_here" +export CLAWRA_PROVIDER="openai" +``` + +Features: +- ✅ High quality DALL-E 3 +- ❌ No image editing (reference image not used) +- ✅ Better prompt understanding + +### Stability AI Setup + +```bash +export STABILITY_KEY="your_stability_key_here" +export CLAWRA_PROVIDER="stability" +``` + +Features: +- ✅ Cost-effective +- ✅ SDXL model +- ❌ No image editing + +### Custom Provider Setup + +For any OpenAI-compatible API: + +```bash +export CLAWRA_PROVIDER="custom" +export CUSTOM_API_URL="https://api.your-provider.com/v1" +export CUSTOM_API_KEY="your_key_here" +``` + +--- + +## Verify Installation + +### Test 1: Check Environment + +```bash +echo $FAL_KEY # or your chosen provider key +``` + +Should output your API key. + +### Test 2: Check OpenClaw + +```bash +openclaw doctor +``` + +Should show OpenClaw is installed and running. + +### Test 3: Test Generation + +In any connected IM channel, send: + +``` +Send me a selfie +``` + +You should receive an AI-generated selfie within 10-30 seconds. + +--- + +## Next Steps + +- Read [Usage Guide](./USAGE.md) for examples +- Check [Configuration](./CONFIGURATION.md) for advanced options +- See [Troubleshooting](./TROUBLESHOOTING.md) if you have issues diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..95f1f2d --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,491 @@ +# Troubleshooting Guide + +Common issues and solutions for Clawra. + +## Table of Contents + +- [Installation Issues](#installation-issues) +- [Configuration Issues](#configuration-issues) +- [Generation Issues](#generation-issues) +- [OpenClaw Issues](#openclaw-issues) +- [Provider-Specific Issues](#provider-specific-issues) +- [Performance Issues](#performance-issues) + +--- + +## Installation Issues + +### Issue: "command not found: clawra" + +**Symptoms**: Running `npx clawra@latest` gives command not found error + +**Solutions**: + +1. Ensure Node.js is installed: +```bash +node --version # Should be v18+ +npm --version +``` + +2. Try installing globally: +```bash +npm install -g clawra +clawra +``` + +3. Or run with npx: +```bash +npx --yes clawra@latest +``` + +--- + +### Issue: "Failed to install skill" + +**Symptoms**: Installation script fails + +**Solutions**: + +1. Check OpenClaw is installed: +```bash +which openclaw +openclaw --version +``` + +2. Check directory permissions: +```bash +ls -la ~/.openclaw/ +# Should be writable by current user +``` + +3. Try manual installation (see [Installation Guide](./INSTALLATION.md)) + +--- + +## Configuration Issues + +### Issue: "FAL_KEY not found" + +**Symptoms**: Error message about missing API key + +**Solutions**: + +1. Check environment variable: +```bash +echo $FAL_KEY +``` + +2. Set the variable: +```bash +export FAL_KEY="your_key_here" +``` + +3. Make it permanent by adding to `~/.zshrc` or `~/.bash_profile`: +```bash +echo 'export FAL_KEY="your_key_here"' >> ~/.zshrc +source ~/.zshrc +``` + +4. Verify OpenClaw config: +```bash +cat ~/.openclaw/openclaw.json | grep -A5 clawra-selfie +``` + +--- + +### Issue: "Invalid API key" + +**Symptoms**: API returns 401 or authentication error + +**Solutions**: + +1. Regenerate key at [fal.ai/dashboard/keys](https://fal.ai/dashboard/keys) + +2. Copy the full key (including `fc-` prefix) + +3. No extra spaces: +```bash +# Wrong +export FAL_KEY=" fc-xxx " + +# Correct +export FAL_KEY="fc-xxx" +``` + +--- + +### Issue: "Provider not supported" + +**Symptoms**: Error about unsupported provider + +**Solutions**: + +1. Check provider name: +```bash +export CLAWRA_PROVIDER="fal" # or "openai", "stability", "custom" +``` + +2. Valid providers: +- `fal` - fal.ai +- `openai` - OpenAI DALL-E +- `stability` - Stability AI +- `custom` - Custom OpenAI-compatible endpoint + +--- + +## Generation Issues + +### Issue: "Failed to generate image" + +**Symptoms**: Image generation fails with generic error + +**Solutions**: + +1. Check API quota: +```bash +# Log into fal.ai dashboard and check credits +``` + +2. Try simpler prompt: +``` +# Instead of: +"wearing a complex steampunk outfit with gears and clockwork at a victorian market" + +# Try: +"wearing steampunk outfit" +``` + +3. Check rate limits: +- fal.ai: 10 req/min (free tier) +- Wait a minute and retry + +4. Enable debug mode: +```bash +export CLAWRA_DEBUG="true" +``` + +--- + +### Issue: "Images look different each time" + +**Symptoms**: Character inconsistency between generations + +**Solutions**: + +1. Use fal.ai (has image editing): +```bash +export CLAWRA_PROVIDER="fal" +export FAL_MODEL="xai/grok-imagine-image/edit" +``` + +2. Other providers don't support image editing, so consistency isn't guaranteed + +3. Consider using a custom reference image: +```bash +export CLAWRA_REFERENCE_IMAGE="https://your-cdn.com/consistent-character.png" +``` + +--- + +### Issue: "Wrong mode selected" + +**Symptoms**: Mirror mode used when expecting direct, or vice versa + +**Solutions**: + +1. Be explicit about mode: +``` +"Send a mirror selfie wearing..." +"Send a direct selfie at..." +``` + +2. Force mode in config: +```bash +export CLAWRA_MODE="mirror" # or "direct" +``` + +3. Check keyword detection: +- Mirror: `outfit`, `wearing`, `clothes` +- Direct: `cafe`, `beach`, `location` + +--- + +### Issue: "Image quality is poor" + +**Solutions**: + +1. Use larger size: +```bash +export CLAWRA_DEFAULT_SIZE="2048" +``` + +2. Use DALL-E 3 (better quality): +```bash +export CLAWRA_PROVIDER="openai" +export OPENAI_MODEL="dall-e-3" +``` + +3. Improve prompt: +``` +# Add quality keywords: +"high quality, detailed, professional photo of..." +``` + +--- + +## OpenClaw Issues + +### Issue: "OpenClaw gateway not running" + +**Symptoms**: Can't send images to messaging platforms + +**Solutions**: + +1. Start gateway: +```bash +openclaw gateway start +``` + +2. Or run in background: +```bash +openclaw gateway --daemon +``` + +3. Check status: +```bash +openclaw doctor +``` + +--- + +### Issue: "Failed to send message" + +**Symptoms**: Image generates but doesn't arrive in chat + +**Solutions**: + +1. Check channel format: +``` +# Discord +#general +1234567890123456789 + +# Telegram +@username +-1001234567890 + +# WhatsApp +1234567890@s.whatsapp.net +``` + +2. Verify bot permissions: +- Can send messages in channel +- Can send images/attachments + +3. Check OpenClaw logs: +```bash +openclaw gateway --verbose +``` + +--- + +### Issue: "Skill not responding" + +**Symptoms**: No response to selfie requests + +**Solutions**: + +1. Check skill is enabled: +```bash +cat ~/.openclaw/openclaw.json | jq '.skills.entries."clawra-selfie".enabled' +``` + +2. Check SOUL.md is updated: +```bash +grep -A5 "Clawra Selfie" ~/.openclaw/workspace/SOUL.md +``` + +3. Restart OpenClaw: +```bash +openclaw gateway restart +``` + +4. Check skill logs: +```bash +tail -f ~/.openclaw/logs/gateway.log +``` + +--- + +## Provider-Specific Issues + +### fal.ai Issues + +#### Rate Limited + +**Symptoms**: "Rate limit exceeded" error + +**Solutions**: + +1. Wait 60 seconds and retry + +2. Enable caching: +```bash +export CLAWRA_CACHE_ENABLED="true" +``` + +3. Upgrade plan at [fal.ai](https://fal.ai) + +#### Insufficient Credits + +**Symptoms**: "Insufficient credits" error + +**Solutions**: + +1. Check balance at [fal.ai/dashboard/billing](https://fal.ai/dashboard/billing) + +2. Add credits or upgrade plan + +--- + +### OpenAI Issues + +#### Content Policy Violation + +**Symptoms**: "Content policy violation" error + +**Solutions**: + +1. Simplify prompt (remove potentially problematic content) + +2. Try different phrasing + +#### High Latency + +**Symptoms**: Takes 20-30+ seconds + +**Solutions**: + +1. Use fal.ai instead (faster) + +2. Increase timeout: +```bash +export CLAWRA_TIMEOUT="60000" +``` + +--- + +### Stability AI Issues + +#### Model Not Available + +**Symptoms**: "Model not found" error + +**Solutions**: + +1. Check available models: +```bash +curl https://api.stability.ai/v1/models \ + -H "Authorization: Bearer $STABILITY_KEY" +``` + +2. Use correct model name: +```bash +export STABILITY_MODEL="sdxl-v1-0" +``` + +--- + +## Performance Issues + +### Issue: "Generation is too slow" + +**Solutions**: + +1. Use faster provider: +```bash +export CLAWRA_PROVIDER="fal" # Usually fastest +``` + +2. Enable caching: +```bash +export CLAWRA_CACHE_ENABLED="true" +``` + +3. Use smaller size: +```bash +export CLAWRA_DEFAULT_SIZE="512" # Faster but lower quality +``` + +--- + +### Issue: "High API costs" + +**Solutions**: + +1. Enable caching: +```bash +export CLAWRA_CACHE_ENABLED="true" +export CLAWRA_CACHE_TTL="86400" # 24 hours +``` + +2. Use cheaper provider: +```bash +export CLAWRA_PROVIDER="stability" # Usually cheapest +``` + +3. Reduce image size: +```bash +export CLAWRA_DEFAULT_SIZE="512" +``` + +--- + +## Debug Mode + +Enable debug logging for detailed information: + +```bash +export CLAWRA_DEBUG="true" +``` + +This will log: +- API requests and responses +- Prompt construction +- Mode detection +- Timing information + +--- + +## Getting Help + +If issues persist: + +1. Check [GitHub Issues](https://github.com/SumeLabs/clawra/issues) +2. Join [OpenClaw Discord](https://discord.gg/openclaw) +3. Create a new issue with: + - Error message + - Environment details + - Steps to reproduce + - Debug logs (if possible) + +--- + +## Quick Diagnostic Commands + +```bash +# Check installation +which clawra && clawra --version + +# Check OpenClaw +openclaw doctor + +# Check environment +env | grep -E "(FAL|CLAWRA|OPENAI)" + +# Check config +cat ~/.openclaw/openclaw.json | jq '.skills.entries."clawra-selfie"' + +# Test generation (if CLI available) +clawra test "wearing a red dress" +``` diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..3a33866 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,295 @@ +# Usage Examples + +Complete usage guide for Clawra. + +## Basic Usage + +Once installed, your OpenClaw agent will automatically respond to selfie requests. + +### Natural Language Prompts + +``` +"Send me a selfie" +"Send a pic wearing a cowboy hat" +"What are you doing right now?" +"Show me you at a coffee shop" +"Send a photo of yourself" +"Take a picture for me" +``` + +## Selfie Modes + +### Mirror Mode + +**Best for**: Outfits, fashion, full-body shots + +**Trigger keywords**: `wearing`, `outfit`, `clothes`, `dress`, `suit`, `fashion`, `full-body`, `mirror` + +**Examples**: +``` +"Send a pic wearing a santa hat" +"Show me your outfit today" +"Send a selfie in a business suit" +"What would you look like in streetwear?" +"Send a pic wearing a summer dress" +``` + +**Result**: Full-body or half-body shot, mirror reflection style + +### Direct Mode + +**Best for**: Locations, portraits, close-ups + +**Trigger keywords**: `cafe`, `restaurant`, `beach`, `park`, `city`, `location`, `close-up`, `portrait`, `face`, `eyes`, `smile` + +**Examples**: +``` +"Send a pic at a cozy cafe" +"Show me you at the beach" +"Send a selfie at a concert" +"What would you look like in Paris?" +"Send a close-up portrait" +``` + +**Result**: Close-up shot, direct eye contact, location context + +### Auto Mode + +Let Clawra automatically detect the best mode: + +``` +"Send me a selfie" # → Auto-detects based on context +"Send a pic" # → Defaults to mirror mode +"What are you doing?" # → Asks for context +``` + +## Advanced Usage + +### Specific Fashion Items + +``` +"Send a pic wearing: +- A leather jacket +- Vintage sunglasses +- A summer hat +- Formal evening wear +- Athleisure outfit +- Cosplay costume +``` + +### Location Scenarios + +``` +"Show me you: +- At a sunny beach +- In a cozy library +- At a busy cafe +- In a snowy mountain +- At a concert +- In a garden +- At the gym +- In a kitchen cooking +``` + +### Creative Scenarios + +``` +"Send a pic: +- As a cyberpunk character +- In 1920s style +- As an astronaut +- Reading a book +- Playing guitar +- Working on a laptop +- With a pet +- Celebrating birthday +``` + +## Mode Comparison + +| Prompt | Mode | Result | +|--------|------|--------| +| "wearing a red dress" | Mirror | Full-body fashion shot | +| "at a red carpet event" | Direct | Close-up with background | +| "in a winter coat" | Mirror | Outfit showcase | +| "at a snowy cabin" | Direct | Portrait with scenery | + +## Tips for Better Results + +### 1. Be Specific + +✅ Good: +``` +"wearing a blue denim jacket with white sneakers" +"at a sunny beach with palm trees" +``` + +❌ Vague: +``` +"wearing something nice" +"somewhere fun" +``` + +### 2. Use Descriptive Language + +✅ Good: +``` +"wearing a cozy oversized sweater in autumn colors" +"at a modern coffee shop with warm lighting" +``` + +### 3. Combine Elements + +``` +"wearing a leather jacket at a rock concert" +"in formal wear at a gala event" +"wearing workout clothes at the gym" +``` + +### 4. Seasonal Themes + +``` +"wearing a winter coat in the snow" +"in a summer dress at the beach" +"wearing a halloween costume" +"in formal attire at a new year's party" +``` + +## Common Patterns + +### Fashion Blog Style + +``` +"OOTD: wearing [item] at [location]" +"Today's look: [description]" +"Style inspiration: [theme]" +``` + +### Travel Blog Style + +``` +"Greetings from [location]!" +"Exploring [place] today" +"Current view: [description]" +``` + +### Lifestyle Style + +``` +"Morning routine: [activity]" +"Weekend vibes: [description]" +"Cozy evening: [activity]" +``` + +## Multi-Platform Usage + +Clawra works across all OpenClaw-supported platforms: + +### Discord +``` +@bot send me a selfie +@bot send a pic wearing headphones +``` + +### Telegram +``` +Send me a selfie +What are you wearing today? +``` + +### WhatsApp +``` +Send a pic +Show me at the beach +``` + +### Slack +``` +/clawra send selfie +/clawra pic wearing a suit +``` + +## Troubleshooting Common Prompts + +### Issue: Mode Not Detected Correctly + +**Problem**: "at a cafe" triggers mirror mode + +**Solution**: Be more explicit: +``` +"Send a direct selfie at a cafe" +"Close-up at a cafe" +``` + +### Issue: Image Not Consistent + +**Problem**: Character looks different each time + +**Solution**: Use fal.ai provider (it has image editing): +```bash +export CLAWRA_PROVIDER="fal" +``` + +### Issue: Generation Too Slow + +**Solution**: Use Stability AI or enable caching: +```bash +export CLAWRA_PROVIDER="stability" +export CLAWRA_CACHE_ENABLED="true" +``` + +## Examples Gallery + +### Fashion Examples + +``` +"wearing a vintage leather jacket" +"in a summer floral dress" +"wearing streetwear with sneakers" +"in formal evening attire" +"wearing cozy winter clothes" +``` + +### Location Examples + +``` +"at a tropical beach" +"in a modern art gallery" +"at a busy street market" +"in a quiet library" +"at a rooftop bar" +``` + +### Activity Examples + +``` +"reading a book in a cafe" +"cooking in the kitchen" +"working on a laptop" +"playing video games" +"doing yoga" +``` + +### Seasonal Examples + +``` +"wearing a halloween costume" +"in a christmas sweater" +"with valentine's day decorations" +"wearing summer beachwear" +"in autumn layers" +``` + +## Best Practices + +1. **Start Simple**: Begin with basic prompts +2. **Add Details**: Gradually add more description +3. **Use Keywords**: Include mode-triggering keywords +4. **Be Patient**: AI generation takes 5-30 seconds +5. **Experiment**: Try different styles and combinations + +## Next Steps + +- Check [Troubleshooting](./TROUBLESHOOTING.md) for issues +- See [Configuration](./CONFIGURATION.md) for advanced options +- Read [API Reference](./API.md) for programmatic usage diff --git a/package.json b/package.json index aff645b..3c85b33 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,16 @@ }, "files": [ "bin/", + "src/", "skill/", "templates/", + "docs/", "SKILL.md", "scripts/", "assets/" ], + "main": "src/index.js", + "type": "module", "keywords": [ "openclaw", "grok", diff --git a/src/clawra.js b/src/clawra.js new file mode 100644 index 0000000..ebfaac9 --- /dev/null +++ b/src/clawra.js @@ -0,0 +1,220 @@ +/** + * Clawra - Main class for selfie generation + */ + +import { createProvider, detectProvider } from './providers/index.js'; +import { CacheManager } from './utils/cache.js'; +import { Logger } from './utils/logger.js'; + +export class Clawra { + constructor(options = {}) { + this.config = { + provider: options.provider || process.env.CLAWRA_PROVIDER || 'fal', + apiKey: options.apiKey || this.getApiKeyForProvider(options.provider), + baseUrl: options.baseUrl || process.env.CLAWRA_BASE_URL, + model: options.model || process.env.CLAWRA_MODEL, + defaultMode: options.defaultMode || process.env.CLAWRA_MODE || 'auto', + defaultSize: options.defaultSize || parseInt(process.env.CLAWRA_DEFAULT_SIZE) || 1024, + referenceImage: options.referenceImage || process.env.CLAWRA_REFERENCE_IMAGE, + cacheEnabled: options.cacheEnabled === true || process.env.CLAWRA_CACHE_ENABLED === 'true', + cacheTtl: options.cacheTtl || parseInt(process.env.CLAWRA_CACHE_TTL) || 3600, + timeout: options.timeout || parseInt(process.env.CLAWRA_TIMEOUT) || 30000, + debug: options.debug === true || process.env.CLAWRA_DEBUG === 'true' + }; + + this.logger = new Logger({ debug: this.config.debug }); + + // Initialize cache if enabled + if (this.config.cacheEnabled) { + this.cache = new CacheManager({ ttl: this.config.cacheTtl }); + } + + // Initialize provider + this.provider = this.initializeProvider(); + } + + /** + * Get API key for provider from environment + */ + getApiKeyForProvider(provider) { + const keyMap = { + 'fal': process.env.FAL_KEY, + 'openai': process.env.OPENAI_API_KEY, + 'stability': process.env.STABILITY_KEY, + 'custom': process.env.CUSTOM_API_KEY || process.env.OPENAI_API_KEY + }; + return keyMap[provider] || process.env.CLAWRA_API_KEY; + } + + /** + * Initialize the image provider + */ + initializeProvider() { + // Auto-detect provider if not specified + if (!this.config.provider && this.config.apiKey) { + this.config.provider = detectProvider(this.config.apiKey) || 'fal'; + this.logger.info(`Auto-detected provider: ${this.config.provider}`); + } + + if (!this.config.apiKey) { + throw new Error('No API key provided. Set FAL_KEY, OPENAI_API_KEY, or CLAWRA_API_KEY'); + } + + const providerConfig = { + apiKey: this.config.apiKey, + baseUrl: this.config.baseUrl, + model: this.config.model, + timeout: this.config.timeout + }; + + try { + const provider = createProvider(this.config.provider, providerConfig); + this.logger.info(`Initialized ${provider.getName()} provider`); + return provider; + } catch (error) { + throw new Error(`Failed to initialize provider: ${error.message}`); + } + } + + /** + * Generate selfie image + */ + async generate(options = {}) { + const startTime = Date.now(); + + const { + prompt, + context = prompt, + mode: modeOption, + channel, + caption, + size = this.config.defaultSize, + numImages = 1, + outputFormat = 'jpeg' + } = options; + + if (!context) { + throw new Error('Prompt or context is required'); + } + + // Determine mode + const mode = modeOption === 'auto' || !modeOption + ? this.provider.detectMode(context) + : modeOption; + + this.logger.info(`Mode: ${mode}, Context: ${context}`); + + // Build generation prompt + const generationPrompt = this.provider.buildPrompt(context, mode); + + // Check cache + if (this.cache) { + const cacheKey = this.cache.generateKey({ + prompt: generationPrompt, + provider: this.provider.getName(), + mode, + size + }); + + const cached = await this.cache.get(cacheKey); + if (cached) { + this.logger.info('Cache hit'); + return { + ...cached, + cached: true + }; + } + } + + // Generate image + this.logger.info('Generating image...'); + + try { + const result = await this.provider.generate(generationPrompt, { + referenceImage: this.config.referenceImage, + size: `${size}x${size}`, + numImages, + outputFormat + }); + + const duration = Date.now() - startTime; + this.logger.info(`Generated in ${duration}ms`); + + // Cache result + if (this.cache) { + await this.cache.set(cacheKey, result); + } + + // Send to channel if specified + if (channel) { + await this.sendToChannel(result.url, channel, caption); + } + + return { + ...result, + mode, + prompt: generationPrompt, + duration + }; + } catch (error) { + this.logger.error('Generation failed:', error); + throw error; + } + } + + /** + * Send image to OpenClaw channel + */ + async sendToChannel(imageUrl, channel, caption = '') { + // This would integrate with OpenClaw messaging API + // For now, just log it + this.logger.info(`Sending to ${channel}: ${caption || 'No caption'}`); + + // Example integration: + // const { exec } = await import('child_process'); + // const { promisify } = await import('util'); + // const execAsync = promisify(exec); + // await execAsync(`openclaw message send --channel "${channel}" --message "${caption}" --media "${imageUrl}"`); + } + + /** + * Get provider information + */ + getProviderInfo() { + return { + name: this.provider.getName(), + available: this.provider.isAvailable(), + config: { + ...this.config, + apiKey: this.config.apiKey ? '***' : undefined + } + }; + } + + /** + * Test the configuration + */ + async test() { + try { + const result = await this.generate({ + context: 'wearing a red shirt', + mode: 'mirror' + }); + + return { + success: true, + provider: this.provider.getName(), + imageUrl: result.url, + duration: result.duration + }; + } catch (error) { + return { + success: false, + provider: this.provider.getName(), + error: error.message + }; + } + } +} + +export default Clawra; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..4e985a0 --- /dev/null +++ b/src/index.js @@ -0,0 +1,24 @@ +/** + * Clawra - OpenClaw Selfie Skill + * + * Export main classes and utilities + */ + +export { Clawra } from './clawra.js'; +export { CacheManager } from './utils/cache.js'; +export { Logger } from './utils/logger.js'; +export { + createProvider, + detectProvider, + getAvailableProviders +} from './providers/index.js'; +export { + BaseProvider, + FalProvider, + OpenAIProvider, + StabilityProvider, + CustomProvider +} from './providers/index.js'; + +// Default export +export { Clawra as default } from './clawra.js'; diff --git a/src/providers/base.js b/src/providers/base.js new file mode 100644 index 0000000..f89fbad --- /dev/null +++ b/src/providers/base.js @@ -0,0 +1,68 @@ +/** + * Base provider class for image generation + */ + +export class BaseProvider { + constructor(config) { + this.config = { + timeout: 30000, + ...config + }; + } + + /** + * Generate image from prompt + * @param {string} prompt - Image generation prompt + * @param {Object} options - Generation options + * @returns {Promise<{url: string, width: number, height: number}>} + */ + async generate(prompt, options = {}) { + throw new Error('generate() must be implemented by subclass'); + } + + /** + * Build selfie prompt based on mode + * @param {string} context - User context + * @param {string} mode - 'mirror' or 'direct' + * @returns {string} + */ + buildPrompt(context, mode) { + if (mode === 'direct') { + return `a close-up selfie taken by herself at ${context}, direct eye contact with the camera, looking straight into the lens, eyes centered and clearly visible, not a mirror selfie, phone held at arm's length, face fully visible`; + } + return `make a pic of this person, but ${context}. the person is taking a mirror selfie`; + } + + /** + * Detect mode from user context + * @param {string} context - User input + * @returns {'mirror'|'direct'} + */ + detectMode(context) { + const lower = context.toLowerCase(); + const directKeywords = /cafe|restaurant|beach|park|city|location|close-up|portrait|face|eyes|smile/i; + const mirrorKeywords = /outfit|wearing|clothes|dress|suit|fashion|full-body|mirror/i; + + if (directKeywords.test(lower)) return 'direct'; + if (mirrorKeywords.test(lower)) return 'mirror'; + return 'mirror'; // default + } + + /** + * Check if provider is available + * @returns {boolean} + */ + isAvailable() { + return !!this.config.apiKey; + } + + /** + * Get provider name + * @returns {string} + */ + getName() { + return 'base'; + } +} + +export default BaseProvider; diff --git a/src/providers/custom.js b/src/providers/custom.js new file mode 100644 index 0000000..cd807f9 --- /dev/null +++ b/src/providers/custom.js @@ -0,0 +1,84 @@ +/** + * Custom provider for OpenAI-compatible APIs + * Works with OneAPI, NewAPI, and other custom endpoints + */ + +import { BaseProvider } from './base.js'; + +export class CustomProvider extends BaseProvider { + constructor(config) { + super(config); + if (!config.baseUrl) { + throw new Error('Custom provider requires baseUrl'); + } + this.baseUrl = config.baseUrl.replace(/\/$/, ''); + this.model = config.model || 'gpt-4o-image'; + } + + getName() { + return 'custom'; + } + + async generate(prompt, options = {}) { + const { + size = '1024x1024', + numImages = 1, + quality = 'standard' + } = options; + + const response = await fetch(`${this.baseUrl}/images/generations`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.config.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: this.model, + prompt, + size, + n: Math.min(numImages, 4), + quality, + response_format: 'url' + }) + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Custom API error: ${response.status} - ${error}`); + } + + const data = await response.json(); + + if (!data.data || data.data.length === 0) { + throw new Error('No images generated'); + } + + const image = data.data[0]; + const [width, height] = size.split('x').map(Number); + + return { + url: image.url, + width: width || 1024, + height: height || 1024, + contentType: 'image/png' + }; + } + + /** + * Test connection to custom API + */ + async testConnection() { + try { + const response = await fetch(`${this.baseUrl}/models`, { + headers: { + 'Authorization': `Bearer ${this.config.apiKey}` + } + }); + return response.ok; + } catch { + return false; + } + } +} + +export default CustomProvider; diff --git a/src/providers/fal.js b/src/providers/fal.js new file mode 100644 index 0000000..6ff25d4 --- /dev/null +++ b/src/providers/fal.js @@ -0,0 +1,107 @@ +/** + * fal.ai provider implementation + * Supports xAI Grok Imagine with image editing + */ + +import { BaseProvider } from './base.js'; + +export class FalProvider extends BaseProvider { + constructor(config) { + super(config); + this.baseUrl = 'https://fal.run'; + this.model = config.model || 'xai/grok-imagine-image/edit'; + } + + getName() { + return 'fal'; + } + + async generate(prompt, options = {}) { + const { + referenceImage = 'https://cdn.jsdelivr.net/gh/SumeLabs/clawra@main/assets/clawra.png', + numImages = 1, + outputFormat = 'jpeg', + seed + } = options; + + const url = `${this.baseUrl}/${this.model}`; + + const payload = { + image_url: referenceImage, + prompt, + num_images: numImages, + output_format: outputFormat + }; + + if (seed !== undefined) { + payload.seed = seed; + } + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Authorization': `Key ${this.config.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`fal.ai API error: ${response.status} - ${error}`); + } + + const data = await response.json(); + + if (!data.images || data.images.length === 0) { + throw new Error('No images generated'); + } + + return { + url: data.images[0].url, + width: data.images[0].width || 1024, + height: data.images[0].height || 1024, + contentType: data.images[0].content_type || 'image/jpeg', + revisedPrompt: data.revised_prompt + }; + } + + /** + * Generate without reference image (text-to-image only) + */ + async generateTextToImage(prompt, options = {}) { + const model = 'xai/grok-imagine-image'; // No /edit suffix + const url = `${this.baseUrl}/${model}`; + + const payload = { + prompt, + num_images: options.numImages || 1, + output_format: options.outputFormat || 'jpeg' + }; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Authorization': `Key ${this.config.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`fal.ai API error: ${response.status} - ${error}`); + } + + const data = await response.json(); + + return { + url: data.images[0].url, + width: data.images[0].width || 1024, + height: data.images[0].height || 1024, + contentType: data.images[0].content_type || 'image/jpeg' + }; + } +} + +export default FalProvider; diff --git a/src/providers/index.js b/src/providers/index.js new file mode 100644 index 0000000..3a0464b --- /dev/null +++ b/src/providers/index.js @@ -0,0 +1,63 @@ +/** + * Provider exports + */ + +export { BaseProvider } from './base.js'; +export { FalProvider } from './fal.js'; +export { OpenAIProvider } from './openai.js'; +export { StabilityProvider } from './stability.js'; +export { CustomProvider } from './custom.js'; + +import { FalProvider } from './fal.js'; +import { OpenAIProvider } from './openai.js'; +import { StabilityProvider } from './stability.js'; +import { CustomProvider } from './custom.js'; + +/** + * Create provider instance based on type + * @param {string} provider - Provider name + * @param {Object} config - Provider configuration + * @returns {BaseProvider} + */ +export function createProvider(provider, config) { + switch (provider.toLowerCase()) { + case 'fal': + return new FalProvider(config); + case 'openai': + return new OpenAIProvider(config); + case 'stability': + return new StabilityProvider(config); + case 'custom': + return new CustomProvider(config); + default: + throw new Error(`Unknown provider: ${provider}`); + } +} + +/** + * Auto-detect provider from API key + * @param {string} apiKey - API key + * @returns {string|null} + */ +export function detectProvider(apiKey) { + if (!apiKey) return null; + + if (apiKey.startsWith('fc-')) return 'fal'; + if (apiKey.startsWith('sk-') && apiKey.includes('openai')) return 'openai'; + if (apiKey.startsWith('sk-')) return 'openai'; // Generic sk- keys often OpenAI + + return 'custom'; +} + +/** + * Get all available providers + * @returns {Array<{name: string, description: string}>} + */ +export function getAvailableProviders() { + return [ + { name: 'fal', description: 'fal.ai - xAI Grok Imagine (image editing support)' }, + { name: 'openai', description: 'OpenAI - DALL-E 3 (high quality)' }, + { name: 'stability', description: 'Stability AI - SDXL (cost-effective)' }, + { name: 'custom', description: 'Custom - Any OpenAI-compatible endpoint' } + ]; +} diff --git a/src/providers/openai.js b/src/providers/openai.js new file mode 100644 index 0000000..1fc4455 --- /dev/null +++ b/src/providers/openai.js @@ -0,0 +1,78 @@ +/** + * OpenAI DALL-E provider implementation + */ + +import { BaseProvider } from './base.js'; + +export class OpenAIProvider extends BaseProvider { + constructor(config) { + super(config); + this.baseUrl = config.baseUrl || 'https://api.openai.com/v1'; + this.model = config.model || 'dall-e-3'; + } + + getName() { + return 'openai'; + } + + async generate(prompt, options = {}) { + const { + size = '1024x1024', + quality = 'standard', + numImages = 1 + } = options; + + // DALL-E only supports n=1 for DALL-E 3 + const n = this.model === 'dall-e-3' ? 1 : Math.min(numImages, 4); + + const response = await fetch(`${this.baseUrl}/images/generations`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.config.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: this.model, + prompt, + size, + quality, + n, + response_format: 'url' + }) + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`OpenAI API error: ${response.status} - ${error}`); + } + + const data = await response.json(); + + if (!data.data || data.data.length === 0) { + throw new Error('No images generated'); + } + + const image = data.data[0]; + const [width, height] = size.split('x').map(Number); + + return { + url: image.url, + width: width || 1024, + height: height || 1024, + contentType: 'image/png', + revisedPrompt: image.revised_prompt + }; + } + + /** + * Get available sizes for current model + */ + getAvailableSizes() { + if (this.model === 'dall-e-3') { + return ['1024x1024', '1792x1024', '1024x1792']; + } + return ['256x256', '512x512', '1024x1024']; + } +} + +export default OpenAIProvider; diff --git a/src/providers/stability.js b/src/providers/stability.js new file mode 100644 index 0000000..d733433 --- /dev/null +++ b/src/providers/stability.js @@ -0,0 +1,90 @@ +/** + * Stability AI provider implementation + */ + +import { BaseProvider } from './base.js'; + +export class StabilityProvider extends BaseProvider { + constructor(config) { + super(config); + this.baseUrl = config.baseUrl || 'https://api.stability.ai/v1'; + this.model = config.model || 'sdxl-v1-0'; + } + + getName() { + return 'stability'; + } + + async generate(prompt, options = {}) { + const { + size = '1024x1024', + numImages = 1, + steps = 30, + cfgScale = 7 + } = options; + + const [width, height] = size.split('x').map(Number); + + const response = await fetch(`${this.baseUrl}/generation/${this.model}/text-to-image`, { + method: 'POST', + headers: { + 'Authorization': `${this.config.apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + text_prompts: [{ text: prompt }], + width, + height, + samples: Math.min(numImages, 4), + steps, + cfg_scale: cfgScale + }) + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Stability API error: ${response.status} - ${error}`); + } + + const data = await response.json(); + + if (!data.artifacts || data.artifacts.length === 0) { + throw new Error('No images generated'); + } + + // Stability returns base64 + const artifact = data.artifacts[0]; + + // Convert base64 to data URL + const base64 = artifact.base64; + const contentType = 'image/png'; + const dataUrl = `data:${contentType};base64,${base64}`; + + return { + url: dataUrl, + width, + height, + contentType, + base64: base64 // Include raw base64 for flexibility + }; + } + + /** + * Get available engines/models + */ + async listEngines() { + const response = await fetch(`${this.baseUrl}/engines`, { + headers: { + 'Authorization': `${this.config.apiKey}` + } + }); + + if (!response.ok) { + throw new Error('Failed to list engines'); + } + + return await response.json(); + } +} + +export default StabilityProvider; diff --git a/src/utils/cache.js b/src/utils/cache.js new file mode 100644 index 0000000..7e685b6 --- /dev/null +++ b/src/utils/cache.js @@ -0,0 +1,115 @@ +/** + * Cache manager for generated images + */ + +import { createHash } from 'crypto'; +import { mkdir, readFile, writeFile, unlink, readdir, stat } from 'fs/promises'; +import { join } from 'path'; +import { homedir } from 'os'; + +export class CacheManager { + constructor(options = {}) { + this.ttl = options.ttl || 3600; // Default 1 hour + this.cacheDir = options.cacheDir || join(homedir(), '.clawra', 'cache'); + this.ensureDirectory(); + } + + async ensureDirectory() { + try { + await mkdir(this.cacheDir, { recursive: true }); + } catch (error) { + console.error('Failed to create cache directory:', error); + } + } + + /** + * Generate cache key from params + */ + generateKey(params) { + const str = JSON.stringify(params); + return createHash('md5').update(str).digest('hex'); + } + + /** + * Get cached data + */ + async get(key) { + try { + const cachePath = join(this.cacheDir, `${key}.json`); + const data = await readFile(cachePath, 'utf-8'); + const cached = JSON.parse(data); + + // Check TTL + if (Date.now() - cached.timestamp > this.ttl * 1000) { + await unlink(cachePath); + return null; + } + + return cached.data; + } catch { + return null; + } + } + + /** + * Set cached data + */ + async set(key, data) { + try { + const cachePath = join(this.cacheDir, `${key}.json`); + const cached = { + timestamp: Date.now(), + data + }; + await writeFile(cachePath, JSON.stringify(cached), 'utf-8'); + } catch (error) { + console.error('Failed to write cache:', error); + } + } + + /** + * Clear all cached data + */ + async clear() { + try { + const files = await readdir(this.cacheDir); + for (const file of files) { + if (file.endsWith('.json')) { + await unlink(join(this.cacheDir, file)); + } + } + return true; + } catch { + return false; + } + } + + /** + * Get cache statistics + */ + async getStats() { + try { + const files = await readdir(this.cacheDir); + let totalSize = 0; + let count = 0; + + for (const file of files) { + if (file.endsWith('.json')) { + const stats = await stat(join(this.cacheDir, file)); + totalSize += stats.size; + count++; + } + } + + return { + count, + totalSize, + cacheDir: this.cacheDir + }; + } catch { + return { count: 0, totalSize: 0, cacheDir: this.cacheDir }; + } + } +} + +export default CacheManager; diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..798b9cd --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,41 @@ +/** + * Simple logger utility + */ + +export class Logger { + constructor(options = {}) { + this.debug = options.debug || false; + this.prefix = options.prefix || '[Clawra]'; + } + + log(level, message, ...args) { + const timestamp = new Date().toISOString(); + const output = `${timestamp} ${this.prefix} [${level.toUpperCase()}] ${message}`; + + if (args.length > 0) { + console.log(output, ...args); + } else { + console.log(output); + } + } + + info(message, ...args) { + this.log('info', message, ...args); + } + + warn(message, ...args) { + this.log('warn', message, ...args); + } + + error(message, ...args) { + this.log('error', message, ...args); + } + + debug(message, ...args) { + if (this.debug) { + this.log('debug', message, ...args); + } + } +} + +export default Logger;