Skill: mattstvartak/openclaw-hedra (version 1.0.1)

# Hedra Plugin for OpenClaw AI video and image generation plugin for OpenClaw powered by Hedra. Generate talking avatars, videos from text prompts, and images — all from your agent. ## Features - **Image generation** — create images from text prompts with configurable aspect ratios and resolutions - **Video generation** — text-to-video and image-to-video with optional start/end keyframes - **Talking avatars** — lip-synced avatar videos from a portrait image + audio or text-to-speech - **Voice listing** — browse available TTS voices for avatar generation - **Model listing** — discover available Hedra models and their capabilities - **Credit tracking** — check your Hedra credit balance ## Requirements - [Hedra](https://www.hedra.com) Creator plan or above - Hedra API key from https://www.hedra.com/api-profile ## Configuration Set your API key in `openclaw.json`: ```json { "plugins": { "entries": { "hedra": { "enabled": true, "config": { "apiKey": "sk_h_your-key-here" } } } } } ``` Or use the `HEDRA_API_KEY` environment variable. | Setting | Default | Description | |---------|---------|-------------| | `apiKey` | | Hedra API key (falls back to HEDRA_API_KEY env var) | | `defaultVideoModel` | | Default model for video generation | | `defaultImageModel` | | Default model for image generation | ## Registered Tools | Tool | Description | |------|-------------| | `hedra_generate_image` | Generate images from text prompts | | `hedra_generate_video` | Generate videos from text prompts, optionally with a start image | | `hedra_generate_avatar` | Generate talking avatar videos from a portrait + audio/TTS | | `hedra_generation_status` | Check generation status and get result URLs | | `hedra_list_models` | List available Hedra AI models | | `hedra_list_voices` | List available TTS voices for avatars | | `hedra_credits` | Check credit balance | ## Usage Examples ### Generate an image > "Generate a photo-realistic image of a mountain lake at sunset" The agent calls `hedra_list_models` to choose the best image model, then `hedra_generate_image` with the prompt and returns the result URL. ### Generate a video > "Create a 5 second video of ocean waves crashing on a beach" The agent calls `hedra_list_models` to choose the best video model, then `hedra_generate_video` with the prompt and duration. ### Create a talking avatar > "Make this portrait say 'Hello, welcome to our product demo'" The agent calls `hedra_list_voices` to pick a voice, then `hedra_generate_avatar` with the portrait asset ID, TTS text, and voice ID.

path: dist/api.d.ts
content: /**
 * Hedra API client — thin wrapper over the v2 public REST API.
 * Base URL: https://api.hedra.com/web-app/public
 * Auth: X-API-Key header
 */
export interface HedraConfig {
    [REDACTED_SECRET];
    defaultVideoModel?: string;
    defaultImageModel?: string;
}
export interface AIModel {
    id: string;
    name: string;
    description: string;
    type: string;
    aspect_ratios?: string[];
    resolutions?: string[];
    durations?: number[];
    max_duration_ms?: number;
    price_details?: {
        credit_cost: number;
        unit_scale: number;
        billing_unit: string;
    };
}
export interface Voice {
    id: string;
    name: string;
    [key: string]: any;
}
export interface Asset {
    id: string;
    name: string;
    type: string;
    url?: string;
}
export interface GenerationResponse {
    id: string;
    asset_id: string;
    status: 'pending' | 'processing' | 'completed' | 'failed';
    progress?: number;
    eta_sec?: number;
    batch_generation_id?: string;
}
export interface GenerationStatus {
    status: 'pending' | 'processing' | 'completed' | 'failed';
    url?: string;
    download_url?: string;
    error_message?: string;
    progress?: number;
    eta_sec?: number;
}
export interface CreditBalance {
    remaining: number;
    expiring: number;
    used: number;
}
export declare function listModels(config: HedraConfig, types?: string[]): Promise<AIModel[]>;
export declare function listVoices(config: HedraConfig): Promise<Voice[]>;
export declare function getCredits(config: HedraConfig): Promise<CreditBalance>;
export declare function createAsset(config: HedraConfig, name: string, type: string): Promise<Asset>;
export declare function uploadAssetFile(config: HedraConfig, assetId: string, fileBuffer: Buffer, filename: string): Promise<any>;
export declare function generateImage(config: HedraConfig, opts: {
    prompt: string;
    aspectRatio?: string;
    resolution?: string;
    modelId?: string;
    batchSize?: number;
    enhancePrompt?: boolean;
}): Promise<GenerationResponse>;
export declare function generateVideo(config: HedraConfig, opts: {
    prompt: string;
    modelId?: string;
    startKeyframeId?: string;
    startKeyframeUrl?: string;
    endKeyframeId?: string;
    audioId?: string;
    aspectRatio?: string;
    resolution?: string;
    durationMs?: number;
    batchSize?: number;
    enhancePrompt?: boolean;
    seed?: number;
}): Promise<GenerationResponse>;
export declare function generateAvatar(config: HedraConfig, opts: {
    imageAssetId: string;
    audioAssetId?: string;
    ttsText?: string;
    ttsVoiceId?: string;
    modelId?: string;
    aspectRatio?: string;
    resolution?: string;
    durationMs?: number;
}): Promise<GenerationResponse>;
export declare function getGenerationStatus(config: HedraConfig, generationId: string): Promise<GenerationStatus>;
export declare function pollUntilDone(config: HedraConfig, generationId: string, timeoutMs?: number, intervalMs?: number): Promise<GenerationStatus>;


path: dist/api.js
content: /**
 * Hedra API client — thin wrapper over the v2 public REST API.
 * Base URL: https://api.hedra.com/web-app/public
 * Auth: X-API-Key header
 */
const BASE = 'https://api.hedra.com/web-app/public';
// ── Helpers ──────────────────────────────────────────────────────────
async function request(config, method, path, body) {
    const url = `${BASE}${path}`;
    const headers = {
        'X-API-Key': config.apiKey,
    };
    if (body && !(body instanceof FormData)) {
        headers['Content-Type'] = 'application/json';
    }
    const res = await fetch(url, {
        method,
        headers,
        body: body instanceof FormData ? body : body ? JSON.stringify(body) : undefined,
    });
    if (!res.ok) {
        const text = await res.text();
        throw new Error(`Hedra API ${res.status}: ${text}`);
    }
    return res.json();
}
// ── API Functions ────────────────────────────────────────────────────
export async function listModels(config, types) {
    const params = types?.length ? `?types=${types.join(',')}` : '';
    return request(config, 'GET', `/models${params}`);
}
export async function listVoices(config) {
    return request(config, 'GET', '/voices');
}
export async function getCredits(config) {
    return request(config, 'GET', '/billing/credits');
}
export async function createAsset(config, name, type) {
    return request(config, 'POST', '/assets', { name, type });
}
export async function uploadAssetFile(config, assetId, fileBuffer, filename) {
    const form = new FormData();
    form.append('file', new Blob([new Uint8Array(fileBuffer)]), filename);
    const res = await fetch(`${BASE}/assets/${assetId}/upload`, {
        method: 'POST',
        headers: { 'X-API-Key': config.apiKey },
        body: form,
    });
    if (!res.ok) {
        const text = await res.text();
        throw new Error(`Hedra upload ${res.status}: ${text}`);
    }
    return res.json();
}
export async function generateImage(config, opts) {
    return request(config, 'POST', '/generations', {
        type: 'image',
        text_prompt: opts.prompt,
        aspect_ratio: opts.aspectRatio ?? '1:1',
        resolution: opts.resolution,
        ai_model_id: opts.modelId || config.defaultImageModel,
        batch_size: opts.batchSize ?? 1,
        enhance_prompt: opts.enhancePrompt ?? true,
    });
}
export async function generateVideo(config, opts) {
    const body = {
        type: 'video',
        text_prompt: opts.prompt,
        ai_model_id: opts.modelId || config.defaultVideoModel,
        aspect_ratio: opts.aspectRatio ?? '16:9',
        resolution: opts.resolution,
        duration_ms: opts.durationMs,
        batch_size: opts.batchSize ?? 1,
        enhance_prompt: opts.enhancePrompt ?? true,
        seed: opts.seed,
    };
    if (opts.startKeyframeId)
        body.start_keyframe_id = opts.startKeyframeId;
    if (opts.startKeyframeUrl)
        body.start_keyframe_url = opts.startKeyframeUrl;
    if (opts.endKeyframeId)
        body.end_keyframe_id = opts.endKeyframeId;
    if (opts.audioId)
        body.audio_id = opts.audioId;
    return request(config, 'POST', '/generations', body);
}
export async function generateAvatar(config, opts) {
    const body = {
        type: 'video',
        ai_model_id: opts.modelId || config.defaultVideoModel,
        start_keyframe_id: opts.imageAssetId,
        aspect_ratio: opts.aspectRatio ?? '1:1',
        resolution: opts.resolution,
        duration_ms: opts.durationMs,
        text_prompt: '',
    };
    if (opts.audioAssetId) {
        body.audio_id = opts.audioAssetId;
    }
    else if (opts.ttsText && opts.ttsVoiceId) {
        body.audio_generation = {
            type: 'text_to_speech',
            voice_id: opts.ttsVoiceId,
            text: opts.ttsText,
        };
    }
    return request(config, 'POST', '/generations', body);
}
export async function getGenerationStatus(config, generationId) {
    return request(config, 'GET', `/generations/${generationId}/status`);
}
export async function pollUntilDone(config, generationId, timeoutMs = 300000, intervalMs = 5000) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        const status = await getGenerationStatus(config, generationId);
        if (status.status === 'completed' || status.status === 'failed')
            return status;
        await new Promise(r => setTimeout(r, intervalMs));
    }
    throw new Error(`Generation ${generationId} timed out after ${timeoutMs / 1000}s`);
}
//# sourceMappingURL=api.js.map

path: dist/index.d.ts
content: export { default } from './plugin.js';


path: dist/index.js
content: export { default } from './plugin.js';
//# sourceMappingURL=index.js.map

path: dist/plugin.d.ts
content: declare const plugin: {
    id: string;
    name: string;
    description: string;
    register(api: any): void;
};
export default plugin;


path: dist/plugin.js
content: import { Type } from '@sinclair/typebox';
import { listModels, listVoices, getCredits, generateImage, generateVideo, generateAvatar, getGenerationStatus, } from './api.js';
// ── Result helpers ───────────────────────────────────────────────────
function textResult(text) {
    return { content: [{ type: 'text', text }] };
}
function jsonResult(data) {
    return textResult(JSON.stringify(data, null, 2));
}
// ── Plugin ───────────────────────────────────────────────────────────
const plugin = {
    id: 'hedra',
    name: 'Hedra',
    description: 'AI video and image generation via Hedra — talking avatars, video generation, and image creation.',
    register(api) {
        const pluginConfig = api.pluginConfig ?? {};
        const config = {
            [REDACTED_SECRET] || process.env.HEDRA_API_KEY || '',
            defaultVideoModel: pluginConfig.defaultVideoModel || undefined,
            defaultImageModel: pluginConfig.defaultImageModel || undefined,
        };
        function requireKey() {
            if (!config.apiKey) {
                throw new Error('Hedra API key not configured. Set it in plugin config (apiKey) or HEDRA_API_KEY env var. Get your key at https://www.hedra.com/api-profile');
            }
        }
        // ── hedra_generate_image ─────────────────────────────────────
        api.registerTool({
            name: 'hedra_generate_image',
            label: 'Hedra Image',
            description: 'Generate an image from a text prompt using Hedra AI. Returns the generation ID immediately. Use hedra_generation_status to poll for the result URL. IMPORTANT: You must call hedra_list_models first to see available image models, then choose the best model for the user\'s request and pass its ID as model_id.',
            parameters: Type.Object({
                prompt: Type.String({ description: 'Text prompt describing the image to generate.' }),
                aspect_ratio: Type.Optional(Type.String({ description: 'Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4 (default: 1:1).' })),
                resolution: Type.Optional(Type.String({ description: 'Resolution (e.g., 1024x1024). Leave empty for model default.' })),
                model_id: Type.String({ description: 'Required. The model ID to use. Call hedra_list_models with types=image first to see available models, then choose the best one for the task.' }),
                batch_size: Type.Optional(Type.Number({ description: 'Number of images to generate (1-8, default: 1).', minimum: 1, maximum: 8 })),
                enhance_prompt: Type.Optional(Type.Boolean({ description: 'Let Hedra enhance the prompt (default: true).' })),
            }),
            async execute(_id, params) {
                requireKey();
                const gen = await generateImage(config, {
                    prompt: params.prompt,
                    aspectRatio: params.aspect_ratio,
                    resolution: params.resolution,
                    modelId: params.model_id,
                    batchSize: params.batch_size,
                    enhancePrompt: params.enhance_prompt,
                });
                return jsonResult({
                    generation_id: gen.id,
                    asset_id: gen.asset_id,
                    status: gen.status,
                    message: 'Image generation started. Use hedra_generation_status to check progress and get the result URL.',
                });
            },
        }, { name: 'hedra_generate_image' });
        // ── hedra_generate_video ─────────────────────────────────────
        api.registerTool({
            name: 'hedra_generate_video',
            label: 'Hedra Video',
            description: 'Generate a video from a text prompt using Hedra AI. Optionally provide a start image URL for image-to-video. Returns the generation ID immediately. Use hedra_generation_status to poll for the result URL. IMPORTANT: You must call hedra_list_models first to see available video models, then choose the best model for the user\'s request and pass its ID as model_id.',
            parameters: Type.Object({
                prompt: Type.String({ description: 'Text prompt describing the video to generate.' }),
                start_image_url: Type.Optional(Type.String({ description: 'URL of a start image (first frame). For image-to-video generation.' })),
                aspect_ratio: Type.Optional(Type.String({ description: 'Aspect ratio: 16:9, 9:16, 1:1, 4:3, 3:4 (default: 16:9).' })),
                resolution: Type.Optional(Type.String({ description: 'Resolution override.' })),
                duration_ms: Type.Optional(Type.Number({ description: 'Video duration in milliseconds.' })),
                model_id: Type.String({ description: 'Required. The model ID to use. Call hedra_list_models with types=video first to see available models, then choose the best one for the task.' }),
                seed: Type.Optional(Type.Number({ description: 'Seed for reproducibility.' })),
                enhance_prompt: Type.Optional(Type.Boolean({ description: 'Let Hedra enhance the prompt (default: true).' })),
            }),
            async execute(_id, params) {
                requireKey();
                const gen = await generateVideo(config, {
                    prompt: params.prompt,
                    startKeyframeUrl: params.start_image_url,
                    aspectRatio: params.aspect_ratio,
                    resolution: params.resolution,
                    durationMs: params.duration_ms,
                    modelId: params.model_id,
                    seed: params.seed,
                    enhancePrompt: params.enhance_prompt,
                });
                return jsonResult({
                    generation_id: gen.id,
                    asset_id: gen.asset_id,
                    status: gen.status,
                    message: 'Video generation started. Use hedra_generation_status to check progress and get the result URL.',
                });
            },
        }, { name: 'hedra_generate_video' });
        // ── hedra_generate_avatar ────────────────────────────────────
        api.registerTool({
            name: 'hedra_generate_avatar',
            label: 'Hedra Avatar',
            description: 'Generate a talking avatar video from a portrait image and either uploaded audio or text-to-speech. The avatar will lip-sync to the audio. Provide either audio_asset_id OR both tts_text and tts_voice_id. Returns the generation ID immediately. Use hedra_generation_status to poll for the result URL.',
            parameters: Type.Object({
                image_asset_id: Type.String({ description: 'Asset ID of the portrait image.' }),
                audio_asset_id: Type.Optional(Type.String({ description: 'Asset ID of uploaded audio. Mutually exclusive with TTS params.' })),
                tts_text: Type.Optional(Type.String({ description: 'Text for text-to-speech. Use with tts_voice_id.' })),
                tts_voice_id: Type.Optional(Type.String({ description: 'Voice ID for TTS. Use hedra_list_voices to see options.' })),
                aspect_ratio: Type.Optional(Type.String({ description: 'Aspect ratio (default: 1:1).' })),
                resolution: Type.Optional(Type.String({ description: 'Resolution override.' })),
                duration_ms: Type.Optional(Type.Number({ description: 'Max duration in milliseconds.' })),
                model_id: Type.String({ description: 'Required. The model ID to use. Call hedra_list_models with types=video first to see available models, then choose the best one for the task.' }),
            }),
            async execute(_id, params) {
                requireKey();
                if (!params.audio_asset_id && !(params.tts_text && params.tts_voice_id)) {
                    return textResult('Provide either audio_asset_id OR both tts_text and tts_voice_id.');
                }
                const gen = await generateAvatar(config, {
                    imageAssetId: params.image_asset_id,
                    audioAssetId: params.audio_asset_id,
                    ttsText: params.tts_text,
                    ttsVoiceId: params.tts_voice_id,
                    aspectRatio: params.aspect_ratio,
                    resolution: params.resolution,
                    durationMs: params.duration_ms,
                    modelId: params.model_id,
                });
                return jsonResult({
                    generation_id: gen.id,
                    asset_id: gen.asset_id,
                    status: gen.status,
                    message: 'Avatar generation started. Use hedra_generation_status to check progress and get the result URL.',
                });
            },
        }, { name: 'hedra_generate_avatar' });
        // ── hedra_generation_status ──────────────────────────────────
        api.registerTool({
            name: 'hedra_generation_status',
            label: 'Hedra Status',
            description: 'Check the status of a Hedra generation by ID. Returns status, progress, and URLs when complete. Call this repeatedly until status is "completed" or "failed".',
            parameters: Type.Object({
                generation_id: Type.String({ description: 'The generation ID to check.' }),
            }),
            async execute(_id, params) {
                requireKey();
                const status = await getGenerationStatus(config, params.generation_id);
                return jsonResult(status);
            },
        }, { name: 'hedra_generation_status' });
        // ── hedra_list_models ────────────────────────────────────────
        api.registerTool({
            name: 'hedra_list_models',
            label: 'Hedra Models',
            description: 'List available Hedra AI models. Optionally filter by type (image, video, etc.).',
            parameters: Type.Object({
                types: Type.Optional(Type.String({ description: 'Comma-separated model types to filter (e.g., image,video).' })),
            }),
            async execute(_id, params) {
                requireKey();
                const types = params.types?.split(',').map((t) => t.trim()) ?? undefined;
                const models = await listModels(config, types);
                return jsonResult(models.map((m) => ({
                    id: m.id,
                    name: m.name,
                    type: m.type,
                    description: m.description,
                    aspect_ratios: m.aspect_ratios,
                    resolutions: m.resolutions,
                    price: m.price_details,
                })));
            },
        }, { name: 'hedra_list_models' });
        // ── hedra_list_voices ────────────────────────────────────────
        api.registerTool({
            name: 'hedra_list_voices',
            label: 'Hedra Voices',
            description: 'List available voices for text-to-speech in avatar generation.',
            parameters: Type.Object({}),
            async execute() {
                requireKey();
                const voices = await listVoices(config);
                return jsonResult(voices);
            },
        }, { name: 'hedra_list_voices' });
        // ── hedra_credits ────────────────────────────────────────────
        api.registerTool({
            name: 'hedra_credits',
            label: 'Hedra Credits',
            description: 'Check your Hedra credit balance.',
            parameters: Type.Object({}),
            async execute() {
                requireKey();
                const credits = await getCredits(config);
                return jsonResult(credits);
            },
        }, { name: 'hedra_credits' });
        api.logger?.info('Hedra plugin registered');
    },
};
export default plugin;
//# sourceMappingURL=plugin.js.map

path: openclaw.plugin.json
content: {
  "id": "hedra",
  "name": "Hedra",
  "description": "AI video and image generation via Hedra — talking avatars, video generation, and image creation.",
  "providerAuthEnvVars": {
    "hedra": ["HEDRA_API_KEY"]
  },
  "providerAuthChoices": [
    {
      "provider": "hedra",
      "method": "api-key",
      "choiceId": "hedra-key",
      "choiceLabel": "Hedra API Key",
      "choiceHint": "Get your key at https://www.hedra.com/api-profile (Creator plan required)",
      "cliOption": "--hedra-api-key <key>",
      "groupId": "hedra",
      "groupLabel": "Hedra"
    }
  ],
  "contracts": {
    "tools": [
      "hedra_generate_image",
      "hedra_generate_video",
      "hedra_generate_avatar",
      "hedra_generation_status",
      "hedra_list_models",
      "hedra_list_voices",
      "hedra_credits"
    ]
  },
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "apiKey": {
        "type": "string",
        "default": "",
        "description": "Hedra API key. Falls back to HEDRA_API_KEY env var."
      },
      "defaultVideoModel": {
        "type": "string",
        "default": "",
        "description": "Default video generation model ID. Leave empty to use Hedra's default."
      },
      "defaultImageModel": {
        "type": "string",
        "default": "",
        "description": "Default image generation model ID. Leave empty to use Hedra's default."
      }
    }
  },
  "uiHints": {
    "apiKey": {
      "label": "API Key",
      "help": "Hedra API key from https://www.hedra.com/api-profile",
      "sensitive": true,
      "placeholder": "sk_h..."
    },
    "defaultVideoModel": {
      "label": "Default Video Model",
      "help": "Override the default model for video generation.",
      "advanced": true
    },
    "defaultImageModel": {
      "label": "Default Image Model",
      "help": "Override the default model for image generation.",
      "advanced": true
    }
  }
}


path: package-lock.json
content: {
  "name": "@onenomad/openclaw-hedra",
  "version": "1.0.1",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "@onenomad/openclaw-hedra",
      "version": "1.0.1",
      "dependencies": {
        "@sinclair/typebox": "^0.34.49"
      },
      "devDependencies": {
        "@types/node": "^22.0.0",
        "typescript": "^5.7.0"
      }
    },
    "node_modules/@sinclair/typebox": {
      "version": "0.34.49",
      "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/@types/node": {
      "version": "22.19.17",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "undici-types": "~6.21.0"
      }
    },
    "node_modules/typescript": {
      "version": "5.9.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "bin": {
        "tsc": "bin/tsc",
        "tsserver": "bin/tsserver"
      },
      "engines": {
        "node": ">=14.17"
      }
    },
    "node_modules/undici-types": {
      "version": "6.21.0",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    }
  }
}


path: package.json
content: {
  "name": "@onenomad/openclaw-hedra",
  "version": "1.0.1",
  "description": "Hedra AI video and image generation plugin for OpenClaw — talking avatars, video generation, and image creation.",
  "type": "module",
  "main": "dist/plugin.js",
  "openclaw": {
    "extensions": [
      "./dist/plugin.js"
    ],
    "compat": {
      "pluginApi": ">=2026.3.0",
      "gatewayApi": ">=2026.3.0"
    },
    "build": {
      "openclawVersion": "2026.3.24",
      "sdkVersion": "2026.3.24"
    }
  },
  "scripts": {
    "build": "rm -rf dist && tsc",
    "dev": "tsc --watch"
  },
  "dependencies": {
    "@sinclair/typebox": "^0.34.49"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "typescript": "^5.7.0"
  }
}


path: README.md
content: # Hedra Plugin for OpenClaw

AI video and image generation plugin for [OpenClaw](https://openclaw.ai) powered by [Hedra](https://www.hedra.com). Generate talking avatars, videos from text prompts, and images — all from your agent.

Built by **OneNomad LLC**.

## Features

- **Image generation** — create images from text prompts with configurable aspect ratios and resolutions
- **Video generation** — text-to-video and image-to-video with optional start/end keyframes
- **Talking avatars** — lip-synced avatar videos from a portrait image + audio or text-to-speech
- **Voice listing** — browse available TTS voices for avatar generation
- **Model listing** — discover available Hedra models and their capabilities
- **Credit tracking** — check your Hedra credit balance

## Requirements

- [Hedra](https://www.hedra.com) Creator plan or above
- Hedra API key from https://www.hedra.com/api-profile

## Installation

### From ClawHub

```bash
openclaw plugins install clawhub:@onenomad/openclaw-hedra
```

### Manual install

```bash
git clone https://github.com/mattstvartak/openclaw-hedra.git
cd openclaw-hedra
npm install
npm run build
openclaw plugins install ./
```

## Configuration

Set your API key in `openclaw.json`:

```json
{
  "plugins": {
    "entries": {
      "hedra": {
        "enabled": true,
        "config": {
          "apiKey": "sk_h_your-key-here"
        }
      }
    }
  }
}
```

Or use the `HEDRA_API_KEY` environment variable.

| Setting | Default | Description |
|---------|---------|-------------|
| `apiKey` | | Hedra API key (falls back to HEDRA_API_KEY env var) |
| `defaultVideoModel` | | Default model for video generation |
| `defaultImageModel` | | Default model for image generation |

## Registered Tools

| Tool | Description |
|------|-------------|
| `hedra_generate_image` | Generate images from text prompts |
| `hedra_generate_video` | Generate videos from text prompts, optionally with a start image |
| `hedra_generate_avatar` | Generate talking avatar videos from a portrait + audio/TTS |
| `hedra_generation_status` | Check generation status and get result URLs |
| `hedra_list_models` | List available Hedra AI models |
| `hedra_list_voices` | List available TTS voices for avatars |
| `hedra_credits` | Check credit balance |

## Usage Examples

### Generate an image
> "Generate a photo-realistic image of a mountain lake at sunset"

The agent calls `hedra_generate_image` with the prompt and returns the result URL.

### Generate a video
> "Create a 5 second video of ocean waves crashing on a beach"

The agent calls `hedra_generate_video` with the prompt and duration.

### Create a talking avatar
> "Make this portrait say 'Hello, welcome to our product demo'"

The agent calls `hedra_list_voices` to pick a voice, then `hedra_generate_avatar` with the portrait asset ID, TTS text, and voice ID.

## Security

- API key can be set in plugin config or as an environment variable
- The plugin only contacts `https://api.hedra.com` — no other endpoints
- No telemetry or analytics

## License

MIT


path: src/api.ts
content: /**
 * Hedra API client — thin wrapper over the v2 public REST API.
 * Base URL: https://api.hedra.com/web-app/public
 * Auth: X-API-Key header
 */

const BASE = 'https://api.hedra.com/web-app/public';

export interface HedraConfig {
  [REDACTED_SECRET];
  defaultVideoModel?: string;
  defaultImageModel?: string;
}

// ── Types ────────────────────────────────────────────────────────────

export interface AIModel {
  id: string;
  name: string;
  description: string;
  type: string;
  aspect_ratios?: string[];
  resolutions?: string[];
  durations?: number[];
  max_duration_ms?: number;
  price_details?: { credit_cost: number; unit_scale: number; billing_unit: string };
}

export interface Voice {
  id: string;
  name: string;
  [key: string]: any;
}

export interface Asset {
  id: string;
  name: string;
  type: string;
  url?: string;
}

export interface GenerationResponse {
  id: string;
  asset_id: string;
  status: 'pending' | 'processing' | 'completed' | 'failed';
  progress?: number;
  eta_sec?: number;
  batch_generation_id?: string;
}

export interface GenerationStatus {
  status: 'pending' | 'processing' | 'completed' | 'failed';
  url?: string;
  download_url?: string;
  error_message?: string;
  progress?: number;
  eta_sec?: number;
}

export interface CreditBalance {
  remaining: number;
  expiring: number;
  used: number;
}

// ── Helpers ──────────────────────────────────────────────────────────

async function request(config: HedraConfig, method: string, path: string, body?: any): Promise<any> {
  const url = `${BASE}${path}`;
  const headers: Record<string, string> = {
    'X-API-Key': config.apiKey,
  };
  if (body && !(body instanceof FormData)) {
    headers['Content-Type'] = 'application/json';
  }

  const res = await fetch(url, {
    method,
    headers,
    body: body instanceof FormData ? body : body ? JSON.stringify(body) : undefined,
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Hedra API ${res.status}: ${text}`);
  }

  return res.json();
}

// ── API Functions ────────────────────────────────────────────────────

export async function listModels(config: HedraConfig, types?: string[]): Promise<AIModel[]> {
  const params = types?.length ? `?types=${types.join(',')}` : '';
  return request(config, 'GET', `/models${params}`);
}

export async function listVoices(config: HedraConfig): Promise<Voice[]> {
  return request(config, 'GET', '/voices');
}

export async function getCredits(config: HedraConfig): Promise<CreditBalance> {
  return request(config, 'GET', '/billing/credits');
}

export async function createAsset(config: HedraConfig, name: string, type: string): Promise<Asset> {
  return request(config, 'POST', '/assets', { name, type });
}

export async function uploadAssetFile(config: HedraConfig, assetId: string, fileBuffer: Buffer, filename: string): Promise<any> {
  const form = new FormData();
  form.append('file', new Blob([new Uint8Array(fileBuffer)]), filename);

  const res = await fetch(`${BASE}/assets/${assetId}/upload`, {
    method: 'POST',
    headers: { 'X-API-Key': config.apiKey },
    body: form,
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Hedra upload ${res.status}: ${text}`);
  }
  return res.json();
}

export async function generateImage(
  config: HedraConfig,
  opts: {
    prompt: string;
    aspectRatio?: string;
    resolution?: string;
    modelId?: string;
    batchSize?: number;
    enhancePrompt?: boolean;
  }
): Promise<GenerationResponse> {
  return request(config, 'POST', '/generations', {
    type: 'image',
    text_prompt: opts.prompt,
    aspect_ratio: opts.aspectRatio ?? '1:1',
    resolution: opts.resolution,
    ai_model_id: opts.modelId || config.defaultImageModel,
    batch_size: opts.batchSize ?? 1,
    enhance_prompt: opts.enhancePrompt ?? true,
  });
}

export async function generateVideo(
  config: HedraConfig,
  opts: {
    prompt: string;
    modelId?: string;
    startKeyframeId?: string;
    startKeyframeUrl?: string;
    endKeyframeId?: string;
    audioId?: string;
    aspectRatio?: string;
    resolution?: string;
    durationMs?: number;
    batchSize?: number;
    enhancePrompt?: boolean;
    seed?: number;
  }
): Promise<GenerationResponse> {
  const body: any = {
    type: 'video',
    text_prompt: opts.prompt,
    ai_model_id: opts.modelId || config.defaultVideoModel,
    aspect_ratio: opts.aspectRatio ?? '16:9',
    resolution: opts.resolution,
    duration_ms: opts.durationMs,
    batch_size: opts.batchSize ?? 1,
    enhance_prompt: opts.enhancePrompt ?? true,
    seed: opts.seed,
  };

  if (opts.startKeyframeId) body.start_keyframe_id = opts.startKeyframeId;
  if (opts.startKeyframeUrl) body.start_keyframe_url = opts.startKeyframeUrl;
  if (opts.endKeyframeId) body.end_keyframe_id = opts.endKeyframeId;
  if (opts.audioId) body.audio_id = opts.audioId;

  return request(config, 'POST', '/generations', body);
}

export async function generateAvatar(
  config: HedraConfig,
  opts: {
    imageAssetId: string;
    audioAssetId?: string;
    ttsText?: string;
    ttsVoiceId?: string;
    modelId?: string;
    aspectRatio?: string;
    resolution?: string;
    durationMs?: number;
  }
): Promise<GenerationResponse> {
  const body: any = {
    type: 'video',
    ai_model_id: opts.modelId || config.defaultVideoModel,
    start_keyframe_id: opts.imageAssetId,
    aspect_ratio: opts.aspectRatio ?? '1:1',
    resolution: opts.resolution,
    duration_ms: opts.durationMs,
    text_prompt: '',
  };

  if (opts.audioAssetId) {
    body.audio_id = opts.audioAssetId;
  } else if (opts.ttsText && opts.ttsVoiceId) {
    body.audio_generation = {
      type: 'text_to_speech',
      voice_id: opts.ttsVoiceId,
      text: opts.ttsText,
    };
  }

  return request(config, 'POST', '/generations', body);
}

export async function getGenerationStatus(config: HedraConfig, generationId: string): Promise<GenerationStatus> {
  return request(config, 'GET', `/generations/${generationId}/status`);
}

export async function pollUntilDone(
  config: HedraConfig,
  generationId: string,
  timeoutMs: number = 300000,
  intervalMs: number = 5000
): Promise<GenerationStatus> {
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    const status = await getGenerationStatus(config, generationId);
    if (status.status === 'completed' || status.status === 'failed') return status;
    await new Promise(r => setTimeout(r, intervalMs));
  }

  throw new Error(`Generation ${generationId} timed out after ${timeoutMs / 1000}s`);
}


path: src/index.ts
content: export { default } from './plugin.js';


path: src/plugin.ts
content: import { Type } from '@sinclair/typebox';
import {
  listModels,
  listVoices,
  getCredits,
  generateImage,
  generateVideo,
  generateAvatar,
  getGenerationStatus,
} from './api.js';
import type { HedraConfig } from './api.js';

// ── Result helpers ───────────────────────────────────────────────────

function textResult(text: string) {
  return { content: [{ type: 'text' as const, text }] };
}

function jsonResult(data: any) {
  return textResult(JSON.stringify(data, null, 2));
}

// ── Plugin ───────────────────────────────────────────────────────────

const plugin = {
  id: 'hedra',
  name: 'Hedra',
  description: 'AI video and image generation via Hedra — talking avatars, video generation, and image creation.',

  register(api: any) {
    const pluginConfig = api.pluginConfig ?? {};
    const config: HedraConfig = {
      [REDACTED_SECRET] || process.env.HEDRA_API_KEY || '',
      defaultVideoModel: pluginConfig.defaultVideoModel || undefined,
      defaultImageModel: pluginConfig.defaultImageModel || undefined,
    };

    function requireKey(): void {
      if (!config.apiKey) {
        throw new Error('Hedra API key not configured. Set it in plugin config (apiKey) or HEDRA_API_KEY env var. Get your key at https://www.hedra.com/api-profile');
      }
    }

    // ── hedra_generate_image ─────────────────────────────────────

    api.registerTool({
      name: 'hedra_generate_image',
      label: 'Hedra Image',
      description: 'Generate an image from a text prompt using Hedra AI. Returns the generation ID immediately. Use hedra_generation_status to poll for the result URL. IMPORTANT: You must call hedra_list_models first to see available image models, then choose the best model for the user\'s request and pass its ID as model_id.',
      parameters: Type.Object({
        prompt: Type.String({ description: 'Text prompt describing the image to generate.' }),
        aspect_ratio: Type.Optional(Type.String({ description: 'Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4 (default: 1:1).' })),
        resolution: Type.Optional(Type.String({ description: 'Resolution (e.g., 1024x1024). Leave empty for model default.' })),
        model_id: Type.String({ description: 'Required. The model ID to use. Call hedra_list_models with types=image first to see available models, then choose the best one for the task.' }),
        batch_size: Type.Optional(Type.Number({ description: 'Number of images to generate (1-8, default: 1).', minimum: 1, maximum: 8 })),
        enhance_prompt: Type.Optional(Type.Boolean({ description: 'Let Hedra enhance the prompt (default: true).' })),
      }),
      async execute(_id: string, params: any) {
        requireKey();
        const gen = await generateImage(config, {
          prompt: params.prompt,
          aspectRatio: params.aspect_ratio,
          resolution: params.resolution,
          modelId: params.model_id,
          batchSize: params.batch_size,
          enhancePrompt: params.enhance_prompt,
        });
        return jsonResult({
          generation_id: gen.id,
          asset_id: gen.asset_id,
          status: gen.status,
          message: 'Image generation started. Use hedra_generation_status to check progress and get the result URL.',
        });
      },
    }, { name: 'hedra_generate_image' });

    // ── hedra_generate_video ─────────────────────────────────────

    api.registerTool({
      name: 'hedra_generate_video',
      label: 'Hedra Video',
      description: 'Generate a video from a text prompt using Hedra AI. Optionally provide a start image URL for image-to-video. Returns the generation ID immediately. Use hedra_generation_status to poll for the result URL. IMPORTANT: You must call hedra_list_models first to see available video models, then choose the best model for the user\'s request and pass its ID as model_id.',
      parameters: Type.Object({
        prompt: Type.String({ description: 'Text prompt describing the video to generate.' }),
        start_image_url: Type.Optional(Type.String({ description: 'URL of a start image (first frame). For image-to-video generation.' })),
        aspect_ratio: Type.Optional(Type.String({ description: 'Aspect ratio: 16:9, 9:16, 1:1, 4:3, 3:4 (default: 16:9).' })),
        resolution: Type.Optional(Type.String({ description: 'Resolution override.' })),
        duration_ms: Type.Optional(Type.Number({ description: 'Video duration in milliseconds.' })),
        model_id: Type.String({ description: 'Required. The model ID to use. Call hedra_list_models with types=video first to see available models, then choose the best one for the task.' }),
        seed: Type.Optional(Type.Number({ description: 'Seed for reproducibility.' })),
        enhance_prompt: Type.Optional(Type.Boolean({ description: 'Let Hedra enhance the prompt (default: true).' })),
      }),
      async execute(_id: string, params: any) {
        requireKey();
        const gen = await generateVideo(config, {
          prompt: params.prompt,
          startKeyframeUrl: params.start_image_url,
          aspectRatio: params.aspect_ratio,
          resolution: params.resolution,
          durationMs: params.duration_ms,
          modelId: params.model_id,
          seed: params.seed,
          enhancePrompt: params.enhance_prompt,
        });
        return jsonResult({
          generation_id: gen.id,
          asset_id: gen.asset_id,
          status: gen.status,
          message: 'Video generation started. Use hedra_generation_status to check progress and get the result URL.',
        });
      },
    }, { name: 'hedra_generate_video' });

    // ── hedra_generate_avatar ────────────────────────────────────

    api.registerTool({
      name: 'hedra_generate_avatar',
      label: 'Hedra Avatar',
      description: 'Generate a talking avatar video from a portrait image and either uploaded audio or text-to-speech. The avatar will lip-sync to the audio. Provide either audio_asset_id OR both tts_text and tts_voice_id. Returns the generation ID immediately. Use hedra_generation_status to poll for the result URL.',
      parameters: Type.Object({
        image_asset_id: Type.String({ description: 'Asset ID of the portrait image.' }),
        audio_asset_id: Type.Optional(Type.String({ description: 'Asset ID of uploaded audio. Mutually exclusive with TTS params.' })),
        tts_text: Type.Optional(Type.String({ description: 'Text for text-to-speech. Use with tts_voice_id.' })),
        tts_voice_id: Type.Optional(Type.String({ description: 'Voice ID for TTS. Use hedra_list_voices to see options.' })),
        aspect_ratio: Type.Optional(Type.String({ description: 'Aspect ratio (default: 1:1).' })),
        resolution: Type.Optional(Type.String({ description: 'Resolution override.' })),
        duration_ms: Type.Optional(Type.Number({ description: 'Max duration in milliseconds.' })),
        model_id: Type.String({ description: 'Required. The model ID to use. Call hedra_list_models with types=video first to see available models, then choose the best one for the task.' }),
      }),
      async execute(_id: string, params: any) {
        requireKey();
        if (!params.audio_asset_id && !(params.tts_text && params.tts_voice_id)) {
          return textResult('Provide either audio_asset_id OR both tts_text and tts_voice_id.');
        }
        const gen = await generateAvatar(config, {
          imageAssetId: params.image_asset_id,
          audioAssetId: params.audio_asset_id,
          ttsText: params.tts_text,
          ttsVoiceId: params.tts_voice_id,
          aspectRatio: params.aspect_ratio,
          resolution: params.resolution,
          durationMs: params.duration_ms,
          modelId: params.model_id,
        });
        return jsonResult({
          generation_id: gen.id,
          asset_id: gen.asset_id,
          status: gen.status,
          message: 'Avatar generation started. Use hedra_generation_status to check progress and get the result URL.',
        });
      },
    }, { name: 'hedra_generate_avatar' });

    // ── hedra_generation_status ──────────────────────────────────

    api.registerTool({
      name: 'hedra_generation_status',
      label: 'Hedra Status',
      description: 'Check the status of a Hedra generation by ID. Returns status, progress, and URLs when complete. Call this repeatedly until status is "completed" or "failed".',
      parameters: Type.Object({
        generation_id: Type.String({ description: 'The generation ID to check.' }),
      }),
      async execute(_id: string, params: any) {
        requireKey();
        const status = await getGenerationStatus(config, params.generation_id);
        return jsonResult(status);
      },
    }, { name: 'hedra_generation_status' });

    // ── hedra_list_models ────────────────────────────────────────

    api.registerTool({
      name: 'hedra_list_models',
      label: 'Hedra Models',
      description: 'List available Hedra AI models. Optionally filter by type (image, video, etc.).',
      parameters: Type.Object({
        types: Type.Optional(Type.String({ description: 'Comma-separated model types to filter (e.g., image,video).' })),
      }),
      async execute(_id: string, params: any) {
        requireKey();
        const types = params.types?.split(',').map((t: string) => t.trim()) ?? undefined;
        const models = await listModels(config, types);
        return jsonResult(models.map((m: any) => ({
          id: m.id,
          name: m.name,
          type: m.type,
          description: m.description,
          aspect_ratios: m.aspect_ratios,
          resolutions: m.resolutions,
          price: m.price_details,
        })));
      },
    }, { name: 'hedra_list_models' });

    // ── hedra_list_voices ────────────────────────────────────────

    api.registerTool({
      name: 'hedra_list_voices',
      label: 'Hedra Voices',
      description: 'List available voices for text-to-speech in avatar generation.',
      parameters: Type.Object({}),
      async execute() {
        requireKey();
        const voices = await listVoices(config);
        return jsonResult(voices);
      },
    }, { name: 'hedra_list_voices' });

    // ── hedra_credits ────────────────────────────────────────────

    api.registerTool({
      name: 'hedra_credits',
      label: 'Hedra Credits',
      description: 'Check your Hedra credit balance.',
      parameters: Type.Object({}),
      async execute() {
        requireKey();
        const credits = await getCredits(config);
        return jsonResult(credits);
      },
    }, { name: 'hedra_credits' });

    api.logger?.info('Hedra plugin registered');
  },
};

export default plugin;


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}


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